UPDATE: Dump of initial files
This commit is contained in:
1
base/.gitignore
vendored
Normal file
1
base/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/*
|
||||
521
base/Cargo.lock
generated
Normal file
521
base/Cargo.lock
generated
Normal file
@@ -0,0 +1,521 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android-tzdata"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.83"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"wasm-bindgen",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbc529705a6e0028189c83f0a5dd9fb214105116f7e3c0eeab7ff0369766b0d1"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build",
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9998fb9f7e9b2111641485bf8beb32f92945f97f92a3d061f744cfef335f751"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf",
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8326b86b6cff230b97d0d312a6c40a60726df3332e721f72a1b035f451663b20"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ironcalc_base"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"js-sys",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"regex",
|
||||
"ryu",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.192"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bca2a08484b285dcb282d0f67b26cadc0df8b19f8c12502c13d966bf9482f001"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.192"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6c7207fbec9faa48073f3e3074cbe553af6ea512d7c21ba46e434e70ea9fbc1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.108"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1c7e3eac408d115102c4c24ad393e0821bb3a5df4d506a80f85f7a742a526b"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_repr"
|
||||
version = "0.1.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3081f5ffbb02284dda55132aa26daecedd7372a42417bbbab6f14ab7d6bb9145"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.39"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.88"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.51.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1f8cf84f35d2db49a46868f947758c7a1138116f7fac3bc844f43ade1292e64"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
25
base/Cargo.toml
Normal file
25
base/Cargo.toml
Normal file
@@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "ironcalc_base"
|
||||
version = "0.1.0"
|
||||
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_repr = "0.1"
|
||||
ryu = "1.0"
|
||||
chrono = "0.4"
|
||||
chrono-tz = "0.7.0"
|
||||
regex = "1.0"
|
||||
once_cell = "1.16.0"
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
js-sys = { version = "0.3.60" }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rand = "0.8.4"
|
||||
|
||||
|
||||
27
base/README.md
Normal file
27
base/README.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# IronCalc Base
|
||||
|
||||
## About
|
||||
|
||||
IronCalc Base is the engine of the IronCalc ecosystem
|
||||
|
||||
|
||||
## Build
|
||||
To build the library
|
||||
|
||||
```bash
|
||||
$ cargo build --release
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
To run the tests:
|
||||
|
||||
```bash
|
||||
$ cargo tests
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
291
base/src/actions.rs
Normal file
291
base/src/actions.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::parser::stringify::DisplaceData;
|
||||
use crate::model::Model;
|
||||
|
||||
// NOTE: There is a difference with Excel behaviour when deleting cells/rows/columns
|
||||
// In Excel if the whole range is deleted then it will substitute for #REF!
|
||||
// In IronCalc, if one of the edges of the range is deleted will replace the edge with #REF!
|
||||
// I feel this is unimportant for now.
|
||||
|
||||
impl Model {
|
||||
fn displace_cells(&mut self, displace_data: &DisplaceData) {
|
||||
let cells = self.get_all_cells();
|
||||
for cell in cells {
|
||||
self.shift_cell_formula(cell.index, cell.row, cell.column, displace_data);
|
||||
}
|
||||
}
|
||||
/// Returns the list of columns in row
|
||||
fn get_columns_for_row(
|
||||
&self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
descending: bool,
|
||||
) -> Result<Vec<i32>, String> {
|
||||
let worksheet = self.workbook.worksheet(sheet)?;
|
||||
if let Some(row_data) = worksheet.sheet_data.get(&row) {
|
||||
let mut columns: Vec<i32> = row_data.keys().copied().collect();
|
||||
columns.sort_unstable();
|
||||
if descending {
|
||||
columns.reverse();
|
||||
}
|
||||
Ok(columns)
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
|
||||
/// Moves the contents of cell (source_row, source_column) tp (target_row, target_column)
|
||||
fn move_cell(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
source_row: i32,
|
||||
source_column: i32,
|
||||
target_row: i32,
|
||||
target_column: i32,
|
||||
) -> Result<(), String> {
|
||||
let source_cell = self
|
||||
.workbook
|
||||
.worksheet(sheet)?
|
||||
.cell(source_row, source_column)
|
||||
.ok_or("Expected Cell to exist")?;
|
||||
let style = source_cell.get_style();
|
||||
// FIXME: we need some user_input getter instead of get_text
|
||||
let formula_or_value = self
|
||||
.cell_formula(sheet, source_row, source_column)?
|
||||
.unwrap_or_else(|| source_cell.get_text(&self.workbook.shared_strings, &self.language));
|
||||
self.set_user_input(sheet, target_row, target_column, formula_or_value);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(target_row, target_column, style);
|
||||
self.delete_cell(sheet, source_row, source_column)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_columns(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
column_count: i32,
|
||||
) -> Result<(), String> {
|
||||
if column_count <= 0 {
|
||||
return Err("Cannot add a negative number of cells :)".to_string());
|
||||
}
|
||||
// check if it is possible:
|
||||
let dimensions = self.workbook.worksheet(sheet)?.dimension();
|
||||
let last_column = dimensions.max_column + column_count;
|
||||
if last_column > LAST_COLUMN {
|
||||
return Err(
|
||||
"Cannot shift cells because that would delete cells at the end of a row"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
let worksheet = self.workbook.worksheet(sheet)?;
|
||||
let all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
||||
for row in all_rows {
|
||||
let sorted_columns = self.get_columns_for_row(sheet, row, true)?;
|
||||
for col in sorted_columns {
|
||||
if col >= column {
|
||||
self.move_cell(sheet, row, col, row, col + column_count)?;
|
||||
} else {
|
||||
// Break because columns are in descending order.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::Column {
|
||||
sheet,
|
||||
column,
|
||||
delta: column_count,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_columns(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
column_count: i32,
|
||||
) -> Result<(), String> {
|
||||
if column_count <= 0 {
|
||||
return Err("Please use insert columns instead".to_string());
|
||||
}
|
||||
|
||||
// Move cells
|
||||
let worksheet = &self.workbook.worksheet(sheet)?;
|
||||
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
||||
// We do not need to do that, but it is safer to eliminate sources of randomness in the algorithm
|
||||
all_rows.sort_unstable();
|
||||
|
||||
for r in all_rows {
|
||||
let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
|
||||
for col in columns {
|
||||
if col >= column {
|
||||
if col >= column + column_count {
|
||||
self.move_cell(sheet, r, col, r, col - column_count)?;
|
||||
} else {
|
||||
self.delete_cell(sheet, r, col)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update all formulas in the workbook
|
||||
|
||||
self.displace_cells(&DisplaceData::Column {
|
||||
sheet,
|
||||
column,
|
||||
delta: -column_count,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn insert_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
|
||||
if row_count <= 0 {
|
||||
return Err("Cannot add a negative number of cells :)".to_string());
|
||||
}
|
||||
// Check if it is possible:
|
||||
let dimensions = self.workbook.worksheet(sheet)?.dimension();
|
||||
let last_row = dimensions.max_row + row_count;
|
||||
if last_row > LAST_ROW {
|
||||
return Err(
|
||||
"Cannot shift cells because that would delete cells at the end of a column"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// Move cells
|
||||
let worksheet = &self.workbook.worksheet(sheet)?;
|
||||
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
||||
all_rows.sort_unstable();
|
||||
all_rows.reverse();
|
||||
for r in all_rows {
|
||||
if r >= row {
|
||||
// We do not really need the columns in any order
|
||||
let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
|
||||
for column in columns {
|
||||
self.move_cell(sheet, r, column, r + row_count, column)?;
|
||||
}
|
||||
} else {
|
||||
// Rows are in descending order
|
||||
break;
|
||||
}
|
||||
}
|
||||
// In the list of rows styles:
|
||||
// * Add all rows above the rows we are inserting unchanged
|
||||
// * Shift the ones below
|
||||
let rows = &self.workbook.worksheets[sheet as usize].rows;
|
||||
let mut new_rows = vec![];
|
||||
for r in rows {
|
||||
if r.r < row {
|
||||
new_rows.push(r.clone());
|
||||
} else if r.r >= row {
|
||||
let mut new_row = r.clone();
|
||||
new_row.r = r.r + row_count;
|
||||
new_rows.push(new_row);
|
||||
}
|
||||
}
|
||||
self.workbook.worksheets[sheet as usize].rows = new_rows;
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::Row {
|
||||
sheet,
|
||||
row,
|
||||
delta: row_count,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn delete_rows(&mut self, sheet: u32, row: i32, row_count: i32) -> Result<(), String> {
|
||||
if row_count <= 0 {
|
||||
return Err("Please use insert rows instead".to_string());
|
||||
}
|
||||
// Move cells
|
||||
let worksheet = &self.workbook.worksheet(sheet)?;
|
||||
let mut all_rows: Vec<i32> = worksheet.sheet_data.keys().copied().collect();
|
||||
all_rows.sort_unstable();
|
||||
|
||||
for r in all_rows {
|
||||
if r >= row {
|
||||
// We do not need ordered, but it is safer to eliminate sources of randomness in the algorithm
|
||||
let columns: Vec<i32> = self.get_columns_for_row(sheet, r, false)?;
|
||||
if r >= row + row_count {
|
||||
// displace all cells in column
|
||||
for column in columns {
|
||||
self.move_cell(sheet, r, column, r - row_count, column)?;
|
||||
}
|
||||
} else {
|
||||
// remove all cells in row
|
||||
// FIXME: We could just remove the entire row in one go
|
||||
for column in columns {
|
||||
self.delete_cell(sheet, r, column)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// In the list of rows styles:
|
||||
// * Add all rows above the rows we are deleting unchanged
|
||||
// * Skip all those we are deleting
|
||||
// * Shift the ones below
|
||||
let rows = &self.workbook.worksheets[sheet as usize].rows;
|
||||
let mut new_rows = vec![];
|
||||
for r in rows {
|
||||
if r.r < row {
|
||||
new_rows.push(r.clone());
|
||||
} else if r.r >= row + row_count {
|
||||
let mut new_row = r.clone();
|
||||
new_row.r = r.r - row_count;
|
||||
new_rows.push(new_row);
|
||||
}
|
||||
}
|
||||
self.workbook.worksheets[sheet as usize].rows = new_rows;
|
||||
self.displace_cells(&DisplaceData::Row {
|
||||
sheet,
|
||||
row,
|
||||
delta: -row_count,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Displaces cells due to a move column action
|
||||
/// from initial_column to target_column = initial_column + column_delta
|
||||
/// References will be updated following:
|
||||
/// Cell references:
|
||||
/// * All cell references to initial_column will go to target_column
|
||||
/// * All cell references to columns in between (initial_column, target_column] will be displaced one to the left
|
||||
/// * All other cell references are left unchanged
|
||||
/// Ranges. This is the tricky bit:
|
||||
/// * Column is one of the extremes of the range. The new extreme would be target_column.
|
||||
/// Range is then normalized
|
||||
/// * Any other case, range is left unchanged.
|
||||
/// NOTE: This does NOT move the data in the columns or move the colum styles
|
||||
pub fn move_column_action(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
) -> Result<(), &'static str> {
|
||||
// Check boundaries
|
||||
let target_column = column + delta;
|
||||
if !(1..=LAST_COLUMN).contains(&target_column) {
|
||||
return Err("Target column out of boundaries");
|
||||
}
|
||||
if !(1..=LAST_COLUMN).contains(&column) {
|
||||
return Err("Initial column out of boundaries");
|
||||
}
|
||||
|
||||
// TODO: Add the actual displacement of data and styles
|
||||
|
||||
// Update all formulas in the workbook
|
||||
self.displace_cells(&DisplaceData::ColumnMove {
|
||||
sheet,
|
||||
column,
|
||||
delta,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
114
base/src/calc_result.rs
Normal file
114
base/src/calc_result.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::expressions::token::Error;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
|
||||
pub struct CellReference {
|
||||
pub sheet: u32,
|
||||
pub column: i32,
|
||||
pub row: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Range {
|
||||
pub left: CellReference,
|
||||
pub right: CellReference,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum CalcResult {
|
||||
String(String),
|
||||
Number(f64),
|
||||
Boolean(bool),
|
||||
Error {
|
||||
error: Error,
|
||||
origin: CellReference,
|
||||
message: String,
|
||||
},
|
||||
Range {
|
||||
left: CellReference,
|
||||
right: CellReference,
|
||||
},
|
||||
EmptyCell,
|
||||
EmptyArg,
|
||||
}
|
||||
|
||||
impl CalcResult {
|
||||
pub fn new_error(error: Error, origin: CellReference, message: String) -> CalcResult {
|
||||
CalcResult::Error {
|
||||
error,
|
||||
origin,
|
||||
message,
|
||||
}
|
||||
}
|
||||
pub fn new_args_number_error(origin: CellReference) -> CalcResult {
|
||||
CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin,
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
}
|
||||
}
|
||||
pub fn is_error(&self) -> bool {
|
||||
matches!(self, CalcResult::Error { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for CalcResult {
|
||||
// ..., -2, -1, 0, 1, 2, ..., A-Z, FALSE, TRUE, empty;
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
match (self, other) {
|
||||
(CalcResult::Number(value1), CalcResult::Number(value2)) => {
|
||||
if (value2 - value1).abs() < f64::EPSILON {
|
||||
return Ordering::Equal;
|
||||
}
|
||||
if value1 < value2 {
|
||||
return Ordering::Less;
|
||||
}
|
||||
Ordering::Greater
|
||||
}
|
||||
(CalcResult::Number(_value1), CalcResult::String(_value2)) => Ordering::Less,
|
||||
(CalcResult::Number(_value1), CalcResult::Boolean(_value2)) => Ordering::Less,
|
||||
(CalcResult::String(value1), CalcResult::String(value2)) => {
|
||||
let value1 = value1.to_uppercase();
|
||||
let value2 = value2.to_uppercase();
|
||||
value1.cmp(&value2)
|
||||
}
|
||||
(CalcResult::String(_value1), CalcResult::Boolean(_value2)) => Ordering::Less,
|
||||
(CalcResult::Boolean(value1), CalcResult::Boolean(value2)) => {
|
||||
if value1 == value2 {
|
||||
return Ordering::Equal;
|
||||
}
|
||||
if *value1 {
|
||||
return Ordering::Greater;
|
||||
}
|
||||
Ordering::Less
|
||||
}
|
||||
(CalcResult::EmptyCell, CalcResult::String(_value2)) => Ordering::Greater,
|
||||
(CalcResult::EmptyArg, CalcResult::String(_value2)) => Ordering::Greater,
|
||||
(CalcResult::String(_value1), CalcResult::EmptyCell) => Ordering::Less,
|
||||
(CalcResult::EmptyCell, CalcResult::Number(_value2)) => Ordering::Greater,
|
||||
(CalcResult::EmptyArg, CalcResult::Number(_value2)) => Ordering::Greater,
|
||||
(CalcResult::Number(_value1), CalcResult::EmptyCell) => Ordering::Less,
|
||||
(CalcResult::EmptyCell, CalcResult::EmptyCell) => Ordering::Equal,
|
||||
(CalcResult::EmptyCell, CalcResult::EmptyArg) => Ordering::Equal,
|
||||
(CalcResult::EmptyArg, CalcResult::EmptyCell) => Ordering::Equal,
|
||||
(CalcResult::EmptyArg, CalcResult::EmptyArg) => Ordering::Equal,
|
||||
// NOTE: Errors and Ranges are not covered
|
||||
(_, _) => Ordering::Greater,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for CalcResult {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for CalcResult {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.cmp(other) == Ordering::Equal
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for CalcResult {}
|
||||
214
base/src/cast.rs
Normal file
214
base/src/cast.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference, Range},
|
||||
expressions::{parser::Node, token::Error},
|
||||
implicit_intersection::implicit_intersection,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn get_number(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
cell: CellReference,
|
||||
) -> Result<f64, CalcResult> {
|
||||
let result = self.evaluate_node_in_context(node, cell);
|
||||
self.cast_to_number(result, cell)
|
||||
}
|
||||
|
||||
fn cast_to_number(
|
||||
&mut self,
|
||||
result: CalcResult,
|
||||
cell: CellReference,
|
||||
) -> Result<f64, CalcResult> {
|
||||
match result {
|
||||
CalcResult::Number(f) => Ok(f),
|
||||
CalcResult::String(s) => match s.parse::<f64>() {
|
||||
Ok(f) => Ok(f),
|
||||
_ => Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expecting number".to_string(),
|
||||
)),
|
||||
},
|
||||
CalcResult::Boolean(f) => {
|
||||
if f {
|
||||
Ok(1.0)
|
||||
} else {
|
||||
Ok(0.0)
|
||||
}
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(0.0),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => {
|
||||
let result = self.evaluate_cell(cell_reference);
|
||||
self.cast_to_number(result, cell_reference)
|
||||
}
|
||||
None => Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid reference (number)".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_number_no_bools(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
cell: CellReference,
|
||||
) -> Result<f64, CalcResult> {
|
||||
let result = self.evaluate_node_in_context(node, cell);
|
||||
if matches!(result, CalcResult::Boolean(_)) {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expecting number".to_string(),
|
||||
));
|
||||
}
|
||||
self.cast_to_number(result, cell)
|
||||
}
|
||||
|
||||
pub(crate) fn get_string(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
cell: CellReference,
|
||||
) -> Result<String, CalcResult> {
|
||||
let result = self.evaluate_node_in_context(node, cell);
|
||||
self.cast_to_string(result, cell)
|
||||
}
|
||||
|
||||
pub(crate) fn cast_to_string(
|
||||
&mut self,
|
||||
result: CalcResult,
|
||||
cell: CellReference,
|
||||
) -> Result<String, CalcResult> {
|
||||
// FIXME: I think when casting a number we should convert it to_precision(x, 15)
|
||||
// See function Exact
|
||||
match result {
|
||||
CalcResult::Number(f) => Ok(format!("{}", f)),
|
||||
CalcResult::String(s) => Ok(s),
|
||||
CalcResult::Boolean(f) => {
|
||||
if f {
|
||||
Ok("TRUE".to_string())
|
||||
} else {
|
||||
Ok("FALSE".to_string())
|
||||
}
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok("".to_string()),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => {
|
||||
let result = self.evaluate_cell(cell_reference);
|
||||
self.cast_to_string(result, cell_reference)
|
||||
}
|
||||
None => Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid reference (string)".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_boolean(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
cell: CellReference,
|
||||
) -> Result<bool, CalcResult> {
|
||||
let result = self.evaluate_node_in_context(node, cell);
|
||||
self.cast_to_bool(result, cell)
|
||||
}
|
||||
|
||||
fn cast_to_bool(
|
||||
&mut self,
|
||||
result: CalcResult,
|
||||
cell: CellReference,
|
||||
) -> Result<bool, CalcResult> {
|
||||
match result {
|
||||
CalcResult::Number(f) => {
|
||||
if f == 0.0 {
|
||||
return Ok(false);
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
CalcResult::String(s) => {
|
||||
if s.to_lowercase() == *"true" {
|
||||
return Ok(true);
|
||||
} else if s.to_lowercase() == *"false" {
|
||||
return Ok(false);
|
||||
}
|
||||
Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Expected boolean".to_string(),
|
||||
})
|
||||
}
|
||||
CalcResult::Boolean(b) => Ok(b),
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Ok(false),
|
||||
error @ CalcResult::Error { .. } => Err(error),
|
||||
CalcResult::Range { left, right } => {
|
||||
match implicit_intersection(&cell, &Range { left, right }) {
|
||||
Some(cell_reference) => {
|
||||
let result = self.evaluate_cell(cell_reference);
|
||||
self.cast_to_bool(result, cell_reference)
|
||||
}
|
||||
None => Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid reference (bool)".to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tries to return a reference. That is either a reference or a formula that evaluates to a range/reference
|
||||
pub(crate) fn get_reference(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
cell: CellReference,
|
||||
) -> Result<Range, CalcResult> {
|
||||
match node {
|
||||
Node::ReferenceKind {
|
||||
column,
|
||||
absolute_column,
|
||||
row,
|
||||
absolute_row,
|
||||
sheet_index,
|
||||
sheet_name: _,
|
||||
} => {
|
||||
let left = CellReference {
|
||||
sheet: *sheet_index,
|
||||
row: if *absolute_row { *row } else { *row + cell.row },
|
||||
column: if *absolute_column {
|
||||
*column
|
||||
} else {
|
||||
*column + cell.column
|
||||
},
|
||||
};
|
||||
|
||||
Ok(Range { left, right: left })
|
||||
}
|
||||
_ => {
|
||||
let value = self.evaluate_node_in_context(node, cell);
|
||||
if value.is_error() {
|
||||
return Err(value);
|
||||
}
|
||||
if let CalcResult::Range { left, right } = value {
|
||||
Ok(Range { left, right })
|
||||
} else {
|
||||
Err(CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Expected reference".to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
193
base/src/cell.rs
Normal file
193
base/src/cell.rs
Normal file
@@ -0,0 +1,193 @@
|
||||
use crate::{
|
||||
expressions::token::Error, language::Language, number_format::to_excel_precision_str, types::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
/// A CellValue is the representation of the cell content.
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq)]
|
||||
#[serde(untagged)]
|
||||
pub enum CellValue {
|
||||
None,
|
||||
String(String),
|
||||
Number(f64),
|
||||
Boolean(bool),
|
||||
}
|
||||
|
||||
impl CellValue {
|
||||
pub fn to_json_str(&self) -> String {
|
||||
match &self {
|
||||
CellValue::None => "null".to_string(),
|
||||
CellValue::String(s) => json!(s).to_string(),
|
||||
CellValue::Number(f) => json!(f).to_string(),
|
||||
CellValue::Boolean(b) => json!(b).to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for CellValue {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::Number(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for CellValue {
|
||||
fn from(value: String) -> Self {
|
||||
Self::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for CellValue {
|
||||
fn from(value: &str) -> Self {
|
||||
Self::String(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for CellValue {
|
||||
fn from(value: bool) -> Self {
|
||||
Self::Boolean(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
/// Creates a new Cell with a shared string (`si` is the string index)
|
||||
pub fn new_string(si: i32, s: i32) -> Cell {
|
||||
Cell::SharedString { si, s }
|
||||
}
|
||||
|
||||
/// Creates a new Cell with a number
|
||||
pub fn new_number(v: f64, s: i32) -> Cell {
|
||||
Cell::NumberCell { v, s }
|
||||
}
|
||||
|
||||
/// Creates a new Cell with a boolean
|
||||
pub fn new_boolean(v: bool, s: i32) -> Cell {
|
||||
Cell::BooleanCell { v, s }
|
||||
}
|
||||
|
||||
/// Creates a new Cell with an error value
|
||||
pub fn new_error(ei: Error, s: i32) -> Cell {
|
||||
Cell::ErrorCell { ei, s }
|
||||
}
|
||||
|
||||
/// Creates a new Cell with an unevaluated formula
|
||||
pub fn new_formula(f: i32, s: i32) -> Cell {
|
||||
Cell::CellFormula { f, s }
|
||||
}
|
||||
|
||||
/// Returns the formula of a cell if any.
|
||||
pub fn get_formula(&self) -> Option<i32> {
|
||||
match self {
|
||||
Cell::CellFormula { f, .. } => Some(*f),
|
||||
Cell::CellFormulaBoolean { f, .. } => Some(*f),
|
||||
Cell::CellFormulaNumber { f, .. } => Some(*f),
|
||||
Cell::CellFormulaString { f, .. } => Some(*f),
|
||||
Cell::CellFormulaError { f, .. } => Some(*f),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_formula(&self) -> bool {
|
||||
self.get_formula().is_some()
|
||||
}
|
||||
|
||||
pub fn set_style(&mut self, style: i32) {
|
||||
match self {
|
||||
Cell::EmptyCell { s, .. } => *s = style,
|
||||
Cell::BooleanCell { s, .. } => *s = style,
|
||||
Cell::NumberCell { s, .. } => *s = style,
|
||||
Cell::ErrorCell { s, .. } => *s = style,
|
||||
Cell::SharedString { s, .. } => *s = style,
|
||||
Cell::CellFormula { s, .. } => *s = style,
|
||||
Cell::CellFormulaBoolean { s, .. } => *s = style,
|
||||
Cell::CellFormulaNumber { s, .. } => *s = style,
|
||||
Cell::CellFormulaString { s, .. } => *s = style,
|
||||
Cell::CellFormulaError { s, .. } => *s = style,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn get_style(&self) -> i32 {
|
||||
match self {
|
||||
Cell::EmptyCell { s, .. } => *s,
|
||||
Cell::BooleanCell { s, .. } => *s,
|
||||
Cell::NumberCell { s, .. } => *s,
|
||||
Cell::ErrorCell { s, .. } => *s,
|
||||
Cell::SharedString { s, .. } => *s,
|
||||
Cell::CellFormula { s, .. } => *s,
|
||||
Cell::CellFormulaBoolean { s, .. } => *s,
|
||||
Cell::CellFormulaNumber { s, .. } => *s,
|
||||
Cell::CellFormulaString { s, .. } => *s,
|
||||
Cell::CellFormulaError { s, .. } => *s,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_type(&self) -> CellType {
|
||||
match self {
|
||||
Cell::EmptyCell { .. } => CellType::Number,
|
||||
Cell::BooleanCell { .. } => CellType::LogicalValue,
|
||||
Cell::NumberCell { .. } => CellType::Number,
|
||||
Cell::ErrorCell { .. } => CellType::ErrorValue,
|
||||
// TODO: An empty string should likely be considered a Number (like an empty cell).
|
||||
Cell::SharedString { .. } => CellType::Text,
|
||||
Cell::CellFormula { .. } => CellType::Number,
|
||||
Cell::CellFormulaBoolean { .. } => CellType::LogicalValue,
|
||||
Cell::CellFormulaNumber { .. } => CellType::Number,
|
||||
Cell::CellFormulaString { .. } => CellType::Text,
|
||||
Cell::CellFormulaError { .. } => CellType::ErrorValue,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_text(&self, shared_strings: &[String], language: &Language) -> String {
|
||||
match self.value(shared_strings, language) {
|
||||
CellValue::None => "".to_string(),
|
||||
CellValue::String(v) => v,
|
||||
CellValue::Boolean(v) => v.to_string().to_uppercase(),
|
||||
CellValue::Number(v) => to_excel_precision_str(v),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self, shared_strings: &[String], language: &Language) -> CellValue {
|
||||
match self {
|
||||
Cell::EmptyCell { .. } => CellValue::None,
|
||||
Cell::BooleanCell { v, s: _ } => CellValue::Boolean(*v),
|
||||
Cell::NumberCell { v, s: _ } => CellValue::Number(*v),
|
||||
Cell::ErrorCell { ei, .. } => {
|
||||
let v = ei.to_localized_error_string(language);
|
||||
CellValue::String(v)
|
||||
}
|
||||
Cell::SharedString { si, .. } => {
|
||||
let s = shared_strings.get(*si as usize);
|
||||
let v = match s {
|
||||
Some(str) => str.clone(),
|
||||
None => "".to_string(),
|
||||
};
|
||||
CellValue::String(v)
|
||||
}
|
||||
Cell::CellFormula { .. } => CellValue::String("#ERROR!".to_string()),
|
||||
Cell::CellFormulaBoolean { v, .. } => CellValue::Boolean(*v),
|
||||
Cell::CellFormulaNumber { v, .. } => CellValue::Number(*v),
|
||||
Cell::CellFormulaString { v, .. } => CellValue::String(v.clone()),
|
||||
Cell::CellFormulaError { ei, .. } => {
|
||||
let v = ei.to_localized_error_string(language);
|
||||
CellValue::String(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn formatted_value<F>(
|
||||
&self,
|
||||
shared_strings: &[String],
|
||||
language: &Language,
|
||||
format_number: F,
|
||||
) -> String
|
||||
where
|
||||
F: Fn(f64) -> String,
|
||||
{
|
||||
match self.value(shared_strings, language) {
|
||||
CellValue::None => "".to_string(),
|
||||
CellValue::String(value) => value,
|
||||
CellValue::Boolean(value) => value.to_string().to_uppercase(),
|
||||
CellValue::Number(value) => format_number(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
16
base/src/constants.rs
Normal file
16
base/src/constants.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
/// Excel compatibility values
|
||||
/// COLUMN_WIDTH and ROW_HEIGHT are pixel values
|
||||
/// A column width of Excel value `w` will result in `w * COLUMN_WIDTH_FACTOR` pixels
|
||||
/// Note that these constants are inlined
|
||||
pub(crate) const DEFAULT_COLUMN_WIDTH: f64 = 100.0;
|
||||
pub(crate) const DEFAULT_ROW_HEIGHT: f64 = 21.0;
|
||||
pub(crate) const COLUMN_WIDTH_FACTOR: f64 = 12.0;
|
||||
pub(crate) const ROW_HEIGHT_FACTOR: f64 = 2.0;
|
||||
|
||||
pub(crate) const LAST_COLUMN: i32 = 16_384;
|
||||
pub(crate) const LAST_ROW: i32 = 1_048_576;
|
||||
|
||||
// 693_594 is computed as:
|
||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||
// The 2 days offset is because of Excel 1900 bug
|
||||
pub(crate) const EXCEL_DATE_BASE: i32 = 693_594;
|
||||
136
base/src/diffs.rs
Normal file
136
base/src/diffs.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use crate::{
|
||||
expressions::{
|
||||
parser::{
|
||||
move_formula::ref_is_in_area,
|
||||
stringify::{to_string, to_string_displaced, DisplaceData},
|
||||
walk::forward_references,
|
||||
},
|
||||
types::{Area, CellReferenceIndex, CellReferenceRC},
|
||||
},
|
||||
model::Model,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[serde(untagged, deny_unknown_fields)]
|
||||
pub enum CellValue {
|
||||
Value(String),
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SetCellValue {
|
||||
cell: CellReferenceIndex,
|
||||
new_value: CellValue,
|
||||
old_value: CellValue,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn shift_cell_formula(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
displace_data: &DisplaceData,
|
||||
) {
|
||||
if let Some(f) = self
|
||||
.workbook
|
||||
.worksheet(sheet)
|
||||
.expect("Worksheet must exist")
|
||||
.cell(row, column)
|
||||
.expect("Cell must exist")
|
||||
.get_formula()
|
||||
{
|
||||
let node = &self.parsed_formulas[sheet as usize][f as usize].clone();
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
// FIXME: This is not a very performant way if the formula has changed :S.
|
||||
let formula = to_string(node, &cell_reference);
|
||||
let formula_displaced = to_string_displaced(node, &cell_reference, displace_data);
|
||||
if formula != formula_displaced {
|
||||
self.update_cell_with_formula(sheet, row, column, format!("={formula_displaced}"))
|
||||
.expect("Failed to shift cell formula");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forward_references(
|
||||
&mut self,
|
||||
source_area: &Area,
|
||||
target: &CellReferenceIndex,
|
||||
) -> Result<Vec<SetCellValue>, String> {
|
||||
let mut diff_list: Vec<SetCellValue> = Vec::new();
|
||||
let target_area = &Area {
|
||||
sheet: target.sheet,
|
||||
row: target.row,
|
||||
column: target.column,
|
||||
width: source_area.width,
|
||||
height: source_area.height,
|
||||
};
|
||||
// Walk over every formula
|
||||
let cells = self.get_all_cells();
|
||||
for cell in cells {
|
||||
if let Some(f) = self
|
||||
.workbook
|
||||
.worksheet(cell.index)
|
||||
.expect("Worksheet must exist")
|
||||
.cell(cell.row, cell.column)
|
||||
.expect("Cell must exist")
|
||||
.get_formula()
|
||||
{
|
||||
let sheet = cell.index;
|
||||
let row = cell.row;
|
||||
let column = cell.column;
|
||||
|
||||
// If cell is in the source or target area, skip
|
||||
if ref_is_in_area(sheet, row, column, source_area)
|
||||
|| ref_is_in_area(sheet, row, column, target_area)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the formula
|
||||
// Get a copy of the AST
|
||||
let node = &mut self.parsed_formulas[sheet as usize][f as usize].clone();
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: self.workbook.worksheets[sheet as usize].get_name(),
|
||||
column: cell.column,
|
||||
row: cell.row,
|
||||
};
|
||||
let context = CellReferenceIndex { sheet, column, row };
|
||||
let formula = to_string(node, &cell_reference);
|
||||
let target_sheet_name = &self.workbook.worksheets[target.sheet as usize].name;
|
||||
forward_references(
|
||||
node,
|
||||
&context,
|
||||
source_area,
|
||||
target.sheet,
|
||||
target_sheet_name,
|
||||
target.row,
|
||||
target.column,
|
||||
);
|
||||
|
||||
// If the string representation of the formula has changed update the cell
|
||||
let updated_formula = to_string(node, &cell_reference);
|
||||
if formula != updated_formula {
|
||||
self.update_cell_with_formula(
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
format!("={updated_formula}"),
|
||||
)?;
|
||||
// Update the diff list
|
||||
diff_list.push(SetCellValue {
|
||||
cell: CellReferenceIndex { sheet, column, row },
|
||||
new_value: CellValue::Value(format!("={}", updated_formula)),
|
||||
old_value: CellValue::Value(format!("={}", formula)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(diff_list)
|
||||
}
|
||||
}
|
||||
762
base/src/expressions/lexer/mod.rs
Normal file
762
base/src/expressions/lexer/mod.rs
Normal file
@@ -0,0 +1,762 @@
|
||||
//! A tokenizer for spreadsheet formulas.
|
||||
//!
|
||||
//! This is meant to feed a formula parser.
|
||||
//!
|
||||
//! You will need to instantiate it with a language and a locale.
|
||||
//!
|
||||
//! It supports two working modes:
|
||||
//!
|
||||
//! 1. A1 or display mode
|
||||
//! This is for user formulas. References are like `D4`, `D$4` or `F5:T10`
|
||||
//! 2. R1C1, internal or runtime mode
|
||||
//! A reference like R1C1 refers to $A$1 and R3C4 to $D$4
|
||||
//! R[2]C[5] refers to a cell two rows below and five columns to the right
|
||||
//! It uses the 'en' locale and language.
|
||||
//! This is used internally at runtime.
|
||||
//!
|
||||
//! Formulas look different in different locales:
|
||||
//!
|
||||
//! =IF(A1, B1, NA()) versus =IF(A1; B1; NA())
|
||||
//!
|
||||
//! Also numbers are different:
|
||||
//!
|
||||
//! 1,123.45 versus 1.123,45
|
||||
//!
|
||||
//! The names of the errors and functions are different in different languages,
|
||||
//! but they stay the same in different locales.
|
||||
//!
|
||||
//! Note that in IronCalc if you are using a locale different from 'en' or a language different from 'en'
|
||||
//! you will still need the 'en' locale and language because formulas are stored in that language and locale
|
||||
//!
|
||||
//! # Examples:
|
||||
//! ```
|
||||
//! use ironcalc_base::expressions::lexer::{Lexer, LexerMode};
|
||||
//! use ironcalc_base::expressions::token::{TokenType, OpCompare};
|
||||
//! use ironcalc_base::locale::get_locale;
|
||||
//! use ironcalc_base::language::get_language;
|
||||
//!
|
||||
//! let locale = get_locale("en").unwrap();
|
||||
//! let language = get_language("en").unwrap();
|
||||
//! let mut lexer = Lexer::new("=A1*SUM(Sheet2!C3:D5)", LexerMode::A1, &locale, &language);
|
||||
//! assert_eq!(lexer.next_token(), TokenType::Compare(OpCompare::Equal));
|
||||
//! assert!(matches!(lexer.next_token(), TokenType::Reference { .. }));
|
||||
//! ```
|
||||
|
||||
use crate::expressions::token::{OpCompare, OpProduct, OpSum};
|
||||
|
||||
use crate::language::Language;
|
||||
use crate::locale::Locale;
|
||||
|
||||
use super::token::{index, Error, TokenType};
|
||||
use super::types::*;
|
||||
use super::utils;
|
||||
|
||||
pub mod util;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
mod ranges;
|
||||
mod structured_references;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LexerError {
|
||||
pub position: usize,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
pub(super) type Result<T> = std::result::Result<T, LexerError>;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub enum LexerMode {
|
||||
A1,
|
||||
R1C1,
|
||||
}
|
||||
|
||||
/// Tokenize an input
|
||||
#[derive(Clone)]
|
||||
pub struct Lexer {
|
||||
position: usize,
|
||||
next_token_position: Option<usize>,
|
||||
len: usize,
|
||||
chars: Vec<char>,
|
||||
mode: LexerMode,
|
||||
locale: Locale,
|
||||
language: Language,
|
||||
}
|
||||
|
||||
impl Lexer {
|
||||
/// Creates a new `Lexer` that returns the tokens of a formula.
|
||||
pub fn new(formula: &str, mode: LexerMode, locale: &Locale, language: &Language) -> Lexer {
|
||||
let chars: Vec<char> = formula.chars().collect();
|
||||
let len = chars.len();
|
||||
Lexer {
|
||||
chars,
|
||||
position: 0,
|
||||
next_token_position: None,
|
||||
len,
|
||||
mode,
|
||||
locale: locale.clone(),
|
||||
language: language.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Changes the lexer mode
|
||||
pub fn set_lexer_mode(&mut self, mode: LexerMode) {
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
// FIXME: I don't think we should have `is_a1_mode` and `get_formula`.
|
||||
// The caller already knows those two
|
||||
|
||||
/// Returns true if mode is A1
|
||||
pub fn is_a1_mode(&self) -> bool {
|
||||
self.mode == LexerMode::A1
|
||||
}
|
||||
|
||||
/// Returns the formula
|
||||
pub fn get_formula(&self) -> String {
|
||||
self.chars.iter().collect()
|
||||
}
|
||||
|
||||
// FIXME: This is used to get the "marked tokens"
|
||||
// I think a better API would be to return the marked tokens
|
||||
/// Returns the position of the lexer
|
||||
pub fn get_position(&self) -> i32 {
|
||||
self.position as i32
|
||||
}
|
||||
|
||||
/// Resets the formula
|
||||
pub fn set_formula(&mut self, content: &str) {
|
||||
self.chars = content.chars().collect();
|
||||
self.len = self.chars.len();
|
||||
self.position = 0;
|
||||
self.next_token_position = None;
|
||||
}
|
||||
|
||||
/// Returns an error if the token is not the expected one.
|
||||
pub fn expect(&mut self, tk: TokenType) -> Result<()> {
|
||||
let nt = self.next_token();
|
||||
if index(&nt) != index(&tk) {
|
||||
return Err(self.set_error(&format!("Error, expected {}", tk), self.position));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks the next token without advancing position
|
||||
/// See also [advance_token](Self::advance_token)
|
||||
pub fn peek_token(&mut self) -> TokenType {
|
||||
let position = self.position;
|
||||
let tk = self.next_token();
|
||||
self.next_token_position = Some(self.position);
|
||||
self.position = position;
|
||||
tk
|
||||
}
|
||||
|
||||
/// Advances position. This is used in conjunction with [`peek_token`](Self::peek_token)
|
||||
/// It is a noop if the has not been a previous peek_token
|
||||
pub fn advance_token(&mut self) {
|
||||
if let Some(position) = self.next_token_position {
|
||||
self.position = position;
|
||||
self.next_token_position = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the next token
|
||||
pub fn next_token(&mut self) -> TokenType {
|
||||
self.next_token_position = None;
|
||||
self.consume_whitespace();
|
||||
|
||||
match self.read_next_char() {
|
||||
Some(char) => {
|
||||
match char {
|
||||
'+' => TokenType::Addition(OpSum::Add),
|
||||
'-' => TokenType::Addition(OpSum::Minus),
|
||||
'*' => TokenType::Product(OpProduct::Times),
|
||||
'/' => TokenType::Product(OpProduct::Divide),
|
||||
'(' => TokenType::LeftParenthesis,
|
||||
')' => TokenType::RightParenthesis,
|
||||
'=' => TokenType::Compare(OpCompare::Equal),
|
||||
'{' => TokenType::LeftBrace,
|
||||
'}' => TokenType::RightBrace,
|
||||
'[' => TokenType::LeftBracket,
|
||||
']' => TokenType::RightBracket,
|
||||
':' => TokenType::Colon,
|
||||
';' => TokenType::Semicolon,
|
||||
',' => {
|
||||
if self.locale.numbers.symbols.decimal == "," {
|
||||
match self.consume_number(',') {
|
||||
Ok(number) => TokenType::Number(number),
|
||||
Err(error) => TokenType::Illegal(error),
|
||||
}
|
||||
} else {
|
||||
TokenType::Comma
|
||||
}
|
||||
}
|
||||
'.' => {
|
||||
if self.locale.numbers.symbols.decimal == "." {
|
||||
match self.consume_number('.') {
|
||||
Ok(number) => TokenType::Number(number),
|
||||
Err(error) => TokenType::Illegal(error),
|
||||
}
|
||||
} else {
|
||||
// There is no TokenType::PERIOD
|
||||
TokenType::Illegal(self.set_error("Expecting a number", self.position))
|
||||
}
|
||||
}
|
||||
'!' => TokenType::Bang,
|
||||
'^' => TokenType::Power,
|
||||
'%' => TokenType::Percent,
|
||||
'&' => TokenType::And,
|
||||
'$' => self.consume_absolute_reference(),
|
||||
'<' => {
|
||||
let next_token = self.peek_char();
|
||||
if next_token == Some('=') {
|
||||
self.position += 1;
|
||||
TokenType::Compare(OpCompare::LessOrEqualThan)
|
||||
} else if next_token == Some('>') {
|
||||
self.position += 1;
|
||||
TokenType::Compare(OpCompare::NonEqual)
|
||||
} else {
|
||||
TokenType::Compare(OpCompare::LessThan)
|
||||
}
|
||||
}
|
||||
'>' => {
|
||||
if self.peek_char() == Some('=') {
|
||||
self.position += 1;
|
||||
TokenType::Compare(OpCompare::GreaterOrEqualThan)
|
||||
} else {
|
||||
TokenType::Compare(OpCompare::GreaterThan)
|
||||
}
|
||||
}
|
||||
'#' => self.consume_error(),
|
||||
'"' => TokenType::String(self.consume_string()),
|
||||
'\'' => self.consume_quoted_sheet_reference(),
|
||||
'0'..='9' => {
|
||||
let position = self.position - 1;
|
||||
match self.consume_number(char) {
|
||||
Ok(number) => {
|
||||
if self.peek_token() == TokenType::Colon
|
||||
&& self.mode == LexerMode::A1
|
||||
{
|
||||
// Its a row range 3:5
|
||||
// FIXME: There are faster ways of parsing this
|
||||
// Like checking that 'number' is integer and that the next token is integer
|
||||
self.position = position;
|
||||
match self.consume_range_a1() {
|
||||
Ok(ParsedRange { left, right }) => {
|
||||
if let Some(right) = right {
|
||||
TokenType::Range {
|
||||
sheet: None,
|
||||
left,
|
||||
right,
|
||||
}
|
||||
} else {
|
||||
TokenType::Illegal(
|
||||
self.set_error("Expecting row range", position),
|
||||
)
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// Examples:
|
||||
// * 'Sheet 1'!3.4:5
|
||||
// * 'Sheet 1'!3:A2
|
||||
// * 'Sheet 1'!3:
|
||||
TokenType::Illegal(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
TokenType::Number(number)
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// tried to read a number but failed
|
||||
self.position = self.len;
|
||||
TokenType::Illegal(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if char.is_alphabetic() || char == '_' {
|
||||
// At this point is one of the following:
|
||||
// 1. A range with sheet: Sheet3!A3:D7
|
||||
// 2. A boolean: TRUE or FALSE (dependent on the language)
|
||||
// 3. A reference like WS34 or R3C5
|
||||
// 4. A range without sheet ER4:ER7
|
||||
// 5. A column range E:E
|
||||
// 6. An identifier like a function name or a defined name
|
||||
// 7. A range operator A1:OFFSET(...)
|
||||
// 8. An Invalid token
|
||||
let position = self.position;
|
||||
self.position -= 1;
|
||||
let name = self.consume_identifier();
|
||||
let position_indent = self.position;
|
||||
|
||||
let peek_char = self.peek_char();
|
||||
let next_char_is_colon = self.peek_char() == Some(':');
|
||||
|
||||
if peek_char == Some('!') {
|
||||
// reference
|
||||
self.position += 1;
|
||||
return self.consume_range(Some(name));
|
||||
} else if peek_char == Some('$') {
|
||||
self.position = position - 1;
|
||||
return self.consume_range(None);
|
||||
}
|
||||
let name_upper = name.to_ascii_uppercase();
|
||||
if name_upper == self.language.booleans.true_value {
|
||||
return TokenType::Boolean(true);
|
||||
} else if name_upper == self.language.booleans.false_value {
|
||||
return TokenType::Boolean(false);
|
||||
}
|
||||
if self.mode == LexerMode::A1 {
|
||||
let parsed_reference = utils::parse_reference_a1(&name_upper);
|
||||
if parsed_reference.is_some()
|
||||
|| (utils::is_valid_column(name_upper.trim_start_matches('$'))
|
||||
&& next_char_is_colon)
|
||||
{
|
||||
self.position = position - 1;
|
||||
match self.consume_range_a1() {
|
||||
Ok(ParsedRange { left, right }) => {
|
||||
if let Some(right) = right {
|
||||
return TokenType::Range {
|
||||
sheet: None,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
} else {
|
||||
return TokenType::Reference {
|
||||
sheet: None,
|
||||
column: left.column,
|
||||
row: left.row,
|
||||
absolute_row: left.absolute_row,
|
||||
absolute_column: left.absolute_column,
|
||||
};
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
// This could be the range operator: ":"
|
||||
if let Some(r) = parsed_reference {
|
||||
if next_char_is_colon {
|
||||
self.position = position_indent;
|
||||
return TokenType::Reference {
|
||||
sheet: None,
|
||||
row: r.row,
|
||||
column: r.column,
|
||||
absolute_column: r.absolute_column,
|
||||
absolute_row: r.absolute_row,
|
||||
};
|
||||
}
|
||||
}
|
||||
self.position = self.len;
|
||||
return TokenType::Illegal(error);
|
||||
}
|
||||
}
|
||||
} else if utils::is_valid_identifier(&name) {
|
||||
if peek_char == Some('[') {
|
||||
if let Ok(r) = self.consume_structured_reference(&name) {
|
||||
return r;
|
||||
}
|
||||
return TokenType::Illegal(self.set_error(
|
||||
"Invalid structured reference",
|
||||
self.position,
|
||||
));
|
||||
}
|
||||
return TokenType::Ident(name);
|
||||
} else {
|
||||
return TokenType::Illegal(
|
||||
self.set_error("Invalid identifier (A1)", self.position),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let pos = self.position;
|
||||
self.position = position - 1;
|
||||
match self.consume_range_r1c1() {
|
||||
// it's a valid R1C1 range
|
||||
// We need to check it's not something like R1C1P
|
||||
Ok(ParsedRange { left, right }) => {
|
||||
if pos > self.position {
|
||||
self.position = pos;
|
||||
if utils::is_valid_identifier(&name) {
|
||||
return TokenType::Ident(name);
|
||||
} else {
|
||||
self.position = self.len;
|
||||
return TokenType::Illegal(
|
||||
self.set_error(
|
||||
"Invalid identifier (R1C1)",
|
||||
pos,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(right) = right {
|
||||
return TokenType::Range {
|
||||
sheet: None,
|
||||
left,
|
||||
right,
|
||||
};
|
||||
} else {
|
||||
return TokenType::Reference {
|
||||
sheet: None,
|
||||
column: left.column,
|
||||
row: left.row,
|
||||
absolute_row: left.absolute_row,
|
||||
absolute_column: left.absolute_column,
|
||||
};
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
self.position = position - 1;
|
||||
if let Ok(r) = self.consume_reference_r1c1() {
|
||||
if self.peek_char() == Some(':') {
|
||||
return TokenType::Reference {
|
||||
sheet: None,
|
||||
row: r.row,
|
||||
column: r.column,
|
||||
absolute_column: r.absolute_column,
|
||||
absolute_row: r.absolute_row,
|
||||
};
|
||||
}
|
||||
}
|
||||
self.position = pos;
|
||||
|
||||
if utils::is_valid_identifier(&name) {
|
||||
return TokenType::Ident(name);
|
||||
} else {
|
||||
return TokenType::Illegal(self.set_error(
|
||||
&format!("Invalid identifier (R1C1): {name}"),
|
||||
error.position,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TokenType::Illegal(self.set_error("Unknown error", self.position))
|
||||
}
|
||||
}
|
||||
}
|
||||
None => TokenType::EOF,
|
||||
}
|
||||
}
|
||||
|
||||
// Private methods
|
||||
|
||||
fn set_error(&mut self, message: &str, position: usize) -> LexerError {
|
||||
self.position = self.len;
|
||||
LexerError {
|
||||
position,
|
||||
message: message.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_char(&mut self) -> Option<char> {
|
||||
let position = self.position;
|
||||
if position < self.len {
|
||||
Some(self.chars[position])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_char(&mut self, ch_expected: char) -> Result<()> {
|
||||
let position = self.position;
|
||||
if position >= self.len {
|
||||
return Err(self.set_error(
|
||||
&format!("Error, expected {} found EOF", &ch_expected),
|
||||
self.position,
|
||||
));
|
||||
} else {
|
||||
let ch = self.chars[position];
|
||||
if ch_expected != ch {
|
||||
return Err(self.set_error(
|
||||
&format!("Error, expected {} found {}", &ch_expected, &ch),
|
||||
self.position,
|
||||
));
|
||||
}
|
||||
self.position += 1;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_next_char(&mut self) -> Option<char> {
|
||||
let position = self.position;
|
||||
if position < self.len {
|
||||
self.position = position + 1;
|
||||
Some(self.chars[position])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Consumes an integer from the input stream
|
||||
fn consume_integer(&mut self, first: char) -> Result<i32> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = first.to_string();
|
||||
while position < len {
|
||||
let next_char = self.chars[position];
|
||||
if next_char.is_ascii_digit() {
|
||||
chars.push(next_char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
self.position = position;
|
||||
chars.parse::<i32>().map_err(|_| LexerError {
|
||||
position,
|
||||
message: format!("Failed to parse to int: {}", chars),
|
||||
})
|
||||
}
|
||||
|
||||
// Consumes a number in the current locale.
|
||||
// It only takes into account the decimal separator
|
||||
// Note that we do not parse the thousands separator
|
||||
// Let's say ',' is the thousands separator. Then 1,234 would be an error.
|
||||
// This is ok for most cases:
|
||||
// =IF(A1=1,234, TRUE, FALSE) will not work
|
||||
// If a user introduces a single number in the cell 1,234 we should be able to parse
|
||||
// and format the cell appropriately
|
||||
fn consume_number(&mut self, first: char) -> Result<f64> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = first.to_string();
|
||||
// numbers before the decimal point
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
if position < len && self.chars[position].to_string() == self.locale.numbers.symbols.decimal
|
||||
{
|
||||
// numbers after the decimal point
|
||||
chars.push('.');
|
||||
position += 1;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
if position + 1 < len && (self.chars[position] == 'e' || self.chars[position] == 'E') {
|
||||
// exponential side
|
||||
let x = self.chars[position + 1];
|
||||
if x == '-' || x == '+' || x.is_ascii_digit() {
|
||||
chars.push('e');
|
||||
chars.push(x);
|
||||
position += 2;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.position = position;
|
||||
match chars.parse::<f64>() {
|
||||
Err(_) => {
|
||||
Err(self.set_error(&format!("Failed to parse to double: {}", chars), position))
|
||||
}
|
||||
Ok(v) => Ok(v),
|
||||
}
|
||||
}
|
||||
|
||||
// Consumes an identifier from the input stream
|
||||
fn consume_identifier(&mut self) -> String {
|
||||
let mut position = self.position;
|
||||
while position < self.len {
|
||||
let next_char = self.chars[position];
|
||||
if next_char.is_alphanumeric() || next_char == '_' || next_char == '.' {
|
||||
position += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let chars = self.chars[self.position..position].iter().collect();
|
||||
self.position = position;
|
||||
chars
|
||||
}
|
||||
|
||||
fn consume_string(&mut self) -> String {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
position += 1;
|
||||
if x != '"' {
|
||||
chars.push(x);
|
||||
} else if position < len && self.chars[position] == '"' {
|
||||
chars.push(x);
|
||||
chars.push(self.chars[position]);
|
||||
position += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
self.position = position;
|
||||
chars
|
||||
}
|
||||
|
||||
// Consumes a quoted string from input
|
||||
// 'This is a quoted string'
|
||||
// ' Also is a ''quoted'' string'
|
||||
// Returns an error if it does not find a closing quote
|
||||
fn consume_single_quote_string(&mut self) -> Result<String> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut success = false;
|
||||
let mut needs_escape = false;
|
||||
while position < len {
|
||||
let next_char = self.chars[position];
|
||||
position += 1;
|
||||
if next_char == '\'' {
|
||||
if position == len {
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
if self.chars[position] != '\'' {
|
||||
success = true;
|
||||
break;
|
||||
} else {
|
||||
// In Excel we escape "'" with "''"
|
||||
needs_escape = true;
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !success {
|
||||
// We reached the end without the closing quote
|
||||
return Err(self.set_error("Expected closing \"'\" but found end of input", position));
|
||||
}
|
||||
let chars: String = self.chars[self.position..position - 1].iter().collect();
|
||||
self.position = position;
|
||||
if needs_escape {
|
||||
// In most cases we will not needs escaping so this would be an overkill
|
||||
return Ok(chars.replace("''", "'"));
|
||||
}
|
||||
|
||||
Ok(chars)
|
||||
}
|
||||
|
||||
// Reads an error from the input stream
|
||||
fn consume_error(&mut self) -> TokenType {
|
||||
let errors = &self.language.errors;
|
||||
let rest_of_formula: String = self.chars[self.position - 1..self.len].iter().collect();
|
||||
if rest_of_formula.starts_with(&errors.ref_value) {
|
||||
self.position += errors.ref_value.chars().count() - 1;
|
||||
return TokenType::Error(Error::REF);
|
||||
} else if rest_of_formula.starts_with(&errors.name) {
|
||||
self.position += errors.name.chars().count() - 1;
|
||||
return TokenType::Error(Error::NAME);
|
||||
} else if rest_of_formula.starts_with(&errors.value) {
|
||||
self.position += errors.value.chars().count() - 1;
|
||||
return TokenType::Error(Error::VALUE);
|
||||
} else if rest_of_formula.starts_with(&errors.div) {
|
||||
self.position += errors.div.chars().count() - 1;
|
||||
return TokenType::Error(Error::DIV);
|
||||
} else if rest_of_formula.starts_with(&errors.na) {
|
||||
self.position += errors.na.chars().count() - 1;
|
||||
return TokenType::Error(Error::NA);
|
||||
} else if rest_of_formula.starts_with(&errors.num) {
|
||||
self.position += errors.num.chars().count() - 1;
|
||||
return TokenType::Error(Error::NUM);
|
||||
} else if rest_of_formula.starts_with(&errors.error) {
|
||||
self.position += errors.error.chars().count() - 1;
|
||||
return TokenType::Error(Error::ERROR);
|
||||
} else if rest_of_formula.starts_with(&errors.nimpl) {
|
||||
self.position += errors.nimpl.chars().count() - 1;
|
||||
return TokenType::Error(Error::NIMPL);
|
||||
} else if rest_of_formula.starts_with(&errors.spill) {
|
||||
self.position += errors.spill.chars().count() - 1;
|
||||
return TokenType::Error(Error::SPILL);
|
||||
} else if rest_of_formula.starts_with(&errors.calc) {
|
||||
self.position += errors.calc.chars().count() - 1;
|
||||
return TokenType::Error(Error::CALC);
|
||||
} else if rest_of_formula.starts_with(&errors.null) {
|
||||
self.position += errors.null.chars().count() - 1;
|
||||
return TokenType::Error(Error::NULL);
|
||||
} else if rest_of_formula.starts_with(&errors.circ) {
|
||||
self.position += errors.circ.chars().count() - 1;
|
||||
return TokenType::Error(Error::CIRC);
|
||||
}
|
||||
TokenType::Illegal(self.set_error("Invalid error.", self.position))
|
||||
}
|
||||
|
||||
fn consume_whitespace(&mut self) {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if !x.is_whitespace() {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
self.position = position;
|
||||
}
|
||||
|
||||
fn consume_absolute_reference(&mut self) -> TokenType {
|
||||
// This is an absolute reference.
|
||||
// $A$4
|
||||
if self.mode == LexerMode::R1C1 {
|
||||
return TokenType::Illegal(
|
||||
self.set_error("Cannot parse A1 reference in R1C1 mode", self.position),
|
||||
);
|
||||
}
|
||||
self.position -= 1;
|
||||
self.consume_range(None)
|
||||
}
|
||||
|
||||
fn consume_quoted_sheet_reference(&mut self) -> TokenType {
|
||||
// This is a reference:
|
||||
// 'First Sheet'!A34
|
||||
let sheet_name = match self.consume_single_quote_string() {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
return TokenType::Illegal(error);
|
||||
}
|
||||
};
|
||||
if self.next_token() != TokenType::Bang {
|
||||
return TokenType::Illegal(self.set_error("Expected '!'", self.position));
|
||||
}
|
||||
self.consume_range(Some(sheet_name))
|
||||
}
|
||||
|
||||
fn consume_range(&mut self, sheet: Option<String>) -> TokenType {
|
||||
let m = if self.mode == LexerMode::A1 {
|
||||
self.consume_range_a1()
|
||||
} else {
|
||||
self.consume_range_r1c1()
|
||||
};
|
||||
match m {
|
||||
Ok(ParsedRange { left, right }) => {
|
||||
if let Some(right) = right {
|
||||
TokenType::Range { sheet, left, right }
|
||||
} else {
|
||||
TokenType::Reference {
|
||||
sheet,
|
||||
column: left.column,
|
||||
row: left.row,
|
||||
absolute_row: left.absolute_row,
|
||||
absolute_column: left.absolute_column,
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(error) => TokenType::Illegal(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
319
base/src/expressions/lexer/ranges.rs
Normal file
319
base/src/expressions/lexer/ranges.rs
Normal file
@@ -0,0 +1,319 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::{token::TokenType, utils::column_to_number};
|
||||
|
||||
use super::Lexer;
|
||||
use super::{ParsedRange, ParsedReference, Result};
|
||||
|
||||
impl Lexer {
|
||||
// Consumes a reference in A1 style like:
|
||||
// AS23, $AS23, AS$23, $AS$23, R12
|
||||
// Or returns an error
|
||||
fn consume_reference_a1(&mut self) -> Result<ParsedReference> {
|
||||
let mut absolute_column = false;
|
||||
let mut absolute_row = false;
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
if position < len && self.chars[position] == '$' {
|
||||
absolute_column = true;
|
||||
position += 1;
|
||||
}
|
||||
let mut column = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position].to_ascii_uppercase();
|
||||
match x {
|
||||
'A'..='Z' => column.push(x),
|
||||
_ => break,
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
if column.is_empty() {
|
||||
return Err(self.set_error("Failed to parse reference", position));
|
||||
}
|
||||
if position < len && self.chars[position] == '$' {
|
||||
absolute_row = true;
|
||||
position += 1;
|
||||
}
|
||||
let mut row = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
match x {
|
||||
'0'..='9' => row.push(x),
|
||||
_ => break,
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
// Note that row numbers could start with 0
|
||||
self.position = position;
|
||||
let column = column_to_number(&column).map_err(|error| self.set_error(&error, position))?;
|
||||
|
||||
match row.parse::<i32>() {
|
||||
Ok(row) => {
|
||||
if row > LAST_ROW {
|
||||
return Err(self.set_error("Row too large in reference", position));
|
||||
}
|
||||
Ok(ParsedReference {
|
||||
column,
|
||||
row,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
})
|
||||
}
|
||||
Err(..) => Err(self.set_error("Failed to parse integer", position)),
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing a range is a parser on it's own right. Here is the grammar:
|
||||
//
|
||||
// range -> cell | cell ':' cell | row ':' row | column ':' column
|
||||
// cell -> column row
|
||||
// column -> '$' column_name | column_name
|
||||
// row -> '$' row_name | row_name
|
||||
// column_name -> 'A'..'XFD'
|
||||
// row_name -> 1..1_048_576
|
||||
pub(super) fn consume_range_a1(&mut self) -> Result<ParsedRange> {
|
||||
// first let's try to parse a cell
|
||||
let mut position = self.position;
|
||||
match self.consume_reference_a1() {
|
||||
Ok(cell) => {
|
||||
if self.peek_char() == Some(':') {
|
||||
// It's a range
|
||||
self.position += 1;
|
||||
if let Ok(cell2) = self.consume_reference_a1() {
|
||||
Ok(ParsedRange {
|
||||
left: cell,
|
||||
right: Some(cell2),
|
||||
})
|
||||
} else {
|
||||
Err(self.set_error("Expecting reference in range", self.position))
|
||||
}
|
||||
} else {
|
||||
// just a reference
|
||||
Ok(ParsedRange {
|
||||
left: cell,
|
||||
right: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
self.position = position;
|
||||
// It's either a row range or a column range (or not a range at all)
|
||||
let len = self.len;
|
||||
let mut absolute_left = false;
|
||||
if position < len && self.chars[position] == '$' {
|
||||
absolute_left = true;
|
||||
position += 1;
|
||||
}
|
||||
let mut column_left = "".to_string();
|
||||
let mut row_left = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position].to_ascii_uppercase();
|
||||
match x {
|
||||
'A'..='Z' => column_left.push(x),
|
||||
'0'..='9' => row_left.push(x),
|
||||
_ => break,
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
if position >= len || self.chars[position] != ':' {
|
||||
return Err(self.set_error("Expecting reference in range", self.position));
|
||||
}
|
||||
position += 1;
|
||||
let mut absolute_right = false;
|
||||
if position < len && self.chars[position] == '$' {
|
||||
absolute_right = true;
|
||||
position += 1;
|
||||
}
|
||||
let mut column_right = "".to_string();
|
||||
let mut row_right = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position].to_ascii_uppercase();
|
||||
match x {
|
||||
'A'..='Z' => column_right.push(x),
|
||||
'0'..='9' => row_right.push(x),
|
||||
_ => break,
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
self.position = position;
|
||||
// At this point either the columns are the empty string or the rows are the empty string
|
||||
if !row_left.is_empty() {
|
||||
// It is a row range 23:56
|
||||
if row_right.is_empty() || !column_left.is_empty() || !column_right.is_empty() {
|
||||
return Err(self.set_error("Error parsing Range", position));
|
||||
}
|
||||
// Note that row numbers can start with 0
|
||||
let row_left = match row_left.parse::<i32>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return Err(self
|
||||
.set_error(&format!("Failed parsing row {}", row_left), position))
|
||||
}
|
||||
};
|
||||
let row_right = match row_right.parse::<i32>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return Err(self
|
||||
.set_error(&format!("Failed parsing row {}", row_right), position))
|
||||
}
|
||||
};
|
||||
if row_left > LAST_ROW {
|
||||
return Err(self.set_error("Row too large in reference", position));
|
||||
}
|
||||
if row_right > LAST_ROW {
|
||||
return Err(self.set_error("Row too large in reference", position));
|
||||
}
|
||||
return Ok(ParsedRange {
|
||||
left: ParsedReference {
|
||||
row: row_left,
|
||||
absolute_row: absolute_left,
|
||||
column: 1,
|
||||
absolute_column: true,
|
||||
},
|
||||
right: Some(ParsedReference {
|
||||
row: row_right,
|
||||
absolute_row: absolute_right,
|
||||
column: LAST_COLUMN,
|
||||
absolute_column: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
// It is a column range
|
||||
if column_right.is_empty() || !row_right.is_empty() {
|
||||
return Err(self.set_error("Error parsing Range", position));
|
||||
}
|
||||
let column_left = column_to_number(&column_left)
|
||||
.map_err(|error| self.set_error(&error, position))?;
|
||||
let column_right = column_to_number(&column_right)
|
||||
.map_err(|error| self.set_error(&error, position))?;
|
||||
Ok(ParsedRange {
|
||||
left: ParsedReference {
|
||||
row: 1,
|
||||
absolute_row: true,
|
||||
column: column_left,
|
||||
absolute_column: absolute_left,
|
||||
},
|
||||
right: Some(ParsedReference {
|
||||
row: LAST_ROW,
|
||||
absolute_row: true,
|
||||
column: column_right,
|
||||
absolute_column: absolute_right,
|
||||
}),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn consume_range_r1c1(&mut self) -> Result<ParsedRange> {
|
||||
// first let's try to parse a cell
|
||||
match self.consume_reference_r1c1() {
|
||||
Ok(cell) => {
|
||||
if self.peek_char() == Some(':') {
|
||||
// It's a range
|
||||
self.position += 1;
|
||||
if let Ok(cell2) = self.consume_reference_r1c1() {
|
||||
Ok(ParsedRange {
|
||||
left: cell,
|
||||
right: Some(cell2),
|
||||
})
|
||||
} else {
|
||||
Err(self.set_error("Expecting reference in range", self.position))
|
||||
}
|
||||
} else {
|
||||
// just a reference
|
||||
Ok(ParsedRange {
|
||||
left: cell,
|
||||
right: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
Err(s) => Err(s),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn consume_reference_r1c1(&mut self) -> Result<ParsedReference> {
|
||||
// R12C3, R[2]C[-2], R3C[6], R[-3]C4, RC1, R[-2]C
|
||||
let absolute_column;
|
||||
let absolute_row;
|
||||
let position = self.position;
|
||||
let row;
|
||||
let column;
|
||||
self.expect_char('R')?;
|
||||
match self.peek_char() {
|
||||
Some('[') => {
|
||||
absolute_row = false;
|
||||
self.expect_char('[')?;
|
||||
let c = match self.read_next_char() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return Err(self.set_error("Expected column number", position));
|
||||
}
|
||||
};
|
||||
match self.consume_integer(c) {
|
||||
Ok(v) => row = v,
|
||||
Err(_) => {
|
||||
return Err(self.set_error("Expected row number", position));
|
||||
}
|
||||
}
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
}
|
||||
Some(c) => {
|
||||
absolute_row = true;
|
||||
self.expect_char(c)?;
|
||||
match self.consume_integer(c) {
|
||||
Ok(v) => row = v,
|
||||
Err(_) => {
|
||||
return Err(self.set_error("Expected row number", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(self.set_error("Expected row number or '['", position));
|
||||
}
|
||||
}
|
||||
self.expect_char('C')?;
|
||||
match self.peek_char() {
|
||||
Some('[') => {
|
||||
self.expect_char('[')?;
|
||||
absolute_column = false;
|
||||
let c = match self.read_next_char() {
|
||||
Some(s) => s,
|
||||
None => {
|
||||
return Err(self.set_error("Expected column number", position));
|
||||
}
|
||||
};
|
||||
match self.consume_integer(c) {
|
||||
Ok(v) => column = v,
|
||||
Err(_) => {
|
||||
return Err(self.set_error("Expected column number", position));
|
||||
}
|
||||
}
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
}
|
||||
Some(c) => {
|
||||
absolute_column = true;
|
||||
self.expect_char(c)?;
|
||||
match self.consume_integer(c) {
|
||||
Ok(v) => column = v,
|
||||
Err(_) => {
|
||||
return Err(self.set_error("Expected column number", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(self.set_error("Expected column number or '['", position));
|
||||
}
|
||||
}
|
||||
if let Some(c) = self.peek_char() {
|
||||
if c.is_alphanumeric() {
|
||||
return Err(self.set_error("Expected end of reference", position));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ParsedReference {
|
||||
column,
|
||||
row,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
})
|
||||
}
|
||||
}
|
||||
188
base/src/expressions/lexer/structured_references.rs
Normal file
188
base/src/expressions/lexer/structured_references.rs
Normal file
@@ -0,0 +1,188 @@
|
||||
// Grammar:
|
||||
// structured references -> table_name "[" arguments "]"
|
||||
// arguments -> table_reference | "["specifier"]" "," table_reference
|
||||
// specifier > "#All" |
|
||||
// "#This Row" |
|
||||
// "#Data" |
|
||||
// "#Headers" |
|
||||
// "#Totals"
|
||||
// table_reference -> column_reference | range_reference
|
||||
// column reference -> column_name | "["column_name"]"
|
||||
// range_reference -> column_reference":"column_reference
|
||||
|
||||
use crate::expressions::token::TokenType;
|
||||
use crate::expressions::token::{TableReference, TableSpecifier};
|
||||
|
||||
use super::Result;
|
||||
use super::{Lexer, LexerError};
|
||||
|
||||
impl Lexer {
|
||||
fn consume_table_specifier(&mut self) -> Result<Option<TableSpecifier>> {
|
||||
if self.peek_char() == Some('#') {
|
||||
// It's a specifier
|
||||
// TODO(TD): There are better ways of doing this :)
|
||||
let rest_of_formula: String = self.chars[self.position..self.len].iter().collect();
|
||||
let specifier = if rest_of_formula.starts_with("#This Row]") {
|
||||
self.position += "#This Row]".bytes().len();
|
||||
TableSpecifier::ThisRow
|
||||
} else if rest_of_formula.starts_with("#All]") {
|
||||
self.position += "#All]".bytes().len();
|
||||
TableSpecifier::All
|
||||
} else if rest_of_formula.starts_with("#Data]") {
|
||||
self.position += "#Data]".bytes().len();
|
||||
TableSpecifier::Data
|
||||
} else if rest_of_formula.starts_with("#Headers]") {
|
||||
self.position += "#Headers]".bytes().len();
|
||||
TableSpecifier::Headers
|
||||
} else if rest_of_formula.starts_with("#Totals]") {
|
||||
self.position += "#Totals]".bytes().len();
|
||||
TableSpecifier::Totals
|
||||
} else {
|
||||
return Err(LexerError {
|
||||
position: self.position,
|
||||
message: "Invalid structured reference".to_string(),
|
||||
});
|
||||
};
|
||||
Ok(Some(specifier))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_column_reference(&mut self) -> Result<String> {
|
||||
self.consume_whitespace();
|
||||
let end_char = if self.peek_char() == Some('[') {
|
||||
self.position += 1;
|
||||
']'
|
||||
} else {
|
||||
')'
|
||||
};
|
||||
|
||||
let mut position = self.position;
|
||||
while position < self.len {
|
||||
let next_char = self.chars[position];
|
||||
if next_char != end_char {
|
||||
position += 1;
|
||||
if next_char == '\'' {
|
||||
if position == self.len {
|
||||
return Err(LexerError {
|
||||
position: self.position,
|
||||
message: "Invalid column name".to_string(),
|
||||
});
|
||||
}
|
||||
// skip next char
|
||||
position += 1
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let chars: String = self.chars[self.position..position].iter().collect();
|
||||
if end_char == ']' {
|
||||
position += 1;
|
||||
}
|
||||
self.position = position;
|
||||
Ok(chars
|
||||
.replace("'[", "[")
|
||||
.replace("']", "]")
|
||||
.replace("'#", "#")
|
||||
.replace("'@", "@")
|
||||
.replace("''", "'"))
|
||||
}
|
||||
|
||||
// Possibilities:
|
||||
// 1. MyTable[#Totals] or MyTable[#This Row]
|
||||
// 2. MyTable[MyColumn]
|
||||
// 3. MyTable[[My Column]]
|
||||
// 4. MyTable[[#This Row], [My Column]]
|
||||
// 5. MyTable[[#Totals], [MyColumn]]
|
||||
// 6. MyTable[[#This Row], [Jan]:[Dec]]
|
||||
// 7. MyTable[]
|
||||
//
|
||||
// Multiple specifiers are not supported yet:
|
||||
// 1. MyTable[[#Data], [#Totals], [MyColumn]]
|
||||
//
|
||||
// In particular note that names of columns are escaped only when they are in the first argument
|
||||
// We use '[' and ']'
|
||||
// When there is only a specifier but not a reference the specifier is not in brackets
|
||||
//
|
||||
// Invalid:
|
||||
// * MyTable[#Totals, [Jan]:[March]] => MyTable[[#Totals], [Jan]:[March]]
|
||||
//
|
||||
// NOTES:
|
||||
// * MyTable[[#Totals]] is translated into MyTable[#Totals]
|
||||
// * Excel shows '@' instead of '#This Row':
|
||||
// MyTable[[#This Row], [Jan]:[Dec]] => MyTable[@[Jan]:[Dec]]
|
||||
// But this is only a UI thing that we will ignore for now.
|
||||
pub(crate) fn consume_structured_reference(&mut self, table_name: &str) -> Result<TokenType> {
|
||||
self.expect(TokenType::LeftBracket)?;
|
||||
let peek_char = self.peek_char();
|
||||
if peek_char == Some(']') {
|
||||
// This is just a reference to the full table
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
return Ok(TokenType::Ident(table_name.to_string()));
|
||||
}
|
||||
if peek_char == Some('#') {
|
||||
// Expecting MyTable[#Totals]
|
||||
if let Some(specifier) = self.consume_table_specifier()? {
|
||||
return Ok(TokenType::StructuredReference {
|
||||
table_name: table_name.to_string(),
|
||||
specifier: Some(specifier),
|
||||
table_reference: None,
|
||||
});
|
||||
} else {
|
||||
return Err(LexerError {
|
||||
position: self.position,
|
||||
message: "Invalid structured reference".to_string(),
|
||||
});
|
||||
}
|
||||
} else if peek_char != Some('[') {
|
||||
// Expecting MyTable[MyColumn]
|
||||
self.position -= 1;
|
||||
let column_name = self.consume_column_reference()?;
|
||||
return Ok(TokenType::StructuredReference {
|
||||
table_name: table_name.to_string(),
|
||||
specifier: None,
|
||||
table_reference: Some(TableReference::ColumnReference(column_name)),
|
||||
});
|
||||
}
|
||||
self.expect(TokenType::LeftBracket)?;
|
||||
let specifier = self.consume_table_specifier()?;
|
||||
if specifier.is_some() {
|
||||
let peek_token = self.peek_token();
|
||||
if peek_token == TokenType::Comma {
|
||||
self.advance_token();
|
||||
self.expect(TokenType::LeftBracket)?;
|
||||
} else if peek_token == TokenType::RightBracket {
|
||||
return Ok(TokenType::StructuredReference {
|
||||
table_name: table_name.to_string(),
|
||||
specifier,
|
||||
table_reference: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Now it's either:
|
||||
// [Column Name]
|
||||
// [Column Name]:[Column Name]
|
||||
self.position -= 1;
|
||||
let column_reference = self.consume_column_reference()?;
|
||||
let table_reference = if self.peek_char() == Some(':') {
|
||||
self.position += 1;
|
||||
let column_reference_right = self.consume_column_reference()?;
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
Some(TableReference::RangeReference((
|
||||
column_reference,
|
||||
column_reference_right,
|
||||
)))
|
||||
} else {
|
||||
self.expect(TokenType::RightBracket)?;
|
||||
Some(TableReference::ColumnReference(column_reference))
|
||||
};
|
||||
Ok(TokenType::StructuredReference {
|
||||
table_name: table_name.to_string(),
|
||||
specifier,
|
||||
table_reference,
|
||||
})
|
||||
}
|
||||
}
|
||||
6
base/src/expressions/lexer/test/mod.rs
Normal file
6
base/src/expressions/lexer/test/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod test_common;
|
||||
mod test_language;
|
||||
mod test_locale;
|
||||
mod test_ranges;
|
||||
mod test_tables;
|
||||
mod test_util;
|
||||
508
base/src/expressions/lexer/test/test_common.rs
Normal file
508
base/src/expressions/lexer/test/test_common.rs
Normal file
@@ -0,0 +1,508 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
use crate::expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::TokenType::*,
|
||||
token::{Error, OpSum},
|
||||
};
|
||||
|
||||
fn new_lexer(formula: &str, a1_mode: bool) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language("en").unwrap();
|
||||
let mode = if a1_mode {
|
||||
LexerMode::A1
|
||||
} else {
|
||||
LexerMode::R1C1
|
||||
};
|
||||
Lexer::new(formula, mode, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_zero() {
|
||||
let mut lx = new_lexer("0", true);
|
||||
assert_eq!(lx.next_token(), Number(0.0));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_integer() {
|
||||
let mut lx = new_lexer("42", true);
|
||||
assert_eq!(lx.next_token(), Number(42.0));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_pi() {
|
||||
let mut lx = new_lexer("3.415", true);
|
||||
assert_eq!(lx.next_token(), Number(3.415));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_less_than_one() {
|
||||
let mut lx = new_lexer(".1415", true);
|
||||
assert_eq!(lx.next_token(), Number(0.1415));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_less_than_one_bis() {
|
||||
let mut lx = new_lexer("0.1415", true);
|
||||
assert_eq!(lx.next_token(), Number(0.1415));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_scientific() {
|
||||
let mut lx = new_lexer("1.1415e12", true);
|
||||
assert_eq!(lx.next_token(), Number(1.1415e12));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
#[test]
|
||||
fn test_number_scientific_1() {
|
||||
let mut lx = new_lexer("2.4e-12", true);
|
||||
assert_eq!(lx.next_token(), Number(2.4e-12));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_scientific_1b() {
|
||||
let mut lx = new_lexer("2.4E-12", true);
|
||||
assert_eq!(lx.next_token(), Number(2.4e-12));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_a_number() {
|
||||
let mut lx = new_lexer("..", true);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string() {
|
||||
let mut lx = new_lexer("\"Hello World!\"", true);
|
||||
assert_eq!(lx.next_token(), String("Hello World!".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_string_unicode() {
|
||||
let mut lx = new_lexer("\"你好,世界!\"", true);
|
||||
assert_eq!(lx.next_token(), String("你好,世界!".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boolean() {
|
||||
let mut lx = new_lexer("FALSE", true);
|
||||
assert_eq!(lx.next_token(), Boolean(false));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_boolean_true() {
|
||||
let mut lx = new_lexer("True", true);
|
||||
assert_eq!(lx.next_token(), Boolean(true));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference() {
|
||||
let mut lx = new_lexer("A1", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_absolute() {
|
||||
let mut lx = new_lexer("$A$1", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_absolute_1() {
|
||||
let mut lx = new_lexer("AB$12", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 28,
|
||||
row: 12,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_absolute_2() {
|
||||
let mut lx = new_lexer("$CC234", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 81,
|
||||
row: 234,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_sheet() {
|
||||
let mut lx = new_lexer("Sheet1!C34", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
column: 3,
|
||||
row: 34,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_sheet_unicode() {
|
||||
// Not that also tests the '!'
|
||||
let mut lx = new_lexer("'A € world!'!C34", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("A € world!".to_string()),
|
||||
column: 3,
|
||||
row: 34,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_sheet_unicode_absolute() {
|
||||
let mut lx = new_lexer("'A €'!$C$34", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("A €".to_string()),
|
||||
column: 3,
|
||||
row: 34,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unmatched_quote() {
|
||||
let mut lx = new_lexer("'A €!$C$34", true);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sum() {
|
||||
let mut lx = new_lexer("2.4+3.415", true);
|
||||
assert_eq!(lx.next_token(), Number(2.4));
|
||||
assert_eq!(lx.next_token(), Addition(OpSum::Add));
|
||||
assert_eq!(lx.next_token(), Number(3.415));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sum_1() {
|
||||
let mut lx = new_lexer("A2 + 'First Sheet'!$B$3", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 1,
|
||||
row: 2,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), Addition(OpSum::Add));
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("First Sheet".to_string()),
|
||||
column: 2,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_value() {
|
||||
let mut lx = new_lexer("#VALUE!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::VALUE));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_error() {
|
||||
let mut lx = new_lexer("#ERROR!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::ERROR));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_div() {
|
||||
let mut lx = new_lexer("#DIV/0!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::DIV));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_na() {
|
||||
let mut lx = new_lexer("#N/A", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::NA));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_name() {
|
||||
let mut lx = new_lexer("#NAME?", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::NAME));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_num() {
|
||||
let mut lx = new_lexer("#NUM!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::NUM));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_calc() {
|
||||
let mut lx = new_lexer("#CALC!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::CALC));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_null() {
|
||||
let mut lx = new_lexer("#NULL!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::NULL));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_spill() {
|
||||
let mut lx = new_lexer("#SPILL!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::SPILL));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_circ() {
|
||||
let mut lx = new_lexer("#CIRC!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::CIRC));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_invalid() {
|
||||
let mut lx = new_lexer("#VALU!", true);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_errors() {
|
||||
let mut lx = new_lexer("#DIV/0!+#NUM!", true);
|
||||
assert_eq!(lx.next_token(), Error(Error::DIV));
|
||||
assert_eq!(lx.next_token(), Addition(OpSum::Add));
|
||||
assert_eq!(lx.next_token(), Error(Error::NUM));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_variable_name() {
|
||||
let mut lx = new_lexer("MyVar", true);
|
||||
assert_eq!(lx.next_token(), Ident("MyVar".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_last_reference() {
|
||||
let mut lx = new_lexer("XFD1048576", true);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 16384,
|
||||
row: 1048576,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_not_a_reference() {
|
||||
let mut lx = new_lexer("XFE10", true);
|
||||
assert_eq!(lx.next_token(), Ident("XFE10".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_r1c1() {
|
||||
let mut lx = new_lexer("R1C1", false);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_r1c1_true() {
|
||||
let mut lx = new_lexer("R1C1", true);
|
||||
// NOTE: This is what google docs does.
|
||||
// Excel will not let you enter this formula.
|
||||
// Online Excel will let you and will mark the cell as in Error
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name_r1c1p() {
|
||||
let mut lx = new_lexer("R1C1P", false);
|
||||
assert_eq!(lx.next_token(), Ident("R1C1P".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_name_wrong_ref() {
|
||||
let mut lx = new_lexer("Sheet1!2", false);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_1() {
|
||||
let mut lx = new_lexer("Sheet1!R[1]C[2]", false);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
column: 2,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_quotes() {
|
||||
let mut lx = new_lexer("'Sheet 1'!R[1]C[2]", false);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet 1".to_string()),
|
||||
column: 2,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_escape_quotes() {
|
||||
let mut lx = new_lexer("'Sheet ''one'' 1'!R[1]C[2]", false);
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet 'one' 1".to_string()),
|
||||
column: 2,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reference_unfinished_quotes() {
|
||||
let mut lx = new_lexer("'Sheet 1!R[1]C[2]", false);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_round_function() {
|
||||
let mut lx = new_lexer("ROUND", false);
|
||||
assert_eq!(lx.next_token(), Ident("ROUND".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ident_with_underscore() {
|
||||
let mut lx = new_lexer("_IDENT", false);
|
||||
assert_eq!(lx.next_token(), Ident("_IDENT".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ident_with_period() {
|
||||
let mut lx = new_lexer("IDENT.IFIER", false);
|
||||
assert_eq!(lx.next_token(), Ident("IDENT.IFIER".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ident_cannot_start_with_period() {
|
||||
let mut lx = new_lexer(".IFIER", false);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_xlfn() {
|
||||
let mut lx = new_lexer("_xlfn.MyVar", true);
|
||||
assert_eq!(lx.next_token(), Ident("_xlfn.MyVar".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
101
base/src/expressions/lexer/test/test_language.rs
Normal file
101
base/src/expressions/lexer/test/test_language.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::{Error, TokenType},
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale,
|
||||
};
|
||||
|
||||
fn new_language_lexer(formula: &str, language: &str) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language(language).unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
// Spanish
|
||||
|
||||
#[test]
|
||||
fn test_verdadero_falso() {
|
||||
let mut lx = new_language_lexer("IF(A1, VERDADERO, FALSO)", "es");
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("IF".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::LeftParenthesis);
|
||||
assert!(matches!(lx.next_token(), TokenType::Reference { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(true));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(false));
|
||||
assert_eq!(lx.next_token(), TokenType::RightParenthesis);
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_spanish_errors_ref() {
|
||||
let mut lx = new_language_lexer("#¡REF!", "es");
|
||||
assert_eq!(lx.next_token(), TokenType::Error(Error::REF));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
// German
|
||||
|
||||
#[test]
|
||||
fn test_wahr_falsch() {
|
||||
let mut lx = new_language_lexer("IF(A1, WAHR, FALSCH)", "de");
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("IF".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::LeftParenthesis);
|
||||
assert!(matches!(lx.next_token(), TokenType::Reference { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(true));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(false));
|
||||
assert_eq!(lx.next_token(), TokenType::RightParenthesis);
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_german_errors_ref() {
|
||||
let mut lx = new_language_lexer("#BEZUG!", "de");
|
||||
assert_eq!(lx.next_token(), TokenType::Error(Error::REF));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
// French
|
||||
|
||||
#[test]
|
||||
fn test_vrai_faux() {
|
||||
let mut lx = new_language_lexer("IF(A1, VRAI, FAUX)", "fr");
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("IF".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::LeftParenthesis);
|
||||
assert!(matches!(lx.next_token(), TokenType::Reference { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(true));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Boolean(false));
|
||||
assert_eq!(lx.next_token(), TokenType::RightParenthesis);
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_french_errors_ref() {
|
||||
let mut lx = new_language_lexer("#REF!", "fr");
|
||||
assert_eq!(lx.next_token(), TokenType::Error(Error::REF));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
// English with errors
|
||||
|
||||
#[test]
|
||||
fn test_english_with_spanish_words() {
|
||||
let mut lx = new_language_lexer("IF(A1, VERDADERO, FALSO)", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("IF".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::LeftParenthesis);
|
||||
assert!(matches!(lx.next_token(), TokenType::Reference { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("VERDADERO".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Ident("FALSO".to_string()));
|
||||
assert_eq!(lx.next_token(), TokenType::RightParenthesis);
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
48
base/src/expressions/lexer/test/test_locale.rs
Normal file
48
base/src/expressions/lexer/test/test_locale.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::TokenType,
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale_fix,
|
||||
};
|
||||
|
||||
fn new_language_lexer(formula: &str, locale: &str, language: &str) -> Lexer {
|
||||
let locale = get_locale_fix(locale).unwrap();
|
||||
let language = get_language(language).unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_german_locale() {
|
||||
let mut lx = new_language_lexer("2,34e-3", "de", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Number(2.34e-3));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_german_locale_does_not_parse() {
|
||||
let mut lx = new_language_lexer("2.34e-3", "de", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Number(2.0));
|
||||
assert!(matches!(lx.next_token(), TokenType::Illegal { .. }));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_english_locale() {
|
||||
let mut lx = new_language_lexer("2.34e-3", "en", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Number(2.34e-3));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_english_locale_does_not_parse() {
|
||||
// a comma is a separator
|
||||
let mut lx = new_language_lexer("2,34e-3", "en", "en");
|
||||
assert_eq!(lx.next_token(), TokenType::Number(2.0));
|
||||
assert_eq!(lx.next_token(), TokenType::Comma);
|
||||
assert_eq!(lx.next_token(), TokenType::Number(34e-3));
|
||||
assert_eq!(lx.next_token(), TokenType::EOF);
|
||||
}
|
||||
487
base/src/expressions/lexer/test/test_ranges.rs
Normal file
487
base/src/expressions/lexer/test/test_ranges.rs
Normal file
@@ -0,0 +1,487 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::lexer::LexerError;
|
||||
use crate::expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::TokenType::*,
|
||||
types::ParsedReference,
|
||||
};
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
fn new_lexer(formula: &str) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language("en").unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range() {
|
||||
let mut lx = new_lexer("C4:D4");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 4,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: 4,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_absolute_column() {
|
||||
let mut lx = new_lexer("$A1:B$4");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 2,
|
||||
row: 4,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_with_sheet() {
|
||||
let mut lx = new_lexer("Sheet1!A1:B4");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 2,
|
||||
row: 4,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_with_sheet_with_space() {
|
||||
let mut lx = new_lexer("'New sheet'!$A$1:B44");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("New sheet".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 2,
|
||||
row: 44,
|
||||
absolute_column: false,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column() {
|
||||
let mut lx = new_lexer("C:D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_out_of_range() {
|
||||
let mut lx = new_lexer("C:XFE");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Illegal(LexerError {
|
||||
position: 5,
|
||||
message: "Column is not valid.".to_string(),
|
||||
})
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_absolute1() {
|
||||
let mut lx = new_lexer("$C:D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_absolute2() {
|
||||
let mut lx = new_lexer("$C:$AA");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 27,
|
||||
row: LAST_ROW,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows() {
|
||||
let mut lx = new_lexer("3:5");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows_absolute1() {
|
||||
let mut lx = new_lexer("$3:5");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows_absolute2() {
|
||||
let mut lx = new_lexer("$3:$55");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 55,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_sheet() {
|
||||
let mut lx = new_lexer("Sheet1!C:D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: false,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_column_sheet_absolute() {
|
||||
let mut lx = new_lexer("Sheet1!$C:$D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
|
||||
let mut lx = new_lexer("'Woops ans'!$C:$D");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Woops ans".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 3,
|
||||
row: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: 4,
|
||||
row: LAST_ROW,
|
||||
absolute_column: true,
|
||||
absolute_row: true,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows_sheet() {
|
||||
let mut lx = new_lexer("'A new sheet'!3:5");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("A new sheet".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
let mut lx = new_lexer("Sheet12!3:5");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: Some("Sheet12".to_string()),
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
// Non ranges
|
||||
|
||||
#[test]
|
||||
fn test_non_range_variable_name() {
|
||||
let mut lx = new_lexer("AB");
|
||||
assert_eq!(lx.next_token(), Ident("AB".to_string()));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_range_invalid_variable_name() {
|
||||
let mut lx = new_lexer("$AB");
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_range_invalid_variable_name_a03() {
|
||||
let mut lx = new_lexer("A03");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: None,
|
||||
row: 3,
|
||||
column: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_range_invalid_variable_name_sheet1_a03() {
|
||||
let mut lx = new_lexer("Sheet1!A03");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Reference {
|
||||
sheet: Some("Sheet1".to_string()),
|
||||
row: 3,
|
||||
column: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_rows_with_0() {
|
||||
let mut lx = new_lexer("03:05");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
Range {
|
||||
sheet: None,
|
||||
left: ParsedReference {
|
||||
column: 1,
|
||||
row: 3,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
},
|
||||
right: ParsedReference {
|
||||
column: LAST_COLUMN,
|
||||
row: 5,
|
||||
absolute_column: true,
|
||||
absolute_row: false,
|
||||
}
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_incomplete_row() {
|
||||
let mut lx = new_lexer("R[");
|
||||
lx.set_lexer_mode(LexerMode::R1C1);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_incomplete_column() {
|
||||
let mut lx = new_lexer("R[3][");
|
||||
lx.set_lexer_mode(LexerMode::R1C1);
|
||||
assert!(matches!(lx.next_token(), Illegal(_)));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn range_operator() {
|
||||
let mut lx = new_lexer("A1:OFFSET(B1,1,2)");
|
||||
lx.set_lexer_mode(LexerMode::A1);
|
||||
assert!(matches!(lx.next_token(), Reference { .. }));
|
||||
assert!(matches!(lx.next_token(), Colon));
|
||||
assert!(matches!(lx.next_token(), Ident(_)));
|
||||
assert!(matches!(lx.next_token(), LeftParenthesis));
|
||||
assert!(matches!(lx.next_token(), Reference { .. }));
|
||||
assert_eq!(lx.next_token(), Comma);
|
||||
assert!(matches!(lx.next_token(), Number(_)));
|
||||
assert_eq!(lx.next_token(), Comma);
|
||||
assert!(matches!(lx.next_token(), Number(_)));
|
||||
assert!(matches!(lx.next_token(), RightParenthesis));
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
73
base/src/expressions/lexer/test/test_tables.rs
Normal file
73
base/src/expressions/lexer/test/test_tables.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::expressions::{
|
||||
lexer::{Lexer, LexerMode},
|
||||
token::{TableReference, TableSpecifier, TokenType::*},
|
||||
};
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
fn new_lexer(formula: &str) -> Lexer {
|
||||
let locale = get_locale("en").unwrap();
|
||||
let language = get_language("en").unwrap();
|
||||
Lexer::new(formula, LexerMode::A1, locale, language)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_this_row() {
|
||||
let mut lx = new_lexer("tbInfo[[#This Row], [Jan]:[Dec]]");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
StructuredReference {
|
||||
table_name: "tbInfo".to_string(),
|
||||
specifier: Some(TableSpecifier::ThisRow),
|
||||
table_reference: Some(TableReference::RangeReference((
|
||||
"Jan".to_string(),
|
||||
"Dec".to_string()
|
||||
)))
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_no_specifier() {
|
||||
let mut lx = new_lexer("tbInfo[December]");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
StructuredReference {
|
||||
table_name: "tbInfo".to_string(),
|
||||
specifier: None,
|
||||
table_reference: Some(TableReference::ColumnReference("December".to_string()))
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_no_specifier_white_spaces() {
|
||||
let mut lx = new_lexer("tbInfo[[First Month]]");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
StructuredReference {
|
||||
table_name: "tbInfo".to_string(),
|
||||
specifier: None,
|
||||
table_reference: Some(TableReference::ColumnReference("First Month".to_string()))
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_totals_no_reference() {
|
||||
let mut lx = new_lexer("tbInfo[#Totals]");
|
||||
assert_eq!(
|
||||
lx.next_token(),
|
||||
StructuredReference {
|
||||
table_name: "tbInfo".to_string(),
|
||||
specifier: Some(TableSpecifier::Totals),
|
||||
table_reference: None
|
||||
}
|
||||
);
|
||||
assert_eq!(lx.next_token(), EOF);
|
||||
}
|
||||
146
base/src/expressions/lexer/test/test_util.rs
Normal file
146
base/src/expressions/lexer/test/test_util.rs
Normal file
@@ -0,0 +1,146 @@
|
||||
use crate::expressions::{
|
||||
lexer::util::get_tokens,
|
||||
token::{OpCompare, OpSum, TokenType},
|
||||
};
|
||||
|
||||
fn get_tokens_types(formula: &str) -> Vec<TokenType> {
|
||||
let marked_tokens = get_tokens(formula);
|
||||
marked_tokens.iter().map(|s| s.token.clone()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_tokens() {
|
||||
let formula = "1+1";
|
||||
let t = get_tokens(formula);
|
||||
assert_eq!(t.len(), 3);
|
||||
|
||||
let formula = "1 + AA23 +";
|
||||
let t = get_tokens(formula);
|
||||
assert_eq!(t.len(), 4);
|
||||
let l = t.get(2).expect("expected token");
|
||||
assert_eq!(l.start, 3);
|
||||
assert_eq!(l.end, 10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_tokens() {
|
||||
assert_eq!(
|
||||
get_tokens_types("()"),
|
||||
vec![TokenType::LeftParenthesis, TokenType::RightParenthesis]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("{}"),
|
||||
vec![TokenType::LeftBrace, TokenType::RightBrace]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("[]"),
|
||||
vec![TokenType::LeftBracket, TokenType::RightBracket]
|
||||
);
|
||||
assert_eq!(get_tokens_types("&"), vec![TokenType::And]);
|
||||
assert_eq!(
|
||||
get_tokens_types("<"),
|
||||
vec![TokenType::Compare(OpCompare::LessThan)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types(">"),
|
||||
vec![TokenType::Compare(OpCompare::GreaterThan)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("<="),
|
||||
vec![TokenType::Compare(OpCompare::LessOrEqualThan)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types(">="),
|
||||
vec![TokenType::Compare(OpCompare::GreaterOrEqualThan)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("IF"),
|
||||
vec![TokenType::Ident("IF".to_owned())]
|
||||
);
|
||||
assert_eq!(get_tokens_types("45"), vec![TokenType::Number(45.0)]);
|
||||
// The lexer parses this as two tokens
|
||||
assert_eq!(
|
||||
get_tokens_types("-45"),
|
||||
vec![TokenType::Addition(OpSum::Minus), TokenType::Number(45.0)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("23.45e-2"),
|
||||
vec![TokenType::Number(23.45e-2)]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("4-3"),
|
||||
vec![
|
||||
TokenType::Number(4.0),
|
||||
TokenType::Addition(OpSum::Minus),
|
||||
TokenType::Number(3.0)
|
||||
]
|
||||
);
|
||||
assert_eq!(get_tokens_types("True"), vec![TokenType::Boolean(true)]);
|
||||
assert_eq!(get_tokens_types("FALSE"), vec![TokenType::Boolean(false)]);
|
||||
assert_eq!(
|
||||
get_tokens_types("2,3.5"),
|
||||
vec![
|
||||
TokenType::Number(2.0),
|
||||
TokenType::Comma,
|
||||
TokenType::Number(3.5)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("2.4;3.5"),
|
||||
vec![
|
||||
TokenType::Number(2.4),
|
||||
TokenType::Semicolon,
|
||||
TokenType::Number(3.5)
|
||||
]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("AB34"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: None,
|
||||
row: 34,
|
||||
column: 28,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("$A3"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: None,
|
||||
row: 3,
|
||||
column: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: false
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("AB$34"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: None,
|
||||
row: 34,
|
||||
column: 28,
|
||||
absolute_column: false,
|
||||
absolute_row: true
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("$AB$34"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: None,
|
||||
row: 34,
|
||||
column: 28,
|
||||
absolute_column: true,
|
||||
absolute_row: true
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
get_tokens_types("'My House'!AB34"),
|
||||
vec![TokenType::Reference {
|
||||
sheet: Some("My House".to_string()),
|
||||
row: 34,
|
||||
column: 28,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
}]
|
||||
);
|
||||
}
|
||||
85
base/src/expressions/lexer/util.rs
Normal file
85
base/src/expressions/lexer/util.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::expressions::token;
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
|
||||
use super::{Lexer, LexerMode};
|
||||
|
||||
/// A MarkedToken is a token together with its position on a formula
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct MarkedToken {
|
||||
pub token: token::TokenType,
|
||||
pub start: i32,
|
||||
pub end: i32,
|
||||
}
|
||||
|
||||
/// Returns a list of marked tokens for a formula
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use ironcalc_base::expressions::{
|
||||
/// lexer::util::{get_tokens, MarkedToken},
|
||||
/// token::{OpSum, TokenType},
|
||||
/// };
|
||||
///
|
||||
/// let marked_tokens = get_tokens("A1+1");
|
||||
/// let first_t = MarkedToken {
|
||||
/// token: TokenType::Reference {
|
||||
/// sheet: None,
|
||||
/// row: 1,
|
||||
/// column: 1,
|
||||
/// absolute_column: false,
|
||||
/// absolute_row: false,
|
||||
/// },
|
||||
/// start: 0,
|
||||
/// end: 2,
|
||||
/// };
|
||||
/// let second_t = MarkedToken {
|
||||
/// token: TokenType::Addition(OpSum::Add),
|
||||
/// start:2,
|
||||
/// end: 3
|
||||
/// };
|
||||
/// let third_t = MarkedToken {
|
||||
/// token: TokenType::Number(1.0),
|
||||
/// start:3,
|
||||
/// end: 4
|
||||
/// };
|
||||
/// assert_eq!(marked_tokens, vec![first_t, second_t, third_t]);
|
||||
/// ```
|
||||
pub fn get_tokens(formula: &str) -> Vec<MarkedToken> {
|
||||
let mut tokens = Vec::new();
|
||||
let mut lexer = Lexer::new(
|
||||
formula,
|
||||
LexerMode::A1,
|
||||
get_locale("en").expect(""),
|
||||
get_language("en").expect(""),
|
||||
);
|
||||
let mut start = lexer.get_position();
|
||||
let mut next_token = lexer.next_token();
|
||||
let mut end = lexer.get_position();
|
||||
loop {
|
||||
match next_token {
|
||||
token::TokenType::EOF => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
tokens.push(MarkedToken {
|
||||
start,
|
||||
end,
|
||||
token: next_token,
|
||||
});
|
||||
start = lexer.get_position();
|
||||
next_token = lexer.next_token();
|
||||
end = lexer.get_position();
|
||||
}
|
||||
}
|
||||
}
|
||||
tokens
|
||||
}
|
||||
|
||||
impl fmt::Display for MarkedToken {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(fmt, "{}", self.token)
|
||||
}
|
||||
}
|
||||
6
base/src/expressions/mod.rs
Normal file
6
base/src/expressions/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// public modules
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
pub mod token;
|
||||
pub mod types;
|
||||
pub mod utils;
|
||||
877
base/src/expressions/parser/mod.rs
Normal file
877
base/src/expressions/parser/mod.rs
Normal file
@@ -0,0 +1,877 @@
|
||||
/*!
|
||||
# GRAMAR
|
||||
|
||||
<pre class="rust">
|
||||
opComp => '=' | '<' | '>' | '<=' } '>=' | '<>'
|
||||
opFactor => '*' | '/'
|
||||
unaryOp => '-' | '+'
|
||||
|
||||
expr => concat (opComp concat)*
|
||||
concat => term ('&' term)*
|
||||
term => factor (opFactor factor)*
|
||||
factor => prod (opProd prod)*
|
||||
prod => power ('^' power)*
|
||||
power => (unaryOp)* range '%'*
|
||||
range => primary (':' primary)?
|
||||
primary => '(' expr ')'
|
||||
=> number
|
||||
=> function '(' f_args ')'
|
||||
=> name
|
||||
=> string
|
||||
=> '{' a_args '}'
|
||||
=> bool
|
||||
=> bool()
|
||||
=> error
|
||||
|
||||
f_args => e (',' e)*
|
||||
</pre>
|
||||
*/
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::functions::Function;
|
||||
use crate::language::get_language;
|
||||
use crate::locale::get_locale;
|
||||
use crate::types::Table;
|
||||
|
||||
use super::lexer;
|
||||
use super::token;
|
||||
use super::token::OpUnary;
|
||||
use super::token::TableReference;
|
||||
use super::token::TokenType;
|
||||
use super::types::*;
|
||||
use super::utils::number_to_column;
|
||||
|
||||
use token::OpCompare;
|
||||
|
||||
pub mod move_formula;
|
||||
pub mod stringify;
|
||||
pub mod walk;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_ranges;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_move_formula;
|
||||
#[cfg(test)]
|
||||
mod test_tables;
|
||||
|
||||
pub(crate) fn parse_range(formula: &str) -> Result<(i32, i32, i32, i32), String> {
|
||||
let mut lexer = lexer::Lexer::new(
|
||||
formula,
|
||||
lexer::LexerMode::A1,
|
||||
get_locale("en").expect(""),
|
||||
get_language("en").expect(""),
|
||||
);
|
||||
if let TokenType::Range {
|
||||
left,
|
||||
right,
|
||||
sheet: _,
|
||||
} = lexer.next_token()
|
||||
{
|
||||
Ok((left.column, left.row, right.column, right.row))
|
||||
} else {
|
||||
Err("Not a range".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn get_table_column_by_name(table_column_name: &str, table: &Table) -> Option<i32> {
|
||||
for (index, table_column) in table.columns.iter().enumerate() {
|
||||
if table_column.name == table_column_name {
|
||||
return Some(index as i32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) struct Reference<'a> {
|
||||
sheet_name: &'a Option<String>,
|
||||
sheet_index: u32,
|
||||
absolute_row: bool,
|
||||
absolute_column: bool,
|
||||
row: i32,
|
||||
column: i32,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum Node {
|
||||
BooleanKind(bool),
|
||||
NumberKind(f64),
|
||||
StringKind(String),
|
||||
ReferenceKind {
|
||||
sheet_name: Option<String>,
|
||||
sheet_index: u32,
|
||||
absolute_row: bool,
|
||||
absolute_column: bool,
|
||||
row: i32,
|
||||
column: i32,
|
||||
},
|
||||
RangeKind {
|
||||
sheet_name: Option<String>,
|
||||
sheet_index: u32,
|
||||
absolute_row1: bool,
|
||||
absolute_column1: bool,
|
||||
row1: i32,
|
||||
column1: i32,
|
||||
absolute_row2: bool,
|
||||
absolute_column2: bool,
|
||||
row2: i32,
|
||||
column2: i32,
|
||||
},
|
||||
WrongReferenceKind {
|
||||
sheet_name: Option<String>,
|
||||
absolute_row: bool,
|
||||
absolute_column: bool,
|
||||
row: i32,
|
||||
column: i32,
|
||||
},
|
||||
WrongRangeKind {
|
||||
sheet_name: Option<String>,
|
||||
absolute_row1: bool,
|
||||
absolute_column1: bool,
|
||||
row1: i32,
|
||||
column1: i32,
|
||||
absolute_row2: bool,
|
||||
absolute_column2: bool,
|
||||
row2: i32,
|
||||
column2: i32,
|
||||
},
|
||||
OpRangeKind {
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
OpConcatenateKind {
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
OpSumKind {
|
||||
kind: token::OpSum,
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
OpProductKind {
|
||||
kind: token::OpProduct,
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
OpPowerKind {
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
FunctionKind {
|
||||
kind: Function,
|
||||
args: Vec<Node>,
|
||||
},
|
||||
InvalidFunctionKind {
|
||||
name: String,
|
||||
args: Vec<Node>,
|
||||
},
|
||||
ArrayKind(Vec<Node>),
|
||||
VariableKind(String),
|
||||
CompareKind {
|
||||
kind: OpCompare,
|
||||
left: Box<Node>,
|
||||
right: Box<Node>,
|
||||
},
|
||||
UnaryKind {
|
||||
kind: OpUnary,
|
||||
right: Box<Node>,
|
||||
},
|
||||
ErrorKind(token::Error),
|
||||
ParseErrorKind {
|
||||
formula: String,
|
||||
message: String,
|
||||
position: usize,
|
||||
},
|
||||
EmptyArgKind,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Parser {
|
||||
lexer: lexer::Lexer,
|
||||
worksheets: Vec<String>,
|
||||
context: Option<CellReferenceRC>,
|
||||
tables: HashMap<String, Table>,
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new(worksheets: Vec<String>, tables: HashMap<String, Table>) -> Parser {
|
||||
let lexer = lexer::Lexer::new(
|
||||
"",
|
||||
lexer::LexerMode::A1,
|
||||
get_locale("en").expect(""),
|
||||
get_language("en").expect(""),
|
||||
);
|
||||
Parser {
|
||||
lexer,
|
||||
worksheets,
|
||||
context: None,
|
||||
tables,
|
||||
}
|
||||
}
|
||||
pub fn set_lexer_mode(&mut self, mode: lexer::LexerMode) {
|
||||
self.lexer.set_lexer_mode(mode)
|
||||
}
|
||||
|
||||
pub fn set_worksheets(&mut self, worksheets: Vec<String>) {
|
||||
self.worksheets = worksheets;
|
||||
}
|
||||
|
||||
pub fn parse(&mut self, formula: &str, context: &Option<CellReferenceRC>) -> Node {
|
||||
self.lexer.set_formula(formula);
|
||||
self.context = context.clone();
|
||||
self.parse_expr()
|
||||
}
|
||||
|
||||
fn get_sheet_index_by_name(&self, name: &str) -> Option<u32> {
|
||||
let worksheets = &self.worksheets;
|
||||
for (i, sheet) in worksheets.iter().enumerate() {
|
||||
if sheet == name {
|
||||
return Some(i as u32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn parse_expr(&mut self) -> Node {
|
||||
let mut t = self.parse_concat();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while let TokenType::Compare(op) = next_token {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_concat();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::CompareKind {
|
||||
kind: op,
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_concat(&mut self) -> Node {
|
||||
let mut t = self.parse_term();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while next_token == TokenType::And {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_term();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::OpConcatenateKind {
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_term(&mut self) -> Node {
|
||||
let mut t = self.parse_factor();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while let TokenType::Addition(op) = next_token {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_factor();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::OpSumKind {
|
||||
kind: op,
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_factor(&mut self) -> Node {
|
||||
let mut t = self.parse_prod();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while let TokenType::Product(op) = next_token {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_prod();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::OpProductKind {
|
||||
kind: op,
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_prod(&mut self) -> Node {
|
||||
let mut t = self.parse_power();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
while next_token == TokenType::Power {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_power();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
t = Node::OpPowerKind {
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_power(&mut self) -> Node {
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
let mut sign = 1;
|
||||
while let TokenType::Addition(op) = next_token {
|
||||
self.lexer.advance_token();
|
||||
if op == token::OpSum::Minus {
|
||||
sign = -sign;
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
|
||||
let mut t = self.parse_range();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
if sign == -1 {
|
||||
t = Node::UnaryKind {
|
||||
kind: token::OpUnary::Minus,
|
||||
right: Box::new(t),
|
||||
}
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
while next_token == TokenType::Percent {
|
||||
self.lexer.advance_token();
|
||||
t = Node::UnaryKind {
|
||||
kind: token::OpUnary::Percentage,
|
||||
right: Box::new(t),
|
||||
};
|
||||
next_token = self.lexer.peek_token();
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_range(&mut self) -> Node {
|
||||
let t = self.parse_primary();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let next_token = self.lexer.peek_token();
|
||||
if next_token == TokenType::Colon {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_primary();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
return Node::OpRangeKind {
|
||||
left: Box::new(t),
|
||||
right: Box::new(p),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
|
||||
fn parse_primary(&mut self) -> Node {
|
||||
let next_token = self.lexer.next_token();
|
||||
match next_token {
|
||||
TokenType::LeftParenthesis => {
|
||||
let t = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
|
||||
if let Err(err) = self.lexer.expect(TokenType::RightParenthesis) {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: err.position,
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
TokenType::Number(s) => Node::NumberKind(s),
|
||||
TokenType::String(s) => Node::StringKind(s),
|
||||
TokenType::LeftBrace => {
|
||||
let t = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return t;
|
||||
}
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
let mut args: Vec<Node> = vec![t];
|
||||
while next_token == TokenType::Semicolon {
|
||||
self.lexer.advance_token();
|
||||
let p = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return p;
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
args.push(p);
|
||||
}
|
||||
if let Err(err) = self.lexer.expect(TokenType::RightBrace) {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: err.position,
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
Node::ArrayKind(args)
|
||||
}
|
||||
TokenType::Reference {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
} => {
|
||||
let context = match &self.context {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: "Expected context for the reference".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let sheet_index = match &sheet {
|
||||
Some(name) => self.get_sheet_index_by_name(name),
|
||||
None => self.get_sheet_index_by_name(&context.sheet),
|
||||
};
|
||||
let a1_mode = self.lexer.is_a1_mode();
|
||||
let row = if absolute_row || !a1_mode {
|
||||
row
|
||||
} else {
|
||||
row - context.row
|
||||
};
|
||||
let column = if absolute_column || !a1_mode {
|
||||
column
|
||||
} else {
|
||||
column - context.column
|
||||
};
|
||||
match sheet_index {
|
||||
Some(index) => Node::ReferenceKind {
|
||||
sheet_name: sheet,
|
||||
sheet_index: index,
|
||||
row,
|
||||
column,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
},
|
||||
None => Node::WrongReferenceKind {
|
||||
sheet_name: sheet,
|
||||
row,
|
||||
column,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
},
|
||||
}
|
||||
}
|
||||
TokenType::Range { sheet, left, right } => {
|
||||
let context = match &self.context {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: "Expected context for the reference".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let sheet_index = match &sheet {
|
||||
Some(name) => self.get_sheet_index_by_name(name),
|
||||
None => self.get_sheet_index_by_name(&context.sheet),
|
||||
};
|
||||
let mut row1 = left.row;
|
||||
let mut column1 = left.column;
|
||||
let mut row2 = right.row;
|
||||
let mut column2 = right.column;
|
||||
|
||||
let mut absolute_column1 = left.absolute_column;
|
||||
let mut absolute_column2 = right.absolute_column;
|
||||
let mut absolute_row1 = left.absolute_row;
|
||||
let mut absolute_row2 = right.absolute_row;
|
||||
|
||||
if self.lexer.is_a1_mode() {
|
||||
if !left.absolute_row {
|
||||
row1 -= context.row
|
||||
};
|
||||
if !left.absolute_column {
|
||||
column1 -= context.column
|
||||
};
|
||||
if !right.absolute_row {
|
||||
row2 -= context.row
|
||||
};
|
||||
if !right.absolute_column {
|
||||
column2 -= context.column
|
||||
};
|
||||
}
|
||||
if row1 > row2 {
|
||||
(row2, row1) = (row1, row2);
|
||||
(absolute_row2, absolute_row1) = (absolute_row1, absolute_row2);
|
||||
}
|
||||
if column1 > column2 {
|
||||
(column2, column1) = (column1, column2);
|
||||
(absolute_column2, absolute_column1) = (absolute_column1, absolute_column2);
|
||||
}
|
||||
match sheet_index {
|
||||
Some(index) => Node::RangeKind {
|
||||
sheet_name: sheet,
|
||||
sheet_index: index,
|
||||
row1,
|
||||
column1,
|
||||
row2,
|
||||
column2,
|
||||
absolute_column1,
|
||||
absolute_column2,
|
||||
absolute_row1,
|
||||
absolute_row2,
|
||||
},
|
||||
None => Node::WrongRangeKind {
|
||||
sheet_name: sheet,
|
||||
row1,
|
||||
column1,
|
||||
row2,
|
||||
column2,
|
||||
absolute_column1,
|
||||
absolute_column2,
|
||||
absolute_row1,
|
||||
absolute_row2,
|
||||
},
|
||||
}
|
||||
}
|
||||
TokenType::Ident(name) => {
|
||||
let next_token = self.lexer.peek_token();
|
||||
if next_token == TokenType::LeftParenthesis {
|
||||
// It's a function call "SUM(.."
|
||||
self.lexer.advance_token();
|
||||
let args = match self.parse_function_args() {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
if let Err(err) = self.lexer.expect(TokenType::RightParenthesis) {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: err.position,
|
||||
message: err.message,
|
||||
};
|
||||
}
|
||||
if let Some(function_kind) = Function::get_function(&name) {
|
||||
return Node::FunctionKind {
|
||||
kind: function_kind,
|
||||
args,
|
||||
};
|
||||
} else {
|
||||
return Node::InvalidFunctionKind { name, args };
|
||||
}
|
||||
}
|
||||
Node::VariableKind(name)
|
||||
}
|
||||
TokenType::Error(kind) => Node::ErrorKind(kind),
|
||||
TokenType::Illegal(error) => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: error.position,
|
||||
message: error.message,
|
||||
},
|
||||
TokenType::EOF => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected end of input.".to_string(),
|
||||
},
|
||||
TokenType::Boolean(value) => Node::BooleanKind(value),
|
||||
TokenType::Compare(_) => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: 'COMPARE'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::Addition(_) => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: 'SUM'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::Product(_) => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: 'PRODUCT'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::Power => {
|
||||
// A primary Node cannot start with an operator
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: 'POWER'".to_string(),
|
||||
}
|
||||
}
|
||||
TokenType::RightParenthesis
|
||||
| TokenType::RightBracket
|
||||
| TokenType::Colon
|
||||
| TokenType::Semicolon
|
||||
| TokenType::RightBrace
|
||||
| TokenType::Comma
|
||||
| TokenType::Bang
|
||||
| TokenType::And
|
||||
| TokenType::Percent => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: format!("Unexpected token: '{}'", next_token),
|
||||
},
|
||||
TokenType::LeftBracket => Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Unexpected token: '['".to_string(),
|
||||
},
|
||||
TokenType::StructuredReference {
|
||||
table_name,
|
||||
specifier,
|
||||
table_reference,
|
||||
} => {
|
||||
// We will try to convert to a normal reference
|
||||
// table_name[column_name] => cell1:cell2
|
||||
// table_name[[#This Row], [column_name]:[column_name]] => cell1:cell2
|
||||
if let Some(context) = &self.context {
|
||||
let context_sheet_index = match self.get_sheet_index_by_name(&context.sheet) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "sheet not found".to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
// table-name => table
|
||||
let table = self.tables.get(&table_name).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"Table not found: '{table_name}' at '{}!{}{}'",
|
||||
context.sheet,
|
||||
number_to_column(context.column).expect(""),
|
||||
context.row
|
||||
)
|
||||
});
|
||||
let table_sheet_index = match self.get_sheet_index_by_name(&table.sheet_name) {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "sheet not found".to_string(),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let sheet_name = if table_sheet_index == context_sheet_index {
|
||||
None
|
||||
} else {
|
||||
Some(table.sheet_name.clone())
|
||||
};
|
||||
|
||||
// context must be with tables.reference
|
||||
let (column_start, mut row_start, column_end, mut row_end) =
|
||||
parse_range(&table.reference).expect("Failed parsing range");
|
||||
|
||||
let totals_row_count = table.totals_row_count as i32;
|
||||
let header_row_count = table.header_row_count as i32;
|
||||
row_end -= totals_row_count;
|
||||
|
||||
match specifier {
|
||||
Some(token::TableSpecifier::ThisRow) => {
|
||||
row_start = context.row;
|
||||
row_end = context.row;
|
||||
}
|
||||
Some(token::TableSpecifier::Totals) => {
|
||||
if totals_row_count != 0 {
|
||||
row_start = row_end + 1;
|
||||
row_end = row_start;
|
||||
} else {
|
||||
// Table1[#Totals] is #REF! if Table1 does not have totals
|
||||
return Node::ErrorKind(token::Error::REF);
|
||||
}
|
||||
}
|
||||
Some(token::TableSpecifier::Headers) => {
|
||||
row_end = row_start;
|
||||
}
|
||||
Some(token::TableSpecifier::Data) => {
|
||||
row_start += header_row_count;
|
||||
}
|
||||
Some(token::TableSpecifier::All) => {
|
||||
if totals_row_count != 0 {
|
||||
row_end += 1;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
// skip the headers
|
||||
row_start += header_row_count;
|
||||
}
|
||||
}
|
||||
match table_reference {
|
||||
None => {
|
||||
return Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index: table_sheet_index,
|
||||
absolute_row1: true,
|
||||
absolute_column1: true,
|
||||
row1: row_start,
|
||||
column1: column_start,
|
||||
absolute_row2: true,
|
||||
absolute_column2: true,
|
||||
row2: row_end,
|
||||
column2: column_end,
|
||||
};
|
||||
}
|
||||
Some(TableReference::ColumnReference(s)) => {
|
||||
let column_index = match get_table_column_by_name(&s, table) {
|
||||
Some(s) => s + column_start,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: format!(
|
||||
"Expecting column: {s} in table {table_name}"
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
if row_start == row_end {
|
||||
return Node::ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index: table_sheet_index,
|
||||
absolute_row: true,
|
||||
absolute_column: true,
|
||||
row: row_start,
|
||||
column: column_index,
|
||||
};
|
||||
}
|
||||
return Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index: table_sheet_index,
|
||||
absolute_row1: true,
|
||||
absolute_column1: true,
|
||||
row1: row_start,
|
||||
column1: column_index,
|
||||
absolute_row2: true,
|
||||
absolute_column2: true,
|
||||
row2: row_end,
|
||||
column2: column_index,
|
||||
};
|
||||
}
|
||||
Some(TableReference::RangeReference((left, right))) => {
|
||||
let left_column_index = match get_table_column_by_name(&left, table) {
|
||||
Some(f) => f + column_start,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: format!(
|
||||
"Expecting column: {left} in table {table_name}"
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
let right_column_index = match get_table_column_by_name(&right, table) {
|
||||
Some(f) => f + column_start,
|
||||
None => {
|
||||
return Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: self.lexer.get_position() as usize,
|
||||
message: format!(
|
||||
"Expecting column: {right} in table {table_name}"
|
||||
),
|
||||
};
|
||||
}
|
||||
};
|
||||
return Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index: table_sheet_index,
|
||||
absolute_row1: true,
|
||||
absolute_column1: true,
|
||||
row1: row_start,
|
||||
column1: left_column_index,
|
||||
absolute_row2: true,
|
||||
absolute_column2: true,
|
||||
row2: row_end,
|
||||
column2: right_column_index,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::ParseErrorKind {
|
||||
formula: self.lexer.get_formula(),
|
||||
position: 0,
|
||||
message: "Structured references not supported in R1C1 mode".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_function_args(&mut self) -> Result<Vec<Node>, Node> {
|
||||
let mut args: Vec<Node> = Vec::new();
|
||||
let mut next_token = self.lexer.peek_token();
|
||||
if next_token == TokenType::RightParenthesis {
|
||||
return Ok(args);
|
||||
}
|
||||
if self.lexer.peek_token() == TokenType::Comma {
|
||||
args.push(Node::EmptyArgKind);
|
||||
} else {
|
||||
let t = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = t {
|
||||
return Err(t);
|
||||
}
|
||||
args.push(t);
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
while next_token == TokenType::Comma {
|
||||
self.lexer.advance_token();
|
||||
if self.lexer.peek_token() == TokenType::Comma {
|
||||
args.push(Node::EmptyArgKind);
|
||||
next_token = TokenType::Comma;
|
||||
} else if self.lexer.peek_token() == TokenType::RightParenthesis {
|
||||
args.push(Node::EmptyArgKind);
|
||||
return Ok(args);
|
||||
} else {
|
||||
let p = self.parse_expr();
|
||||
if let Node::ParseErrorKind { .. } = p {
|
||||
return Err(p);
|
||||
}
|
||||
next_token = self.lexer.peek_token();
|
||||
args.push(p);
|
||||
}
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
}
|
||||
397
base/src/expressions/parser/move_formula.rs
Normal file
397
base/src/expressions/parser/move_formula.rs
Normal file
@@ -0,0 +1,397 @@
|
||||
use super::{
|
||||
stringify::{stringify_reference, DisplaceData},
|
||||
Node, Reference,
|
||||
};
|
||||
use crate::{
|
||||
constants::{LAST_COLUMN, LAST_ROW},
|
||||
expressions::token::OpUnary,
|
||||
};
|
||||
use crate::{
|
||||
expressions::types::{Area, CellReferenceRC},
|
||||
number_format::to_excel_precision_str,
|
||||
};
|
||||
|
||||
pub(crate) fn ref_is_in_area(sheet: u32, row: i32, column: i32, area: &Area) -> bool {
|
||||
if area.sheet != sheet {
|
||||
return false;
|
||||
}
|
||||
if row < area.row || row > area.row + area.height - 1 {
|
||||
return false;
|
||||
}
|
||||
if column < area.column || column > area.column + area.width - 1 {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) struct MoveContext<'a> {
|
||||
pub source_sheet_name: &'a str,
|
||||
pub row: i32,
|
||||
pub column: i32,
|
||||
pub area: &'a Area,
|
||||
pub target_sheet_name: &'a str,
|
||||
pub row_delta: i32,
|
||||
pub column_delta: i32,
|
||||
}
|
||||
|
||||
/// This implements Excel's cut && paste
|
||||
/// We are moving a formula in (row, column) to (row+row_delta, column + column_delta).
|
||||
/// All references that do not point to a cell in area will be left untouched.
|
||||
/// All references that point to a cell in area will be displaced
|
||||
pub(crate) fn move_formula(node: &Node, move_context: &MoveContext) -> String {
|
||||
to_string_moved(node, move_context)
|
||||
}
|
||||
|
||||
fn move_function(name: &str, args: &Vec<Node>, move_context: &MoveContext) -> String {
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
|
||||
} else {
|
||||
first = false;
|
||||
arguments = to_string_moved(el, move_context);
|
||||
}
|
||||
}
|
||||
format!("{}({})", name, arguments)
|
||||
}
|
||||
|
||||
fn to_string_moved(node: &Node, move_context: &MoveContext) -> String {
|
||||
use self::Node::*;
|
||||
match node {
|
||||
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
||||
NumberKind(number) => to_excel_precision_str(*number),
|
||||
StringKind(value) => format!("\"{}\"", value),
|
||||
ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
row,
|
||||
column,
|
||||
} => {
|
||||
let reference_row = if *absolute_row {
|
||||
*row
|
||||
} else {
|
||||
row + move_context.row
|
||||
};
|
||||
let reference_column = if *absolute_column {
|
||||
*column
|
||||
} else {
|
||||
column + move_context.column
|
||||
};
|
||||
|
||||
let new_row;
|
||||
let new_column;
|
||||
let mut ref_sheet_name = sheet_name;
|
||||
let source_sheet_name = &Some(move_context.source_sheet_name.to_string());
|
||||
|
||||
if ref_is_in_area(
|
||||
*sheet_index,
|
||||
reference_row,
|
||||
reference_column,
|
||||
move_context.area,
|
||||
) {
|
||||
// if the reference is in the area we are moving we want to displace the reference
|
||||
new_row = row + move_context.row_delta;
|
||||
new_column = column + move_context.column_delta;
|
||||
} else {
|
||||
// If the reference is not in the area we are moving the reference remains unchanged
|
||||
new_row = *row;
|
||||
new_column = *column;
|
||||
if move_context.target_sheet_name != move_context.source_sheet_name
|
||||
&& sheet_name.is_none()
|
||||
{
|
||||
ref_sheet_name = source_sheet_name;
|
||||
}
|
||||
};
|
||||
let context = CellReferenceRC {
|
||||
sheet: move_context.source_sheet_name.to_string(),
|
||||
column: move_context.column,
|
||||
row: move_context.row,
|
||||
};
|
||||
stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: ref_sheet_name,
|
||||
sheet_index: *sheet_index,
|
||||
absolute_row: *absolute_row,
|
||||
absolute_column: *absolute_column,
|
||||
row: new_row,
|
||||
column: new_column,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
}
|
||||
RangeKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
||||
let full_column = *absolute_column1
|
||||
&& *absolute_column2
|
||||
&& (*column1 == 1)
|
||||
&& (*column2 == LAST_COLUMN);
|
||||
|
||||
let reference_row1 = if *absolute_row1 {
|
||||
*row1
|
||||
} else {
|
||||
row1 + move_context.row
|
||||
};
|
||||
let reference_column1 = if *absolute_column1 {
|
||||
*column1
|
||||
} else {
|
||||
column1 + move_context.column
|
||||
};
|
||||
|
||||
let reference_row2 = if *absolute_row2 {
|
||||
*row2
|
||||
} else {
|
||||
row2 + move_context.row
|
||||
};
|
||||
let reference_column2 = if *absolute_column2 {
|
||||
*column2
|
||||
} else {
|
||||
column2 + move_context.column
|
||||
};
|
||||
|
||||
let new_row1;
|
||||
let new_column1;
|
||||
let new_row2;
|
||||
let new_column2;
|
||||
let mut ref_sheet_name = sheet_name;
|
||||
let source_sheet_name = &Some(move_context.source_sheet_name.to_string());
|
||||
if ref_is_in_area(
|
||||
*sheet_index,
|
||||
reference_row1,
|
||||
reference_column1,
|
||||
move_context.area,
|
||||
) && ref_is_in_area(
|
||||
*sheet_index,
|
||||
reference_row2,
|
||||
reference_column2,
|
||||
move_context.area,
|
||||
) {
|
||||
// if the whole range is inside the area we are moving we want to displace the context
|
||||
new_row1 = row1 + move_context.row_delta;
|
||||
new_column1 = column1 + move_context.column_delta;
|
||||
new_row2 = row2 + move_context.row_delta;
|
||||
new_column2 = column2 + move_context.column_delta;
|
||||
} else {
|
||||
// If the reference is not in the area we are moving the context remains unchanged
|
||||
new_row1 = *row1;
|
||||
new_column1 = *column1;
|
||||
new_row2 = *row2;
|
||||
new_column2 = *column2;
|
||||
if move_context.target_sheet_name != move_context.source_sheet_name
|
||||
&& sheet_name.is_none()
|
||||
{
|
||||
ref_sheet_name = source_sheet_name;
|
||||
}
|
||||
};
|
||||
let context = CellReferenceRC {
|
||||
sheet: move_context.source_sheet_name.to_string(),
|
||||
column: move_context.column,
|
||||
row: move_context.row,
|
||||
};
|
||||
let s1 = stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: ref_sheet_name,
|
||||
sheet_index: *sheet_index,
|
||||
absolute_row: *absolute_row1,
|
||||
absolute_column: *absolute_column1,
|
||||
row: new_row1,
|
||||
column: new_column1,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
let s2 = stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: &None,
|
||||
sheet_index: *sheet_index,
|
||||
absolute_row: *absolute_row2,
|
||||
absolute_column: *absolute_column2,
|
||||
row: new_row2,
|
||||
column: new_column2,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
WrongReferenceKind {
|
||||
sheet_name,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
row,
|
||||
column,
|
||||
} => {
|
||||
// NB: Excel does not displace wrong references but Google Docs does. We follow Excel
|
||||
let context = CellReferenceRC {
|
||||
sheet: move_context.source_sheet_name.to_string(),
|
||||
column: move_context.column,
|
||||
row: move_context.row,
|
||||
};
|
||||
// It's a wrong reference, so there is no valid `sheet_index`.
|
||||
// We don't need it, since the `sheet_index` is only used if `displace_data` is not `None`.
|
||||
// I should fix it, maybe putting the `sheet_index` inside the `displace_data`
|
||||
stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row,
|
||||
column: *column,
|
||||
absolute_row: *absolute_row,
|
||||
absolute_column: *absolute_column,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
)
|
||||
}
|
||||
WrongRangeKind {
|
||||
sheet_name,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
||||
let full_column = *absolute_column1
|
||||
&& *absolute_column2
|
||||
&& (*column1 == 1)
|
||||
&& (*column2 == LAST_COLUMN);
|
||||
|
||||
// NB: Excel does not displace wrong references but Google Docs does. We follow Excel
|
||||
let context = CellReferenceRC {
|
||||
sheet: move_context.source_sheet_name.to_string(),
|
||||
column: move_context.column,
|
||||
row: move_context.row,
|
||||
};
|
||||
let s1 = stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row1,
|
||||
column: *column1,
|
||||
absolute_row: *absolute_row1,
|
||||
absolute_column: *absolute_column1,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
let s2 = stringify_reference(
|
||||
Some(&context),
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: &None,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row2,
|
||||
column: *column2,
|
||||
absolute_row: *absolute_row2,
|
||||
absolute_column: *absolute_column2,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
OpRangeKind { left, right } => format!(
|
||||
"{}:{}",
|
||||
to_string_moved(left, move_context),
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
OpConcatenateKind { left, right } => format!(
|
||||
"{}&{}",
|
||||
to_string_moved(left, move_context),
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
OpSumKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
to_string_moved(left, move_context),
|
||||
kind,
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
OpProductKind { kind, left, right } => {
|
||||
let x = match **left {
|
||||
OpSumKind { .. } => format!("({})", to_string_moved(left, move_context)),
|
||||
CompareKind { .. } => format!("({})", to_string_moved(left, move_context)),
|
||||
_ => to_string_moved(left, move_context),
|
||||
};
|
||||
let y = match **right {
|
||||
OpSumKind { .. } => format!("({})", to_string_moved(right, move_context)),
|
||||
CompareKind { .. } => format!("({})", to_string_moved(right, move_context)),
|
||||
OpProductKind { .. } => format!("({})", to_string_moved(right, move_context)),
|
||||
UnaryKind { .. } => {
|
||||
format!("({})", to_string_moved(right, move_context))
|
||||
}
|
||||
_ => to_string_moved(right, move_context),
|
||||
};
|
||||
format!("{}{}{}", x, kind, y)
|
||||
}
|
||||
OpPowerKind { left, right } => format!(
|
||||
"{}^{}",
|
||||
to_string_moved(left, move_context),
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
InvalidFunctionKind { name, args } => move_function(name, args, move_context),
|
||||
FunctionKind { kind, args } => {
|
||||
let name = &kind.to_string();
|
||||
move_function(name, args, move_context)
|
||||
}
|
||||
ArrayKind(args) => {
|
||||
// This code is a placeholder. Arrays are not yet implemented
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!("{},{}", arguments, to_string_moved(el, move_context));
|
||||
} else {
|
||||
first = false;
|
||||
arguments = to_string_moved(el, move_context);
|
||||
}
|
||||
}
|
||||
format!("{{{}}}", arguments)
|
||||
}
|
||||
VariableKind(value) => value.to_string(),
|
||||
CompareKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
to_string_moved(left, move_context),
|
||||
kind,
|
||||
to_string_moved(right, move_context),
|
||||
),
|
||||
UnaryKind { kind, right } => match kind {
|
||||
OpUnary::Minus => format!("-{}", to_string_moved(right, move_context)),
|
||||
OpUnary::Percentage => format!("{}%", to_string_moved(right, move_context)),
|
||||
},
|
||||
ErrorKind(kind) => format!("{}", kind),
|
||||
ParseErrorKind {
|
||||
formula,
|
||||
message: _,
|
||||
position: _,
|
||||
} => formula.to_string(),
|
||||
EmptyArgKind => "".to_string(),
|
||||
}
|
||||
}
|
||||
612
base/src/expressions/parser/stringify.rs
Normal file
612
base/src/expressions/parser/stringify.rs
Normal file
@@ -0,0 +1,612 @@
|
||||
use super::{super::utils::quote_name, Node, Reference};
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::expressions::token::OpUnary;
|
||||
use crate::{expressions::types::CellReferenceRC, number_format::to_excel_precision_str};
|
||||
|
||||
pub enum DisplaceData {
|
||||
Column {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
Row {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
delta: i32,
|
||||
},
|
||||
CellHorizontal {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
CellVertical {
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
ColumnMove {
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
delta: i32,
|
||||
},
|
||||
None,
|
||||
}
|
||||
|
||||
pub fn to_rc_format(node: &Node) -> String {
|
||||
stringify(node, None, &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
pub fn to_string_displaced(
|
||||
node: &Node,
|
||||
context: &CellReferenceRC,
|
||||
displace_data: &DisplaceData,
|
||||
) -> String {
|
||||
stringify(node, Some(context), displace_data, false)
|
||||
}
|
||||
|
||||
pub fn to_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, false)
|
||||
}
|
||||
|
||||
pub fn to_excel_string(node: &Node, context: &CellReferenceRC) -> String {
|
||||
stringify(node, Some(context), &DisplaceData::None, true)
|
||||
}
|
||||
|
||||
/// Converts a local reference to a string applying some displacement if needed.
|
||||
/// It uses A1 style if context is not None. If context is None it uses R1C1 style
|
||||
/// If full_row is true then the row details will be omitted in the A1 case
|
||||
/// If full_colum is true then column details will be omitted.
|
||||
pub(crate) fn stringify_reference(
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
reference: &Reference,
|
||||
full_row: bool,
|
||||
full_column: bool,
|
||||
) -> String {
|
||||
let sheet_name = reference.sheet_name;
|
||||
let sheet_index = reference.sheet_index;
|
||||
let absolute_row = reference.absolute_row;
|
||||
let absolute_column = reference.absolute_column;
|
||||
let row = reference.row;
|
||||
let column = reference.column;
|
||||
match context {
|
||||
Some(context) => {
|
||||
let mut row = if absolute_row { row } else { row + context.row };
|
||||
let mut column = if absolute_column {
|
||||
column
|
||||
} else {
|
||||
column + context.column
|
||||
};
|
||||
match displace_data {
|
||||
DisplaceData::Row {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && !full_row {
|
||||
if *delta < 0 {
|
||||
if &row >= displace_row {
|
||||
if row < displace_row - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
row += *delta;
|
||||
}
|
||||
} else if &row >= displace_row {
|
||||
row += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::Column {
|
||||
sheet,
|
||||
column: displace_column,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && !full_column {
|
||||
if *delta < 0 {
|
||||
if &column >= displace_column {
|
||||
if column < displace_column - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
column += *delta;
|
||||
}
|
||||
} else if &column >= displace_column {
|
||||
column += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::CellHorizontal {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && displace_row == &row {
|
||||
if *delta < 0 {
|
||||
if &column >= displace_column {
|
||||
if column < displace_column - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
column += *delta;
|
||||
}
|
||||
} else if &column >= displace_column {
|
||||
column += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::CellVertical {
|
||||
sheet,
|
||||
row: displace_row,
|
||||
column: displace_column,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet && displace_column == &column {
|
||||
if *delta < 0 {
|
||||
if &row >= displace_row {
|
||||
if row < displace_row - *delta {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
row += *delta;
|
||||
}
|
||||
} else if &row >= displace_row {
|
||||
row += *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::ColumnMove {
|
||||
sheet,
|
||||
column: move_column,
|
||||
delta,
|
||||
} => {
|
||||
if sheet_index == *sheet {
|
||||
if column == *move_column {
|
||||
column += *delta;
|
||||
} else if (*delta > 0
|
||||
&& column > *move_column
|
||||
&& column <= *move_column + *delta)
|
||||
|| (*delta < 0
|
||||
&& column < *move_column
|
||||
&& column >= *move_column + *delta)
|
||||
{
|
||||
column -= *delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
DisplaceData::None => {}
|
||||
}
|
||||
if row < 1 {
|
||||
return "#REF!".to_string();
|
||||
}
|
||||
let mut row_abs = if absolute_row {
|
||||
format!("${}", row)
|
||||
} else {
|
||||
format!("{}", row)
|
||||
};
|
||||
let column = match crate::expressions::utils::number_to_column(column) {
|
||||
Some(s) => s,
|
||||
None => return "#REF!".to_string(),
|
||||
};
|
||||
let mut col_abs = if absolute_column {
|
||||
format!("${}", column)
|
||||
} else {
|
||||
column
|
||||
};
|
||||
if full_row {
|
||||
row_abs = "".to_string()
|
||||
}
|
||||
if full_column {
|
||||
col_abs = "".to_string()
|
||||
}
|
||||
match &sheet_name {
|
||||
Some(name) => {
|
||||
format!("{}!{}{}", quote_name(name), col_abs, row_abs)
|
||||
}
|
||||
None => {
|
||||
format!("{}{}", col_abs, row_abs)
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let row_abs = if absolute_row {
|
||||
format!("R{}", row)
|
||||
} else {
|
||||
format!("R[{}]", row)
|
||||
};
|
||||
let col_abs = if absolute_column {
|
||||
format!("C{}", column)
|
||||
} else {
|
||||
format!("C[{}]", column)
|
||||
};
|
||||
match &sheet_name {
|
||||
Some(name) => {
|
||||
format!("{}!{}{}", quote_name(name), row_abs, col_abs)
|
||||
}
|
||||
None => {
|
||||
format!("{}{}", row_abs, col_abs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_function(
|
||||
name: &str,
|
||||
args: &Vec<Node>,
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
use_original_name: bool,
|
||||
) -> String {
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!(
|
||||
"{},{}",
|
||||
arguments,
|
||||
stringify(el, context, displace_data, use_original_name)
|
||||
);
|
||||
} else {
|
||||
first = false;
|
||||
arguments = stringify(el, context, displace_data, use_original_name);
|
||||
}
|
||||
}
|
||||
format!("{}({})", name, arguments)
|
||||
}
|
||||
|
||||
fn stringify(
|
||||
node: &Node,
|
||||
context: Option<&CellReferenceRC>,
|
||||
displace_data: &DisplaceData,
|
||||
use_original_name: bool,
|
||||
) -> String {
|
||||
use self::Node::*;
|
||||
match node {
|
||||
BooleanKind(value) => format!("{}", value).to_ascii_uppercase(),
|
||||
NumberKind(number) => to_excel_precision_str(*number),
|
||||
StringKind(value) => format!("\"{}\"", value),
|
||||
WrongReferenceKind {
|
||||
sheet_name,
|
||||
column,
|
||||
row,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
} => stringify_reference(
|
||||
context,
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: 0,
|
||||
row: *row,
|
||||
column: *column,
|
||||
absolute_row: *absolute_row,
|
||||
absolute_column: *absolute_column,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
),
|
||||
ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
column,
|
||||
row,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
} => stringify_reference(
|
||||
context,
|
||||
displace_data,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: *sheet_index,
|
||||
row: *row,
|
||||
column: *column,
|
||||
absolute_row: *absolute_row,
|
||||
absolute_column: *absolute_column,
|
||||
},
|
||||
false,
|
||||
false,
|
||||
),
|
||||
RangeKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
// Note that open ranges SUM(A:A) or SUM(1:1) will be treated as normal ranges in the R1C1 (internal) representation
|
||||
// A:A will be R1C[0]:R1048576C[0]
|
||||
// So when we are forming the A1 range we need to strip the irrelevant information
|
||||
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
||||
let full_column = *absolute_column1
|
||||
&& *absolute_column2
|
||||
&& (*column1 == 1)
|
||||
&& (*column2 == LAST_COLUMN);
|
||||
let s1 = stringify_reference(
|
||||
context,
|
||||
displace_data,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: *sheet_index,
|
||||
row: *row1,
|
||||
column: *column1,
|
||||
absolute_row: *absolute_row1,
|
||||
absolute_column: *absolute_column1,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
let s2 = stringify_reference(
|
||||
context,
|
||||
displace_data,
|
||||
&Reference {
|
||||
sheet_name: &None,
|
||||
sheet_index: *sheet_index,
|
||||
row: *row2,
|
||||
column: *column2,
|
||||
absolute_row: *absolute_row2,
|
||||
absolute_column: *absolute_column2,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
WrongRangeKind {
|
||||
sheet_name,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
// Note that open ranges SUM(A:A) or SUM(1:1) will be treated as normal ranges in the R1C1 (internal) representation
|
||||
// A:A will be R1C[0]:R1048576C[0]
|
||||
// So when we are forming the A1 range we need to strip the irrelevant information
|
||||
let full_row = *absolute_row1 && *absolute_row2 && (*row1 == 1) && (*row2 == LAST_ROW);
|
||||
let full_column = *absolute_column1
|
||||
&& *absolute_column2
|
||||
&& (*column1 == 1)
|
||||
&& (*column2 == LAST_COLUMN);
|
||||
let s1 = stringify_reference(
|
||||
context,
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row1,
|
||||
column: *column1,
|
||||
absolute_row: *absolute_row1,
|
||||
absolute_column: *absolute_column1,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
let s2 = stringify_reference(
|
||||
context,
|
||||
&DisplaceData::None,
|
||||
&Reference {
|
||||
sheet_name: &None,
|
||||
sheet_index: 0, // HACK
|
||||
row: *row2,
|
||||
column: *column2,
|
||||
absolute_row: *absolute_row2,
|
||||
absolute_column: *absolute_column2,
|
||||
},
|
||||
full_row,
|
||||
full_column,
|
||||
);
|
||||
format!("{}:{}", s1, s2)
|
||||
}
|
||||
OpRangeKind { left, right } => format!(
|
||||
"{}:{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
OpConcatenateKind { left, right } => format!(
|
||||
"{}&{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
CompareKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
kind,
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
OpSumKind { kind, left, right } => format!(
|
||||
"{}{}{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
kind,
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
OpProductKind { kind, left, right } => {
|
||||
let x = match **left {
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(left, context, displace_data, use_original_name)
|
||||
),
|
||||
_ => stringify(left, context, displace_data, use_original_name),
|
||||
};
|
||||
let y = match **right {
|
||||
OpSumKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
CompareKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
OpProductKind { .. } => format!(
|
||||
"({})",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
_ => stringify(right, context, displace_data, use_original_name),
|
||||
};
|
||||
format!("{}{}{}", x, kind, y)
|
||||
}
|
||||
OpPowerKind { left, right } => format!(
|
||||
"{}^{}",
|
||||
stringify(left, context, displace_data, use_original_name),
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
),
|
||||
InvalidFunctionKind { name, args } => {
|
||||
format_function(name, args, context, displace_data, use_original_name)
|
||||
}
|
||||
FunctionKind { kind, args } => {
|
||||
let name = if use_original_name {
|
||||
kind.to_xlsx_string()
|
||||
} else {
|
||||
kind.to_string()
|
||||
};
|
||||
format_function(&name, args, context, displace_data, use_original_name)
|
||||
}
|
||||
ArrayKind(args) => {
|
||||
let mut first = true;
|
||||
let mut arguments = "".to_string();
|
||||
for el in args {
|
||||
if !first {
|
||||
arguments = format!(
|
||||
"{},{}",
|
||||
arguments,
|
||||
stringify(el, context, displace_data, use_original_name)
|
||||
);
|
||||
} else {
|
||||
first = false;
|
||||
arguments = stringify(el, context, displace_data, use_original_name);
|
||||
}
|
||||
}
|
||||
format!("{{{}}}", arguments)
|
||||
}
|
||||
VariableKind(value) => value.to_string(),
|
||||
UnaryKind { kind, right } => match kind {
|
||||
OpUnary::Minus => {
|
||||
format!(
|
||||
"-{}",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
)
|
||||
}
|
||||
OpUnary::Percentage => {
|
||||
format!(
|
||||
"{}%",
|
||||
stringify(right, context, displace_data, use_original_name)
|
||||
)
|
||||
}
|
||||
},
|
||||
ErrorKind(kind) => format!("{}", kind),
|
||||
ParseErrorKind {
|
||||
formula,
|
||||
position: _,
|
||||
message: _,
|
||||
} => formula.to_string(),
|
||||
EmptyArgKind => "".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn rename_sheet_in_node(node: &mut Node, sheet_index: u32, new_name: &str) {
|
||||
match node {
|
||||
// Rename
|
||||
Node::ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index: index,
|
||||
..
|
||||
} => {
|
||||
if *index == sheet_index && sheet_name.is_some() {
|
||||
*sheet_name = Some(new_name.to_owned());
|
||||
}
|
||||
}
|
||||
Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index: index,
|
||||
..
|
||||
} => {
|
||||
if *index == sheet_index && sheet_name.is_some() {
|
||||
*sheet_name = Some(new_name.to_owned());
|
||||
}
|
||||
}
|
||||
Node::WrongReferenceKind { sheet_name, .. } => {
|
||||
if let Some(name) = sheet_name {
|
||||
if name.to_uppercase() == new_name.to_uppercase() {
|
||||
*sheet_name = Some(name.to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
Node::WrongRangeKind { sheet_name, .. } => {
|
||||
if sheet_name.is_some() {
|
||||
*sheet_name = Some(new_name.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// Go next level
|
||||
Node::OpRangeKind { left, right } => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::OpConcatenateKind { left, right } => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::OpSumKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::OpProductKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::OpPowerKind { left, right } => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::FunctionKind { kind: _, args } => {
|
||||
for arg in args {
|
||||
rename_sheet_in_node(arg, sheet_index, new_name);
|
||||
}
|
||||
}
|
||||
Node::InvalidFunctionKind { name: _, args } => {
|
||||
for arg in args {
|
||||
rename_sheet_in_node(arg, sheet_index, new_name);
|
||||
}
|
||||
}
|
||||
Node::CompareKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
rename_sheet_in_node(left, sheet_index, new_name);
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
Node::UnaryKind { kind: _, right } => {
|
||||
rename_sheet_in_node(right, sheet_index, new_name);
|
||||
}
|
||||
|
||||
// Do nothing
|
||||
Node::BooleanKind(_) => {}
|
||||
Node::NumberKind(_) => {}
|
||||
Node::StringKind(_) => {}
|
||||
Node::ErrorKind(_) => {}
|
||||
Node::ParseErrorKind { .. } => {}
|
||||
Node::ArrayKind(_) => {}
|
||||
Node::VariableKind(_) => {}
|
||||
Node::EmptyArgKind => {}
|
||||
}
|
||||
}
|
||||
497
base/src/expressions/parser/test.rs
Normal file
497
base/src/expressions/parser/test.rs
Normal file
@@ -0,0 +1,497 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
use crate::expressions::parser::stringify::DisplaceData;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
use super::{
|
||||
super::parser::{
|
||||
stringify::{to_rc_format, to_string},
|
||||
Node,
|
||||
},
|
||||
stringify::to_string_displaced,
|
||||
};
|
||||
|
||||
struct Formula<'a> {
|
||||
initial: &'a str,
|
||||
expected: &'a str,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_reference() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("A2", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[1]C[0]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_absolute_column() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("$A1", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[0]C1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_absolute_row_col() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("$C$5", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R5C3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_absolute_row_col_1() {
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("$A$1", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R1C1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_simple_formula() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("C3+Sheet2!D4", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[2]C[2]+Sheet2!R[3]C[3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_boolean() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("true", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "TRUE");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_bad_formula() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("#Value", &Some(cell_reference));
|
||||
match &t {
|
||||
Node::ParseErrorKind {
|
||||
formula,
|
||||
message,
|
||||
position,
|
||||
} => {
|
||||
assert_eq!(formula, "#Value");
|
||||
assert_eq!(message, "Invalid error.");
|
||||
assert_eq!(*position, 1);
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected error in formula");
|
||||
}
|
||||
}
|
||||
assert_eq!(to_rc_format(&t), "#Value");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_bad_formula_1() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("<5", &Some(cell_reference));
|
||||
match &t {
|
||||
Node::ParseErrorKind {
|
||||
formula,
|
||||
message,
|
||||
position,
|
||||
} => {
|
||||
assert_eq!(formula, "<5");
|
||||
assert_eq!(message, "Unexpected token: 'COMPARE'");
|
||||
assert_eq!(*position, 0);
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected error in formula");
|
||||
}
|
||||
}
|
||||
assert_eq!(to_rc_format(&t), "<5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_bad_formula_2() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("*5", &Some(cell_reference));
|
||||
match &t {
|
||||
Node::ParseErrorKind {
|
||||
formula,
|
||||
message,
|
||||
position,
|
||||
} => {
|
||||
assert_eq!(formula, "*5");
|
||||
assert_eq!(message, "Unexpected token: 'PRODUCT'");
|
||||
assert_eq!(*position, 0);
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected error in formula");
|
||||
}
|
||||
}
|
||||
assert_eq!(to_rc_format(&t), "*5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_bad_formula_3() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let t = parser.parse("SUM(#VALVE!)", &Some(cell_reference));
|
||||
match &t {
|
||||
Node::ParseErrorKind {
|
||||
formula,
|
||||
message,
|
||||
position,
|
||||
} => {
|
||||
assert_eq!(formula, "SUM(#VALVE!)");
|
||||
assert_eq!(message, "Invalid error.");
|
||||
assert_eq!(*position, 5);
|
||||
}
|
||||
_ => {
|
||||
panic!("Expected error in formula");
|
||||
}
|
||||
}
|
||||
assert_eq!(to_rc_format(&t), "SUM(#VALVE!)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_formulas() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let formulas = vec![
|
||||
Formula {
|
||||
initial: "IF(C3:D4>2,B5,SUM(D1:D7))",
|
||||
expected: "IF(R[2]C[2]:R[3]C[3]>2,R[4]C[1],SUM(R[0]C[3]:R[6]C[3]))",
|
||||
},
|
||||
Formula {
|
||||
initial: "-A1",
|
||||
expected: "-R[0]C[0]",
|
||||
},
|
||||
Formula {
|
||||
initial: "#VALUE!",
|
||||
expected: "#VALUE!",
|
||||
},
|
||||
Formula {
|
||||
initial: "SUM(C3:D4)",
|
||||
expected: "SUM(R[2]C[2]:R[3]C[3])",
|
||||
},
|
||||
Formula {
|
||||
initial: "A1/(B1-C1)",
|
||||
expected: "R[0]C[0]/(R[0]C[1]-R[0]C[2])",
|
||||
},
|
||||
];
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
for formula in formulas {
|
||||
let t = parser.parse(
|
||||
formula.initial,
|
||||
&Some(CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(to_rc_format(&t), formula.expected);
|
||||
assert_eq!(to_string(&t, &cell_reference), formula.initial);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_r1c1_formulas() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
parser.set_lexer_mode(LexerMode::R1C1);
|
||||
|
||||
let formulas = vec![
|
||||
Formula {
|
||||
initial: "IF(R[2]C[2]:R[3]C[3]>2,R[4]C[1],SUM(R[0]C[3]:R[6]C[3]))",
|
||||
expected: "IF(E5:F6>2,D7,SUM(F3:F9))",
|
||||
},
|
||||
Formula {
|
||||
initial: "-R[0]C[0]",
|
||||
expected: "-C3",
|
||||
},
|
||||
Formula {
|
||||
initial: "R[1]C[-1]+1",
|
||||
expected: "B4+1",
|
||||
},
|
||||
Formula {
|
||||
initial: "#VALUE!",
|
||||
expected: "#VALUE!",
|
||||
},
|
||||
Formula {
|
||||
initial: "SUM(R[2]C[2]:R[3]C[3])",
|
||||
expected: "SUM(E5:F6)",
|
||||
},
|
||||
Formula {
|
||||
initial: "R[-3]C[0]",
|
||||
expected: "#REF!",
|
||||
},
|
||||
Formula {
|
||||
initial: "R[0]C[-3]",
|
||||
expected: "#REF!",
|
||||
},
|
||||
Formula {
|
||||
initial: "R[-2]C[-2]",
|
||||
expected: "A1",
|
||||
},
|
||||
Formula {
|
||||
initial: "SIN(R[-3]C[-3])",
|
||||
expected: "SIN(#REF!)",
|
||||
},
|
||||
];
|
||||
|
||||
// Reference cell is Sheet1!C3
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 3,
|
||||
column: 3,
|
||||
};
|
||||
for formula in formulas {
|
||||
let t = parser.parse(
|
||||
formula.initial,
|
||||
&Some(CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(to_string(&t, &cell_reference), formula.expected);
|
||||
assert_eq!(to_rc_format(&t), formula.initial);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_quotes() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("C3+'Second Sheet'!D4", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second Sheet'!R[3]C[3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_escape_quotes() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second '2' Sheet".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("C3+'Second ''2'' Sheet'!D4", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "R[2]C[2]+'Second ''2'' Sheet'!R[3]C[3]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_parenthesis() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("(C3=\"Yes\")*5", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "(R[2]C[2]=\"Yes\")*5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_excel_xlfn() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
let t = parser.parse("_xlfn.CONCAT(C3)", &Some(cell_reference));
|
||||
assert_eq!(to_rc_format(&t), "CONCAT(R[2]C[2])");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_displaced() {
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let node = parser.parse("C3", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Column {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
delta: 4,
|
||||
};
|
||||
let t = to_string_displaced(&node, context, &displace_data);
|
||||
assert_eq!(t, "G3".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_displaced_full_ranges() {
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let node = parser.parse("SUM(3:3)", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Column {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
delta: 4,
|
||||
};
|
||||
assert_eq!(
|
||||
to_string_displaced(&node, context, &displace_data),
|
||||
"SUM(3:3)".to_string()
|
||||
);
|
||||
|
||||
let node = parser.parse("SUM(D:D)", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Row {
|
||||
sheet: 0,
|
||||
row: 3,
|
||||
delta: 4,
|
||||
};
|
||||
assert_eq!(
|
||||
to_string_displaced(&node, context, &displace_data),
|
||||
"SUM(D:D)".to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_displaced_too_low() {
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let node = parser.parse("C3", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Column {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
delta: -40,
|
||||
};
|
||||
let t = to_string_displaced(&node, context, &displace_data);
|
||||
assert_eq!(t, "#REF!".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_string_displaced_too_high() {
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let node = parser.parse("C3", &Some(context.clone()));
|
||||
let displace_data = DisplaceData::Column {
|
||||
sheet: 0,
|
||||
column: 1,
|
||||
delta: 4000000,
|
||||
};
|
||||
let t = to_string_displaced(&node, context, &displace_data);
|
||||
assert_eq!(t, "#REF!".to_string());
|
||||
}
|
||||
482
base/src/expressions/parser/test_move_formula.rs
Normal file
482
base/src/expressions/parser/test_move_formula.rs
Normal file
@@ -0,0 +1,482 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::move_formula::{move_formula, MoveContext};
|
||||
use crate::expressions::types::Area;
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
#[test]
|
||||
fn test_move_formula() {
|
||||
// top left corner C2
|
||||
let row = 2;
|
||||
let column = 3;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row,
|
||||
column,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
|
||||
// formula AB31 will not change
|
||||
let node = parser.parse("AB31", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "AB31");
|
||||
|
||||
// formula $AB$31 will not change
|
||||
let node = parser.parse("AB31", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "AB31");
|
||||
|
||||
// but formula D5 will change to N15 (N = D + 10)
|
||||
let node = parser.parse("D5", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "N15");
|
||||
|
||||
// Also formula $D$5 will change to N15 (N = D + 10)
|
||||
let node = parser.parse("$D$5", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "$N$15");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_context_offset() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
|
||||
let node = parser.parse("-X9+C2%", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "-X9+M12%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_area_limits() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
|
||||
// Outside of the area. Not moved
|
||||
let node = parser.parse("B2+B3+C1+G6+H5", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "B2+B3+C1+G6+H5");
|
||||
|
||||
// In the area. Moved
|
||||
let node = parser.parse("C2+F4+F5+F6", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "M12+P14+P15+P16");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_ranges() {
|
||||
// top left corner C2
|
||||
let row = 2;
|
||||
let column = 3;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row,
|
||||
column,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
// Ranges inside the area are fully displaced (absolute or not)
|
||||
let node = parser.parse("SUM(C2:F5)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(M12:P15)");
|
||||
|
||||
let node = parser.parse("SUM($C$2:$F$5)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM($M$12:$P$15)");
|
||||
|
||||
// Ranges completely outside of the area are not touched
|
||||
let node = parser.parse("SUM(A1:B3)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(A1:B3)");
|
||||
|
||||
let node = parser.parse("SUM($A$1:$B$3)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM($A$1:$B$3)");
|
||||
|
||||
// Ranges that overlap with the area are also NOT displaced
|
||||
let node = parser.parse("SUM(A1:F5)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(A1:F5)");
|
||||
|
||||
// Ranges that contain the area are also NOT displaced
|
||||
let node = parser.parse("SUM(A1:X50)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(A1:X50)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_wrong_reference() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
// Area is C2:G5
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Wrong formulas will NOT be displaced
|
||||
let node = parser.parse("Sheet3!AB31", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "Sheet3!AB31");
|
||||
let node = parser.parse("Sheet3!$X$9", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "Sheet3!$X$9");
|
||||
|
||||
let node = parser.parse("SUM(Sheet3!D2:D3)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "SUM(Sheet3!D2:D3)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_misc() {
|
||||
// context is E4
|
||||
let row = 4;
|
||||
let column = 5;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
let worksheets = vec!["Sheet1".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row: 2,
|
||||
column: 3,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
let node = parser.parse("X9^C2-F4*H2", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "X9^M12-P14*H2");
|
||||
|
||||
let node = parser.parse("F5*(-D5)*SUM(A1, X9, $D$5)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "P15*(-N15)*SUM(A1,X9,$N$15)");
|
||||
|
||||
let node = parser.parse("IF(F5 < -D5, X9 & F5, FALSE)", &Some(context.clone()));
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet1",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(t, "IF(P15<-N15,X9&P15,FALSE)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_formula_another_sheet() {
|
||||
// top left corner C2
|
||||
let row = 2;
|
||||
let column = 3;
|
||||
let context = &CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row,
|
||||
column,
|
||||
};
|
||||
// we add two sheets and we cut/paste from Sheet1 to Sheet2
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Area is C2:F6
|
||||
let area = &Area {
|
||||
sheet: 0,
|
||||
row,
|
||||
column,
|
||||
width: 4,
|
||||
height: 5,
|
||||
};
|
||||
|
||||
// Formula AB31 and JJ3:JJ4 refers to original Sheet1!AB31 and Sheet1!JJ3:JJ4
|
||||
let node = parser.parse(
|
||||
"AB31*SUM(JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(C2:F6)",
|
||||
&Some(context.clone()),
|
||||
);
|
||||
let t = move_formula(
|
||||
&node,
|
||||
&MoveContext {
|
||||
source_sheet_name: "Sheet1",
|
||||
row,
|
||||
column,
|
||||
area,
|
||||
target_sheet_name: "Sheet2",
|
||||
row_delta: 10,
|
||||
column_delta: 10,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
t,
|
||||
"Sheet1!AB31*SUM(Sheet1!JJ3:JJ4)+SUM(Sheet2!C2:F6)*SUM(M12:P16)"
|
||||
);
|
||||
}
|
||||
102
base/src/expressions/parser/test_ranges.rs
Normal file
102
base/src/expressions/parser/test_ranges.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::lexer::LexerMode;
|
||||
|
||||
use super::super::parser::stringify::{to_rc_format, to_string};
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
struct Formula<'a> {
|
||||
formula_a1: &'a str,
|
||||
formula_r1c1: &'a str,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_formulas_with_full_ranges() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Second Sheet".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
let formulas = vec![
|
||||
Formula {
|
||||
formula_a1: "IF(C:D>2,B5,SUM(D:D))",
|
||||
formula_r1c1: "IF(R1C[2]:R1048576C[3]>2,R[4]C[1],SUM(R1C[3]:R1048576C[3]))",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "A:A",
|
||||
formula_r1c1: "R1C[0]:R1048576C[0]",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "SUM(3:3)",
|
||||
formula_r1c1: "SUM(R[2]C1:R[2]C16384)",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "SUM($3:$3)",
|
||||
formula_r1c1: "SUM(R3C1:R3C16384)",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "SUM(Sheet1!3:$3)",
|
||||
formula_r1c1: "SUM(Sheet1!R[2]C1:R3C16384)",
|
||||
},
|
||||
Formula {
|
||||
formula_a1: "SUM('Second Sheet'!C:D)",
|
||||
formula_r1c1: "SUM('Second Sheet'!R1C[2]:R1048576C[3])",
|
||||
},
|
||||
];
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
for formula in &formulas {
|
||||
let t = parser.parse(
|
||||
formula.formula_a1,
|
||||
&Some(CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(to_rc_format(&t), formula.formula_r1c1);
|
||||
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
|
||||
}
|
||||
|
||||
// Now the inverse
|
||||
parser.set_lexer_mode(LexerMode::R1C1);
|
||||
for formula in &formulas {
|
||||
let t = parser.parse(
|
||||
formula.formula_r1c1,
|
||||
&Some(CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(to_rc_format(&t), formula.formula_r1c1);
|
||||
assert_eq!(to_string(&t, &cell_reference), formula.formula_a1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_range_inverse_order() {
|
||||
let worksheets = vec!["Sheet1".to_string(), "Sheet2".to_string()];
|
||||
let mut parser = Parser::new(worksheets, HashMap::new());
|
||||
|
||||
// Reference cell is Sheet1!A1
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet1".to_string(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
};
|
||||
|
||||
// D4:C2 => C2:D4
|
||||
let t = parser.parse(
|
||||
"SUM(D4:C2)*SUM(Sheet2!D4:C20)*SUM($C$20:D4)",
|
||||
&Some(cell_reference.clone()),
|
||||
);
|
||||
assert_eq!(
|
||||
to_string(&t, &cell_reference),
|
||||
"SUM(C2:D4)*SUM(Sheet2!C4:D20)*SUM($C4:D$20)".to_string()
|
||||
);
|
||||
}
|
||||
100
base/src/expressions/parser/test_tables.rs
Normal file
100
base/src/expressions/parser/test_tables.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::expressions::parser::stringify::to_string;
|
||||
use crate::expressions::utils::{number_to_column, parse_reference_a1};
|
||||
use crate::types::{Table, TableColumn, TableStyleInfo};
|
||||
|
||||
use super::super::types::CellReferenceRC;
|
||||
use super::Parser;
|
||||
|
||||
fn create_test_table(
|
||||
table_name: &str,
|
||||
column_names: &[&str],
|
||||
cell_ref: &str,
|
||||
row_count: i32,
|
||||
) -> HashMap<String, Table> {
|
||||
let mut table = HashMap::new();
|
||||
let mut columns = Vec::new();
|
||||
for (id, name) in column_names.iter().enumerate() {
|
||||
columns.push(TableColumn {
|
||||
id: id as u32,
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
let init_cell = parse_reference_a1(cell_ref).unwrap();
|
||||
let start_row = init_cell.row;
|
||||
let start_column = number_to_column(init_cell.column).unwrap();
|
||||
let end_column = number_to_column(init_cell.column + column_names.len() as i32).unwrap();
|
||||
let end_row = start_row + row_count - 1;
|
||||
|
||||
let area_ref = format!("{start_column}{start_row}:{end_column}{end_row}");
|
||||
|
||||
table.insert(
|
||||
table_name.to_string(),
|
||||
Table {
|
||||
name: table_name.to_string(),
|
||||
display_name: table_name.to_string(),
|
||||
sheet_name: "Sheet One".to_string(),
|
||||
reference: area_ref,
|
||||
totals_row_count: 0,
|
||||
header_row_count: 1,
|
||||
header_row_dxf_id: None,
|
||||
data_dxf_id: None,
|
||||
columns,
|
||||
style_info: TableStyleInfo {
|
||||
..Default::default()
|
||||
},
|
||||
totals_row_dxf_id: None,
|
||||
has_filters: false,
|
||||
},
|
||||
);
|
||||
table
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_table() {
|
||||
let worksheets = vec!["Sheet One".to_string(), "Second Sheet".to_string()];
|
||||
|
||||
// This is a table A1:F3, the column F has a formula
|
||||
let column_names = ["Jan", "Feb", "Mar", "Apr", "Dec", "Year"];
|
||||
let row_count = 3;
|
||||
let tables = create_test_table("tblIncome", &column_names, "A1", row_count);
|
||||
|
||||
let mut parser = Parser::new(worksheets, tables);
|
||||
// Reference cell is 'Sheet One'!F2
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet One".to_string(),
|
||||
row: 2,
|
||||
column: 6,
|
||||
};
|
||||
|
||||
let formula = "SUM(tblIncome[[#This Row],[Jan]:[Dec]])";
|
||||
let t = parser.parse(formula, &Some(cell_reference.clone()));
|
||||
assert_eq!(to_string(&t, &cell_reference), "SUM($A$2:$E$2)");
|
||||
|
||||
// Cell A3
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Sheet One".to_string(),
|
||||
row: 4,
|
||||
column: 1,
|
||||
};
|
||||
let formula = "SUBTOTAL(109, tblIncome[Jan])";
|
||||
let t = parser.parse(formula, &Some(cell_reference.clone()));
|
||||
assert_eq!(to_string(&t, &cell_reference), "SUBTOTAL(109,$A$2:$A$3)");
|
||||
|
||||
// Cell A3 in 'Second Sheet'
|
||||
let cell_reference = CellReferenceRC {
|
||||
sheet: "Second Sheet".to_string(),
|
||||
row: 4,
|
||||
column: 1,
|
||||
};
|
||||
let formula = "SUBTOTAL(109, tblIncome[Jan])";
|
||||
let t = parser.parse(formula, &Some(cell_reference.clone()));
|
||||
assert_eq!(
|
||||
to_string(&t, &cell_reference),
|
||||
"SUBTOTAL(109,'Sheet One'!$A$2:$A$3)"
|
||||
);
|
||||
}
|
||||
276
base/src/expressions/parser/walk.rs
Normal file
276
base/src/expressions/parser/walk.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
use super::{move_formula::ref_is_in_area, Node};
|
||||
|
||||
use crate::expressions::types::{Area, CellReferenceIndex};
|
||||
|
||||
pub(crate) fn forward_references(
|
||||
node: &mut Node,
|
||||
context: &CellReferenceIndex,
|
||||
source_area: &Area,
|
||||
target_sheet: u32,
|
||||
target_sheet_name: &str,
|
||||
target_row: i32,
|
||||
target_column: i32,
|
||||
) {
|
||||
match node {
|
||||
Node::ReferenceKind {
|
||||
sheet_name,
|
||||
sheet_index: reference_sheet,
|
||||
absolute_row,
|
||||
absolute_column,
|
||||
row: reference_row,
|
||||
column: reference_column,
|
||||
} => {
|
||||
let reference_row_absolute = if *absolute_row {
|
||||
*reference_row
|
||||
} else {
|
||||
*reference_row + context.row
|
||||
};
|
||||
let reference_column_absolute = if *absolute_column {
|
||||
*reference_column
|
||||
} else {
|
||||
*reference_column + context.column
|
||||
};
|
||||
if ref_is_in_area(
|
||||
*reference_sheet,
|
||||
reference_row_absolute,
|
||||
reference_column_absolute,
|
||||
source_area,
|
||||
) {
|
||||
if *reference_sheet != target_sheet {
|
||||
*sheet_name = Some(target_sheet_name.to_string());
|
||||
*reference_sheet = target_sheet;
|
||||
}
|
||||
*reference_row = target_row + *reference_row - source_area.row;
|
||||
*reference_column = target_column + *reference_column - source_area.column;
|
||||
}
|
||||
}
|
||||
Node::RangeKind {
|
||||
sheet_name,
|
||||
sheet_index,
|
||||
absolute_row1,
|
||||
absolute_column1,
|
||||
row1,
|
||||
column1,
|
||||
absolute_row2,
|
||||
absolute_column2,
|
||||
row2,
|
||||
column2,
|
||||
} => {
|
||||
let reference_row1 = if *absolute_row1 {
|
||||
*row1
|
||||
} else {
|
||||
*row1 + context.row
|
||||
};
|
||||
let reference_column1 = if *absolute_column1 {
|
||||
*column1
|
||||
} else {
|
||||
*column1 + context.column
|
||||
};
|
||||
|
||||
let reference_row2 = if *absolute_row2 {
|
||||
*row2
|
||||
} else {
|
||||
*row2 + context.row
|
||||
};
|
||||
let reference_column2 = if *absolute_column2 {
|
||||
*column2
|
||||
} else {
|
||||
*column2 + context.column
|
||||
};
|
||||
if ref_is_in_area(*sheet_index, reference_row1, reference_column1, source_area)
|
||||
&& ref_is_in_area(*sheet_index, reference_row2, reference_column2, source_area)
|
||||
{
|
||||
if *sheet_index != target_sheet {
|
||||
*sheet_index = target_sheet;
|
||||
*sheet_name = Some(target_sheet_name.to_string());
|
||||
}
|
||||
*row1 = target_row + *row1 - source_area.row;
|
||||
*column1 = target_column + *column1 - source_area.column;
|
||||
*row2 = target_row + *row2 - source_area.row;
|
||||
*column2 = target_column + *column2 - source_area.column;
|
||||
}
|
||||
}
|
||||
// Recurse
|
||||
Node::OpRangeKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpConcatenateKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpSumKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpProductKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::OpPowerKind { left, right } => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::FunctionKind { kind: _, args } => {
|
||||
for arg in args {
|
||||
forward_references(
|
||||
arg,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
}
|
||||
Node::InvalidFunctionKind { name: _, args } => {
|
||||
for arg in args {
|
||||
forward_references(
|
||||
arg,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
}
|
||||
Node::CompareKind {
|
||||
kind: _,
|
||||
left,
|
||||
right,
|
||||
} => {
|
||||
forward_references(
|
||||
left,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
Node::UnaryKind { kind: _, right } => {
|
||||
forward_references(
|
||||
right,
|
||||
context,
|
||||
source_area,
|
||||
target_sheet,
|
||||
target_sheet_name,
|
||||
target_row,
|
||||
target_column,
|
||||
);
|
||||
}
|
||||
// TODO: Not implemented
|
||||
Node::ArrayKind(_) => {}
|
||||
// Do nothing. Note: we could do a blanket _ => {}
|
||||
Node::VariableKind(_) => {}
|
||||
Node::ErrorKind(_) => {}
|
||||
Node::ParseErrorKind { .. } => {}
|
||||
Node::EmptyArgKind => {}
|
||||
Node::BooleanKind(_) => {}
|
||||
Node::NumberKind(_) => {}
|
||||
Node::StringKind(_) => {}
|
||||
Node::WrongReferenceKind { .. } => {}
|
||||
Node::WrongRangeKind { .. } => {}
|
||||
}
|
||||
}
|
||||
21
base/src/expressions/test.rs
Normal file
21
base/src/expressions/test.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_error_codes() {
|
||||
let errors = vec![
|
||||
Error::REF,
|
||||
Error::NAME,
|
||||
Error::VALUE,
|
||||
Error::DIV,
|
||||
Error::NA,
|
||||
Error::NUM,
|
||||
Error::ERROR,
|
||||
];
|
||||
for (i, error) in errors.iter().enumerate() {
|
||||
let s = format!("{}", error);
|
||||
let index = error_index(s.clone()).unwrap();
|
||||
assert_eq!(i as i32, index);
|
||||
let s2 = error_string(i as usize).unwrap();
|
||||
assert_eq!(s, s2);
|
||||
}
|
||||
}
|
||||
388
base/src/expressions/token.rs
Normal file
388
base/src/expressions/token.rs
Normal file
@@ -0,0 +1,388 @@
|
||||
use std::fmt;
|
||||
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
|
||||
use crate::language::Language;
|
||||
|
||||
use super::{lexer::LexerError, types::ParsedReference};
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum OpCompare {
|
||||
LessThan,
|
||||
GreaterThan,
|
||||
Equal,
|
||||
LessOrEqualThan,
|
||||
GreaterOrEqualThan,
|
||||
NonEqual,
|
||||
}
|
||||
|
||||
impl fmt::Display for OpCompare {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
OpCompare::LessThan => write!(fmt, "<"),
|
||||
OpCompare::GreaterThan => write!(fmt, ">"),
|
||||
OpCompare::Equal => write!(fmt, "="),
|
||||
OpCompare::LessOrEqualThan => write!(fmt, "<="),
|
||||
OpCompare::GreaterOrEqualThan => write!(fmt, ">="),
|
||||
OpCompare::NonEqual => write!(fmt, "<>"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum OpUnary {
|
||||
Minus,
|
||||
Percentage,
|
||||
}
|
||||
|
||||
impl fmt::Display for OpUnary {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
OpUnary::Minus => write!(fmt, "-"),
|
||||
OpUnary::Percentage => write!(fmt, "%"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum OpSum {
|
||||
Add,
|
||||
Minus,
|
||||
}
|
||||
|
||||
impl fmt::Display for OpSum {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
OpSum::Add => write!(fmt, "+"),
|
||||
OpSum::Minus => write!(fmt, "-"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub enum OpProduct {
|
||||
Times,
|
||||
Divide,
|
||||
}
|
||||
|
||||
impl fmt::Display for OpProduct {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
OpProduct::Times => write!(fmt, "*"),
|
||||
OpProduct::Divide => write!(fmt, "/"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// List of `errors`
|
||||
/// Note that "#ERROR!" and "#N/IMPL!" are not part of the xlsx standard
|
||||
/// * "#ERROR!" means there was an error processing the formula (for instance "=A1+")
|
||||
/// * "#N/IMPL!" means the formula or feature in Excel but has not been implemented in IronCalc
|
||||
/// Note that they are serialized/deserialized by index
|
||||
#[derive(Serialize_repr, Deserialize_repr, Debug, PartialEq, Eq, Clone)]
|
||||
#[repr(u8)]
|
||||
pub enum Error {
|
||||
REF,
|
||||
NAME,
|
||||
VALUE,
|
||||
DIV,
|
||||
NA,
|
||||
NUM,
|
||||
ERROR,
|
||||
NIMPL,
|
||||
SPILL,
|
||||
CALC,
|
||||
CIRC,
|
||||
NULL,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::NULL => write!(fmt, "#NULL!"),
|
||||
Error::REF => write!(fmt, "#REF!"),
|
||||
Error::NAME => write!(fmt, "#NAME?"),
|
||||
Error::VALUE => write!(fmt, "#VALUE!"),
|
||||
Error::DIV => write!(fmt, "#DIV/0!"),
|
||||
Error::NA => write!(fmt, "#N/A"),
|
||||
Error::NUM => write!(fmt, "#NUM!"),
|
||||
Error::ERROR => write!(fmt, "#ERROR!"),
|
||||
Error::NIMPL => write!(fmt, "#N/IMPL"),
|
||||
Error::SPILL => write!(fmt, "#SPILL!"),
|
||||
Error::CALC => write!(fmt, "#CALC!"),
|
||||
Error::CIRC => write!(fmt, "#CIRC!"),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Error {
|
||||
pub fn to_localized_error_string(&self, language: &Language) -> String {
|
||||
match self {
|
||||
Error::NULL => language.errors.null.to_string(),
|
||||
Error::REF => language.errors.ref_value.to_string(),
|
||||
Error::NAME => language.errors.name.to_string(),
|
||||
Error::VALUE => language.errors.value.to_string(),
|
||||
Error::DIV => language.errors.div.to_string(),
|
||||
Error::NA => language.errors.na.to_string(),
|
||||
Error::NUM => language.errors.num.to_string(),
|
||||
Error::ERROR => language.errors.error.to_string(),
|
||||
Error::NIMPL => language.errors.nimpl.to_string(),
|
||||
Error::SPILL => language.errors.spill.to_string(),
|
||||
Error::CALC => language.errors.calc.to_string(),
|
||||
Error::CIRC => language.errors.circ.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_error_by_name(name: &str, language: &Language) -> Option<Error> {
|
||||
let errors = &language.errors;
|
||||
if name == errors.ref_value {
|
||||
return Some(Error::REF);
|
||||
} else if name == errors.name {
|
||||
return Some(Error::NAME);
|
||||
} else if name == errors.value {
|
||||
return Some(Error::VALUE);
|
||||
} else if name == errors.div {
|
||||
return Some(Error::DIV);
|
||||
} else if name == errors.na {
|
||||
return Some(Error::NA);
|
||||
} else if name == errors.num {
|
||||
return Some(Error::NUM);
|
||||
} else if name == errors.error {
|
||||
return Some(Error::ERROR);
|
||||
} else if name == errors.nimpl {
|
||||
return Some(Error::NIMPL);
|
||||
} else if name == errors.spill {
|
||||
return Some(Error::SPILL);
|
||||
} else if name == errors.calc {
|
||||
return Some(Error::CALC);
|
||||
} else if name == errors.circ {
|
||||
return Some(Error::CIRC);
|
||||
} else if name == errors.null {
|
||||
return Some(Error::NULL);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_error_by_english_name(name: &str) -> Option<Error> {
|
||||
if name == "#REF!" {
|
||||
return Some(Error::REF);
|
||||
} else if name == "#NAME?" {
|
||||
return Some(Error::NAME);
|
||||
} else if name == "#VALUE!" {
|
||||
return Some(Error::VALUE);
|
||||
} else if name == "#DIV/0!" {
|
||||
return Some(Error::DIV);
|
||||
} else if name == "#N/A" {
|
||||
return Some(Error::NA);
|
||||
} else if name == "#NUM!" {
|
||||
return Some(Error::NUM);
|
||||
} else if name == "#ERROR!" {
|
||||
return Some(Error::ERROR);
|
||||
} else if name == "#N/IMPL!" {
|
||||
return Some(Error::NIMPL);
|
||||
} else if name == "#SPILL!" {
|
||||
return Some(Error::SPILL);
|
||||
} else if name == "#CALC!" {
|
||||
return Some(Error::CALC);
|
||||
} else if name == "#CIRC!" {
|
||||
return Some(Error::CIRC);
|
||||
} else if name == "#NULL!" {
|
||||
return Some(Error::NULL);
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_english_error_string(name: &str) -> bool {
|
||||
let names = [
|
||||
"#REF!", "#NAME?", "#VALUE!", "#DIV/0!", "#N/A", "#NUM!", "#ERROR!", "#N/IMPL!", "#SPILL!",
|
||||
"#CALC!", "#CIRC!", "#NULL!",
|
||||
];
|
||||
names.iter().any(|e| *e == name)
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TableSpecifier {
|
||||
All,
|
||||
Data,
|
||||
Headers,
|
||||
ThisRow,
|
||||
Totals,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TableReference {
|
||||
ColumnReference(String),
|
||||
RangeReference((String, String)),
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum TokenType {
|
||||
Illegal(LexerError),
|
||||
EOF,
|
||||
Ident(String), // abc123
|
||||
String(String), // "A season"
|
||||
Number(f64), // 123.4
|
||||
Boolean(bool), // TRUE | FALSE
|
||||
Error(Error), // #VALUE!
|
||||
Compare(OpCompare), // <,>, ...
|
||||
Addition(OpSum), // +,-
|
||||
Product(OpProduct), // *,/
|
||||
Power, // ^
|
||||
LeftParenthesis, // (
|
||||
RightParenthesis, // )
|
||||
Colon, // :
|
||||
Semicolon, // ;
|
||||
LeftBracket, // [
|
||||
RightBracket, // ]
|
||||
LeftBrace, // {
|
||||
RightBrace, // }
|
||||
Comma, // ,
|
||||
Bang, // !
|
||||
Percent, // %
|
||||
And, // &
|
||||
Reference {
|
||||
sheet: Option<String>,
|
||||
row: i32,
|
||||
column: i32,
|
||||
absolute_column: bool,
|
||||
absolute_row: bool,
|
||||
},
|
||||
Range {
|
||||
sheet: Option<String>,
|
||||
left: ParsedReference,
|
||||
right: ParsedReference,
|
||||
},
|
||||
StructuredReference {
|
||||
table_name: String,
|
||||
specifier: Option<TableSpecifier>,
|
||||
table_reference: Option<TableReference>,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for TokenType {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::TokenType::*;
|
||||
match self {
|
||||
Illegal(_) => write!(fmt, "Illegal"),
|
||||
EOF => write!(fmt, ""),
|
||||
Ident(value) => write!(fmt, "{}", value),
|
||||
String(value) => write!(fmt, "\"{}\"", value),
|
||||
Number(value) => write!(fmt, "{}", value),
|
||||
Boolean(value) => write!(fmt, "{}", value),
|
||||
Error(value) => write!(fmt, "{}", value),
|
||||
Compare(value) => write!(fmt, "{}", value),
|
||||
Addition(value) => write!(fmt, "{}", value),
|
||||
Product(value) => write!(fmt, "{}", value),
|
||||
Power => write!(fmt, "^"),
|
||||
LeftParenthesis => write!(fmt, "("),
|
||||
RightParenthesis => write!(fmt, ")"),
|
||||
Colon => write!(fmt, ":"),
|
||||
Semicolon => write!(fmt, ";"),
|
||||
LeftBracket => write!(fmt, "["),
|
||||
RightBracket => write!(fmt, "]"),
|
||||
LeftBrace => write!(fmt, "{{"),
|
||||
RightBrace => write!(fmt, "}}"),
|
||||
Comma => write!(fmt, ","),
|
||||
Bang => write!(fmt, "!"),
|
||||
Percent => write!(fmt, "%"),
|
||||
And => write!(fmt, "&"),
|
||||
Reference {
|
||||
sheet,
|
||||
row,
|
||||
column,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
} => {
|
||||
let row_data = if *absolute_row {
|
||||
format!("{}", row)
|
||||
} else {
|
||||
format!("${}", row)
|
||||
};
|
||||
let column_data = if *absolute_column {
|
||||
format!("{}", column)
|
||||
} else {
|
||||
format!("${}", column)
|
||||
};
|
||||
match sheet {
|
||||
Some(name) => write!(fmt, "{}!{}{}", name, column_data, row_data),
|
||||
None => write!(fmt, "{}{}", column, row),
|
||||
}
|
||||
}
|
||||
Range { sheet, left, right } => {
|
||||
let row_left_data = if left.absolute_row {
|
||||
format!("{}", left.row)
|
||||
} else {
|
||||
format!("${}", left.row)
|
||||
};
|
||||
let column_left_data = if left.absolute_column {
|
||||
format!("{}", left.column)
|
||||
} else {
|
||||
format!("${}", left.column)
|
||||
};
|
||||
|
||||
let row_right_data = if right.absolute_row {
|
||||
format!("{}", right.row)
|
||||
} else {
|
||||
format!("${}", right.row)
|
||||
};
|
||||
let column_right_data = if right.absolute_column {
|
||||
format!("{}", right.column)
|
||||
} else {
|
||||
format!("${}", right.column)
|
||||
};
|
||||
match sheet {
|
||||
Some(name) => write!(
|
||||
fmt,
|
||||
"{}!{}{}:{}{}",
|
||||
name, column_left_data, row_left_data, column_right_data, row_right_data
|
||||
),
|
||||
None => write!(
|
||||
fmt,
|
||||
"{}{}:{}{}",
|
||||
left.column, left.row, right.column, right.row
|
||||
),
|
||||
}
|
||||
}
|
||||
StructuredReference {
|
||||
table_name: _,
|
||||
specifier: _,
|
||||
table_reference: _,
|
||||
} => {
|
||||
// This should never happen
|
||||
write!(fmt, "-----ERROR-----")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn index(token: &TokenType) -> u32 {
|
||||
use self::TokenType::*;
|
||||
match token {
|
||||
Illegal(..) => 1,
|
||||
EOF => 2,
|
||||
Ident(..) => 3,
|
||||
String(..) => 4,
|
||||
Number(..) => 6,
|
||||
Boolean(..) => 7,
|
||||
Error(..) => 8,
|
||||
Addition(..) => 9,
|
||||
Product(..) => 10,
|
||||
Power => 14,
|
||||
LeftParenthesis => 15,
|
||||
RightParenthesis => 16,
|
||||
Colon => 17,
|
||||
Semicolon => 18,
|
||||
LeftBracket => 19,
|
||||
RightBracket => 20,
|
||||
LeftBrace => 21,
|
||||
RightBrace => 22,
|
||||
Comma => 23,
|
||||
Bang => 24,
|
||||
Percent => 30,
|
||||
And => 31,
|
||||
Reference { .. } => 34,
|
||||
Range { .. } => 35,
|
||||
Compare(..) => 37,
|
||||
StructuredReference { .. } => 40,
|
||||
}
|
||||
}
|
||||
51
base/src/expressions/types.rs
Normal file
51
base/src/expressions/types.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// $A$34
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
|
||||
pub struct ParsedReference {
|
||||
pub column: i32,
|
||||
pub row: i32,
|
||||
pub absolute_column: bool,
|
||||
pub absolute_row: bool,
|
||||
}
|
||||
|
||||
/// If right is None it is just a reference
|
||||
/// Column ranges like D:D will have `absolute_row=true` and `left.row=1` and `right.row=LAST_ROW`
|
||||
/// Row ranges like 5:5 will have `absolute_column=true` and `left.column=1` and `right.column=LAST_COLUMN`
|
||||
pub struct ParsedRange {
|
||||
pub left: ParsedReference,
|
||||
pub right: Option<ParsedReference>,
|
||||
}
|
||||
|
||||
// FIXME: It does not make sense to have two different structures.
|
||||
// We should have a single one CellReferenceNamed or something like that.
|
||||
// Sheet1!C3
|
||||
pub struct CellReference {
|
||||
pub sheet: String,
|
||||
pub column: String,
|
||||
pub row: String,
|
||||
}
|
||||
|
||||
// Sheet1!C3 -> CellReferenceRC{Sheet1, 3, 3}
|
||||
#[derive(Clone)]
|
||||
pub struct CellReferenceRC {
|
||||
pub sheet: String,
|
||||
pub column: i32,
|
||||
pub row: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CellReferenceIndex {
|
||||
pub sheet: u32,
|
||||
pub column: i32,
|
||||
pub row: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Area {
|
||||
pub sheet: u32,
|
||||
pub row: i32,
|
||||
pub column: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
}
|
||||
281
base/src/expressions/utils/mod.rs
Normal file
281
base/src/expressions/utils/mod.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
use super::types::*;
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
/// Converts column letter identifier to number.
|
||||
pub fn column_to_number(column: &str) -> Result<i32, String> {
|
||||
if column.is_empty() {
|
||||
return Err("Column identifier cannot be empty.".to_string());
|
||||
}
|
||||
|
||||
if !column.is_ascii() {
|
||||
return Err("Column identifier must be ASCII.".to_string());
|
||||
}
|
||||
|
||||
let mut column_number = 0;
|
||||
for character in column.chars() {
|
||||
if !character.is_ascii_uppercase() {
|
||||
return Err("Column identifier can use only A-Z characters".to_string());
|
||||
}
|
||||
column_number = column_number * 26 + ((character as i32) - 64);
|
||||
}
|
||||
|
||||
match is_valid_column_number(column_number) {
|
||||
true => Ok(column_number),
|
||||
false => Err("Column is not valid.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
/// If input number is outside valid range `None` is returned.
|
||||
pub fn number_to_column(mut i: i32) -> Option<String> {
|
||||
if !is_valid_column_number(i) {
|
||||
return None;
|
||||
}
|
||||
let mut column = "".to_string();
|
||||
while i > 0 {
|
||||
let r = ((i - 1) % 26) as u8;
|
||||
column.insert(0, (65 + r) as char);
|
||||
i = (i - 1) / 26;
|
||||
}
|
||||
Some(column)
|
||||
}
|
||||
|
||||
/// Checks if column number is in valid range.
|
||||
pub fn is_valid_column_number(column: i32) -> bool {
|
||||
(1..=LAST_COLUMN).contains(&column)
|
||||
}
|
||||
|
||||
pub fn is_valid_column(column: &str) -> bool {
|
||||
// last column XFD
|
||||
if column.len() > 3 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let column_number = column_to_number(column);
|
||||
|
||||
match column_number {
|
||||
Ok(column_number) => is_valid_column_number(column_number),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_valid_row(row: i32) -> bool {
|
||||
(1..=LAST_ROW).contains(&row)
|
||||
}
|
||||
|
||||
fn is_valid_row_str(row: &str) -> bool {
|
||||
match row.parse::<i32>() {
|
||||
Ok(r) => is_valid_row(r),
|
||||
Err(_r) => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_reference_r1c1(r: &str) -> Option<ParsedReference> {
|
||||
let chars = r.as_bytes();
|
||||
let len = chars.len();
|
||||
let absolute_column;
|
||||
let absolute_row;
|
||||
let mut row = "".to_string();
|
||||
let mut column = "".to_string();
|
||||
if len < 4 {
|
||||
return None;
|
||||
}
|
||||
if chars[0] != b'R' {
|
||||
return None;
|
||||
}
|
||||
let mut i = 1;
|
||||
if chars[i] == b'[' {
|
||||
i += 1;
|
||||
absolute_row = false;
|
||||
if chars[i] == b'-' {
|
||||
i += 1;
|
||||
row.push('-');
|
||||
}
|
||||
} else {
|
||||
absolute_row = true;
|
||||
}
|
||||
while i < len {
|
||||
let ch = chars[i];
|
||||
if ch.is_ascii_digit() {
|
||||
row.push(ch as char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if !absolute_row {
|
||||
if i >= len || chars[i] != b']' {
|
||||
return None;
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
if i >= len || chars[i] != b'C' {
|
||||
return None;
|
||||
};
|
||||
i += 1;
|
||||
if i < len && chars[i] == b'[' {
|
||||
absolute_column = false;
|
||||
i += 1;
|
||||
if i < len && chars[i] == b'-' {
|
||||
i += 1;
|
||||
column.push('-');
|
||||
}
|
||||
} else {
|
||||
absolute_column = true;
|
||||
}
|
||||
while i < len {
|
||||
let ch = chars[i];
|
||||
if ch.is_ascii_digit() {
|
||||
column.push(ch as char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
if !absolute_column {
|
||||
if i >= len || chars[i] != b']' {
|
||||
return None;
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
if i != len {
|
||||
return None;
|
||||
}
|
||||
Some(ParsedReference {
|
||||
row: row.parse::<i32>().unwrap_or(0),
|
||||
column: column.parse::<i32>().unwrap_or(0),
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn parse_reference_a1(r: &str) -> Option<ParsedReference> {
|
||||
let chars = r.chars();
|
||||
let mut absolute_column = false;
|
||||
let mut absolute_row = false;
|
||||
let mut row = "".to_string();
|
||||
let mut column = "".to_string();
|
||||
let mut state = 1; // 1(colum), 2(row)
|
||||
|
||||
for ch in chars {
|
||||
match ch {
|
||||
'A'..='Z' => {
|
||||
if state == 1 {
|
||||
column.push(ch);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
'0'..='9' => {
|
||||
if state == 1 {
|
||||
state = 2
|
||||
}
|
||||
row.push(ch);
|
||||
}
|
||||
'$' => {
|
||||
if column == *"" {
|
||||
absolute_column = true;
|
||||
} else if state == 1 {
|
||||
absolute_row = true;
|
||||
state = 2;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !is_valid_column(&column) {
|
||||
return None;
|
||||
}
|
||||
if !is_valid_row_str(&row) {
|
||||
return None;
|
||||
}
|
||||
let row = match row.parse::<i32>() {
|
||||
Ok(r) => r,
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
Some(ParsedReference {
|
||||
row,
|
||||
column: column_to_number(&column).ok()?,
|
||||
absolute_column,
|
||||
absolute_row,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn is_valid_identifier(name: &str) -> bool {
|
||||
// https://support.microsoft.com/en-us/office/names-in-formulas-fc2935f9-115d-4bef-a370-3aa8bb4c91f1
|
||||
// https://github.com/MartinTrummer/excel-names/
|
||||
// NOTE: We are being much more restrictive than Excel.
|
||||
// In particular we do not support non ascii characters.
|
||||
let upper = name.to_ascii_uppercase();
|
||||
let bytes = upper.as_bytes();
|
||||
let len = bytes.len();
|
||||
if len > 255 || len == 0 {
|
||||
return false;
|
||||
}
|
||||
let first = bytes[0] as char;
|
||||
// The first character of a name must be a letter, an underscore character (_), or a backslash (\).
|
||||
if !(first.is_ascii_alphabetic() || first == '_' || first == '\\') {
|
||||
return false;
|
||||
}
|
||||
// You cannot use the uppercase and lowercase characters "C", "c", "R", or "r" as a defined name
|
||||
if len == 1 && (first == 'R' || first == 'C') {
|
||||
return false;
|
||||
}
|
||||
if upper == *"TRUE" || upper == *"FALSE" {
|
||||
return false;
|
||||
}
|
||||
if parse_reference_a1(name).is_some() {
|
||||
return false;
|
||||
}
|
||||
if parse_reference_r1c1(name).is_some() {
|
||||
return false;
|
||||
}
|
||||
let mut i = 1;
|
||||
while i < len {
|
||||
let ch = bytes[i] as char;
|
||||
match ch {
|
||||
'a'..='z' => {}
|
||||
'A'..='Z' => {}
|
||||
'0'..='9' => {}
|
||||
'_' => {}
|
||||
'.' => {}
|
||||
_ => {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn name_needs_quoting(name: &str) -> bool {
|
||||
let chars = name.chars();
|
||||
// it contains any of these characters: ()'$,;-+{} or space
|
||||
for char in chars {
|
||||
if [' ', '(', ')', '\'', '$', ',', ';', '-', '+', '{', '}'].contains(&char) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// TODO:
|
||||
// cell reference in A1 notation, e.g. B1048576 is quoted, B1048577 is not
|
||||
// cell reference in R1C1 notation, e.g. RC, RC2, R5C, R-4C, RC-8, R, C
|
||||
// integers
|
||||
false
|
||||
}
|
||||
|
||||
/// Quotes a string sheet name if it needs to
|
||||
/// NOTE: Invalid characters in a sheet name \, /, *, \[, \], :, ?
|
||||
pub fn quote_name(name: &str) -> String {
|
||||
if name_needs_quoting(name) {
|
||||
return format!("'{}'", name.replace('\'', "''"));
|
||||
};
|
||||
name.to_string()
|
||||
}
|
||||
214
base/src/expressions/utils/test.rs
Normal file
214
base/src/expressions/utils/test.rs
Normal file
@@ -0,0 +1,214 @@
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_column_to_number() {
|
||||
assert_eq!(column_to_number("A"), Ok(1));
|
||||
assert_eq!(column_to_number("Z"), Ok(26));
|
||||
assert_eq!(column_to_number("AA"), Ok(27));
|
||||
assert_eq!(column_to_number("AB"), Ok(28));
|
||||
assert_eq!(column_to_number("XFD"), Ok(16_384));
|
||||
assert_eq!(column_to_number("XFD"), Ok(LAST_COLUMN));
|
||||
|
||||
assert_eq!(
|
||||
column_to_number("XFE"),
|
||||
Err("Column is not valid.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
column_to_number(""),
|
||||
Err("Column identifier cannot be empty.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
column_to_number("💥"),
|
||||
Err("Column identifier must be ASCII.".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
column_to_number("A1"),
|
||||
Err("Column identifier can use only A-Z characters".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
column_to_number("ab"),
|
||||
Err("Column identifier can use only A-Z characters".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_column() {
|
||||
assert!(is_valid_column("A"));
|
||||
assert!(is_valid_column("AA"));
|
||||
assert!(is_valid_column("XFD"));
|
||||
|
||||
assert!(!is_valid_column("a"));
|
||||
assert!(!is_valid_column("aa"));
|
||||
assert!(!is_valid_column("xfd"));
|
||||
|
||||
assert!(!is_valid_column("1"));
|
||||
assert!(!is_valid_column("-1"));
|
||||
assert!(!is_valid_column("XFE"));
|
||||
assert!(!is_valid_column(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_number_to_column() {
|
||||
assert_eq!(number_to_column(1), Some("A".to_string()));
|
||||
assert_eq!(number_to_column(26), Some("Z".to_string()));
|
||||
assert_eq!(number_to_column(27), Some("AA".to_string()));
|
||||
assert_eq!(number_to_column(28), Some("AB".to_string()));
|
||||
assert_eq!(number_to_column(16_384), Some("XFD".to_string()));
|
||||
|
||||
assert_eq!(number_to_column(0), None);
|
||||
assert_eq!(number_to_column(16_385), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_references() {
|
||||
assert_eq!(
|
||||
parse_reference_a1("A1"),
|
||||
Some(ParsedReference {
|
||||
row: 1,
|
||||
column: 1,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_references_1() {
|
||||
assert_eq!(
|
||||
parse_reference_a1("AB$23"),
|
||||
Some(ParsedReference {
|
||||
row: 23,
|
||||
column: 28,
|
||||
absolute_column: false,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_references_2() {
|
||||
assert_eq!(
|
||||
parse_reference_a1("$AB123"),
|
||||
Some(ParsedReference {
|
||||
row: 123,
|
||||
column: 28,
|
||||
absolute_column: true,
|
||||
absolute_row: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_references_3() {
|
||||
assert_eq!(
|
||||
parse_reference_a1("$AB$123"),
|
||||
Some(ParsedReference {
|
||||
row: 123,
|
||||
column: 28,
|
||||
absolute_column: true,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("R1C1"),
|
||||
Some(ParsedReference {
|
||||
row: 1,
|
||||
column: 1,
|
||||
absolute_column: true,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references_1() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("R32C[-3]"),
|
||||
Some(ParsedReference {
|
||||
row: 32,
|
||||
column: -3,
|
||||
absolute_column: false,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references_2() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("R32C"),
|
||||
Some(ParsedReference {
|
||||
row: 32,
|
||||
column: 0,
|
||||
absolute_column: true,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references_3() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("R[-2]C[-3]"),
|
||||
Some(ParsedReference {
|
||||
row: -2,
|
||||
column: -3,
|
||||
absolute_column: false,
|
||||
absolute_row: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_r1c1_references_4() {
|
||||
assert_eq!(
|
||||
parse_reference_r1c1("RC[-3]"),
|
||||
Some(ParsedReference {
|
||||
row: 0,
|
||||
column: -3,
|
||||
absolute_column: false,
|
||||
absolute_row: true
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_names() {
|
||||
assert!(is_valid_identifier("hola1"));
|
||||
assert!(is_valid_identifier("hola_1"));
|
||||
assert!(is_valid_identifier("hola.1"));
|
||||
assert!(is_valid_identifier("sum_total_"));
|
||||
assert!(is_valid_identifier("sum.total"));
|
||||
assert!(is_valid_identifier("_hola"));
|
||||
assert!(is_valid_identifier("t"));
|
||||
assert!(is_valid_identifier("q"));
|
||||
assert!(is_valid_identifier("true_that"));
|
||||
assert!(is_valid_identifier("true1"));
|
||||
|
||||
// weird names apparently valid in Excel
|
||||
assert!(is_valid_identifier("_"));
|
||||
assert!(is_valid_identifier("\\hola1"));
|
||||
assert!(is_valid_identifier("__"));
|
||||
assert!(is_valid_identifier("_."));
|
||||
assert!(is_valid_identifier("_1"));
|
||||
assert!(is_valid_identifier("\\."));
|
||||
|
||||
// invalid
|
||||
assert!(!is_valid_identifier("true"));
|
||||
assert!(!is_valid_identifier("false"));
|
||||
assert!(!is_valid_identifier("SUM THAT"));
|
||||
assert!(!is_valid_identifier("A23"));
|
||||
assert!(!is_valid_identifier("R1C1"));
|
||||
assert!(!is_valid_identifier("R23C"));
|
||||
assert!(!is_valid_identifier("R"));
|
||||
assert!(!is_valid_identifier("c"));
|
||||
assert!(!is_valid_identifier("1true"));
|
||||
|
||||
assert!(!is_valid_identifier("test€"));
|
||||
assert!(!is_valid_identifier("truñe"));
|
||||
assert!(!is_valid_identifier("tr&ue"));
|
||||
}
|
||||
17
base/src/formatter/dates.rs
Normal file
17
base/src/formatter/dates.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use chrono::Datelike;
|
||||
use chrono::Duration;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use crate::constants::EXCEL_DATE_BASE;
|
||||
|
||||
pub fn from_excel_date(days: i64) -> NaiveDate {
|
||||
let dt = NaiveDate::from_ymd_opt(1900, 1, 1).expect("problem with chrono::NaiveDate");
|
||||
dt + Duration::days(days - 2)
|
||||
}
|
||||
|
||||
pub fn date_to_serial_number(day: u32, month: u32, year: i32) -> Result<i32, String> {
|
||||
match NaiveDate::from_ymd_opt(year, month, day) {
|
||||
Some(native_date) => Ok(native_date.num_days_from_ce() - EXCEL_DATE_BASE),
|
||||
None => Err("Out of range parameters for date".to_string()),
|
||||
}
|
||||
}
|
||||
763
base/src/formatter/format.rs
Normal file
763
base/src/formatter/format.rs
Normal file
@@ -0,0 +1,763 @@
|
||||
use chrono::Datelike;
|
||||
|
||||
use crate::{locale::Locale, number_format::to_precision};
|
||||
|
||||
use super::{
|
||||
dates::{date_to_serial_number, from_excel_date},
|
||||
parser::{ParsePart, Parser, TextToken},
|
||||
};
|
||||
|
||||
pub struct Formatted {
|
||||
pub color: Option<i32>,
|
||||
pub text: String,
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
/// Returns the vector of chars of the fractional part of a *positive* number:
|
||||
/// 3.1415926 ==> ['1', '4', '1', '5', '9', '2', '6']
|
||||
fn get_fract_part(value: f64, precision: i32) -> Vec<char> {
|
||||
let b = format!("{:.1$}", value.fract(), precision as usize)
|
||||
.chars()
|
||||
.collect::<Vec<char>>();
|
||||
let l = b.len() - 1;
|
||||
let mut last_non_zero = b.len() - 1;
|
||||
for i in 0..l {
|
||||
if b[l - i] != '0' {
|
||||
last_non_zero = l - i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if last_non_zero < 2 {
|
||||
return vec![];
|
||||
}
|
||||
b[2..last_non_zero].to_vec()
|
||||
}
|
||||
|
||||
/// Return true if we need to add a separator in position digit_index
|
||||
/// It normally happens if if digit_index -1 is 3, 6, 9,... digit_index ≡ 1 mod 3
|
||||
fn use_group_separator(use_thousands: bool, digit_index: i32, group_sizes: &str) -> bool {
|
||||
if use_thousands {
|
||||
if group_sizes == "#,##0.###" {
|
||||
if digit_index > 1 && (digit_index - 1) % 3 == 0 {
|
||||
return true;
|
||||
}
|
||||
} else if group_sizes == "#,##,##0.###"
|
||||
&& (digit_index == 3 || (digit_index > 3 && digit_index % 2 == 0))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn format_number(value_original: f64, format: &str, locale: &Locale) -> Formatted {
|
||||
let mut parser = Parser::new(format);
|
||||
parser.parse();
|
||||
let parts = parser.parts;
|
||||
// There are four parts:
|
||||
// 1) Positive numbers
|
||||
// 2) Negative numbers
|
||||
// 3) Zero
|
||||
// 4) Text
|
||||
// If you specify only one section of format code, the code in that section is used for all numbers.
|
||||
// If you specify two sections of format code, the first section of code is used
|
||||
// for positive numbers and zeros, and the second section of code is used for negative numbers.
|
||||
// When you skip code sections in your number format,
|
||||
// you must include a semicolon for each of the missing sections of code.
|
||||
// You can use the ampersand (&) text operator to join, or concatenate, two values.
|
||||
let mut value = value_original;
|
||||
let part;
|
||||
match parts.len() {
|
||||
1 => {
|
||||
part = &parts[0];
|
||||
}
|
||||
2 => {
|
||||
if value >= 0.0 {
|
||||
part = &parts[0]
|
||||
} else {
|
||||
value = -value;
|
||||
part = &parts[1];
|
||||
}
|
||||
}
|
||||
3 => {
|
||||
if value > 0.0 {
|
||||
part = &parts[0]
|
||||
} else if value < 0.0 {
|
||||
value = -value;
|
||||
part = &parts[1];
|
||||
} else {
|
||||
value = 0.0;
|
||||
part = &parts[2];
|
||||
}
|
||||
}
|
||||
4 => {
|
||||
if value > 0.0 {
|
||||
part = &parts[0]
|
||||
} else if value < 0.0 {
|
||||
value = -value;
|
||||
part = &parts[1];
|
||||
} else {
|
||||
value = 0.0;
|
||||
part = &parts[2];
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Formatted {
|
||||
text: "#VALUE!".to_owned(),
|
||||
color: None,
|
||||
error: Some("Too many parts".to_owned()),
|
||||
};
|
||||
}
|
||||
}
|
||||
match part {
|
||||
ParsePart::Error(..) => Formatted {
|
||||
text: "#VALUE!".to_owned(),
|
||||
color: None,
|
||||
error: Some("Problem parsing format string".to_owned()),
|
||||
},
|
||||
ParsePart::General(..) => {
|
||||
// FIXME: This is "General formatting"
|
||||
// We should have different codepaths for general formatting and errors
|
||||
let value_abs = value.abs();
|
||||
if (1.0e-8..1.0e+11).contains(&value_abs) {
|
||||
let mut text = format!("{:.9}", value);
|
||||
text = text.trim_end_matches('0').trim_end_matches('.').to_string();
|
||||
Formatted {
|
||||
text,
|
||||
color: None,
|
||||
error: None,
|
||||
}
|
||||
} else {
|
||||
if value_abs == 0.0 {
|
||||
return Formatted {
|
||||
text: "0".to_string(),
|
||||
color: None,
|
||||
error: None,
|
||||
};
|
||||
}
|
||||
let exponent = value_abs.log10().floor();
|
||||
value /= 10.0_f64.powf(exponent);
|
||||
let sign = if exponent < 0.0 { '-' } else { '+' };
|
||||
let s = format!("{:.5}", value);
|
||||
Formatted {
|
||||
text: format!(
|
||||
"{}E{}{:02}",
|
||||
s.trim_end_matches('0').trim_end_matches('.'),
|
||||
sign,
|
||||
exponent.abs()
|
||||
),
|
||||
color: None,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
ParsePart::Date(p) => {
|
||||
let tokens = &p.tokens;
|
||||
let mut text = "".to_string();
|
||||
if !(1.0..=2_958_465.0).contains(&value) {
|
||||
// 2_958_465 is 31 December 9999
|
||||
return Formatted {
|
||||
text: "#VALUE!".to_owned(),
|
||||
color: None,
|
||||
error: Some("Date negative or too long".to_owned()),
|
||||
};
|
||||
}
|
||||
let date = from_excel_date(value as i64);
|
||||
for token in tokens {
|
||||
match token {
|
||||
TextToken::Literal(c) => {
|
||||
text = format!("{}{}", text, c);
|
||||
}
|
||||
TextToken::Text(t) => {
|
||||
text = format!("{}{}", text, t);
|
||||
}
|
||||
TextToken::Ghost(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Spacer(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Raw => {
|
||||
text = format!("{}{}", text, value);
|
||||
}
|
||||
TextToken::Digit(_) => {}
|
||||
TextToken::Period => {}
|
||||
TextToken::Day => {
|
||||
let day = date.day() as usize;
|
||||
text = format!("{}{}", text, day);
|
||||
}
|
||||
TextToken::DayPadded => {
|
||||
let day = date.day() as usize;
|
||||
text = format!("{}{:02}", text, day);
|
||||
}
|
||||
TextToken::DayNameShort => {
|
||||
let mut day = date.weekday().number_from_monday() as usize;
|
||||
if day == 7 {
|
||||
day = 0;
|
||||
}
|
||||
text = format!("{}{}", text, &locale.dates.day_names_short[day]);
|
||||
}
|
||||
TextToken::DayName => {
|
||||
let mut day = date.weekday().number_from_monday() as usize;
|
||||
if day == 7 {
|
||||
day = 0;
|
||||
}
|
||||
text = format!("{}{}", text, &locale.dates.day_names[day]);
|
||||
}
|
||||
TextToken::Month => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{}{}", text, month);
|
||||
}
|
||||
TextToken::MonthPadded => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{}{:02}", text, month);
|
||||
}
|
||||
TextToken::MonthNameShort => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{}{}", text, &locale.dates.months_short[month - 1]);
|
||||
}
|
||||
TextToken::MonthName => {
|
||||
let month = date.month() as usize;
|
||||
text = format!("{}{}", text, &locale.dates.months[month - 1]);
|
||||
}
|
||||
TextToken::MonthLetter => {
|
||||
let month = date.month() as usize;
|
||||
let months_letter = &locale.dates.months_letter[month - 1];
|
||||
text = format!("{}{}", text, months_letter);
|
||||
}
|
||||
TextToken::YearShort => {
|
||||
text = format!("{}{}", text, date.format("%y"));
|
||||
}
|
||||
TextToken::Year => {
|
||||
text = format!("{}{}", text, date.year());
|
||||
}
|
||||
}
|
||||
}
|
||||
Formatted {
|
||||
text,
|
||||
color: p.color,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
ParsePart::Number(p) => {
|
||||
let mut text = "".to_string();
|
||||
let tokens = &p.tokens;
|
||||
value = value * 100.0_f64.powi(p.percent) / (1000.0_f64.powi(p.comma));
|
||||
// p.precision is the number of significant digits _after_ the decimal point
|
||||
value = to_precision(
|
||||
value,
|
||||
(p.precision as usize) + format!("{}", value.abs().floor()).len(),
|
||||
);
|
||||
let mut value_abs = value.abs();
|
||||
let mut exponent_part: Vec<char> = vec![];
|
||||
let mut exponent_is_negative = value_abs < 10.0;
|
||||
if p.is_scientific {
|
||||
if value_abs == 0.0 {
|
||||
exponent_part = vec!['0'];
|
||||
exponent_is_negative = false;
|
||||
} else {
|
||||
// TODO: Implement engineering formatting.
|
||||
let exponent = value_abs.log10().floor();
|
||||
exponent_part = format!("{}", exponent.abs()).chars().collect();
|
||||
value /= 10.0_f64.powf(exponent);
|
||||
value = to_precision(value, 15);
|
||||
value_abs = value.abs();
|
||||
}
|
||||
}
|
||||
let l_exp = exponent_part.len() as i32;
|
||||
let mut int_part: Vec<char> = format!("{}", value_abs.floor()).chars().collect();
|
||||
if value_abs as i64 == 0 {
|
||||
int_part = vec![];
|
||||
}
|
||||
let fract_part = get_fract_part(value_abs, p.precision);
|
||||
// ln is the number of digits of the integer part of the value
|
||||
let ln = int_part.len() as i32;
|
||||
// digit count is the number of digit tokens ('0', '?' and '#') to the left of the decimal point
|
||||
let digit_count = p.digit_count;
|
||||
// digit_index points to the digit index in value that we have already formatted
|
||||
let mut digit_index = 0;
|
||||
|
||||
let symbols = &locale.numbers.symbols;
|
||||
let group_sizes = locale.numbers.decimal_formats.standard.to_owned();
|
||||
let group_separator = symbols.group.to_owned();
|
||||
let decimal_separator = symbols.decimal.to_owned();
|
||||
// There probably are better ways to check if a number at a given precision is negative :/
|
||||
let is_negative = value < -(10.0_f64.powf(-(p.precision as f64)));
|
||||
|
||||
for token in tokens {
|
||||
match token {
|
||||
TextToken::Literal(c) => {
|
||||
text = format!("{}{}", text, c);
|
||||
}
|
||||
TextToken::Text(t) => {
|
||||
text = format!("{}{}", text, t);
|
||||
}
|
||||
TextToken::Ghost(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Spacer(_) => {
|
||||
// we just leave a whitespace
|
||||
// This is what the TEXT function does
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
TextToken::Raw => {
|
||||
text = format!("{}{}", text, value);
|
||||
}
|
||||
TextToken::Period => {
|
||||
text = format!("{}{}", text, decimal_separator);
|
||||
}
|
||||
TextToken::Digit(digit) => {
|
||||
if digit.number == 'i' {
|
||||
// 1. Integer part
|
||||
let index = digit.index;
|
||||
let number_index = ln - digit_count + index;
|
||||
if index == 0 && is_negative {
|
||||
text = format!("-{}", text);
|
||||
}
|
||||
if ln <= digit_count {
|
||||
// The number of digits is less or equal than the number of digit tokens
|
||||
// i.e. the value is 123 and the format_code is ##### (ln = 3 and digit_count = 5)
|
||||
if !(number_index < 0 && digit.kind == '#') {
|
||||
let c = if number_index < 0 {
|
||||
if digit.kind == '0' {
|
||||
'0'
|
||||
} else {
|
||||
// digit.kind = '?'
|
||||
' '
|
||||
}
|
||||
} else {
|
||||
int_part[number_index as usize]
|
||||
};
|
||||
let sep = if use_group_separator(
|
||||
p.use_thousands,
|
||||
ln - digit_index,
|
||||
&group_sizes,
|
||||
) {
|
||||
&group_separator
|
||||
} else {
|
||||
""
|
||||
};
|
||||
text = format!("{}{}{}", text, c, sep);
|
||||
}
|
||||
digit_index += 1;
|
||||
} else {
|
||||
// The number is larger than the formatting code 12345 and 0##
|
||||
// We just hit the first formatting digit (0 in the example above) so we write as many digits as we can (123 in the example)
|
||||
for i in digit_index..number_index + 1 {
|
||||
let sep = if use_group_separator(
|
||||
p.use_thousands,
|
||||
ln - i,
|
||||
&group_sizes,
|
||||
) {
|
||||
&group_separator
|
||||
} else {
|
||||
""
|
||||
};
|
||||
text = format!("{}{}{}", text, int_part[i as usize], sep);
|
||||
}
|
||||
digit_index = number_index + 1;
|
||||
}
|
||||
} else if digit.number == 'd' {
|
||||
// 2. After the decimal point
|
||||
let index = digit.index as usize;
|
||||
if index < fract_part.len() {
|
||||
text = format!("{}{}", text, fract_part[index]);
|
||||
} else if digit.kind == '0' {
|
||||
text = format!("{}0", text);
|
||||
} else if digit.kind == '?' {
|
||||
text = format!("{} ", text);
|
||||
}
|
||||
} else if digit.number == 'e' {
|
||||
// 3. Exponent part
|
||||
let index = digit.index;
|
||||
if index == 0 {
|
||||
if exponent_is_negative {
|
||||
text = format!("{}E-", text);
|
||||
} else {
|
||||
text = format!("{}E+", text);
|
||||
}
|
||||
}
|
||||
let number_index = l_exp - (p.exponent_digit_count - index);
|
||||
if l_exp <= p.exponent_digit_count {
|
||||
if !(number_index < 0 && digit.kind == '#') {
|
||||
let c = if number_index < 0 {
|
||||
if digit.kind == '?' {
|
||||
' '
|
||||
} else {
|
||||
'0'
|
||||
}
|
||||
} else {
|
||||
exponent_part[number_index as usize]
|
||||
};
|
||||
|
||||
text = format!("{}{}", text, c);
|
||||
}
|
||||
} else {
|
||||
for i in 0..number_index + 1 {
|
||||
text = format!("{}{}", text, exponent_part[i as usize]);
|
||||
}
|
||||
digit_index += number_index + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Date tokens should not be present
|
||||
TextToken::Day => {}
|
||||
TextToken::DayPadded => {}
|
||||
TextToken::DayNameShort => {}
|
||||
TextToken::DayName => {}
|
||||
TextToken::Month => {}
|
||||
TextToken::MonthPadded => {}
|
||||
TextToken::MonthNameShort => {}
|
||||
TextToken::MonthName => {}
|
||||
TextToken::MonthLetter => {}
|
||||
TextToken::YearShort => {}
|
||||
TextToken::Year => {}
|
||||
}
|
||||
}
|
||||
Formatted {
|
||||
text,
|
||||
color: p.color,
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_day(day_str: &str) -> Result<(u32, String), String> {
|
||||
let bytes = day_str.bytes();
|
||||
let bytes_len = bytes.len();
|
||||
if bytes_len <= 2 {
|
||||
match day_str.parse::<u32>() {
|
||||
Ok(y) => {
|
||||
if bytes_len == 2 {
|
||||
return Ok((y, "dd".to_string()));
|
||||
} else {
|
||||
return Ok((y, "d".to_string()));
|
||||
}
|
||||
}
|
||||
Err(_) => return Err("Not a valid year".to_string()),
|
||||
}
|
||||
}
|
||||
Err("Not a valid day".to_string())
|
||||
}
|
||||
|
||||
fn parse_month(month_str: &str) -> Result<(u32, String), String> {
|
||||
let bytes = month_str.bytes();
|
||||
let bytes_len = bytes.len();
|
||||
if bytes_len <= 2 {
|
||||
match month_str.parse::<u32>() {
|
||||
Ok(y) => {
|
||||
if bytes_len == 2 {
|
||||
return Ok((y, "mm".to_string()));
|
||||
} else {
|
||||
return Ok((y, "m".to_string()));
|
||||
}
|
||||
}
|
||||
Err(_) => return Err("Not a valid year".to_string()),
|
||||
}
|
||||
}
|
||||
let month_names_short = [
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec",
|
||||
];
|
||||
let month_names_long = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
];
|
||||
if let Some(m) = month_names_short.iter().position(|&r| r == month_str) {
|
||||
return Ok((m as u32 + 1, "mmm".to_string()));
|
||||
}
|
||||
if let Some(m) = month_names_long.iter().position(|&r| r == month_str) {
|
||||
return Ok((m as u32 + 1, "mmmm".to_string()));
|
||||
}
|
||||
Err("Not a valid day".to_string())
|
||||
}
|
||||
|
||||
fn parse_year(year_str: &str) -> Result<(i32, String), String> {
|
||||
// year is either 2 digits or 4 digits
|
||||
// 23 -> 2023
|
||||
// 75 -> 1975
|
||||
// 30 is the split number (yeah, that's not going to be a problem any time soon)
|
||||
// 30 => 1930
|
||||
// 29 => 2029
|
||||
let bytes = year_str.bytes();
|
||||
let bytes_len = bytes.len();
|
||||
if bytes_len != 2 && bytes_len != 4 {
|
||||
return Err("Not a valid year".to_string());
|
||||
}
|
||||
match year_str.parse::<i32>() {
|
||||
Ok(y) => {
|
||||
if y < 30 {
|
||||
Ok((2000 + y, "yy".to_string()))
|
||||
} else if y < 100 {
|
||||
Ok((1900 + y, "yy".to_string()))
|
||||
} else {
|
||||
Ok((y, "yyyy".to_string()))
|
||||
}
|
||||
}
|
||||
Err(_) => Err("Not a valid year".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it is a date. Other spreadsheet engines support a wide variety of dates formats
|
||||
// Here we support a small subset of them.
|
||||
//
|
||||
// The grammar is:
|
||||
//
|
||||
// date -> long_date | short_date | iso-date
|
||||
// short_date -> month separator year
|
||||
// long_date -> day separator month separator year
|
||||
// iso_date -> long_year separator number_month separator number_day
|
||||
// separator -> "/" | "-"
|
||||
// day -> number | padded number
|
||||
// month -> number_month | name_month
|
||||
// number_month -> number | padded number |
|
||||
// name_month -> short name | full name
|
||||
// year -> short_year | long year
|
||||
//
|
||||
// NOTE 1: The separator has to be the same
|
||||
// NOTE 2: In some engines "2/3" is implemented ad "2/March of the present year"
|
||||
// NOTE 3: I did not implement the "short date"
|
||||
fn parse_date(value: &str) -> Result<(i32, String), String> {
|
||||
let separator = if value.contains('/') {
|
||||
'/'
|
||||
} else if value.contains('-') {
|
||||
'-'
|
||||
} else {
|
||||
return Err("Not a valid date".to_string());
|
||||
};
|
||||
|
||||
let parts: Vec<&str> = value.split(separator).collect();
|
||||
let mut is_iso_date = false;
|
||||
let (day_str, month_str, year_str) = if parts.len() == 3 {
|
||||
if parts[0].len() == 4 {
|
||||
// ISO date yyyy-mm-dd
|
||||
if !parts[1].chars().all(char::is_numeric) {
|
||||
return Err("Not a valid date".to_string());
|
||||
}
|
||||
if !parts[2].chars().all(char::is_numeric) {
|
||||
return Err("Not a valid date".to_string());
|
||||
}
|
||||
is_iso_date = true;
|
||||
(parts[2], parts[1], parts[0])
|
||||
} else {
|
||||
(parts[0], parts[1], parts[2])
|
||||
}
|
||||
} else {
|
||||
return Err("Not a valid date".to_string());
|
||||
};
|
||||
let (day, day_format) = parse_day(day_str)?;
|
||||
let (month, month_format) = parse_month(month_str)?;
|
||||
let (year, year_format) = parse_year(year_str)?;
|
||||
let serial_number = match date_to_serial_number(day, month, year) {
|
||||
Ok(n) => n,
|
||||
Err(_) => return Err("Not a valid date".to_string()),
|
||||
};
|
||||
if is_iso_date {
|
||||
Ok((
|
||||
serial_number,
|
||||
format!("yyyy{separator}{month_format}{separator}{day_format}"),
|
||||
))
|
||||
} else {
|
||||
Ok((
|
||||
serial_number,
|
||||
format!("{day_format}{separator}{month_format}{separator}{year_format}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a formatted number, returning the numeric value together with the format
|
||||
/// Uses heuristics to guess the format string
|
||||
/// "$ 123,345.678" => (123345.678, "$#,##0.00")
|
||||
/// "30.34%" => (0.3034, "0.00%")
|
||||
/// 100€ => (100, "100€")
|
||||
pub(crate) fn parse_formatted_number(
|
||||
value: &str,
|
||||
currencies: &[&str],
|
||||
) -> Result<(f64, Option<String>), String> {
|
||||
let value = value.trim();
|
||||
let scientific_format = "0.00E+00";
|
||||
|
||||
// Check if it is a percentage
|
||||
if let Some(p) = value.strip_suffix('%') {
|
||||
let (f, options) = parse_number(p.trim())?;
|
||||
if options.is_scientific {
|
||||
return Ok((f / 100.0, Some(scientific_format.to_string())));
|
||||
}
|
||||
// We ignore the separator
|
||||
if options.decimal_digits > 0 {
|
||||
// Percentage format with decimals
|
||||
return Ok((f / 100.0, Some("#,##0.00%".to_string())));
|
||||
}
|
||||
// Percentage format standard
|
||||
return Ok((f / 100.0, Some("#,##0%".to_string())));
|
||||
}
|
||||
|
||||
// check if it is a currency in currencies
|
||||
for currency in currencies {
|
||||
if let Some(p) = value.strip_prefix(&format!("-{}", currency)) {
|
||||
let (f, options) = parse_number(p.trim())?;
|
||||
if options.is_scientific {
|
||||
return Ok((f, Some(scientific_format.to_string())));
|
||||
}
|
||||
if options.decimal_digits > 0 {
|
||||
return Ok((-f, Some(format!("{currency}#,##0.00"))));
|
||||
}
|
||||
return Ok((-f, Some(format!("{currency}#,##0"))));
|
||||
} else if let Some(p) = value.strip_prefix(currency) {
|
||||
let (f, options) = parse_number(p.trim())?;
|
||||
if options.is_scientific {
|
||||
return Ok((f, Some(scientific_format.to_string())));
|
||||
}
|
||||
if options.decimal_digits > 0 {
|
||||
return Ok((f, Some(format!("{currency}#,##0.00"))));
|
||||
}
|
||||
return Ok((f, Some(format!("{currency}#,##0"))));
|
||||
} else if let Some(p) = value.strip_suffix(currency) {
|
||||
let (f, options) = parse_number(p.trim())?;
|
||||
if options.is_scientific {
|
||||
return Ok((f, Some(scientific_format.to_string())));
|
||||
}
|
||||
if options.decimal_digits > 0 {
|
||||
let currency_format = &format!("#,##0.00{currency}");
|
||||
return Ok((f, Some(currency_format.to_string())));
|
||||
}
|
||||
let currency_format = &format!("#,##0{currency}");
|
||||
return Ok((f, Some(currency_format.to_string())));
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok((serial_number, format)) = parse_date(value) {
|
||||
return Ok((serial_number as f64, Some(format)));
|
||||
}
|
||||
|
||||
// Lastly we check if it is a number
|
||||
let (f, options) = parse_number(value)?;
|
||||
if options.is_scientific {
|
||||
return Ok((f, Some(scientific_format.to_string())));
|
||||
}
|
||||
if options.has_commas {
|
||||
if options.decimal_digits > 0 {
|
||||
// group separator and two decimal points
|
||||
return Ok((f, Some("#,##0.00".to_string())));
|
||||
}
|
||||
// Group separator and no decimal points
|
||||
return Ok((f, Some("#,##0".to_string())));
|
||||
}
|
||||
Ok((f, None))
|
||||
}
|
||||
|
||||
struct NumberOptions {
|
||||
has_commas: bool,
|
||||
is_scientific: bool,
|
||||
decimal_digits: usize,
|
||||
}
|
||||
|
||||
// tries to parse 'value' as a number.
|
||||
// If it is a number it either uses commas as thousands separator or it does not
|
||||
fn parse_number(value: &str) -> Result<(f64, NumberOptions), String> {
|
||||
let mut position = 0;
|
||||
let bytes = value.as_bytes();
|
||||
let len = bytes.len();
|
||||
if len == 0 {
|
||||
return Err("Cannot parse number".to_string());
|
||||
}
|
||||
let mut chars = String::from("");
|
||||
let decimal_separator = b'.';
|
||||
let group_separator = b',';
|
||||
let mut group_separator_index = Vec::new();
|
||||
// get the sign
|
||||
let sign = if bytes[0] == b'-' {
|
||||
position += 1;
|
||||
-1.0
|
||||
} else if bytes[0] == b'+' {
|
||||
position += 1;
|
||||
1.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
// numbers before the decimal point
|
||||
while position < len {
|
||||
let x = bytes[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x as char);
|
||||
} else if x == group_separator {
|
||||
group_separator_index.push(chars.len());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
// Check the group separator is in multiples of three
|
||||
for index in &group_separator_index {
|
||||
if (chars.len() - index) % 3 != 0 {
|
||||
return Err("Cannot parse number".to_string());
|
||||
}
|
||||
}
|
||||
let mut decimal_digits = 0;
|
||||
if position < len && bytes[position] == decimal_separator {
|
||||
// numbers after the decimal point
|
||||
chars.push('.');
|
||||
position += 1;
|
||||
let start_position = 0;
|
||||
while position < len {
|
||||
let x = bytes[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x as char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
decimal_digits = position - start_position;
|
||||
}
|
||||
let mut is_scientific = false;
|
||||
if position + 1 < len && (bytes[position] == b'e' || bytes[position] == b'E') {
|
||||
// exponential side
|
||||
is_scientific = true;
|
||||
let x = bytes[position + 1];
|
||||
if x == b'-' || x == b'+' || x.is_ascii_digit() {
|
||||
chars.push('e');
|
||||
chars.push(x as char);
|
||||
position += 2;
|
||||
while position < len {
|
||||
let x = bytes[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x as char);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if position != len {
|
||||
return Err("Could not parse number".to_string());
|
||||
};
|
||||
match chars.parse::<f64>() {
|
||||
Err(_) => Err("Failed to parse to double".to_string()),
|
||||
Ok(v) => Ok((
|
||||
sign * v,
|
||||
NumberOptions {
|
||||
has_commas: !group_separator_index.is_empty(),
|
||||
is_scientific,
|
||||
decimal_digits,
|
||||
},
|
||||
)),
|
||||
}
|
||||
}
|
||||
408
base/src/formatter/lexer.rs
Normal file
408
base/src/formatter/lexer.rs
Normal file
@@ -0,0 +1,408 @@
|
||||
pub struct Lexer {
|
||||
position: usize,
|
||||
len: usize,
|
||||
chars: Vec<char>,
|
||||
error_message: String,
|
||||
error_position: usize,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum Token {
|
||||
Color(i32), // [Red] or [Color 23]
|
||||
Condition(Compare, f64), // [<=100] (Comparator, number)
|
||||
Literal(char), // €, $, (, ), /, :, +, -, ^, ', {, }, <, =, !, ~, > and space or scaped \X
|
||||
Spacer(char), // *X
|
||||
Ghost(char), // _X
|
||||
Text(String), // "Text"
|
||||
Separator, // ;
|
||||
Raw, // @
|
||||
Percent, // %
|
||||
Comma, // ,
|
||||
Period, // .
|
||||
Sharp, // #
|
||||
Zero, // 0
|
||||
QuestionMark, // ?
|
||||
Scientific, // E+
|
||||
ScientificMinus, // E-
|
||||
General, // General
|
||||
// Dates
|
||||
Day, // d
|
||||
DayPadded, // dd
|
||||
DayNameShort, // ddd
|
||||
DayName, // dddd+
|
||||
Month, // m
|
||||
MonthPadded, // mm
|
||||
MonthNameShort, // mmm
|
||||
MonthName, // mmmm or mmmmmm+
|
||||
MonthLetter, // mmmmm
|
||||
YearShort, // y or yy
|
||||
Year, // yyy+
|
||||
// TODO: Hours Minutes and Seconds
|
||||
ILLEGAL,
|
||||
EOF,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub enum Compare {
|
||||
Equal,
|
||||
LessThan,
|
||||
GreaterThan,
|
||||
LessOrEqualThan,
|
||||
GreaterOrEqualThan,
|
||||
}
|
||||
|
||||
impl Token {
|
||||
pub fn is_digit(&self) -> bool {
|
||||
(self == &Token::Zero) || (self == &Token::Sharp) || (self == &Token::QuestionMark)
|
||||
}
|
||||
|
||||
pub fn is_date(&self) -> bool {
|
||||
self == &Token::Day
|
||||
|| self == &Token::DayPadded
|
||||
|| self == &Token::DayNameShort
|
||||
|| self == &Token::DayName
|
||||
|| self == &Token::MonthName
|
||||
|| self == &Token::MonthNameShort
|
||||
|| self == &Token::Month
|
||||
|| self == &Token::MonthPadded
|
||||
|| self == &Token::MonthLetter
|
||||
|| self == &Token::YearShort
|
||||
|| self == &Token::Year
|
||||
}
|
||||
}
|
||||
|
||||
impl Lexer {
|
||||
pub fn new(format: &str) -> Lexer {
|
||||
let chars: Vec<char> = format.chars().collect();
|
||||
let len = chars.len();
|
||||
Lexer {
|
||||
chars,
|
||||
position: 0,
|
||||
len,
|
||||
error_message: "".to_string(),
|
||||
error_position: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn peek_char(&mut self) -> Option<char> {
|
||||
let position = self.position;
|
||||
if position < self.len {
|
||||
Some(self.chars[position])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn read_next_char(&mut self) -> Option<char> {
|
||||
let position = self.position;
|
||||
if position < self.len {
|
||||
self.position = position + 1;
|
||||
Some(self.chars[position])
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn set_error(&mut self, error: &str) {
|
||||
self.error_message = error.to_string();
|
||||
self.error_position = self.position;
|
||||
self.position = self.len;
|
||||
}
|
||||
|
||||
fn consume_string(&mut self) -> Option<String> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = "".to_string();
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
position += 1;
|
||||
if x != '"' {
|
||||
chars.push(x);
|
||||
} else if position < len && self.chars[position] == '"' {
|
||||
chars.push(x);
|
||||
chars.push(self.chars[position]);
|
||||
position += 1;
|
||||
} else {
|
||||
self.position = position;
|
||||
return Some(chars);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn consume_number(&mut self) -> Option<f64> {
|
||||
let mut position = self.position;
|
||||
let len = self.len;
|
||||
let mut chars = "".to_string();
|
||||
// numbers before the '.'
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
if position < len && self.chars[position] == '.' {
|
||||
// numbers after the'.'
|
||||
chars.push('.');
|
||||
position += 1;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
if position + 1 < len && self.chars[position] == 'e' {
|
||||
// exponential side
|
||||
let x = self.chars[position + 1];
|
||||
if x == '-' || x == '+' || x.is_ascii_digit() {
|
||||
chars.push('e');
|
||||
chars.push(x);
|
||||
position += 2;
|
||||
while position < len {
|
||||
let x = self.chars[position];
|
||||
if x.is_ascii_digit() {
|
||||
chars.push(x);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
position += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.position = position;
|
||||
match chars.parse::<f64>() {
|
||||
Err(_) => None,
|
||||
Ok(v) => Some(v),
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_condition(&mut self) -> Option<(Compare, f64)> {
|
||||
let cmp;
|
||||
match self.read_next_char() {
|
||||
Some('<') => {
|
||||
if let Some('=') = self.peek_char() {
|
||||
self.read_next_char();
|
||||
cmp = Compare::LessOrEqualThan;
|
||||
} else {
|
||||
cmp = Compare::LessThan;
|
||||
}
|
||||
}
|
||||
Some('>') => {
|
||||
if let Some('=') = self.peek_char() {
|
||||
self.read_next_char();
|
||||
cmp = Compare::GreaterOrEqualThan;
|
||||
} else {
|
||||
cmp = Compare::GreaterThan;
|
||||
}
|
||||
}
|
||||
Some('=') => {
|
||||
cmp = Compare::Equal;
|
||||
}
|
||||
_ => {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
if let Some(v) = self.consume_number() {
|
||||
return Some((cmp, v));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn consume_color(&mut self) -> Option<i32> {
|
||||
let colors = [
|
||||
"black", "white", "red", "green", "blue", "yellow", "magenta",
|
||||
];
|
||||
let mut chars = "".to_string();
|
||||
while let Some(ch) = self.read_next_char() {
|
||||
if ch == ']' {
|
||||
if let Some(index) = colors.iter().position(|&x| x == chars.to_lowercase()) {
|
||||
return Some(index as i32);
|
||||
}
|
||||
if !chars.starts_with("Color") {
|
||||
return None;
|
||||
}
|
||||
if let Ok(index) = chars[5..].trim().parse::<i32>() {
|
||||
if index < 57 && index > 0 {
|
||||
return Some(index);
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
return None;
|
||||
} else {
|
||||
chars.push(ch);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn peek_token(&mut self) -> Token {
|
||||
let position = self.position;
|
||||
let token = self.next_token();
|
||||
self.position = position;
|
||||
token
|
||||
}
|
||||
|
||||
pub fn next_token(&mut self) -> Token {
|
||||
let ch = self.read_next_char();
|
||||
match ch {
|
||||
Some(x) => match x {
|
||||
'$' | '€' | '(' | ')' | '/' | ':' | '+' | '-' | '^' | '\'' | '{' | '}' | '<'
|
||||
| '=' | '!' | '~' | '>' | ' ' => Token::Literal(x),
|
||||
'?' => Token::QuestionMark,
|
||||
';' => Token::Separator,
|
||||
'#' => Token::Sharp,
|
||||
',' => Token::Comma,
|
||||
'.' => Token::Period,
|
||||
'0' => Token::Zero,
|
||||
'@' => Token::Raw,
|
||||
'%' => Token::Percent,
|
||||
'[' => {
|
||||
if let Some(c) = self.peek_char() {
|
||||
if c == '<' || c == '>' || c == '=' {
|
||||
// Condition
|
||||
if let Some((cmp, value)) = self.consume_condition() {
|
||||
Token::Condition(cmp, value)
|
||||
} else {
|
||||
self.set_error("Failed to parse condition");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
} else {
|
||||
// Color
|
||||
if let Some(index) = self.consume_color() {
|
||||
return Token::Color(index);
|
||||
}
|
||||
self.set_error("Failed to parse color");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'_' => {
|
||||
if let Some(y) = self.read_next_char() {
|
||||
Token::Ghost(y)
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'*' => {
|
||||
if let Some(y) = self.read_next_char() {
|
||||
Token::Spacer(y)
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'\\' => {
|
||||
if let Some(y) = self.read_next_char() {
|
||||
Token::Literal(y)
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'"' => {
|
||||
if let Some(s) = self.consume_string() {
|
||||
Token::Text(s)
|
||||
} else {
|
||||
self.set_error("Did not find end of text string");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'E' => {
|
||||
if let Some(s) = self.read_next_char() {
|
||||
if s == '+' {
|
||||
Token::Scientific
|
||||
} else if s == '-' {
|
||||
Token::ScientificMinus
|
||||
} else {
|
||||
self.set_error(&format!("Unexpected char: {}. Expected + or -", s));
|
||||
Token::ILLEGAL
|
||||
}
|
||||
} else {
|
||||
self.set_error("Unexpected end of input");
|
||||
Token::ILLEGAL
|
||||
}
|
||||
}
|
||||
'd' => {
|
||||
let mut d = 1;
|
||||
while let Some('d') = self.peek_char() {
|
||||
d += 1;
|
||||
self.read_next_char();
|
||||
}
|
||||
match d {
|
||||
1 => Token::Day,
|
||||
2 => Token::DayPadded,
|
||||
3 => Token::DayNameShort,
|
||||
_ => Token::DayName,
|
||||
}
|
||||
}
|
||||
'm' => {
|
||||
let mut m = 1;
|
||||
while let Some('m') = self.peek_char() {
|
||||
m += 1;
|
||||
self.read_next_char();
|
||||
}
|
||||
match m {
|
||||
1 => Token::Month,
|
||||
2 => Token::MonthPadded,
|
||||
3 => Token::MonthNameShort,
|
||||
4 => Token::MonthName,
|
||||
5 => Token::MonthLetter,
|
||||
_ => Token::MonthName,
|
||||
}
|
||||
}
|
||||
'y' => {
|
||||
let mut y = 1;
|
||||
while let Some('y') = self.peek_char() {
|
||||
y += 1;
|
||||
self.read_next_char();
|
||||
}
|
||||
if y == 1 || y == 2 {
|
||||
Token::YearShort
|
||||
} else {
|
||||
Token::Year
|
||||
}
|
||||
}
|
||||
'g' | 'G' => {
|
||||
for c in "eneral".chars() {
|
||||
let cc = self.read_next_char();
|
||||
if Some(c) != cc {
|
||||
self.set_error(&format!("Unexpected character: {}", x));
|
||||
return Token::ILLEGAL;
|
||||
}
|
||||
}
|
||||
Token::General
|
||||
}
|
||||
_ => {
|
||||
self.set_error(&format!("Unexpected character: {}", x));
|
||||
Token::ILLEGAL
|
||||
}
|
||||
},
|
||||
None => Token::EOF,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_likely_date_number_format(format: &str) -> bool {
|
||||
let mut lexer = Lexer::new(format);
|
||||
loop {
|
||||
let token = lexer.next_token();
|
||||
if token == Token::EOF {
|
||||
return false;
|
||||
}
|
||||
if token.is_date() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
105
base/src/formatter/mod.rs
Normal file
105
base/src/formatter/mod.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
pub mod dates;
|
||||
pub mod format;
|
||||
pub mod lexer;
|
||||
pub mod parser;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
// Excel formatting is extremely tricky and I think implementing all it's rules might be borderline impossible.
|
||||
// But the essentials are easy to understand.
|
||||
//
|
||||
// A general Excel formatting string is divided iun four parts:
|
||||
//
|
||||
// <POSITIVE>;<NEGATIVE>;<ZERO>;<TEXT>
|
||||
//
|
||||
// * How many decimal digits do you need?
|
||||
//
|
||||
// 0.000 for exactly three
|
||||
// 0.00??? for at least two and up to five
|
||||
//
|
||||
// * Do you need a thousands separator?
|
||||
//
|
||||
// #,##
|
||||
// # will just write the number
|
||||
// #, will write the number up to the thousand separator (if there is nothing else)
|
||||
//
|
||||
// But #,# and any number of '#' to the right will work just as good. So the following all produce the same results:
|
||||
// #,##0.00 #,######0.00 #,0.00
|
||||
//
|
||||
// For us in IronCalc the most general format string for a number (non-scientific notation) will be:
|
||||
//
|
||||
// 1. Will have #,## at the beginning if we use the thousand separator
|
||||
// 2. Then 0.0* with as many 0 as mandatory decimal places
|
||||
// 3. Then ?* with as many question marks as possible decimal places
|
||||
//
|
||||
// Valid examples:
|
||||
// #,##0.??? Thousand separator, up to three decimal digits
|
||||
// 0.00 No thousand separator. Two mandatory decimal places
|
||||
// 0.0? No thousand separator. One mandatory decimal digit and one extra if present.
|
||||
//
|
||||
// * Do you what the text in color?
|
||||
//
|
||||
// Use [RED] or any color in https://www.excelsupersite.com/what-are-the-56-colorindex-colors-in-excel/
|
||||
|
||||
// Weird things
|
||||
// ============
|
||||
//
|
||||
// ####0.0E+00 of 12345467.890123 (changing the number of '#' produces results I do not understand)
|
||||
// ?www??.????0220000 will format 1234567.890123 to 12345www67.89012223000
|
||||
//
|
||||
// Things we will not implement
|
||||
// ============================
|
||||
//
|
||||
// 1.- The accounting format can leave white spaces of the size of a particular character. For instance:
|
||||
//
|
||||
// #,##0.00_);[Red](#,##0.00)
|
||||
//
|
||||
// Will leave a white space to the right of positive numbers so that they are always aligned with negative numbers
|
||||
//
|
||||
// 2.- Excel can repeat a character as many times as needed to fill the cell:
|
||||
//
|
||||
// _($* #,##0_);_($* (#,##0))
|
||||
//
|
||||
// This will put a '$' sign to the left most (leaving a space the size of '(') and then as many empty spaces as possible
|
||||
// and then the number:
|
||||
// | $ 234 |
|
||||
// | $ 1234 |
|
||||
// We can't do this easily in IronCalc
|
||||
//
|
||||
// 3.- You can use ?/? to format fractions in Excel (this is probably not too hard)
|
||||
|
||||
// TOKENs
|
||||
// ======
|
||||
//
|
||||
// * Color [Red] or [Color 23] or [Color23]
|
||||
// * Conditions [<100]
|
||||
// * Space _X when X is any given char
|
||||
// * A spacer of chars: *X where X is repeated as much as possible
|
||||
// * Literals: $, (, ), :, +, - and space
|
||||
// * Text: "Some Text"
|
||||
// * Escaped char: \X where X is anything
|
||||
// * % appears literal and multiplies number by 100
|
||||
// * , If it's in between digit characters it uses the thousand separator. If it is after the digit characters it multiplies by 1000
|
||||
// * Digit characters: 0, #, ?
|
||||
// * ; Types formatter divider
|
||||
// * @ inserts raw text
|
||||
// * Scientific literals E+, E-, e+, e-
|
||||
// * . period. First one is the decimal point, subsequent are literals.
|
||||
|
||||
// d day of the month
|
||||
// dd day of the month (padded i.e 05)
|
||||
// ddd day of the week abbreviation
|
||||
// dddd+ day of the week
|
||||
// mmm Abbreviation month
|
||||
// mmmm Month name
|
||||
// mmmmm First letter of the month
|
||||
// y or yy 2-digit year
|
||||
// yyy+ 4 digit year
|
||||
|
||||
// References
|
||||
// ==========
|
||||
//
|
||||
// [1] https://support.microsoft.com/en-us/office/number-format-codes-5026bbd6-04bc-48cd-bf33-80f18b4eae68?ui=en-us&rs=en-us&ad=us
|
||||
// [2] https://developers.google.com/sheets/api/guides/formats
|
||||
// [3] https://docs.microsoft.com/en-us/openspecs/office_standards/ms-oe376/0e59abdb-7f4e-48fc-9b89-67832fa11789
|
||||
297
base/src/formatter/parser.rs
Normal file
297
base/src/formatter/parser.rs
Normal file
@@ -0,0 +1,297 @@
|
||||
use super::lexer::{Compare, Lexer, Token};
|
||||
|
||||
pub struct Digit {
|
||||
pub kind: char, // '#' | '?' | '0'
|
||||
pub index: i32,
|
||||
pub number: char, // 'i' | 'd' | 'e' (integer, decimal or exponent)
|
||||
}
|
||||
|
||||
pub enum TextToken {
|
||||
Literal(char),
|
||||
Text(String),
|
||||
Ghost(char),
|
||||
Spacer(char),
|
||||
// Text
|
||||
Raw,
|
||||
Digit(Digit),
|
||||
Period,
|
||||
// Dates
|
||||
Day,
|
||||
DayPadded,
|
||||
DayNameShort,
|
||||
DayName,
|
||||
Month,
|
||||
MonthPadded,
|
||||
MonthNameShort,
|
||||
MonthName,
|
||||
MonthLetter,
|
||||
YearShort,
|
||||
Year,
|
||||
}
|
||||
pub struct NumberPart {
|
||||
pub color: Option<i32>,
|
||||
pub condition: Option<(Compare, f64)>,
|
||||
pub use_thousands: bool,
|
||||
pub percent: i32, // multiply number by 100^percent
|
||||
pub comma: i32, // divide number by 1000^comma
|
||||
pub tokens: Vec<TextToken>,
|
||||
pub digit_count: i32, // number of digit tokens (#, 0 or ?) to the left of the decimal point
|
||||
pub precision: i32, // number of digits to the right of the decimal point
|
||||
pub is_scientific: bool,
|
||||
pub scientific_minus: bool,
|
||||
pub exponent_digit_count: i32,
|
||||
}
|
||||
|
||||
pub struct DatePart {
|
||||
pub color: Option<i32>,
|
||||
pub tokens: Vec<TextToken>,
|
||||
}
|
||||
|
||||
pub struct ErrorPart {}
|
||||
|
||||
pub struct GeneralPart {}
|
||||
|
||||
pub enum ParsePart {
|
||||
Number(NumberPart),
|
||||
Date(DatePart),
|
||||
Error(ErrorPart),
|
||||
General(GeneralPart),
|
||||
}
|
||||
|
||||
pub struct Parser {
|
||||
pub parts: Vec<ParsePart>,
|
||||
lexer: Lexer,
|
||||
}
|
||||
|
||||
impl ParsePart {
|
||||
pub fn is_error(&self) -> bool {
|
||||
match &self {
|
||||
ParsePart::Date(..) => false,
|
||||
ParsePart::Number(..) => false,
|
||||
ParsePart::Error(..) => true,
|
||||
ParsePart::General(..) => false,
|
||||
}
|
||||
}
|
||||
pub fn is_date(&self) -> bool {
|
||||
match &self {
|
||||
ParsePart::Date(..) => true,
|
||||
ParsePart::Number(..) => false,
|
||||
ParsePart::Error(..) => false,
|
||||
ParsePart::General(..) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
pub fn new(format: &str) -> Self {
|
||||
let lexer = Lexer::new(format);
|
||||
let parts = vec![];
|
||||
Parser { parts, lexer }
|
||||
}
|
||||
pub fn parse(&mut self) {
|
||||
while self.lexer.peek_token() != Token::EOF {
|
||||
let part = self.parse_part();
|
||||
self.parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_part(&mut self) -> ParsePart {
|
||||
let mut token = self.lexer.next_token();
|
||||
let mut digit_count = 0;
|
||||
let mut precision = 0;
|
||||
let mut is_date = false;
|
||||
let mut is_number = false;
|
||||
let mut found_decimal_dot = false;
|
||||
let mut use_thousands = false;
|
||||
let mut comma = 0;
|
||||
let mut percent = 0;
|
||||
let mut last_token_is_digit = false;
|
||||
let mut color = None;
|
||||
let mut condition = None;
|
||||
let mut tokens = vec![];
|
||||
let mut is_scientific = false;
|
||||
let mut scientific_minus = false;
|
||||
let mut exponent_digit_count = 0;
|
||||
let mut number = 'i';
|
||||
let mut index = 0;
|
||||
|
||||
while token != Token::EOF && token != Token::Separator {
|
||||
let next_token = self.lexer.next_token();
|
||||
let token_is_digit = token.is_digit();
|
||||
is_number = is_number || token_is_digit;
|
||||
let next_token_is_digit = next_token.is_digit();
|
||||
if token_is_digit {
|
||||
if is_scientific {
|
||||
exponent_digit_count += 1;
|
||||
} else if found_decimal_dot {
|
||||
precision += 1;
|
||||
} else {
|
||||
digit_count += 1;
|
||||
}
|
||||
}
|
||||
match token {
|
||||
Token::General => {
|
||||
if tokens.is_empty() {
|
||||
return ParsePart::General(GeneralPart {});
|
||||
} else {
|
||||
return ParsePart::Error(ErrorPart {});
|
||||
}
|
||||
}
|
||||
Token::Comma => {
|
||||
// If it is in between digit token then we use the thousand separator
|
||||
if last_token_is_digit && next_token_is_digit {
|
||||
use_thousands = true;
|
||||
} else if digit_count > 0 {
|
||||
comma += 1;
|
||||
} else {
|
||||
// Before the number is just a literal.
|
||||
tokens.push(TextToken::Literal(','));
|
||||
}
|
||||
}
|
||||
Token::Percent => {
|
||||
tokens.push(TextToken::Literal('%'));
|
||||
percent += 1;
|
||||
}
|
||||
Token::Period => {
|
||||
if !found_decimal_dot {
|
||||
tokens.push(TextToken::Period);
|
||||
found_decimal_dot = true;
|
||||
if number == 'i' {
|
||||
number = 'd';
|
||||
index = 0;
|
||||
}
|
||||
} else {
|
||||
tokens.push(TextToken::Literal('.'));
|
||||
}
|
||||
}
|
||||
Token::Color(index) => {
|
||||
color = Some(index);
|
||||
}
|
||||
Token::Condition(cmp, value) => {
|
||||
condition = Some((cmp, value));
|
||||
}
|
||||
Token::QuestionMark => {
|
||||
tokens.push(TextToken::Digit(Digit {
|
||||
kind: '?',
|
||||
index,
|
||||
number,
|
||||
}));
|
||||
index += 1;
|
||||
}
|
||||
Token::Sharp => {
|
||||
tokens.push(TextToken::Digit(Digit {
|
||||
kind: '#',
|
||||
index,
|
||||
number,
|
||||
}));
|
||||
index += 1;
|
||||
}
|
||||
Token::Zero => {
|
||||
tokens.push(TextToken::Digit(Digit {
|
||||
kind: '0',
|
||||
index,
|
||||
number,
|
||||
}));
|
||||
index += 1;
|
||||
}
|
||||
Token::Literal(value) => {
|
||||
tokens.push(TextToken::Literal(value));
|
||||
}
|
||||
Token::Text(value) => {
|
||||
tokens.push(TextToken::Text(value));
|
||||
}
|
||||
Token::Ghost(value) => {
|
||||
tokens.push(TextToken::Ghost(value));
|
||||
}
|
||||
Token::Spacer(value) => {
|
||||
tokens.push(TextToken::Spacer(value));
|
||||
}
|
||||
Token::Day => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::Day);
|
||||
}
|
||||
Token::DayPadded => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::DayPadded);
|
||||
}
|
||||
Token::DayNameShort => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::DayNameShort);
|
||||
}
|
||||
Token::DayName => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::DayName);
|
||||
}
|
||||
Token::MonthNameShort => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::MonthNameShort);
|
||||
}
|
||||
Token::MonthName => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::MonthName);
|
||||
}
|
||||
Token::Month => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::Month);
|
||||
}
|
||||
Token::MonthPadded => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::MonthPadded);
|
||||
}
|
||||
Token::MonthLetter => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::MonthLetter);
|
||||
}
|
||||
Token::YearShort => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::YearShort);
|
||||
}
|
||||
Token::Year => {
|
||||
is_date = true;
|
||||
tokens.push(TextToken::Year);
|
||||
}
|
||||
Token::Scientific => {
|
||||
if !is_scientific {
|
||||
index = 0;
|
||||
number = 'e';
|
||||
}
|
||||
is_scientific = true;
|
||||
}
|
||||
Token::ScientificMinus => {
|
||||
is_scientific = true;
|
||||
scientific_minus = true;
|
||||
}
|
||||
Token::Separator => {}
|
||||
Token::Raw => {
|
||||
tokens.push(TextToken::Raw);
|
||||
}
|
||||
Token::ILLEGAL => {
|
||||
return ParsePart::Error(ErrorPart {});
|
||||
}
|
||||
Token::EOF => {}
|
||||
}
|
||||
last_token_is_digit = token_is_digit;
|
||||
token = next_token;
|
||||
}
|
||||
if is_date {
|
||||
if is_number {
|
||||
return ParsePart::Error(ErrorPart {});
|
||||
}
|
||||
ParsePart::Date(DatePart { color, tokens })
|
||||
} else {
|
||||
ParsePart::Number(NumberPart {
|
||||
color,
|
||||
condition,
|
||||
use_thousands,
|
||||
percent,
|
||||
comma,
|
||||
tokens,
|
||||
digit_count,
|
||||
precision,
|
||||
is_scientific,
|
||||
scientific_minus,
|
||||
exponent_digit_count,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
2
base/src/formatter/test/mod.rs
Normal file
2
base/src/formatter/test/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod test_general;
|
||||
mod test_parse_formatted_number;
|
||||
196
base/src/formatter/test/test_general.rs
Normal file
196
base/src/formatter/test/test_general.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::{
|
||||
formatter::format::format_number,
|
||||
locale::{get_locale, Locale},
|
||||
};
|
||||
|
||||
fn get_default_locale() -> &'static Locale {
|
||||
get_locale("en").unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_test() {
|
||||
let locale = get_default_locale();
|
||||
let bond_james_bond = format_number(7.0, "000", locale);
|
||||
assert_eq!(bond_james_bond.text, "007");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_general() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(7.0, "General", locale).text, "7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_test_comma() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(1007.0, "000", locale).text, "1007");
|
||||
assert_eq!(format_number(1008.0, "#", locale).text, "1008");
|
||||
assert_eq!(format_number(1009.0, "#,#", locale).text, "1,009");
|
||||
assert_eq!(
|
||||
format_number(12_345_678.0, "#,#", locale).text,
|
||||
"12,345,678"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(12_345_678.0, "0,0", locale).text,
|
||||
"12,345,678"
|
||||
);
|
||||
assert_eq!(format_number(1005.0, "00-00", locale).text, "10-05");
|
||||
assert_eq!(format_number(7.0, "0?0", locale).text, "0 7");
|
||||
assert_eq!(format_number(7.0, "0#0", locale).text, "07");
|
||||
assert_eq!(
|
||||
format_number(1234.0, "000 \"Millions\"", locale).text,
|
||||
"1234 Millions"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(1235.0, "#,000 \"Millions\"", locale).text,
|
||||
"1,235 Millions"
|
||||
);
|
||||
assert_eq!(format_number(1007.0, "0,00", locale).text, "1,007");
|
||||
assert_eq!(
|
||||
format_number(10_000_007.0, "0,00", locale).text,
|
||||
"10,000,007"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_numbers() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(-123.0, "0.0", locale).text, "-123.0");
|
||||
assert_eq!(format_number(-3.0, "000.0", locale).text, "-003.0");
|
||||
assert_eq!(format_number(-0.00001, "000.0", locale).text, "000.0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decimal_part() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(3.1, "0.00", locale).text, "3.10");
|
||||
assert_eq!(format_number(3.1, "00-.-0?0", locale).text, "03-.-1 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_color() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(3.1, "[blue]0.00", locale).text, "3.10");
|
||||
assert_eq!(format_number(3.1, "[blue]0.00", locale).color, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parts() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(3.1, "0.00;(0.00);(-)", locale).text, "3.10");
|
||||
assert_eq!(
|
||||
format_number(-3.1, "0.00;(0.00);(-)", locale).text,
|
||||
"(3.10)"
|
||||
);
|
||||
assert_eq!(format_number(0.0, "0.00;(0.00);(-)", locale).text, "(-)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_zero() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(0.0, "$#,##0", locale).text, "$0");
|
||||
assert_eq!(format_number(-1.0 / 3.0, "0", locale).text, "0");
|
||||
assert_eq!(format_number(-1.0 / 3.0, "0;(0)", locale).text, "(0)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_currencies() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(-23.0, "$#,##0", locale).text, "-$23");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(0.12, "0.00%", locale).text, "12.00%");
|
||||
assert_eq!(format_number(0.12, "0.00%%", locale).text, "1200.00%%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_percent_correct_rounding() {
|
||||
let locale = get_default_locale();
|
||||
// Formatting does Excel rounding (15 significant digits)
|
||||
assert_eq!(
|
||||
format_number(0.1399999999999999, "0.0%", locale).text,
|
||||
"14.0%"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(-0.1399999999999999, "0.0%", locale).text,
|
||||
"-14.0%"
|
||||
);
|
||||
// Formatting does proper rounding
|
||||
assert_eq!(format_number(0.1399, "0.0%", locale).text, "14.0%");
|
||||
assert_eq!(format_number(-0.1399, "0.0%", locale).text, "-14.0%");
|
||||
assert_eq!(format_number(0.02666, "0.00%", locale).text, "2.67%");
|
||||
assert_eq!(format_number(0.0266, "0.00%", locale).text, "2.66%");
|
||||
assert_eq!(format_number(0.0233, "0.00%", locale).text, "2.33%");
|
||||
assert_eq!(format_number(0.02666, "0%", locale).text, "3%");
|
||||
assert_eq!(format_number(-0.02666, "0.00%", locale).text, "-2.67%");
|
||||
assert_eq!(format_number(-0.02666, "0%", locale).text, "-3%");
|
||||
|
||||
// precision 0
|
||||
assert_eq!(format_number(0.135, "0%", locale).text, "14%");
|
||||
assert_eq!(format_number(0.13499, "0%", locale).text, "13%");
|
||||
assert_eq!(format_number(-0.135, "0%", locale).text, "-14%");
|
||||
assert_eq!(format_number(-0.13499, "0%", locale).text, "-13%");
|
||||
|
||||
// precision 1
|
||||
assert_eq!(format_number(0.1345, "0.0%", locale).text, "13.5%");
|
||||
assert_eq!(format_number(0.1343, "0.0%", locale).text, "13.4%");
|
||||
assert_eq!(format_number(-0.1345, "0.0%", locale).text, "-13.5%");
|
||||
assert_eq!(format_number(-0.134499, "0.0%", locale).text, "-13.4%");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_scientific() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(2.5e-14, "0.00E+0", locale).text, "2.50E-14");
|
||||
assert_eq!(format_number(3e-4, "0.00E+00", locale).text, "3.00E-04");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_currency() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(format_number(123.1, "$#,##0", locale).text, "$123");
|
||||
assert_eq!(format_number(123.1, "#,##0 €", locale).text, "123 €");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_date() {
|
||||
let locale = get_default_locale();
|
||||
assert_eq!(
|
||||
format_number(41181.0, "dd/mm/yyyy", locale).text,
|
||||
"29/09/2012"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41181.0, "dd-mm-yyyy", locale).text,
|
||||
"29-09-2012"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41304.0, "dd-mm-yyyy", locale).text,
|
||||
"30-01-2013"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(42657.0, "dd-mm-yyyy", locale).text,
|
||||
"14-10-2016"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
format_number(41181.0, "dddd-mmmm-yyyy", locale).text,
|
||||
"Saturday-September-2012"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41181.0, "ddd-mmm-yy", locale).text,
|
||||
"Sat-Sep-12"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41181.0, "ddd-mmmmm-yy", locale).text,
|
||||
"Sat-S-12"
|
||||
);
|
||||
assert_eq!(
|
||||
format_number(41181.0, "ddd-mmmmmmm-yy", locale).text,
|
||||
"Sat-September-12"
|
||||
);
|
||||
}
|
||||
206
base/src/formatter/test/test_parse_formatted_number.rs
Normal file
206
base/src/formatter/test/test_parse_formatted_number.rs
Normal file
@@ -0,0 +1,206 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::formatter::format::parse_formatted_number as parse;
|
||||
|
||||
const PARSE_ERROR_MSG: &str = "Could not parse number";
|
||||
|
||||
#[test]
|
||||
fn numbers() {
|
||||
// whole numbers
|
||||
assert_eq!(parse("400", &["$"]), Ok((400.0, None)));
|
||||
|
||||
// decimal numbers
|
||||
assert_eq!(parse("4.456", &["$"]), Ok((4.456, None)));
|
||||
|
||||
// scientific notation
|
||||
assert_eq!(
|
||||
parse("23e-12", &["$"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2.123456789e-11", &["$"]),
|
||||
Ok((2.123456789e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("4.5E-9", &["$"]),
|
||||
Ok((4.5e-9, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("23e+2", &["$"]),
|
||||
Ok((2300.0, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("4.5E9", &["$"]),
|
||||
Ok((4.5e9, Some("0.00E+00".to_string())))
|
||||
);
|
||||
|
||||
// negative numbers
|
||||
assert_eq!(parse("-400", &["$"]), Ok((-400.0, None)));
|
||||
assert_eq!(parse("-4.456", &["$"]), Ok((-4.456, None)));
|
||||
assert_eq!(
|
||||
parse("-23e-12", &["$"]),
|
||||
Ok((-2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
|
||||
// trims space
|
||||
assert_eq!(parse(" 400 ", &["$"]), Ok((400.0, None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn percentage() {
|
||||
// whole numbers
|
||||
assert_eq!(parse("400%", &["$"]), Ok((4.0, Some("#,##0%".to_string()))));
|
||||
// decimal numbers
|
||||
assert_eq!(
|
||||
parse("4.456$", &["$"]),
|
||||
Ok((4.456, Some("#,##0.00$".to_string())))
|
||||
);
|
||||
// Percentage in scientific notation will not be formatted as percentage
|
||||
assert_eq!(
|
||||
parse("23e-12%", &["$"]),
|
||||
Ok((23e-12 / 100.0, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2.3E4%", &["$"]),
|
||||
Ok((230.0, Some("0.00E+00".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn currency() {
|
||||
// whole numbers
|
||||
assert_eq!(
|
||||
parse("400$", &["$"]),
|
||||
Ok((400.0, Some("#,##0$".to_string())))
|
||||
);
|
||||
// decimal numbers
|
||||
assert_eq!(
|
||||
parse("4.456$", &["$"]),
|
||||
Ok((4.456, Some("#,##0.00$".to_string())))
|
||||
);
|
||||
// Currencies in scientific notation will not be formatted as currencies
|
||||
assert_eq!(
|
||||
parse("23e-12$", &["$"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2.3e-12$", &["$"]),
|
||||
Ok((2.3e-12, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("€23e-12", &["€"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
|
||||
// switch side of currencies
|
||||
assert_eq!(
|
||||
parse("$400", &["$"]),
|
||||
Ok((400.0, Some("$#,##0".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("$4.456", &["$"]),
|
||||
Ok((4.456, Some("$#,##0.00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("$23e-12", &["$"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("$2.3e-12", &["$"]),
|
||||
Ok((2.3e-12, Some("0.00E+00".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("23e-12€", &["€"]),
|
||||
Ok((2.3e-11, Some("0.00E+00".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_currencies() {
|
||||
assert_eq!(
|
||||
parse("-400$", &["$"]),
|
||||
Ok((-400.0, Some("#,##0$".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("-$400", &["$"]),
|
||||
Ok((-400.0, Some("$#,##0".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("$-400", &["$"]),
|
||||
Ok((-400.0, Some("$#,##0".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors() {
|
||||
// Strings are not numbers
|
||||
assert_eq!(parse("One", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
// Not partial parsing
|
||||
assert_eq!(parse("23 Hello", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
assert_eq!(parse("Hello 23", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
assert_eq!(parse("2 3", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
// No space between
|
||||
assert_eq!(parse("- 23", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn errors_wrong_currency() {
|
||||
assert_eq!(parse("123€", &["$"]), Err(PARSE_ERROR_MSG.to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_dates() {
|
||||
assert_eq!(
|
||||
parse("02/03/2024", &["$"]),
|
||||
Ok((45353.0, Some("dd/mm/yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("02/3/2024", &["$"]),
|
||||
Ok((45353.0, Some("dd/m/yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("02/Mar/2024", &["$"]),
|
||||
Ok((45353.0, Some("dd/mmm/yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("02/March/2024", &["$"]),
|
||||
Ok((45353.0, Some("dd/mmmm/yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2/3/24", &["$"]),
|
||||
Ok((45353.0, Some("d/m/yy".to_string())))
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
parse("10-02-1975", &["$"]),
|
||||
Ok((27435.0, Some("dd-mm-yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("10-2-1975", &["$"]),
|
||||
Ok((27435.0, Some("dd-m-yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("10-Feb-1975", &["$"]),
|
||||
Ok((27435.0, Some("dd-mmm-yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("10-February-1975", &["$"]),
|
||||
Ok((27435.0, Some("dd-mmmm-yyyy".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("10-2-75", &["$"]),
|
||||
Ok((27435.0, Some("dd-m-yy".to_string())))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iso_dates() {
|
||||
assert_eq!(
|
||||
parse("2024/03/02", &["$"]),
|
||||
Ok((45353.0, Some("yyyy/mm/dd".to_string())))
|
||||
);
|
||||
assert_eq!(
|
||||
parse("2024/March/02", &["$"]),
|
||||
Err(PARSE_ERROR_MSG.to_string())
|
||||
);
|
||||
}
|
||||
210
base/src/functions/binary_search.rs
Normal file
210
base/src/functions/binary_search.rs
Normal file
@@ -0,0 +1,210 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
model::Model,
|
||||
};
|
||||
|
||||
use super::util::compare_values;
|
||||
|
||||
// NOTE: We don't know how Excel exactly implements binary search internally.
|
||||
// This means that if the values on the lookup range are not in order our results and Excel's will differ
|
||||
|
||||
// Assumes values are in ascending order, returns matching index or the largest value smaller than target.
|
||||
// Returns None if target is smaller than the smaller value.
|
||||
pub(crate) fn binary_search_or_smaller<T: Ord>(target: &T, array: &[T]) -> Option<i32> {
|
||||
// We apply binary search leftmost for value in the range
|
||||
let n = array.len();
|
||||
let mut l = 0;
|
||||
let mut r = n;
|
||||
while l < r {
|
||||
let m = (l + r) / 2;
|
||||
if &array[m] < target {
|
||||
l = m + 1;
|
||||
} else {
|
||||
r = m;
|
||||
}
|
||||
}
|
||||
if l == n {
|
||||
return Some((l - 1) as i32);
|
||||
}
|
||||
// Now l points to the leftmost element
|
||||
if &array[l] == target {
|
||||
return Some(l as i32);
|
||||
}
|
||||
// If target is less than the minimum return None
|
||||
if l == 0 {
|
||||
return None;
|
||||
}
|
||||
Some((l - 1) as i32)
|
||||
}
|
||||
|
||||
// Assumes values are in ascending order, returns matching index or the smaller value larger than target.
|
||||
// Returns None if target is smaller than the smaller value.
|
||||
pub(crate) fn binary_search_or_greater<T: Ord>(target: &T, array: &[T]) -> Option<i32> {
|
||||
let mut l = 0;
|
||||
let mut r = array.len();
|
||||
while l < r {
|
||||
let mut m = (l + r) / 2;
|
||||
match &array[m].cmp(target) {
|
||||
Ordering::Less => {
|
||||
l = m + 1;
|
||||
}
|
||||
Ordering::Greater => {
|
||||
r = m;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
while m > 1 {
|
||||
if &array[m - 1] == target {
|
||||
m -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Some(m as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If target is larger than the maximum return None
|
||||
if r == array.len() {
|
||||
return None;
|
||||
}
|
||||
// Now r points to the rightmost element
|
||||
Some(r as i32)
|
||||
}
|
||||
|
||||
// Assumes values are in descending order
|
||||
pub(crate) fn binary_search_descending_or_smaller<T: Ord>(target: &T, array: &[T]) -> Option<i32> {
|
||||
let n = array.len();
|
||||
let mut l = 0;
|
||||
let mut r = n;
|
||||
while l < r {
|
||||
let m = (l + r) / 2;
|
||||
let mut index = n - m - 1;
|
||||
match &array[index].cmp(target) {
|
||||
Ordering::Less => {
|
||||
l = m + 1;
|
||||
}
|
||||
Ordering::Greater => {
|
||||
r = m;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
while index < n - 1 {
|
||||
if &array[index + 1] == target {
|
||||
index += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Some(index as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
if l == 0 {
|
||||
return None;
|
||||
}
|
||||
Some((n - l) as i32)
|
||||
}
|
||||
|
||||
// Assumes values are in descending order, returns matching index or the smaller value larger than target.
|
||||
// Returns None if target is smaller than the smaller value.
|
||||
pub(crate) fn binary_search_descending_or_greater<T: Ord>(target: &T, array: &[T]) -> Option<i32> {
|
||||
let n = array.len();
|
||||
let mut l = 0;
|
||||
let mut r = n;
|
||||
while l < r {
|
||||
let m = (l + r) / 2;
|
||||
let mut index = n - m - 1;
|
||||
match &array[index].cmp(target) {
|
||||
Ordering::Less => {
|
||||
l = m + 1;
|
||||
}
|
||||
Ordering::Greater => {
|
||||
r = m;
|
||||
}
|
||||
Ordering::Equal => {
|
||||
while index < n - 1 {
|
||||
if &array[index + 1] == target {
|
||||
index += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return Some(index as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
if r == n {
|
||||
return None;
|
||||
}
|
||||
Some((n - r - 1) as i32)
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Returns an array with the list of cell values in the range
|
||||
pub(crate) fn prepare_array(
|
||||
&mut self,
|
||||
left: &CellReference,
|
||||
right: &CellReference,
|
||||
is_row_vector: bool,
|
||||
) -> Vec<CalcResult> {
|
||||
let n = if is_row_vector {
|
||||
right.row - left.row
|
||||
} else {
|
||||
right.column - left.column
|
||||
} + 1;
|
||||
let mut result = vec![];
|
||||
for index in 0..n {
|
||||
let row;
|
||||
let column;
|
||||
if is_row_vector {
|
||||
row = left.row + index;
|
||||
column = left.column;
|
||||
} else {
|
||||
column = left.column + index;
|
||||
row = left.row;
|
||||
}
|
||||
let value = self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
});
|
||||
result.push(value);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Old style binary search. Used in HLOOKUP, etc
|
||||
pub(crate) fn binary_search(
|
||||
&mut self,
|
||||
target: &CalcResult,
|
||||
left: &CellReference,
|
||||
right: &CellReference,
|
||||
is_row_vector: bool,
|
||||
) -> i32 {
|
||||
let array = self.prepare_array(left, right, is_row_vector);
|
||||
// We apply binary search leftmost for value in the range
|
||||
let mut l = 0;
|
||||
let mut r = array.len();
|
||||
while l < r {
|
||||
let m = (l + r) / 2;
|
||||
match compare_values(&array[m], target) {
|
||||
-1 => {
|
||||
l = m + 1;
|
||||
}
|
||||
1 => {
|
||||
r = m;
|
||||
}
|
||||
_ => {
|
||||
return m as i32;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If target is less than the minimum return #N/A
|
||||
if l == 0 {
|
||||
return -2;
|
||||
}
|
||||
// Now l points to the leftmost element
|
||||
(l - 1) as i32
|
||||
}
|
||||
}
|
||||
314
base/src/functions/date_and_time.rs
Normal file
314
base/src/functions/date_and_time.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
use chrono::Datelike;
|
||||
use chrono::Months;
|
||||
use chrono::NaiveDateTime;
|
||||
use chrono::TimeZone;
|
||||
use chrono::Timelike;
|
||||
|
||||
use crate::formatter::dates::date_to_serial_number;
|
||||
use crate::model::get_milliseconds_since_epoch;
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
constants::EXCEL_DATE_BASE,
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
formatter::dates::from_excel_date,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_day(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let serial_number = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i64;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function DAY parameter 1 value is negative. It should be positive or zero.".to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let date = from_excel_date(serial_number);
|
||||
let day = date.day() as f64;
|
||||
CalcResult::Number(day)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_month(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let serial_number = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i64;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function MONTH parameter 1 value is negative. It should be positive or zero.".to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let date = from_excel_date(serial_number);
|
||||
let month = date.month() as f64;
|
||||
CalcResult::Number(month)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_eomonth(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let serial_number = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i64;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function EOMONTH parameter 1 value is negative. It should be positive or zero.".to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let months = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.trunc();
|
||||
t as i32
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let months_abs = months.unsigned_abs();
|
||||
|
||||
let native_date = if months > 0 {
|
||||
from_excel_date(serial_number) + Months::new(months_abs)
|
||||
} else {
|
||||
from_excel_date(serial_number) - Months::new(months_abs)
|
||||
};
|
||||
|
||||
// Instead of calculating the end of month we compute the first day of the following month
|
||||
// and take one day.
|
||||
let mut month = native_date.month() + 1;
|
||||
let mut year = native_date.year();
|
||||
if month == 13 {
|
||||
month = 1;
|
||||
year += 1;
|
||||
}
|
||||
match date_to_serial_number(1, month, year) {
|
||||
Ok(serial_number) => CalcResult::Number(serial_number as f64 - 1.0),
|
||||
Err(message) => CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// year, month, day
|
||||
pub(crate) fn fn_date(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let year = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i32;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let month = match self.get_number(&args[1], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor();
|
||||
if t < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
};
|
||||
}
|
||||
t as u32
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let day = match self.get_number(&args[2], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor();
|
||||
if t < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Out of range parameters for date".to_string(),
|
||||
};
|
||||
}
|
||||
t as u32
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
match date_to_serial_number(day, month, year) {
|
||||
Ok(serial_number) => CalcResult::Number(serial_number as f64),
|
||||
Err(message) => CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_year(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let serial_number = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i64;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Function YEAR parameter 1 value is negative. It should be positive or zero.".to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let date = from_excel_date(serial_number);
|
||||
let year = date.year() as f64;
|
||||
CalcResult::Number(year)
|
||||
}
|
||||
|
||||
// date, months
|
||||
pub(crate) fn fn_edate(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let serial_number = match self.get_number(&args[0], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.floor() as i64;
|
||||
if t < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Parameter 1 value is negative. It should be positive or zero."
|
||||
.to_string(),
|
||||
};
|
||||
}
|
||||
t
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let months = match self.get_number(&args[1], cell) {
|
||||
Ok(c) => {
|
||||
let t = c.trunc();
|
||||
t as i32
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
let months_abs = months.unsigned_abs();
|
||||
|
||||
let native_date = if months > 0 {
|
||||
from_excel_date(serial_number) + Months::new(months_abs)
|
||||
} else {
|
||||
from_excel_date(serial_number) - Months::new(months_abs)
|
||||
};
|
||||
|
||||
let serial_number = native_date.num_days_from_ce() - EXCEL_DATE_BASE;
|
||||
if serial_number < 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "EDATE out of bounds".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(serial_number as f64)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_today(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count != 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
};
|
||||
}
|
||||
// milliseconds since January 1, 1970 00:00:00 UTC.
|
||||
let milliseconds = get_milliseconds_since_epoch();
|
||||
let seconds = milliseconds / 1000;
|
||||
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||
Some(dt) => dt,
|
||||
None => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Invalid date".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let local_time = self.tz.from_utc_datetime(&dt);
|
||||
// 693_594 is computed as:
|
||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||
// The 2 days offset is because of Excel 1900 bug
|
||||
let days_from_1900 = local_time.num_days_from_ce() - 693_594;
|
||||
|
||||
CalcResult::Number(days_from_1900 as f64)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_now(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count != 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Wrong number of arguments".to_string(),
|
||||
};
|
||||
}
|
||||
// milliseconds since January 1, 1970 00:00:00 UTC.
|
||||
let milliseconds = get_milliseconds_since_epoch();
|
||||
let seconds = milliseconds / 1000;
|
||||
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||
Some(dt) => dt,
|
||||
None => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Invalid date".to_string(),
|
||||
}
|
||||
}
|
||||
};
|
||||
let local_time = self.tz.from_utc_datetime(&dt);
|
||||
// 693_594 is computed as:
|
||||
// NaiveDate::from_ymd(1900, 1, 1).num_days_from_ce() - 2
|
||||
// The 2 days offset is because of Excel 1900 bug
|
||||
let days_from_1900 = local_time.num_days_from_ce() - 693_594;
|
||||
let days = (local_time.num_seconds_from_midnight() as f64) / (60.0 * 60.0 * 24.0);
|
||||
|
||||
CalcResult::Number(days_from_1900 as f64 + days.fract())
|
||||
}
|
||||
}
|
||||
176
base/src/functions/engineering/bessel.rs
Normal file
176
base/src/functions/engineering/bessel.rs
Normal file
@@ -0,0 +1,176 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::{parser::Node, token::Error},
|
||||
model::Model,
|
||||
};
|
||||
|
||||
use super::transcendental::{bessel_i, bessel_j, bessel_k, bessel_y, erf};
|
||||
// https://root.cern/doc/v610/TMath_8cxx_source.html
|
||||
|
||||
// Notice that the parameters for Bessel functions in Excel and here have inverted order
|
||||
// EXCEL_BESSEL(x, n) => bessel(n, x)
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_besseli(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let n = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let n = n.trunc() as i32;
|
||||
let result = bessel_i(n, x);
|
||||
if result.is_infinite() || result.is_nan() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Bessel function".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
pub(crate) fn fn_besselj(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let n = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let n = n.trunc() as i32;
|
||||
if n < 0 {
|
||||
// In Excel this ins #NUM!
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Bessel function".to_string(),
|
||||
};
|
||||
}
|
||||
let result = bessel_j(n, x);
|
||||
if result.is_infinite() || result.is_nan() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Bessel function".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_besselk(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let n = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let n = n.trunc() as i32;
|
||||
let result = bessel_k(n, x);
|
||||
if result.is_infinite() || result.is_nan() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Bessel function".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_bessely(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let n = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let n = n.trunc() as i32;
|
||||
if n < 0 {
|
||||
// In Excel this ins #NUM!
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Bessel function".to_string(),
|
||||
};
|
||||
}
|
||||
let result = bessel_y(n, x);
|
||||
if result.is_infinite() || result.is_nan() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid parameter for Bessel function".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_erf(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if args.len() == 2 {
|
||||
let y = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Number(erf(y) - erf(x))
|
||||
} else {
|
||||
CalcResult::Number(erf(x))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_erfprecise(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
};
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Number(erf(x))
|
||||
}
|
||||
|
||||
pub(crate) fn fn_erfc(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
};
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Number(1.0 - erf(x))
|
||||
}
|
||||
|
||||
pub(crate) fn fn_erfcprecise(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
};
|
||||
let x = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Number(1.0 - erf(x))
|
||||
}
|
||||
}
|
||||
233
base/src/functions/engineering/bit_operations.rs
Normal file
233
base/src/functions/engineering/bit_operations.rs
Normal file
@@ -0,0 +1,233 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
// 2^48-1
|
||||
const MAX: f64 = 281474976710655.0;
|
||||
|
||||
impl Model {
|
||||
// BITAND( number1, number2)
|
||||
pub(crate) fn fn_bitand(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let number1 = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let number2 = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if number1.trunc() != number1 || number2.trunc() != number2 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "numbers must be integers".to_string());
|
||||
}
|
||||
if number1 < 0.0 || number2 < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be positive or zero".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if number1 > MAX || number2 > MAX {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be less than 2^48-1".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let number1 = number1.trunc() as i64;
|
||||
let number2 = number2.trunc() as i64;
|
||||
let result = number1 & number2;
|
||||
CalcResult::Number(result as f64)
|
||||
}
|
||||
|
||||
// BITOR(number1, number2)
|
||||
pub(crate) fn fn_bitor(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let number1 = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let number2 = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if number1.trunc() != number1 || number2.trunc() != number2 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "numbers must be integers".to_string());
|
||||
}
|
||||
if number1 < 0.0 || number2 < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be positive or zero".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if number1 > MAX || number2 > MAX {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be less than 2^48-1".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let number1 = number1.trunc() as i64;
|
||||
let number2 = number2.trunc() as i64;
|
||||
let result = number1 | number2;
|
||||
CalcResult::Number(result as f64)
|
||||
}
|
||||
|
||||
// BITXOR(number1, number2)
|
||||
pub(crate) fn fn_bitxor(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let number1 = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let number2 = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if number1.trunc() != number1 || number2.trunc() != number2 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "numbers must be integers".to_string());
|
||||
}
|
||||
if number1 < 0.0 || number2 < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be positive or zero".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if number1 > MAX || number2 > MAX {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be less than 2^48-1".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let number1 = number1.trunc() as i64;
|
||||
let number2 = number2.trunc() as i64;
|
||||
let result = number1 ^ number2;
|
||||
CalcResult::Number(result as f64)
|
||||
}
|
||||
|
||||
// BITLSHIFT(number, shift_amount)
|
||||
pub(crate) fn fn_bitlshift(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let number = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let shift = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if number.trunc() != number {
|
||||
return CalcResult::new_error(Error::NUM, cell, "numbers must be integers".to_string());
|
||||
}
|
||||
if number < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be positive or zero".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if number > MAX {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be less than 2^48-1".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if shift.abs() > 53.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"shift amount must be less than 53".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let number = number.trunc() as i64;
|
||||
let shift = shift.trunc() as i64;
|
||||
let result = if shift > 0 {
|
||||
number << shift
|
||||
} else {
|
||||
number >> -shift
|
||||
};
|
||||
let result = result as f64;
|
||||
if result.abs() > MAX {
|
||||
return CalcResult::new_error(Error::NUM, cell, "BITLSHIFT overflow".to_string());
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
// BITRSHIFT(number, shift_amount)
|
||||
pub(crate) fn fn_bitrshift(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let number = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let shift = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if number.trunc() != number {
|
||||
return CalcResult::new_error(Error::NUM, cell, "numbers must be integers".to_string());
|
||||
}
|
||||
if number < 0.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be positive or zero".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if number > MAX {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"numbers must be less than 2^48-1".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
if shift.abs() > 53.0 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"shift amount must be less than 53".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let number = number.trunc() as i64;
|
||||
let shift = shift.trunc() as i64;
|
||||
let result = if shift > 0 {
|
||||
number >> shift
|
||||
} else {
|
||||
number << -shift
|
||||
};
|
||||
let result = result as f64;
|
||||
if result.abs() > MAX {
|
||||
return CalcResult::new_error(Error::NUM, cell, "BITRSHIFT overflow".to_string());
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
793
base/src/functions/engineering/complex.rs
Normal file
793
base/src/functions/engineering/complex.rs
Normal file
@@ -0,0 +1,793 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::{
|
||||
lexer::util::get_tokens,
|
||||
parser::Node,
|
||||
token::{Error, OpSum, TokenType},
|
||||
},
|
||||
model::Model,
|
||||
number_format::to_precision,
|
||||
};
|
||||
|
||||
/// This implements all functions with complex arguments in the standard
|
||||
/// NOTE: If performance is ever needed we should have a new entry in CalcResult,
|
||||
/// So this functions will return CalcResult::Complex(x,y, Suffix)
|
||||
/// and not having to parse it over and over again.
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum Suffix {
|
||||
I,
|
||||
J,
|
||||
}
|
||||
|
||||
impl fmt::Display for Suffix {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Suffix::I => write!(f, "i"),
|
||||
Suffix::J => write!(f, "j"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Complex {
|
||||
x: f64,
|
||||
y: f64,
|
||||
suffix: Suffix,
|
||||
}
|
||||
|
||||
impl fmt::Display for Complex {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let x = to_precision(self.x, 15);
|
||||
let y = to_precision(self.y, 15);
|
||||
let suffix = &self.suffix;
|
||||
// it is a bit weird what Excel does but it seems it uses general notation for
|
||||
// numbers > 1e-20 and scientific notation for the rest
|
||||
let y_str = if y.abs() <= 9e-20 {
|
||||
format!("{:E}", y)
|
||||
} else if y == 1.0 {
|
||||
"".to_string()
|
||||
} else if y == -1.0 {
|
||||
"-".to_string()
|
||||
} else {
|
||||
format!("{}", y)
|
||||
};
|
||||
let x_str = if x.abs() <= 9e-20 {
|
||||
format!("{:E}", x)
|
||||
} else {
|
||||
format!("{}", x)
|
||||
};
|
||||
if y == 0.0 && x == 0.0 {
|
||||
write!(f, "0")
|
||||
} else if y == 0.0 {
|
||||
write!(f, "{x_str}")
|
||||
} else if x == 0.0 {
|
||||
write!(f, "{y_str}{suffix}")
|
||||
} else if y > 0.0 {
|
||||
write!(f, "{x_str}+{y_str}{suffix}")
|
||||
} else {
|
||||
write!(f, "{x_str}{y_str}{suffix}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_complex_number(s: &str) -> Result<(f64, f64, Suffix), String> {
|
||||
// Check for i, j, -i, -j
|
||||
let (sign, s) = match s.strip_prefix('-') {
|
||||
Some(r) => (-1.0, r),
|
||||
None => (1.0, s),
|
||||
};
|
||||
match s {
|
||||
"i" => return Ok((0.0, sign * 1.0, Suffix::I)),
|
||||
"j" => return Ok((0.0, sign * 1.0, Suffix::J)),
|
||||
_ => {
|
||||
// Let it go
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: This is an overuse
|
||||
let tokens = get_tokens(s);
|
||||
|
||||
// There has to be 1, 2 3, or 4 tokens
|
||||
// number
|
||||
// number suffix
|
||||
// number1+suffix
|
||||
// number1+number2 suffix
|
||||
|
||||
match tokens.len() {
|
||||
1 => {
|
||||
// Real number
|
||||
let number1 = match tokens[0].token {
|
||||
TokenType::Number(f) => f,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
};
|
||||
// i is the default
|
||||
Ok((sign * number1, 0.0, Suffix::I))
|
||||
}
|
||||
2 => {
|
||||
// number2 i
|
||||
let number2 = match tokens[0].token {
|
||||
TokenType::Number(f) => f,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
};
|
||||
let suffix = match &tokens[1].token {
|
||||
TokenType::Ident(w) => match w.as_str() {
|
||||
"i" => Suffix::I,
|
||||
"j" => Suffix::J,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
},
|
||||
_ => {
|
||||
return Err(format!("Not a complex number: {s}"));
|
||||
}
|
||||
};
|
||||
Ok((0.0, sign * number2, suffix))
|
||||
}
|
||||
3 => {
|
||||
let number1 = match tokens[0].token {
|
||||
TokenType::Number(f) => f,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
};
|
||||
let operation = match &tokens[1].token {
|
||||
TokenType::Addition(f) => f,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
};
|
||||
let suffix = match &tokens[2].token {
|
||||
TokenType::Ident(w) => match w.as_str() {
|
||||
"i" => Suffix::I,
|
||||
"j" => Suffix::J,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
},
|
||||
_ => {
|
||||
return Err(format!("Not a complex number: {s}"));
|
||||
}
|
||||
};
|
||||
let number2 = if matches!(operation, OpSum::Minus) {
|
||||
-1.0
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
Ok((sign * number1, number2, suffix))
|
||||
}
|
||||
4 => {
|
||||
let number1 = match tokens[0].token {
|
||||
TokenType::Number(f) => f,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
};
|
||||
let operation = match &tokens[1].token {
|
||||
TokenType::Addition(f) => f,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
};
|
||||
let mut number2 = match tokens[2].token {
|
||||
TokenType::Number(f) => f,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
};
|
||||
let suffix = match &tokens[3].token {
|
||||
TokenType::Ident(w) => match w.as_str() {
|
||||
"i" => Suffix::I,
|
||||
"j" => Suffix::J,
|
||||
_ => return Err(format!("Not a complex number: {s}")),
|
||||
},
|
||||
_ => {
|
||||
return Err(format!("Not a complex number: {s}"));
|
||||
}
|
||||
};
|
||||
if matches!(operation, OpSum::Minus) {
|
||||
number2 = -number2
|
||||
}
|
||||
Ok((sign * number1, number2, suffix))
|
||||
}
|
||||
_ => Err(format!("Not a complex number: {s}")),
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn get_complex_number(
|
||||
&mut self,
|
||||
node: &Node,
|
||||
cell: CellReference,
|
||||
) -> Result<(f64, f64, Suffix), CalcResult> {
|
||||
let value = match self.get_string(node, cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return Err(s),
|
||||
};
|
||||
if value.is_empty() {
|
||||
return Ok((0.0, 0.0, Suffix::I));
|
||||
}
|
||||
match parse_complex_number(&value) {
|
||||
Ok(s) => Ok(s),
|
||||
Err(message) => Err(CalcResult::new_error(Error::NUM, cell, message)),
|
||||
}
|
||||
}
|
||||
// COMPLEX(real_num, i_num, [suffix])
|
||||
pub(crate) fn fn_complex(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(2..=3).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let y = match self.get_number(&args[1], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let suffix = if args.len() == 3 {
|
||||
match self.get_string(&args[2], cell) {
|
||||
Ok(s) => {
|
||||
if s == "i" || s.is_empty() {
|
||||
Suffix::I
|
||||
} else if s == "j" {
|
||||
Suffix::J
|
||||
} else {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Invalid suffix".to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
Suffix::I
|
||||
};
|
||||
|
||||
let complex = Complex { x, y, suffix };
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_imabs(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, _) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
CalcResult::Number(f64::sqrt(x * x + y * y))
|
||||
}
|
||||
|
||||
pub(crate) fn fn_imaginary(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (_, y, _) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
CalcResult::Number(y)
|
||||
}
|
||||
pub(crate) fn fn_imargument(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, _) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
if x == 0.0 && y == 0.0 {
|
||||
return CalcResult::new_error(Error::DIV, cell, "Division by zero".to_string());
|
||||
}
|
||||
let angle = f64::atan2(y, x);
|
||||
CalcResult::Number(angle)
|
||||
}
|
||||
pub(crate) fn fn_imconjugate(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let complex = Complex { x, y: -y, suffix };
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imcos(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let (x, y, suffix) = match parse_complex_number(&value) {
|
||||
Ok(s) => s,
|
||||
Err(message) => return CalcResult::new_error(Error::NUM, cell, message),
|
||||
};
|
||||
|
||||
let complex = Complex {
|
||||
x: x.cos() * y.cosh(),
|
||||
y: -x.sin() * y.sinh(),
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imcosh(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let (x, y, suffix) = match parse_complex_number(&value) {
|
||||
Ok(s) => s,
|
||||
Err(message) => return CalcResult::new_error(Error::NUM, cell, message),
|
||||
};
|
||||
|
||||
let complex = Complex {
|
||||
x: x.cosh() * y.cos(),
|
||||
y: x.sinh() * y.sin(),
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imcot(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let (x, y, suffix) = match parse_complex_number(&value) {
|
||||
Ok(s) => s,
|
||||
Err(message) => return CalcResult::new_error(Error::NUM, cell, message),
|
||||
};
|
||||
|
||||
if x == 0.0 && y != 0.0 {
|
||||
let complex = Complex {
|
||||
x: 0.0,
|
||||
y: -1.0 / y.tanh(),
|
||||
suffix,
|
||||
};
|
||||
return CalcResult::String(complex.to_string());
|
||||
} else if y == 0.0 {
|
||||
let complex = Complex {
|
||||
x: 1.0 / x.tan(),
|
||||
y: 0.0,
|
||||
suffix,
|
||||
};
|
||||
return CalcResult::String(complex.to_string());
|
||||
}
|
||||
|
||||
let x_cot = 1.0 / x.tan();
|
||||
let y_coth = 1.0 / y.tanh();
|
||||
|
||||
let t = x_cot * x_cot + y_coth * y_coth;
|
||||
let x = (x_cot * y_coth * y_coth - x_cot) / t;
|
||||
let y = (-x_cot * x_cot * y_coth - y_coth) / t;
|
||||
|
||||
if x.is_infinite() || y.is_infinite() || x.is_nan() || y.is_nan() {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Invalid operation".to_string());
|
||||
}
|
||||
|
||||
let complex = Complex { x, y, suffix };
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_imcsc(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let (x, y, suffix) = match parse_complex_number(&value) {
|
||||
Ok(s) => s,
|
||||
Err(message) => return CalcResult::new_error(Error::NUM, cell, message),
|
||||
};
|
||||
let x_cos = x.cos();
|
||||
let x_sin = x.sin();
|
||||
|
||||
let y_cosh = y.cosh();
|
||||
let y_sinh = y.sinh();
|
||||
|
||||
let t = x_sin * x_sin * y_cosh * y_cosh + x_cos * x_cos * y_sinh * y_sinh;
|
||||
|
||||
let complex = Complex {
|
||||
x: x_sin * y_cosh / t,
|
||||
y: -x_cos * y_sinh / t,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_imcsch(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let (x, y, suffix) = match parse_complex_number(&value) {
|
||||
Ok(s) => s,
|
||||
Err(message) => return CalcResult::new_error(Error::NUM, cell, message),
|
||||
};
|
||||
let x_cosh = x.cosh();
|
||||
let x_sinh = x.sinh();
|
||||
|
||||
let y_cos = y.cos();
|
||||
let y_sin = y.sin();
|
||||
|
||||
let t = x_sinh * x_sinh * y_cos * y_cos + x_cosh * x_cosh * y_sin * y_sin;
|
||||
|
||||
let complex = Complex {
|
||||
x: x_sinh * y_cos / t,
|
||||
y: -x_cosh * y_sin / t,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_imdiv(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x1, y1, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let (x2, y2, suffix2) = match self.get_complex_number(&args[1], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
if suffix != suffix2 {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "Different suffixes".to_string());
|
||||
}
|
||||
let t = x2 * x2 + y2 * y2;
|
||||
if t == 0.0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Invalid".to_string());
|
||||
}
|
||||
let complex = Complex {
|
||||
x: (x1 * x2 + y1 * y2) / t,
|
||||
y: (-x1 * y2 + y1 * x2) / t,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_imexp(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let complex = Complex {
|
||||
x: x.exp() * y.cos(),
|
||||
y: x.exp() * y.sin(),
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imln(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let r = f64::sqrt(x * x + y * y);
|
||||
let a = f64::atan2(y, x);
|
||||
|
||||
let complex = Complex {
|
||||
x: r.ln(),
|
||||
y: a,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imlog10(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let r = f64::sqrt(x * x + y * y);
|
||||
let a = f64::atan2(y, x);
|
||||
|
||||
let complex = Complex {
|
||||
x: r.log10(),
|
||||
y: a * f64::log10(f64::exp(1.0)),
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imlog2(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let r = f64::sqrt(x * x + y * y);
|
||||
let a = f64::atan2(y, x);
|
||||
|
||||
let complex = Complex {
|
||||
x: r.log2(),
|
||||
y: a * f64::log2(f64::exp(1.0)),
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
|
||||
// IMPOWER(imnumber, power)
|
||||
// If $(r, \theta)$ is the polar representation the formula is:
|
||||
// $$ x = r^n*\cos(n\dot\theta), y = r^n*\csin(n\dot\theta) $
|
||||
pub(crate) fn fn_impower(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let n = match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
|
||||
// if n == n.trunc() && n < 10.0 {
|
||||
// // for small powers we compute manually
|
||||
// let (mut x0, mut y0) = (x, y);
|
||||
// for _ in 1..(n.trunc() as i32) {
|
||||
// (x0, y0) = (x0 * x - y0 * y, x0 * y + y0 * x);
|
||||
// }
|
||||
// let complex = Complex {
|
||||
// x: x0,
|
||||
// y: y0,
|
||||
// suffix,
|
||||
// };
|
||||
// return CalcResult::String(complex.to_string());
|
||||
// };
|
||||
|
||||
let r = f64::sqrt(x * x + y * y);
|
||||
let a = f64::atan2(y, x);
|
||||
|
||||
let x = r.powf(n) * f64::cos(a * n);
|
||||
let y = r.powf(n) * f64::sin(a * n);
|
||||
|
||||
if x.is_infinite() || y.is_infinite() || x.is_nan() || y.is_nan() {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Invalid operation".to_string());
|
||||
}
|
||||
|
||||
let complex = Complex { x, y, suffix };
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_improduct(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let (x1, y1, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let (x2, y2, suffix2) = match self.get_complex_number(&args[1], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
if suffix != suffix2 {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "Different suffixes".to_string());
|
||||
}
|
||||
let complex = Complex {
|
||||
x: x1 * x2 - y1 * y2,
|
||||
y: x1 * y2 + y1 * x2,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imreal(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, _, _) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
CalcResult::Number(x)
|
||||
}
|
||||
pub(crate) fn fn_imsec(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let x_cos = x.cos();
|
||||
let x_sin = x.sin();
|
||||
|
||||
let y_cosh = y.cosh();
|
||||
let y_sinh = y.sinh();
|
||||
|
||||
let t = x_cos * x_cos * y_cosh * y_cosh + x_sin * x_sin * y_sinh * y_sinh;
|
||||
|
||||
let complex = Complex {
|
||||
x: x_cos * y_cosh / t,
|
||||
y: x_sin * y_sinh / t,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imsech(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let x_cosh = x.cosh();
|
||||
let x_sinh = x.sinh();
|
||||
|
||||
let y_cos = y.cos();
|
||||
let y_sin = y.sin();
|
||||
|
||||
let t = x_cosh * x_cosh * y_cos * y_cos + x_sinh * x_sinh * y_sin * y_sin;
|
||||
|
||||
let complex = Complex {
|
||||
x: x_cosh * y_cos / t,
|
||||
y: -x_sinh * y_sin / t,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imsin(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let complex = Complex {
|
||||
x: x.sin() * y.cosh(),
|
||||
y: x.cos() * y.sinh(),
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imsinh(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let complex = Complex {
|
||||
x: x.sinh() * y.cos(),
|
||||
y: x.cosh() * y.sin(),
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imsqrt(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let r = f64::sqrt(x * x + y * y).sqrt();
|
||||
let a = f64::atan2(y, x);
|
||||
|
||||
let complex = Complex {
|
||||
x: r * f64::cos(a / 2.0),
|
||||
y: r * f64::sin(a / 2.0),
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
pub(crate) fn fn_imsub(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x1, y1, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let (x2, y2, suffix2) = match self.get_complex_number(&args[1], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
if suffix != suffix2 {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "Different suffixes".to_string());
|
||||
}
|
||||
let complex = Complex {
|
||||
x: x1 - x2,
|
||||
y: y1 - y2,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_imsum(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x1, y1, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let (x2, y2, suffix2) = match self.get_complex_number(&args[1], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
if suffix != suffix2 {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "Different suffixes".to_string());
|
||||
}
|
||||
let complex = Complex {
|
||||
x: x1 + x2,
|
||||
y: y1 + y2,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_imtan(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let (x, y, suffix) = match self.get_complex_number(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
|
||||
let x_tan = x.tan();
|
||||
let y_tanh = y.tanh();
|
||||
|
||||
let t = 1.0 + x_tan * x_tan * y_tanh * y_tanh;
|
||||
|
||||
let complex = Complex {
|
||||
x: (x_tan - x_tan * y_tanh * y_tanh) / t,
|
||||
y: (y_tanh + x_tan * x_tan * y_tanh) / t,
|
||||
suffix,
|
||||
};
|
||||
CalcResult::String(complex.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::functions::engineering::complex::Suffix;
|
||||
|
||||
use super::parse_complex_number as parse;
|
||||
|
||||
#[test]
|
||||
fn test_parse_complex() {
|
||||
assert_eq!(parse("1+2i"), Ok((1.0, 2.0, Suffix::I)));
|
||||
assert_eq!(parse("2i"), Ok((0.0, 2.0, Suffix::I)));
|
||||
assert_eq!(parse("7.5"), Ok((7.5, 0.0, Suffix::I)));
|
||||
assert_eq!(parse("-7.5"), Ok((-7.5, 0.0, Suffix::I)));
|
||||
assert_eq!(parse("7-5i"), Ok((7.0, -5.0, Suffix::I)));
|
||||
assert_eq!(parse("i"), Ok((0.0, 1.0, Suffix::I)));
|
||||
assert_eq!(parse("7+i"), Ok((7.0, 1.0, Suffix::I)));
|
||||
assert_eq!(parse("7-i"), Ok((7.0, -1.0, Suffix::I)));
|
||||
assert_eq!(parse("-i"), Ok((0.0, -1.0, Suffix::I)));
|
||||
assert_eq!(parse("0"), Ok((0.0, 0.0, Suffix::I)));
|
||||
}
|
||||
}
|
||||
418
base/src/functions/engineering/convert.rs
Normal file
418
base/src/functions/engineering/convert.rs
Normal file
@@ -0,0 +1,418 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
enum Temperature {
|
||||
Kelvin,
|
||||
Celsius,
|
||||
Rankine,
|
||||
Reaumur,
|
||||
Fahrenheit,
|
||||
}
|
||||
|
||||
// To Kelvin
|
||||
// T_K = T_C + 273.15
|
||||
// T_K = 5/9 * T_rank
|
||||
// T_K = (T_R-273.15)*4/5
|
||||
// T_K = 5/9 ( T_F + 459.67)
|
||||
fn convert_temperature(
|
||||
value: f64,
|
||||
from_temperature: Temperature,
|
||||
to_temperature: Temperature,
|
||||
) -> f64 {
|
||||
let from_t_kelvin = match from_temperature {
|
||||
Temperature::Kelvin => value,
|
||||
Temperature::Celsius => value + 273.15,
|
||||
Temperature::Rankine => 5.0 * value / 9.0,
|
||||
Temperature::Reaumur => 5.0 * value / 4.0 + 273.15,
|
||||
Temperature::Fahrenheit => 5.0 / 9.0 * (value + 459.67),
|
||||
};
|
||||
|
||||
match to_temperature {
|
||||
Temperature::Kelvin => from_t_kelvin,
|
||||
Temperature::Celsius => from_t_kelvin - 273.5,
|
||||
Temperature::Rankine => 9.0 * from_t_kelvin / 5.0,
|
||||
Temperature::Reaumur => 4.0 * (from_t_kelvin - 273.15) / 5.0,
|
||||
Temperature::Fahrenheit => 9.0 * from_t_kelvin / 5.0 - 459.67,
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
// CONVERT(number, from_unit, to_unit)
|
||||
pub(crate) fn fn_convert(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let from_unit = match self.get_string(&args[1], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let to_unit = match self.get_string(&args[2], cell) {
|
||||
Ok(s) => s,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let prefix = HashMap::from([
|
||||
("Y", 1E+24),
|
||||
("Z", 1E+21),
|
||||
("E", 1000000000000000000.0),
|
||||
("P", 1000000000000000.0),
|
||||
("T", 1000000000000.0),
|
||||
("G", 1000000000.0),
|
||||
("M", 1000000.0),
|
||||
("k", 1000.0),
|
||||
("h", 100.0),
|
||||
("da", 10.0),
|
||||
("e", 10.0),
|
||||
("d", 0.1),
|
||||
("c", 0.01),
|
||||
("m", 0.001),
|
||||
("u", 0.000001),
|
||||
("n", 0.000000001),
|
||||
("p", 1E-12),
|
||||
("f", 1E-15),
|
||||
("a", 1E-18),
|
||||
("z", 1E-21),
|
||||
("y", 1E-24),
|
||||
("Yi", 2.0_f64.powf(80.0)),
|
||||
("Ei", 2.0_f64.powf(70.0)),
|
||||
("Yi", 2.0_f64.powf(80.0)),
|
||||
("Zi", 2.0_f64.powf(70.0)),
|
||||
("Ei", 2.0_f64.powf(60.0)),
|
||||
("Pi", 2.0_f64.powf(50.0)),
|
||||
("Ti", 2.0_f64.powf(40.0)),
|
||||
("Gi", 2.0_f64.powf(30.0)),
|
||||
("Mi", 2.0_f64.powf(20.0)),
|
||||
("ki", 2.0_f64.powf(10.0)),
|
||||
]);
|
||||
|
||||
let mut units = HashMap::new();
|
||||
|
||||
let weight = HashMap::from([
|
||||
("g", 1.0),
|
||||
("sg", 14593.9029372064),
|
||||
("lbm", 453.59237),
|
||||
("u", 1.660538782E-24),
|
||||
("ozm", 28.349523125),
|
||||
("grain", 0.06479891),
|
||||
("cwt", 45359.237),
|
||||
("shweight", 45359.237),
|
||||
("uk_cwt", 50802.34544),
|
||||
("lcwt", 50802.34544),
|
||||
("stone", 6350.29318),
|
||||
("ton", 907184.74),
|
||||
("brton", 1016046.9088), // g-sheets has a different value for this
|
||||
("LTON", 1016046.9088),
|
||||
("uk_ton", 1016046.9088),
|
||||
]);
|
||||
|
||||
units.insert("weight", weight);
|
||||
|
||||
let distance = HashMap::from([
|
||||
("m", 1.0),
|
||||
("mi", 1609.344),
|
||||
("Nmi", 1852.0),
|
||||
("in", 0.0254),
|
||||
("ft", 0.3048),
|
||||
("yd", 0.9144),
|
||||
("ang", 0.0000000001),
|
||||
("ell", 1.143),
|
||||
("ly", 9460730472580800.0),
|
||||
("parsec", 30856775812815500.0),
|
||||
("pc", 30856775812815500.0),
|
||||
("Picapt", 0.000352777777777778),
|
||||
("Pica", 0.000352777777777778),
|
||||
("pica", 0.00423333333333333),
|
||||
("survey_mi", 1609.34721869444),
|
||||
]);
|
||||
units.insert("distance", distance);
|
||||
let time = HashMap::from([
|
||||
("yr", 31557600.0),
|
||||
("day", 86400.0),
|
||||
("d", 86400.0),
|
||||
("hr", 3600.0),
|
||||
("mn", 60.0),
|
||||
("min", 60.0),
|
||||
("sec", 1.0),
|
||||
("s", 1.0),
|
||||
]);
|
||||
|
||||
units.insert("time", time);
|
||||
|
||||
let pressure = HashMap::from([
|
||||
("Pa", 1.0),
|
||||
("p", 1.0),
|
||||
("atm", 101325.0),
|
||||
("at", 101325.0),
|
||||
("mmHg", 133.322),
|
||||
("psi", 6894.75729316836),
|
||||
("Torr", 133.322368421053),
|
||||
]);
|
||||
units.insert("pressure", pressure);
|
||||
let force = HashMap::from([
|
||||
("N", 1.0),
|
||||
("dyn", 0.00001),
|
||||
("dy", 0.00001),
|
||||
("lbf", 4.4482216152605),
|
||||
("pond", 0.00980665),
|
||||
]);
|
||||
units.insert("force", force);
|
||||
|
||||
let energy = HashMap::from([
|
||||
("J", 1.0),
|
||||
("e", 0.0000001),
|
||||
("c", 4.184),
|
||||
("cal", 4.1868),
|
||||
("eV", 1.602176487E-19),
|
||||
("ev", 1.602176487E-19),
|
||||
("HPh", 2684519.53769617),
|
||||
("hh", 2684519.53769617),
|
||||
("Wh", 3600.0),
|
||||
("wh", 3600.0),
|
||||
("flb", 1.3558179483314),
|
||||
("BTU", 1055.05585262),
|
||||
("btu", 1055.05585262),
|
||||
]);
|
||||
units.insert("energy", energy);
|
||||
|
||||
let power = HashMap::from([
|
||||
("HP", 745.69987158227),
|
||||
("h", 745.69987158227),
|
||||
("PS", 735.49875),
|
||||
("W", 1.0),
|
||||
("w", 1.0),
|
||||
]);
|
||||
units.insert("power", power);
|
||||
|
||||
let magnetism = HashMap::from([("T", 1.0), ("ga", 0.0001)]);
|
||||
units.insert("magnetism", magnetism);
|
||||
|
||||
let volume = HashMap::from([
|
||||
("tsp", 0.00000492892159375),
|
||||
("tspm", 0.000005),
|
||||
("tbs", 0.00001478676478125),
|
||||
("oz", 0.0000295735295625),
|
||||
("cup", 0.0002365882365),
|
||||
("pt", 0.000473176473),
|
||||
("us_pt", 0.000473176473),
|
||||
("uk_pt", 0.00056826125),
|
||||
("qt", 0.000946352946),
|
||||
("uk_qt", 0.0011365225),
|
||||
("gal", 0.003785411784),
|
||||
("uk_gal", 0.00454609),
|
||||
("l", 0.001),
|
||||
("L", 0.001),
|
||||
("lt", 0.001),
|
||||
("ang3", 1E-30),
|
||||
("ang^3", 1E-30),
|
||||
("barrel", 0.158987294928),
|
||||
("bushel", 0.03523907016688),
|
||||
("ft3", 0.028316846592),
|
||||
("ft^3", 0.028316846592),
|
||||
("in3", 0.000016387064),
|
||||
("in^3", 0.000016387064),
|
||||
("ly3", 8.46786664623715E+47),
|
||||
("ly^3", 8.46786664623715E+47),
|
||||
("m3", 1.0),
|
||||
("m^3", 1.0),
|
||||
("mi3", 4168181825.44058),
|
||||
("mi^3", 4168181825.44058),
|
||||
("yd3", 0.764554857984),
|
||||
("yd^3", 0.764554857984),
|
||||
("Nmi3", 6352182208.0),
|
||||
("Nmi^3", 6352182208.0),
|
||||
("Picapt3", 4.39039566186557E-11),
|
||||
("Picapt^3", 4.39039566186557E-11),
|
||||
("Pica3", 4.39039566186557E-11),
|
||||
("Pica^3", 4.39039566186557E-11),
|
||||
("GRT", 2.8316846592),
|
||||
("regton", 2.8316846592),
|
||||
("MTON", 1.13267386368),
|
||||
]);
|
||||
units.insert("volume", volume);
|
||||
|
||||
let area = HashMap::from([
|
||||
("uk_acre", 4046.8564224),
|
||||
("us_acre", 4046.87260987425),
|
||||
("ang2", 1E-20),
|
||||
("ang^2", 1E-20),
|
||||
("ar", 100.0),
|
||||
("ft2", 0.09290304),
|
||||
("ft^2", 0.09290304),
|
||||
("ha", 10000.0),
|
||||
("in2", 0.00064516),
|
||||
("in^2", 0.00064516),
|
||||
("ly2", 8.95054210748189E+31),
|
||||
("ly^2", 8.95054210748189E+31),
|
||||
("m2", 1.0),
|
||||
("m^2", 1.0),
|
||||
("Morgen", 2500.0),
|
||||
("mi2", 2589988.110336),
|
||||
("mi^2", 2589988.110336),
|
||||
("Nmi2", 3429904.0),
|
||||
("Nmi^2", 3429904.0),
|
||||
("Picapt2", 0.000000124452160493827),
|
||||
("Pica2", 0.000000124452160493827),
|
||||
("Pica^2", 0.000000124452160493827),
|
||||
("Picapt^2", 0.000000124452160493827),
|
||||
("yd2", 0.83612736),
|
||||
("yd^2", 0.83612736),
|
||||
]);
|
||||
units.insert("area", area);
|
||||
|
||||
let information = HashMap::from([("bit", 1.0), ("byte", 8.0)]);
|
||||
units.insert("information", information);
|
||||
|
||||
let speed = HashMap::from([
|
||||
("admkn", 0.514773333333333),
|
||||
("kn", 0.514444444444444),
|
||||
("m/h", 0.000277777777777778),
|
||||
("m/hr", 0.000277777777777778),
|
||||
("m/s", 1.0),
|
||||
("m/sec", 1.0),
|
||||
("mph", 0.44704),
|
||||
]);
|
||||
units.insert("speed", speed);
|
||||
|
||||
let temperature = HashMap::from([
|
||||
("C", 1.0),
|
||||
("cel", 1.0),
|
||||
("F", 1.0),
|
||||
("fah", 1.0),
|
||||
("K", 1.0),
|
||||
("kel", 1.0),
|
||||
("Rank", 1.0),
|
||||
("Reau", 1.0),
|
||||
]);
|
||||
units.insert("temperature", temperature);
|
||||
|
||||
// only some units admit prefixes (the is no kC, kilo Celsius, for instance)
|
||||
let mks = [
|
||||
"Pa", "p", "atm", "at", "mmHg", "g", "u", "m", "ang", "ly", "parsec", "pc", "ang2",
|
||||
"ang^2", "ar", "m2", "m^2", "N", "dyn", "dy", "pond", "J", "e", "c", "cal", "eV", "ev",
|
||||
"Wh", "wh", "W", "w", "T", "ga", "uk_pt", "l", "L", "lt", "ang3", "ang^3", "m3", "m^3",
|
||||
"bit", "byte", "m/h", "m/hr", "m/s", "m/sec", "mph", "K", "kel",
|
||||
];
|
||||
let volumes = ["ang3", "ang^3", "m3", "m^3"];
|
||||
|
||||
// We need all_units to make sure tha pc is interpreted as parsec and not pico centimeters
|
||||
// We could have this list hard coded, of course.
|
||||
let mut all_units = Vec::new();
|
||||
for unit in units.values() {
|
||||
for &unit_name in unit.keys() {
|
||||
all_units.push(unit_name);
|
||||
}
|
||||
}
|
||||
|
||||
let mut to_unit_prefix = 1.0;
|
||||
let mut from_unit_prefix = 1.0;
|
||||
|
||||
// kind of units (weight, distance, time, ...)
|
||||
let mut to_unit_kind = "";
|
||||
let mut from_unit_kind = "";
|
||||
|
||||
let mut to_unit_name = "";
|
||||
let mut from_unit_name = "";
|
||||
|
||||
for (&name, unit) in &units {
|
||||
for (&unit_name, unit_value) in unit {
|
||||
if let Some(pk) = from_unit.strip_suffix(unit_name) {
|
||||
if pk.is_empty() {
|
||||
from_unit_kind = name;
|
||||
from_unit_prefix = 1.0 * unit_value;
|
||||
from_unit_name = unit_name;
|
||||
} else if let Some(modifier) = prefix.get(pk) {
|
||||
if mks.contains(&unit_name) && !all_units.contains(&from_unit.as_str()) {
|
||||
// We make sure:
|
||||
// 1. It is a unit that admits a modifier (like metres or grams)
|
||||
// 2. from_unit is not itself a unit
|
||||
let scale = if name == "area" && unit_name != "ar" {
|
||||
// 1 km2 is actually 10^6 m2
|
||||
*modifier * modifier
|
||||
} else if name == "volume" && volumes.contains(&unit_name) {
|
||||
// don't look at me I don't make the rules!
|
||||
*modifier * modifier * modifier
|
||||
} else {
|
||||
*modifier
|
||||
};
|
||||
from_unit_kind = name;
|
||||
from_unit_prefix = scale * unit_value;
|
||||
from_unit_name = unit_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(pk) = to_unit.strip_suffix(unit_name) {
|
||||
if pk.is_empty() {
|
||||
to_unit_kind = name;
|
||||
to_unit_prefix = 1.0 * unit_value;
|
||||
to_unit_name = unit_name;
|
||||
} else if let Some(modifier) = prefix.get(pk) {
|
||||
if mks.contains(&unit_name) && !all_units.contains(&to_unit.as_str()) {
|
||||
let scale = if name == "area" && unit_name != "ar" {
|
||||
*modifier * modifier
|
||||
} else if name == "volume" && volumes.contains(&unit_name) {
|
||||
*modifier * modifier * modifier
|
||||
} else {
|
||||
*modifier
|
||||
};
|
||||
to_unit_kind = name;
|
||||
to_unit_prefix = scale * unit_value;
|
||||
to_unit_name = unit_name;
|
||||
}
|
||||
}
|
||||
}
|
||||
if !from_unit_kind.is_empty() && !to_unit_kind.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !from_unit_kind.is_empty() && !to_unit_kind.is_empty() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if from_unit_kind != to_unit_kind {
|
||||
return CalcResult::new_error(Error::NA, cell, "Different units".to_string());
|
||||
}
|
||||
|
||||
// Let's check if it is temperature;
|
||||
if from_unit_kind.is_empty() {
|
||||
return CalcResult::new_error(Error::NA, cell, "Unit not found".to_string());
|
||||
}
|
||||
|
||||
if from_unit_kind == "temperature" {
|
||||
// Temperature requires formula conversion
|
||||
// Kelvin (K, k), Celsius (C,cel), Rankine (Rank), Réaumur (Reau)
|
||||
let to_temperature = match to_unit_name {
|
||||
"K" | "kel" => Temperature::Kelvin,
|
||||
"C" | "cel" => Temperature::Celsius,
|
||||
"Rank" => Temperature::Rankine,
|
||||
"Reau" => Temperature::Reaumur,
|
||||
"F" | "fah" => Temperature::Fahrenheit,
|
||||
_ => {
|
||||
return CalcResult::new_error(Error::ERROR, cell, "Internal error".to_string());
|
||||
}
|
||||
};
|
||||
let from_temperature = match from_unit_name {
|
||||
"K" | "kel" => Temperature::Kelvin,
|
||||
"C" | "cel" => Temperature::Celsius,
|
||||
"Rank" => Temperature::Rankine,
|
||||
"Reau" => Temperature::Reaumur,
|
||||
"F" | "fah" => Temperature::Fahrenheit,
|
||||
_ => {
|
||||
return CalcResult::new_error(Error::ERROR, cell, "Internal error".to_string());
|
||||
}
|
||||
};
|
||||
let t = convert_temperature(value * from_unit_prefix, from_temperature, to_temperature)
|
||||
/ to_unit_prefix;
|
||||
return CalcResult::Number(t);
|
||||
}
|
||||
CalcResult::Number(value * from_unit_prefix / to_unit_prefix)
|
||||
}
|
||||
}
|
||||
59
base/src/functions/engineering/misc.rs
Normal file
59
base/src/functions/engineering/misc.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::parser::Node,
|
||||
model::Model,
|
||||
number_format::to_precision,
|
||||
};
|
||||
|
||||
impl Model {
|
||||
// DELTA(number1, [number2])
|
||||
pub(crate) fn fn_delta(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
if !(1..=2).contains(&arg_count) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let number1 = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let number2 = if arg_count > 1 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(error) => return error,
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
if to_precision(number1, 16) == to_precision(number2, 16) {
|
||||
CalcResult::Number(1.0)
|
||||
} else {
|
||||
CalcResult::Number(0.0)
|
||||
}
|
||||
}
|
||||
|
||||
// GESTEP(number, [step])
|
||||
pub(crate) fn fn_gestep(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
if !(1..=2).contains(&arg_count) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let number = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(error) => return error,
|
||||
};
|
||||
let step = if arg_count > 1 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(error) => return error,
|
||||
}
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
if to_precision(number, 16) >= to_precision(step, 16) {
|
||||
CalcResult::Number(1.0)
|
||||
} else {
|
||||
CalcResult::Number(0.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
7
base/src/functions/engineering/mod.rs
Normal file
7
base/src/functions/engineering/mod.rs
Normal file
@@ -0,0 +1,7 @@
|
||||
mod bessel;
|
||||
mod bit_operations;
|
||||
mod complex;
|
||||
mod convert;
|
||||
mod misc;
|
||||
mod number_basis;
|
||||
mod transcendental;
|
||||
546
base/src/functions/engineering/number_basis.rs
Normal file
546
base/src/functions/engineering/number_basis.rs
Normal file
@@ -0,0 +1,546 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
// 8_i64.pow(10);
|
||||
const OCT_MAX: i64 = 1_073_741_824;
|
||||
const OCT_MAX_HALF: i64 = 536_870_912;
|
||||
// 16_i64.pow(10)
|
||||
const HEX_MAX: i64 = 1_099_511_627_776;
|
||||
const HEX_MAX_HALF: i64 = 549_755_813_888;
|
||||
// Binary numbers are 10 bits and the most significant bit is the sign
|
||||
|
||||
fn from_binary_to_decimal(value: f64) -> Result<i64, String> {
|
||||
let value = format!("{value}");
|
||||
|
||||
let result = match i64::from_str_radix(&value, 2) {
|
||||
Ok(b) => b,
|
||||
Err(_) => {
|
||||
return Err("cannot parse into binary".to_string());
|
||||
}
|
||||
};
|
||||
if !(0..=1023).contains(&result) {
|
||||
// 2^10
|
||||
return Err("too large".to_string());
|
||||
} else if result > 511 {
|
||||
// 2^9
|
||||
return Ok(result - 1024);
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
impl Model {
|
||||
// BIN2DEC(number)
|
||||
pub(crate) fn fn_bin2dec(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
match from_binary_to_decimal(value) {
|
||||
Ok(n) => CalcResult::Number(n as f64),
|
||||
Err(message) => CalcResult::new_error(Error::NUM, cell, message),
|
||||
}
|
||||
}
|
||||
|
||||
// BIN2HEX(number, [places])
|
||||
pub(crate) fn fn_bin2hex(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let places = if args.len() == 2 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => Some(f.trunc() as i32),
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(p) = places {
|
||||
if p <= 0 || p > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Not enough places".to_string());
|
||||
}
|
||||
}
|
||||
let value = match from_binary_to_decimal(value) {
|
||||
Ok(n) => n,
|
||||
Err(message) => return CalcResult::new_error(Error::NUM, cell, message),
|
||||
};
|
||||
if value < 0 {
|
||||
CalcResult::String(format!("{:0width$X}", HEX_MAX + value, width = 9))
|
||||
} else {
|
||||
let result = format!("{:X}", value);
|
||||
if let Some(places) = places {
|
||||
if places < result.len() as i32 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Not enough places".to_string(),
|
||||
);
|
||||
}
|
||||
return CalcResult::String(format!("{:0width$X}", value, width = places as usize));
|
||||
}
|
||||
CalcResult::String(result)
|
||||
}
|
||||
}
|
||||
|
||||
// BIN2OCT(number, [places])
|
||||
pub(crate) fn fn_bin2oct(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let places = if args.len() == 2 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => Some(f.trunc() as i32),
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let value = match from_binary_to_decimal(value) {
|
||||
Ok(n) => n,
|
||||
Err(message) => return CalcResult::new_error(Error::NUM, cell, message),
|
||||
};
|
||||
if let Some(p) = places {
|
||||
if p <= 0 || p > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Not enough places".to_string());
|
||||
}
|
||||
}
|
||||
if value < 0 {
|
||||
CalcResult::String(format!("{:0width$o}", OCT_MAX + value, width = 9))
|
||||
} else {
|
||||
let result = format!("{:o}", value);
|
||||
if let Some(places) = places {
|
||||
if places < result.len() as i32 {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Not enough places".to_string(),
|
||||
);
|
||||
}
|
||||
return CalcResult::String(format!("{:0width$o}", value, width = places as usize));
|
||||
}
|
||||
CalcResult::String(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_dec2bin(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value_raw = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let places = if args.len() == 2 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => Some(f.trunc() as i32),
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(p) = places {
|
||||
if p <= 0 || p > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Not enough places".to_string());
|
||||
}
|
||||
}
|
||||
let mut value = value_raw.trunc() as i64;
|
||||
if !(-512..=511).contains(&value) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
if value < 0 {
|
||||
value += 1024;
|
||||
}
|
||||
let result = format!("{:b}", value);
|
||||
if let Some(places) = places {
|
||||
if value_raw > 0.0 && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
let result = format!("{:0width$b}", value, width = places as usize);
|
||||
return CalcResult::String(result);
|
||||
}
|
||||
CalcResult::String(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_dec2hex(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value_raw = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f.trunc(),
|
||||
Err(s) => return s,
|
||||
};
|
||||
let places = if args.len() == 2 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => Some(f.trunc() as i32),
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(p) = places {
|
||||
if p <= 0 || p > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Not enough places".to_string());
|
||||
}
|
||||
}
|
||||
let mut value = value_raw.trunc() as i64;
|
||||
if !(-HEX_MAX_HALF..=HEX_MAX_HALF - 1).contains(&value) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
if value < 0 {
|
||||
value += HEX_MAX;
|
||||
}
|
||||
let result = format!("{:X}", value);
|
||||
if let Some(places) = places {
|
||||
if value_raw > 0.0 && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
let result = format!("{:0width$X}", value, width = places as usize);
|
||||
return CalcResult::String(result);
|
||||
}
|
||||
CalcResult::String(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_dec2oct(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value_raw = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let places = if args.len() == 2 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => Some(f.trunc() as i32),
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(p) = places {
|
||||
if p <= 0 || p > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Not enough places".to_string());
|
||||
}
|
||||
}
|
||||
let mut value = value_raw.trunc() as i64;
|
||||
|
||||
if !(-OCT_MAX_HALF..=OCT_MAX_HALF - 1).contains(&value) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
if value < 0 {
|
||||
value += OCT_MAX;
|
||||
}
|
||||
let result = format!("{:o}", value);
|
||||
if let Some(places) = places {
|
||||
if value_raw > 0.0 && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
let result = format!("{:0width$o}", value, width = places as usize);
|
||||
return CalcResult::String(result);
|
||||
}
|
||||
CalcResult::String(result)
|
||||
}
|
||||
|
||||
// HEX2BIN(number, [places])
|
||||
pub(crate) fn fn_hex2bin(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let places = if args.len() == 2 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => Some(f.trunc() as i32),
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if value.len() > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Value too large".to_string());
|
||||
}
|
||||
if let Some(p) = places {
|
||||
if p <= 0 || p > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Not enough places".to_string());
|
||||
}
|
||||
}
|
||||
let mut value = match i64::from_str_radix(&value, 16) {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Error parsing hex number".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
if value < 0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Negative value".to_string());
|
||||
}
|
||||
|
||||
if value >= HEX_MAX_HALF {
|
||||
value -= HEX_MAX;
|
||||
}
|
||||
if !(-512..=511).contains(&value) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
if value < 0 {
|
||||
value += 1024;
|
||||
}
|
||||
let result = format!("{:b}", value);
|
||||
if let Some(places) = places {
|
||||
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
let result = format!("{:0width$b}", value, width = places as usize);
|
||||
return CalcResult::String(result);
|
||||
}
|
||||
CalcResult::String(result)
|
||||
}
|
||||
|
||||
// HEX2DEC(number)
|
||||
pub(crate) fn fn_hex2dec(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if value.len() > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Value too large".to_string());
|
||||
}
|
||||
let mut value = match i64::from_str_radix(&value, 16) {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Error parsing hex number".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
if value < 0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Negative value".to_string());
|
||||
}
|
||||
|
||||
if value >= HEX_MAX_HALF {
|
||||
value -= HEX_MAX;
|
||||
}
|
||||
CalcResult::Number(value as f64)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_hex2oct(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let places = if args.len() == 2 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => Some(f.trunc() as i32),
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(p) = places {
|
||||
if p <= 0 || p > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Not enough places".to_string());
|
||||
}
|
||||
}
|
||||
if value.len() > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Value too large".to_string());
|
||||
}
|
||||
let mut value = match i64::from_str_radix(&value, 16) {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Error parsing hex number".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
if value < 0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Negative value".to_string());
|
||||
}
|
||||
|
||||
if value > HEX_MAX_HALF {
|
||||
value -= HEX_MAX;
|
||||
}
|
||||
if !(-OCT_MAX_HALF..=OCT_MAX_HALF - 1).contains(&value) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
if value < 0 {
|
||||
value += OCT_MAX;
|
||||
}
|
||||
let result = format!("{:o}", value);
|
||||
if let Some(places) = places {
|
||||
if places <= 0 || (value > 0 && places < result.len() as i32) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
let result = format!("{:0width$o}", value, width = places as usize);
|
||||
return CalcResult::String(result);
|
||||
}
|
||||
CalcResult::String(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_oct2bin(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let places = if args.len() == 2 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => Some(f.trunc() as i32),
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(p) = places {
|
||||
if p <= 0 || p > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Not enough places".to_string());
|
||||
}
|
||||
}
|
||||
let mut value = match i64::from_str_radix(&value, 8) {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Error parsing hex number".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
if value < 0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Negative value".to_string());
|
||||
}
|
||||
|
||||
if value >= OCT_MAX_HALF {
|
||||
value -= OCT_MAX;
|
||||
}
|
||||
if !(-512..=511).contains(&value) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
if value < 0 {
|
||||
value += 1024;
|
||||
}
|
||||
let result = format!("{:b}", value);
|
||||
if let Some(places) = places {
|
||||
if value < 512 && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
let result = format!("{:0width$b}", value, width = places as usize);
|
||||
return CalcResult::String(result);
|
||||
}
|
||||
CalcResult::String(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_oct2dec(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut value = match i64::from_str_radix(&value, 8) {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Error parsing hex number".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
if value < 0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Negative value".to_string());
|
||||
}
|
||||
|
||||
if value >= OCT_MAX_HALF {
|
||||
value -= OCT_MAX
|
||||
}
|
||||
CalcResult::Number(value as f64)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_oct2hex(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !(1..=2).contains(&args.len()) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_string(&args[0], cell) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let places = if args.len() == 2 {
|
||||
match self.get_number_no_bools(&args[1], cell) {
|
||||
Ok(f) => Some(f.trunc() as i32),
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
// There is not a default value for places
|
||||
// But if there is a value it needs to be positive and less than 11
|
||||
if let Some(p) = places {
|
||||
if p <= 0 || p > 10 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Not enough places".to_string());
|
||||
}
|
||||
}
|
||||
let mut value = match i64::from_str_radix(&value, 8) {
|
||||
Ok(f) => f,
|
||||
Err(_) => {
|
||||
return CalcResult::new_error(
|
||||
Error::NUM,
|
||||
cell,
|
||||
"Error parsing hex number".to_string(),
|
||||
);
|
||||
}
|
||||
};
|
||||
if value < 0 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Negative value".to_string());
|
||||
}
|
||||
|
||||
if value >= OCT_MAX_HALF {
|
||||
value -= OCT_MAX;
|
||||
}
|
||||
|
||||
if !(-HEX_MAX_HALF..=HEX_MAX_HALF - 1).contains(&value) {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
if value < 0 {
|
||||
value += HEX_MAX;
|
||||
}
|
||||
let result = format!("{:X}", value);
|
||||
if let Some(places) = places {
|
||||
if value < HEX_MAX_HALF && places < result.len() as i32 {
|
||||
return CalcResult::new_error(Error::NUM, cell, "Out of bounds".to_string());
|
||||
}
|
||||
let result = format!("{:0width$X}", value, width = places as usize);
|
||||
return CalcResult::String(result);
|
||||
}
|
||||
CalcResult::String(result)
|
||||
}
|
||||
}
|
||||
29
base/src/functions/engineering/transcendental/README.md
Normal file
29
base/src/functions/engineering/transcendental/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Creating tests from transcendental functions
|
||||
|
||||
Excel supports a number of transcendental functions like the error functions, gamma nad beta functions.
|
||||
In this folder we have tests for the Bessel functions.
|
||||
Some other platform's implementations of those functions are remarkably poor (including Excel), sometimes failing on the third decimal digit. We strive to do better.
|
||||
|
||||
To properly test you need to compute some known values with established arbitrary precision arithmetic.
|
||||
|
||||
I use for this purpose Arb[1], created by the unrivalled Fredrik Johansson[2]. You might find some python bindings, but I use Julia's Nemo[3]:
|
||||
|
||||
```julia
|
||||
julia> using Nemo
|
||||
julia> CC = AcbField(200)
|
||||
julia> besseli(CC("17"), CC("5.6"))
|
||||
```
|
||||
|
||||
If you are new to Julia, just install Julia and in the Julia terminal run:
|
||||
|
||||
```
|
||||
julia> using Pkg; Pkg.add("Nemo")
|
||||
```
|
||||
|
||||
You only need to do that once (like the R philosophy)
|
||||
|
||||
Will give you any Bessel function of any order (integer or not) of any value real or complex
|
||||
|
||||
[1]: https://arblib.org/
|
||||
[2]: https://fredrikj.net/
|
||||
[3]: https://nemocas.github.io/Nemo.jl/latest/
|
||||
144
base/src/functions/engineering/transcendental/bessel_i.rs
Normal file
144
base/src/functions/engineering/transcendental/bessel_i.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
// This are somewhat lower precision than the BesselJ and BesselY
|
||||
|
||||
// needed for BesselK
|
||||
pub(crate) fn bessel_i0(x: f64) -> f64 {
|
||||
// Parameters of the polynomial approximation
|
||||
let p1 = 1.0;
|
||||
let p2 = 3.5156229;
|
||||
let p3 = 3.0899424;
|
||||
let p4 = 1.2067492;
|
||||
let p5 = 0.2659732;
|
||||
let p6 = 3.60768e-2;
|
||||
let p7 = 4.5813e-3;
|
||||
|
||||
let q1 = 0.39894228;
|
||||
let q2 = 1.328592e-2;
|
||||
let q3 = 2.25319e-3;
|
||||
let q4 = -1.57565e-3;
|
||||
let q5 = 9.16281e-3;
|
||||
let q6 = -2.057706e-2;
|
||||
let q7 = 2.635537e-2;
|
||||
let q8 = -1.647633e-2;
|
||||
let q9 = 3.92377e-3;
|
||||
|
||||
let k1 = 3.75;
|
||||
let ax = x.abs();
|
||||
|
||||
if x.is_infinite() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if ax < k1 {
|
||||
// let xx = x / k1;
|
||||
// let y = xx * xx;
|
||||
// let mut result = 1.0;
|
||||
// let max_iter = 50;
|
||||
// let mut term = 1.0;
|
||||
// for i in 1..max_iter {
|
||||
// term = term * k1*k1*y /(2.0*i as f64).powi(2);
|
||||
// result += term;
|
||||
// };
|
||||
// result
|
||||
|
||||
let xx = x / k1;
|
||||
let y = xx * xx;
|
||||
p1 + y * (p2 + y * (p3 + y * (p4 + y * (p5 + y * (p6 + y * p7)))))
|
||||
} else {
|
||||
let y = k1 / ax;
|
||||
((ax).exp() / (ax).sqrt())
|
||||
* (q1
|
||||
+ y * (q2
|
||||
+ y * (q3 + y * (q4 + y * (q5 + y * (q6 + y * (q7 + y * (q8 + y * q9))))))))
|
||||
}
|
||||
}
|
||||
|
||||
// needed for BesselK
|
||||
pub(crate) fn bessel_i1(x: f64) -> f64 {
|
||||
let p1 = 0.5;
|
||||
let p2 = 0.87890594;
|
||||
let p3 = 0.51498869;
|
||||
let p4 = 0.15084934;
|
||||
let p5 = 2.658733e-2;
|
||||
let p6 = 3.01532e-3;
|
||||
let p7 = 3.2411e-4;
|
||||
|
||||
let q1 = 0.39894228;
|
||||
let q2 = -3.988024e-2;
|
||||
let q3 = -3.62018e-3;
|
||||
let q4 = 1.63801e-3;
|
||||
let q5 = -1.031555e-2;
|
||||
let q6 = 2.282967e-2;
|
||||
let q7 = -2.895312e-2;
|
||||
let q8 = 1.787654e-2;
|
||||
let q9 = -4.20059e-3;
|
||||
|
||||
let k1 = 3.75;
|
||||
let ax = x.abs();
|
||||
|
||||
if ax < k1 {
|
||||
let xx = x / k1;
|
||||
let y = xx * xx;
|
||||
x * (p1 + y * (p2 + y * (p3 + y * (p4 + y * (p5 + y * (p6 + y * p7))))))
|
||||
} else {
|
||||
let y = k1 / ax;
|
||||
let result = ((ax).exp() / (ax).sqrt())
|
||||
* (q1
|
||||
+ y * (q2
|
||||
+ y * (q3 + y * (q4 + y * (q5 + y * (q6 + y * (q7 + y * (q8 + y * q9))))))));
|
||||
if x < 0.0 {
|
||||
return -result;
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bessel_i(n: i32, x: f64) -> f64 {
|
||||
let accuracy = 40;
|
||||
let large_number = 1e10;
|
||||
let small_number = 1e-10;
|
||||
|
||||
if n < 0 {
|
||||
return f64::NAN;
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return bessel_i0(x);
|
||||
}
|
||||
if x == 0.0 {
|
||||
// I_n(0) = 0 for all n!= 0
|
||||
return 0.0;
|
||||
}
|
||||
if n == 1 {
|
||||
return bessel_i1(x);
|
||||
}
|
||||
|
||||
if x.abs() > large_number {
|
||||
return 0.0;
|
||||
};
|
||||
|
||||
let tox = 2.0 / x.abs();
|
||||
let mut bip = 0.0;
|
||||
let mut bi = 1.0;
|
||||
let mut result = 0.0;
|
||||
let m = 2 * (((accuracy * n) as f64).sqrt().trunc() as i32 + n);
|
||||
|
||||
for j in (1..=m).rev() {
|
||||
(bip, bi) = (bi, bip + (j as f64) * tox * bi);
|
||||
// Prevent overflow
|
||||
if bi.abs() > large_number {
|
||||
result *= small_number;
|
||||
bi *= small_number;
|
||||
bip *= small_number;
|
||||
}
|
||||
if j == n {
|
||||
result = bip;
|
||||
}
|
||||
}
|
||||
|
||||
result *= bessel_i0(x) / bi;
|
||||
if (x < 0.0) && (n % 2 == 1) {
|
||||
result = -result;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
402
base/src/functions/engineering/transcendental/bessel_j0_y0.rs
Normal file
402
base/src/functions/engineering/transcendental/bessel_j0_y0.rs
Normal file
@@ -0,0 +1,402 @@
|
||||
/* @(#)e_j0.c 1.3 95/01/18 */
|
||||
/*
|
||||
* ====================================================
|
||||
* Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
|
||||
*
|
||||
* Developed at SunSoft, a Sun Microsystems, Inc. business.
|
||||
* Permission to use, copy, modify, and distribute this
|
||||
* software is freely granted, provided that this notice
|
||||
* is preserved.
|
||||
* ====================================================
|
||||
*/
|
||||
|
||||
/* j0(x), y0(x)
|
||||
* Bessel function of the first and second kinds of order zero.
|
||||
* Method -- j0(x):
|
||||
* 1. For tiny x, we use j0(x) = 1 - x^2/4 + x^4/64 - ...
|
||||
* 2. Reduce x to |x| since j0(x)=j0(-x), and
|
||||
* for x in (0,2)
|
||||
* j0(x) = 1-z/4+ z^2*R0/S0, where z = x*x;
|
||||
* (precision: |j0-1+z/4-z^2R0/S0 |<2**-63.67 )
|
||||
* for x in (2,inf)
|
||||
* j0(x) = sqrt(2/(pi*x))*(p0(x)*cos(x0)-q0(x)*sin(x0))
|
||||
* where x0 = x-pi/4. It is better to compute sin(x0),cos(x0)
|
||||
* as follow:
|
||||
* cos(x0) = cos(x)cos(pi/4)+sin(x)sin(pi/4)
|
||||
* = 1/sqrt(2) * (cos(x) + sin(x))
|
||||
* sin(x0) = sin(x)cos(pi/4)-cos(x)sin(pi/4)
|
||||
* = 1/sqrt(2) * (sin(x) - cos(x))
|
||||
* (To avoid cancellation, use
|
||||
* sin(x) +- cos(x) = -cos(2x)/(sin(x) -+ cos(x))
|
||||
* to compute the worse 1.)
|
||||
*
|
||||
* 3 Special cases
|
||||
* j0(nan)= nan
|
||||
* j0(0) = 1
|
||||
* j0(inf) = 0
|
||||
*
|
||||
* Method -- y0(x):
|
||||
* 1. For x<2.
|
||||
* Since
|
||||
* y0(x) = 2/pi*(j0(x)*(ln(x/2)+Euler) + x^2/4 - ...)
|
||||
* therefore y0(x)-2/pi*j0(x)*ln(x) is an even function.
|
||||
* We use the following function to approximate y0,
|
||||
* y0(x) = U(z)/V(z) + (2/pi)*(j0(x)*ln(x)), z= x^2
|
||||
* where
|
||||
* U(z) = u00 + u01*z + ... + u06*z^6
|
||||
* V(z) = 1 + v01*z + ... + v04*z^4
|
||||
* with absolute approximation error bounded by 2**-72.
|
||||
* Note: For tiny x, U/V = u0 and j0(x)~1, hence
|
||||
* y0(tiny) = u0 + (2/pi)*ln(tiny), (choose tiny<2**-27)
|
||||
* 2. For x>=2.
|
||||
* y0(x) = sqrt(2/(pi*x))*(p0(x)*cos(x0)+q0(x)*sin(x0))
|
||||
* where x0 = x-pi/4. It is better to compute sin(x0),cos(x0)
|
||||
* by the method menti1d above.
|
||||
* 3. Special cases: y0(0)=-inf, y0(x<0)=NaN, y0(inf)=0.
|
||||
*/
|
||||
|
||||
use std::f64::consts::FRAC_2_PI;
|
||||
|
||||
use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
|
||||
|
||||
// R0/S0 on [0, 2.00]
|
||||
const R02: f64 = 1.562_499_999_999_999_5e-2; // 0x3F8FFFFF, 0xFFFFFFFD
|
||||
const R03: f64 = -1.899_792_942_388_547_2e-4; // 0xBF28E6A5, 0xB61AC6E9
|
||||
const R04: f64 = 1.829_540_495_327_006_7e-6; // 0x3EBEB1D1, 0x0C503919
|
||||
const R05: f64 = -4.618_326_885_321_032e-9; // 0xBE33D5E7, 0x73D63FCE
|
||||
const S01: f64 = 1.561_910_294_648_900_1e-2; // 0x3F8FFCE8, 0x82C8C2A4
|
||||
const S02: f64 = 1.169_267_846_633_374_5e-4; // 0x3F1EA6D2, 0xDD57DBF4
|
||||
const S03: f64 = 5.135_465_502_073_181e-7; // 0x3EA13B54, 0xCE84D5A9
|
||||
const S04: f64 = 1.166_140_033_337_9e-9; // 0x3E1408BC, 0xF4745D8F
|
||||
|
||||
/* The asymptotic expansions of pzero is
|
||||
* 1 - 9/128 s^2 + 11025/98304 s^4 - ..., where s = 1/x.
|
||||
* For x >= 2, We approximate pzero by
|
||||
* pzero(x) = 1 + (R/S)
|
||||
* where R = pR0 + pR1*s^2 + pR2*s^4 + ... + pR5*s^10
|
||||
* S = 1 + pS0*s^2 + ... + pS4*s^10
|
||||
* and
|
||||
* | pzero(x)-1-R/S | <= 2 ** ( -60.26)
|
||||
*/
|
||||
const P_R8: [f64; 6] = [
|
||||
/* for x in [inf, 8]=1/[0,0.125] */
|
||||
0.00000000000000000000e+00, /* 0x00000000, 0x00000000 */
|
||||
-7.031_249_999_999_004e-2, /* 0xBFB1FFFF, 0xFFFFFD32 */
|
||||
-8.081_670_412_753_498, /* 0xC02029D0, 0xB44FA779 */
|
||||
-2.570_631_056_797_048_5e2, /* 0xC0701102, 0x7B19E863 */
|
||||
-2.485_216_410_094_288e3, /* 0xC0A36A6E, 0xCD4DCAFC */
|
||||
-5.253_043_804_907_295e3, /* 0xC0B4850B, 0x36CC643D */
|
||||
];
|
||||
const P_S8: [f64; 5] = [
|
||||
1.165_343_646_196_681_8e2, /* 0x405D2233, 0x07A96751 */
|
||||
3.833_744_753_641_218_3e3, /* 0x40ADF37D, 0x50596938 */
|
||||
4.059_785_726_484_725_5e4, /* 0x40E3D2BB, 0x6EB6B05F */
|
||||
1.167_529_725_643_759_2e5, /* 0x40FC810F, 0x8F9FA9BD */
|
||||
4.762_772_841_467_309_6e4, /* 0x40E74177, 0x4F2C49DC */
|
||||
];
|
||||
|
||||
const P_R5: [f64; 6] = [
|
||||
/* for x in [8,4.5454]=1/[0.125,0.22001] */
|
||||
-1.141_254_646_918_945e-11, /* 0xBDA918B1, 0x47E495CC */
|
||||
-7.031_249_408_735_993e-2, /* 0xBFB1FFFF, 0xE69AFBC6 */
|
||||
-4.159_610_644_705_878, /* 0xC010A370, 0xF90C6BBF */
|
||||
-6.767_476_522_651_673e1, /* 0xC050EB2F, 0x5A7D1783 */
|
||||
-3.312_312_996_491_729_7e2, /* 0xC074B3B3, 0x6742CC63 */
|
||||
-3.464_333_883_656_049e2, /* 0xC075A6EF, 0x28A38BD7 */
|
||||
];
|
||||
const P_S5: [f64; 5] = [
|
||||
6.075_393_826_923_003_4e1, /* 0x404E6081, 0x0C98C5DE */
|
||||
1.051_252_305_957_045_8e3, /* 0x40906D02, 0x5C7E2864 */
|
||||
5.978_970_943_338_558e3, /* 0x40B75AF8, 0x8FBE1D60 */
|
||||
9.625_445_143_577_745e3, /* 0x40C2CCB8, 0xFA76FA38 */
|
||||
2.406_058_159_229_391e3, /* 0x40A2CC1D, 0xC70BE864 */
|
||||
];
|
||||
|
||||
const P_R3: [f64; 6] = [
|
||||
/* for x in [4.547,2.8571]=1/[0.2199,0.35001] */
|
||||
-2.547_046_017_719_519e-9, /* 0xBE25E103, 0x6FE1AA86 */
|
||||
-7.031_196_163_814_817e-2, /* 0xBFB1FFF6, 0xF7C0E24B */
|
||||
-2.409_032_215_495_296, /* 0xC00345B2, 0xAEA48074 */
|
||||
-2.196_597_747_348_831e1, /* 0xC035F74A, 0x4CB94E14 */
|
||||
-5.807_917_047_017_376e1, /* 0xC04D0A22, 0x420A1A45 */
|
||||
-3.144_794_705_948_885e1, /* 0xC03F72AC, 0xA892D80F */
|
||||
];
|
||||
const P_S3: [f64; 5] = [
|
||||
3.585_603_380_552_097e1, /* 0x4041ED92, 0x84077DD3 */
|
||||
3.615_139_830_503_038_6e2, /* 0x40769839, 0x464A7C0E */
|
||||
1.193_607_837_921_115_3e3, /* 0x4092A66E, 0x6D1061D6 */
|
||||
1.127_996_798_569_074_1e3, /* 0x40919FFC, 0xB8C39B7E */
|
||||
1.735_809_308_133_357_5e2, /* 0x4065B296, 0xFC379081 */
|
||||
];
|
||||
|
||||
const P_R2: [f64; 6] = [
|
||||
/* for x in [2.8570,2]=1/[0.3499,0.5] */
|
||||
-8.875_343_330_325_264e-8, /* 0xBE77D316, 0xE927026D */
|
||||
-7.030_309_954_836_247e-2, /* 0xBFB1FF62, 0x495E1E42 */
|
||||
-1.450_738_467_809_529_9, /* 0xBFF73639, 0x8A24A843 */
|
||||
-7.635_696_138_235_278, /* 0xC01E8AF3, 0xEDAFA7F3 */
|
||||
-1.119_316_688_603_567_5e1, /* 0xC02662E6, 0xC5246303 */
|
||||
-3.233_645_793_513_353_4, /* 0xC009DE81, 0xAF8FE70F */
|
||||
];
|
||||
const P_S2: [f64; 5] = [
|
||||
2.222_029_975_320_888e1, /* 0x40363865, 0x908B5959 */
|
||||
1.362_067_942_182_152e2, /* 0x4061069E, 0x0EE8878F */
|
||||
2.704_702_786_580_835e2, /* 0x4070E786, 0x42EA079B */
|
||||
1.538_753_942_083_203_3e2, /* 0x40633C03, 0x3AB6FAFF */
|
||||
1.465_761_769_482_562e1, /* 0x402D50B3, 0x44391809 */
|
||||
];
|
||||
|
||||
// Note: This function is only called for ix>=0x40000000 (see above)
|
||||
fn pzero(x: f64) -> f64 {
|
||||
let ix = high_word(x) & 0x7fffffff;
|
||||
// ix>=0x40000000 && ix<=0x48000000);
|
||||
let (p, q) = if ix >= 0x40200000 {
|
||||
(P_R8, P_S8)
|
||||
} else if ix >= 0x40122E8B {
|
||||
(P_R5, P_S5)
|
||||
} else if ix >= 0x4006DB6D {
|
||||
(P_R3, P_S3)
|
||||
} else {
|
||||
(P_R2, P_S2)
|
||||
};
|
||||
let z = 1.0 / (x * x);
|
||||
let r = p[0] + z * (p[1] + z * (p[2] + z * (p[3] + z * (p[4] + z * p[5]))));
|
||||
let s = 1.0 + z * (q[0] + z * (q[1] + z * (q[2] + z * (q[3] + z * q[4]))));
|
||||
1.0 + r / s
|
||||
}
|
||||
|
||||
/* For x >= 8, the asymptotic expansions of qzero is
|
||||
* -1/8 s + 75/1024 s^3 - ..., where s = 1/x.
|
||||
* We approximate pzero by
|
||||
* qzero(x) = s*(-1.25 + (R/S))
|
||||
* where R = qR0 + qR1*s^2 + qR2*s^4 + ... + qR5*s^10
|
||||
* S = 1 + qS0*s^2 + ... + qS5*s^12
|
||||
* and
|
||||
* | qzero(x)/s +1.25-R/S | <= 2 ** ( -61.22)
|
||||
*/
|
||||
const Q_R8: [f64; 6] = [
|
||||
/* for x in [inf, 8]=1/[0,0.125] */
|
||||
0.00000000000000000000e+00, /* 0x00000000, 0x00000000 */
|
||||
7.324_218_749_999_35e-2, /* 0x3FB2BFFF, 0xFFFFFE2C */
|
||||
1.176_820_646_822_527e1, /* 0x40278952, 0x5BB334D6 */
|
||||
5.576_733_802_564_019e2, /* 0x40816D63, 0x15301825 */
|
||||
8.859_197_207_564_686e3, /* 0x40C14D99, 0x3E18F46D */
|
||||
3.701_462_677_768_878e4, /* 0x40E212D4, 0x0E901566 */
|
||||
];
|
||||
const Q_S8: [f64; 6] = [
|
||||
1.637_760_268_956_898_2e2, /* 0x406478D5, 0x365B39BC */
|
||||
8.098_344_946_564_498e3, /* 0x40BFA258, 0x4E6B0563 */
|
||||
1.425_382_914_191_204_8e5, /* 0x41016652, 0x54D38C3F */
|
||||
8.033_092_571_195_144e5, /* 0x412883DA, 0x83A52B43 */
|
||||
8.405_015_798_190_605e5, /* 0x4129A66B, 0x28DE0B3D */
|
||||
-3.438_992_935_378_666e5, /* 0xC114FD6D, 0x2C9530C5 */
|
||||
];
|
||||
|
||||
const Q_R5: [f64; 6] = [
|
||||
/* for x in [8,4.5454]=1/[0.125,0.22001] */
|
||||
1.840_859_635_945_155_3e-11, /* 0x3DB43D8F, 0x29CC8CD9 */
|
||||
7.324_217_666_126_848e-2, /* 0x3FB2BFFF, 0xD172B04C */
|
||||
5.835_635_089_620_569_5, /* 0x401757B0, 0xB9953DD3 */
|
||||
1.351_115_772_864_498_3e2, /* 0x4060E392, 0x0A8788E9 */
|
||||
1.027_243_765_961_641e3, /* 0x40900CF9, 0x9DC8C481 */
|
||||
1.989_977_858_646_053_8e3, /* 0x409F17E9, 0x53C6E3A6 */
|
||||
];
|
||||
const Q_S5: [f64; 6] = [
|
||||
8.277_661_022_365_378e1, /* 0x4054B1B3, 0xFB5E1543 */
|
||||
2.077_814_164_213_93e3, /* 0x40A03BA0, 0xDA21C0CE */
|
||||
1.884_728_877_857_181e4, /* 0x40D267D2, 0x7B591E6D */
|
||||
5.675_111_228_949_473e4, /* 0x40EBB5E3, 0x97E02372 */
|
||||
3.597_675_384_251_145e4, /* 0x40E19118, 0x1F7A54A0 */
|
||||
-5.354_342_756_019_448e3, /* 0xC0B4EA57, 0xBEDBC609 */
|
||||
];
|
||||
|
||||
const Q_R3: [f64; 6] = [
|
||||
/* for x in [4.547,2.8571]=1/[0.2199,0.35001] */
|
||||
4.377_410_140_897_386e-9, /* 0x3E32CD03, 0x6ADECB82 */
|
||||
7.324_111_800_429_114e-2, /* 0x3FB2BFEE, 0x0E8D0842 */
|
||||
3.344_231_375_161_707, /* 0x400AC0FC, 0x61149CF5 */
|
||||
4.262_184_407_454_126_5e1, /* 0x40454F98, 0x962DAEDD */
|
||||
1.708_080_913_405_656e2, /* 0x406559DB, 0xE25EFD1F */
|
||||
1.667_339_486_966_511_7e2, /* 0x4064D77C, 0x81FA21E0 */
|
||||
];
|
||||
const Q_S3: [f64; 6] = [
|
||||
4.875_887_297_245_872e1, /* 0x40486122, 0xBFE343A6 */
|
||||
7.096_892_210_566_06e2, /* 0x40862D83, 0x86544EB3 */
|
||||
3.704_148_226_201_113_6e3, /* 0x40ACF04B, 0xE44DFC63 */
|
||||
6.460_425_167_525_689e3, /* 0x40B93C6C, 0xD7C76A28 */
|
||||
2.516_333_689_203_689_6e3, /* 0x40A3A8AA, 0xD94FB1C0 */
|
||||
-1.492_474_518_361_564e2, /* 0xC062A7EB, 0x201CF40F */
|
||||
];
|
||||
|
||||
const Q_R2: [f64; 6] = [
|
||||
/* for x in [2.8570,2]=1/[0.3499,0.5] */
|
||||
1.504_444_448_869_832_7e-7, /* 0x3E84313B, 0x54F76BDB */
|
||||
7.322_342_659_630_793e-2, /* 0x3FB2BEC5, 0x3E883E34 */
|
||||
1.998_191_740_938_16, /* 0x3FFFF897, 0xE727779C */
|
||||
1.449_560_293_478_857_4e1, /* 0x402CFDBF, 0xAAF96FE5 */
|
||||
3.166_623_175_047_815_4e1, /* 0x403FAA8E, 0x29FBDC4A */
|
||||
1.625_270_757_109_292_7e1, /* 0x403040B1, 0x71814BB4 */
|
||||
];
|
||||
const Q_S2: [f64; 6] = [
|
||||
3.036_558_483_552_192e1, /* 0x403E5D96, 0xF7C07AED */
|
||||
2.693_481_186_080_498_4e2, /* 0x4070D591, 0xE4D14B40 */
|
||||
8.447_837_575_953_201e2, /* 0x408A6645, 0x22B3BF22 */
|
||||
8.829_358_451_124_886e2, /* 0x408B977C, 0x9C5CC214 */
|
||||
2.126_663_885_117_988_3e2, /* 0x406A9553, 0x0E001365 */
|
||||
-5.310_954_938_826_669_5, /* 0xC0153E6A, 0xF8B32931 */
|
||||
];
|
||||
|
||||
fn qzero(x: f64) -> f64 {
|
||||
let ix = high_word(x) & 0x7fffffff;
|
||||
let (p, q) = if ix >= 0x40200000 {
|
||||
(Q_R8, Q_S8)
|
||||
} else if ix >= 0x40122E8B {
|
||||
(Q_R5, Q_S5)
|
||||
} else if ix >= 0x4006DB6D {
|
||||
(Q_R3, Q_S3)
|
||||
} else {
|
||||
(Q_R2, Q_S2)
|
||||
};
|
||||
let z = 1.0 / (x * x);
|
||||
let r = p[0] + z * (p[1] + z * (p[2] + z * (p[3] + z * (p[4] + z * p[5]))));
|
||||
let s = 1.0 + z * (q[0] + z * (q[1] + z * (q[2] + z * (q[3] + z * (q[4] + z * q[5])))));
|
||||
(-0.125 + r / s) / x
|
||||
}
|
||||
|
||||
const U00: f64 = -7.380_429_510_868_723e-2; /* 0xBFB2E4D6, 0x99CBD01F */
|
||||
const U01: f64 = 1.766_664_525_091_811_2e-1; /* 0x3FC69D01, 0x9DE9E3FC */
|
||||
const U02: f64 = -1.381_856_719_455_969e-2; /* 0xBF8C4CE8, 0xB16CFA97 */
|
||||
const U03: f64 = 3.474_534_320_936_836_5e-4; /* 0x3F36C54D, 0x20B29B6B */
|
||||
const U04: f64 = -3.814_070_537_243_641_6e-6; /* 0xBECFFEA7, 0x73D25CAD */
|
||||
const U05: f64 = 1.955_901_370_350_229_2e-8; /* 0x3E550057, 0x3B4EABD4 */
|
||||
const U06: f64 = -3.982_051_941_321_034e-11; /* 0xBDC5E43D, 0x693FB3C8 */
|
||||
const V01: f64 = 1.273_048_348_341_237e-2; /* 0x3F8A1270, 0x91C9C71A */
|
||||
const V02: f64 = 7.600_686_273_503_533e-5; /* 0x3F13ECBB, 0xF578C6C1 */
|
||||
const V03: f64 = 2.591_508_518_404_578e-7; /* 0x3E91642D, 0x7FF202FD */
|
||||
const V04: f64 = 4.411_103_113_326_754_7e-10; /* 0x3DFE5018, 0x3BD6D9EF */
|
||||
|
||||
pub(crate) fn y0(x: f64) -> f64 {
|
||||
let (lx, hx) = split_words(x);
|
||||
let ix = 0x7fffffff & hx;
|
||||
|
||||
// Y0(NaN) is NaN, y0(-inf) is Nan, y0(inf) is 0
|
||||
if ix >= 0x7ff00000 {
|
||||
return 1.0 / (x + x * x);
|
||||
}
|
||||
if (ix | lx) == 0 {
|
||||
return f64::NEG_INFINITY;
|
||||
}
|
||||
if hx < 0 {
|
||||
return f64::NAN;
|
||||
}
|
||||
|
||||
if ix >= 0x40000000 {
|
||||
// |x| >= 2.0
|
||||
// y0(x) = sqrt(2/(pi*x))*(p0(x)*sin(x0)+q0(x)*cos(x0))
|
||||
// where x0 = x-pi/4
|
||||
// Better formula:
|
||||
// cos(x0) = cos(x)cos(pi/4)+sin(x)sin(pi/4)
|
||||
// = 1/sqrt(2) * (sin(x) + cos(x))
|
||||
// sin(x0) = sin(x)cos(3pi/4)-cos(x)sin(3pi/4)
|
||||
// = 1/sqrt(2) * (sin(x) - cos(x))
|
||||
// To avoid cancellation, use
|
||||
// sin(x) +- cos(x) = -cos(2x)/(sin(x) -+ cos(x))
|
||||
// to compute the worse 1.
|
||||
|
||||
let s = x.sin();
|
||||
let c = x.cos();
|
||||
let mut ss = s - c;
|
||||
let mut cc = s + c;
|
||||
|
||||
// j0(x) = 1/sqrt(pi) * (P(0,x)*cc - Q(0,x)*ss) / sqrt(x)
|
||||
// y0(x) = 1/sqrt(pi) * (P(0,x)*ss + Q(0,x)*cc) / sqrt(x)
|
||||
|
||||
if ix < 0x7fe00000 {
|
||||
// make sure x+x not overflow
|
||||
let z = -(x + x).cos();
|
||||
if (s * c) < 0.0 {
|
||||
cc = z / ss;
|
||||
} else {
|
||||
ss = z / cc;
|
||||
}
|
||||
}
|
||||
return if ix > 0x48000000 {
|
||||
FRAC_2_SQRT_PI * ss / x.sqrt()
|
||||
} else {
|
||||
let u = pzero(x);
|
||||
let v = qzero(x);
|
||||
FRAC_2_SQRT_PI * (u * ss + v * cc) / x.sqrt()
|
||||
};
|
||||
}
|
||||
|
||||
if ix <= 0x3e400000 {
|
||||
// x < 2^(-27)
|
||||
return U00 + FRAC_2_PI * x.ln();
|
||||
}
|
||||
let z = x * x;
|
||||
let u = U00 + z * (U01 + z * (U02 + z * (U03 + z * (U04 + z * (U05 + z * U06)))));
|
||||
let v = 1.0 + z * (V01 + z * (V02 + z * (V03 + z * V04)));
|
||||
u / v + FRAC_2_PI * (j0(x) * x.ln())
|
||||
}
|
||||
|
||||
pub(crate) fn j0(x: f64) -> f64 {
|
||||
let hx = high_word(x);
|
||||
let ix = hx & 0x7fffffff;
|
||||
if x.is_nan() {
|
||||
return x;
|
||||
} else if x.is_infinite() {
|
||||
return 0.0;
|
||||
}
|
||||
// the function is even
|
||||
let x = x.abs();
|
||||
if ix >= 0x40000000 {
|
||||
// |x| >= 2.0
|
||||
// let (s, c) = x.sin_cos()
|
||||
let s = x.sin();
|
||||
let c = x.cos();
|
||||
let mut ss = s - c;
|
||||
let mut cc = s + c;
|
||||
// makes sure that x+x does not overflow
|
||||
if ix < 0x7fe00000 {
|
||||
// |x| < f64::MAX / 2.0
|
||||
let z = -(x + x).cos();
|
||||
if s * c < 0.0 {
|
||||
cc = z / ss;
|
||||
} else {
|
||||
ss = z / cc;
|
||||
}
|
||||
}
|
||||
|
||||
// j0(x) = 1/sqrt(pi) * (P(0,x)*cc - Q(0,x)*ss) / sqrt(x)
|
||||
// y0(x) = 1/sqrt(pi) * (P(0,x)*ss + Q(0,x)*cc) / sqrt(x)
|
||||
return if ix > 0x48000000 {
|
||||
// x < 5.253807105661922e-287 (2^(-951))
|
||||
FRAC_2_SQRT_PI * cc / (x.sqrt())
|
||||
} else {
|
||||
let u = pzero(x);
|
||||
let v = qzero(x);
|
||||
FRAC_2_SQRT_PI * (u * cc - v * ss) / x.sqrt()
|
||||
};
|
||||
};
|
||||
if ix < 0x3f200000 {
|
||||
// |x| < 2^(-13)
|
||||
if HUGE + x > 1.0 {
|
||||
// raise inexact if x != 0
|
||||
if ix < 0x3e400000 {
|
||||
return 1.0; // |x|<2^(-27)
|
||||
} else {
|
||||
return 1.0 - 0.25 * x * x;
|
||||
}
|
||||
}
|
||||
}
|
||||
let z = x * x;
|
||||
let r = z * (R02 + z * (R03 + z * (R04 + z * R05)));
|
||||
let s = 1.0 + z * (S01 + z * (S02 + z * (S03 + z * S04)));
|
||||
if ix < 0x3FF00000 {
|
||||
// |x| < 1.00
|
||||
1.0 + z * (-0.25 + (r / s))
|
||||
} else {
|
||||
let u = 0.5 * x;
|
||||
(1.0 + u) * (1.0 - u) + z * (r / s)
|
||||
}
|
||||
}
|
||||
391
base/src/functions/engineering/transcendental/bessel_j1_y1.rs
Normal file
391
base/src/functions/engineering/transcendental/bessel_j1_y1.rs
Normal file
@@ -0,0 +1,391 @@
|
||||
/*
|
||||
* ====================================================
|
||||
* Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
|
||||
*
|
||||
* Developed at SunSoft, a Sun Microsystems, Inc. business.
|
||||
* Permission to use, copy, modify, and distribute this
|
||||
* software is freely granted, provided that this notice
|
||||
* is preserved.
|
||||
* ====================================================
|
||||
*/
|
||||
|
||||
/* __ieee754_j1(x), __ieee754_y1(x)
|
||||
* Bessel function of the first and second kinds of order zero.
|
||||
* Method -- j1(x):
|
||||
* 1. For tiny x, we use j1(x) = x/2 - x^3/16 + x^5/384 - ...
|
||||
* 2. Reduce x to |x| since j1(x)=-j1(-x), and
|
||||
* for x in (0,2)
|
||||
* j1(x) = x/2 + x*z*R0/S0, where z = x*x;
|
||||
* (precision: |j1/x - 1/2 - R0/S0 |<2**-61.51 )
|
||||
* for x in (2,inf)
|
||||
* j1(x) = sqrt(2/(pi*x))*(p1(x)*cos(x1)-q1(x)*sin(x1))
|
||||
* y1(x) = sqrt(2/(pi*x))*(p1(x)*sin(x1)+q1(x)*cos(x1))
|
||||
* where x1 = x-3*pi/4. It is better to compute sin(x1),cos(x1)
|
||||
* as follow:
|
||||
* cos(x1) = cos(x)cos(3pi/4)+sin(x)sin(3pi/4)
|
||||
* = 1/sqrt(2) * (sin(x) - cos(x))
|
||||
* sin(x1) = sin(x)cos(3pi/4)-cos(x)sin(3pi/4)
|
||||
* = -1/sqrt(2) * (sin(x) + cos(x))
|
||||
* (To avoid cancellation, use
|
||||
* sin(x) +- cos(x) = -cos(2x)/(sin(x) -+ cos(x))
|
||||
* to compute the worse one.)
|
||||
*
|
||||
* 3 Special cases
|
||||
* j1(nan)= nan
|
||||
* j1(0) = 0
|
||||
* j1(inf) = 0
|
||||
*
|
||||
* Method -- y1(x):
|
||||
* 1. screen out x<=0 cases: y1(0)=-inf, y1(x<0)=NaN
|
||||
* 2. For x<2.
|
||||
* Since
|
||||
* y1(x) = 2/pi*(j1(x)*(ln(x/2)+Euler)-1/x-x/2+5/64*x^3-...)
|
||||
* therefore y1(x)-2/pi*j1(x)*ln(x)-1/x is an odd function.
|
||||
* We use the following function to approximate y1,
|
||||
* y1(x) = x*U(z)/V(z) + (2/pi)*(j1(x)*ln(x)-1/x), z= x^2
|
||||
* where for x in [0,2] (abs err less than 2**-65.89)
|
||||
* U(z) = U0[0] + U0[1]*z + ... + U0[4]*z^4
|
||||
* V(z) = 1 + v0[0]*z + ... + v0[4]*z^5
|
||||
* Note: For tiny x, 1/x dominate y1 and hence
|
||||
* y1(tiny) = -2/pi/tiny, (choose tiny<2**-54)
|
||||
* 3. For x>=2.
|
||||
* y1(x) = sqrt(2/(pi*x))*(p1(x)*sin(x1)+q1(x)*cos(x1))
|
||||
* where x1 = x-3*pi/4. It is better to compute sin(x1),cos(x1)
|
||||
* by method mentioned above.
|
||||
*/
|
||||
|
||||
use std::f64::consts::FRAC_2_PI;
|
||||
|
||||
use super::bessel_util::{high_word, split_words, FRAC_2_SQRT_PI, HUGE};
|
||||
|
||||
// R0/S0 on [0,2]
|
||||
const R00: f64 = -6.25e-2; // 0xBFB00000, 0x00000000
|
||||
const R01: f64 = 1.407_056_669_551_897e-3; // 0x3F570D9F, 0x98472C61
|
||||
const R02: f64 = -1.599_556_310_840_356e-5; // 0xBEF0C5C6, 0xBA169668
|
||||
const R03: f64 = 4.967_279_996_095_844_5e-8; // 0x3E6AAAFA, 0x46CA0BD9
|
||||
const S01: f64 = 1.915_375_995_383_634_6e-2; // 0x3F939D0B, 0x12637E53
|
||||
const S02: f64 = 1.859_467_855_886_309_2e-4; // 0x3F285F56, 0xB9CDF664
|
||||
const S03: f64 = 1.177_184_640_426_236_8e-6; // 0x3EB3BFF8, 0x333F8498
|
||||
const S04: f64 = 5.046_362_570_762_170_4e-9; // 0x3E35AC88, 0xC97DFF2C
|
||||
const S05: f64 = 1.235_422_744_261_379_1e-11; // 0x3DAB2ACF, 0xCFB97ED8
|
||||
|
||||
pub(crate) fn j1(x: f64) -> f64 {
|
||||
let hx = high_word(x);
|
||||
let ix = hx & 0x7fffffff;
|
||||
if ix >= 0x7ff00000 {
|
||||
return 1.0 / x;
|
||||
}
|
||||
let y = x.abs();
|
||||
if ix >= 0x40000000 {
|
||||
/* |x| >= 2.0 */
|
||||
let s = y.sin();
|
||||
let c = y.cos();
|
||||
let mut ss = -s - c;
|
||||
let mut cc = s - c;
|
||||
if ix < 0x7fe00000 {
|
||||
/* make sure y+y not overflow */
|
||||
let z = (y + y).cos();
|
||||
if s * c > 0.0 {
|
||||
cc = z / ss;
|
||||
} else {
|
||||
ss = z / cc;
|
||||
}
|
||||
}
|
||||
|
||||
// j1(x) = 1/sqrt(pi) * (P(1,x)*cc - Q(1,x)*ss) / sqrt(x)
|
||||
// y1(x) = 1/sqrt(pi) * (P(1,x)*ss + Q(1,x)*cc) / sqrt(x)
|
||||
|
||||
let z = if ix > 0x48000000 {
|
||||
FRAC_2_SQRT_PI * cc / y.sqrt()
|
||||
} else {
|
||||
let u = pone(y);
|
||||
let v = qone(y);
|
||||
FRAC_2_SQRT_PI * (u * cc - v * ss) / y.sqrt()
|
||||
};
|
||||
if hx < 0 {
|
||||
return -z;
|
||||
} else {
|
||||
return z;
|
||||
}
|
||||
}
|
||||
if ix < 0x3e400000 {
|
||||
/* |x|<2**-27 */
|
||||
if HUGE + x > 1.0 {
|
||||
return 0.5 * x; /* inexact if x!=0 necessary */
|
||||
}
|
||||
}
|
||||
let z = x * x;
|
||||
let mut r = z * (R00 + z * (R01 + z * (R02 + z * R03)));
|
||||
let s = 1.0 + z * (S01 + z * (S02 + z * (S03 + z * (S04 + z * S05))));
|
||||
r *= x;
|
||||
x * 0.5 + r / s
|
||||
}
|
||||
|
||||
const U0: [f64; 5] = [
|
||||
-1.960_570_906_462_389_4e-1, /* 0xBFC91866, 0x143CBC8A */
|
||||
5.044_387_166_398_113e-2, /* 0x3FA9D3C7, 0x76292CD1 */
|
||||
-1.912_568_958_757_635_5e-3, /* 0xBF5F55E5, 0x4844F50F */
|
||||
2.352_526_005_616_105e-5, /* 0x3EF8AB03, 0x8FA6B88E */
|
||||
-9.190_991_580_398_789e-8, /* 0xBE78AC00, 0x569105B8 */
|
||||
];
|
||||
const V0: [f64; 5] = [
|
||||
1.991_673_182_366_499e-2, /* 0x3F94650D, 0x3F4DA9F0 */
|
||||
2.025_525_810_251_351_7e-4, /* 0x3F2A8C89, 0x6C257764 */
|
||||
1.356_088_010_975_162_3e-6, /* 0x3EB6C05A, 0x894E8CA6 */
|
||||
6.227_414_523_646_215e-9, /* 0x3E3ABF1D, 0x5BA69A86 */
|
||||
1.665_592_462_079_920_8e-11, /* 0x3DB25039, 0xDACA772A */
|
||||
];
|
||||
|
||||
pub(crate) fn y1(x: f64) -> f64 {
|
||||
let (lx, hx) = split_words(x);
|
||||
let ix = 0x7fffffff & hx;
|
||||
// if Y1(NaN) is NaN, Y1(-inf) is NaN, Y1(inf) is 0
|
||||
if ix >= 0x7ff00000 {
|
||||
return 1.0 / (x + x * x);
|
||||
}
|
||||
if (ix | lx) == 0 {
|
||||
return f64::NEG_INFINITY;
|
||||
}
|
||||
if hx < 0 {
|
||||
return f64::NAN;
|
||||
}
|
||||
if ix >= 0x40000000 {
|
||||
// |x| >= 2.0
|
||||
let s = x.sin();
|
||||
let c = x.cos();
|
||||
let mut ss = -s - c;
|
||||
let mut cc = s - c;
|
||||
if ix < 0x7fe00000 {
|
||||
// make sure x+x not overflow
|
||||
let z = (x + x).cos();
|
||||
if s * c > 0.0 {
|
||||
cc = z / ss;
|
||||
} else {
|
||||
ss = z / cc;
|
||||
}
|
||||
}
|
||||
/* y1(x) = sqrt(2/(pi*x))*(p1(x)*sin(x0)+q1(x)*cos(x0))
|
||||
* where x0 = x-3pi/4
|
||||
* Better formula:
|
||||
* cos(x0) = cos(x)cos(3pi/4)+sin(x)sin(3pi/4)
|
||||
* = 1/sqrt(2) * (sin(x) - cos(x))
|
||||
* sin(x0) = sin(x)cos(3pi/4)-cos(x)sin(3pi/4)
|
||||
* = -1/sqrt(2) * (cos(x) + sin(x))
|
||||
* To avoid cancellation, use
|
||||
* sin(x) +- cos(x) = -cos(2x)/(sin(x) -+ cos(x))
|
||||
* to compute the worse one.
|
||||
*/
|
||||
return if ix > 0x48000000 {
|
||||
FRAC_2_SQRT_PI * ss / x.sqrt()
|
||||
} else {
|
||||
let u = pone(x);
|
||||
let v = qone(x);
|
||||
FRAC_2_SQRT_PI * (u * ss + v * cc) / x.sqrt()
|
||||
};
|
||||
}
|
||||
if ix <= 0x3c900000 {
|
||||
// x < 2^(-54)
|
||||
return -FRAC_2_PI / x;
|
||||
}
|
||||
let z = x * x;
|
||||
let u = U0[0] + z * (U0[1] + z * (U0[2] + z * (U0[3] + z * U0[4])));
|
||||
let v = 1.0 + z * (V0[0] + z * (V0[1] + z * (V0[2] + z * (V0[3] + z * V0[4]))));
|
||||
x * (u / v) + FRAC_2_PI * (j1(x) * x.ln() - 1.0 / x)
|
||||
}
|
||||
|
||||
/* For x >= 8, the asymptotic expansions of pone is
|
||||
* 1 + 15/128 s^2 - 4725/2^15 s^4 - ..., where s = 1/x.
|
||||
* We approximate pone by
|
||||
* pone(x) = 1 + (R/S)
|
||||
* where R = pr0 + pr1*s^2 + pr2*s^4 + ... + pr5*s^10
|
||||
* S = 1 + ps0*s^2 + ... + ps4*s^10
|
||||
* and
|
||||
* | pone(x)-1-R/S | <= 2 ** ( -60.06)
|
||||
*/
|
||||
|
||||
const PR8: [f64; 6] = [
|
||||
/* for x in [inf, 8]=1/[0,0.125] */
|
||||
0.00000000000000000000e+00, /* 0x00000000, 0x00000000 */
|
||||
1.171_874_999_999_886_5e-1, /* 0x3FBDFFFF, 0xFFFFFCCE */
|
||||
1.323_948_065_930_735_8e1, /* 0x402A7A9D, 0x357F7FCE */
|
||||
4.120_518_543_073_785_6e2, /* 0x4079C0D4, 0x652EA590 */
|
||||
3.874_745_389_139_605_3e3, /* 0x40AE457D, 0xA3A532CC */
|
||||
7.914_479_540_318_917e3, /* 0x40BEEA7A, 0xC32782DD */
|
||||
];
|
||||
|
||||
const PS8: [f64; 5] = [
|
||||
1.142_073_703_756_784_1e2, /* 0x405C8D45, 0x8E656CAC */
|
||||
3.650_930_834_208_534_6e3, /* 0x40AC85DC, 0x964D274F */
|
||||
3.695_620_602_690_334_6e4, /* 0x40E20B86, 0x97C5BB7F */
|
||||
9.760_279_359_349_508e4, /* 0x40F7D42C, 0xB28F17BB */
|
||||
3.080_427_206_278_888e4, /* 0x40DE1511, 0x697A0B2D */
|
||||
];
|
||||
|
||||
const PR5: [f64; 6] = [
|
||||
/* for x in [8,4.5454]=1/[0.125,0.22001] */
|
||||
1.319_905_195_562_435_2e-11, /* 0x3DAD0667, 0xDAE1CA7D */
|
||||
1.171_874_931_906_141e-1, /* 0x3FBDFFFF, 0xE2C10043 */
|
||||
6.802_751_278_684_329, /* 0x401B3604, 0x6E6315E3 */
|
||||
1.083_081_829_901_891_1e2, /* 0x405B13B9, 0x452602ED */
|
||||
5.176_361_395_331_998e2, /* 0x40802D16, 0xD052D649 */
|
||||
5.287_152_013_633_375e2, /* 0x408085B8, 0xBB7E0CB7 */
|
||||
];
|
||||
const PS5: [f64; 5] = [
|
||||
5.928_059_872_211_313e1, /* 0x404DA3EA, 0xA8AF633D */
|
||||
9.914_014_187_336_144e2, /* 0x408EFB36, 0x1B066701 */
|
||||
5.353_266_952_914_88e3, /* 0x40B4E944, 0x5706B6FB */
|
||||
7.844_690_317_495_512e3, /* 0x40BEA4B0, 0xB8A5BB15 */
|
||||
1.504_046_888_103_610_6e3, /* 0x40978030, 0x036F5E51 */
|
||||
];
|
||||
|
||||
const PR3: [f64; 6] = [
|
||||
3.025_039_161_373_736e-9, /* 0x3E29FC21, 0xA7AD9EDD */
|
||||
1.171_868_655_672_535_9e-1, /* 0x3FBDFFF5, 0x5B21D17B */
|
||||
3.932_977_500_333_156_4, /* 0x400F76BC, 0xE85EAD8A */
|
||||
3.511_940_355_916_369e1, /* 0x40418F48, 0x9DA6D129 */
|
||||
9.105_501_107_507_813e1, /* 0x4056C385, 0x4D2C1837 */
|
||||
4.855_906_851_973_649e1, /* 0x4048478F, 0x8EA83EE5 */
|
||||
];
|
||||
const PS3: [f64; 5] = [
|
||||
3.479_130_950_012_515e1, /* 0x40416549, 0xA134069C */
|
||||
3.367_624_587_478_257_5e2, /* 0x40750C33, 0x07F1A75F */
|
||||
1.046_871_399_757_751_3e3, /* 0x40905B7C, 0x5037D523 */
|
||||
8.908_113_463_982_564e2, /* 0x408BD67D, 0xA32E31E9 */
|
||||
1.037_879_324_396_392_8e2, /* 0x4059F26D, 0x7C2EED53 */
|
||||
];
|
||||
|
||||
const PR2: [f64; 6] = [
|
||||
/* for x in [2.8570,2]=1/[0.3499,0.5] */
|
||||
1.077_108_301_068_737_4e-7, /* 0x3E7CE9D4, 0xF65544F4 */
|
||||
1.171_762_194_626_833_5e-1, /* 0x3FBDFF42, 0xBE760D83 */
|
||||
2.368_514_966_676_088, /* 0x4002F2B7, 0xF98FAEC0 */
|
||||
1.224_261_091_482_612_3e1, /* 0x40287C37, 0x7F71A964 */
|
||||
1.769_397_112_716_877_3e1, /* 0x4031B1A8, 0x177F8EE2 */
|
||||
5.073_523_125_888_185, /* 0x40144B49, 0xA574C1FE */
|
||||
];
|
||||
const PS2: [f64; 5] = [
|
||||
2.143_648_593_638_214e1, /* 0x40356FBD, 0x8AD5ECDC */
|
||||
1.252_902_271_684_027_5e2, /* 0x405F5293, 0x14F92CD5 */
|
||||
2.322_764_690_571_628e2, /* 0x406D08D8, 0xD5A2DBD9 */
|
||||
1.176_793_732_871_471e2, /* 0x405D6B7A, 0xDA1884A9 */
|
||||
8.364_638_933_716_183, /* 0x4020BAB1, 0xF44E5192 */
|
||||
];
|
||||
|
||||
/* Note: This function is only called for ix>=0x40000000 (see above) */
|
||||
fn pone(x: f64) -> f64 {
|
||||
let ix = high_word(x) & 0x7fffffff;
|
||||
// ix>=0x40000000 && ix<=0x48000000)
|
||||
let (p, q) = if ix >= 0x40200000 {
|
||||
(PR8, PS8)
|
||||
} else if ix >= 0x40122E8B {
|
||||
(PR5, PS5)
|
||||
} else if ix >= 0x4006DB6D {
|
||||
(PR3, PS3)
|
||||
} else {
|
||||
(PR2, PS2)
|
||||
};
|
||||
let z = 1.0 / (x * x);
|
||||
let r = p[0] + z * (p[1] + z * (p[2] + z * (p[3] + z * (p[4] + z * p[5]))));
|
||||
let s = 1.0 + z * (q[0] + z * (q[1] + z * (q[2] + z * (q[3] + z * q[4]))));
|
||||
1.0 + r / s
|
||||
}
|
||||
|
||||
/* For x >= 8, the asymptotic expansions of qone is
|
||||
* 3/8 s - 105/1024 s^3 - ..., where s = 1/x.
|
||||
* We approximate pone by
|
||||
* qone(x) = s*(0.375 + (R/S))
|
||||
* where R = qr1*s^2 + qr2*s^4 + ... + qr5*s^10
|
||||
* S = 1 + qs1*s^2 + ... + qs6*s^12
|
||||
* and
|
||||
* | qone(x)/s -0.375-R/S | <= 2 ** ( -61.13)
|
||||
*/
|
||||
|
||||
const QR8: [f64; 6] = [
|
||||
/* for x in [inf, 8]=1/[0,0.125] */
|
||||
0.00000000000000000000e+00, /* 0x00000000, 0x00000000 */
|
||||
-1.025_390_624_999_927_1e-1, /* 0xBFBA3FFF, 0xFFFFFDF3 */
|
||||
-1.627_175_345_445_9e1, /* 0xC0304591, 0xA26779F7 */
|
||||
-7.596_017_225_139_501e2, /* 0xC087BCD0, 0x53E4B576 */
|
||||
-1.184_980_667_024_295_9e4, /* 0xC0C724E7, 0x40F87415 */
|
||||
-4.843_851_242_857_503_5e4, /* 0xC0E7A6D0, 0x65D09C6A */
|
||||
];
|
||||
const QS8: [f64; 6] = [
|
||||
1.613_953_697_007_229e2, /* 0x40642CA6, 0xDE5BCDE5 */
|
||||
7.825_385_999_233_485e3, /* 0x40BE9162, 0xD0D88419 */
|
||||
1.338_753_362_872_495_8e5, /* 0x4100579A, 0xB0B75E98 */
|
||||
7.196_577_236_832_409e5, /* 0x4125F653, 0x72869C19 */
|
||||
6.666_012_326_177_764e5, /* 0x412457D2, 0x7719AD5C */
|
||||
-2.944_902_643_038_346_4e5, /* 0xC111F969, 0x0EA5AA18 */
|
||||
];
|
||||
|
||||
const QR5: [f64; 6] = [
|
||||
/* for x in [8,4.5454]=1/[0.125,0.22001] */
|
||||
-2.089_799_311_417_641e-11, /* 0xBDB6FA43, 0x1AA1A098 */
|
||||
-1.025_390_502_413_754_3e-1, /* 0xBFBA3FFF, 0xCB597FEF */
|
||||
-8.056_448_281_239_36, /* 0xC0201CE6, 0xCA03AD4B */
|
||||
-1.836_696_074_748_883_8e2, /* 0xC066F56D, 0x6CA7B9B0 */
|
||||
-1.373_193_760_655_081_6e3, /* 0xC09574C6, 0x6931734F */
|
||||
-2.612_444_404_532_156_6e3, /* 0xC0A468E3, 0x88FDA79D */
|
||||
];
|
||||
const QS5: [f64; 6] = [
|
||||
8.127_655_013_843_358e1, /* 0x405451B2, 0xFF5A11B2 */
|
||||
1.991_798_734_604_859_6e3, /* 0x409F1F31, 0xE77BF839 */
|
||||
1.746_848_519_249_089e4, /* 0x40D10F1F, 0x0D64CE29 */
|
||||
4.985_142_709_103_523e4, /* 0x40E8576D, 0xAABAD197 */
|
||||
2.794_807_516_389_181_2e4, /* 0x40DB4B04, 0xCF7C364B */
|
||||
-4.719_183_547_951_285e3, /* 0xC0B26F2E, 0xFCFFA004 */
|
||||
];
|
||||
|
||||
const QR3: [f64; 6] = [
|
||||
-5.078_312_264_617_666e-9, /* 0xBE35CFA9, 0xD38FC84F */
|
||||
-1.025_378_298_208_370_9e-1, /* 0xBFBA3FEB, 0x51AEED54 */
|
||||
-4.610_115_811_394_734, /* 0xC01270C2, 0x3302D9FF */
|
||||
-5.784_722_165_627_836_4e1, /* 0xC04CEC71, 0xC25D16DA */
|
||||
-2.282_445_407_376_317e2, /* 0xC06C87D3, 0x4718D55F */
|
||||
-2.192_101_284_789_093_3e2, /* 0xC06B66B9, 0x5F5C1BF6 */
|
||||
];
|
||||
const QS3: [f64; 6] = [
|
||||
4.766_515_503_237_295e1, /* 0x4047D523, 0xCCD367E4 */
|
||||
6.738_651_126_766_997e2, /* 0x40850EEB, 0xC031EE3E */
|
||||
3.380_152_866_795_263_4e3, /* 0x40AA684E, 0x448E7C9A */
|
||||
5.547_729_097_207_228e3, /* 0x40B5ABBA, 0xA61D54A6 */
|
||||
1.903_119_193_388_108e3, /* 0x409DBC7A, 0x0DD4DF4B */
|
||||
-1.352_011_914_443_073_4e2, /* 0xC060E670, 0x290A311F */
|
||||
];
|
||||
|
||||
const QR2: [f64; 6] = [
|
||||
/* for x in [2.8570,2]=1/[0.3499,0.5] */
|
||||
-1.783_817_275_109_588_7e-7, /* 0xBE87F126, 0x44C626D2 */
|
||||
-1.025_170_426_079_855_5e-1, /* 0xBFBA3E8E, 0x9148B010 */
|
||||
-2.752_205_682_781_874_6, /* 0xC0060484, 0x69BB4EDA */
|
||||
-1.966_361_626_437_037_2e1, /* 0xC033A9E2, 0xC168907F */
|
||||
-4.232_531_333_728_305e1, /* 0xC04529A3, 0xDE104AAA */
|
||||
-2.137_192_117_037_040_6e1, /* 0xC0355F36, 0x39CF6E52 */
|
||||
];
|
||||
const QS2: [f64; 6] = [
|
||||
2.953_336_290_605_238_5e1, /* 0x403D888A, 0x78AE64FF */
|
||||
2.529_815_499_821_905_3e2, /* 0x406F9F68, 0xDB821CBA */
|
||||
7.575_028_348_686_454e2, /* 0x4087AC05, 0xCE49A0F7 */
|
||||
7.393_932_053_204_672e2, /* 0x40871B25, 0x48D4C029 */
|
||||
1.559_490_033_366_661_2e2, /* 0x40637E5E, 0x3C3ED8D4 */
|
||||
-4.959_498_988_226_282, /* 0xC013D686, 0xE71BE86B */
|
||||
];
|
||||
|
||||
// Note: This function is only called for ix>=0x40000000 (see above)
|
||||
fn qone(x: f64) -> f64 {
|
||||
let ix = high_word(x) & 0x7fffffff;
|
||||
// ix>=0x40000000 && ix<=0x48000000
|
||||
let (p, q) = if ix >= 0x40200000 {
|
||||
(QR8, QS8)
|
||||
} else if ix >= 0x40122E8B {
|
||||
(QR5, QS5)
|
||||
} else if ix >= 0x4006DB6D {
|
||||
(QR3, QS3)
|
||||
} else {
|
||||
(QR2, QS2)
|
||||
};
|
||||
let z = 1.0 / (x * x);
|
||||
let r = p[0] + z * (p[1] + z * (p[2] + z * (p[3] + z * (p[4] + z * p[5]))));
|
||||
let s = 1.0 + z * (q[0] + z * (q[1] + z * (q[2] + z * (q[3] + z * (q[4] + z * q[5])))));
|
||||
(0.375 + r / s) / x
|
||||
}
|
||||
329
base/src/functions/engineering/transcendental/bessel_jn_yn.rs
Normal file
329
base/src/functions/engineering/transcendental/bessel_jn_yn.rs
Normal file
@@ -0,0 +1,329 @@
|
||||
// https://github.com/JuliaLang/openlibm/blob/master/src/e_jn.c
|
||||
|
||||
/*
|
||||
* ====================================================
|
||||
* Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved.
|
||||
*
|
||||
* Developed at SunSoft, a Sun Microsystems, Inc. business.
|
||||
* Permission to use, copy, modify, and distribute this
|
||||
* software is freely granted, provided that this notice
|
||||
* is preserved.
|
||||
* ====================================================
|
||||
*/
|
||||
|
||||
/*
|
||||
* __ieee754_jn(n, x), __ieee754_yn(n, x)
|
||||
* floating point Bessel's function of the 1st and 2nd kind
|
||||
* of order n
|
||||
*
|
||||
* Special cases:
|
||||
* y0(0)=y1(0)=yn(n,0) = -inf with division by 0 signal;
|
||||
* y0(-ve)=y1(-ve)=yn(n,-ve) are NaN with invalid signal.
|
||||
* Note 2. About jn(n,x), yn(n,x)
|
||||
* For n=0, j0(x) is called,
|
||||
* for n=1, j1(x) is called,
|
||||
* for n<x, forward recursion us used starting
|
||||
* from values of j0(x) and j1(x).
|
||||
* for n>x, a continued fraction approximation to
|
||||
* j(n,x)/j(n-1,x) is evaluated and then backward
|
||||
* recursion is used starting from a supposed value
|
||||
* for j(n,x). The resulting value of j(0,x) is
|
||||
* compared with the actual value to correct the
|
||||
* supposed value of j(n,x).
|
||||
*
|
||||
* yn(n,x) is similar in all respects, except
|
||||
* that forward recursion is used for all
|
||||
* values of n>1.
|
||||
*
|
||||
*/
|
||||
|
||||
use super::{
|
||||
bessel_j0_y0::{j0, y0},
|
||||
bessel_j1_y1::{j1, y1},
|
||||
bessel_util::{split_words, FRAC_2_SQRT_PI},
|
||||
};
|
||||
|
||||
// Special cases are:
|
||||
//
|
||||
// $ J_n(n, ±\Infinity) = 0$
|
||||
// $ J_n(n, NaN} = NaN $
|
||||
// $ J_n(n, 0) = 0 $
|
||||
pub(crate) fn jn(n: i32, x: f64) -> f64 {
|
||||
let (lx, mut hx) = split_words(x);
|
||||
let ix = 0x7fffffff & hx;
|
||||
// if J(n,NaN) is NaN
|
||||
if x.is_nan() {
|
||||
return x;
|
||||
}
|
||||
// if (ix | (/*(u_int32_t)*/(lx | -lx)) >> 31) > 0x7ff00000 {
|
||||
// return x + x;
|
||||
// }
|
||||
let (n, x) = if n < 0 {
|
||||
// hx ^= 0x80000000;
|
||||
hx = -hx;
|
||||
(-n, -x)
|
||||
} else {
|
||||
(n, x)
|
||||
};
|
||||
if n == 0 {
|
||||
return j0(x);
|
||||
}
|
||||
if n == 1 {
|
||||
return j1(x);
|
||||
}
|
||||
let sign = (n & 1) & (hx >> 31); /* even n -- 0, odd n -- sign(x) */
|
||||
// let sign = if x < 0.0 { -1 } else { 1 };
|
||||
let x = x.abs();
|
||||
let b = if (ix | lx) == 0 || ix >= 0x7ff00000 {
|
||||
// if x is 0 or inf
|
||||
0.0
|
||||
} else if n as f64 <= x {
|
||||
/* Safe to use J(n+1,x)=2n/x *J(n,x)-J(n-1,x) */
|
||||
if ix >= 0x52D00000 {
|
||||
/* x > 2**302 */
|
||||
/* (x >> n**2)
|
||||
* Jn(x) = cos(x-(2n+1)*pi/4)*sqrt(2/x*pi)
|
||||
* Yn(x) = sin(x-(2n+1)*pi/4)*sqrt(2/x*pi)
|
||||
* Let s=x.sin(), c=x.cos(),
|
||||
* xn=x-(2n+1)*pi/4, sqt2 = sqrt(2),then
|
||||
*
|
||||
* n sin(xn)*sqt2 cos(xn)*sqt2
|
||||
* ----------------------------------
|
||||
* 0 s-c c+s
|
||||
* 1 -s-c -c+s
|
||||
* 2 -s+c -c-s
|
||||
* 3 s+c c-s
|
||||
*/
|
||||
let temp = match n & 3 {
|
||||
0 => x.cos() + x.sin(),
|
||||
1 => -x.cos() + x.sin(),
|
||||
2 => -x.cos() - x.sin(),
|
||||
3 => x.cos() - x.sin(),
|
||||
_ => {
|
||||
// Impossible: FIXME!
|
||||
// panic!("")
|
||||
0.0
|
||||
}
|
||||
};
|
||||
FRAC_2_SQRT_PI * temp / x.sqrt()
|
||||
} else {
|
||||
let mut a = j0(x);
|
||||
let mut b = j1(x);
|
||||
for i in 1..n {
|
||||
let temp = b;
|
||||
b = b * (((i + i) as f64) / x) - a; /* avoid underflow */
|
||||
a = temp;
|
||||
}
|
||||
b
|
||||
}
|
||||
} else {
|
||||
// x < 2^(-29)
|
||||
if ix < 0x3e100000 {
|
||||
// x is tiny, return the first Taylor expansion of J(n,x)
|
||||
// J(n,x) = 1/n!*(x/2)^n - ...
|
||||
if n > 33 {
|
||||
// underflow
|
||||
0.0
|
||||
} else {
|
||||
let temp = x * 0.5;
|
||||
let mut b = temp;
|
||||
let mut a = 1;
|
||||
for i in 2..=n {
|
||||
a *= i; /* a = n! */
|
||||
b *= temp; /* b = (x/2)^n */
|
||||
}
|
||||
b / (a as f64)
|
||||
}
|
||||
} else {
|
||||
/* use backward recurrence */
|
||||
/* x x^2 x^2
|
||||
* J(n,x)/J(n-1,x) = ---- ------ ------ .....
|
||||
* 2n - 2(n+1) - 2(n+2)
|
||||
*
|
||||
* 1 1 1
|
||||
* (for large x) = ---- ------ ------ .....
|
||||
* 2n 2(n+1) 2(n+2)
|
||||
* -- - ------ - ------ -
|
||||
* x x x
|
||||
*
|
||||
* Let w = 2n/x and h=2/x, then the above quotient
|
||||
* is equal to the continued fraction:
|
||||
* 1
|
||||
* = -----------------------
|
||||
* 1
|
||||
* w - -----------------
|
||||
* 1
|
||||
* w+h - ---------
|
||||
* w+2h - ...
|
||||
*
|
||||
* To determine how many terms needed, let
|
||||
* Q(0) = w, Q(1) = w(w+h) - 1,
|
||||
* Q(k) = (w+k*h)*Q(k-1) - Q(k-2),
|
||||
* When Q(k) > 1e4 good for single
|
||||
* When Q(k) > 1e9 good for double
|
||||
* When Q(k) > 1e17 good for quadruple
|
||||
*/
|
||||
|
||||
let w = ((n + n) as f64) / x;
|
||||
let h = 2.0 / x;
|
||||
let mut q0 = w;
|
||||
let mut z = w + h;
|
||||
let mut q1 = w * z - 1.0;
|
||||
let mut k = 1;
|
||||
while q1 < 1.0e9 {
|
||||
k += 1;
|
||||
z += h;
|
||||
let tmp = z * q1 - q0;
|
||||
q0 = q1;
|
||||
q1 = tmp;
|
||||
}
|
||||
let m = n + n;
|
||||
let mut t = 0.0;
|
||||
for i in (m..2 * (n + k)).step_by(2).rev() {
|
||||
t = 1.0 / ((i as f64) / x - t);
|
||||
}
|
||||
// for (t=0, i = 2*(n+k); i>=m; i -= 2) t = 1/(i/x-t);
|
||||
let mut a = t;
|
||||
let mut b = 1.0;
|
||||
/* estimate log((2/x)^n*n!) = n*log(2/x)+n*ln(n)
|
||||
* Hence, if n*(log(2n/x)) > ...
|
||||
* single 8.8722839355e+01
|
||||
* double 7.09782712893383973096e+02
|
||||
* long double 1.1356523406294143949491931077970765006170e+04
|
||||
* then recurrent value may overflow and the result is
|
||||
* likely underflow to 0
|
||||
*/
|
||||
let mut tmp = n as f64;
|
||||
let v = 2.0 / x;
|
||||
tmp = tmp * f64::ln(f64::abs(v * tmp));
|
||||
if tmp < 7.097_827_128_933_84e2 {
|
||||
// for(i=n-1, di=(i+i); i>0; i--){
|
||||
let mut di = 2.0 * ((n - 1) as f64);
|
||||
for _ in (1..=n - 1).rev() {
|
||||
let temp = b;
|
||||
b *= di;
|
||||
b = b / x - a;
|
||||
a = temp;
|
||||
di -= 2.0;
|
||||
}
|
||||
} else {
|
||||
// for(i=n-1, di=(i+i); i>0; i--) {
|
||||
let mut di = 2.0 * ((n - 1) as f64);
|
||||
for _ in (1..=n - 1).rev() {
|
||||
let temp = b;
|
||||
b *= di;
|
||||
b = b / x - a;
|
||||
a = temp;
|
||||
di -= 2.0;
|
||||
/* scale b to avoid spurious overflow */
|
||||
if b > 1e100 {
|
||||
a /= b;
|
||||
t /= b;
|
||||
b = 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
let z = j0(x);
|
||||
let w = j1(x);
|
||||
if z.abs() >= w.abs() {
|
||||
t * z / b
|
||||
} else {
|
||||
t * w / a
|
||||
}
|
||||
}
|
||||
};
|
||||
if sign == 1 {
|
||||
-b
|
||||
} else {
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
// Yn returns the order-n Bessel function of the second kind.
|
||||
//
|
||||
// Special cases are:
|
||||
//
|
||||
// Y(n, +Inf) = 0
|
||||
// Y(n ≥ 0, 0) = -Inf
|
||||
// Y(n < 0, 0) = +Inf if n is odd, -Inf if n is even
|
||||
// Y(n, x < 0) = NaN
|
||||
// Y(n, NaN) = NaN
|
||||
pub(crate) fn yn(n: i32, x: f64) -> f64 {
|
||||
let (lx, hx) = split_words(x);
|
||||
let ix = 0x7fffffff & hx;
|
||||
|
||||
// if Y(n, NaN) is NaN
|
||||
if x.is_nan() {
|
||||
return x;
|
||||
}
|
||||
// if (ix | (/*(u_int32_t)*/(lx | -lx)) >> 31) > 0x7ff00000 {
|
||||
// return x + x;
|
||||
// }
|
||||
|
||||
if (ix | lx) == 0 {
|
||||
return f64::NEG_INFINITY;
|
||||
}
|
||||
if hx < 0 {
|
||||
return f64::NAN;
|
||||
}
|
||||
|
||||
let (n, sign) = if n < 0 {
|
||||
(-n, 1 - ((n & 1) << 1))
|
||||
} else {
|
||||
(n, 1)
|
||||
};
|
||||
if n == 0 {
|
||||
return y0(x);
|
||||
}
|
||||
if n == 1 {
|
||||
return (sign as f64) * y1(x);
|
||||
}
|
||||
if ix == 0x7ff00000 {
|
||||
return 0.0;
|
||||
}
|
||||
let b = if ix >= 0x52D00000 {
|
||||
// x > 2^302
|
||||
/* (x >> n**2)
|
||||
* Jn(x) = cos(x-(2n+1)*pi/4)*sqrt(2/x*pi)
|
||||
* Yn(x) = sin(x-(2n+1)*pi/4)*sqrt(2/x*pi)
|
||||
* Let s=x.sin(), c=x.cos(),
|
||||
* xn=x-(2n+1)*pi/4, sqt2 = sqrt(2),then
|
||||
*
|
||||
* n sin(xn)*sqt2 cos(xn)*sqt2
|
||||
* ----------------------------------
|
||||
* 0 s-c c+s
|
||||
* 1 -s-c -c+s
|
||||
* 2 -s+c -c-s
|
||||
* 3 s+c c-s
|
||||
*/
|
||||
let temp = match n & 3 {
|
||||
0 => x.sin() - x.cos(),
|
||||
1 => -x.sin() - x.cos(),
|
||||
2 => -x.sin() + x.cos(),
|
||||
3 => x.sin() + x.cos(),
|
||||
_ => {
|
||||
// unreachable
|
||||
0.0
|
||||
}
|
||||
};
|
||||
FRAC_2_SQRT_PI * temp / x.sqrt()
|
||||
} else {
|
||||
let mut a = y0(x);
|
||||
let mut b = y1(x);
|
||||
for i in 1..n {
|
||||
if b.is_infinite() {
|
||||
break;
|
||||
}
|
||||
// if high_word(b) != 0xfff00000 {
|
||||
// break;
|
||||
// }
|
||||
(a, b) = (b, ((2.0 * i as f64) / x) * b - a);
|
||||
}
|
||||
b
|
||||
};
|
||||
if sign > 0 {
|
||||
b
|
||||
} else {
|
||||
-b
|
||||
}
|
||||
}
|
||||
90
base/src/functions/engineering/transcendental/bessel_k.rs
Normal file
90
base/src/functions/engineering/transcendental/bessel_k.rs
Normal file
@@ -0,0 +1,90 @@
|
||||
// This are somewhat lower precision than the BesselJ and BesselY
|
||||
|
||||
use super::bessel_i::bessel_i0;
|
||||
use super::bessel_i::bessel_i1;
|
||||
|
||||
fn bessel_k0(x: f64) -> f64 {
|
||||
let p1 = -0.57721566;
|
||||
let p2 = 0.42278420;
|
||||
let p3 = 0.23069756;
|
||||
let p4 = 3.488590e-2;
|
||||
let p5 = 2.62698e-3;
|
||||
let p6 = 1.0750e-4;
|
||||
let p7 = 7.4e-6;
|
||||
|
||||
let q1 = 1.25331414;
|
||||
let q2 = -7.832358e-2;
|
||||
let q3 = 2.189568e-2;
|
||||
let q4 = -1.062446e-2;
|
||||
let q5 = 5.87872e-3;
|
||||
let q6 = -2.51540e-3;
|
||||
let q7 = 5.3208e-4;
|
||||
|
||||
if x <= 0.0 {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
if x <= 2.0 {
|
||||
let y = x * x / 4.0;
|
||||
(-(x / 2.0).ln() * bessel_i0(x))
|
||||
+ (p1 + y * (p2 + y * (p3 + y * (p4 + y * (p5 + y * (p6 + y * p7))))))
|
||||
} else {
|
||||
let y = 2.0 / x;
|
||||
((-x).exp() / x.sqrt())
|
||||
* (q1 + y * (q2 + y * (q3 + y * (q4 + y * (q5 + y * (q6 + y * q7))))))
|
||||
}
|
||||
}
|
||||
|
||||
fn bessel_k1(x: f64) -> f64 {
|
||||
let p1 = 1.0;
|
||||
let p2 = 0.15443144;
|
||||
let p3 = -0.67278579;
|
||||
let p4 = -0.18156897;
|
||||
let p5 = -1.919402e-2;
|
||||
let p6 = -1.10404e-3;
|
||||
let p7 = -4.686e-5;
|
||||
|
||||
let q1 = 1.25331414;
|
||||
let q2 = 0.23498619;
|
||||
let q3 = -3.655620e-2;
|
||||
let q4 = 1.504268e-2;
|
||||
let q5 = -7.80353e-3;
|
||||
let q6 = 3.25614e-3;
|
||||
let q7 = -6.8245e-4;
|
||||
|
||||
if x <= 0.0 {
|
||||
return f64::NAN;
|
||||
}
|
||||
|
||||
if x <= 2.0 {
|
||||
let y = x * x / 4.0;
|
||||
((x / 2.0).ln() * bessel_i1(x))
|
||||
+ (1. / x) * (p1 + y * (p2 + y * (p3 + y * (p4 + y * (p5 + y * (p6 + y * p7))))))
|
||||
} else {
|
||||
let y = 2.0 / x;
|
||||
((-x).exp() / x.sqrt())
|
||||
* (q1 + y * (q2 + y * (q3 + y * (q4 + y * (q5 + y * (q6 + y * q7))))))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn bessel_k(n: i32, x: f64) -> f64 {
|
||||
if x <= 0.0 || n < 0 {
|
||||
return f64::NAN;
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
return bessel_k0(x);
|
||||
}
|
||||
if n == 1 {
|
||||
return bessel_k1(x);
|
||||
}
|
||||
|
||||
// Perform upward recurrence for all x
|
||||
let tox = 2.0 / x;
|
||||
let mut bkm = bessel_k0(x);
|
||||
let mut bk = bessel_k1(x);
|
||||
for j in 1..n {
|
||||
(bkm, bk) = (bk, bkm + (j as f64) * tox * bk);
|
||||
}
|
||||
bk
|
||||
}
|
||||
19
base/src/functions/engineering/transcendental/bessel_util.rs
Normal file
19
base/src/functions/engineering/transcendental/bessel_util.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
pub(crate) const HUGE: f64 = 1e300;
|
||||
pub(crate) const FRAC_2_SQRT_PI: f64 = 5.641_895_835_477_563e-1;
|
||||
|
||||
pub(crate) fn high_word(x: f64) -> i32 {
|
||||
let [_, _, _, _, a1, a2, a3, a4] = x.to_ne_bytes();
|
||||
// let binding = x.to_ne_bytes();
|
||||
// let high = <&[u8; 4]>::try_from(&binding[4..8]).expect("");
|
||||
i32::from_ne_bytes([a1, a2, a3, a4])
|
||||
}
|
||||
|
||||
pub(crate) fn split_words(x: f64) -> (i32, i32) {
|
||||
let [a1, a2, a3, a4, b1, b2, b3, b4] = x.to_ne_bytes();
|
||||
// let binding = x.to_ne_bytes();
|
||||
// let high = <&[u8; 4]>::try_from(&binding[4..8]).expect("");
|
||||
(
|
||||
i32::from_ne_bytes([a1, a2, a3, a4]),
|
||||
i32::from_ne_bytes([b1, b2, b3, b4]),
|
||||
)
|
||||
}
|
||||
14
base/src/functions/engineering/transcendental/create_test.jl
Normal file
14
base/src/functions/engineering/transcendental/create_test.jl
Normal file
@@ -0,0 +1,14 @@
|
||||
# Example file creating testing cases for BesselI
|
||||
|
||||
using Nemo
|
||||
|
||||
CC = AcbField(100)
|
||||
|
||||
values = [1, 2, 3, -2, 5, 30, 2e-8]
|
||||
|
||||
for value in values
|
||||
y_acb = besseli(CC(1), CC(value))
|
||||
real64 = convert(Float64, real(y_acb))
|
||||
im64 = convert(Float64, real(y_acb))
|
||||
println("(", value, ", ", real64, "),")
|
||||
end
|
||||
53
base/src/functions/engineering/transcendental/erf.rs
Normal file
53
base/src/functions/engineering/transcendental/erf.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
pub(crate) fn erf(x: f64) -> f64 {
|
||||
let cof = vec![
|
||||
-1.3026537197817094,
|
||||
6.419_697_923_564_902e-1,
|
||||
1.9476473204185836e-2,
|
||||
-9.561_514_786_808_63e-3,
|
||||
-9.46595344482036e-4,
|
||||
3.66839497852761e-4,
|
||||
4.2523324806907e-5,
|
||||
-2.0278578112534e-5,
|
||||
-1.624290004647e-6,
|
||||
1.303655835580e-6,
|
||||
1.5626441722e-8,
|
||||
-8.5238095915e-8,
|
||||
6.529054439e-9,
|
||||
5.059343495e-9,
|
||||
-9.91364156e-10,
|
||||
-2.27365122e-10,
|
||||
9.6467911e-11,
|
||||
2.394038e-12,
|
||||
-6.886027e-12,
|
||||
8.94487e-13,
|
||||
3.13092e-13,
|
||||
-1.12708e-13,
|
||||
3.81e-16,
|
||||
7.106e-15,
|
||||
-1.523e-15,
|
||||
-9.4e-17,
|
||||
1.21e-16,
|
||||
-2.8e-17,
|
||||
];
|
||||
|
||||
let mut d = 0.0;
|
||||
let mut dd = 0.0;
|
||||
|
||||
let x_abs = x.abs();
|
||||
|
||||
let t = 2.0 / (2.0 + x_abs);
|
||||
let ty = 4.0 * t - 2.0;
|
||||
|
||||
for j in (1..=cof.len() - 1).rev() {
|
||||
let tmp = d;
|
||||
d = ty * d - dd + cof[j];
|
||||
dd = tmp;
|
||||
}
|
||||
|
||||
let res = t * f64::exp(-x_abs * x_abs + 0.5 * (cof[0] + ty * d) - dd);
|
||||
if x < 0.0 {
|
||||
res - 1.0
|
||||
} else {
|
||||
1.0 - res
|
||||
}
|
||||
}
|
||||
16
base/src/functions/engineering/transcendental/mod.rs
Normal file
16
base/src/functions/engineering/transcendental/mod.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
mod bessel_i;
|
||||
mod bessel_j0_y0;
|
||||
mod bessel_j1_y1;
|
||||
mod bessel_jn_yn;
|
||||
mod bessel_k;
|
||||
mod bessel_util;
|
||||
mod erf;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_bessel;
|
||||
|
||||
pub(crate) use bessel_i::bessel_i;
|
||||
pub(crate) use bessel_jn_yn::jn as bessel_j;
|
||||
pub(crate) use bessel_jn_yn::yn as bessel_y;
|
||||
pub(crate) use bessel_k::bessel_k;
|
||||
pub(crate) use erf::erf;
|
||||
183
base/src/functions/engineering/transcendental/test_bessel.rs
Normal file
183
base/src/functions/engineering/transcendental/test_bessel.rs
Normal file
@@ -0,0 +1,183 @@
|
||||
use crate::functions::engineering::transcendental::bessel_k;
|
||||
|
||||
use super::{
|
||||
bessel_i::bessel_i,
|
||||
bessel_j0_y0::{j0, y0},
|
||||
bessel_j1_y1::j1,
|
||||
bessel_jn_yn::{jn, yn},
|
||||
};
|
||||
|
||||
const EPS: f64 = 1e-13;
|
||||
const EPS_LOW: f64 = 1e-6;
|
||||
|
||||
// Known values computed with Arb via Nemo.jl in Julia
|
||||
// You can also use Mathematica
|
||||
/// But please do not use Excel or any other software without arbitrary precision
|
||||
|
||||
fn numbers_are_close(a: f64, b: f64) -> bool {
|
||||
if a == b {
|
||||
// avoid underflow if a = b = 0.0
|
||||
return true;
|
||||
}
|
||||
(a - b).abs() / ((a * a + b * b).sqrt()) < EPS
|
||||
}
|
||||
|
||||
fn numbers_are_somewhat_close(a: f64, b: f64) -> bool {
|
||||
if a == b {
|
||||
// avoid underflow if a = b = 0.0
|
||||
return true;
|
||||
}
|
||||
(a - b).abs() / ((a * a + b * b).sqrt()) < EPS_LOW
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bessel_j0_known_values() {
|
||||
let cases = [
|
||||
(2.4, 0.002507683297243813),
|
||||
(0.5, 0.9384698072408129),
|
||||
(1.0, 0.7651976865579666),
|
||||
(1.12345, 0.7084999488947348),
|
||||
(27.0, 0.07274191800588709),
|
||||
(33.0, 0.09727067223550946),
|
||||
(2e-4, 0.9999999900000001),
|
||||
(0.0, 1.0),
|
||||
(1e10, 2.175591750246892e-6),
|
||||
];
|
||||
for (value, known) in cases {
|
||||
let f = j0(value);
|
||||
assert!(
|
||||
numbers_are_close(f, known),
|
||||
"Got: {f}, expected: {known} for j0({value})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bessel_y0_known_values() {
|
||||
let cases = [
|
||||
(2.4, 0.5104147486657438),
|
||||
(0.5, -0.4445187335067065),
|
||||
(1.0, 0.08825696421567692),
|
||||
(1.12345, 0.1783162909790613),
|
||||
(27.0, 0.1352149762078722),
|
||||
(33.0, 0.0991348255208796),
|
||||
(2e-4, -5.496017824512429),
|
||||
(1e10, -7.676508175792937e-6),
|
||||
(1e-300, -439.8351636227653),
|
||||
];
|
||||
for (value, known) in cases {
|
||||
let f = y0(value);
|
||||
assert!(
|
||||
numbers_are_close(f, known),
|
||||
"Got: {f}, expected: {known} for y0({value})"
|
||||
);
|
||||
}
|
||||
assert!(y0(0.0).is_infinite());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bessel_j1_known_values() {
|
||||
// Values computed with Maxima, the computer algebra system
|
||||
// TODO: Recompute
|
||||
let cases = [
|
||||
(2.4, 0.5201852681819311),
|
||||
(0.5, 0.2422684576748738),
|
||||
(1.0, 0.4400505857449335),
|
||||
(1.17232, 0.4910665691824317),
|
||||
(27.5, 0.1521418932046569),
|
||||
(42.0, -0.04599388822188721),
|
||||
(3e-5, 1.499999999831249E-5),
|
||||
(350.0, -0.02040531295214455),
|
||||
(0.0, 0.0),
|
||||
(1e12, -7.913802683850441e-7),
|
||||
];
|
||||
for (value, known) in cases {
|
||||
let f = j1(value);
|
||||
assert!(
|
||||
numbers_are_close(f, known),
|
||||
"Got: {f}, expected: {known} for j1({value})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bessel_jn_known_values() {
|
||||
// Values computed with Maxima, the computer algebra system
|
||||
// TODO: Recompute
|
||||
let cases = [
|
||||
(3, 0.5, 0.002_563_729_994_587_244),
|
||||
(4, 0.5, 0.000_160_736_476_364_287_6),
|
||||
(-3, 0.5, -0.002_563_729_994_587_244),
|
||||
(-4, 0.5, 0.000_160_736_476_364_287_6),
|
||||
(3, 30.0, 0.129211228759725),
|
||||
(-3, 30.0, -0.129211228759725),
|
||||
(4, 30.0, -0.052609000321320355),
|
||||
(20, 30.0, 0.0048310199934040645),
|
||||
(7, 0.0, 0.0),
|
||||
];
|
||||
for (n, value, known) in cases {
|
||||
let f = jn(n, value);
|
||||
assert!(
|
||||
numbers_are_close(f, known),
|
||||
"Got: {f}, expected: {known} for jn({n}, {value})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bessel_yn_known_values() {
|
||||
let cases = [
|
||||
(3, 0.5, -42.059494304723883),
|
||||
(4, 0.5, -499.272_560_819_512_3),
|
||||
(-3, 0.5, 42.059494304723883),
|
||||
(-4, 0.5, -499.272_560_819_512_3),
|
||||
(3, 35.0, -0.13191405300596323),
|
||||
(-12, 12.2, -0.310438011314211),
|
||||
(7, 1e12, 1.016_712_505_197_956_3e-7),
|
||||
(35, 3.0, -6.895_879_073_343_495e31),
|
||||
];
|
||||
for (n, value, known) in cases {
|
||||
let f = yn(n, value);
|
||||
assert!(
|
||||
numbers_are_close(f, known),
|
||||
"Got: {f}, expected: {known} for yn({n}, {value})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bessel_in_known_values() {
|
||||
let cases = [
|
||||
(1, 0.5, 0.2578943053908963),
|
||||
(3, 0.5, 0.002645111968990286),
|
||||
(7, 0.2, 1.986608521182497e-11),
|
||||
(7, 0.0, 0.0),
|
||||
(0, -0.5, 1.0634833707413236),
|
||||
// worse case scenario
|
||||
(0, 3.7499, 9.118167894541882),
|
||||
(0, 3.7501, 9.119723897590003),
|
||||
];
|
||||
for (n, value, known) in cases {
|
||||
let f = bessel_i(n, value);
|
||||
assert!(
|
||||
numbers_are_somewhat_close(f, known),
|
||||
"Got: {f}, expected: {known} for in({n}, {value})"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bessel_kn_known_values() {
|
||||
let cases = [
|
||||
(1, 0.5, 1.656441120003301),
|
||||
(0, 0.5, 0.9244190712276659),
|
||||
(3, 0.5, 62.05790952993026),
|
||||
];
|
||||
for (n, value, known) in cases {
|
||||
let f = bessel_k(n, value);
|
||||
assert!(
|
||||
numbers_are_somewhat_close(f, known),
|
||||
"Got: {f}, expected: {known} for kn({n}, {value})"
|
||||
);
|
||||
}
|
||||
}
|
||||
1884
base/src/functions/financial.rs
Normal file
1884
base/src/functions/financial.rs
Normal file
File diff suppressed because it is too large
Load Diff
255
base/src/functions/financial_util.rs
Normal file
255
base/src/functions/financial_util.rs
Normal file
@@ -0,0 +1,255 @@
|
||||
use crate::expressions::token::Error;
|
||||
|
||||
// Here we use some numerical routines to solve for some functions:
|
||||
// RATE, IRR, XIRR
|
||||
// We use a combination of heuristics, bisection and Newton-Raphson
|
||||
// If you want to improve on this methods you probably would want to find failing tests first
|
||||
|
||||
// From Microsoft docs:
|
||||
// https://support.microsoft.com/en-us/office/rate-function-9f665657-4a7e-4bb7-a030-83fc59e748ce
|
||||
// Returns the interest rate per period of an annuity.
|
||||
// RATE is calculated by iteration (using Newton-Raphson) and can have zero or more solutions.
|
||||
// If the successive results of RATE do not converge to within 0.0000001 after 20 iterations,
|
||||
// RATE returns the #NUM! error value.
|
||||
// NOTE: We need a better algorithm here
|
||||
pub(crate) fn compute_rate(
|
||||
pv: f64,
|
||||
fv: f64,
|
||||
nper: f64,
|
||||
pmt: f64,
|
||||
annuity_type: i32,
|
||||
guess: f64,
|
||||
) -> Result<f64, (Error, String)> {
|
||||
let mut rate = guess;
|
||||
// Excel _claims_ to do 20 iterations, but that will have tests failing
|
||||
let max_iterations = 50;
|
||||
let eps = 0.0000001;
|
||||
let annuity_type = annuity_type as f64;
|
||||
if guess <= -1.0 {
|
||||
return Err((Error::VALUE, "Rate initial guess must be > -1".to_string()));
|
||||
}
|
||||
for _ in 1..=max_iterations {
|
||||
let t = (1.0 + rate).powf(nper - 1.0);
|
||||
let tt = t * (1.0 + rate);
|
||||
let f = pv * tt + pmt * (1.0 + rate * annuity_type) * (tt - 1.0) / rate + fv;
|
||||
let f_prime = pv * nper * t - pmt * (tt - 1.0) / (rate * rate)
|
||||
+ pmt * (1.0 + rate * annuity_type) * t * nper / rate;
|
||||
let new_rate = rate - f / f_prime;
|
||||
if new_rate <= -1.0 {
|
||||
return Err((Error::NUM, "Failed to converge".to_string()));
|
||||
}
|
||||
if (new_rate - rate).abs() < eps {
|
||||
return Ok(new_rate);
|
||||
}
|
||||
rate = new_rate;
|
||||
}
|
||||
Err((Error::NUM, "Failed to converge".to_string()))
|
||||
}
|
||||
|
||||
pub(crate) fn compute_npv(rate: f64, values: &[f64]) -> Result<f64, (Error, String)> {
|
||||
let mut npv = 0.0;
|
||||
for (i, item) in values.iter().enumerate() {
|
||||
npv += item / (1.0 + rate).powi(i as i32 + 1)
|
||||
}
|
||||
Ok(npv)
|
||||
}
|
||||
|
||||
// Tries to solve npv(r, values) = 0 for r given values
|
||||
// Uses a bit of heuristics:
|
||||
// * First tries Newton-Raphson around the guess
|
||||
// * Failing that uses bisection and bracketing
|
||||
// * If that fails (no root found of the interval) uses Newton-Raphson around the edges
|
||||
// Values for x1, x2 and the guess for N-R are fine tuned using heuristics
|
||||
pub(crate) fn compute_irr(values: &[f64], guess: f64) -> Result<f64, (Error, String)> {
|
||||
if guess <= -1.0 {
|
||||
return Err((Error::VALUE, "Rate initial guess must be > -1".to_string()));
|
||||
}
|
||||
// The values cannot be all positive or all negative
|
||||
if values.iter().all(|&x| x >= 0.0) || values.iter().all(|&x| x <= 0.0) {
|
||||
return Err((Error::NUM, "Failed to converge".to_string()));
|
||||
}
|
||||
if let Ok(f) = compute_irr_newton_raphson(values, guess) {
|
||||
return Ok(f);
|
||||
};
|
||||
// We try bisection
|
||||
let max_iterations = 50;
|
||||
let eps = 1e-10;
|
||||
let x1 = -0.99999;
|
||||
let x2 = 100.0;
|
||||
let f1 = compute_npv(x1, values)?;
|
||||
let f2 = compute_npv(x2, values)?;
|
||||
if f1 * f2 > 0.0 {
|
||||
// The root is not within the limits or there are two roots
|
||||
// We try Newton-Raphson a bit above the upper limit and a bit below the lower limit
|
||||
if let Ok(f) = compute_irr_newton_raphson(values, 200.0) {
|
||||
return Ok(f);
|
||||
};
|
||||
if let Ok(f) = compute_irr_newton_raphson(values, -2.0) {
|
||||
return Ok(f);
|
||||
};
|
||||
return Err((Error::NUM, "Failed to converge".to_string()));
|
||||
}
|
||||
let (mut rtb, mut dx) = if f1 < 0.0 {
|
||||
(x1, x2 - x1)
|
||||
} else {
|
||||
(x2, x1 - x2)
|
||||
};
|
||||
for _ in 1..max_iterations {
|
||||
dx *= 0.5;
|
||||
let x_mid = rtb + dx;
|
||||
let f_mid = compute_npv(x_mid, values)?;
|
||||
if f_mid <= 0.0 {
|
||||
rtb = x_mid;
|
||||
}
|
||||
if f_mid.abs() < eps || dx.abs() < eps {
|
||||
return Ok(x_mid);
|
||||
}
|
||||
}
|
||||
|
||||
Err((Error::NUM, "Failed to converge".to_string()))
|
||||
}
|
||||
|
||||
fn compute_npv_prime(rate: f64, values: &[f64]) -> Result<f64, (Error, String)> {
|
||||
let mut npv = 0.0;
|
||||
for (i, item) in values.iter().enumerate() {
|
||||
npv += -item * (i as f64 + 1.0) / (1.0 + rate).powi(i as i32 + 2)
|
||||
}
|
||||
if npv.is_infinite() || npv.is_nan() {
|
||||
return Err((Error::NUM, "NaN".to_string()));
|
||||
}
|
||||
Ok(npv)
|
||||
}
|
||||
|
||||
fn compute_irr_newton_raphson(values: &[f64], guess: f64) -> Result<f64, (Error, String)> {
|
||||
let mut irr = guess;
|
||||
let max_iterations = 50;
|
||||
let eps = 1e-8;
|
||||
for _ in 1..=max_iterations {
|
||||
let f = compute_npv(irr, values)?;
|
||||
let f_prime = compute_npv_prime(irr, values)?;
|
||||
let new_irr = irr - f / f_prime;
|
||||
if (new_irr - irr).abs() < eps {
|
||||
return Ok(new_irr);
|
||||
}
|
||||
irr = new_irr;
|
||||
}
|
||||
Err((Error::NUM, "Failed to converge".to_string()))
|
||||
}
|
||||
|
||||
// Formula is:
|
||||
// $$\sum_{i=1}^n\frac{v_i}{(1+r)^{\frac{(d_j-d_1)}{365}}}$$
|
||||
// where $v_i$ is the ith-1 value and $d_i$ is the ith-1 date
|
||||
pub(crate) fn compute_xnpv(
|
||||
rate: f64,
|
||||
values: &[f64],
|
||||
dates: &[f64],
|
||||
) -> Result<f64, (Error, String)> {
|
||||
let mut xnpv = values[0];
|
||||
let d0 = dates[0];
|
||||
let n = values.len();
|
||||
for i in 1..n {
|
||||
let vi = values[i];
|
||||
let di = dates[i];
|
||||
xnpv += vi / ((1.0 + rate).powf((di - d0) / 365.0))
|
||||
}
|
||||
if xnpv.is_infinite() || xnpv.is_nan() {
|
||||
return Err((Error::NUM, "NaN".to_string()));
|
||||
}
|
||||
Ok(xnpv)
|
||||
}
|
||||
|
||||
fn compute_xnpv_prime(rate: f64, values: &[f64], dates: &[f64]) -> Result<f64, (Error, String)> {
|
||||
let mut xnpv = 0.0;
|
||||
let d0 = dates[0];
|
||||
let n = values.len();
|
||||
for i in 1..n {
|
||||
let vi = values[i];
|
||||
let di = dates[i];
|
||||
let ratio = (di - d0) / 365.0;
|
||||
let power = (1.0 + rate).powf(ratio + 1.0);
|
||||
xnpv -= vi * ratio / power;
|
||||
}
|
||||
if xnpv.is_infinite() || xnpv.is_nan() {
|
||||
return Err((Error::NUM, "NaN".to_string()));
|
||||
}
|
||||
Ok(xnpv)
|
||||
}
|
||||
|
||||
fn compute_xirr_newton_raphson(
|
||||
values: &[f64],
|
||||
dates: &[f64],
|
||||
guess: f64,
|
||||
) -> Result<f64, (Error, String)> {
|
||||
let mut xirr = guess;
|
||||
let max_iterations = 100;
|
||||
let eps = 1e-7;
|
||||
for _ in 1..=max_iterations {
|
||||
let f = compute_xnpv(xirr, values, dates)?;
|
||||
let f_prime = compute_xnpv_prime(xirr, values, dates)?;
|
||||
let new_xirr = xirr - f / f_prime;
|
||||
if (new_xirr - xirr).abs() < eps {
|
||||
return Ok(new_xirr);
|
||||
}
|
||||
xirr = new_xirr;
|
||||
}
|
||||
Err((Error::NUM, "Failed to converge".to_string()))
|
||||
}
|
||||
|
||||
// NOTES:
|
||||
// 1. If the cash-flows (value[i] for i > 0) are always of the same sign, the function is monotonous
|
||||
// 3. Say (d_max, v_max) are the pair where d_max is the largest date,
|
||||
// then if v_max and v_0 have different signs, it's guaranteed there is a zero
|
||||
// The algorithm needs to be improved but it works so far in all practical cases
|
||||
pub(crate) fn compute_xirr(
|
||||
values: &[f64],
|
||||
dates: &[f64],
|
||||
guess: f64,
|
||||
) -> Result<f64, (Error, String)> {
|
||||
if guess <= -1.0 {
|
||||
return Err((Error::VALUE, "Rate initial guess must be > -1".to_string()));
|
||||
}
|
||||
// The values cannot be all positive or all negative
|
||||
if values.iter().all(|&x| x >= 0.0) || values.iter().all(|&x| x <= 0.0) {
|
||||
return Err((Error::NUM, "Failed to converge".to_string()));
|
||||
}
|
||||
if let Ok(f) = compute_xirr_newton_raphson(values, dates, guess) {
|
||||
return Ok(f);
|
||||
};
|
||||
// We try bisection
|
||||
let max_iterations = 50;
|
||||
let eps = 1e-8;
|
||||
// This will miss 0's very close to -1
|
||||
let x1 = -0.9999;
|
||||
let x2 = 100.0;
|
||||
let f1 = compute_xnpv(x1, values, dates)?;
|
||||
let f2 = compute_xnpv(x2, values, dates)?;
|
||||
if f1 * f2 > 0.0 {
|
||||
// The root is not within the limits (or there are two roots)
|
||||
// We try Newton-Raphson above the upper limit
|
||||
// (we cannot go to the left of -1)
|
||||
if let Ok(f) = compute_xirr_newton_raphson(values, dates, 200.0) {
|
||||
return Ok(f);
|
||||
};
|
||||
return Err((Error::NUM, "Failed to converge".to_string()));
|
||||
}
|
||||
|
||||
let (mut rtb, mut dx) = if f1 < 0.0 {
|
||||
(x1, x2 - x1)
|
||||
} else {
|
||||
(x2, x1 - x2)
|
||||
};
|
||||
|
||||
for _ in 1..max_iterations {
|
||||
dx *= 0.5;
|
||||
let x_mid = rtb + dx;
|
||||
let f_mid = compute_xnpv(x_mid, values, dates)?;
|
||||
if f_mid <= 0.0 {
|
||||
rtb = x_mid;
|
||||
}
|
||||
if f_mid.abs() < eps || dx.abs() < eps {
|
||||
return Ok(x_mid);
|
||||
}
|
||||
}
|
||||
|
||||
Err((Error::NUM, "Failed to converge".to_string()))
|
||||
}
|
||||
296
base/src/functions/information.rs
Normal file
296
base/src/functions/information.rs
Normal file
@@ -0,0 +1,296 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::{Model, ParsedDefinedName},
|
||||
};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_isnumber(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Number(_) => return CalcResult::Boolean(true),
|
||||
_ => {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
pub(crate) fn fn_istext(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::String(_) => return CalcResult::Boolean(true),
|
||||
_ => {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
pub(crate) fn fn_isnontext(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::String(_) => return CalcResult::Boolean(false),
|
||||
_ => {
|
||||
return CalcResult::Boolean(true);
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
pub(crate) fn fn_islogical(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Boolean(_) => return CalcResult::Boolean(true),
|
||||
_ => {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
pub(crate) fn fn_isblank(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::EmptyCell => return CalcResult::Boolean(true),
|
||||
_ => {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
pub(crate) fn fn_iserror(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Error { .. } => return CalcResult::Boolean(true),
|
||||
_ => {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
pub(crate) fn fn_iserr(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Error { error, .. } => {
|
||||
if Error::NA == error {
|
||||
return CalcResult::Boolean(false);
|
||||
} else {
|
||||
return CalcResult::Boolean(true);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
pub(crate) fn fn_isna(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Error { error, .. } => {
|
||||
if error == Error::NA {
|
||||
return CalcResult::Boolean(true);
|
||||
} else {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
|
||||
// Returns true if it is a reference or evaluates to a reference
|
||||
// But DOES NOT evaluate
|
||||
pub(crate) fn fn_isref(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
match &args[0] {
|
||||
Node::ReferenceKind { .. } | Node::RangeKind { .. } | Node::OpRangeKind { .. } => {
|
||||
CalcResult::Boolean(true)
|
||||
}
|
||||
Node::FunctionKind { kind, args: _ } => CalcResult::Boolean(kind.returns_reference()),
|
||||
_ => CalcResult::Boolean(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_isodd(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f.abs().trunc() as i64,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Boolean(value % 2 == 1)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_iseven(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number_no_bools(&args[0], cell) {
|
||||
Ok(f) => f.abs().trunc() as i64,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Boolean(value % 2 == 0)
|
||||
}
|
||||
|
||||
// ISFORMULA arg needs to be a reference or something that evaluates to a reference
|
||||
pub(crate) fn fn_isformula(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
if let CalcResult::Range { left, right } = self.evaluate_node_with_reference(&args[0], cell)
|
||||
{
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "3D ranges not supported".to_string(),
|
||||
};
|
||||
}
|
||||
if left.row != right.row && left.column != right.column {
|
||||
// FIXME: Implicit intersection or dynamic arrays
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "argument must be a reference to a single cell".to_string(),
|
||||
};
|
||||
}
|
||||
let is_formula = if let Ok(f) = self.cell_formula(left.sheet, left.row, left.column) {
|
||||
f.is_some()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
CalcResult::Boolean(is_formula)
|
||||
} else {
|
||||
CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Argument must be a reference".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_errortype(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Error { error, .. } => {
|
||||
match error {
|
||||
Error::NULL => CalcResult::Number(1.0),
|
||||
Error::DIV => CalcResult::Number(2.0),
|
||||
Error::VALUE => CalcResult::Number(3.0),
|
||||
Error::REF => CalcResult::Number(4.0),
|
||||
Error::NAME => CalcResult::Number(5.0),
|
||||
Error::NUM => CalcResult::Number(6.0),
|
||||
Error::NA => CalcResult::Number(7.0),
|
||||
Error::SPILL => CalcResult::Number(9.0),
|
||||
Error::CALC => CalcResult::Number(14.0),
|
||||
// IronCalc specific
|
||||
Error::ERROR => CalcResult::Number(101.0),
|
||||
Error::NIMPL => CalcResult::Number(102.0),
|
||||
Error::CIRC => CalcResult::Number(104.0),
|
||||
// Missing from Excel
|
||||
// #GETTING_DATA => 8
|
||||
// #CONNECT => 10
|
||||
// #BLOCKED => 11
|
||||
// #UNKNOWN => 12
|
||||
// #FIELD => 13
|
||||
// #EXTERNAL => 19
|
||||
}
|
||||
}
|
||||
_ => CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not an error".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Excel believes for some reason that TYPE(A1:A7) is an array formula
|
||||
// Although we evaluate the same as Excel we cannot, ATM import this from excel
|
||||
pub(crate) fn fn_type(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::String(_) => CalcResult::Number(2.0),
|
||||
CalcResult::Number(_) => CalcResult::Number(1.0),
|
||||
CalcResult::Boolean(_) => CalcResult::Number(4.0),
|
||||
CalcResult::Error { .. } => CalcResult::Number(16.0),
|
||||
CalcResult::Range { .. } => CalcResult::Number(64.0),
|
||||
CalcResult::EmptyCell => CalcResult::Number(1.0),
|
||||
CalcResult::EmptyArg => {
|
||||
// This cannot happen
|
||||
CalcResult::Number(1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) fn fn_sheet(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let arg_count = args.len();
|
||||
if arg_count > 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
if arg_count == 0 {
|
||||
// Sheets are 0-indexed`
|
||||
return CalcResult::Number(cell.sheet as f64 + 1.0);
|
||||
}
|
||||
// The arg could be a defined name or a table
|
||||
let arg = &args[0];
|
||||
if let Node::VariableKind(name) = arg {
|
||||
// Let's see if it is a defined name
|
||||
if let Some(defined_name) = self.parsed_defined_names.get(&(None, name.to_lowercase()))
|
||||
{
|
||||
match defined_name {
|
||||
ParsedDefinedName::CellReference(reference) => {
|
||||
return CalcResult::Number(reference.sheet as f64 + 1.0)
|
||||
}
|
||||
ParsedDefinedName::RangeReference(range) => {
|
||||
return CalcResult::Number(range.left.sheet as f64 + 1.0)
|
||||
}
|
||||
ParsedDefinedName::InvalidDefinedNameFormula => {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Invalid name".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
// Now let's see if it is a table
|
||||
for (table_name, table) in &self.workbook.tables {
|
||||
if table_name == name {
|
||||
if let Some(sheet_index) = self.get_sheet_index_by_name(&table.sheet_name) {
|
||||
return CalcResult::Number(sheet_index as f64 + 1.0);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Now it should be the name of a sheet
|
||||
let sheet_name = match self.get_string(arg, cell) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return e,
|
||||
};
|
||||
if let Some(sheet_index) = self.get_sheet_index_by_name(&sheet_name) {
|
||||
return CalcResult::Number(sheet_index as f64 + 1.0);
|
||||
}
|
||||
CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Invalid name".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
321
base/src/functions/logical.rs
Normal file
321
base/src/functions/logical.rs
Normal file
@@ -0,0 +1,321 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
use super::util::compare_values;
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_if(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 2 || args.len() == 3 {
|
||||
let cond_result = self.get_boolean(&args[0], cell);
|
||||
let cond = match cond_result {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
if cond {
|
||||
return self.evaluate_node_in_context(&args[1], cell);
|
||||
} else if args.len() == 3 {
|
||||
return self.evaluate_node_in_context(&args[2], cell);
|
||||
} else {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_iferror(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 2 {
|
||||
let value = self.evaluate_node_in_context(&args[0], cell);
|
||||
match value {
|
||||
CalcResult::Error { .. } => {
|
||||
return self.evaluate_node_in_context(&args[1], cell);
|
||||
}
|
||||
_ => return value,
|
||||
}
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_ifna(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 2 {
|
||||
let value = self.evaluate_node_in_context(&args[0], cell);
|
||||
if let CalcResult::Error { error, .. } = &value {
|
||||
if error == &Error::NA {
|
||||
return self.evaluate_node_in_context(&args[1], cell);
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_not(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 1 {
|
||||
match self.get_boolean(&args[0], cell) {
|
||||
Ok(f) => return CalcResult::Boolean(!f),
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_and(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let mut true_count = 0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Boolean(b) => {
|
||||
if !b {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
true_count += 1;
|
||||
}
|
||||
CalcResult::Number(value) => {
|
||||
if value == 0.0 {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
true_count += 1;
|
||||
}
|
||||
CalcResult::String(_value) => {
|
||||
true_count += 1;
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Boolean(b) => {
|
||||
if !b {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
true_count += 1;
|
||||
}
|
||||
CalcResult::Number(value) => {
|
||||
if value == 0.0 {
|
||||
return CalcResult::Boolean(false);
|
||||
}
|
||||
true_count += 1;
|
||||
}
|
||||
CalcResult::String(_value) => {
|
||||
true_count += 1;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
};
|
||||
}
|
||||
if true_count == 0 {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Boolean values not found".to_string(),
|
||||
);
|
||||
}
|
||||
CalcResult::Boolean(true)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_or(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let mut result = false;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Boolean(value) => result = value || result,
|
||||
CalcResult::Number(value) => {
|
||||
if value != 0.0 {
|
||||
return CalcResult::Boolean(true);
|
||||
}
|
||||
}
|
||||
CalcResult::String(_value) => {
|
||||
return CalcResult::Boolean(true);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Boolean(value) => {
|
||||
result = value || result;
|
||||
}
|
||||
CalcResult::Number(value) => {
|
||||
if value != 0.0 {
|
||||
return CalcResult::Boolean(true);
|
||||
}
|
||||
}
|
||||
CalcResult::String(_value) => {
|
||||
return CalcResult::Boolean(true);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
};
|
||||
}
|
||||
CalcResult::Boolean(result)
|
||||
}
|
||||
|
||||
/// XOR(logical1, [logical]*,...)
|
||||
/// Logical1 is required, subsequent logical values are optional. Can be logical values, arrays, or references.
|
||||
/// The result of XOR is TRUE when the number of TRUE inputs is odd and FALSE when the number of TRUE inputs is even.
|
||||
pub(crate) fn fn_xor(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let mut true_count = 0;
|
||||
let mut false_count = 0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Boolean(b) => {
|
||||
if b {
|
||||
true_count += 1;
|
||||
} else {
|
||||
false_count += 1;
|
||||
}
|
||||
}
|
||||
CalcResult::Number(value) => {
|
||||
if value != 0.0 {
|
||||
true_count += 1;
|
||||
} else {
|
||||
false_count += 1;
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Boolean(b) => {
|
||||
if b {
|
||||
true_count += 1;
|
||||
} else {
|
||||
false_count += 1;
|
||||
}
|
||||
}
|
||||
CalcResult::Number(value) => {
|
||||
if value != 0.0 {
|
||||
true_count += 1;
|
||||
} else {
|
||||
false_count += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
if true_count == 0 && false_count == 0 {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "No booleans found".to_string());
|
||||
}
|
||||
CalcResult::Boolean(true_count % 2 == 1)
|
||||
}
|
||||
|
||||
/// =SWITCH(expression, case1, value1, [case, value]*, [default])
|
||||
pub(crate) fn fn_switch(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count < 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
// TODO add implicit intersection
|
||||
let expr = self.evaluate_node_in_context(&args[0], cell);
|
||||
if expr.is_error() {
|
||||
return expr;
|
||||
}
|
||||
|
||||
// How many cases we have?
|
||||
// 3, 4 args -> 1 case
|
||||
let case_count = (args_count - 1) / 2;
|
||||
for case_index in 0..case_count {
|
||||
let case = self.evaluate_node_in_context(&args[2 * case_index + 1], cell);
|
||||
if case.is_error() {
|
||||
return case;
|
||||
}
|
||||
if compare_values(&expr, &case) == 0 {
|
||||
return self.evaluate_node_in_context(&args[2 * case_index + 2], cell);
|
||||
}
|
||||
}
|
||||
// None of the cases matched so we return the default
|
||||
// If there is an even number of args is the last one otherwise is #N/A
|
||||
if args_count % 2 == 0 {
|
||||
return self.evaluate_node_in_context(&args[args_count - 1], cell);
|
||||
}
|
||||
CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Did not find a match".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// =IFS(condition1, value, [condition, value]*)
|
||||
pub(crate) fn fn_ifs(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count < 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
if args_count % 2 != 0 {
|
||||
// Missing value for last condition
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let case_count = args_count / 2;
|
||||
for case_index in 0..case_count {
|
||||
let value = self.get_boolean(&args[2 * case_index], cell);
|
||||
match value {
|
||||
Ok(b) => {
|
||||
if b {
|
||||
return self.evaluate_node_in_context(&args[2 * case_index + 1], cell);
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
}
|
||||
}
|
||||
CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Did not find a match".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
843
base/src/functions/lookup_and_reference.rs
Normal file
843
base/src/functions/lookup_and_reference.rs
Normal file
@@ -0,0 +1,843 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
utils::ParsedReference,
|
||||
};
|
||||
|
||||
use super::util::{compare_values, from_wildcard_to_regex, result_matches_regex, values_are_equal};
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_index(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let row_num;
|
||||
let col_num;
|
||||
if args.len() == 3 {
|
||||
row_num = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
if row_num < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument must be >= 1".to_string(),
|
||||
};
|
||||
}
|
||||
col_num = match self.get_number(&args[2], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
if col_num < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument must be >= 1".to_string(),
|
||||
};
|
||||
}
|
||||
} else if args.len() == 2 {
|
||||
row_num = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => {
|
||||
return s;
|
||||
}
|
||||
};
|
||||
if row_num < 1.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument must be >= 1".to_string(),
|
||||
};
|
||||
}
|
||||
col_num = -1.0;
|
||||
} else {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
match self.evaluate_node_in_context(&args[0], cell) {
|
||||
CalcResult::Range { left, right } => {
|
||||
let row;
|
||||
let column;
|
||||
if (col_num + 1.0).abs() < f64::EPSILON {
|
||||
if left.row == right.row {
|
||||
column = left.column + (row_num as i32) - 1;
|
||||
row = left.row;
|
||||
} else {
|
||||
column = left.column;
|
||||
row = left.row + (row_num as i32) - 1;
|
||||
}
|
||||
} else {
|
||||
row = left.row + (row_num as i32) - 1;
|
||||
column = left.column + (col_num as i32) - 1;
|
||||
}
|
||||
if row > right.row {
|
||||
return CalcResult::Error {
|
||||
error: Error::REF,
|
||||
origin: cell,
|
||||
message: "Wrong reference".to_string(),
|
||||
};
|
||||
}
|
||||
if column > right.column {
|
||||
return CalcResult::Error {
|
||||
error: Error::REF,
|
||||
origin: cell,
|
||||
message: "Wrong reference".to_string(),
|
||||
};
|
||||
}
|
||||
self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
})
|
||||
}
|
||||
error @ CalcResult::Error { .. } => error,
|
||||
_ => CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Expecting a Range".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// MATCH(lookup_value, lookup_array, [match_type])
|
||||
// The MATCH function syntax has the following arguments:
|
||||
// * lookup_value Required. The value that you want to match in lookup_array.
|
||||
// The lookup_value argument can be a value (number, text, or logical value)
|
||||
// or a cell reference to a number, text, or logical value.
|
||||
// * lookup_array Required. The range of cells being searched.
|
||||
// * match_type Optional. The number -1, 0, or 1.
|
||||
// The match_type argument specifies how Excel matches lookup_value
|
||||
// with values in lookup_array. The default value for this argument is 1.
|
||||
// NOTE: Please read the caveat above in binary search
|
||||
pub(crate) fn fn_match(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() > 3 || args.len() < 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let target = self.evaluate_node_in_context(&args[0], cell);
|
||||
if target.is_error() {
|
||||
return target;
|
||||
}
|
||||
if matches!(target, CalcResult::EmptyCell) {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Cannot match empty cell".to_string(),
|
||||
};
|
||||
}
|
||||
let match_type = if args.len() == 3 {
|
||||
match self.get_number(&args[2], cell) {
|
||||
Ok(v) => v as i32,
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
1
|
||||
};
|
||||
let match_range = self.evaluate_node_in_context(&args[1], cell);
|
||||
|
||||
match match_range {
|
||||
CalcResult::Range { left, right } => {
|
||||
match match_type {
|
||||
-1 => {
|
||||
// We apply binary search leftmost for value in the range
|
||||
let is_row_vector;
|
||||
if left.row == right.row {
|
||||
is_row_vector = false;
|
||||
} else if left.column == right.column {
|
||||
is_row_vector = true;
|
||||
} else {
|
||||
// second argument must be a vector
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Argument must be a vector".to_string(),
|
||||
};
|
||||
}
|
||||
let n = if is_row_vector {
|
||||
right.row - left.row
|
||||
} else {
|
||||
right.column - left.column
|
||||
} + 1;
|
||||
let mut l = 0;
|
||||
let mut r = n;
|
||||
while l < r {
|
||||
let m = (l + r) / 2;
|
||||
let row;
|
||||
let column;
|
||||
if is_row_vector {
|
||||
row = left.row + m;
|
||||
column = left.column;
|
||||
} else {
|
||||
column = left.column + m;
|
||||
row = left.row;
|
||||
}
|
||||
let value = self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
});
|
||||
|
||||
if compare_values(&value, &target) >= 0 {
|
||||
l = m + 1;
|
||||
} else {
|
||||
r = m;
|
||||
}
|
||||
}
|
||||
// r is the number of elements less than target in the vector
|
||||
// If target is less than the minimum return #N/A
|
||||
if l == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
};
|
||||
}
|
||||
// Now l points to the leftmost element
|
||||
CalcResult::Number(l as f64)
|
||||
}
|
||||
0 => {
|
||||
// We apply linear search
|
||||
let is_row_vector;
|
||||
if left.row == right.row {
|
||||
is_row_vector = false;
|
||||
} else if left.column == right.column {
|
||||
is_row_vector = true;
|
||||
} else {
|
||||
// second argument must be a vector
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Argument must be a vector".to_string(),
|
||||
};
|
||||
}
|
||||
let n = if is_row_vector {
|
||||
right.row - left.row
|
||||
} else {
|
||||
right.column - left.column
|
||||
} + 1;
|
||||
let result_matches: Box<dyn Fn(&CalcResult) -> bool> =
|
||||
if let CalcResult::String(s) = &target {
|
||||
if let Ok(reg) = from_wildcard_to_regex(&s.to_lowercase(), true) {
|
||||
Box::new(move |x| result_matches_regex(x, ®))
|
||||
} else {
|
||||
Box::new(move |_| false)
|
||||
}
|
||||
} else {
|
||||
Box::new(move |x| values_are_equal(x, &target))
|
||||
};
|
||||
for l in 0..n {
|
||||
let row;
|
||||
let column;
|
||||
if is_row_vector {
|
||||
row = left.row + l;
|
||||
column = left.column;
|
||||
} else {
|
||||
column = left.column + l;
|
||||
row = left.row;
|
||||
}
|
||||
let value = self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
});
|
||||
if result_matches(&value) {
|
||||
return CalcResult::Number(l as f64 + 1.0);
|
||||
}
|
||||
}
|
||||
CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// l is the number of elements less than target in the vector
|
||||
let is_row_vector;
|
||||
if left.row == right.row {
|
||||
is_row_vector = false;
|
||||
} else if left.column == right.column {
|
||||
is_row_vector = true;
|
||||
} else {
|
||||
// second argument must be a vector
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Argument must be a vector".to_string(),
|
||||
};
|
||||
}
|
||||
let l = self.binary_search(&target, &left, &right, is_row_vector);
|
||||
if l == -2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
CalcResult::Number(l as f64 + 1.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => error,
|
||||
_ => CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Invalid".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// HLOOKUP(lookup_value, table_array, row_index, [is_sorted])
|
||||
/// We look for `lookup_value` in the first row of table array
|
||||
/// We return the value in row `row_index` of the same column in `table_array`
|
||||
/// `is_sorted` is true by default and assumes that values in first row are ordered
|
||||
pub(crate) fn fn_hlookup(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() > 4 || args.len() < 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let lookup_value = self.evaluate_node_in_context(&args[0], cell);
|
||||
if lookup_value.is_error() {
|
||||
return lookup_value;
|
||||
}
|
||||
let row_index = match self.get_number(&args[2], cell) {
|
||||
Ok(v) => v.floor() as i32,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let is_sorted = if args.len() == 4 {
|
||||
match self.get_boolean(&args[3], cell) {
|
||||
Ok(v) => v,
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
let range = self.evaluate_node_in_context(&args[1], cell);
|
||||
match range {
|
||||
CalcResult::Range { left, right } => {
|
||||
if is_sorted {
|
||||
// This assumes the values in row are in order
|
||||
let l = self.binary_search(&lookup_value, &left, &right, false);
|
||||
if l == -2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
};
|
||||
}
|
||||
let row = left.row + row_index - 1;
|
||||
let column = left.column + l;
|
||||
if row > right.row {
|
||||
return CalcResult::Error {
|
||||
error: Error::REF,
|
||||
origin: cell,
|
||||
message: "Invalid reference".to_string(),
|
||||
};
|
||||
}
|
||||
self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
})
|
||||
} else {
|
||||
// Linear search for exact match
|
||||
let n = right.column - left.column + 1;
|
||||
let row = left.row + row_index - 1;
|
||||
if row > right.row {
|
||||
return CalcResult::Error {
|
||||
error: Error::REF,
|
||||
origin: cell,
|
||||
message: "Invalid reference".to_string(),
|
||||
};
|
||||
}
|
||||
let result_matches: Box<dyn Fn(&CalcResult) -> bool> =
|
||||
if let CalcResult::String(s) = &lookup_value {
|
||||
if let Ok(reg) = from_wildcard_to_regex(&s.to_lowercase(), true) {
|
||||
Box::new(move |x| result_matches_regex(x, ®))
|
||||
} else {
|
||||
Box::new(move |_| false)
|
||||
}
|
||||
} else {
|
||||
Box::new(move |x| compare_values(x, &lookup_value) == 0)
|
||||
};
|
||||
for l in 0..n {
|
||||
let value = self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row: left.row,
|
||||
column: left.column + l,
|
||||
});
|
||||
if result_matches(&value) {
|
||||
return self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column: left.column + l,
|
||||
});
|
||||
}
|
||||
}
|
||||
CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => error,
|
||||
CalcResult::String(_) => CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Range expected".to_string(),
|
||||
},
|
||||
_ => CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Range expected".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// VLOOKUP(lookup_value, table_array, row_index, [is_sorted])
|
||||
/// We look for `lookup_value` in the first column of table array
|
||||
/// We return the value in column `column_index` of the same row in `table_array`
|
||||
/// `is_sorted` is true by default and assumes that values in first column are ordered
|
||||
pub(crate) fn fn_vlookup(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() > 4 || args.len() < 3 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let lookup_value = self.evaluate_node_in_context(&args[0], cell);
|
||||
if lookup_value.is_error() {
|
||||
return lookup_value;
|
||||
}
|
||||
let column_index = match self.get_number(&args[2], cell) {
|
||||
Ok(v) => v.floor() as i32,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let is_sorted = if args.len() == 4 {
|
||||
match self.get_boolean(&args[3], cell) {
|
||||
Ok(v) => v,
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
let range = self.evaluate_node_in_context(&args[1], cell);
|
||||
match range {
|
||||
CalcResult::Range { left, right } => {
|
||||
if is_sorted {
|
||||
// This assumes the values in column are in order
|
||||
let l = self.binary_search(&lookup_value, &left, &right, true);
|
||||
if l == -2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
};
|
||||
}
|
||||
let row = left.row + l;
|
||||
let column = left.column + column_index - 1;
|
||||
if column > right.column {
|
||||
return CalcResult::Error {
|
||||
error: Error::REF,
|
||||
origin: cell,
|
||||
message: "Invalid reference".to_string(),
|
||||
};
|
||||
}
|
||||
self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
})
|
||||
} else {
|
||||
// Linear search for exact match
|
||||
let n = right.row - left.row + 1;
|
||||
let column = left.column + column_index - 1;
|
||||
if column > right.column {
|
||||
return CalcResult::Error {
|
||||
error: Error::REF,
|
||||
origin: cell,
|
||||
message: "Invalid reference".to_string(),
|
||||
};
|
||||
}
|
||||
let result_matches: Box<dyn Fn(&CalcResult) -> bool> =
|
||||
if let CalcResult::String(s) = &lookup_value {
|
||||
if let Ok(reg) = from_wildcard_to_regex(&s.to_lowercase(), true) {
|
||||
Box::new(move |x| result_matches_regex(x, ®))
|
||||
} else {
|
||||
Box::new(move |_| false)
|
||||
}
|
||||
} else {
|
||||
Box::new(move |x| compare_values(x, &lookup_value) == 0)
|
||||
};
|
||||
for l in 0..n {
|
||||
let value = self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row: left.row + l,
|
||||
column: left.column,
|
||||
});
|
||||
if result_matches(&value) {
|
||||
return self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row: left.row + l,
|
||||
column,
|
||||
});
|
||||
}
|
||||
}
|
||||
CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => error,
|
||||
CalcResult::String(_) => CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Range expected".to_string(),
|
||||
},
|
||||
_ => CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Range expected".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// LOOKUP(lookup_value, lookup_vector, [result_vector])
|
||||
// Important: The values in lookup_vector must be placed in ascending order:
|
||||
// ..., -2, -1, 0, 1, 2, ..., A-Z, FALSE, TRUE;
|
||||
// otherwise, LOOKUP might not return the correct value.
|
||||
// Uppercase and lowercase text are equivalent.
|
||||
// TODO: Implement the other form of INDEX:
|
||||
// INDEX(reference, row_num, [column_num], [area_num])
|
||||
// NOTE: Please read the caveat above in binary search
|
||||
pub(crate) fn fn_lookup(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() > 3 || args.len() < 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let target = self.evaluate_node_in_context(&args[0], cell);
|
||||
if target.is_error() {
|
||||
return target;
|
||||
}
|
||||
let value = self.evaluate_node_in_context(&args[1], cell);
|
||||
match value {
|
||||
CalcResult::Range { left, right } => {
|
||||
let is_row_vector;
|
||||
if left.row == right.row {
|
||||
is_row_vector = false;
|
||||
} else if left.column == right.column {
|
||||
is_row_vector = true;
|
||||
} else {
|
||||
// second argument must be a vector
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Second argument must be a vector".to_string(),
|
||||
};
|
||||
}
|
||||
let l = self.binary_search(&target, &left, &right, is_row_vector);
|
||||
if l == -2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
if args.len() == 3 {
|
||||
let target_range = self.evaluate_node_in_context(&args[2], cell);
|
||||
match target_range {
|
||||
CalcResult::Range {
|
||||
left: l1,
|
||||
right: _r1,
|
||||
} => {
|
||||
let row;
|
||||
let column;
|
||||
if is_row_vector {
|
||||
row = l1.row + l;
|
||||
column = l1.column;
|
||||
} else {
|
||||
column = l1.column + l;
|
||||
row = l1.row;
|
||||
}
|
||||
self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
})
|
||||
}
|
||||
error @ CalcResult::Error { .. } => error,
|
||||
_ => CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Range expected".to_string(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
let row;
|
||||
let column;
|
||||
if is_row_vector {
|
||||
row = left.row + l;
|
||||
column = left.column;
|
||||
} else {
|
||||
column = left.column + l;
|
||||
row = left.row;
|
||||
}
|
||||
self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
})
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => error,
|
||||
_ => CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Range expected".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ROW([reference])
|
||||
// If reference is not present returns the row of the present cell.
|
||||
// Otherwise returns the row number of reference
|
||||
pub(crate) fn fn_row(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() > 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
if args.is_empty() {
|
||||
return CalcResult::Number(cell.row as f64);
|
||||
}
|
||||
match self.get_reference(&args[0], cell) {
|
||||
Ok(c) => CalcResult::Number(c.left.row as f64),
|
||||
Err(s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
// ROWS(range)
|
||||
// Returns the number of rows in range
|
||||
pub(crate) fn fn_rows(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
match self.get_reference(&args[0], cell) {
|
||||
Ok(c) => CalcResult::Number((c.right.row - c.left.row + 1) as f64),
|
||||
Err(s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
// COLUMN([reference])
|
||||
// If reference is not present returns the column of the present cell.
|
||||
// Otherwise returns the column number of reference
|
||||
pub(crate) fn fn_column(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() > 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
if args.is_empty() {
|
||||
return CalcResult::Number(cell.column as f64);
|
||||
}
|
||||
|
||||
match self.get_reference(&args[0], cell) {
|
||||
Ok(range) => CalcResult::Number(range.left.column as f64),
|
||||
Err(s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
/// CHOOSE(index_num, value1, [value2], ...)
|
||||
/// Uses index_num to return a value from the list of value arguments.
|
||||
pub(crate) fn fn_choose(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() < 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let index_num = match self.get_number(&args[0], cell) {
|
||||
Ok(index_num) => index_num as usize,
|
||||
Err(calc_err) => return calc_err,
|
||||
};
|
||||
|
||||
if index_num < 1 || index_num > (args.len() - 1) {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Invalid index".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
self.evaluate_node_with_reference(&args[index_num], cell)
|
||||
}
|
||||
|
||||
// COLUMNS(range)
|
||||
// Returns the number of columns in range
|
||||
pub(crate) fn fn_columns(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
match self.get_reference(&args[0], cell) {
|
||||
Ok(c) => CalcResult::Number((c.right.column - c.left.column + 1) as f64),
|
||||
Err(s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
// INDIRECT(ref_tex)
|
||||
// Returns the reference specified by 'ref_text'
|
||||
pub(crate) fn fn_indirect(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() > 2 || args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = self.get_string(&args[0], cell);
|
||||
match value {
|
||||
Ok(s) => {
|
||||
if args.len() == 2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NIMPL,
|
||||
origin: cell,
|
||||
message: "Not implemented".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let parsed_reference = ParsedReference::parse_reference_formula(
|
||||
Some(cell.sheet),
|
||||
&s,
|
||||
&self.locale,
|
||||
|name| self.get_sheet_index_by_name(name),
|
||||
);
|
||||
|
||||
let parsed_reference = match parsed_reference {
|
||||
Ok(reference) => reference,
|
||||
Err(message) => {
|
||||
return CalcResult::Error {
|
||||
error: Error::REF,
|
||||
origin: cell,
|
||||
message,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
match parsed_reference {
|
||||
ParsedReference::CellReference(reference) => CalcResult::Range {
|
||||
left: reference,
|
||||
right: reference,
|
||||
},
|
||||
ParsedReference::Range(left, right) => CalcResult::Range { left, right },
|
||||
}
|
||||
}
|
||||
Err(v) => v,
|
||||
}
|
||||
}
|
||||
|
||||
// OFFSET(reference, rows, cols, [height], [width])
|
||||
// Returns a reference to a range that is a specified number of rows and columns from a cell or range of cells.
|
||||
// The reference that is returned can be a single cell or a range of cells.
|
||||
// You can specify the number of rows and the number of columns to be returned.
|
||||
pub(crate) fn fn_offset(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let l = args.len();
|
||||
if !(3..=5).contains(&l) {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let reference = match self.get_reference(&args[0], cell) {
|
||||
Ok(c) => c,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let rows = match self.get_number(&args[1], cell) {
|
||||
Ok(c) => {
|
||||
if c < 0.0 {
|
||||
c.ceil() as i32
|
||||
} else {
|
||||
c.floor() as i32
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let cols = match self.get_number(&args[2], cell) {
|
||||
Ok(c) => {
|
||||
if c < 0.0 {
|
||||
c.ceil() as i32
|
||||
} else {
|
||||
c.floor() as i32
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let row_start = reference.left.row + rows;
|
||||
let column_start = reference.left.column + cols;
|
||||
let width;
|
||||
let height;
|
||||
if l == 4 {
|
||||
height = match self.get_number(&args[3], cell) {
|
||||
Ok(c) => {
|
||||
if c < 1.0 {
|
||||
c.ceil() as i32 - 1
|
||||
} else {
|
||||
c.floor() as i32 - 1
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
width = reference.right.column - reference.left.column;
|
||||
} else if l == 5 {
|
||||
height = match self.get_number(&args[3], cell) {
|
||||
Ok(c) => {
|
||||
if c < 1.0 {
|
||||
c.ceil() as i32 - 1
|
||||
} else {
|
||||
c.floor() as i32 - 1
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
width = match self.get_number(&args[4], cell) {
|
||||
Ok(c) => {
|
||||
if c < 1.0 {
|
||||
c.ceil() as i32 - 1
|
||||
} else {
|
||||
c.floor() as i32 - 1
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
} else {
|
||||
width = reference.right.column - reference.left.column;
|
||||
height = reference.right.row - reference.left.row;
|
||||
}
|
||||
// This is what Excel does
|
||||
if width == -1 || height == -1 {
|
||||
return CalcResult::Error {
|
||||
error: Error::REF,
|
||||
origin: cell,
|
||||
message: "Invalid reference".to_string(),
|
||||
};
|
||||
}
|
||||
// NB: Excel documentation says that negative values of width and height are not valid
|
||||
// but in practice they are valid. We follow the documentation and not Excel
|
||||
if width < -1 || height < -1 {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "width and height cannot be negative".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
let column_end = column_start + width;
|
||||
let row_end = row_start + height;
|
||||
if row_start < 1 || row_end > LAST_ROW || column_start < 1 || column_end > LAST_COLUMN {
|
||||
return CalcResult::Error {
|
||||
error: Error::REF,
|
||||
origin: cell,
|
||||
message: "Invalid reference".to_string(),
|
||||
};
|
||||
}
|
||||
let left = CellReference {
|
||||
sheet: reference.left.sheet,
|
||||
row: row_start,
|
||||
column: column_start,
|
||||
};
|
||||
let right = CellReference {
|
||||
sheet: reference.right.sheet,
|
||||
row: row_end,
|
||||
column: column_end,
|
||||
};
|
||||
CalcResult::Range { left, right }
|
||||
}
|
||||
}
|
||||
671
base/src/functions/mathematical.rs
Normal file
671
base/src/functions/mathematical.rs
Normal file
@@ -0,0 +1,671 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
};
|
||||
use std::f64::consts::PI;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn random() -> f64 {
|
||||
rand::random()
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn random() -> f64 {
|
||||
use js_sys::Math;
|
||||
Math::random()
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_min(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let mut result = f64::NAN;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => result = value.min(result),
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
result = value.min(result);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
};
|
||||
}
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Number(0.0);
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_max(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let mut result = f64::NAN;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => result = value.max(result),
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
result = value.max(result);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
};
|
||||
}
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Number(0.0);
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sum(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let mut result = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => result += value,
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
// TODO: We should do this for all functions that run through ranges
|
||||
// Running cargo test for the ironcalc takes around .8 seconds with this speedup
|
||||
// and ~ 3.5 seconds without it. Note that once properly in place sheet.dimension should be almost a noop
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = self
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = self
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
result += value;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_product(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut result = 1.0;
|
||||
let mut seen_value = false;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
seen_value = true;
|
||||
result *= value;
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
let row1 = left.row;
|
||||
let mut row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let mut column2 = right.column;
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = self
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = self
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
for row in row1..row2 + 1 {
|
||||
for column in column1..(column2 + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
seen_value = true;
|
||||
result *= value;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
};
|
||||
}
|
||||
if !seen_value {
|
||||
return CalcResult::Number(0.0);
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
/// SUMIF(criteria_range, criteria, [sum_range])
|
||||
/// if sum_rage is missing then criteria_range will be used
|
||||
pub(crate) fn fn_sumif(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 2 {
|
||||
let arguments = vec![args[0].clone(), args[0].clone(), args[1].clone()];
|
||||
self.fn_sumifs(&arguments, cell)
|
||||
} else if args.len() == 3 {
|
||||
let arguments = vec![args[2].clone(), args[0].clone(), args[1].clone()];
|
||||
self.fn_sumifs(&arguments, cell)
|
||||
} else {
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
}
|
||||
|
||||
/// SUMIFS(sum_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...)
|
||||
pub(crate) fn fn_sumifs(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let mut total = 0.0;
|
||||
let sum = |value| total += value;
|
||||
if let Err(e) = self.apply_ifs(args, cell, sum) {
|
||||
return e;
|
||||
}
|
||||
CalcResult::Number(total)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_round(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
// Incorrect number of arguments
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let number_of_digits = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => {
|
||||
if f > 0.0 {
|
||||
f.floor()
|
||||
} else {
|
||||
f.ceil()
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let scale = 10.0_f64.powf(number_of_digits);
|
||||
CalcResult::Number((value * scale).round() / scale)
|
||||
}
|
||||
pub(crate) fn fn_roundup(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let number_of_digits = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => {
|
||||
if f > 0.0 {
|
||||
f.floor()
|
||||
} else {
|
||||
f.ceil()
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let scale = 10.0_f64.powf(number_of_digits);
|
||||
if value > 0.0 {
|
||||
CalcResult::Number((value * scale).ceil() / scale)
|
||||
} else {
|
||||
CalcResult::Number((value * scale).floor() / scale)
|
||||
}
|
||||
}
|
||||
pub(crate) fn fn_rounddown(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let number_of_digits = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => {
|
||||
if f > 0.0 {
|
||||
f.floor()
|
||||
} else {
|
||||
f.ceil()
|
||||
}
|
||||
}
|
||||
Err(s) => return s,
|
||||
};
|
||||
let scale = 10.0_f64.powf(number_of_digits);
|
||||
if value > 0.0 {
|
||||
CalcResult::Number((value * scale).floor() / scale)
|
||||
} else {
|
||||
CalcResult::Number((value * scale).ceil() / scale)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sin(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.sin();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
pub(crate) fn fn_cos(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.cos();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_tan(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.tan();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sinh(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.sinh();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
pub(crate) fn fn_cosh(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.cosh();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_tanh(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.tanh();
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_asin(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.asin();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ASIN".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
pub(crate) fn fn_acos(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.acos();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for COS".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_atan(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.atan();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ATAN".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_asinh(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.asinh();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ASINH".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
pub(crate) fn fn_acosh(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.acosh();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ACOSH".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_atanh(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let result = value.atanh();
|
||||
if result.is_nan() || result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid argument for ATANH".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_pi(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
CalcResult::Number(PI)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_abs(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
CalcResult::Number(value.abs())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sqrtpi(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if value < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Argument of SQRTPI should be >= 0".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number((value * PI).sqrt())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_sqrt(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if value < 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Argument of SQRT should be >= 0".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(value.sqrt())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_atan2(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let y = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if x == 0.0 && y == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Arguments can't be both zero".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(f64::atan2(y, x))
|
||||
}
|
||||
|
||||
pub(crate) fn fn_power(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let y = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if x == 0.0 && y == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Arguments can't be both zero".to_string(),
|
||||
};
|
||||
}
|
||||
if y == 0.0 {
|
||||
return CalcResult::Number(1.0);
|
||||
}
|
||||
let result = x.powf(y);
|
||||
if result.is_infinite() {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "POWER returned infinity".to_string(),
|
||||
};
|
||||
}
|
||||
if result.is_nan() {
|
||||
// This might happen for some combinations of negative base and exponent
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: "Invalid arguments for POWER".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_rand(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if !args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
CalcResult::Number(random())
|
||||
}
|
||||
|
||||
// TODO: Add tests for RANDBETWEEN
|
||||
pub(crate) fn fn_randbetween(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() != 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let x = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f.floor(),
|
||||
Err(s) => return s,
|
||||
};
|
||||
let y = match self.get_number(&args[1], cell) {
|
||||
Ok(f) => f.ceil() + 1.0,
|
||||
Err(s) => return s,
|
||||
};
|
||||
if x > y {
|
||||
return CalcResult::Error {
|
||||
error: Error::NUM,
|
||||
origin: cell,
|
||||
message: format!("{x}>{y}"),
|
||||
};
|
||||
}
|
||||
CalcResult::Number((x + random() * (y - x)).floor())
|
||||
}
|
||||
}
|
||||
930
base/src/functions/mod.rs
Normal file
930
base/src/functions/mod.rs
Normal file
@@ -0,0 +1,930 @@
|
||||
use core::fmt;
|
||||
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::{parser::Node, token::Error},
|
||||
model::Model,
|
||||
};
|
||||
|
||||
pub(crate) mod binary_search;
|
||||
mod date_and_time;
|
||||
mod engineering;
|
||||
mod financial;
|
||||
mod financial_util;
|
||||
mod information;
|
||||
mod logical;
|
||||
mod lookup_and_reference;
|
||||
mod mathematical;
|
||||
mod statistical;
|
||||
mod subtotal;
|
||||
mod text;
|
||||
mod text_util;
|
||||
pub(crate) mod util;
|
||||
mod xlookup;
|
||||
|
||||
/// List of all implemented functions
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum Function {
|
||||
// Logical
|
||||
And,
|
||||
False,
|
||||
If,
|
||||
Iferror,
|
||||
Ifna,
|
||||
Ifs,
|
||||
Not,
|
||||
Or,
|
||||
Switch,
|
||||
True,
|
||||
Xor,
|
||||
|
||||
// Mathematical and trigonometry
|
||||
Abs,
|
||||
Acos,
|
||||
Acosh,
|
||||
Asin,
|
||||
Asinh,
|
||||
Atan,
|
||||
Atan2,
|
||||
Atanh,
|
||||
Choose,
|
||||
Column,
|
||||
Columns,
|
||||
Cos,
|
||||
Cosh,
|
||||
Max,
|
||||
Min,
|
||||
Pi,
|
||||
Power,
|
||||
Product,
|
||||
Rand,
|
||||
Randbetween,
|
||||
Round,
|
||||
Rounddown,
|
||||
Roundup,
|
||||
Sin,
|
||||
Sinh,
|
||||
Sqrt,
|
||||
Sqrtpi,
|
||||
Sum,
|
||||
Sumif,
|
||||
Sumifs,
|
||||
Tan,
|
||||
Tanh,
|
||||
|
||||
// Information
|
||||
ErrorType,
|
||||
Isblank,
|
||||
Iserr,
|
||||
Iserror,
|
||||
Iseven,
|
||||
Isformula,
|
||||
Islogical,
|
||||
Isna,
|
||||
Isnontext,
|
||||
Isnumber,
|
||||
Isodd,
|
||||
Isref,
|
||||
Istext,
|
||||
Na,
|
||||
Sheet,
|
||||
Type,
|
||||
|
||||
// Lookup and reference
|
||||
Hlookup,
|
||||
Index,
|
||||
Indirect,
|
||||
Lookup,
|
||||
Match,
|
||||
Offset,
|
||||
Row,
|
||||
Rows,
|
||||
Vlookup,
|
||||
Xlookup,
|
||||
|
||||
// Text
|
||||
Concat,
|
||||
Concatenate,
|
||||
Exact,
|
||||
Find,
|
||||
Left,
|
||||
Len,
|
||||
Lower,
|
||||
Mid,
|
||||
Rept,
|
||||
Right,
|
||||
Search,
|
||||
Substitute,
|
||||
T,
|
||||
Text,
|
||||
Textafter,
|
||||
Textbefore,
|
||||
Textjoin,
|
||||
Trim,
|
||||
Upper,
|
||||
Value,
|
||||
Valuetotext,
|
||||
|
||||
// Statistical
|
||||
Average,
|
||||
Averagea,
|
||||
Averageif,
|
||||
Averageifs,
|
||||
Count,
|
||||
Counta,
|
||||
Countblank,
|
||||
Countif,
|
||||
Countifs,
|
||||
Maxifs,
|
||||
Minifs,
|
||||
|
||||
// Date and time
|
||||
Date,
|
||||
Day,
|
||||
Edate,
|
||||
Eomonth,
|
||||
Month,
|
||||
Now,
|
||||
Today,
|
||||
Year,
|
||||
|
||||
// Financial
|
||||
Cumipmt,
|
||||
Cumprinc,
|
||||
Db,
|
||||
Ddb,
|
||||
Dollarde,
|
||||
Dollarfr,
|
||||
Effect,
|
||||
Fv,
|
||||
Ipmt,
|
||||
Irr,
|
||||
Ispmt,
|
||||
Mirr,
|
||||
Nominal,
|
||||
Nper,
|
||||
Npv,
|
||||
Pduration,
|
||||
Pmt,
|
||||
Ppmt,
|
||||
Pv,
|
||||
Rate,
|
||||
Rri,
|
||||
Sln,
|
||||
Syd,
|
||||
Tbilleq,
|
||||
Tbillprice,
|
||||
Tbillyield,
|
||||
Xirr,
|
||||
Xnpv,
|
||||
|
||||
// Engineering: Bessel and transcendental functions
|
||||
Besseli,
|
||||
Besselj,
|
||||
Besselk,
|
||||
Bessely,
|
||||
Erf,
|
||||
Erfc,
|
||||
ErfcPrecise,
|
||||
ErfPrecise,
|
||||
|
||||
// Engineering: Number systems
|
||||
Bin2dec,
|
||||
Bin2hex,
|
||||
Bin2oct,
|
||||
Dec2Bin,
|
||||
Dec2hex,
|
||||
Dec2oct,
|
||||
Hex2bin,
|
||||
Hex2dec,
|
||||
Hex2oct,
|
||||
Oct2bin,
|
||||
Oct2dec,
|
||||
Oct2hex,
|
||||
|
||||
// Engineering: Bit functions
|
||||
Bitand,
|
||||
Bitlshift,
|
||||
Bitor,
|
||||
Bitrshift,
|
||||
Bitxor,
|
||||
|
||||
// Engineering: Complex functions
|
||||
Complex,
|
||||
Imabs,
|
||||
Imaginary,
|
||||
Imargument,
|
||||
Imconjugate,
|
||||
Imcos,
|
||||
Imcosh,
|
||||
Imcot,
|
||||
Imcsc,
|
||||
Imcsch,
|
||||
Imdiv,
|
||||
Imexp,
|
||||
Imln,
|
||||
Imlog10,
|
||||
Imlog2,
|
||||
Impower,
|
||||
Improduct,
|
||||
Imreal,
|
||||
Imsec,
|
||||
Imsech,
|
||||
Imsin,
|
||||
Imsinh,
|
||||
Imsqrt,
|
||||
Imsub,
|
||||
Imsum,
|
||||
Imtan,
|
||||
|
||||
// Engineering: Misc function
|
||||
Convert,
|
||||
Delta,
|
||||
Gestep,
|
||||
Subtotal,
|
||||
}
|
||||
|
||||
impl Function {
|
||||
/// Some functions in Excel like CONCAT are stringified as `_xlfn.CONCAT`.
|
||||
pub fn to_xlsx_string(&self) -> String {
|
||||
match self {
|
||||
Function::Concat => "_xlfn.CONCAT".to_string(),
|
||||
Function::Ifna => "_xlfn.IFNA".to_string(),
|
||||
Function::Ifs => "_xlfn.IFS".to_string(),
|
||||
Function::Maxifs => "_xlfn.MAXIFS".to_string(),
|
||||
Function::Minifs => "_xlfn.MINIFS".to_string(),
|
||||
Function::Switch => "_xlfn.SWITCH".to_string(),
|
||||
Function::Xlookup => "_xlfn.XLOOKUP".to_string(),
|
||||
Function::Xor => "_xlfn.XOR".to_string(),
|
||||
Function::Textbefore => "_xlfn.TEXTBEFORE".to_string(),
|
||||
Function::Textafter => "_xlfn.TEXTAFTER".to_string(),
|
||||
Function::Textjoin => "_xlfn.TEXTJOIN".to_string(),
|
||||
Function::Rri => "_xlfn.RRI".to_string(),
|
||||
Function::Pduration => "_xlfn.PDURATION".to_string(),
|
||||
Function::Bitand => "_xlfn.BITAND".to_string(),
|
||||
Function::Bitor => "_xlfn.BITOR".to_string(),
|
||||
Function::Bitxor => "_xlfn.BITXOR".to_string(),
|
||||
Function::Bitlshift => "_xlfn.BITLSHIFT".to_string(),
|
||||
Function::Bitrshift => "_xlfn.BITRSHIFT".to_string(),
|
||||
Function::Imtan => "_xlfn.IMTAN".to_string(),
|
||||
Function::Imsinh => "_xlfn.IMSINH".to_string(),
|
||||
Function::Imcosh => "_xlfn.IMCOSH".to_string(),
|
||||
Function::Imcot => "_xlfn.IMCOT".to_string(),
|
||||
Function::Imcsc => "_xlfn.IMCSC".to_string(),
|
||||
Function::Imcsch => "_xlfn.IMCSCH".to_string(),
|
||||
Function::Imsec => "_xlfn.IMSEC".to_string(),
|
||||
Function::ErfcPrecise => "_xlfn.ERFC.PRECISE".to_string(),
|
||||
Function::ErfPrecise => "_xlfn.ERF.PRECISE".to_string(),
|
||||
Function::Valuetotext => "_xlfn.VALUETOTEXT".to_string(),
|
||||
Function::Isformula => "_xlfn.ISFORMULA".to_string(),
|
||||
Function::Sheet => "_xlfn.SHEET".to_string(),
|
||||
_ => self.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn returns_reference(&self) -> bool {
|
||||
matches!(self, Function::Indirect | Function::Offset)
|
||||
}
|
||||
/// Gets the function from the name.
|
||||
/// Note that in Excel some (modern) functions are prefixed by `_xlfn.`
|
||||
pub fn get_function(name: &str) -> Option<Function> {
|
||||
match name.to_ascii_uppercase().as_str() {
|
||||
"AND" => Some(Function::And),
|
||||
"FALSE" => Some(Function::False),
|
||||
"IF" => Some(Function::If),
|
||||
"IFERROR" => Some(Function::Iferror),
|
||||
"IFNA" | "_XLFN.IFNA" => Some(Function::Ifna),
|
||||
"IFS" | "_XLFN.IFS" => Some(Function::Ifs),
|
||||
"NOT" => Some(Function::Not),
|
||||
"OR" => Some(Function::Or),
|
||||
"SWITCH" | "_XLFN.SWITCH" => Some(Function::Switch),
|
||||
"TRUE" => Some(Function::True),
|
||||
"XOR" | "_XLFN.XOR" => Some(Function::Xor),
|
||||
|
||||
"SIN" => Some(Function::Sin),
|
||||
"COS" => Some(Function::Cos),
|
||||
"TAN" => Some(Function::Tan),
|
||||
|
||||
"ASIN" => Some(Function::Asin),
|
||||
"ACOS" => Some(Function::Acos),
|
||||
"ATAN" => Some(Function::Atan),
|
||||
|
||||
"SINH" => Some(Function::Sinh),
|
||||
"COSH" => Some(Function::Cosh),
|
||||
"TANH" => Some(Function::Tanh),
|
||||
|
||||
"ASINH" => Some(Function::Asinh),
|
||||
"ACOSH" => Some(Function::Acosh),
|
||||
"ATANH" => Some(Function::Atanh),
|
||||
|
||||
"PI" => Some(Function::Pi),
|
||||
"ABS" => Some(Function::Abs),
|
||||
"SQRT" => Some(Function::Sqrt),
|
||||
"SQRTPI" => Some(Function::Sqrtpi),
|
||||
"POWER" => Some(Function::Power),
|
||||
"ATAN2" => Some(Function::Atan2),
|
||||
|
||||
"MAX" => Some(Function::Max),
|
||||
"MIN" => Some(Function::Min),
|
||||
"PRODUCT" => Some(Function::Product),
|
||||
"RAND" => Some(Function::Rand),
|
||||
"RANDBETWEEN" => Some(Function::Randbetween),
|
||||
"ROUND" => Some(Function::Round),
|
||||
"ROUNDDOWN" => Some(Function::Rounddown),
|
||||
"ROUNDUP" => Some(Function::Roundup),
|
||||
"SUM" => Some(Function::Sum),
|
||||
"SUMIF" => Some(Function::Sumif),
|
||||
"SUMIFS" => Some(Function::Sumifs),
|
||||
|
||||
// Lookup and Reference
|
||||
"CHOOSE" => Some(Function::Choose),
|
||||
"COLUMN" => Some(Function::Column),
|
||||
"COLUMNS" => Some(Function::Columns),
|
||||
"INDEX" => Some(Function::Index),
|
||||
"INDIRECT" => Some(Function::Indirect),
|
||||
"HLOOKUP" => Some(Function::Hlookup),
|
||||
"LOOKUP" => Some(Function::Lookup),
|
||||
"MATCH" => Some(Function::Match),
|
||||
"OFFSET" => Some(Function::Offset),
|
||||
"ROW" => Some(Function::Row),
|
||||
"ROWS" => Some(Function::Rows),
|
||||
"VLOOKUP" => Some(Function::Vlookup),
|
||||
"XLOOKUP" | "_XLFN.XLOOKUP" => Some(Function::Xlookup),
|
||||
|
||||
"CONCATENATE" => Some(Function::Concatenate),
|
||||
"EXACT" => Some(Function::Exact),
|
||||
"VALUE" => Some(Function::Value),
|
||||
"T" => Some(Function::T),
|
||||
"VALUETOTEXT" | "_XLFN.VALUETOTEXT" => Some(Function::Valuetotext),
|
||||
"CONCAT" | "_XLFN.CONCAT" => Some(Function::Concat),
|
||||
"FIND" => Some(Function::Find),
|
||||
"LEFT" => Some(Function::Left),
|
||||
"LEN" => Some(Function::Len),
|
||||
"LOWER" => Some(Function::Lower),
|
||||
"MID" => Some(Function::Mid),
|
||||
"RIGHT" => Some(Function::Right),
|
||||
"SEARCH" => Some(Function::Search),
|
||||
"TEXT" => Some(Function::Text),
|
||||
"TRIM" => Some(Function::Trim),
|
||||
"UPPER" => Some(Function::Upper),
|
||||
|
||||
"REPT" => Some(Function::Rept),
|
||||
"TEXTAFTER" | "_XLFN.TEXTAFTER" => Some(Function::Textafter),
|
||||
"TEXTBEFORE" | "_XLFN.TEXTBEFORE" => Some(Function::Textbefore),
|
||||
"TEXTJOIN" | "_XLFN.TEXTJOIN" => Some(Function::Textjoin),
|
||||
"SUBSTITUTE" => Some(Function::Substitute),
|
||||
|
||||
"ISNUMBER" => Some(Function::Isnumber),
|
||||
"ISNONTEXT" => Some(Function::Isnontext),
|
||||
"ISTEXT" => Some(Function::Istext),
|
||||
"ISLOGICAL" => Some(Function::Islogical),
|
||||
"ISBLANK" => Some(Function::Isblank),
|
||||
"ISERR" => Some(Function::Iserr),
|
||||
"ISERROR" => Some(Function::Iserror),
|
||||
"ISNA" => Some(Function::Isna),
|
||||
"NA" => Some(Function::Na),
|
||||
"ISREF" => Some(Function::Isref),
|
||||
"ISODD" => Some(Function::Isodd),
|
||||
"ISEVEN" => Some(Function::Iseven),
|
||||
"ERROR.TYPE" => Some(Function::ErrorType),
|
||||
"ISFORMULA" | "_XLFN.ISFORMULA" => Some(Function::Isformula),
|
||||
"TYPE" => Some(Function::Type),
|
||||
"SHEET" | "_XLFN.SHEET" => Some(Function::Sheet),
|
||||
|
||||
"AVERAGE" => Some(Function::Average),
|
||||
"AVERAGEA" => Some(Function::Averagea),
|
||||
"AVERAGEIF" => Some(Function::Averageif),
|
||||
"AVERAGEIFS" => Some(Function::Averageifs),
|
||||
"COUNT" => Some(Function::Count),
|
||||
"COUNTA" => Some(Function::Counta),
|
||||
"COUNTBLANK" => Some(Function::Countblank),
|
||||
"COUNTIF" => Some(Function::Countif),
|
||||
"COUNTIFS" => Some(Function::Countifs),
|
||||
"MAXIFS" | "_XLFN.MAXIFS" => Some(Function::Maxifs),
|
||||
"MINIFS" | "_XLFN.MINIFS" => Some(Function::Minifs),
|
||||
// Date and Time
|
||||
"YEAR" => Some(Function::Year),
|
||||
"DAY" => Some(Function::Day),
|
||||
"EOMONTH" => Some(Function::Eomonth),
|
||||
"MONTH" => Some(Function::Month),
|
||||
"DATE" => Some(Function::Date),
|
||||
"EDATE" => Some(Function::Edate),
|
||||
"TODAY" => Some(Function::Today),
|
||||
"NOW" => Some(Function::Now),
|
||||
// Financial
|
||||
"PMT" => Some(Function::Pmt),
|
||||
"PV" => Some(Function::Pv),
|
||||
"RATE" => Some(Function::Rate),
|
||||
"NPER" => Some(Function::Nper),
|
||||
"FV" => Some(Function::Fv),
|
||||
"PPMT" => Some(Function::Ppmt),
|
||||
"IPMT" => Some(Function::Ipmt),
|
||||
"NPV" => Some(Function::Npv),
|
||||
"XNPV" => Some(Function::Xnpv),
|
||||
"MIRR" => Some(Function::Mirr),
|
||||
"IRR" => Some(Function::Irr),
|
||||
"XIRR" => Some(Function::Xirr),
|
||||
"ISPMT" => Some(Function::Ispmt),
|
||||
"RRI" | "_XLFN.RRI" => Some(Function::Rri),
|
||||
|
||||
"SLN" => Some(Function::Sln),
|
||||
"SYD" => Some(Function::Syd),
|
||||
"NOMINAL" => Some(Function::Nominal),
|
||||
"EFFECT" => Some(Function::Effect),
|
||||
"PDURATION" | "_XLFN.PDURATION" => Some(Function::Pduration),
|
||||
|
||||
"TBILLYIELD" => Some(Function::Tbillyield),
|
||||
"TBILLPRICE" => Some(Function::Tbillprice),
|
||||
"TBILLEQ" => Some(Function::Tbilleq),
|
||||
|
||||
"DOLLARDE" => Some(Function::Dollarde),
|
||||
"DOLLARFR" => Some(Function::Dollarfr),
|
||||
|
||||
"DDB" => Some(Function::Ddb),
|
||||
"DB" => Some(Function::Db),
|
||||
|
||||
"CUMPRINC" => Some(Function::Cumprinc),
|
||||
"CUMIPMT" => Some(Function::Cumipmt),
|
||||
|
||||
"BESSELI" => Some(Function::Besseli),
|
||||
"BESSELJ" => Some(Function::Besselj),
|
||||
"BESSELK" => Some(Function::Besselk),
|
||||
"BESSELY" => Some(Function::Bessely),
|
||||
"ERF" => Some(Function::Erf),
|
||||
"ERF.PRECISE" | "_XLFN.ERF.PRECISE" => Some(Function::ErfPrecise),
|
||||
"ERFC" => Some(Function::Erfc),
|
||||
"ERFC.PRECISE" | "_XLFN.ERFC.PRECISE" => Some(Function::ErfcPrecise),
|
||||
"BIN2DEC" => Some(Function::Bin2dec),
|
||||
"BIN2HEX" => Some(Function::Bin2hex),
|
||||
"BIN2OCT" => Some(Function::Bin2oct),
|
||||
"DEC2BIN" => Some(Function::Dec2Bin),
|
||||
"DEC2HEX" => Some(Function::Dec2hex),
|
||||
"DEC2OCT" => Some(Function::Dec2oct),
|
||||
"HEX2BIN" => Some(Function::Hex2bin),
|
||||
"HEX2DEC" => Some(Function::Hex2dec),
|
||||
"HEX2OCT" => Some(Function::Hex2oct),
|
||||
"OCT2BIN" => Some(Function::Oct2bin),
|
||||
"OCT2DEC" => Some(Function::Oct2dec),
|
||||
"OCT2HEX" => Some(Function::Oct2hex),
|
||||
"BITAND" | "_XLFN.BITAND" => Some(Function::Bitand),
|
||||
"BITLSHIFT" | "_XLFN.BITLSHIFT" => Some(Function::Bitlshift),
|
||||
"BITOR" | "_XLFN.BITOR" => Some(Function::Bitor),
|
||||
"BITRSHIFT" | "_XLFN.BITRSHIFT" => Some(Function::Bitrshift),
|
||||
"BITXOR" | "_XLFN.BITXOR" => Some(Function::Bitxor),
|
||||
"COMPLEX" => Some(Function::Complex),
|
||||
"IMABS" => Some(Function::Imabs),
|
||||
"IMAGINARY" => Some(Function::Imaginary),
|
||||
"IMARGUMENT" => Some(Function::Imargument),
|
||||
"IMCONJUGATE" => Some(Function::Imconjugate),
|
||||
"IMCOS" => Some(Function::Imcos),
|
||||
"IMCOSH" | "_XLFN.IMCOSH" => Some(Function::Imcosh),
|
||||
"IMCOT" | "_XLFN.IMCOT" => Some(Function::Imcot),
|
||||
"IMCSC" | "_XLFN.IMCSC" => Some(Function::Imcsc),
|
||||
"IMCSCH" | "_XLFN.IMCSCH" => Some(Function::Imcsch),
|
||||
"IMDIV" => Some(Function::Imdiv),
|
||||
"IMEXP" => Some(Function::Imexp),
|
||||
"IMLN" => Some(Function::Imln),
|
||||
"IMLOG10" => Some(Function::Imlog10),
|
||||
"IMLOG2" => Some(Function::Imlog2),
|
||||
"IMPOWER" => Some(Function::Impower),
|
||||
"IMPRODUCT" => Some(Function::Improduct),
|
||||
"IMREAL" => Some(Function::Imreal),
|
||||
"IMSEC" | "_XLFN.IMSEC" => Some(Function::Imsec),
|
||||
"IMSECH" | "_XLFN.IMSECH" => Some(Function::Imsech),
|
||||
"IMSIN" => Some(Function::Imsin),
|
||||
"IMSINH" | "_XLFN.IMSINH" => Some(Function::Imsinh),
|
||||
"IMSQRT" => Some(Function::Imsqrt),
|
||||
"IMSUB" => Some(Function::Imsub),
|
||||
"IMSUM" => Some(Function::Imsum),
|
||||
"IMTAN" | "_XLFN.IMTAN" => Some(Function::Imtan),
|
||||
"CONVERT" => Some(Function::Convert),
|
||||
"DELTA" => Some(Function::Delta),
|
||||
"GESTEP" => Some(Function::Gestep),
|
||||
|
||||
"SUBTOTAL" => Some(Function::Subtotal),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Function {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Function::And => write!(f, "AND"),
|
||||
Function::False => write!(f, "FALSE"),
|
||||
Function::If => write!(f, "IF"),
|
||||
Function::Iferror => write!(f, "IFERROR"),
|
||||
Function::Ifna => write!(f, "IFNA"),
|
||||
Function::Ifs => write!(f, "IFS"),
|
||||
Function::Not => write!(f, "NOT"),
|
||||
Function::Or => write!(f, "OR"),
|
||||
Function::Switch => write!(f, "SWITCH"),
|
||||
Function::True => write!(f, "TRUE"),
|
||||
Function::Xor => write!(f, "XOR"),
|
||||
Function::Sin => write!(f, "SIN"),
|
||||
Function::Cos => write!(f, "COS"),
|
||||
Function::Tan => write!(f, "TAN"),
|
||||
Function::Asin => write!(f, "ASIN"),
|
||||
Function::Acos => write!(f, "ACOS"),
|
||||
Function::Atan => write!(f, "ATAN"),
|
||||
Function::Sinh => write!(f, "SINH"),
|
||||
Function::Cosh => write!(f, "COSH"),
|
||||
Function::Tanh => write!(f, "TANH"),
|
||||
Function::Asinh => write!(f, "ASINH"),
|
||||
Function::Acosh => write!(f, "ACOSH"),
|
||||
Function::Atanh => write!(f, "ATANH"),
|
||||
Function::Abs => write!(f, "ABS"),
|
||||
Function::Pi => write!(f, "PI"),
|
||||
Function::Sqrt => write!(f, "SQRT"),
|
||||
Function::Sqrtpi => write!(f, "SQRTPI"),
|
||||
Function::Atan2 => write!(f, "ATAN2"),
|
||||
Function::Power => write!(f, "POWER"),
|
||||
Function::Max => write!(f, "MAX"),
|
||||
Function::Min => write!(f, "MIN"),
|
||||
Function::Product => write!(f, "PRODUCT"),
|
||||
Function::Rand => write!(f, "RAND"),
|
||||
Function::Randbetween => write!(f, "RANDBETWEEN"),
|
||||
Function::Round => write!(f, "ROUND"),
|
||||
Function::Rounddown => write!(f, "ROUNDDOWN"),
|
||||
Function::Roundup => write!(f, "ROUNDUP"),
|
||||
Function::Sum => write!(f, "SUM"),
|
||||
Function::Sumif => write!(f, "SUMIF"),
|
||||
Function::Sumifs => write!(f, "SUMIFS"),
|
||||
Function::Choose => write!(f, "CHOOSE"),
|
||||
Function::Column => write!(f, "COLUMN"),
|
||||
Function::Columns => write!(f, "COLUMNS"),
|
||||
Function::Index => write!(f, "INDEX"),
|
||||
Function::Indirect => write!(f, "INDIRECT"),
|
||||
Function::Hlookup => write!(f, "HLOOKUP"),
|
||||
Function::Lookup => write!(f, "LOOKUP"),
|
||||
Function::Match => write!(f, "MATCH"),
|
||||
Function::Offset => write!(f, "OFFSET"),
|
||||
Function::Row => write!(f, "ROW"),
|
||||
Function::Rows => write!(f, "ROWS"),
|
||||
Function::Vlookup => write!(f, "VLOOKUP"),
|
||||
Function::Xlookup => write!(f, "XLOOKUP"),
|
||||
Function::Concatenate => write!(f, "CONCATENATE"),
|
||||
Function::Exact => write!(f, "EXACT"),
|
||||
Function::Value => write!(f, "VALUE"),
|
||||
Function::T => write!(f, "T"),
|
||||
Function::Valuetotext => write!(f, "VALUETOTEXT"),
|
||||
Function::Concat => write!(f, "CONCAT"),
|
||||
Function::Find => write!(f, "FIND"),
|
||||
Function::Left => write!(f, "LEFT"),
|
||||
Function::Len => write!(f, "LEN"),
|
||||
Function::Lower => write!(f, "LOWER"),
|
||||
Function::Mid => write!(f, "MID"),
|
||||
Function::Right => write!(f, "RIGHT"),
|
||||
Function::Search => write!(f, "SEARCH"),
|
||||
Function::Text => write!(f, "TEXT"),
|
||||
Function::Trim => write!(f, "TRIM"),
|
||||
Function::Upper => write!(f, "UPPER"),
|
||||
Function::Isnumber => write!(f, "ISNUMBER"),
|
||||
Function::Isnontext => write!(f, "ISNONTEXT"),
|
||||
Function::Istext => write!(f, "ISTEXT"),
|
||||
Function::Islogical => write!(f, "ISLOGICAL"),
|
||||
Function::Isblank => write!(f, "ISBLANK"),
|
||||
Function::Iserr => write!(f, "ISERR"),
|
||||
Function::Iserror => write!(f, "ISERROR"),
|
||||
Function::Isna => write!(f, "ISNA"),
|
||||
Function::Na => write!(f, "NA"),
|
||||
Function::Isref => write!(f, "ISREF"),
|
||||
Function::Isodd => write!(f, "ISODD"),
|
||||
Function::Iseven => write!(f, "ISEVEN"),
|
||||
Function::ErrorType => write!(f, "ERROR.TYPE"),
|
||||
Function::Isformula => write!(f, "ISFORMULA"),
|
||||
Function::Type => write!(f, "TYPE"),
|
||||
Function::Sheet => write!(f, "SHEET"),
|
||||
|
||||
Function::Average => write!(f, "AVERAGE"),
|
||||
Function::Averagea => write!(f, "AVERAGEA"),
|
||||
Function::Averageif => write!(f, "AVERAGEIF"),
|
||||
Function::Averageifs => write!(f, "AVERAGEIFS"),
|
||||
Function::Count => write!(f, "COUNT"),
|
||||
Function::Counta => write!(f, "COUNTA"),
|
||||
Function::Countblank => write!(f, "COUNTBLANK"),
|
||||
Function::Countif => write!(f, "COUNTIF"),
|
||||
Function::Countifs => write!(f, "COUNTIFS"),
|
||||
Function::Maxifs => write!(f, "MAXIFS"),
|
||||
Function::Minifs => write!(f, "MINIFS"),
|
||||
Function::Year => write!(f, "YEAR"),
|
||||
Function::Day => write!(f, "DAY"),
|
||||
Function::Month => write!(f, "MONTH"),
|
||||
Function::Eomonth => write!(f, "EOMONTH"),
|
||||
Function::Date => write!(f, "DATE"),
|
||||
Function::Edate => write!(f, "EDATE"),
|
||||
Function::Today => write!(f, "TODAY"),
|
||||
Function::Now => write!(f, "NOW"),
|
||||
Function::Pmt => write!(f, "PMT"),
|
||||
Function::Pv => write!(f, "PV"),
|
||||
Function::Rate => write!(f, "RATE"),
|
||||
Function::Nper => write!(f, "NPER"),
|
||||
Function::Fv => write!(f, "FV"),
|
||||
Function::Ppmt => write!(f, "PPMT"),
|
||||
Function::Ipmt => write!(f, "IPMT"),
|
||||
Function::Npv => write!(f, "NPV"),
|
||||
Function::Mirr => write!(f, "MIRR"),
|
||||
Function::Irr => write!(f, "IRR"),
|
||||
Function::Xirr => write!(f, "XIRR"),
|
||||
Function::Xnpv => write!(f, "XNPV"),
|
||||
Function::Rept => write!(f, "REPT"),
|
||||
Function::Textafter => write!(f, "TEXTAFTER"),
|
||||
Function::Textbefore => write!(f, "TEXTBEFORE"),
|
||||
Function::Textjoin => write!(f, "TEXTJOIN"),
|
||||
Function::Substitute => write!(f, "SUBSTITUTE"),
|
||||
Function::Ispmt => write!(f, "ISPMT"),
|
||||
Function::Rri => write!(f, "RRI"),
|
||||
Function::Sln => write!(f, "SLN"),
|
||||
Function::Syd => write!(f, "SYD"),
|
||||
Function::Nominal => write!(f, "NOMINAL"),
|
||||
Function::Effect => write!(f, "EFFECT"),
|
||||
Function::Pduration => write!(f, "PDURATION"),
|
||||
Function::Tbillyield => write!(f, "TBILLYIELD"),
|
||||
Function::Tbillprice => write!(f, "TBILLPRICE"),
|
||||
Function::Tbilleq => write!(f, "TBILLEQ"),
|
||||
Function::Dollarde => write!(f, "DOLLARDE"),
|
||||
Function::Dollarfr => write!(f, "DOLLARFR"),
|
||||
Function::Ddb => write!(f, "DDB"),
|
||||
Function::Db => write!(f, "DB"),
|
||||
Function::Cumprinc => write!(f, "CUMPRINC"),
|
||||
Function::Cumipmt => write!(f, "CUMIPMT"),
|
||||
Function::Besseli => write!(f, "BESSELI"),
|
||||
Function::Besselj => write!(f, "BESSELJ"),
|
||||
Function::Besselk => write!(f, "BESSELK"),
|
||||
Function::Bessely => write!(f, "BESSELY"),
|
||||
Function::Erf => write!(f, "ERF"),
|
||||
Function::ErfPrecise => write!(f, "ERF.PRECISE"),
|
||||
Function::Erfc => write!(f, "ERFC"),
|
||||
Function::ErfcPrecise => write!(f, "ERFC.PRECISE"),
|
||||
Function::Bin2dec => write!(f, "BIN2DEC"),
|
||||
Function::Bin2hex => write!(f, "BIN2HEX"),
|
||||
Function::Bin2oct => write!(f, "BIN2OCT"),
|
||||
Function::Dec2Bin => write!(f, "DEC2BIN"),
|
||||
Function::Dec2hex => write!(f, "DEC2HEX"),
|
||||
Function::Dec2oct => write!(f, "DEC2OCT"),
|
||||
Function::Hex2bin => write!(f, "HEX2BIN"),
|
||||
Function::Hex2dec => write!(f, "HEX2DEC"),
|
||||
Function::Hex2oct => write!(f, "HEX2OCT"),
|
||||
Function::Oct2bin => write!(f, "OCT2BIN"),
|
||||
Function::Oct2dec => write!(f, "OCT2DEC"),
|
||||
Function::Oct2hex => write!(f, "OCT2HEX"),
|
||||
Function::Bitand => write!(f, "BITAND"),
|
||||
Function::Bitlshift => write!(f, "BITLSHIFT"),
|
||||
Function::Bitor => write!(f, "BITOR"),
|
||||
Function::Bitrshift => write!(f, "BITRSHIFT"),
|
||||
Function::Bitxor => write!(f, "BITXOR"),
|
||||
Function::Complex => write!(f, "COMPLEX"),
|
||||
Function::Imabs => write!(f, "IMABS"),
|
||||
Function::Imaginary => write!(f, "IMAGINARY"),
|
||||
Function::Imargument => write!(f, "IMARGUMENT"),
|
||||
Function::Imconjugate => write!(f, "IMCONJUGATE"),
|
||||
Function::Imcos => write!(f, "IMCOS"),
|
||||
Function::Imcosh => write!(f, "IMCOSH"),
|
||||
Function::Imcot => write!(f, "IMCOT"),
|
||||
Function::Imcsc => write!(f, "IMCSC"),
|
||||
Function::Imcsch => write!(f, "IMCSCH"),
|
||||
Function::Imdiv => write!(f, "IMDIV"),
|
||||
Function::Imexp => write!(f, "IMEXP"),
|
||||
Function::Imln => write!(f, "IMLN"),
|
||||
Function::Imlog10 => write!(f, "IMLOG10"),
|
||||
Function::Imlog2 => write!(f, "IMLOG2"),
|
||||
Function::Impower => write!(f, "IMPOWER"),
|
||||
Function::Improduct => write!(f, "IMPRODUCT"),
|
||||
Function::Imreal => write!(f, "IMREAL"),
|
||||
Function::Imsec => write!(f, "IMSEC"),
|
||||
Function::Imsech => write!(f, "IMSECH"),
|
||||
Function::Imsin => write!(f, "IMSIN"),
|
||||
Function::Imsinh => write!(f, "IMSINH"),
|
||||
Function::Imsqrt => write!(f, "IMSQRT"),
|
||||
Function::Imsub => write!(f, "IMSUB"),
|
||||
Function::Imsum => write!(f, "IMSUM"),
|
||||
Function::Imtan => write!(f, "IMTAN"),
|
||||
Function::Convert => write!(f, "CONVERT"),
|
||||
Function::Delta => write!(f, "DELTA"),
|
||||
Function::Gestep => write!(f, "GESTEP"),
|
||||
|
||||
Function::Subtotal => write!(f, "SUBTOTAL"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn evaluate_function(
|
||||
&mut self,
|
||||
kind: &Function,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
) -> CalcResult {
|
||||
match kind {
|
||||
// Logical
|
||||
Function::And => self.fn_and(args, cell),
|
||||
Function::False => CalcResult::Boolean(false),
|
||||
Function::If => self.fn_if(args, cell),
|
||||
Function::Iferror => self.fn_iferror(args, cell),
|
||||
Function::Ifna => self.fn_ifna(args, cell),
|
||||
Function::Ifs => self.fn_ifs(args, cell),
|
||||
Function::Not => self.fn_not(args, cell),
|
||||
Function::Or => self.fn_or(args, cell),
|
||||
Function::Switch => self.fn_switch(args, cell),
|
||||
Function::True => CalcResult::Boolean(true),
|
||||
Function::Xor => self.fn_xor(args, cell),
|
||||
// Math and trigonometry
|
||||
Function::Sin => self.fn_sin(args, cell),
|
||||
Function::Cos => self.fn_cos(args, cell),
|
||||
Function::Tan => self.fn_tan(args, cell),
|
||||
|
||||
Function::Asin => self.fn_asin(args, cell),
|
||||
Function::Acos => self.fn_acos(args, cell),
|
||||
Function::Atan => self.fn_atan(args, cell),
|
||||
|
||||
Function::Sinh => self.fn_sinh(args, cell),
|
||||
Function::Cosh => self.fn_cosh(args, cell),
|
||||
Function::Tanh => self.fn_tanh(args, cell),
|
||||
|
||||
Function::Asinh => self.fn_asinh(args, cell),
|
||||
Function::Acosh => self.fn_acosh(args, cell),
|
||||
Function::Atanh => self.fn_atanh(args, cell),
|
||||
|
||||
Function::Pi => self.fn_pi(args, cell),
|
||||
Function::Abs => self.fn_abs(args, cell),
|
||||
|
||||
Function::Sqrt => self.fn_sqrt(args, cell),
|
||||
Function::Sqrtpi => self.fn_sqrtpi(args, cell),
|
||||
Function::Atan2 => self.fn_atan2(args, cell),
|
||||
Function::Power => self.fn_power(args, cell),
|
||||
|
||||
Function::Max => self.fn_max(args, cell),
|
||||
Function::Min => self.fn_min(args, cell),
|
||||
Function::Product => self.fn_product(args, cell),
|
||||
Function::Rand => self.fn_rand(args, cell),
|
||||
Function::Randbetween => self.fn_randbetween(args, cell),
|
||||
Function::Round => self.fn_round(args, cell),
|
||||
Function::Rounddown => self.fn_rounddown(args, cell),
|
||||
Function::Roundup => self.fn_roundup(args, cell),
|
||||
Function::Sum => self.fn_sum(args, cell),
|
||||
Function::Sumif => self.fn_sumif(args, cell),
|
||||
Function::Sumifs => self.fn_sumifs(args, cell),
|
||||
|
||||
// Lookup and Reference
|
||||
Function::Choose => self.fn_choose(args, cell),
|
||||
Function::Column => self.fn_column(args, cell),
|
||||
Function::Columns => self.fn_columns(args, cell),
|
||||
Function::Index => self.fn_index(args, cell),
|
||||
Function::Indirect => self.fn_indirect(args, cell),
|
||||
Function::Hlookup => self.fn_hlookup(args, cell),
|
||||
Function::Lookup => self.fn_lookup(args, cell),
|
||||
Function::Match => self.fn_match(args, cell),
|
||||
Function::Offset => self.fn_offset(args, cell),
|
||||
Function::Row => self.fn_row(args, cell),
|
||||
Function::Rows => self.fn_rows(args, cell),
|
||||
Function::Vlookup => self.fn_vlookup(args, cell),
|
||||
Function::Xlookup => self.fn_xlookup(args, cell),
|
||||
// Text
|
||||
Function::Concatenate => self.fn_concatenate(args, cell),
|
||||
Function::Exact => self.fn_exact(args, cell),
|
||||
Function::Value => self.fn_value(args, cell),
|
||||
Function::T => self.fn_t(args, cell),
|
||||
Function::Valuetotext => self.fn_valuetotext(args, cell),
|
||||
Function::Concat => self.fn_concat(args, cell),
|
||||
Function::Find => self.fn_find(args, cell),
|
||||
Function::Left => self.fn_left(args, cell),
|
||||
Function::Len => self.fn_len(args, cell),
|
||||
Function::Lower => self.fn_lower(args, cell),
|
||||
Function::Mid => self.fn_mid(args, cell),
|
||||
Function::Right => self.fn_right(args, cell),
|
||||
Function::Search => self.fn_search(args, cell),
|
||||
Function::Text => self.fn_text(args, cell),
|
||||
Function::Trim => self.fn_trim(args, cell),
|
||||
Function::Upper => self.fn_upper(args, cell),
|
||||
// Information
|
||||
Function::Isnumber => self.fn_isnumber(args, cell),
|
||||
Function::Isnontext => self.fn_isnontext(args, cell),
|
||||
Function::Istext => self.fn_istext(args, cell),
|
||||
Function::Islogical => self.fn_islogical(args, cell),
|
||||
Function::Isblank => self.fn_isblank(args, cell),
|
||||
Function::Iserr => self.fn_iserr(args, cell),
|
||||
Function::Iserror => self.fn_iserror(args, cell),
|
||||
Function::Isna => self.fn_isna(args, cell),
|
||||
Function::Na => CalcResult::new_error(Error::NA, cell, "".to_string()),
|
||||
Function::Isref => self.fn_isref(args, cell),
|
||||
Function::Isodd => self.fn_isodd(args, cell),
|
||||
Function::Iseven => self.fn_iseven(args, cell),
|
||||
Function::ErrorType => self.fn_errortype(args, cell),
|
||||
Function::Isformula => self.fn_isformula(args, cell),
|
||||
Function::Type => self.fn_type(args, cell),
|
||||
Function::Sheet => self.fn_sheet(args, cell),
|
||||
// Statistical
|
||||
Function::Average => self.fn_average(args, cell),
|
||||
Function::Averagea => self.fn_averagea(args, cell),
|
||||
Function::Averageif => self.fn_averageif(args, cell),
|
||||
Function::Averageifs => self.fn_averageifs(args, cell),
|
||||
Function::Count => self.fn_count(args, cell),
|
||||
Function::Counta => self.fn_counta(args, cell),
|
||||
Function::Countblank => self.fn_countblank(args, cell),
|
||||
Function::Countif => self.fn_countif(args, cell),
|
||||
Function::Countifs => self.fn_countifs(args, cell),
|
||||
Function::Maxifs => self.fn_maxifs(args, cell),
|
||||
Function::Minifs => self.fn_minifs(args, cell),
|
||||
// Date and Time
|
||||
Function::Year => self.fn_year(args, cell),
|
||||
Function::Day => self.fn_day(args, cell),
|
||||
Function::Eomonth => self.fn_eomonth(args, cell),
|
||||
Function::Month => self.fn_month(args, cell),
|
||||
Function::Date => self.fn_date(args, cell),
|
||||
Function::Edate => self.fn_edate(args, cell),
|
||||
Function::Today => self.fn_today(args, cell),
|
||||
Function::Now => self.fn_now(args, cell),
|
||||
// Financial
|
||||
Function::Pmt => self.fn_pmt(args, cell),
|
||||
Function::Pv => self.fn_pv(args, cell),
|
||||
Function::Rate => self.fn_rate(args, cell),
|
||||
Function::Nper => self.fn_nper(args, cell),
|
||||
Function::Fv => self.fn_fv(args, cell),
|
||||
Function::Ppmt => self.fn_ppmt(args, cell),
|
||||
Function::Ipmt => self.fn_ipmt(args, cell),
|
||||
Function::Npv => self.fn_npv(args, cell),
|
||||
Function::Mirr => self.fn_mirr(args, cell),
|
||||
Function::Irr => self.fn_irr(args, cell),
|
||||
Function::Xirr => self.fn_xirr(args, cell),
|
||||
Function::Xnpv => self.fn_xnpv(args, cell),
|
||||
Function::Rept => self.fn_rept(args, cell),
|
||||
Function::Textafter => self.fn_textafter(args, cell),
|
||||
Function::Textbefore => self.fn_textbefore(args, cell),
|
||||
Function::Textjoin => self.fn_textjoin(args, cell),
|
||||
Function::Substitute => self.fn_substitute(args, cell),
|
||||
Function::Ispmt => self.fn_ispmt(args, cell),
|
||||
Function::Rri => self.fn_rri(args, cell),
|
||||
Function::Sln => self.fn_sln(args, cell),
|
||||
Function::Syd => self.fn_syd(args, cell),
|
||||
Function::Nominal => self.fn_nominal(args, cell),
|
||||
Function::Effect => self.fn_effect(args, cell),
|
||||
Function::Pduration => self.fn_pduration(args, cell),
|
||||
Function::Tbillyield => self.fn_tbillyield(args, cell),
|
||||
Function::Tbillprice => self.fn_tbillprice(args, cell),
|
||||
Function::Tbilleq => self.fn_tbilleq(args, cell),
|
||||
Function::Dollarde => self.fn_dollarde(args, cell),
|
||||
Function::Dollarfr => self.fn_dollarfr(args, cell),
|
||||
Function::Ddb => self.fn_ddb(args, cell),
|
||||
Function::Db => self.fn_db(args, cell),
|
||||
Function::Cumprinc => self.fn_cumprinc(args, cell),
|
||||
Function::Cumipmt => self.fn_cumipmt(args, cell),
|
||||
// Engineering
|
||||
Function::Besseli => self.fn_besseli(args, cell),
|
||||
Function::Besselj => self.fn_besselj(args, cell),
|
||||
Function::Besselk => self.fn_besselk(args, cell),
|
||||
Function::Bessely => self.fn_bessely(args, cell),
|
||||
Function::Erf => self.fn_erf(args, cell),
|
||||
Function::ErfPrecise => self.fn_erfprecise(args, cell),
|
||||
Function::Erfc => self.fn_erfc(args, cell),
|
||||
Function::ErfcPrecise => self.fn_erfcprecise(args, cell),
|
||||
Function::Bin2dec => self.fn_bin2dec(args, cell),
|
||||
Function::Bin2hex => self.fn_bin2hex(args, cell),
|
||||
Function::Bin2oct => self.fn_bin2oct(args, cell),
|
||||
Function::Dec2Bin => self.fn_dec2bin(args, cell),
|
||||
Function::Dec2hex => self.fn_dec2hex(args, cell),
|
||||
Function::Dec2oct => self.fn_dec2oct(args, cell),
|
||||
Function::Hex2bin => self.fn_hex2bin(args, cell),
|
||||
Function::Hex2dec => self.fn_hex2dec(args, cell),
|
||||
Function::Hex2oct => self.fn_hex2oct(args, cell),
|
||||
Function::Oct2bin => self.fn_oct2bin(args, cell),
|
||||
Function::Oct2dec => self.fn_oct2dec(args, cell),
|
||||
Function::Oct2hex => self.fn_oct2hex(args, cell),
|
||||
Function::Bitand => self.fn_bitand(args, cell),
|
||||
Function::Bitlshift => self.fn_bitlshift(args, cell),
|
||||
Function::Bitor => self.fn_bitor(args, cell),
|
||||
Function::Bitrshift => self.fn_bitrshift(args, cell),
|
||||
Function::Bitxor => self.fn_bitxor(args, cell),
|
||||
Function::Complex => self.fn_complex(args, cell),
|
||||
Function::Imabs => self.fn_imabs(args, cell),
|
||||
Function::Imaginary => self.fn_imaginary(args, cell),
|
||||
Function::Imargument => self.fn_imargument(args, cell),
|
||||
Function::Imconjugate => self.fn_imconjugate(args, cell),
|
||||
Function::Imcos => self.fn_imcos(args, cell),
|
||||
Function::Imcosh => self.fn_imcosh(args, cell),
|
||||
Function::Imcot => self.fn_imcot(args, cell),
|
||||
Function::Imcsc => self.fn_imcsc(args, cell),
|
||||
Function::Imcsch => self.fn_imcsch(args, cell),
|
||||
Function::Imdiv => self.fn_imdiv(args, cell),
|
||||
Function::Imexp => self.fn_imexp(args, cell),
|
||||
Function::Imln => self.fn_imln(args, cell),
|
||||
Function::Imlog10 => self.fn_imlog10(args, cell),
|
||||
Function::Imlog2 => self.fn_imlog2(args, cell),
|
||||
Function::Impower => self.fn_impower(args, cell),
|
||||
Function::Improduct => self.fn_improduct(args, cell),
|
||||
Function::Imreal => self.fn_imreal(args, cell),
|
||||
Function::Imsec => self.fn_imsec(args, cell),
|
||||
Function::Imsech => self.fn_imsech(args, cell),
|
||||
Function::Imsin => self.fn_imsin(args, cell),
|
||||
Function::Imsinh => self.fn_imsinh(args, cell),
|
||||
Function::Imsqrt => self.fn_imsqrt(args, cell),
|
||||
Function::Imsub => self.fn_imsub(args, cell),
|
||||
Function::Imsum => self.fn_imsum(args, cell),
|
||||
Function::Imtan => self.fn_imtan(args, cell),
|
||||
Function::Convert => self.fn_convert(args, cell),
|
||||
Function::Delta => self.fn_delta(args, cell),
|
||||
Function::Gestep => self.fn_gestep(args, cell),
|
||||
|
||||
Function::Subtotal => self.fn_subtotal(args, cell),
|
||||
}
|
||||
}
|
||||
}
|
||||
624
base/src/functions/statistical.rs
Normal file
624
base/src/functions/statistical.rs
Normal file
@@ -0,0 +1,624 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference, Range},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
use super::util::build_criteria;
|
||||
|
||||
impl Model {
|
||||
pub(crate) fn fn_average(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut count = 0.0;
|
||||
let mut sum = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
} else {
|
||||
sum += if b { 1.0 } else { 0.0 };
|
||||
count += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Unexpected Range".to_string(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::String(s) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
// Do nothing
|
||||
} else if let Ok(t) = s.parse::<f64>() {
|
||||
sum += t;
|
||||
count += 1.0;
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument cannot be cast into number".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore everything else
|
||||
}
|
||||
};
|
||||
}
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by Zero".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(sum / count)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_averagea(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut count = 0.0;
|
||||
let mut sum = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::String(_) => count += 1.0,
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
if b {
|
||||
sum += 1.0;
|
||||
}
|
||||
count += 1.0;
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::Range { .. } => {
|
||||
return CalcResult::new_error(
|
||||
Error::ERROR,
|
||||
cell,
|
||||
"Unexpected Range".to_string(),
|
||||
);
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Number(value) => {
|
||||
count += 1.0;
|
||||
sum += value;
|
||||
}
|
||||
CalcResult::String(s) => {
|
||||
if let Node::ReferenceKind { .. } = arg {
|
||||
// Do nothing
|
||||
count += 1.0;
|
||||
} else if let Ok(t) = s.parse::<f64>() {
|
||||
sum += t;
|
||||
count += 1.0;
|
||||
} else {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Argument cannot be cast into number".to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
CalcResult::Boolean(b) => {
|
||||
count += 1.0;
|
||||
if b {
|
||||
sum += 1.0;
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return error,
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
};
|
||||
}
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by Zero".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(sum / count)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_count(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut result = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::Number(_) => {
|
||||
result += 1.0;
|
||||
}
|
||||
CalcResult::Boolean(_) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) {
|
||||
result += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::String(s) => {
|
||||
if !matches!(arg, Node::ReferenceKind { .. }) && s.parse::<f64>().is_ok() {
|
||||
result += 1.0;
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
if let CalcResult::Number(_) = self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
result += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Ignore everything else
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_counta(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.is_empty() {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut result = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {}
|
||||
_ => {
|
||||
result += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
result += 1.0;
|
||||
}
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_countblank(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
// COUNTBLANK requires only one argument
|
||||
if args.len() != 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let mut result = 0.0;
|
||||
for arg in args {
|
||||
match self.evaluate_node_in_context(arg, cell) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => result += 1.0,
|
||||
CalcResult::String(s) => {
|
||||
if s.is_empty() {
|
||||
result += 1.0
|
||||
}
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
for row in left.row..(right.row + 1) {
|
||||
for column in left.column..(right.column + 1) {
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => result += 1.0,
|
||||
CalcResult::String(s) => {
|
||||
if s.is_empty() {
|
||||
result += 1.0
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_countif(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 2 {
|
||||
let arguments = vec![args[0].clone(), args[1].clone()];
|
||||
self.fn_countifs(&arguments, cell)
|
||||
} else {
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
}
|
||||
|
||||
/// AVERAGEIF(criteria_range, criteria, [average_range])
|
||||
/// if average_rage is missing then criteria_range will be used
|
||||
pub(crate) fn fn_averageif(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() == 2 {
|
||||
let arguments = vec![args[0].clone(), args[0].clone(), args[1].clone()];
|
||||
self.fn_averageifs(&arguments, cell)
|
||||
} else if args.len() == 3 {
|
||||
let arguments = vec![args[2].clone(), args[0].clone(), args[1].clone()];
|
||||
self.fn_averageifs(&arguments, cell)
|
||||
} else {
|
||||
CalcResult::new_args_number_error(cell)
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This function shares a lot of code with apply_ifs. Can we merge them?
|
||||
pub(crate) fn fn_countifs(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let args_count = args.len();
|
||||
if args_count < 2 || args_count % 2 == 1 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
|
||||
let case_count = args_count / 2;
|
||||
// NB: this is a beautiful example of the borrow checker
|
||||
// The order of these two definitions cannot be swapped.
|
||||
let mut criteria = Vec::new();
|
||||
let mut fn_criteria = Vec::new();
|
||||
let ranges = &mut Vec::new();
|
||||
for case_index in 0..case_count {
|
||||
let criterion = self.evaluate_node_in_context(&args[case_index * 2 + 1], cell);
|
||||
criteria.push(criterion);
|
||||
// NB: We cannot do:
|
||||
// fn_criteria.push(build_criteria(&criterion));
|
||||
// because criterion doesn't live long enough
|
||||
let result = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
||||
if result.is_error() {
|
||||
return result;
|
||||
}
|
||||
if let CalcResult::Range { left, right } = result {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
// TODO test ranges are of the same size as sum_range
|
||||
ranges.push(Range { left, right });
|
||||
} else {
|
||||
return CalcResult::new_error(Error::VALUE, cell, "Expected a range".to_string());
|
||||
}
|
||||
}
|
||||
for criterion in criteria.iter() {
|
||||
fn_criteria.push(build_criteria(criterion));
|
||||
}
|
||||
|
||||
let mut total = 0.0;
|
||||
let first_range = &ranges[0];
|
||||
let left_row = first_range.left.row;
|
||||
let left_column = first_range.left.column;
|
||||
let right_row = first_range.right.row;
|
||||
let right_column = first_range.right.column;
|
||||
|
||||
let dimension = self
|
||||
.workbook
|
||||
.worksheet(first_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension();
|
||||
let max_row = dimension.max_row;
|
||||
let max_column = dimension.max_column;
|
||||
|
||||
let open_row = left_row == 1 && right_row == LAST_ROW;
|
||||
let open_column = left_column == 1 && right_column == LAST_COLUMN;
|
||||
|
||||
for row in left_row..right_row + 1 {
|
||||
if open_row && row > max_row {
|
||||
// If the row is larger than the max row in the sheet then all cells are empty.
|
||||
// We compute it only once
|
||||
let mut is_true = true;
|
||||
for fn_criterion in fn_criteria.iter() {
|
||||
if !fn_criterion(&CalcResult::EmptyCell) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
total += ((LAST_ROW - max_row) * (right_column - left_column + 1)) as f64;
|
||||
}
|
||||
break;
|
||||
}
|
||||
for column in left_column..right_column + 1 {
|
||||
if open_column && column > max_column {
|
||||
// If the column is larger than the max column in the sheet then all cells are empty.
|
||||
// We compute it only once
|
||||
let mut is_true = true;
|
||||
for fn_criterion in fn_criteria.iter() {
|
||||
if !fn_criterion(&CalcResult::EmptyCell) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
total += (LAST_COLUMN - max_column) as f64;
|
||||
}
|
||||
break;
|
||||
}
|
||||
let mut is_true = true;
|
||||
for case_index in 0..case_count {
|
||||
// We check if value in range n meets criterion n
|
||||
let range = &ranges[case_index];
|
||||
let fn_criterion = &fn_criteria[case_index];
|
||||
let value = self.evaluate_cell(CellReference {
|
||||
sheet: range.left.sheet,
|
||||
row: range.left.row + row - first_range.left.row,
|
||||
column: range.left.column + column - first_range.left.column,
|
||||
});
|
||||
if !fn_criterion(&value) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
total += 1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Number(total)
|
||||
}
|
||||
|
||||
pub(crate) fn apply_ifs<F>(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mut apply: F,
|
||||
) -> Result<(), CalcResult>
|
||||
where
|
||||
F: FnMut(f64),
|
||||
{
|
||||
let args_count = args.len();
|
||||
if args_count < 3 || args_count % 2 == 0 {
|
||||
return Err(CalcResult::new_args_number_error(cell));
|
||||
}
|
||||
let arg_0 = self.evaluate_node_in_context(&args[0], cell);
|
||||
if arg_0.is_error() {
|
||||
return Err(arg_0);
|
||||
}
|
||||
let sum_range = if let CalcResult::Range { left, right } = arg_0 {
|
||||
if left.sheet != right.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
Range { left, right }
|
||||
} else {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expected a range".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let case_count = (args_count - 1) / 2;
|
||||
// NB: this is a beautiful example of the borrow checker
|
||||
// The order of these two definitions cannot be swapped.
|
||||
let mut criteria = Vec::new();
|
||||
let mut fn_criteria = Vec::new();
|
||||
let ranges = &mut Vec::new();
|
||||
for case_index in 1..=case_count {
|
||||
let criterion = self.evaluate_node_in_context(&args[case_index * 2], cell);
|
||||
// NB: criterion might be an error. That's ok
|
||||
criteria.push(criterion);
|
||||
// NB: We cannot do:
|
||||
// fn_criteria.push(build_criteria(&criterion));
|
||||
// because criterion doesn't live long enough
|
||||
let result = self.evaluate_node_in_context(&args[case_index * 2 - 1], cell);
|
||||
if result.is_error() {
|
||||
return Err(result);
|
||||
}
|
||||
if let CalcResult::Range { left, right } = result {
|
||||
if left.sheet != right.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
// TODO test ranges are of the same size as sum_range
|
||||
ranges.push(Range { left, right });
|
||||
} else {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Expected a range".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
for criterion in criteria.iter() {
|
||||
fn_criteria.push(build_criteria(criterion));
|
||||
}
|
||||
|
||||
let left_row = sum_range.left.row;
|
||||
let left_column = sum_range.left.column;
|
||||
let mut right_row = sum_range.right.row;
|
||||
let mut right_column = sum_range.right.column;
|
||||
|
||||
if left_row == 1 && right_row == LAST_ROW {
|
||||
right_row = self
|
||||
.workbook
|
||||
.worksheet(sum_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if left_column == 1 && right_column == LAST_COLUMN {
|
||||
right_column = self
|
||||
.workbook
|
||||
.worksheet(sum_range.left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
|
||||
for row in left_row..right_row + 1 {
|
||||
for column in left_column..right_column + 1 {
|
||||
let mut is_true = true;
|
||||
for case_index in 0..case_count {
|
||||
// We check if value in range n meets criterion n
|
||||
let range = &ranges[case_index];
|
||||
let fn_criterion = &fn_criteria[case_index];
|
||||
let value = self.evaluate_cell(CellReference {
|
||||
sheet: range.left.sheet,
|
||||
row: range.left.row + row - sum_range.left.row,
|
||||
column: range.left.column + column - sum_range.left.column,
|
||||
});
|
||||
if !fn_criterion(&value) {
|
||||
is_true = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if is_true {
|
||||
let v = self.evaluate_cell(CellReference {
|
||||
sheet: sum_range.left.sheet,
|
||||
row,
|
||||
column,
|
||||
});
|
||||
match v {
|
||||
CalcResult::Number(n) => apply(n),
|
||||
CalcResult::Error { .. } => return Err(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn fn_averageifs(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let mut total = 0.0;
|
||||
let mut count = 0.0;
|
||||
|
||||
let average = |value: f64| {
|
||||
total += value;
|
||||
count += 1.0;
|
||||
};
|
||||
if let Err(e) = self.apply_ifs(args, cell, average) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if count == 0.0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "division by 0".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(total / count)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_minifs(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let mut min = f64::INFINITY;
|
||||
let apply_min = |value: f64| min = value.min(min);
|
||||
if let Err(e) = self.apply_ifs(args, cell, apply_min) {
|
||||
return e;
|
||||
}
|
||||
|
||||
if min.is_infinite() {
|
||||
min = 0.0;
|
||||
}
|
||||
CalcResult::Number(min)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_maxifs(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
let mut max = -f64::INFINITY;
|
||||
let apply_max = |value: f64| max = value.max(max);
|
||||
if let Err(e) = self.apply_ifs(args, cell, apply_max) {
|
||||
return e;
|
||||
}
|
||||
if max.is_infinite() {
|
||||
max = 0.0;
|
||||
}
|
||||
CalcResult::Number(max)
|
||||
}
|
||||
}
|
||||
584
base/src/functions/subtotal.rs
Normal file
584
base/src/functions/subtotal.rs
Normal file
@@ -0,0 +1,584 @@
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::{
|
||||
parser::{parse_range, Node},
|
||||
token::Error,
|
||||
},
|
||||
functions::Function,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
/// Excel has a complicated way of filtering + hidden rows
|
||||
/// As a first a approximation a table can either have filtered rows or hidden rows, but not both.
|
||||
/// Internally hey both will be marked as hidden rows. Hidden rows
|
||||
/// The behaviour is the same for SUBTOTAL(100s,) => ignore those
|
||||
/// But changes for the SUBTOTAL(1-11, ) those ignore filtered but take hidden into account.
|
||||
/// In Excel filters are non-dynamic. Once you apply filters in a table (say value in column 1 should > 20) they
|
||||
/// stay that way, even if you change the value of the values in the table after the fact.
|
||||
/// If you try to hide rows in a table with filtered rows they will behave as if filtered
|
||||
/// // Also subtotals ignore subtotals
|
||||
///
|
||||
#[derive(PartialEq)]
|
||||
enum SubTotalMode {
|
||||
Full,
|
||||
SkipHidden,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum CellTableStatus {
|
||||
Normal,
|
||||
Hidden,
|
||||
Filtered,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
fn get_table_for_cell(&self, sheet_index: u32, row: i32, column: i32) -> bool {
|
||||
let worksheet = match self.workbook.worksheet(sheet_index) {
|
||||
Ok(ws) => ws,
|
||||
Err(_) => return false,
|
||||
};
|
||||
for table in self.workbook.tables.values() {
|
||||
if worksheet.name != table.sheet_name {
|
||||
continue;
|
||||
}
|
||||
// (column, row, column, row)
|
||||
if let Ok((column1, row1, column2, row2)) = parse_range(&table.reference) {
|
||||
if ((column >= column1) && (column <= column2)) && ((row >= row1) && (row <= row2))
|
||||
{
|
||||
return table.has_filters;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn cell_hidden_status(&self, sheet_index: u32, row: i32, column: i32) -> CellTableStatus {
|
||||
let worksheet = self.workbook.worksheet(sheet_index).expect("");
|
||||
let mut hidden = false;
|
||||
for row_style in &worksheet.rows {
|
||||
if row_style.r == row {
|
||||
hidden = row_style.hidden;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !hidden {
|
||||
return CellTableStatus::Normal;
|
||||
}
|
||||
// The row is hidden we need to know if the table has filters
|
||||
if self.get_table_for_cell(sheet_index, row, column) {
|
||||
CellTableStatus::Filtered
|
||||
} else {
|
||||
CellTableStatus::Hidden
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME(TD): This is too much
|
||||
fn cell_is_subtotal(&self, sheet_index: u32, row: i32, column: i32) -> bool {
|
||||
let row_data = match self.workbook.worksheets[sheet_index as usize]
|
||||
.sheet_data
|
||||
.get(&row)
|
||||
{
|
||||
Some(r) => r,
|
||||
None => return false,
|
||||
};
|
||||
let cell = match row_data.get(&column) {
|
||||
Some(c) => c,
|
||||
None => {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
match cell.get_formula() {
|
||||
Some(f) => {
|
||||
let node = &self.parsed_formulas[sheet_index as usize][f as usize];
|
||||
matches!(
|
||||
node,
|
||||
Node::FunctionKind {
|
||||
kind: Function::Subtotal,
|
||||
args: _
|
||||
}
|
||||
)
|
||||
}
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn subtotal_get_values(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> Result<Vec<f64>, CalcResult> {
|
||||
let mut result: Vec<f64> = Vec::new();
|
||||
for arg in args {
|
||||
match arg {
|
||||
Node::FunctionKind {
|
||||
kind: Function::Subtotal,
|
||||
args: _,
|
||||
} => {
|
||||
// skip
|
||||
}
|
||||
_ => {
|
||||
match self.evaluate_node_with_reference(arg, cell) {
|
||||
CalcResult::String(_) | CalcResult::Boolean(_) => {
|
||||
// Skip
|
||||
}
|
||||
CalcResult::Number(f) => result.push(f),
|
||||
error @ CalcResult::Error { .. } => {
|
||||
return Err(error);
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return Err(CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
));
|
||||
}
|
||||
// We are not expecting subtotal to have open ranges
|
||||
let row1 = left.row;
|
||||
let row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let column2 = right.column;
|
||||
|
||||
for row in row1..=row2 {
|
||||
let cell_status = self.cell_hidden_status(left.sheet, row, column1);
|
||||
if cell_status == CellTableStatus::Filtered {
|
||||
continue;
|
||||
}
|
||||
if mode == SubTotalMode::SkipHidden
|
||||
&& cell_status == CellTableStatus::Hidden
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for column in column1..=column2 {
|
||||
if self.cell_is_subtotal(left.sheet, row, column) {
|
||||
continue;
|
||||
}
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::Number(value) => {
|
||||
result.push(value);
|
||||
}
|
||||
error @ CalcResult::Error { .. } => return Err(error),
|
||||
_ => {
|
||||
// We ignore booleans and strings
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => result.push(0.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) fn fn_subtotal(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() < 2 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let value = match self.get_number(&args[0], cell) {
|
||||
Ok(f) => f.trunc() as i32,
|
||||
Err(s) => return s,
|
||||
};
|
||||
match value {
|
||||
1 => self.subtotal_average(&args[1..], cell, SubTotalMode::Full),
|
||||
2 => self.subtotal_count(&args[1..], cell, SubTotalMode::Full),
|
||||
3 => self.subtotal_counta(&args[1..], cell, SubTotalMode::Full),
|
||||
4 => self.subtotal_max(&args[1..], cell, SubTotalMode::Full),
|
||||
5 => self.subtotal_min(&args[1..], cell, SubTotalMode::Full),
|
||||
6 => self.subtotal_product(&args[1..], cell, SubTotalMode::Full),
|
||||
7 => self.subtotal_stdevs(&args[1..], cell, SubTotalMode::Full),
|
||||
8 => self.subtotal_stdevp(&args[1..], cell, SubTotalMode::Full),
|
||||
9 => self.subtotal_sum(&args[1..], cell, SubTotalMode::Full),
|
||||
10 => self.subtotal_vars(&args[1..], cell, SubTotalMode::Full),
|
||||
11 => self.subtotal_varp(&args[1..], cell, SubTotalMode::Full),
|
||||
101 => self.subtotal_average(&args[1..], cell, SubTotalMode::SkipHidden),
|
||||
102 => self.subtotal_count(&args[1..], cell, SubTotalMode::SkipHidden),
|
||||
103 => self.subtotal_counta(&args[1..], cell, SubTotalMode::SkipHidden),
|
||||
104 => self.subtotal_max(&args[1..], cell, SubTotalMode::SkipHidden),
|
||||
105 => self.subtotal_min(&args[1..], cell, SubTotalMode::SkipHidden),
|
||||
106 => self.subtotal_product(&args[1..], cell, SubTotalMode::SkipHidden),
|
||||
107 => self.subtotal_stdevs(&args[1..], cell, SubTotalMode::SkipHidden),
|
||||
108 => self.subtotal_stdevp(&args[1..], cell, SubTotalMode::SkipHidden),
|
||||
109 => self.subtotal_sum(&args[1..], cell, SubTotalMode::SkipHidden),
|
||||
110 => self.subtotal_vars(&args[1..], cell, SubTotalMode::Full),
|
||||
111 => self.subtotal_varp(&args[1..], cell, SubTotalMode::Full),
|
||||
_ => CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
format!("Invalid value for SUBTOTAL: {value}"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn subtotal_vars(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let values = match self.subtotal_get_values(args, cell, mode) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut result = 0.0;
|
||||
let l = values.len();
|
||||
for value in &values {
|
||||
result += value;
|
||||
}
|
||||
if l < 2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by 0!".to_string(),
|
||||
};
|
||||
}
|
||||
// average
|
||||
let average = result / (l as f64);
|
||||
let mut result = 0.0;
|
||||
for value in &values {
|
||||
result += (value - average).powi(2) / (l as f64 - 1.0)
|
||||
}
|
||||
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
fn subtotal_varp(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let values = match self.subtotal_get_values(args, cell, mode) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut result = 0.0;
|
||||
let l = values.len();
|
||||
for value in &values {
|
||||
result += value;
|
||||
}
|
||||
if l == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by 0!".to_string(),
|
||||
};
|
||||
}
|
||||
// average
|
||||
let average = result / (l as f64);
|
||||
let mut result = 0.0;
|
||||
for value in &values {
|
||||
result += (value - average).powi(2) / (l as f64)
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
fn subtotal_stdevs(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let values = match self.subtotal_get_values(args, cell, mode) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut result = 0.0;
|
||||
let l = values.len();
|
||||
for value in &values {
|
||||
result += value;
|
||||
}
|
||||
if l < 2 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by 0!".to_string(),
|
||||
};
|
||||
}
|
||||
// average
|
||||
let average = result / (l as f64);
|
||||
let mut result = 0.0;
|
||||
for value in &values {
|
||||
result += (value - average).powi(2) / (l as f64 - 1.0)
|
||||
}
|
||||
|
||||
CalcResult::Number(result.sqrt())
|
||||
}
|
||||
|
||||
fn subtotal_stdevp(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let values = match self.subtotal_get_values(args, cell, mode) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut result = 0.0;
|
||||
let l = values.len();
|
||||
for value in &values {
|
||||
result += value;
|
||||
}
|
||||
if l == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by 0!".to_string(),
|
||||
};
|
||||
}
|
||||
// average
|
||||
let average = result / (l as f64);
|
||||
let mut result = 0.0;
|
||||
for value in &values {
|
||||
result += (value - average).powi(2) / (l as f64)
|
||||
}
|
||||
CalcResult::Number(result.sqrt())
|
||||
}
|
||||
|
||||
fn subtotal_counta(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let mut counta = 0;
|
||||
for arg in args {
|
||||
match arg {
|
||||
Node::FunctionKind {
|
||||
kind: Function::Subtotal,
|
||||
args: _,
|
||||
} => {
|
||||
// skip
|
||||
}
|
||||
_ => {
|
||||
match self.evaluate_node_with_reference(arg, cell) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {
|
||||
// skip
|
||||
}
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
// We are not expecting subtotal to have open ranges
|
||||
let row1 = left.row;
|
||||
let row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let column2 = right.column;
|
||||
|
||||
for row in row1..=row2 {
|
||||
let cell_status = self.cell_hidden_status(left.sheet, row, column1);
|
||||
if cell_status == CellTableStatus::Filtered {
|
||||
continue;
|
||||
}
|
||||
if mode == SubTotalMode::SkipHidden
|
||||
&& cell_status == CellTableStatus::Hidden
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for column in column1..=column2 {
|
||||
if self.cell_is_subtotal(left.sheet, row, column) {
|
||||
continue;
|
||||
}
|
||||
match self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
}) {
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => {
|
||||
// skip
|
||||
}
|
||||
_ => counta += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::String(_)
|
||||
| CalcResult::Number(_)
|
||||
| CalcResult::Boolean(_)
|
||||
| CalcResult::Error { .. } => counta += 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Number(counta as f64)
|
||||
}
|
||||
|
||||
fn subtotal_count(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let mut count = 0;
|
||||
for arg in args {
|
||||
match arg {
|
||||
Node::FunctionKind {
|
||||
kind: Function::Subtotal,
|
||||
args: _,
|
||||
} => {
|
||||
// skip
|
||||
}
|
||||
_ => {
|
||||
match self.evaluate_node_with_reference(arg, cell) {
|
||||
CalcResult::Range { left, right } => {
|
||||
if left.sheet != right.sheet {
|
||||
return CalcResult::new_error(
|
||||
Error::VALUE,
|
||||
cell,
|
||||
"Ranges are in different sheets".to_string(),
|
||||
);
|
||||
}
|
||||
// We are not expecting subtotal to have open ranges
|
||||
let row1 = left.row;
|
||||
let row2 = right.row;
|
||||
let column1 = left.column;
|
||||
let column2 = right.column;
|
||||
|
||||
for row in row1..=row2 {
|
||||
let cell_status = self.cell_hidden_status(left.sheet, row, column1);
|
||||
if cell_status == CellTableStatus::Filtered {
|
||||
continue;
|
||||
}
|
||||
if mode == SubTotalMode::SkipHidden
|
||||
&& cell_status == CellTableStatus::Hidden
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for column in column1..=column2 {
|
||||
if self.cell_is_subtotal(left.sheet, row, column) {
|
||||
continue;
|
||||
}
|
||||
if let CalcResult::Number(_) =
|
||||
self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row,
|
||||
column,
|
||||
})
|
||||
{
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// This hasn't been tested
|
||||
CalcResult::Number(_) => count += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Number(count as f64)
|
||||
}
|
||||
|
||||
fn subtotal_average(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let values = match self.subtotal_get_values(args, cell, mode) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut result = 0.0;
|
||||
let l = values.len();
|
||||
for value in values {
|
||||
result += value;
|
||||
}
|
||||
if l == 0 {
|
||||
return CalcResult::Error {
|
||||
error: Error::DIV,
|
||||
origin: cell,
|
||||
message: "Division by 0!".to_string(),
|
||||
};
|
||||
}
|
||||
CalcResult::Number(result / (l as f64))
|
||||
}
|
||||
|
||||
fn subtotal_sum(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let values = match self.subtotal_get_values(args, cell, mode) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut result = 0.0;
|
||||
for value in values {
|
||||
result += value;
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
fn subtotal_product(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let values = match self.subtotal_get_values(args, cell, mode) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut result = 1.0;
|
||||
for value in values {
|
||||
result *= value;
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
fn subtotal_max(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let values = match self.subtotal_get_values(args, cell, mode) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut result = f64::NAN;
|
||||
for value in values {
|
||||
result = value.max(result);
|
||||
}
|
||||
if result.is_nan() {
|
||||
return CalcResult::Number(0.0);
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
|
||||
fn subtotal_min(
|
||||
&mut self,
|
||||
args: &[Node],
|
||||
cell: CellReference,
|
||||
mode: SubTotalMode,
|
||||
) -> CalcResult {
|
||||
let values = match self.subtotal_get_values(args, cell, mode) {
|
||||
Ok(s) => s,
|
||||
Err(s) => return s,
|
||||
};
|
||||
let mut result = f64::NAN;
|
||||
for value in values {
|
||||
result = value.min(result);
|
||||
}
|
||||
if result.is_nan() {
|
||||
return CalcResult::Number(0.0);
|
||||
}
|
||||
CalcResult::Number(result)
|
||||
}
|
||||
}
|
||||
1104
base/src/functions/text.rs
Normal file
1104
base/src/functions/text.rs
Normal file
File diff suppressed because it is too large
Load Diff
196
base/src/functions/text_util.rs
Normal file
196
base/src/functions/text_util.rs
Normal file
@@ -0,0 +1,196 @@
|
||||
pub(crate) enum Case {
|
||||
Sensitive,
|
||||
Insensitive,
|
||||
}
|
||||
|
||||
/// Finds the text after the occurrence instance of 'search_for' in text
|
||||
pub(crate) fn text_after(
|
||||
text: &str,
|
||||
delimiter: &str,
|
||||
instance_num: i32,
|
||||
match_mode: Case,
|
||||
) -> Option<String> {
|
||||
if let Some((_, right)) = match_text(text, delimiter, instance_num, match_mode) {
|
||||
return Some(text[right..].to_string());
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn text_before(
|
||||
text: &str,
|
||||
delimiter: &str,
|
||||
instance_num: i32,
|
||||
match_mode: Case,
|
||||
) -> Option<String> {
|
||||
if let Some((left, _)) = match_text(text, delimiter, instance_num, match_mode) {
|
||||
return Some(text[..left].to_string());
|
||||
};
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn substitute(text: &str, old_text: &str, new_text: &str, instance_num: i32) -> String {
|
||||
if let Some((left, right)) = match_text(text, old_text, instance_num, Case::Sensitive) {
|
||||
return format!("{}{}{}", &text[..left], new_text, &text[right..]);
|
||||
};
|
||||
text.to_string()
|
||||
}
|
||||
|
||||
fn match_text(
|
||||
text: &str,
|
||||
delimiter: &str,
|
||||
instance_num: i32,
|
||||
match_mode: Case,
|
||||
) -> Option<(usize, usize)> {
|
||||
match match_mode {
|
||||
Case::Sensitive => {
|
||||
if instance_num > 0 {
|
||||
text_sensitive(text, delimiter, instance_num)
|
||||
} else {
|
||||
text_sensitive_reverse(text, delimiter, -instance_num)
|
||||
}
|
||||
}
|
||||
Case::Insensitive => {
|
||||
if instance_num > 0 {
|
||||
text_sensitive(
|
||||
&text.to_lowercase(),
|
||||
&delimiter.to_lowercase(),
|
||||
instance_num,
|
||||
)
|
||||
} else {
|
||||
text_sensitive_reverse(
|
||||
&text.to_lowercase(),
|
||||
&delimiter.to_lowercase(),
|
||||
-instance_num,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn text_sensitive(text: &str, delimiter: &str, instance_num: i32) -> Option<(usize, usize)> {
|
||||
let mut byte_index = 0;
|
||||
let mut local_index = 1;
|
||||
// delimiter length in bytes
|
||||
let delimiter_len = delimiter.len();
|
||||
for c in text.chars() {
|
||||
if text[byte_index..].starts_with(delimiter) {
|
||||
if local_index == instance_num {
|
||||
return Some((byte_index, byte_index + delimiter_len));
|
||||
} else {
|
||||
local_index += 1;
|
||||
}
|
||||
}
|
||||
byte_index += c.len_utf8();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn text_sensitive_reverse(
|
||||
text: &str,
|
||||
delimiter: &str,
|
||||
instance_num: i32,
|
||||
) -> Option<(usize, usize)> {
|
||||
let text_len = text.len();
|
||||
let mut byte_index = text_len;
|
||||
let mut local_index = 1;
|
||||
let delimiter_len = delimiter.len();
|
||||
for c in text.chars().rev() {
|
||||
if text[byte_index..].starts_with(delimiter) {
|
||||
if local_index == instance_num {
|
||||
return Some((byte_index, byte_index + delimiter_len));
|
||||
} else {
|
||||
local_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
byte_index -= c.len_utf8();
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::functions::text_util::Case;
|
||||
|
||||
use super::{text_after, text_before};
|
||||
#[test]
|
||||
fn test_text_after_sensitive() {
|
||||
assert_eq!(
|
||||
text_after("One element", "ele", 1, Case::Sensitive),
|
||||
Some("ment".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
text_after("One element", "e", 1, Case::Sensitive),
|
||||
Some(" element".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
text_after("One element", "e", 4, Case::Sensitive),
|
||||
Some("nt".to_string())
|
||||
);
|
||||
assert_eq!(text_after("One element", "e", 5, Case::Sensitive), None);
|
||||
assert_eq!(
|
||||
text_after("長壽相等!", "相", 1, Case::Sensitive),
|
||||
Some("等!".to_string())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_text_before_sensitive() {
|
||||
assert_eq!(
|
||||
text_before("One element", "ele", 1, Case::Sensitive),
|
||||
Some("One ".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
text_before("One element", "e", 1, Case::Sensitive),
|
||||
Some("On".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
text_before("One element", "e", 4, Case::Sensitive),
|
||||
Some("One elem".to_string())
|
||||
);
|
||||
assert_eq!(text_before("One element", "e", 5, Case::Sensitive), None);
|
||||
assert_eq!(
|
||||
text_before("長壽相等!", "相", 1, Case::Sensitive),
|
||||
Some("長壽".to_string())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_text_after_insensitive() {
|
||||
assert_eq!(
|
||||
text_after("One element", "eLe", 1, Case::Insensitive),
|
||||
Some("ment".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
text_after("One element", "E", 1, Case::Insensitive),
|
||||
Some(" element".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
text_after("One element", "E", 4, Case::Insensitive),
|
||||
Some("nt".to_string())
|
||||
);
|
||||
assert_eq!(text_after("One element", "E", 5, Case::Insensitive), None);
|
||||
assert_eq!(
|
||||
text_after("長壽相等!", "相", 1, Case::Insensitive),
|
||||
Some("等!".to_string())
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_text_before_insensitive() {
|
||||
assert_eq!(
|
||||
text_before("One element", "eLe", 1, Case::Insensitive),
|
||||
Some("One ".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
text_before("One element", "E", 1, Case::Insensitive),
|
||||
Some("On".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
text_before("One element", "E", 4, Case::Insensitive),
|
||||
Some("One elem".to_string())
|
||||
);
|
||||
assert_eq!(text_before("One element", "E", 5, Case::Insensitive), None);
|
||||
assert_eq!(
|
||||
text_before("長壽相等!", "相", 1, Case::Insensitive),
|
||||
Some("長壽".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
401
base/src/functions/util.rs
Normal file
401
base/src/functions/util.rs
Normal file
@@ -0,0 +1,401 @@
|
||||
use regex::{escape, Regex};
|
||||
|
||||
use crate::{calc_result::CalcResult, expressions::token::is_english_error_string};
|
||||
|
||||
/// This test for exact match (modulo case).
|
||||
/// * strings are not cast into bools or numbers
|
||||
/// * empty cell is not cast into empty string or zero
|
||||
pub(crate) fn values_are_equal(left: &CalcResult, right: &CalcResult) -> bool {
|
||||
match (left, right) {
|
||||
(CalcResult::Number(value1), CalcResult::Number(value2)) => {
|
||||
if (value2 - value1).abs() < f64::EPSILON {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
(CalcResult::String(value1), CalcResult::String(value2)) => {
|
||||
let value1 = value1.to_uppercase();
|
||||
let value2 = value2.to_uppercase();
|
||||
value1 == value2
|
||||
}
|
||||
(CalcResult::Boolean(value1), CalcResult::Boolean(value2)) => value1 == value2,
|
||||
(CalcResult::EmptyCell, CalcResult::EmptyCell) => true,
|
||||
// NOTE: Errors and Ranges are not covered
|
||||
(_, _) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// In Excel there are two ways of comparing cell values.
|
||||
/// The old school comparison valid in formulas like D3 < D4 or HLOOKUP,... cast empty cells into empty strings or 0
|
||||
/// For the new formulas like XLOOKUP or SORT an empty cell is always larger than anything else.
|
||||
|
||||
// ..., -2, -1, 0, 1, 2, ..., A-Z, FALSE, TRUE;
|
||||
pub(crate) fn compare_values(left: &CalcResult, right: &CalcResult) -> i32 {
|
||||
match (left, right) {
|
||||
(CalcResult::Number(value1), CalcResult::Number(value2)) => {
|
||||
if (value2 - value1).abs() < f64::EPSILON {
|
||||
return 0;
|
||||
}
|
||||
if value1 < value2 {
|
||||
return -1;
|
||||
}
|
||||
1
|
||||
}
|
||||
(CalcResult::Number(_value1), CalcResult::String(_value2)) => -1,
|
||||
(CalcResult::Number(_value1), CalcResult::Boolean(_value2)) => -1,
|
||||
(CalcResult::String(value1), CalcResult::String(value2)) => {
|
||||
let value1 = value1.to_uppercase();
|
||||
let value2 = value2.to_uppercase();
|
||||
match value1.cmp(&value2) {
|
||||
std::cmp::Ordering::Less => -1,
|
||||
std::cmp::Ordering::Equal => 0,
|
||||
std::cmp::Ordering::Greater => 1,
|
||||
}
|
||||
}
|
||||
(CalcResult::String(_value1), CalcResult::Boolean(_value2)) => -1,
|
||||
(CalcResult::Boolean(value1), CalcResult::Boolean(value2)) => {
|
||||
if value1 == value2 {
|
||||
return 0;
|
||||
}
|
||||
if *value1 {
|
||||
return 1;
|
||||
}
|
||||
-1
|
||||
}
|
||||
(CalcResult::EmptyCell, CalcResult::String(_value2)) => {
|
||||
compare_values(&CalcResult::String("".to_string()), right)
|
||||
}
|
||||
(CalcResult::String(_value1), CalcResult::EmptyCell) => {
|
||||
compare_values(left, &CalcResult::String("".to_string()))
|
||||
}
|
||||
(CalcResult::EmptyCell, CalcResult::Number(_value2)) => {
|
||||
compare_values(&CalcResult::Number(0.0), right)
|
||||
}
|
||||
(CalcResult::Number(_value1), CalcResult::EmptyCell) => {
|
||||
compare_values(left, &CalcResult::Number(0.0))
|
||||
}
|
||||
(CalcResult::EmptyCell, CalcResult::EmptyCell) => 0,
|
||||
// NOTE: Errors and Ranges are not covered
|
||||
(_, _) => 1,
|
||||
}
|
||||
}
|
||||
|
||||
/// We convert an Excel wildcard into a Rust (Perl family) regex
|
||||
pub(crate) fn from_wildcard_to_regex(
|
||||
wildcard: &str,
|
||||
exact: bool,
|
||||
) -> Result<regex::Regex, regex::Error> {
|
||||
// 1. Escape all
|
||||
let reg = &escape(wildcard);
|
||||
|
||||
// 2. We convert the escaped '?' into '.' (matches a single character)
|
||||
let reg = ®.replace("\\?", ".");
|
||||
// 3. We convert the escaped '*' into '.*' (matches anything)
|
||||
let reg = ®.replace("\\*", ".*");
|
||||
|
||||
// 4. We send '\\~\\~' to '??' that is an unescaped regular expression, therefore cannot be in reg
|
||||
let reg = ®.replace("\\~\\~", "??");
|
||||
|
||||
// 5. If the escaped and converted '*' is preceded by '~' then it's a raw '*'
|
||||
let reg = ®.replace("\\~.*", "\\*");
|
||||
// 6. If the escaped and converted '.' is preceded by '~' then it's a raw '?'
|
||||
let reg = ®.replace("\\~.", "\\?");
|
||||
// '~' is used in Excel to escape any other character.
|
||||
// So ~x goes to x (whatever x is)
|
||||
// 7. Remove all the others '\\~d' --> 'd'
|
||||
let reg = ®.replace("\\~", "");
|
||||
// 8. Put back the '\\~\\~' as '\\~'
|
||||
let reg = ®.replace("??", "\\~");
|
||||
|
||||
// And we have a valid Perl regex! (As Kim Kardashian said before me: "I know, right?")
|
||||
if exact {
|
||||
return Regex::new(&format!("^{}$", reg));
|
||||
}
|
||||
Regex::new(reg)
|
||||
}
|
||||
|
||||
/// NUMBERS ///
|
||||
///*********///
|
||||
|
||||
// It could be either the number or a string representation of the number
|
||||
// In the rest of the cases calc_result needs to be a number (cannot be the string "23", for instance)
|
||||
fn result_is_equal_to_number(calc_result: &CalcResult, target: f64) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Number(f) => {
|
||||
if (f - target).abs() < f64::EPSILON {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
CalcResult::String(s) => {
|
||||
if let Ok(f) = s.parse::<f64>() {
|
||||
if (f - target).abs() < f64::EPSILON {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_less_than_number(calc_result: &CalcResult, target: f64) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Number(f) => *f < target,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_less_or_equal_than_number(calc_result: &CalcResult, target: f64) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Number(f) => *f <= target,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_greater_than_number(calc_result: &CalcResult, target: f64) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Number(f) => *f > target,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_greater_or_equal_than_number(calc_result: &CalcResult, target: f64) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Number(f) => *f >= target,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_not_equal_to_number(calc_result: &CalcResult, target: f64) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Number(f) => {
|
||||
if (f - target).abs() > f64::EPSILON {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// BOOLEANS ///
|
||||
///**********///
|
||||
|
||||
// Booleans have to be "exactly" equal
|
||||
fn result_is_equal_to_bool(calc_result: &CalcResult, target: bool) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Boolean(f) => target == *f,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_not_equal_to_bool(calc_result: &CalcResult, target: bool) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Boolean(f) => target != *f,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// STRINGS ///
|
||||
///*********///
|
||||
|
||||
/// Note that strings are case insensitive. `target` must always be lower case.
|
||||
|
||||
pub(crate) fn result_matches_regex(calc_result: &CalcResult, reg: &Regex) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::String(s) => reg.is_match(&s.to_lowercase()),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_equal_to_string(calc_result: &CalcResult, target: &str) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::String(s) => {
|
||||
if target == s.to_lowercase() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
CalcResult::EmptyCell => target.is_empty(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_not_equal_to_string(calc_result: &CalcResult, target: &str) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::String(s) => {
|
||||
if target != s.to_lowercase() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_less_than_string(calc_result: &CalcResult, target: &str) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::String(s) => target.cmp(&s.to_lowercase()) == std::cmp::Ordering::Greater,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_less_or_equal_than_string(calc_result: &CalcResult, target: &str) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::String(s) => {
|
||||
let lower_case = &s.to_lowercase();
|
||||
target.cmp(lower_case) == std::cmp::Ordering::Less || lower_case == target
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_greater_than_string(calc_result: &CalcResult, target: &str) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::String(s) => target.cmp(&s.to_lowercase()) == std::cmp::Ordering::Less,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_greater_or_equal_than_string(calc_result: &CalcResult, target: &str) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::String(s) => {
|
||||
let lower_case = &s.to_lowercase();
|
||||
target.cmp(lower_case) == std::cmp::Ordering::Greater || lower_case == target
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// ERRORS ///
|
||||
///********///
|
||||
|
||||
fn result_is_equal_to_error(calc_result: &CalcResult, target: &str) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Error { error, .. } => target == error.to_string(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn result_is_not_equal_to_error(calc_result: &CalcResult, target: &str) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Error { error, .. } => target != error.to_string(),
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// EMPTY ///
|
||||
///*******///
|
||||
|
||||
// Note that these two are not inverse of each other.
|
||||
// In particular, you can never match an empty cell.
|
||||
|
||||
fn result_is_not_equal_to_empty(calc_result: &CalcResult) -> bool {
|
||||
!matches!(calc_result, CalcResult::EmptyCell)
|
||||
}
|
||||
|
||||
fn result_is_equal_to_empty(calc_result: &CalcResult) -> bool {
|
||||
match calc_result {
|
||||
CalcResult::Number(f) => (f - 0.0).abs() < f64::EPSILON,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// This returns a function (closure) of signature fn(&CalcResult) -> bool
|
||||
/// It is Boxed because it returns different closures, so the size cannot be known at compile time
|
||||
/// The lifetime (a) of value has to be longer or equal to the lifetime of the returned closure
|
||||
pub(crate) fn build_criteria<'a>(value: &'a CalcResult) -> Box<dyn Fn(&CalcResult) -> bool + 'a> {
|
||||
match value {
|
||||
CalcResult::String(s) => {
|
||||
if let Some(v) = s.strip_prefix("<=") {
|
||||
// TODO: I am not implementing <= ERROR or <= BOOLEAN
|
||||
if let Ok(f) = v.parse::<f64>() {
|
||||
Box::new(move |x| result_is_less_or_equal_than_number(x, f))
|
||||
} else if v.is_empty() {
|
||||
Box::new(move |_x| false)
|
||||
} else {
|
||||
Box::new(move |x| result_is_less_or_equal_than_string(x, &v.to_lowercase()))
|
||||
}
|
||||
} else if let Some(v) = s.strip_prefix(">=") {
|
||||
// TODO: I am not implementing >= ERROR or >= BOOLEAN
|
||||
if let Ok(f) = v.parse::<f64>() {
|
||||
Box::new(move |x| result_is_greater_or_equal_than_number(x, f))
|
||||
} else if v.is_empty() {
|
||||
Box::new(move |_x| false)
|
||||
} else {
|
||||
Box::new(move |x| result_is_greater_or_equal_than_string(x, &v.to_lowercase()))
|
||||
}
|
||||
} else if let Some(v) = s.strip_prefix("<>") {
|
||||
if let Ok(f) = v.parse::<f64>() {
|
||||
Box::new(move |x| result_is_not_equal_to_number(x, f))
|
||||
} else if let Ok(b) = v.to_lowercase().parse::<bool>() {
|
||||
Box::new(move |x| result_is_not_equal_to_bool(x, b))
|
||||
} else if is_english_error_string(v) {
|
||||
Box::new(move |x| result_is_not_equal_to_error(x, v))
|
||||
} else if v.contains('*') || v.contains('?') {
|
||||
if let Ok(reg) = from_wildcard_to_regex(&v.to_lowercase(), true) {
|
||||
Box::new(move |x| !result_matches_regex(x, ®))
|
||||
} else {
|
||||
Box::new(move |_| false)
|
||||
}
|
||||
} else if v.is_empty() {
|
||||
Box::new(result_is_not_equal_to_empty)
|
||||
} else {
|
||||
Box::new(move |x| result_is_not_equal_to_string(x, &v.to_lowercase()))
|
||||
}
|
||||
} else if let Some(v) = s.strip_prefix('<') {
|
||||
// TODO: I am not implementing < ERROR or < BOOLEAN
|
||||
if let Ok(f) = v.parse::<f64>() {
|
||||
Box::new(move |x| result_is_less_than_number(x, f))
|
||||
} else if v.is_empty() {
|
||||
Box::new(move |_x| false)
|
||||
} else {
|
||||
Box::new(move |x| result_is_less_than_string(x, &v.to_lowercase()))
|
||||
}
|
||||
} else if let Some(v) = s.strip_prefix('>') {
|
||||
// TODO: I am not implementing > ERROR or > BOOLEAN
|
||||
if let Ok(f) = v.parse::<f64>() {
|
||||
Box::new(move |x| result_is_greater_than_number(x, f))
|
||||
} else if v.is_empty() {
|
||||
Box::new(move |_x| false)
|
||||
} else {
|
||||
Box::new(move |x| result_is_greater_than_string(x, &v.to_lowercase()))
|
||||
}
|
||||
} else {
|
||||
let v = if let Some(a) = s.strip_prefix('=') {
|
||||
a
|
||||
} else {
|
||||
s
|
||||
};
|
||||
if let Ok(f) = v.parse::<f64>() {
|
||||
Box::new(move |x| result_is_equal_to_number(x, f))
|
||||
} else if let Ok(b) = v.to_lowercase().parse::<bool>() {
|
||||
Box::new(move |x| result_is_equal_to_bool(x, b))
|
||||
} else if is_english_error_string(v) {
|
||||
Box::new(move |x| result_is_equal_to_error(x, v))
|
||||
} else if v.contains('*') || v.contains('?') {
|
||||
if let Ok(reg) = from_wildcard_to_regex(&v.to_lowercase(), true) {
|
||||
Box::new(move |x| result_matches_regex(x, ®))
|
||||
} else {
|
||||
Box::new(move |_| false)
|
||||
}
|
||||
} else {
|
||||
Box::new(move |x| result_is_equal_to_string(x, &v.to_lowercase()))
|
||||
}
|
||||
}
|
||||
}
|
||||
CalcResult::Number(target) => Box::new(move |x| result_is_equal_to_number(x, *target)),
|
||||
CalcResult::Boolean(b) => Box::new(move |x| result_is_equal_to_bool(x, *b)),
|
||||
CalcResult::Error { error, .. } => {
|
||||
// An error will match an error (never a string that is an error)
|
||||
Box::new(move |x| result_is_equal_to_error(x, &error.to_string()))
|
||||
}
|
||||
CalcResult::Range { left: _, right: _ } => {
|
||||
// TODO: Implicit Intersection
|
||||
Box::new(move |_x| false)
|
||||
}
|
||||
CalcResult::EmptyCell | CalcResult::EmptyArg => Box::new(result_is_equal_to_empty),
|
||||
}
|
||||
}
|
||||
384
base/src/functions/xlookup.rs
Normal file
384
base/src/functions/xlookup.rs
Normal file
@@ -0,0 +1,384 @@
|
||||
use crate::constants::{LAST_COLUMN, LAST_ROW};
|
||||
use crate::{
|
||||
calc_result::{CalcResult, CellReference},
|
||||
expressions::parser::Node,
|
||||
expressions::token::Error,
|
||||
model::Model,
|
||||
};
|
||||
|
||||
use super::{
|
||||
binary_search::{
|
||||
binary_search_descending_or_greater, binary_search_descending_or_smaller,
|
||||
binary_search_or_greater, binary_search_or_smaller,
|
||||
},
|
||||
util::{compare_values, from_wildcard_to_regex, result_matches_regex},
|
||||
};
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum SearchMode {
|
||||
StartAtFirstItem = 1,
|
||||
StartAtLastItem = -1,
|
||||
BinarySearchDescending = -2,
|
||||
BinarySearchAscending = 2,
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum MatchMode {
|
||||
ExactMatchSmaller = -1,
|
||||
ExactMatch = 0,
|
||||
ExactMatchLarger = 1,
|
||||
WildcardMatch = 2,
|
||||
}
|
||||
|
||||
// lookup_value in array, match_mode search_mode
|
||||
fn linear_search(
|
||||
lookup_value: &CalcResult,
|
||||
array: &[CalcResult],
|
||||
search_mode: SearchMode,
|
||||
match_mode: MatchMode,
|
||||
) -> Option<usize> {
|
||||
let length = array.len();
|
||||
|
||||
match match_mode {
|
||||
MatchMode::ExactMatch => {
|
||||
// exact match
|
||||
for l in 0..length {
|
||||
let index = if search_mode == SearchMode::StartAtFirstItem {
|
||||
l
|
||||
} else {
|
||||
length - l - 1
|
||||
};
|
||||
|
||||
let value = &array[index];
|
||||
if compare_values(value, lookup_value) == 0 {
|
||||
return Some(index);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
MatchMode::ExactMatchSmaller | MatchMode::ExactMatchLarger => {
|
||||
// exact match, if none found return the next smaller/larger item
|
||||
let mut found_index = 0;
|
||||
let mut approx = None;
|
||||
let m_mode = match_mode as i32;
|
||||
for l in 0..length {
|
||||
let index = if search_mode == SearchMode::StartAtFirstItem {
|
||||
l
|
||||
} else {
|
||||
length - l - 1
|
||||
};
|
||||
|
||||
let value = &array[index];
|
||||
let c = compare_values(value, lookup_value);
|
||||
if c == 0 {
|
||||
return Some(index);
|
||||
} else if c == m_mode {
|
||||
match approx {
|
||||
None => {
|
||||
approx = Some(value.clone());
|
||||
found_index = index;
|
||||
}
|
||||
Some(ref p) => {
|
||||
if compare_values(p, value) == m_mode {
|
||||
approx = Some(value.clone());
|
||||
found_index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if approx.is_none() {
|
||||
return None;
|
||||
} else {
|
||||
return Some(found_index);
|
||||
}
|
||||
}
|
||||
MatchMode::WildcardMatch => {
|
||||
let result_matches: Box<dyn Fn(&CalcResult) -> bool> =
|
||||
if let CalcResult::String(s) = &lookup_value {
|
||||
if let Ok(reg) = from_wildcard_to_regex(&s.to_lowercase(), true) {
|
||||
Box::new(move |x| result_matches_regex(x, ®))
|
||||
} else {
|
||||
Box::new(move |_| false)
|
||||
}
|
||||
} else {
|
||||
Box::new(move |x| compare_values(x, lookup_value) == 0)
|
||||
};
|
||||
for l in 0..length {
|
||||
let index = if search_mode == SearchMode::StartAtFirstItem {
|
||||
l
|
||||
} else {
|
||||
length - l - 1
|
||||
};
|
||||
let value = &array[index];
|
||||
if result_matches(value) {
|
||||
return Some(index);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// The XLOOKUP function searches a range or an array, and then returns the item corresponding
|
||||
/// to the first match it finds. If no match exists, then XLOOKUP can return the closest (approximate) match.
|
||||
/// =XLOOKUP(lookup_value, lookup_array, return_array, [if_not_found], [match_mode], [search_mode])
|
||||
///
|
||||
/// lookup_array and return_array must be column or row arrays and of the same dimension.
|
||||
/// Otherwise #VALUE! is returned
|
||||
/// [if_not_found]
|
||||
/// Where a valid match is not found, return the [if_not_found] text you supply.
|
||||
/// If a valid match is not found, and [if_not_found] is missing, #N/A is returned.
|
||||
///
|
||||
/// [match_mode]
|
||||
/// Specify the match type:
|
||||
/// * 0 - Exact match. If none found, return #N/A. This is the default.
|
||||
/// * -1 - Exact match. If none found, return the next smaller item.
|
||||
/// * 1 - Exact match. If none found, return the next larger item.
|
||||
/// * 2 - A wildcard match where *, ?, and ~ have special meaning.
|
||||
///
|
||||
/// [search_mode]
|
||||
/// Specify the search mode to use:
|
||||
/// * 1 - Perform a search starting at the first item. This is the default.
|
||||
/// * -1 - Perform a reverse search starting at the last item.
|
||||
/// * 2 - Perform a binary search that relies on lookup_array being sorted
|
||||
/// in ascending order. If not sorted, invalid results will be returned.
|
||||
/// * -2 - Perform a binary search that relies on lookup_array being sorted
|
||||
/// in descending order. If not sorted, invalid results will be returned.
|
||||
pub(crate) fn fn_xlookup(&mut self, args: &[Node], cell: CellReference) -> CalcResult {
|
||||
if args.len() < 3 || args.len() > 6 {
|
||||
return CalcResult::new_args_number_error(cell);
|
||||
}
|
||||
let lookup_value = self.evaluate_node_in_context(&args[0], cell);
|
||||
if lookup_value.is_error() {
|
||||
return lookup_value;
|
||||
}
|
||||
// Get optional arguments
|
||||
let if_not_found = if args.len() >= 4 {
|
||||
let v = self.evaluate_node_in_context(&args[3], cell);
|
||||
match v {
|
||||
CalcResult::EmptyArg => CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
},
|
||||
_ => v,
|
||||
}
|
||||
} else {
|
||||
// default
|
||||
CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Not found".to_string(),
|
||||
}
|
||||
};
|
||||
let match_mode = if args.len() >= 5 {
|
||||
match self.get_number(&args[4], cell) {
|
||||
Ok(c) => match c.floor() as i32 {
|
||||
-1 => MatchMode::ExactMatchSmaller,
|
||||
1 => MatchMode::ExactMatchLarger,
|
||||
0 => MatchMode::ExactMatch,
|
||||
2 => MatchMode::WildcardMatch,
|
||||
_ => {
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Unexpected number".to_string(),
|
||||
};
|
||||
}
|
||||
},
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
// default
|
||||
MatchMode::ExactMatch
|
||||
};
|
||||
let search_mode = if args.len() == 6 {
|
||||
match self.get_number(&args[5], cell) {
|
||||
Ok(c) => match c.floor() as i32 {
|
||||
1 => SearchMode::StartAtFirstItem,
|
||||
-1 => SearchMode::StartAtLastItem,
|
||||
-2 => SearchMode::BinarySearchDescending,
|
||||
2 => SearchMode::BinarySearchAscending,
|
||||
_ => {
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Unexpected number".to_string(),
|
||||
};
|
||||
}
|
||||
},
|
||||
Err(s) => return s,
|
||||
}
|
||||
} else {
|
||||
// default
|
||||
SearchMode::StartAtFirstItem
|
||||
};
|
||||
// lookup_array
|
||||
match self.evaluate_node_in_context(&args[1], cell) {
|
||||
CalcResult::Range { left, right } => {
|
||||
let is_row_vector;
|
||||
if left.row == right.row {
|
||||
is_row_vector = false;
|
||||
} else if left.column == right.column {
|
||||
is_row_vector = true;
|
||||
} else {
|
||||
// second argument must be a vector
|
||||
return CalcResult::Error {
|
||||
error: Error::ERROR,
|
||||
origin: cell,
|
||||
message: "Second argument must be a vector".to_string(),
|
||||
};
|
||||
}
|
||||
// return array
|
||||
match self.evaluate_node_in_context(&args[2], cell) {
|
||||
CalcResult::Range {
|
||||
left: result_left,
|
||||
right: result_right,
|
||||
} => {
|
||||
if result_right.row - result_left.row != right.row - left.row
|
||||
|| result_right.column - result_left.column
|
||||
!= right.column - left.column
|
||||
{
|
||||
return CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Arrays must be of the same size".to_string(),
|
||||
};
|
||||
}
|
||||
let mut row2 = right.row;
|
||||
let row1 = left.row;
|
||||
let mut column2 = right.column;
|
||||
let column1 = left.column;
|
||||
|
||||
if row1 == 1 && row2 == LAST_ROW {
|
||||
row2 = self
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.max_row;
|
||||
}
|
||||
if column1 == 1 && column2 == LAST_COLUMN {
|
||||
column2 = self
|
||||
.workbook
|
||||
.worksheet(left.sheet)
|
||||
.expect("Sheet expected during evaluation.")
|
||||
.dimension()
|
||||
.max_column;
|
||||
}
|
||||
let left = CellReference {
|
||||
sheet: left.sheet,
|
||||
column: column1,
|
||||
row: row1,
|
||||
};
|
||||
let right = CellReference {
|
||||
sheet: left.sheet,
|
||||
column: column2,
|
||||
row: row2,
|
||||
};
|
||||
match search_mode {
|
||||
SearchMode::StartAtFirstItem | SearchMode::StartAtLastItem => {
|
||||
let array = &self.prepare_array(&left, &right, is_row_vector);
|
||||
match linear_search(&lookup_value, array, search_mode, match_mode) {
|
||||
Some(index) => {
|
||||
let row_index =
|
||||
if is_row_vector { index as i32 } else { 0 };
|
||||
let column_index =
|
||||
if is_row_vector { 0 } else { index as i32 };
|
||||
self.evaluate_cell(CellReference {
|
||||
sheet: result_left.sheet,
|
||||
row: result_left.row + row_index,
|
||||
column: result_left.column + column_index,
|
||||
})
|
||||
}
|
||||
None => if_not_found,
|
||||
}
|
||||
}
|
||||
SearchMode::BinarySearchAscending
|
||||
| SearchMode::BinarySearchDescending => {
|
||||
let index = if match_mode == MatchMode::ExactMatchLarger {
|
||||
if search_mode == SearchMode::BinarySearchAscending {
|
||||
binary_search_or_greater(
|
||||
&lookup_value,
|
||||
&self.prepare_array(&left, &right, is_row_vector),
|
||||
)
|
||||
} else {
|
||||
binary_search_descending_or_greater(
|
||||
&lookup_value,
|
||||
&self.prepare_array(&left, &right, is_row_vector),
|
||||
)
|
||||
}
|
||||
} else if search_mode == SearchMode::BinarySearchAscending {
|
||||
binary_search_or_smaller(
|
||||
&lookup_value,
|
||||
&self.prepare_array(&left, &right, is_row_vector),
|
||||
)
|
||||
} else {
|
||||
binary_search_descending_or_smaller(
|
||||
&lookup_value,
|
||||
&self.prepare_array(&left, &right, is_row_vector),
|
||||
)
|
||||
};
|
||||
match index {
|
||||
None => if_not_found,
|
||||
Some(l) => {
|
||||
let row =
|
||||
result_left.row + if is_row_vector { l } else { 0 };
|
||||
let column =
|
||||
result_left.column + if is_row_vector { 0 } else { l };
|
||||
if match_mode == MatchMode::ExactMatch {
|
||||
let value = self.evaluate_cell(CellReference {
|
||||
sheet: left.sheet,
|
||||
row: left.row + if is_row_vector { l } else { 0 },
|
||||
column: left.column
|
||||
+ if is_row_vector { 0 } else { l },
|
||||
});
|
||||
if compare_values(&value, &lookup_value) == 0 {
|
||||
self.evaluate_cell(CellReference {
|
||||
sheet: result_left.sheet,
|
||||
row,
|
||||
column,
|
||||
})
|
||||
} else {
|
||||
if_not_found
|
||||
}
|
||||
} else if match_mode == MatchMode::ExactMatchSmaller
|
||||
|| match_mode == MatchMode::ExactMatchLarger
|
||||
{
|
||||
self.evaluate_cell(CellReference {
|
||||
sheet: result_left.sheet,
|
||||
row,
|
||||
column,
|
||||
})
|
||||
} else {
|
||||
CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Cannot use wildcard in binary search"
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => error,
|
||||
_ => CalcResult::Error {
|
||||
error: Error::VALUE,
|
||||
origin: cell,
|
||||
message: "Range expected".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
error @ CalcResult::Error { .. } => error,
|
||||
_ => CalcResult::Error {
|
||||
error: Error::NA,
|
||||
origin: cell,
|
||||
message: "Range expected".to_string(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
48
base/src/implicit_intersection.rs
Normal file
48
base/src/implicit_intersection.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use crate::calc_result::{CellReference, Range};
|
||||
|
||||
/// It returns the closest cell from cell_reference to range in the same column/row
|
||||
/// Examples
|
||||
/// * i_i(B5, A2:A9) -> B5
|
||||
/// * i_i(B5, A7:A9) -> None
|
||||
/// * i_i(B5, A2:D2) -> B2
|
||||
pub(crate) fn implicit_intersection(
|
||||
cell_reference: &CellReference,
|
||||
range: &Range,
|
||||
) -> Option<CellReference> {
|
||||
let left = &range.left;
|
||||
let right = &range.right;
|
||||
let sheet = cell_reference.sheet;
|
||||
// If they are not all in the same sheet there is no intersection
|
||||
if sheet != left.sheet && sheet != right.sheet {
|
||||
return None;
|
||||
}
|
||||
let row = cell_reference.row;
|
||||
let column = cell_reference.column;
|
||||
if row >= left.row && row <= right.row {
|
||||
if left.column != right.column {
|
||||
return None;
|
||||
}
|
||||
return Some(CellReference {
|
||||
sheet,
|
||||
row,
|
||||
column: left.column,
|
||||
});
|
||||
} else if column >= left.column && column <= right.column {
|
||||
if left.row != right.row {
|
||||
return None;
|
||||
}
|
||||
return Some(CellReference {
|
||||
sheet,
|
||||
row: left.row,
|
||||
column,
|
||||
});
|
||||
} else if left.row == right.row && left.column == right.column {
|
||||
// If the range is a single cell, then return it.
|
||||
return Some(CellReference {
|
||||
sheet,
|
||||
row: left.row,
|
||||
column: right.column,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
82
base/src/language/language.json
Normal file
82
base/src/language/language.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"en": {
|
||||
"booleans": {
|
||||
"true": "TRUE",
|
||||
"false": "FALSE"
|
||||
},
|
||||
"errors":{
|
||||
"ref": "#REF!",
|
||||
"name": "#NAME?",
|
||||
"value": "#VALUE!",
|
||||
"div": "#DIV/0!",
|
||||
"na": "#N/A",
|
||||
"num": "#NUM!",
|
||||
"error": "#ERROR!",
|
||||
"nimpl": "#N/IMPL!",
|
||||
"spill": "#SPILL!",
|
||||
"null": "#NULL!",
|
||||
"calc": "#CALC!",
|
||||
"circ": "#CIRC!"
|
||||
}
|
||||
},
|
||||
"de": {
|
||||
"booleans": {
|
||||
"true": "WAHR",
|
||||
"false": "FALSCH"
|
||||
},
|
||||
"errors":{
|
||||
"ref": "#BEZUG!",
|
||||
"name": "#NAME?",
|
||||
"value": "#WERT!",
|
||||
"div": "#DIV/0!",
|
||||
"na": "#NV",
|
||||
"num": "#ZAHL!",
|
||||
"error": "#ERROR!",
|
||||
"nimpl": "#N/IMPL!",
|
||||
"spill": "#ÜBERLAUF!",
|
||||
"null": "#NULL!",
|
||||
"calc": "#CALC!",
|
||||
"circ": "#CIRC!"
|
||||
}
|
||||
},
|
||||
"fr": {
|
||||
"booleans": {
|
||||
"true": "VRAI",
|
||||
"false": "FAUX"
|
||||
},
|
||||
"errors":{
|
||||
"ref": "#REF!",
|
||||
"name": "#NOM?",
|
||||
"value": "#VALEUR!",
|
||||
"div": "#DIV/0!",
|
||||
"na": "#N/A",
|
||||
"num": "#NOMBRE!",
|
||||
"error": "#ERROR!",
|
||||
"nimpl": "#N/IMPL!",
|
||||
"spill": "#SPILL!",
|
||||
"null": "#NULL!",
|
||||
"calc": "#CALC!",
|
||||
"circ": "#CIRC!"
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
"booleans": {
|
||||
"true": "VERDADERO",
|
||||
"false": "FALSO"
|
||||
},
|
||||
"errors": {
|
||||
"ref": "#¡REF!",
|
||||
"name": "#¿NOMBRE?",
|
||||
"value": "#¡VALOR!",
|
||||
"div": "#¡DIV/0!",
|
||||
"na": "#N/A",
|
||||
"num": "#¡NUM!",
|
||||
"error": "#ERROR!",
|
||||
"nimpl": "#N/IMPL!",
|
||||
"spill": "#SPILL!",
|
||||
"null": "#NULL!",
|
||||
"calc": "#CALC!",
|
||||
"circ": "#CIRC!"
|
||||
}
|
||||
}
|
||||
}
|
||||
46
base/src/language/mod.rs
Normal file
46
base/src/language/mod.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Booleans {
|
||||
#[serde(rename = "true")]
|
||||
pub true_value: String,
|
||||
#[serde(rename = "false")]
|
||||
pub false_value: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Errors {
|
||||
#[serde(rename = "ref")]
|
||||
pub ref_value: String,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
pub div: String,
|
||||
pub na: String,
|
||||
pub num: String,
|
||||
pub nimpl: String,
|
||||
pub spill: String,
|
||||
pub calc: String,
|
||||
pub circ: String,
|
||||
pub error: String,
|
||||
pub null: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Language {
|
||||
pub booleans: Booleans,
|
||||
pub errors: Errors,
|
||||
}
|
||||
|
||||
static LANGUAGES: Lazy<HashMap<String, Language>> = Lazy::new(|| {
|
||||
serde_json::from_str(include_str!("language.json")).expect("Failed parsing language file")
|
||||
});
|
||||
|
||||
pub fn get_language(id: &str) -> Result<&Language, String> {
|
||||
let language = LANGUAGES
|
||||
.get(id)
|
||||
.ok_or(format!("Language is not supported: '{}'", id))?;
|
||||
Ok(language)
|
||||
}
|
||||
32
base/src/lib.rs
Normal file
32
base/src/lib.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
#![deny(clippy::unwrap_used)]
|
||||
pub mod calc_result;
|
||||
pub mod cell;
|
||||
pub mod expressions;
|
||||
pub mod formatter;
|
||||
pub mod language;
|
||||
pub mod locale;
|
||||
pub mod model;
|
||||
pub mod new_empty;
|
||||
pub mod number_format;
|
||||
pub mod types;
|
||||
pub mod worksheet;
|
||||
|
||||
mod functions;
|
||||
|
||||
mod actions;
|
||||
mod cast;
|
||||
mod constants;
|
||||
mod styles;
|
||||
|
||||
mod diffs;
|
||||
mod implicit_intersection;
|
||||
|
||||
mod units;
|
||||
mod utils;
|
||||
mod workbook;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod mock_time;
|
||||
1
base/src/locale/locales.json
Normal file
1
base/src/locale/locales.json
Normal file
@@ -0,0 +1 @@
|
||||
{"en":{"dates":{"day_names":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"day_names_short":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"months":["January","February","March","April","May","June","July","August","September","October","November","December"],"months_short":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],"months_letter":["J","F","M","A","M","J","J","A","S","O","N","D"]},"numbers":{"symbols-numberSystem-latn":{"decimal":".","group":",","list":";","percentSign":"%","plusSign":"+","minusSign":"-","approximatelySign":"~","exponential":"E","superscriptingExponent":"×","perMille":"‰","infinity":"∞","nan":"NaN","timeSeparator":":"},"decimalFormats-numberSystem-latn":{"standard":"#,##0.###"},"currencyFormats-numberSystem-latn":{"standard":"¤#,##0.00","standard-alphaNextToNumber":"¤ #,##0.00","standard-noCurrency":"#,##0.00","accounting":"¤#,##0.00;(¤#,##0.00)","accounting-alphaNextToNumber":"¤ #,##0.00;(¤ #,##0.00)","accounting-noCurrency":"#,##0.00;(#,##0.00)"}},"currency":{"iso":"USD","symbol":"$"}},"en-GB":{"dates":{"day_names":["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],"day_names_short":["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],"months":["January","February","March","April","May","June","July","August","September","October","November","December"],"months_short":["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sept","Oct","Nov","Dec"],"months_letter":["J","F","M","A","M","J","J","A","S","O","N","D"]},"numbers":{"symbols-numberSystem-latn":{"decimal":".","group":",","list":";","percentSign":"%","plusSign":"+","minusSign":"-","approximatelySign":"~","exponential":"E","superscriptingExponent":"×","perMille":"‰","infinity":"∞","nan":"NaN","timeSeparator":":"},"decimalFormats-numberSystem-latn":{"standard":"#,##0.###"},"currencyFormats-numberSystem-latn":{"standard":"¤#,##0.00","standard-alphaNextToNumber":"¤ #,##0.00","standard-noCurrency":"#,##0.00","accounting":"¤#,##0.00;(¤#,##0.00)","accounting-alphaNextToNumber":"¤ #,##0.00;(¤ #,##0.00)","accounting-noCurrency":"#,##0.00;(#,##0.00)"}},"currency":{"iso":"USD","symbol":"$"}},"es":{"dates":{"day_names":["domingo","lunes","martes","miércoles","jueves","viernes","sábado"],"day_names_short":["dom","lun","mar","mié","jue","vie","sáb"],"months":["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"],"months_short":["ene","feb","mar","abr","may","jun","jul","ago","sept","oct","nov","dic"],"months_letter":["E","F","M","A","M","J","J","A","S","O","N","D"]},"numbers":{"symbols-numberSystem-latn":{"decimal":",","group":".","list":";","percentSign":"%","plusSign":"+","minusSign":"-","approximatelySign":"~","exponential":"E","superscriptingExponent":"×","perMille":"‰","infinity":"∞","nan":"NaN","timeSeparator":":"},"decimalFormats-numberSystem-latn":{"standard":"#,##0.###"},"currencyFormats-numberSystem-latn":{"standard":"#,##0.00 ¤","standard-noCurrency":"#,##0.00","accounting":"#,##0.00 ¤","accounting-noCurrency":"#,##0.00"}},"currency":{"iso":"USD","symbol":"$"}},"de":{"dates":{"day_names":["Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"],"day_names_short":["So.","Mo.","Di.","Mi.","Do.","Fr.","Sa."],"months":["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"],"months_short":["Jan.","Feb.","März","Apr.","Mai","Juni","Juli","Aug.","Sept.","Okt.","Nov.","Dez."],"months_letter":["J","F","M","A","M","J","J","A","S","O","N","D"]},"numbers":{"symbols-numberSystem-latn":{"decimal":",","group":".","list":";","percentSign":"%","plusSign":"+","minusSign":"-","approximatelySign":"≈","exponential":"E","superscriptingExponent":"·","perMille":"‰","infinity":"∞","nan":"NaN","timeSeparator":":"},"decimalFormats-numberSystem-latn":{"standard":"#,##0.###"},"currencyFormats-numberSystem-latn":{"standard":"#,##0.00 ¤","standard-noCurrency":"#,##0.00","accounting":"#,##0.00 ¤","accounting-noCurrency":"#,##0.00"}},"currency":{"iso":"USD","symbol":"$"}}}
|
||||
93
base/src/locale/mod.rs
Normal file
93
base/src/locale/mod.rs
Normal file
@@ -0,0 +1,93 @@
|
||||
use once_cell::sync::Lazy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Locale {
|
||||
pub dates: Dates,
|
||||
pub numbers: NumbersProperties,
|
||||
pub currency: Currency,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Currency {
|
||||
pub iso: String,
|
||||
pub symbol: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct NumbersProperties {
|
||||
#[serde(rename = "symbols-numberSystem-latn")]
|
||||
pub symbols: NumbersSymbols,
|
||||
#[serde(rename = "decimalFormats-numberSystem-latn")]
|
||||
pub decimal_formats: DecimalFormats,
|
||||
#[serde(rename = "currencyFormats-numberSystem-latn")]
|
||||
pub currency_formats: CurrencyFormats,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Dates {
|
||||
pub day_names: Vec<String>,
|
||||
pub day_names_short: Vec<String>,
|
||||
pub months: Vec<String>,
|
||||
pub months_short: Vec<String>,
|
||||
pub months_letter: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct NumbersSymbols {
|
||||
pub decimal: String,
|
||||
pub group: String,
|
||||
pub list: String,
|
||||
pub percent_sign: String,
|
||||
pub plus_sign: String,
|
||||
pub minus_sign: String,
|
||||
pub approximately_sign: String,
|
||||
pub exponential: String,
|
||||
pub superscripting_exponent: String,
|
||||
pub per_mille: String,
|
||||
pub infinity: String,
|
||||
pub nan: String,
|
||||
pub time_separator: String,
|
||||
}
|
||||
|
||||
// See: https://cldr.unicode.org/translation/number-currency-formats/number-and-currency-patterns
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct CurrencyFormats {
|
||||
pub standard: String,
|
||||
#[serde(rename = "standard-alphaNextToNumber")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub standard_alpha_next_to_number: Option<String>,
|
||||
#[serde(rename = "standard-noCurrency")]
|
||||
pub standard_no_currency: String,
|
||||
pub accounting: String,
|
||||
#[serde(rename = "accounting-alphaNextToNumber")]
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub accounting_alpha_next_to_number: Option<String>,
|
||||
#[serde(rename = "accounting-noCurrency")]
|
||||
pub accounting_no_currency: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DecimalFormats {
|
||||
pub standard: String,
|
||||
}
|
||||
|
||||
static LOCALES: Lazy<HashMap<String, Locale>> = Lazy::new(|| {
|
||||
serde_json::from_str(include_str!("locales.json")).expect("Failed parsing locale")
|
||||
});
|
||||
|
||||
pub fn get_locale(_id: &str) -> Result<&Locale, String> {
|
||||
// TODO: pass the locale once we implement locales in Rust
|
||||
let locale = LOCALES.get("en").ok_or("Invalid locale")?;
|
||||
Ok(locale)
|
||||
}
|
||||
|
||||
// TODO: Remove this function one we implement locales properly
|
||||
pub fn get_locale_fix(id: &str) -> Result<&Locale, String> {
|
||||
let locale = LOCALES.get(id).ok_or("Invalid locale")?;
|
||||
Ok(locale)
|
||||
}
|
||||
22
base/src/mock_time.rs
Normal file
22
base/src/mock_time.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
use std::cell::RefCell;
|
||||
|
||||
// main idea borrowed_mut from:
|
||||
// https://blog.iany.me/2019/03/how-to-mock-time-in-rust-tests-and-cargo-gotchas-we-met/
|
||||
// see also:
|
||||
// https://docs.rs/mock_instant/latest/mock_instant/
|
||||
|
||||
// FIXME: This should be November 12 1955 06:38, of course
|
||||
// (or maybe OCT 21, 2015 07:28)
|
||||
// 8 November 2022 12:13 Berlin time
|
||||
|
||||
thread_local! {
|
||||
static MOCK_TIME: RefCell<i64> = RefCell::new(1667906008578);
|
||||
}
|
||||
|
||||
pub fn get_milliseconds_since_epoch() -> i64 {
|
||||
MOCK_TIME.with(|t| *t.borrow())
|
||||
}
|
||||
|
||||
pub fn set_mock_time(time: i64) {
|
||||
MOCK_TIME.with(|cell| *cell.borrow_mut() = time);
|
||||
}
|
||||
1507
base/src/model.rs
Normal file
1507
base/src/model.rs
Normal file
File diff suppressed because it is too large
Load Diff
395
base/src/new_empty.rs
Normal file
395
base/src/new_empty.rs
Normal file
@@ -0,0 +1,395 @@
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
calc_result::Range,
|
||||
expressions::{
|
||||
lexer::LexerMode,
|
||||
parser::stringify::{rename_sheet_in_node, to_rc_format},
|
||||
parser::Parser,
|
||||
types::CellReferenceRC,
|
||||
},
|
||||
language::get_language,
|
||||
locale::get_locale,
|
||||
model::{get_milliseconds_since_epoch, Model, ParsedDefinedName},
|
||||
types::{Metadata, SheetState, Workbook, WorkbookSettings, Worksheet},
|
||||
utils::ParsedReference,
|
||||
};
|
||||
|
||||
pub use chrono_tz::Tz;
|
||||
|
||||
pub const APPLICATION: &str = "IronCalc Sheets";
|
||||
pub const APP_VERSION: &str = "10.0000";
|
||||
pub const IRONCALC_USER: &str = "IronCalc User";
|
||||
|
||||
/// Name cannot be blank, must be shorter than 31 characters.
|
||||
/// You can use all alphanumeric characters but not the following special characters:
|
||||
/// \ , / , * , ? , : , [ , ].
|
||||
fn is_valid_sheet_name(name: &str) -> bool {
|
||||
let invalid = ['\\', '/', '*', '?', ':', '[', ']'];
|
||||
return !name.is_empty() && name.chars().count() <= 31 && !name.contains(&invalid[..]);
|
||||
}
|
||||
|
||||
impl Model {
|
||||
/// Creates a new worksheet. Note that it does not check if the name or the sheet_id exists
|
||||
fn new_empty_worksheet(name: &str, sheet_id: u32) -> Worksheet {
|
||||
Worksheet {
|
||||
cols: vec![],
|
||||
rows: vec![],
|
||||
comments: vec![],
|
||||
dimension: "A1".to_string(),
|
||||
merge_cells: vec![],
|
||||
name: name.to_string(),
|
||||
shared_formulas: vec![],
|
||||
sheet_data: Default::default(),
|
||||
sheet_id,
|
||||
state: SheetState::Visible,
|
||||
color: Default::default(),
|
||||
frozen_columns: 0,
|
||||
frozen_rows: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_new_sheet_id(&self) -> u32 {
|
||||
let mut index = 1;
|
||||
let worksheets = &self.workbook.worksheets;
|
||||
for worksheet in worksheets {
|
||||
index = index.max(worksheet.sheet_id);
|
||||
}
|
||||
index + 1
|
||||
}
|
||||
|
||||
pub(crate) fn parse_formulas(&mut self) {
|
||||
self.parser.set_lexer_mode(LexerMode::R1C1);
|
||||
let worksheets = &self.workbook.worksheets;
|
||||
for worksheet in worksheets {
|
||||
let shared_formulas = &worksheet.shared_formulas;
|
||||
let cell_reference = &Some(CellReferenceRC {
|
||||
sheet: worksheet.get_name(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
});
|
||||
let mut parse_formula = Vec::new();
|
||||
for formula in shared_formulas {
|
||||
let t = self.parser.parse(formula, cell_reference);
|
||||
parse_formula.push(t);
|
||||
}
|
||||
self.parsed_formulas.push(parse_formula);
|
||||
}
|
||||
self.parser.set_lexer_mode(LexerMode::A1);
|
||||
}
|
||||
|
||||
pub(crate) fn parse_defined_names(&mut self) {
|
||||
let mut parsed_defined_names = HashMap::new();
|
||||
for defined_name in &self.workbook.defined_names {
|
||||
let parsed_defined_name_formula = if let Ok(reference) =
|
||||
ParsedReference::parse_reference_formula(
|
||||
None,
|
||||
&defined_name.formula,
|
||||
&self.locale,
|
||||
|name| self.get_sheet_index_by_name(name),
|
||||
) {
|
||||
match reference {
|
||||
ParsedReference::CellReference(cell_reference) => {
|
||||
ParsedDefinedName::CellReference(cell_reference)
|
||||
}
|
||||
ParsedReference::Range(left, right) => {
|
||||
ParsedDefinedName::RangeReference(Range { left, right })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ParsedDefinedName::InvalidDefinedNameFormula
|
||||
};
|
||||
|
||||
let local_sheet_index = if let Some(sheet_id) = defined_name.sheet_id {
|
||||
if let Some(sheet_index) = self.get_sheet_index_by_sheet_id(sheet_id) {
|
||||
Some(sheet_index)
|
||||
} else {
|
||||
// TODO: Error: Sheet with given sheet_id not found.
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
parsed_defined_names.insert(
|
||||
(local_sheet_index, defined_name.name.to_lowercase()),
|
||||
parsed_defined_name_formula,
|
||||
);
|
||||
}
|
||||
|
||||
self.parsed_defined_names = parsed_defined_names;
|
||||
}
|
||||
|
||||
// Reparses all formulas and defined names
|
||||
fn reset_parsed_structures(&mut self) {
|
||||
self.parser
|
||||
.set_worksheets(self.workbook.get_worksheet_names());
|
||||
self.parsed_formulas = vec![];
|
||||
self.parse_formulas();
|
||||
self.parsed_defined_names = HashMap::new();
|
||||
self.parse_defined_names();
|
||||
self.evaluate();
|
||||
}
|
||||
|
||||
/// Adds a sheet with a automatically generated name
|
||||
pub fn new_sheet(&mut self) {
|
||||
// First we find a name
|
||||
|
||||
// TODO: When/if we support i18n the name could depend on the locale
|
||||
let base_name = "Sheet";
|
||||
let base_name_uppercase = base_name.to_uppercase();
|
||||
let mut index = 1;
|
||||
while self
|
||||
.workbook
|
||||
.get_worksheet_names()
|
||||
.iter()
|
||||
.map(|s| s.to_uppercase())
|
||||
.any(|x| x == format!("{}{}", base_name_uppercase, index))
|
||||
{
|
||||
index += 1;
|
||||
}
|
||||
let sheet_name = format!("{}{}", base_name, index);
|
||||
// Now we need a sheet_id
|
||||
let sheet_id = self.get_new_sheet_id();
|
||||
let worksheet = Model::new_empty_worksheet(&sheet_name, sheet_id);
|
||||
self.workbook.worksheets.push(worksheet);
|
||||
self.reset_parsed_structures();
|
||||
}
|
||||
|
||||
/// Inserts a sheet with a particular index
|
||||
/// Fails if a worksheet with that name already exists or the name is invalid
|
||||
/// Fails if the index is too large
|
||||
pub fn insert_sheet(
|
||||
&mut self,
|
||||
sheet_name: &str,
|
||||
sheet_index: u32,
|
||||
sheet_id: Option<u32>,
|
||||
) -> Result<(), String> {
|
||||
if !is_valid_sheet_name(sheet_name) {
|
||||
return Err(format!("Invalid name for a sheet: '{}'", sheet_name));
|
||||
}
|
||||
if self
|
||||
.workbook
|
||||
.get_worksheet_names()
|
||||
.iter()
|
||||
.map(|s| s.to_uppercase())
|
||||
.any(|x| x == sheet_name.to_uppercase())
|
||||
{
|
||||
return Err("A worksheet already exists with that name".to_string());
|
||||
}
|
||||
let sheet_id = match sheet_id {
|
||||
Some(id) => id,
|
||||
None => self.get_new_sheet_id(),
|
||||
};
|
||||
let worksheet = Model::new_empty_worksheet(sheet_name, sheet_id);
|
||||
if sheet_index as usize > self.workbook.worksheets.len() {
|
||||
return Err("Sheet index out of range".to_string());
|
||||
}
|
||||
self.workbook
|
||||
.worksheets
|
||||
.insert(sheet_index as usize, worksheet);
|
||||
self.reset_parsed_structures();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Adds a sheet with a specific name
|
||||
/// Fails if a worksheet with that name already exists or the name is invalid
|
||||
pub fn add_sheet(&mut self, sheet_name: &str) -> Result<(), String> {
|
||||
self.insert_sheet(sheet_name, self.workbook.worksheets.len() as u32, None)
|
||||
}
|
||||
|
||||
/// Renames a sheet and updates all existing references to that sheet.
|
||||
/// It can fail if:
|
||||
/// * The original sheet does not exists
|
||||
/// * The target sheet already exists
|
||||
/// * The target sheet name is invalid
|
||||
pub fn rename_sheet(&mut self, old_name: &str, new_name: &str) -> Result<(), String> {
|
||||
if let Some(sheet_index) = self.get_sheet_index_by_name(old_name) {
|
||||
return self.rename_sheet_by_index(sheet_index, new_name);
|
||||
}
|
||||
Err(format!("Could not find sheet {}", old_name))
|
||||
}
|
||||
|
||||
/// Renames a sheet and updates all existing references to that sheet.
|
||||
/// It can fail if:
|
||||
/// * The original index is too large
|
||||
/// * The target sheet name already exists
|
||||
/// * The target sheet name is invalid
|
||||
pub fn rename_sheet_by_index(
|
||||
&mut self,
|
||||
sheet_index: u32,
|
||||
new_name: &str,
|
||||
) -> Result<(), String> {
|
||||
if !is_valid_sheet_name(new_name) {
|
||||
return Err(format!("Invalid name for a sheet: '{}'", new_name));
|
||||
}
|
||||
if self.get_sheet_index_by_name(new_name).is_some() {
|
||||
return Err(format!("Sheet already exists: '{}'", new_name));
|
||||
}
|
||||
let worksheets = &self.workbook.worksheets;
|
||||
let sheet_count = worksheets.len() as u32;
|
||||
if sheet_index >= sheet_count {
|
||||
return Err("Sheet index out of bounds".to_string());
|
||||
}
|
||||
// Parse all formulas with the old name
|
||||
// All internal formulas are R1C1
|
||||
self.parser.set_lexer_mode(LexerMode::R1C1);
|
||||
// We use iter because the default would be a mut_iter and we don't need a mutable reference
|
||||
let worksheets = &mut self.workbook.worksheets;
|
||||
for worksheet in worksheets {
|
||||
let cell_reference = &Some(CellReferenceRC {
|
||||
sheet: worksheet.get_name(),
|
||||
row: 1,
|
||||
column: 1,
|
||||
});
|
||||
let mut formulas = Vec::new();
|
||||
for formula in &worksheet.shared_formulas {
|
||||
let mut t = self.parser.parse(formula, cell_reference);
|
||||
rename_sheet_in_node(&mut t, sheet_index, new_name);
|
||||
formulas.push(to_rc_format(&t));
|
||||
}
|
||||
worksheet.shared_formulas = formulas;
|
||||
}
|
||||
// Se the mode back to A1
|
||||
self.parser.set_lexer_mode(LexerMode::A1);
|
||||
// Update the name of the worksheet
|
||||
let worksheets = &mut self.workbook.worksheets;
|
||||
worksheets[sheet_index as usize].set_name(new_name);
|
||||
self.reset_parsed_structures();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a sheet by index. Fails if:
|
||||
/// * The sheet does not exists
|
||||
/// * It is the last sheet
|
||||
pub fn delete_sheet(&mut self, sheet_index: u32) -> Result<(), String> {
|
||||
let worksheets = &self.workbook.worksheets;
|
||||
let sheet_count = worksheets.len() as u32;
|
||||
if sheet_count == 1 {
|
||||
return Err("Cannot delete only sheet".to_string());
|
||||
};
|
||||
if sheet_index > sheet_count {
|
||||
return Err("Sheet index too large".to_string());
|
||||
}
|
||||
self.workbook.worksheets.remove(sheet_index as usize);
|
||||
self.reset_parsed_structures();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes a sheet by name. Fails if:
|
||||
/// * The sheet does not exists
|
||||
/// * It is the last sheet
|
||||
pub fn delete_sheet_by_name(&mut self, name: &str) -> Result<(), String> {
|
||||
if let Some(sheet_index) = self.get_sheet_index_by_name(name) {
|
||||
self.delete_sheet(sheet_index)
|
||||
} else {
|
||||
Err("Sheet not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Deletes a sheet by sheet_id. Fails if:
|
||||
/// * The sheet by sheet_id does not exists
|
||||
/// * It is the last sheet
|
||||
pub fn delete_sheet_by_sheet_id(&mut self, sheet_id: u32) -> Result<(), String> {
|
||||
if let Some(sheet_index) = self.get_sheet_index_by_sheet_id(sheet_id) {
|
||||
self.delete_sheet(sheet_index)
|
||||
} else {
|
||||
Err("Sheet not found".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_sheet_index_by_sheet_id(&self, sheet_id: u32) -> Option<u32> {
|
||||
let worksheets = &self.workbook.worksheets;
|
||||
for (index, worksheet) in worksheets.iter().enumerate() {
|
||||
if worksheet.sheet_id == sheet_id {
|
||||
return Some(index as u32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Creates a new workbook with one empty sheet
|
||||
pub fn new_empty(name: &str, locale_id: &str, timezone: &str) -> Result<Model, String> {
|
||||
let tz: Tz = match &timezone.parse() {
|
||||
Ok(tz) => *tz,
|
||||
Err(_) => return Err(format!("Invalid timezone: {}", &timezone)),
|
||||
};
|
||||
let locale = match get_locale(locale_id) {
|
||||
Ok(l) => l.clone(),
|
||||
Err(_) => return Err(format!("Invalid locale: {}", locale_id)),
|
||||
};
|
||||
|
||||
let milliseconds = get_milliseconds_since_epoch();
|
||||
let seconds = milliseconds / 1000;
|
||||
let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) {
|
||||
Some(s) => s,
|
||||
None => return Err(format!("Invalid timestamp: {}", milliseconds)),
|
||||
};
|
||||
// "2020-08-06T21:20:53Z
|
||||
let now = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
|
||||
|
||||
// String versions of the locale are added here to simplify the serialize/deserialize logic
|
||||
let workbook = Workbook {
|
||||
shared_strings: vec![],
|
||||
defined_names: vec![],
|
||||
worksheets: vec![Model::new_empty_worksheet("Sheet1", 1)],
|
||||
styles: Default::default(),
|
||||
name: name.to_string(),
|
||||
settings: WorkbookSettings {
|
||||
tz: timezone.to_string(),
|
||||
locale: locale_id.to_string(),
|
||||
},
|
||||
metadata: Metadata {
|
||||
application: APPLICATION.to_string(),
|
||||
app_version: APP_VERSION.to_string(),
|
||||
creator: IRONCALC_USER.to_string(),
|
||||
last_modified_by: IRONCALC_USER.to_string(),
|
||||
created: now.clone(),
|
||||
last_modified: now,
|
||||
},
|
||||
tables: HashMap::new(),
|
||||
};
|
||||
let parsed_formulas = Vec::new();
|
||||
let worksheets = &workbook.worksheets;
|
||||
let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect();
|
||||
let parser = Parser::new(worksheet_names, HashMap::new());
|
||||
let cells = HashMap::new();
|
||||
|
||||
// FIXME: Add support for display languages
|
||||
let language = get_language("en").expect("").clone();
|
||||
|
||||
let mut model = Model {
|
||||
workbook,
|
||||
parsed_formulas,
|
||||
parsed_defined_names: HashMap::new(),
|
||||
parser,
|
||||
cells,
|
||||
locale,
|
||||
language,
|
||||
tz,
|
||||
};
|
||||
model.parse_formulas();
|
||||
Ok(model)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_is_valid_sheet_name() {
|
||||
assert!(is_valid_sheet_name("Sheet1"));
|
||||
assert!(is_valid_sheet_name("Zażółć gęślą jaźń"));
|
||||
|
||||
assert!(is_valid_sheet_name(" "));
|
||||
assert!(!is_valid_sheet_name(""));
|
||||
|
||||
assert!(is_valid_sheet_name("🙈"));
|
||||
|
||||
assert!(is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCD")); // 31
|
||||
assert!(!is_valid_sheet_name("AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDE")); // 32
|
||||
}
|
||||
}
|
||||
157
base/src/number_format.rs
Normal file
157
base/src/number_format.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use crate::{
|
||||
formatter::{self, format::Formatted},
|
||||
locale::get_locale,
|
||||
types::NumFmt,
|
||||
};
|
||||
|
||||
const DEFAULT_NUM_FMTS: &[&str] = &[
|
||||
"general",
|
||||
"0",
|
||||
"0.00",
|
||||
"#,## 0",
|
||||
"#,## 0.00",
|
||||
"$#,## 0; \\ - $#,## 0",
|
||||
"$#,## 0; [Red] \\ - $#,## 0",
|
||||
"$#,## 0.00; \\ - $#,## 0.00",
|
||||
"$#,## 0.00; [Red] \\ - $#,## 0.00",
|
||||
"0%",
|
||||
"0.00%",
|
||||
"0.00E + 00",
|
||||
"#?/?",
|
||||
"#?? / ??",
|
||||
"mm-dd-yy",
|
||||
"d-mmm-yy",
|
||||
"d-mmm",
|
||||
"mmm-yy",
|
||||
"h:mm AM / PM",
|
||||
"h:mm:ss AM / PM",
|
||||
"h:mm",
|
||||
"h:mm:ss",
|
||||
"m / d / yy h:mm",
|
||||
"#,## 0;()#,## 0)",
|
||||
"#,## 0; [Red]()#,## 0)",
|
||||
"#,## 0.00;()#,## 0.00)",
|
||||
"#,## 0.00; [Red]()#,## 0.00)",
|
||||
"_()$”*#,## 0.00 _); _()$”* \\()#,## 0.00\\); _()$”* - ?? _); _()@_)",
|
||||
"mm:ss",
|
||||
"[h]:mm:ss",
|
||||
"mmss .0",
|
||||
"## 0.0E + 0",
|
||||
"@",
|
||||
"[$ -404] e / m / d ",
|
||||
"m / d / yy",
|
||||
"[$ -404] e / m / d",
|
||||
"[$ -404] e / / d",
|
||||
"[$ -404] e / m / d",
|
||||
"t0",
|
||||
"t0.00",
|
||||
"t#,## 0",
|
||||
"t#,## 0.00",
|
||||
"t0%",
|
||||
"t0.00 %",
|
||||
"t#?/?",
|
||||
];
|
||||
|
||||
pub fn get_default_num_fmt_id(num_fmt: &str) -> Option<i32> {
|
||||
for (index, default_num_fmt) in DEFAULT_NUM_FMTS.iter().enumerate() {
|
||||
if default_num_fmt == &num_fmt {
|
||||
return Some(index as i32);
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn get_num_fmt(num_fmt_id: i32, num_fmts: &[NumFmt]) -> String {
|
||||
// Check if it defined
|
||||
for num_fmt in num_fmts {
|
||||
if num_fmt.num_fmt_id == num_fmt_id {
|
||||
return num_fmt.format_code.clone();
|
||||
}
|
||||
}
|
||||
// Return one of the default ones
|
||||
if num_fmt_id < DEFAULT_NUM_FMTS.len() as i32 {
|
||||
return DEFAULT_NUM_FMTS[num_fmt_id as usize].to_string();
|
||||
}
|
||||
// Return general
|
||||
DEFAULT_NUM_FMTS[0].to_string()
|
||||
}
|
||||
|
||||
pub fn get_new_num_fmt_index(num_fmts: &[NumFmt]) -> i32 {
|
||||
let mut index = DEFAULT_NUM_FMTS.len() as i32;
|
||||
let mut found = true;
|
||||
while found {
|
||||
found = false;
|
||||
for num_fmt in num_fmts {
|
||||
if num_fmt.num_fmt_id == index {
|
||||
found = true;
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
pub fn to_precision(value: f64, precision: usize) -> f64 {
|
||||
if value.is_infinite() || value.is_nan() {
|
||||
return value;
|
||||
}
|
||||
to_precision_str(value, precision)
|
||||
.parse::<f64>()
|
||||
.unwrap_or({
|
||||
// TODO: do this in a way that does not require a possible error
|
||||
0.0
|
||||
})
|
||||
}
|
||||
|
||||
/// It rounds a `f64` with `p` significant figures:
|
||||
/// ```
|
||||
/// use ironcalc_base::number_format;
|
||||
/// assert_eq!(number_format::to_precision(0.1+0.2, 15), 0.3);
|
||||
/// assert_eq!(number_format::to_excel_precision_str(0.1+0.2), "0.3");
|
||||
/// ```
|
||||
/// This intends to be equivalent to the js: `${parseFloat(value.toPrecision(precision)})`
|
||||
/// See ([ecma](https://tc39.es/ecma262/#sec-number.prototype.toprecision)).
|
||||
/// FIXME: There has to be a better algorithm :/
|
||||
pub fn to_excel_precision_str(value: f64) -> String {
|
||||
to_precision_str(value, 15)
|
||||
}
|
||||
pub fn to_precision_str(value: f64, precision: usize) -> String {
|
||||
if value.is_infinite() {
|
||||
return "inf".to_string();
|
||||
}
|
||||
if value.is_nan() {
|
||||
return "NaN".to_string();
|
||||
}
|
||||
let exponent = value.abs().log10().floor();
|
||||
let base = value / 10.0_f64.powf(exponent);
|
||||
let base = format!("{0:.1$}", base, precision - 1);
|
||||
let value = format!("{}e{}", base, exponent).parse::<f64>().unwrap_or({
|
||||
// TODO: do this in a way that does not require a possible error
|
||||
0.0
|
||||
});
|
||||
// I would love to use the std library. There is not a speed concern here
|
||||
// problem is it doesn't do the right thing
|
||||
// Also ryu is my favorite _modern_ algorithm
|
||||
let mut buffer = ryu::Buffer::new();
|
||||
let text = buffer.format(value);
|
||||
// The above algorithm converts 2 to 2.0 regrettably
|
||||
if let Some(stripped) = text.strip_suffix(".0") {
|
||||
return stripped.to_string();
|
||||
}
|
||||
text.to_string()
|
||||
}
|
||||
|
||||
pub fn format_number(value: f64, format_code: &str, locale: &str) -> Formatted {
|
||||
let locale = match get_locale(locale) {
|
||||
Ok(l) => l,
|
||||
Err(_) => {
|
||||
return Formatted {
|
||||
text: "#ERROR!".to_owned(),
|
||||
color: None,
|
||||
error: Some("Invalid locale".to_string()),
|
||||
}
|
||||
}
|
||||
};
|
||||
formatter::format::format_number(value, format_code, locale)
|
||||
}
|
||||
291
base/src/styles.rs
Normal file
291
base/src/styles.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
use crate::{
|
||||
model::{Model, Style},
|
||||
number_format::{get_default_num_fmt_id, get_new_num_fmt_index, get_num_fmt},
|
||||
types::{Border, CellStyles, CellXfs, Fill, Font, NumFmt, Styles},
|
||||
};
|
||||
|
||||
// TODO: Move Styles and all related types from crate::types here
|
||||
// Not doing it right now to not have conflicts with exporter branch
|
||||
impl Styles {
|
||||
fn get_font_index(&self, font: &Font) -> Option<i32> {
|
||||
for (font_index, item) in self.fonts.iter().enumerate() {
|
||||
if item == font {
|
||||
return Some(font_index as i32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
fn get_fill_index(&self, fill: &Fill) -> Option<i32> {
|
||||
for (fill_index, item) in self.fills.iter().enumerate() {
|
||||
if item == fill {
|
||||
return Some(fill_index as i32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
fn get_border_index(&self, border: &Border) -> Option<i32> {
|
||||
for (border_index, item) in self.borders.iter().enumerate() {
|
||||
if item == border {
|
||||
return Some(border_index as i32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
fn get_num_fmt_index(&self, format_code: &str) -> Option<i32> {
|
||||
if let Some(index) = get_default_num_fmt_id(format_code) {
|
||||
return Some(index);
|
||||
}
|
||||
for item in self.num_fmts.iter() {
|
||||
if item.format_code == format_code {
|
||||
return Some(item.num_fmt_id);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn create_new_style(&mut self, style: &Style) -> i32 {
|
||||
let font = &style.font;
|
||||
let font_id = if let Some(index) = self.get_font_index(font) {
|
||||
index
|
||||
} else {
|
||||
self.fonts.push(font.clone());
|
||||
self.fonts.len() as i32 - 1
|
||||
};
|
||||
let fill = &style.fill;
|
||||
let fill_id = if let Some(index) = self.get_fill_index(fill) {
|
||||
index
|
||||
} else {
|
||||
self.fills.push(fill.clone());
|
||||
self.fills.len() as i32 - 1
|
||||
};
|
||||
let border = &style.border;
|
||||
let border_id = if let Some(index) = self.get_border_index(border) {
|
||||
index
|
||||
} else {
|
||||
self.borders.push(border.clone());
|
||||
self.borders.len() as i32 - 1
|
||||
};
|
||||
let num_fmt = &style.num_fmt;
|
||||
let num_fmt_id;
|
||||
if let Some(index) = self.get_num_fmt_index(num_fmt) {
|
||||
num_fmt_id = index;
|
||||
} else {
|
||||
num_fmt_id = get_new_num_fmt_index(&self.num_fmts);
|
||||
self.num_fmts.push(NumFmt {
|
||||
format_code: num_fmt.to_string(),
|
||||
num_fmt_id,
|
||||
});
|
||||
}
|
||||
self.cell_xfs.push(CellXfs {
|
||||
xf_id: 0,
|
||||
num_fmt_id,
|
||||
font_id,
|
||||
fill_id,
|
||||
border_id,
|
||||
apply_number_format: false,
|
||||
apply_border: false,
|
||||
apply_alignment: false,
|
||||
apply_protection: false,
|
||||
apply_font: false,
|
||||
apply_fill: false,
|
||||
quote_prefix: style.quote_prefix,
|
||||
alignment: style.alignment.clone(),
|
||||
});
|
||||
self.cell_xfs.len() as i32 - 1
|
||||
}
|
||||
|
||||
pub fn get_style_index(&self, style: &Style) -> Option<i32> {
|
||||
for (index, cell_xf) in self.cell_xfs.iter().enumerate() {
|
||||
let border_id = cell_xf.border_id as usize;
|
||||
let fill_id = cell_xf.fill_id as usize;
|
||||
let font_id = cell_xf.font_id as usize;
|
||||
let num_fmt_id = cell_xf.num_fmt_id;
|
||||
let quote_prefix = cell_xf.quote_prefix;
|
||||
if style
|
||||
== &(Style {
|
||||
alignment: cell_xf.alignment.clone(),
|
||||
num_fmt: get_num_fmt(num_fmt_id, &self.num_fmts),
|
||||
fill: self.fills[fill_id].clone(),
|
||||
font: self.fonts[font_id].clone(),
|
||||
border: self.borders[border_id].clone(),
|
||||
quote_prefix,
|
||||
})
|
||||
{
|
||||
return Some(index as i32);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn get_style_index_or_create(&mut self, style: &Style) -> i32 {
|
||||
// Check if style exist. If so sets style cell number to that otherwise create a new style.
|
||||
if let Some(index) = self.get_style_index(style) {
|
||||
index
|
||||
} else {
|
||||
self.create_new_style(style)
|
||||
}
|
||||
}
|
||||
|
||||
/// Adds a named cell style from an existing index
|
||||
/// Fails if the named style already exists or if there is not a style with that index
|
||||
pub fn add_named_cell_style(
|
||||
&mut self,
|
||||
style_name: &str,
|
||||
style_index: i32,
|
||||
) -> Result<(), String> {
|
||||
if self.get_style_index_by_name(style_name).is_ok() {
|
||||
return Err("A style with that name already exists".to_string());
|
||||
}
|
||||
if self.cell_xfs.len() < style_index as usize {
|
||||
return Err("There is no style with that index".to_string());
|
||||
}
|
||||
let cell_style = CellStyles {
|
||||
name: style_name.to_string(),
|
||||
xf_id: style_index,
|
||||
builtin_id: 0,
|
||||
};
|
||||
self.cell_styles.push(cell_style);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Returns the index of the style or fails.
|
||||
// NB: this method is case sensitive
|
||||
pub fn get_style_index_by_name(&self, style_name: &str) -> Result<i32, String> {
|
||||
for cell_style in &self.cell_styles {
|
||||
if cell_style.name == style_name {
|
||||
return Ok(cell_style.xf_id);
|
||||
}
|
||||
}
|
||||
Err(format!("Style '{}' not found", style_name))
|
||||
}
|
||||
|
||||
pub fn create_named_style(&mut self, style_name: &str, style: &Style) -> Result<(), String> {
|
||||
let style_index = self.create_new_style(style);
|
||||
self.add_named_cell_style(style_name, style_index)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn get_style_with_quote_prefix(&mut self, index: i32) -> i32 {
|
||||
let mut style = self.get_style(index);
|
||||
style.quote_prefix = true;
|
||||
self.get_style_index_or_create(&style)
|
||||
}
|
||||
|
||||
pub(crate) fn get_style_with_format(&mut self, index: i32, num_fmt: &str) -> i32 {
|
||||
let mut style = self.get_style(index);
|
||||
style.num_fmt = num_fmt.to_string();
|
||||
self.get_style_index_or_create(&style)
|
||||
}
|
||||
|
||||
pub(crate) fn get_style_without_quote_prefix(&mut self, index: i32) -> i32 {
|
||||
let mut style = self.get_style(index);
|
||||
style.quote_prefix = false;
|
||||
self.get_style_index_or_create(&style)
|
||||
}
|
||||
|
||||
pub(crate) fn style_is_quote_prefix(&self, index: i32) -> bool {
|
||||
let cell_xf = &self.cell_xfs[index as usize];
|
||||
cell_xf.quote_prefix
|
||||
}
|
||||
|
||||
pub(crate) fn get_style(&self, index: i32) -> Style {
|
||||
let cell_xf = &self.cell_xfs[index as usize];
|
||||
|
||||
let border_id = cell_xf.border_id as usize;
|
||||
let fill_id = cell_xf.fill_id as usize;
|
||||
let font_id = cell_xf.font_id as usize;
|
||||
let num_fmt_id = cell_xf.num_fmt_id;
|
||||
let quote_prefix = cell_xf.quote_prefix;
|
||||
let alignment = cell_xf.alignment.clone();
|
||||
|
||||
Style {
|
||||
alignment,
|
||||
num_fmt: get_num_fmt(num_fmt_id, &self.num_fmts),
|
||||
fill: self.fills[fill_id].clone(),
|
||||
font: self.fonts[font_id].clone(),
|
||||
border: self.borders[border_id].clone(),
|
||||
quote_prefix,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Try to find a better spot for styles setters
|
||||
impl Model {
|
||||
pub fn set_cell_style(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
style: &Style,
|
||||
) -> Result<(), String> {
|
||||
let style_index = self.workbook.styles.get_style_index_or_create(style);
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(row, column, style_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn copy_cell_style(
|
||||
&mut self,
|
||||
source_cell: (u32, i32, i32),
|
||||
destination_cell: (u32, i32, i32),
|
||||
) -> Result<(), String> {
|
||||
let source_style_index = self
|
||||
.workbook
|
||||
.worksheet(source_cell.0)?
|
||||
.get_style(source_cell.1, source_cell.2);
|
||||
|
||||
self.workbook
|
||||
.worksheet_mut(destination_cell.0)?
|
||||
.set_cell_style(destination_cell.1, destination_cell.2, source_style_index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets the style "style_name" in cell
|
||||
pub fn set_cell_style_by_name(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
column: i32,
|
||||
style_name: &str,
|
||||
) -> Result<(), String> {
|
||||
let style_index = self.workbook.styles.get_style_index_by_name(style_name)?;
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_cell_style(row, column, style_index);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_sheet_style(&mut self, sheet: u32, style_name: &str) -> Result<(), String> {
|
||||
let style_index = self.workbook.styles.get_style_index_by_name(style_name)?;
|
||||
self.workbook.worksheet_mut(sheet)?.set_style(style_index)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_sheet_row_style(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
row: i32,
|
||||
style_name: &str,
|
||||
) -> Result<(), String> {
|
||||
let style_index = self.workbook.styles.get_style_index_by_name(style_name)?;
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_row_style(row, style_index)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_sheet_column_style(
|
||||
&mut self,
|
||||
sheet: u32,
|
||||
column: i32,
|
||||
style_name: &str,
|
||||
) -> Result<(), String> {
|
||||
let style_index = self.workbook.styles.get_style_index_by_name(style_name)?;
|
||||
self.workbook
|
||||
.worksheet_mut(sheet)?
|
||||
.set_column_style(column, style_index)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
6
base/src/test/engineering/mod.rs
Normal file
6
base/src/test/engineering/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
mod test_bessel;
|
||||
mod test_bit_operations;
|
||||
mod test_complex;
|
||||
mod test_convert;
|
||||
mod test_misc;
|
||||
mod test_number_basis;
|
||||
53
base/src/test/engineering/test_bessel.rs
Normal file
53
base/src/test/engineering/test_bessel.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn fn_besseli() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("B1", "=BESSELI()");
|
||||
model._set("B2", "=BESSELI(1,2, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_besselj() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("B1", "=BESSELJ()");
|
||||
model._set("B2", "=BESSELJ(1,2, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_besselk() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("B1", "=BESSELK()");
|
||||
model._set("B2", "=BESSELK(1,2, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_bessely() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("B1", "=BESSELY()");
|
||||
model._set("B2", "=BESSELY(1,2, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
99
base/src/test/engineering/test_bit_operations.rs
Normal file
99
base/src/test/engineering/test_bit_operations.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn fn_bitand() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BITAND(1,5)");
|
||||
model._set("A2", "=BITAND(13, 25");
|
||||
|
||||
model._set("B1", "=BITAND(1)");
|
||||
model._set("B2", "=BITAND(1, 2, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "1");
|
||||
assert_eq!(model._get_text("A2"), "9");
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_bitor() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BITOR(1, 5)");
|
||||
model._set("A2", "=BITOR(13, 10");
|
||||
|
||||
model._set("B1", "=BITOR(1)");
|
||||
model._set("B2", "=BITOR(1, 2, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "5");
|
||||
assert_eq!(model._get_text("A2"), "15");
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_bitxor() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BITXOR(1, 5)");
|
||||
model._set("A2", "=BITXOR(13, 25");
|
||||
|
||||
model._set("B1", "=BITXOR(1)");
|
||||
model._set("B2", "=BITXOR(1, 2, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "4");
|
||||
assert_eq!(model._get_text("A2"), "20");
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_bitlshift() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BITLSHIFT(4, 2)");
|
||||
model._set("A2", "=BITLSHIFT(13, 7");
|
||||
|
||||
model._set("B1", "=BITLSHIFT(1)");
|
||||
model._set("B2", "=BITLSHIFT(1, 2, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "16");
|
||||
assert_eq!(model._get_text("A2"), "1664");
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_bitrshift() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BITRSHIFT(4, 2)");
|
||||
model._set("A2", "=BITRSHIFT(13, 7");
|
||||
model._set("A3", "=BITRSHIFT(145, -3");
|
||||
|
||||
model._set("B1", "=BITRSHIFT(1)");
|
||||
model._set("B2", "=BITRSHIFT(1, 2, 3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "1");
|
||||
assert_eq!(model._get_text("A2"), "0");
|
||||
assert_eq!(model._get_text("A3"), "1160");
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
// Excel does not pass this test (g sheets does)
|
||||
#[test]
|
||||
fn fn_bitshift_overflow() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BITRSHIFT(12, -53)");
|
||||
model._set("A2", "=BITLSHIFT(12, 53)");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
}
|
||||
162
base/src/test/engineering/test_complex.rs
Normal file
162
base/src/test/engineering/test_complex.rs
Normal file
@@ -0,0 +1,162 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn fn_complex() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=COMPLEX(3, 4.5, "i")"#);
|
||||
model._set("A2", r#"=COMPLEX(3, -5)"#);
|
||||
model._set("A3", r#"=COMPLEX(0, 42, "j")"#);
|
||||
|
||||
model._set("B1", "=COMPLEX()");
|
||||
model._set("B2", r#"=COMPLEX(1,2, "i", 1)"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "3+4.5i");
|
||||
assert_eq!(model._get_text("A2"), "3-5i");
|
||||
assert_eq!(model._get_text("A3"), "42j");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imabs() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMABS("3+4i")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "5");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imaginary() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMAGINARY("3+4i")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imreal() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMREAL("3+4i")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imargument() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMARGUMENT("4+3i")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "0.643501109");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imconjugate() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMCONJUGATE("3+4i")"#);
|
||||
model._set("A2", r#"=IMCONJUGATE("12.7-32j")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "3-4i");
|
||||
assert_eq!(model._get_text("A2"), "12.7+32j");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imcos() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMCOS("4+3i")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "-6.58066304055116+7.58155274274654i");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imsin() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMSIN("4+3i")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "-7.61923172032141-6.548120040911i");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imaginary_misc() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMAGINARY("3.4i")"#);
|
||||
model._set("A2", r#"=IMAGINARY("-3.4")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "3.4");
|
||||
assert_eq!(model._get_text("A2"), "0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imcosh() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMCOSH("4+3i")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "-27.0349456030742+3.85115333481178i");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imcot() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMCOT("4+3i")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"0.0049011823943045-0.999266927805902i"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_imtan() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=IMTAN("4+3i")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(
|
||||
model._get_text("A1"),
|
||||
"0.00490825806749608+1.00070953606723i"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_power() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A2", r#"=IMPOWER("4+3i", 3)"#);
|
||||
model._set("A3", r#"=IMABS(IMSUB(IMPOWER("-i", -3), "-1"))<G1"#);
|
||||
model._set("A3", r#"=IMABS(IMSUB(IMPOWER("-1", 0.5), "i"))<G1"#);
|
||||
|
||||
model._set("A1", r#"=IMABS(IMSUB(B1, "-1"))<G1"#);
|
||||
model._set("B1", r#"=IMPOWER("i", 2)"#);
|
||||
|
||||
// small number
|
||||
model._set("G1", "0.0000001");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "TRUE");
|
||||
assert_eq!(model._get_text("A2"), "-44+117i");
|
||||
assert_eq!(model._get_text("A3"), "TRUE");
|
||||
assert_eq!(model._get_text("A3"), "TRUE");
|
||||
}
|
||||
35
base/src/test/engineering/test_convert.rs
Normal file
35
base/src/test/engineering/test_convert.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn fn_convert() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=CONVERT(1, "lbm", "kg")"#);
|
||||
model._set("A2", r#"=CONVERT(68, "F", "C")"#);
|
||||
model._set("A3", r#"=CONVERT(2.5, "ft", "sec")"#);
|
||||
model._set("A4", r#"=CONVERT(CONVERT(100,"ft","m"),"ft","m""#);
|
||||
|
||||
model._set("B1", "6");
|
||||
|
||||
model._set("A5", r#"=CONVERT(B1,"C","F")"#);
|
||||
model._set("A6", r#"=CONVERT(B1,"tsp","tbs")"#);
|
||||
model._set("A7", r#"=CONVERT(B1,"gal","l")"#);
|
||||
model._set("A8", r#"=CONVERT(B1,"mi","km")"#);
|
||||
model._set("A9", r#"=CONVERT(B1,"km","mi")"#);
|
||||
model._set("A10", r#"=CONVERT(B1,"in","ft")"#);
|
||||
model._set("A11", r#"=CONVERT(B1,"cm","in")"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "0.45359237");
|
||||
assert_eq!(model._get_text("A2"), "19.65");
|
||||
assert_eq!(model._get_text("A3"), "#N/A");
|
||||
assert_eq!(model._get_text("A4"), "9.290304");
|
||||
|
||||
assert_eq!(model._get_text("A5"), "42.8");
|
||||
assert_eq!(model._get_text("A6"), "2");
|
||||
assert_eq!(model._get_text("A7"), "22.712470704"); //22.71741274");
|
||||
assert_eq!(model._get_text("A8"), "9.656064");
|
||||
assert_eq!(model._get_text("A9"), "3.728227153");
|
||||
assert_eq!(model._get_text("A10"), "0.5");
|
||||
assert_eq!(model._get_text("A11"), "2.362204724");
|
||||
}
|
||||
66
base/src/test/engineering/test_misc.rs
Normal file
66
base/src/test/engineering/test_misc.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn fn_getstep() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=GESTEP(7, 4.6)");
|
||||
model._set("A2", "=GESTEP(45, 45)");
|
||||
model._set("A3", "=GESTEP(-7, -6)");
|
||||
model._set("A4", "=GESTEP(0.1)");
|
||||
model._set("A5", "=GESTEP(-0.1)");
|
||||
|
||||
model._set("B1", "=GESTEP()");
|
||||
model._set("B2", "=GESTEP(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"1");
|
||||
assert_eq!(model._get_text("A2"), *"1");
|
||||
assert_eq!(model._get_text("A3"), *"0");
|
||||
assert_eq!(model._get_text("A4"), *"1");
|
||||
assert_eq!(model._get_text("A5"), *"0");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_delta() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DELTA(7, 7)");
|
||||
model._set("A2", "=DELTA(-7, -7)");
|
||||
model._set("A3", "=DELTA(-7, 7)");
|
||||
model._set("A4", "=DELTA(5, 0.5)");
|
||||
model._set("A5", "=DELTA(-0, 0)");
|
||||
|
||||
model._set("B1", "=DELTA()");
|
||||
model._set("B2", "=DELTA(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"1");
|
||||
assert_eq!(model._get_text("A2"), *"1");
|
||||
assert_eq!(model._get_text("A3"), *"0");
|
||||
assert_eq!(model._get_text("A4"), *"0");
|
||||
assert_eq!(model._get_text("A5"), *"1");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_delta_misc() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=3+1e-16");
|
||||
model._set("A2", "=3");
|
||||
model._set("A3", "=3+1e-15");
|
||||
model._set("B1", "=DELTA(A1, A2)");
|
||||
model._set("B2", "=DELTA(A1, A3)");
|
||||
|
||||
model._set("B1", "1");
|
||||
model._set("B2", "0");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"1");
|
||||
}
|
||||
345
base/src/test/engineering/test_number_basis.rs
Normal file
345
base/src/test/engineering/test_number_basis.rs
Normal file
@@ -0,0 +1,345 @@
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn fn_bin2dec() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BIN2DEC(1100100)");
|
||||
model._set("A2", "=BIN2DEC(1111111111)");
|
||||
|
||||
model._set("B1", "=BIN2DEC()");
|
||||
model._set("B2", "=BIN2DEC(1,2)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "100");
|
||||
assert_eq!(model._get_text("A2"), "-1");
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_bin2hex() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BIN2HEX(11111011, 4)");
|
||||
model._set("A2", "=BIN2HEX(1110)");
|
||||
model._set("A3", "=BIN2HEX(1111111111)");
|
||||
model._set("A4", "=BIN2HEX(1100011011)");
|
||||
|
||||
model._set("B1", "=BIN2HEX()");
|
||||
model._set("B2", "=BIN2HEX(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "00FB");
|
||||
assert_eq!(model._get_text("A2"), "E");
|
||||
assert_eq!(model._get_text("A3"), "FFFFFFFFFF");
|
||||
assert_eq!(model._get_text("A4"), "FFFFFFFF1B");
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_bin2oct() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BIN2OCT(11111011, 4)");
|
||||
model._set("A2", "=BIN2OCT(1110)");
|
||||
model._set("A3", "=BIN2OCT(1111111111)");
|
||||
model._set("A4", "=BIN2OCT(1100011011)");
|
||||
|
||||
model._set("B1", "=BIN2OCT()");
|
||||
model._set("B2", "=BIN2OCT(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "0373");
|
||||
assert_eq!(model._get_text("A2"), "16");
|
||||
assert_eq!(model._get_text("A3"), "7777777777");
|
||||
assert_eq!(model._get_text("A4"), "7777777433");
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_dec2bin() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DEC2BIN(9, 4)");
|
||||
model._set("A2", "=DEC2BIN(-100)");
|
||||
model._set("A3", "=DEC2BIN(-1)");
|
||||
model._set("A4", "=DEC2BIN(0, 3)");
|
||||
|
||||
model._set("B1", "=DEC2BIN()");
|
||||
model._set("B2", "=DEC2BIN(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "1001");
|
||||
assert_eq!(model._get_text("A2"), "1110011100");
|
||||
assert_eq!(model._get_text("A3"), "1111111111");
|
||||
assert_eq!(model._get_text("A4"), "000");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_dec2hex() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DEC2HEX(100, 4)");
|
||||
model._set("A2", "=DEC2HEX(-54)");
|
||||
model._set("A3", "=DEC2HEX(28)");
|
||||
model._set("A4", "=DEC2HEX(64, 1)");
|
||||
|
||||
model._set("B1", "=DEC2HEX()");
|
||||
model._set("B2", "=DEC2HEX(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "0064");
|
||||
assert_eq!(model._get_text("A2"), "FFFFFFFFCA");
|
||||
assert_eq!(model._get_text("A3"), "1C");
|
||||
assert_eq!(model._get_text("A4"), "#NUM!");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_dec2oct() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DEC2OCT(58, 3)");
|
||||
model._set("A2", "=DEC2OCT(-100)");
|
||||
|
||||
model._set("B1", "=DEC2OCT()");
|
||||
model._set("B2", "=DEC2OCT(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "072");
|
||||
assert_eq!(model._get_text("A2"), "7777777634");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_hex2bin() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=HEX2BIN("F", 8)"#);
|
||||
model._set("A2", r#"=HEX2BIN("B7")"#);
|
||||
model._set("A3", r#"=HEX2BIN("FFFFFFFFFF")"#);
|
||||
|
||||
model._set("B1", "=HEX2BIN()");
|
||||
model._set("B2", "=HEX2BIN(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "00001111");
|
||||
assert_eq!(model._get_text("A2"), "10110111");
|
||||
assert_eq!(model._get_text("A3"), "1111111111");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_hex2dec() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=HEX2DEC("A5")"#);
|
||||
model._set("A2", r#"=HEX2DEC("FFFFFFFF5B")"#);
|
||||
model._set("A3", r#"=HEX2DEC("3DA408B9")"#);
|
||||
model._set("A4", r#"=HEX2DEC("FE")"#);
|
||||
|
||||
model._set("B1", "=HEX2DEC()");
|
||||
model._set("B2", "=HHEX2DEC(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "165");
|
||||
assert_eq!(model._get_text("A2"), "-165");
|
||||
assert_eq!(model._get_text("A3"), "1034160313");
|
||||
assert_eq!(model._get_text("A4"), "254");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_hex2oct() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=HEX2OCT("F", 3)"#);
|
||||
model._set("A2", r#"=HEX2OCT("3B4E")"#);
|
||||
model._set("A3", r#"=HEX2OCT("FFFFFFFF00")"#);
|
||||
|
||||
model._set("B1", "=HEX2OCT()");
|
||||
model._set("B2", "=HEX2OCT(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "017");
|
||||
assert_eq!(model._get_text("A2"), "35516");
|
||||
assert_eq!(model._get_text("A3"), "7777777400");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_oct2bin() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=OCT2BIN(3, 3)"#);
|
||||
model._set("A2", r#"=OCT2BIN(7777777000)"#);
|
||||
|
||||
// bounds
|
||||
model._set("G1", r#"=OCT2BIN(777)"#);
|
||||
model._set("G2", r#"=OCT2BIN(778)"#);
|
||||
|
||||
model._set("B1", "=OCT2BIN()");
|
||||
model._set("B2", "=OCT2BIN(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "011");
|
||||
assert_eq!(model._get_text("A2"), "1000000000");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
|
||||
assert_eq!(model._get_text("G1"), "111111111");
|
||||
assert_eq!(model._get_text("G2"), "#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_oct2dec() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=OCT2DEC(54)"#);
|
||||
model._set("A2", r#"=OCT2DEC(7777777533)"#);
|
||||
|
||||
model._set("B1", "=OCT2DEC()");
|
||||
model._set("B2", "=OCT2DEC(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "44");
|
||||
assert_eq!(model._get_text("A2"), "-165");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_oct2hex() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=OCT2HEX(100, 4)"#);
|
||||
model._set("A2", r#"=OCT2HEX(7777777533)"#);
|
||||
|
||||
model._set("B1", "=OCT2HEX()");
|
||||
model._set("B2", "=OCT2HEX(1,2,3)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), "0040");
|
||||
assert_eq!(model._get_text("A2"), "FFFFFFFF5B");
|
||||
|
||||
assert_eq!(model._get_text("B1"), *"#ERROR!");
|
||||
assert_eq!(model._get_text("B2"), *"#ERROR!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_bin2hex_misc() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BIN2HEX(1100011011, -2)");
|
||||
model._set("A2", "=BIN2HEX(1100011011, 11)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_bin2oct_misc() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=BIN2OCT(1100011011, -2)");
|
||||
model._set("A2", "=BIN2OCT(1100011011, 11)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_dec2oct_misc() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DEC2OCT(-1213, 1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"7777775503");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_dec2bin_misc() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=DEC2BIN(-511, 4)");
|
||||
model._set("A2", "=DEC2BIN(TRUE, -1)");
|
||||
model._set("A3", "=DEC2OCT(TRUE, -1)");
|
||||
model._set("A4", "=DEC2HEX(TRUE, -1)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"1000000001");
|
||||
// Note Excel here return #NUM! instead
|
||||
assert_eq!(model._get_text("A2"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("A3"), *"#VALUE!");
|
||||
assert_eq!(model._get_text("A4"), *"#VALUE!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_hex2whatever_misc() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=HEX2BIN(TRUE, 4)"#);
|
||||
model._set("A2", r#"=HEX2DEC(TRUE, 4)"#);
|
||||
model._set("A3", r#"=HEX2OCT(TRUE, 4)"#);
|
||||
|
||||
model.evaluate();
|
||||
// Note Excel here return #VALUE! instead
|
||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_oct2whatever_misc() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=OCT2BIN(TRUE, 4)"#);
|
||||
model._set("A2", r#"=OCT2DEC(TRUE, 4)"#);
|
||||
model._set("A3", r#"=OCT2HEX(TRUE, 4)"#);
|
||||
|
||||
model.evaluate();
|
||||
// Note Excel here return #VALUE! instead
|
||||
assert_eq!(model._get_text("A1"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A2"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fn_oct2dec_misc() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", r#"=OCT2DEC(777)"#);
|
||||
model._set("A2", r#"=OCT2DEC("777")"#);
|
||||
model._set("A3", r#"=OCT2DEC("-1")"#);
|
||||
model._set("A4", r#"=OCT2BIN("-1")"#);
|
||||
model._set("A5", r#"=OCT2HEX("-1")"#);
|
||||
model._set("A6", r#"=OCT2DEC(4000000000)"#);
|
||||
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_text("A1"), *"511");
|
||||
assert_eq!(model._get_text("A1"), *"511");
|
||||
|
||||
assert_eq!(model._get_text("A3"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A4"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A5"), *"#NUM!");
|
||||
assert_eq!(model._get_text("A6"), *"-536870912");
|
||||
}
|
||||
51
base/src/test/mod.rs
Normal file
51
base/src/test/mod.rs
Normal file
@@ -0,0 +1,51 @@
|
||||
mod test_actions;
|
||||
mod test_binary_search;
|
||||
mod test_cell;
|
||||
mod test_circular_references;
|
||||
mod test_column_width;
|
||||
mod test_criteria;
|
||||
mod test_currency;
|
||||
mod test_date_and_time;
|
||||
mod test_error_propagation;
|
||||
mod test_fn_average;
|
||||
mod test_fn_averageifs;
|
||||
mod test_fn_choose;
|
||||
mod test_fn_concatenate;
|
||||
mod test_fn_count;
|
||||
mod test_fn_exact;
|
||||
mod test_fn_financial;
|
||||
mod test_fn_if;
|
||||
mod test_fn_maxifs;
|
||||
mod test_fn_minifs;
|
||||
mod test_fn_product;
|
||||
mod test_fn_rept;
|
||||
mod test_fn_sum;
|
||||
mod test_fn_sumifs;
|
||||
mod test_fn_textbefore;
|
||||
mod test_fn_textjoin;
|
||||
mod test_forward_references;
|
||||
mod test_frozen_rows_columns;
|
||||
mod test_general;
|
||||
mod test_math;
|
||||
mod test_metadata;
|
||||
mod test_model_delete_cell;
|
||||
mod test_model_is_empty_cell;
|
||||
mod test_model_set_cell_empty;
|
||||
mod test_move_formula;
|
||||
mod test_quote_prefix;
|
||||
mod test_set_user_input;
|
||||
mod test_sheet_markup;
|
||||
mod test_sheets;
|
||||
mod test_styles;
|
||||
mod test_trigonometric;
|
||||
mod test_worksheet;
|
||||
pub(crate) mod util;
|
||||
|
||||
mod engineering;
|
||||
mod test_fn_offset;
|
||||
mod test_number_format;
|
||||
|
||||
mod test_escape_quotes;
|
||||
mod test_fn_type;
|
||||
mod test_percentage;
|
||||
mod test_today;
|
||||
287
base/src/test/test_actions.rs
Normal file
287
base/src/test/test_actions.rs
Normal file
@@ -0,0 +1,287 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::LAST_COLUMN;
|
||||
use crate::model::Model;
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_insert_columns() {
|
||||
let mut model = new_empty_model();
|
||||
// We populate cells A1 to C1
|
||||
model._set("A1", "1");
|
||||
model._set("B1", "2");
|
||||
model._set("C1", "=B1*2");
|
||||
|
||||
model._set("F1", "=B1");
|
||||
|
||||
model._set("L11", "300");
|
||||
model._set("M11", "=L11*5");
|
||||
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("C1"), *"4");
|
||||
|
||||
// Let's insert 5 columns in column F (6)
|
||||
let r = model.insert_columns(0, 6, 5);
|
||||
assert!(r.is_ok());
|
||||
model.evaluate();
|
||||
|
||||
// Check F1 is now empty
|
||||
assert!(model.is_empty_cell(0, 1, 6).unwrap());
|
||||
|
||||
// The old F1 is K1
|
||||
assert_eq!(model._get_formula("K1"), *"=B1");
|
||||
|
||||
// L11 and M11 are Q11 and R11
|
||||
assert_eq!(model._get_text("Q11"), *"300");
|
||||
assert_eq!(model._get_formula("R11"), *"=Q11*5");
|
||||
|
||||
assert_eq!(model._get_formula("C1"), "=B1*2");
|
||||
assert_eq!(model._get_text("A1"), "1");
|
||||
|
||||
// inserting a negative number of columns fails:
|
||||
let r = model.insert_columns(0, 6, -5);
|
||||
assert!(r.is_err());
|
||||
let r = model.insert_columns(0, 6, -5);
|
||||
assert!(r.is_err());
|
||||
|
||||
// If you have data at the very ebd it fails
|
||||
model._set("XFC12", "300");
|
||||
let r = model.insert_columns(0, 6, 5);
|
||||
assert!(r.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_rows() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("C4", "3");
|
||||
model._set("C5", "7");
|
||||
model._set("C6", "=C5");
|
||||
|
||||
model._set("H11", "=C4");
|
||||
|
||||
model._set("R10", "=C6");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// Let's insert 5 rows in row 6
|
||||
let r = model.insert_rows(0, 6, 5);
|
||||
assert!(r.is_ok());
|
||||
model.evaluate();
|
||||
|
||||
// Check C6 is now empty
|
||||
assert!(model.is_empty_cell(0, 6, 3).unwrap());
|
||||
|
||||
// Old C6 is now C11
|
||||
assert_eq!(model._get_formula("C11"), *"=C5");
|
||||
assert_eq!(model._get_formula("H16"), *"=C4");
|
||||
|
||||
assert_eq!(model._get_formula("R15"), *"=C11");
|
||||
assert_eq!(model._get_text("C4"), *"3");
|
||||
assert_eq!(model._get_text("C5"), *"7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_insert_rows_styles() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
assert!(
|
||||
(21.0 - model.workbook.worksheet(0).unwrap().row_height(10).unwrap()).abs() < f64::EPSILON
|
||||
);
|
||||
// sets height 42 in row 10
|
||||
model
|
||||
.workbook
|
||||
.worksheet_mut(0)
|
||||
.unwrap()
|
||||
.set_row_height(10, 42.0)
|
||||
.unwrap();
|
||||
assert!(
|
||||
(42.0 - model.workbook.worksheet(0).unwrap().row_height(10).unwrap()).abs() < f64::EPSILON
|
||||
);
|
||||
|
||||
// Let's insert 5 rows in row 3
|
||||
let r = model.insert_rows(0, 3, 5);
|
||||
assert!(r.is_ok());
|
||||
|
||||
// Row 10 has the default height
|
||||
assert!(
|
||||
(21.0 - model.workbook.worksheet(0).unwrap().row_height(10).unwrap()).abs() < f64::EPSILON
|
||||
);
|
||||
|
||||
// Row 10 is now row 15
|
||||
assert!(
|
||||
(42.0 - model.workbook.worksheet(0).unwrap().row_height(15).unwrap()).abs() < f64::EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_rows_styles() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
assert!(
|
||||
(21.0 - model.workbook.worksheet(0).unwrap().row_height(10).unwrap()).abs() < f64::EPSILON
|
||||
);
|
||||
// sets height 42 in row 10
|
||||
model
|
||||
.workbook
|
||||
.worksheet_mut(0)
|
||||
.unwrap()
|
||||
.set_row_height(10, 42.0)
|
||||
.unwrap();
|
||||
assert!(
|
||||
(42.0 - model.workbook.worksheet(0).unwrap().row_height(10).unwrap()).abs() < f64::EPSILON
|
||||
);
|
||||
|
||||
// Let's delete 5 rows in row 3 (3-8)
|
||||
let r = model.delete_rows(0, 3, 5);
|
||||
assert!(r.is_ok());
|
||||
|
||||
// Row 10 has the default height
|
||||
assert!(
|
||||
(21.0 - model.workbook.worksheet(0).unwrap().row_height(10).unwrap()).abs() < f64::EPSILON
|
||||
);
|
||||
|
||||
// Row 10 is now row 5
|
||||
assert!(
|
||||
(42.0 - model.workbook.worksheet(0).unwrap().row_height(5).unwrap()).abs() < f64::EPSILON
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_columns() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("C4", "3");
|
||||
model._set("D4", "7");
|
||||
model._set("E4", "=D4");
|
||||
model._set("F4", "=C4");
|
||||
|
||||
model._set("H11", "=D4");
|
||||
|
||||
model._set("R10", "=C6");
|
||||
|
||||
model._set("M5", "300");
|
||||
model._set("N5", "=M5*6");
|
||||
|
||||
model._set("A1", "=SUM(M5:N5)");
|
||||
model._set("A2", "=SUM(C4:M4)");
|
||||
model._set("A3", "=SUM(E4:M4)");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// We delete columns D and E
|
||||
let r = model.delete_columns(0, 4, 2);
|
||||
assert!(r.is_ok());
|
||||
model.evaluate();
|
||||
|
||||
// Old H11 will be F11 and contain =#REF!
|
||||
assert_eq!(model._get_formula("F11"), *"=#REF!");
|
||||
|
||||
// Old F4 will be D4 now
|
||||
assert_eq!(model._get_formula("D4"), *"=C4");
|
||||
|
||||
// Old N5 will be L5
|
||||
assert_eq!(model._get_formula("L5"), *"=K5*6");
|
||||
|
||||
// Range in A1 is displaced correctly
|
||||
assert_eq!(model._get_formula("A1"), *"=SUM(K5:L5)");
|
||||
|
||||
// Note that range in A2 would contain some of the deleted cells
|
||||
// A long as the borders of the range are not included that's ok.
|
||||
assert_eq!(model._get_formula("A2"), *"=SUM(C4:K4)");
|
||||
|
||||
// FIXME: In Excel this would be (lower limit won't change)
|
||||
// assert_eq!(model._get_formula("A3"), *"=SUM(E4:K4)");
|
||||
assert_eq!(model._get_formula("A3"), *"=SUM(#REF!:K4)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_rows() {
|
||||
let mut model = new_empty_model();
|
||||
|
||||
model._set("C4", "4");
|
||||
model._set("C5", "5");
|
||||
model._set("C6", "6");
|
||||
model._set("C7", "=C6*2");
|
||||
|
||||
model._set("C72", "=C1*3");
|
||||
|
||||
model.evaluate();
|
||||
|
||||
// We delete rows 5, 6
|
||||
let r = model.delete_rows(0, 5, 2);
|
||||
assert!(r.is_ok());
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_formula("C5"), *"=#REF!*2");
|
||||
assert_eq!(model._get_formula("C70"), *"=C1*3");
|
||||
}
|
||||
|
||||
// E F G H I J K
|
||||
// 3 1 1 2
|
||||
// 4 2 5 8
|
||||
// -2 3 6 7
|
||||
fn populate_table(model: &mut Model) {
|
||||
model._set("G1", "3");
|
||||
model._set("H1", "1");
|
||||
model._set("I1", "1");
|
||||
model._set("J1", "2");
|
||||
|
||||
model._set("G2", "4");
|
||||
model._set("H2", "2");
|
||||
model._set("I2", "5");
|
||||
model._set("J2", "8");
|
||||
|
||||
model._set("G3", "-2");
|
||||
model._set("H3", "3");
|
||||
model._set("I3", "6");
|
||||
model._set("J3", "7");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_move_column_right() {
|
||||
let mut model = new_empty_model();
|
||||
populate_table(&mut model);
|
||||
model._set("E3", "=G3");
|
||||
model._set("E4", "=H3");
|
||||
model._set("E5", "=SUM(G3:J7)");
|
||||
model._set("E6", "=SUM(G3:G7)");
|
||||
model._set("E7", "=SUM(H3:H7)");
|
||||
model.evaluate();
|
||||
|
||||
// Wee swap column G with column H
|
||||
let result = model.move_column_action(0, 7, 1);
|
||||
assert!(result.is_ok());
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_formula("E3"), "=H3");
|
||||
assert_eq!(model._get_formula("E4"), "=G3");
|
||||
assert_eq!(model._get_formula("E5"), "=SUM(H3:J7)");
|
||||
assert_eq!(model._get_formula("E6"), "=SUM(H3:H7)");
|
||||
assert_eq!(model._get_formula("E7"), "=SUM(G3:G7)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tets_move_column_error() {
|
||||
let mut model = new_empty_model();
|
||||
model.evaluate();
|
||||
|
||||
let result = model.move_column_action(0, 7, -10);
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = model.move_column_action(0, -7, 20);
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = model.move_column_action(0, LAST_COLUMN, 1);
|
||||
assert!(result.is_err());
|
||||
|
||||
let result = model.move_column_action(0, LAST_COLUMN + 1, -10);
|
||||
assert!(result.is_err());
|
||||
|
||||
// This works
|
||||
let result = model.move_column_action(0, LAST_COLUMN, -1);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// A B C D E F G H I J K L M N O P Q R
|
||||
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
||||
28
base/src/test/test_binary_search.rs
Normal file
28
base/src/test/test_binary_search.rs
Normal file
@@ -0,0 +1,28 @@
|
||||
use crate::functions::binary_search::*;
|
||||
|
||||
#[test]
|
||||
fn test_binary_search() {
|
||||
let t = vec![1, 2, 3, 40, 55, 155];
|
||||
assert_eq!(binary_search_or_smaller(&40, &t), Some(3));
|
||||
assert_eq!(binary_search_or_greater(&40, &t), Some(3));
|
||||
assert_eq!(binary_search_or_smaller(&45, &t), Some(3));
|
||||
assert_eq!(binary_search_or_greater(&45, &t), Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_search_descending() {
|
||||
let t = vec![100, 33, 23, 14, 5, -155];
|
||||
assert_eq!(binary_search_descending_or_smaller(&23, &t), Some(2));
|
||||
assert_eq!(binary_search_descending_or_greater(&23, &t), Some(2));
|
||||
assert_eq!(binary_search_descending_or_smaller(&25, &t), Some(2));
|
||||
assert_eq!(binary_search_descending_or_greater(&25, &t), Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_binary_search_multiple() {
|
||||
let t = vec![1, 2, 3, 40, 40, 40, 40, 55, 155];
|
||||
assert_eq!(binary_search_or_smaller(&40, &t), Some(3));
|
||||
assert_eq!(binary_search_or_smaller(&39, &t), Some(2));
|
||||
assert_eq!(binary_search_or_greater(&40, &t), Some(3));
|
||||
assert_eq!(binary_search_or_greater(&41, &t), Some(7));
|
||||
}
|
||||
35
base/src/test/test_cell.rs
Normal file
35
base/src/test/test_cell.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::types::CellType;
|
||||
|
||||
#[test]
|
||||
fn test_cell_get_type() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "");
|
||||
model._set("A2", "42");
|
||||
model._set("A3", "12.34");
|
||||
model._set("A4", "foobar");
|
||||
model._set("A5", "1+2");
|
||||
model._set("A6", "TRUE");
|
||||
model._set("A7", "#VALUE!");
|
||||
model._set("A8", "=Z100"); // an empty cell, considered to be a CellType::Number
|
||||
model._set("A9", "=2*3*7");
|
||||
model._set("A10", "=\"foo\"");
|
||||
model._set("A11", "=1/0");
|
||||
model._set("A12", "=1>0");
|
||||
model.evaluate();
|
||||
|
||||
assert_eq!(model._get_cell("A1").get_type(), CellType::Text);
|
||||
assert_eq!(model._get_cell("A2").get_type(), CellType::Number);
|
||||
assert_eq!(model._get_cell("A3").get_type(), CellType::Number);
|
||||
assert_eq!(model._get_cell("A4").get_type(), CellType::Text);
|
||||
assert_eq!(model._get_cell("A5").get_type(), CellType::Text);
|
||||
assert_eq!(model._get_cell("A6").get_type(), CellType::LogicalValue);
|
||||
assert_eq!(model._get_cell("A7").get_type(), CellType::ErrorValue);
|
||||
assert_eq!(model._get_cell("A8").get_type(), CellType::Number);
|
||||
assert_eq!(model._get_cell("A9").get_type(), CellType::Number);
|
||||
assert_eq!(model._get_cell("A10").get_type(), CellType::Text);
|
||||
assert_eq!(model._get_cell("A11").get_type(), CellType::ErrorValue);
|
||||
assert_eq!(model._get_cell("A12").get_type(), CellType::LogicalValue);
|
||||
}
|
||||
27
base/src/test/test_circular_references.rs
Normal file
27
base/src/test/test_circular_references.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::test::util::new_empty_model;
|
||||
|
||||
#[test]
|
||||
fn test_simple_circ() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=A1+1");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("A1"), "#CIRC!");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_simple_circ_propagate() {
|
||||
let mut model = new_empty_model();
|
||||
model._set("A1", "=B6");
|
||||
model._set("A2", "=A1+1");
|
||||
model._set("A3", "=A2+1");
|
||||
model._set("A4", "=A3+5");
|
||||
model._set("B6", "=A4*7");
|
||||
model.evaluate();
|
||||
assert_eq!(model._get_text("A1"), "#CIRC!");
|
||||
assert_eq!(model._get_text("A2"), "#CIRC!");
|
||||
assert_eq!(model._get_text("A3"), "#CIRC!");
|
||||
assert_eq!(model._get_text("A4"), "#CIRC!");
|
||||
assert_eq!(model._get_text("B6"), "#CIRC!");
|
||||
}
|
||||
82
base/src/test/test_column_width.rs
Normal file
82
base/src/test/test_column_width.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::constants::{COLUMN_WIDTH_FACTOR, DEFAULT_COLUMN_WIDTH};
|
||||
use crate::test::util::new_empty_model;
|
||||
use crate::types::Col;
|
||||
|
||||
#[test]
|
||||
fn test_column_width() {
|
||||
let mut model = new_empty_model();
|
||||
let cols = vec![Col {
|
||||
custom_width: false,
|
||||
max: 16384,
|
||||
min: 1,
|
||||
style: Some(6),
|
||||
width: 8.7,
|
||||
}];
|
||||
model.workbook.worksheets[0].cols = cols;
|
||||
model
|
||||
.workbook
|
||||
.worksheet_mut(0)
|
||||
.unwrap()
|
||||
.set_column_width(2, 30.0)
|
||||
.unwrap();
|
||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 3);
|
||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||
assert!((worksheet.column_width(1).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(2).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(3).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert_eq!(model.get_cell_style_index(0, 23, 2), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column_width_lower_edge() {
|
||||
let mut model = new_empty_model();
|
||||
let cols = vec![Col {
|
||||
custom_width: true,
|
||||
max: 16,
|
||||
min: 5,
|
||||
style: Some(1),
|
||||
width: 10.0,
|
||||
}];
|
||||
model.workbook.worksheets[0].cols = cols;
|
||||
model
|
||||
.workbook
|
||||
.worksheet_mut(0)
|
||||
.unwrap()
|
||||
.set_column_width(5, 30.0)
|
||||
.unwrap();
|
||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
|
||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||
assert!((worksheet.column_width(4).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(5).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(6).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON);
|
||||
assert_eq!(model.get_cell_style_index(0, 23, 5), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_column_width_higher_edge() {
|
||||
let mut model = new_empty_model();
|
||||
let cols = vec![Col {
|
||||
custom_width: true,
|
||||
max: 16,
|
||||
min: 5,
|
||||
style: Some(1),
|
||||
width: 10.0,
|
||||
}];
|
||||
model.workbook.worksheets[0].cols = cols;
|
||||
model
|
||||
.workbook
|
||||
.worksheet_mut(0)
|
||||
.unwrap()
|
||||
.set_column_width(16, 30.0)
|
||||
.unwrap();
|
||||
assert_eq!(model.workbook.worksheets[0].cols.len(), 2);
|
||||
let worksheet = model.workbook.worksheet(0).unwrap();
|
||||
assert!(
|
||||
(worksheet.column_width(15).unwrap() - 10.0 * COLUMN_WIDTH_FACTOR).abs() < f64::EPSILON
|
||||
);
|
||||
assert!((worksheet.column_width(16).unwrap() - 30.0).abs() < f64::EPSILON);
|
||||
assert!((worksheet.column_width(17).unwrap() - DEFAULT_COLUMN_WIDTH).abs() < f64::EPSILON);
|
||||
assert_eq!(model.get_cell_style_index(0, 23, 16), 1);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user