commit c5b8efd83d87d6d442c8576b3fe2509a6fbbe19d Author: Nicolás Hatcher Date: Sat Nov 18 21:26:18 2023 +0100 UPDATE: Dump of initial files diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c8b241f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4519a4a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,716 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[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 = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[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 = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[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" +version = "0.1.0" +dependencies = [ + "chrono", + "ironcalc_base", + "itertools", + "roxmltree", + "serde", + "serde_json", + "thiserror", + "uuid", + "zip", +] + +[[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 = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[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 = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[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 = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[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 = "roxmltree" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf7d7b1ea646d380d0e8153158063a6da7efe30ddbf3184042848e3f8a6f671" +dependencies = [ + "xmlparser", +] + +[[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 = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[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 = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d1113d5 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[workspace] +resolver = "2" + +members = [ + "base", + "xlsx", +] + +exclude = [ + "generate_locale", +] + +[profile.release] +lto = true diff --git a/LICENSE-Apache-2.0 b/LICENSE-Apache-2.0 new file mode 100644 index 0000000..9b63f9b --- /dev/null +++ b/LICENSE-Apache-2.0 @@ -0,0 +1,191 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2023 EqualTo GmbH, 2023 Nicolás Hatcher + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..8fd86e2 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2023 EqualTo GmbH, 2023 Nicolás Hatcher + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ecfc3f7 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +lint: + cargo fmt -- --check + cargo clippy --all-targets --all-features + +format: + cargo fmt + +tests: lint + cargo test + +clean: + cargo clean + rm -r -f base/target + rm -r -f xlsx/target + rm cargo-test-* + rm base/cargo-test-* + rm xlsx/cargo-test-* + + +coverage: + CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='cargo-test-%p-%m.profraw' cargo test + grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore "/*" -o target/coverage/html + +docs: + cargo doc --no-deps + +.PHONY: lint format tests docs coverage all \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..6216718 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# 📚 IronCalc + +IronCalc is a new, modern, work-in-progress spreadsheet engine and set of tools to work with spreadsheets in diverse settings. + +This repository contains the main engine and the xlsx importer and exporter. + +Programmed in Rust, you can use it from a variety of programming languages like [Python](https://github.com/ironcalc/bindings-python), [JavaScript (wasm)](https://github.com/ironcalc/bindings-js), [nodejs](https://github.com/ironcalc/bindings-nodejs) and soon R, Julia, Go and possibly others. + +It has several different _skins_. You can use it in the [terminal](https://github.com/ironcalc/skin-terminal), as a [desktop application](https://github.com/ironcalc/bindings-desktop) or use it in you own [web application](https://github.com/ironcalc/skin-web). + +# 🛠️ Building + +```bash +cargo build --release +``` + +# Testing, linting and code coverage + +Testing: +```bash +cargo test +``` + +Linting: +```bash +make lint +``` + +Testing and linting: +```bash +make tests +``` + +Code coverage: +```bash +make coverage +cd target/coverage/html/ +python -m http.server +``` + +# 🖹 API Documentation + +Documentation might be generated with + +```bash +$ cargo doc --no-deps +``` + +# 📝 ROADMAP + +> [!WARNING] +> This is work-in-progress. IronCalc in developed in the open. Expect things to be broken and change quickly until version 0.5 + +* We intend to have a working version by mid January 2024 (version 0.5, MVP) +* Version 1.0.0 will come later in 2024 + + +# License + +Licensed under either of + +* [MIT license](LICENSE-MIT) +* [Apache license, version 2.0](LICENSE-Apache-2.0) + +at your option. \ No newline at end of file diff --git a/base/.gitignore b/base/.gitignore new file mode 100644 index 0000000..c8b241f --- /dev/null +++ b/base/.gitignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/base/Cargo.lock b/base/Cargo.lock new file mode 100644 index 0000000..cf7c3c0 --- /dev/null +++ b/base/Cargo.lock @@ -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" diff --git a/base/Cargo.toml b/base/Cargo.toml new file mode 100644 index 0000000..bf713b3 --- /dev/null +++ b/base/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ironcalc_base" +version = "0.1.0" +authors = ["Nicolás Hatcher "] +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" + + diff --git a/base/README.md b/base/README.md new file mode 100644 index 0000000..e4ba81d --- /dev/null +++ b/base/README.md @@ -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 +``` + + + + + + diff --git a/base/src/actions.rs b/base/src/actions.rs new file mode 100644 index 0000000..e93709e --- /dev/null +++ b/base/src/actions.rs @@ -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, String> { + let worksheet = self.workbook.worksheet(sheet)?; + if let Some(row_data) = worksheet.sheet_data.get(&row) { + let mut columns: Vec = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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(()) + } +} diff --git a/base/src/calc_result.rs b/base/src/calc_result.rs new file mode 100644 index 0000000..72cf971 --- /dev/null +++ b/base/src/calc_result.rs @@ -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 { + Some(self.cmp(other)) + } +} + +impl PartialEq for CalcResult { + fn eq(&self, other: &Self) -> bool { + self.cmp(other) == Ordering::Equal + } +} + +impl Eq for CalcResult {} diff --git a/base/src/cast.rs b/base/src/cast.rs new file mode 100644 index 0000000..3155e94 --- /dev/null +++ b/base/src/cast.rs @@ -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 { + 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 { + match result { + CalcResult::Number(f) => Ok(f), + CalcResult::String(s) => match s.parse::() { + 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 { + 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 { + 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 { + // 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 { + 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 { + 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 { + 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(), + }) + } + } + } + } +} diff --git a/base/src/cell.rs b/base/src/cell.rs new file mode 100644 index 0000000..84e33c7 --- /dev/null +++ b/base/src/cell.rs @@ -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 for CellValue { + fn from(value: f64) -> Self { + Self::Number(value) + } +} + +impl From 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 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 { + 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( + &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), + } + } +} diff --git a/base/src/constants.rs b/base/src/constants.rs new file mode 100644 index 0000000..f1f7348 --- /dev/null +++ b/base/src/constants.rs @@ -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; diff --git a/base/src/diffs.rs b/base/src/diffs.rs new file mode 100644 index 0000000..dbdfdc7 --- /dev/null +++ b/base/src/diffs.rs @@ -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, String> { + let mut diff_list: Vec = 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) + } +} diff --git a/base/src/expressions/lexer/mod.rs b/base/src/expressions/lexer/mod.rs new file mode 100644 index 0000000..f377542 --- /dev/null +++ b/base/src/expressions/lexer/mod.rs @@ -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 = std::result::Result; + +#[derive(Clone, PartialEq, Eq)] +pub enum LexerMode { + A1, + R1C1, +} + +/// Tokenize an input +#[derive(Clone)] +pub struct Lexer { + position: usize, + next_token_position: Option, + len: usize, + chars: Vec, + 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 = 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 { + 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 { + 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 { + 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::().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 { + 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::() { + 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 { + 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) -> 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), + } + } +} diff --git a/base/src/expressions/lexer/ranges.rs b/base/src/expressions/lexer/ranges.rs new file mode 100644 index 0000000..2876971 --- /dev/null +++ b/base/src/expressions/lexer/ranges.rs @@ -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 { + 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::() { + 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 { + // 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::() { + Ok(n) => n, + Err(_) => { + return Err(self + .set_error(&format!("Failed parsing row {}", row_left), position)) + } + }; + let row_right = match row_right.parse::() { + 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 { + // 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 { + // 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, + }) + } +} diff --git a/base/src/expressions/lexer/structured_references.rs b/base/src/expressions/lexer/structured_references.rs new file mode 100644 index 0000000..c225fc7 --- /dev/null +++ b/base/src/expressions/lexer/structured_references.rs @@ -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> { + 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 { + 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 { + 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, + }) + } +} diff --git a/base/src/expressions/lexer/test/mod.rs b/base/src/expressions/lexer/test/mod.rs new file mode 100644 index 0000000..f795ba5 --- /dev/null +++ b/base/src/expressions/lexer/test/mod.rs @@ -0,0 +1,6 @@ +mod test_common; +mod test_language; +mod test_locale; +mod test_ranges; +mod test_tables; +mod test_util; diff --git a/base/src/expressions/lexer/test/test_common.rs b/base/src/expressions/lexer/test/test_common.rs new file mode 100644 index 0000000..0b4123d --- /dev/null +++ b/base/src/expressions/lexer/test/test_common.rs @@ -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); +} diff --git a/base/src/expressions/lexer/test/test_language.rs b/base/src/expressions/lexer/test/test_language.rs new file mode 100644 index 0000000..28b3125 --- /dev/null +++ b/base/src/expressions/lexer/test/test_language.rs @@ -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); +} diff --git a/base/src/expressions/lexer/test/test_locale.rs b/base/src/expressions/lexer/test/test_locale.rs new file mode 100644 index 0000000..24eff09 --- /dev/null +++ b/base/src/expressions/lexer/test/test_locale.rs @@ -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); +} diff --git a/base/src/expressions/lexer/test/test_ranges.rs b/base/src/expressions/lexer/test/test_ranges.rs new file mode 100644 index 0000000..628a878 --- /dev/null +++ b/base/src/expressions/lexer/test/test_ranges.rs @@ -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); +} diff --git a/base/src/expressions/lexer/test/test_tables.rs b/base/src/expressions/lexer/test/test_tables.rs new file mode 100644 index 0000000..9a2199d --- /dev/null +++ b/base/src/expressions/lexer/test/test_tables.rs @@ -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); +} diff --git a/base/src/expressions/lexer/test/test_util.rs b/base/src/expressions/lexer/test/test_util.rs new file mode 100644 index 0000000..fff5985 --- /dev/null +++ b/base/src/expressions/lexer/test/test_util.rs @@ -0,0 +1,146 @@ +use crate::expressions::{ + lexer::util::get_tokens, + token::{OpCompare, OpSum, TokenType}, +}; + +fn get_tokens_types(formula: &str) -> Vec { + 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 + }] + ); +} diff --git a/base/src/expressions/lexer/util.rs b/base/src/expressions/lexer/util.rs new file mode 100644 index 0000000..9942936 --- /dev/null +++ b/base/src/expressions/lexer/util.rs @@ -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 { + 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) + } +} diff --git a/base/src/expressions/mod.rs b/base/src/expressions/mod.rs new file mode 100644 index 0000000..35f9756 --- /dev/null +++ b/base/src/expressions/mod.rs @@ -0,0 +1,6 @@ +// public modules +pub mod lexer; +pub mod parser; +pub mod token; +pub mod types; +pub mod utils; diff --git a/base/src/expressions/parser/mod.rs b/base/src/expressions/parser/mod.rs new file mode 100644 index 0000000..c66f54f --- /dev/null +++ b/base/src/expressions/parser/mod.rs @@ -0,0 +1,877 @@ +/*! +# GRAMAR + +
+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)*
+
+*/ + +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 { + 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, + 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, + sheet_index: u32, + absolute_row: bool, + absolute_column: bool, + row: i32, + column: i32, + }, + RangeKind { + sheet_name: Option, + 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, + absolute_row: bool, + absolute_column: bool, + row: i32, + column: i32, + }, + WrongRangeKind { + sheet_name: Option, + absolute_row1: bool, + absolute_column1: bool, + row1: i32, + column1: i32, + absolute_row2: bool, + absolute_column2: bool, + row2: i32, + column2: i32, + }, + OpRangeKind { + left: Box, + right: Box, + }, + OpConcatenateKind { + left: Box, + right: Box, + }, + OpSumKind { + kind: token::OpSum, + left: Box, + right: Box, + }, + OpProductKind { + kind: token::OpProduct, + left: Box, + right: Box, + }, + OpPowerKind { + left: Box, + right: Box, + }, + FunctionKind { + kind: Function, + args: Vec, + }, + InvalidFunctionKind { + name: String, + args: Vec, + }, + ArrayKind(Vec), + VariableKind(String), + CompareKind { + kind: OpCompare, + left: Box, + right: Box, + }, + UnaryKind { + kind: OpUnary, + right: Box, + }, + ErrorKind(token::Error), + ParseErrorKind { + formula: String, + message: String, + position: usize, + }, + EmptyArgKind, +} + +#[derive(Clone)] +pub struct Parser { + lexer: lexer::Lexer, + worksheets: Vec, + context: Option, + tables: HashMap, +} + +impl Parser { + pub fn new(worksheets: Vec, tables: HashMap) -> 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) { + self.worksheets = worksheets; + } + + pub fn parse(&mut self, formula: &str, context: &Option) -> Node { + self.lexer.set_formula(formula); + self.context = context.clone(); + self.parse_expr() + } + + fn get_sheet_index_by_name(&self, name: &str) -> Option { + 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 = 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, Node> { + let mut args: Vec = 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) + } +} diff --git a/base/src/expressions/parser/move_formula.rs b/base/src/expressions/parser/move_formula.rs new file mode 100644 index 0000000..b6336cc --- /dev/null +++ b/base/src/expressions/parser/move_formula.rs @@ -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, 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(), + } +} diff --git a/base/src/expressions/parser/stringify.rs b/base/src/expressions/parser/stringify.rs new file mode 100644 index 0000000..352b285 --- /dev/null +++ b/base/src/expressions/parser/stringify.rs @@ -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, + 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 => {} + } +} diff --git a/base/src/expressions/parser/test.rs b/base/src/expressions/parser/test.rs new file mode 100644 index 0000000..3d8369a --- /dev/null +++ b/base/src/expressions/parser/test.rs @@ -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()); +} diff --git a/base/src/expressions/parser/test_move_formula.rs b/base/src/expressions/parser/test_move_formula.rs new file mode 100644 index 0000000..0b84811 --- /dev/null +++ b/base/src/expressions/parser/test_move_formula.rs @@ -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)" + ); +} diff --git a/base/src/expressions/parser/test_ranges.rs b/base/src/expressions/parser/test_ranges.rs new file mode 100644 index 0000000..2e876dc --- /dev/null +++ b/base/src/expressions/parser/test_ranges.rs @@ -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() + ); +} diff --git a/base/src/expressions/parser/test_tables.rs b/base/src/expressions/parser/test_tables.rs new file mode 100644 index 0000000..89fd880 --- /dev/null +++ b/base/src/expressions/parser/test_tables.rs @@ -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 { + 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)" + ); +} diff --git a/base/src/expressions/parser/walk.rs b/base/src/expressions/parser/walk.rs new file mode 100644 index 0000000..8cef6af --- /dev/null +++ b/base/src/expressions/parser/walk.rs @@ -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 { .. } => {} + } +} diff --git a/base/src/expressions/test.rs b/base/src/expressions/test.rs new file mode 100644 index 0000000..2c61aac --- /dev/null +++ b/base/src/expressions/test.rs @@ -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); + } +} diff --git a/base/src/expressions/token.rs b/base/src/expressions/token.rs new file mode 100644 index 0000000..8f47f36 --- /dev/null +++ b/base/src/expressions/token.rs @@ -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 { + 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 { + 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, + row: i32, + column: i32, + absolute_column: bool, + absolute_row: bool, + }, + Range { + sheet: Option, + left: ParsedReference, + right: ParsedReference, + }, + StructuredReference { + table_name: String, + specifier: Option, + table_reference: Option, + }, +} + +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, + } +} diff --git a/base/src/expressions/types.rs b/base/src/expressions/types.rs new file mode 100644 index 0000000..74bb0c2 --- /dev/null +++ b/base/src/expressions/types.rs @@ -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, +} + +// 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, +} diff --git a/base/src/expressions/utils/mod.rs b/base/src/expressions/utils/mod.rs new file mode 100644 index 0000000..6e52c40 --- /dev/null +++ b/base/src/expressions/utils/mod.rs @@ -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 { + 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 { + 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::() { + Ok(r) => is_valid_row(r), + Err(_r) => false, + } +} + +pub fn parse_reference_r1c1(r: &str) -> Option { + 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::().unwrap_or(0), + column: column.parse::().unwrap_or(0), + absolute_column, + absolute_row, + }) +} + +pub fn parse_reference_a1(r: &str) -> Option { + 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::() { + 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() +} diff --git a/base/src/expressions/utils/test.rs b/base/src/expressions/utils/test.rs new file mode 100644 index 0000000..b6dc3da --- /dev/null +++ b/base/src/expressions/utils/test.rs @@ -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")); +} diff --git a/base/src/formatter/dates.rs b/base/src/formatter/dates.rs new file mode 100644 index 0000000..af1a6a2 --- /dev/null +++ b/base/src/formatter/dates.rs @@ -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 { + 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()), + } +} diff --git a/base/src/formatter/format.rs b/base/src/formatter/format.rs new file mode 100644 index 0000000..55d8eb7 --- /dev/null +++ b/base/src/formatter/format.rs @@ -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, + pub text: String, + pub error: Option, +} + +/// 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 { + let b = format!("{:.1$}", value.fract(), precision as usize) + .chars() + .collect::>(); + 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 = 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 = 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::() { + 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::() { + 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::() { + 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> { + 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::() { + 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, + }, + )), + } +} diff --git a/base/src/formatter/lexer.rs b/base/src/formatter/lexer.rs new file mode 100644 index 0000000..094065d --- /dev/null +++ b/base/src/formatter/lexer.rs @@ -0,0 +1,408 @@ +pub struct Lexer { + position: usize, + len: usize, + chars: Vec, + 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 = 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 { + let position = self.position; + if position < self.len { + Some(self.chars[position]) + } else { + None + } + } + + fn read_next_char(&mut self) -> Option { + 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 { + 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 { + 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::() { + 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 { + 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::() { + 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; + } + } +} diff --git a/base/src/formatter/mod.rs b/base/src/formatter/mod.rs new file mode 100644 index 0000000..e38cb1e --- /dev/null +++ b/base/src/formatter/mod.rs @@ -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: +// +// ;;; +// +// * 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 diff --git a/base/src/formatter/parser.rs b/base/src/formatter/parser.rs new file mode 100644 index 0000000..1682492 --- /dev/null +++ b/base/src/formatter/parser.rs @@ -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, + 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, + 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, + pub tokens: Vec, +} + +pub struct ErrorPart {} + +pub struct GeneralPart {} + +pub enum ParsePart { + Number(NumberPart), + Date(DatePart), + Error(ErrorPart), + General(GeneralPart), +} + +pub struct Parser { + pub parts: Vec, + 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, + }) + } + } +} diff --git a/base/src/formatter/test/mod.rs b/base/src/formatter/test/mod.rs new file mode 100644 index 0000000..12c6271 --- /dev/null +++ b/base/src/formatter/test/mod.rs @@ -0,0 +1,2 @@ +mod test_general; +mod test_parse_formatted_number; diff --git a/base/src/formatter/test/test_general.rs b/base/src/formatter/test/test_general.rs new file mode 100644 index 0000000..cdebe36 --- /dev/null +++ b/base/src/formatter/test/test_general.rs @@ -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" + ); +} diff --git a/base/src/formatter/test/test_parse_formatted_number.rs b/base/src/formatter/test/test_parse_formatted_number.rs new file mode 100644 index 0000000..9044339 --- /dev/null +++ b/base/src/formatter/test/test_parse_formatted_number.rs @@ -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()) + ); +} diff --git a/base/src/functions/binary_search.rs b/base/src/functions/binary_search.rs new file mode 100644 index 0000000..09db5e4 --- /dev/null +++ b/base/src/functions/binary_search.rs @@ -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(target: &T, array: &[T]) -> Option { + // 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(target: &T, array: &[T]) -> Option { + 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(target: &T, array: &[T]) -> Option { + 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(target: &T, array: &[T]) -> Option { + 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 { + 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 + } +} diff --git a/base/src/functions/date_and_time.rs b/base/src/functions/date_and_time.rs new file mode 100644 index 0000000..b7f72b6 --- /dev/null +++ b/base/src/functions/date_and_time.rs @@ -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()) + } +} diff --git a/base/src/functions/engineering/bessel.rs b/base/src/functions/engineering/bessel.rs new file mode 100644 index 0000000..a7109f0 --- /dev/null +++ b/base/src/functions/engineering/bessel.rs @@ -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)) + } +} diff --git a/base/src/functions/engineering/bit_operations.rs b/base/src/functions/engineering/bit_operations.rs new file mode 100644 index 0000000..e71c50e --- /dev/null +++ b/base/src/functions/engineering/bit_operations.rs @@ -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) + } +} diff --git a/base/src/functions/engineering/complex.rs b/base/src/functions/engineering/complex.rs new file mode 100644 index 0000000..fe7d4f6 --- /dev/null +++ b/base/src/functions/engineering/complex.rs @@ -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))); + } +} diff --git a/base/src/functions/engineering/convert.rs b/base/src/functions/engineering/convert.rs new file mode 100644 index 0000000..ab59983 --- /dev/null +++ b/base/src/functions/engineering/convert.rs @@ -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) + } +} diff --git a/base/src/functions/engineering/misc.rs b/base/src/functions/engineering/misc.rs new file mode 100644 index 0000000..48246a7 --- /dev/null +++ b/base/src/functions/engineering/misc.rs @@ -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) + } + } +} diff --git a/base/src/functions/engineering/mod.rs b/base/src/functions/engineering/mod.rs new file mode 100644 index 0000000..1f549cf --- /dev/null +++ b/base/src/functions/engineering/mod.rs @@ -0,0 +1,7 @@ +mod bessel; +mod bit_operations; +mod complex; +mod convert; +mod misc; +mod number_basis; +mod transcendental; diff --git a/base/src/functions/engineering/number_basis.rs b/base/src/functions/engineering/number_basis.rs new file mode 100644 index 0000000..63c931b --- /dev/null +++ b/base/src/functions/engineering/number_basis.rs @@ -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 { + 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) + } +} diff --git a/base/src/functions/engineering/transcendental/README.md b/base/src/functions/engineering/transcendental/README.md new file mode 100644 index 0000000..ed9e40d --- /dev/null +++ b/base/src/functions/engineering/transcendental/README.md @@ -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/ \ No newline at end of file diff --git a/base/src/functions/engineering/transcendental/bessel_i.rs b/base/src/functions/engineering/transcendental/bessel_i.rs new file mode 100644 index 0000000..12829f0 --- /dev/null +++ b/base/src/functions/engineering/transcendental/bessel_i.rs @@ -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 +} diff --git a/base/src/functions/engineering/transcendental/bessel_j0_y0.rs b/base/src/functions/engineering/transcendental/bessel_j0_y0.rs new file mode 100644 index 0000000..18c5c39 --- /dev/null +++ b/base/src/functions/engineering/transcendental/bessel_j0_y0.rs @@ -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) + } +} diff --git a/base/src/functions/engineering/transcendental/bessel_j1_y1.rs b/base/src/functions/engineering/transcendental/bessel_j1_y1.rs new file mode 100644 index 0000000..c059339 --- /dev/null +++ b/base/src/functions/engineering/transcendental/bessel_j1_y1.rs @@ -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 +} diff --git a/base/src/functions/engineering/transcendental/bessel_jn_yn.rs b/base/src/functions/engineering/transcendental/bessel_jn_yn.rs new file mode 100644 index 0000000..6217bbd --- /dev/null +++ b/base/src/functions/engineering/transcendental/bessel_jn_yn.rs @@ -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 nx, 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 + } +} diff --git a/base/src/functions/engineering/transcendental/bessel_k.rs b/base/src/functions/engineering/transcendental/bessel_k.rs new file mode 100644 index 0000000..5cfd0ad --- /dev/null +++ b/base/src/functions/engineering/transcendental/bessel_k.rs @@ -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 +} diff --git a/base/src/functions/engineering/transcendental/bessel_util.rs b/base/src/functions/engineering/transcendental/bessel_util.rs new file mode 100644 index 0000000..ebde9eb --- /dev/null +++ b/base/src/functions/engineering/transcendental/bessel_util.rs @@ -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]), + ) +} diff --git a/base/src/functions/engineering/transcendental/create_test.jl b/base/src/functions/engineering/transcendental/create_test.jl new file mode 100644 index 0000000..b02d28f --- /dev/null +++ b/base/src/functions/engineering/transcendental/create_test.jl @@ -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 \ No newline at end of file diff --git a/base/src/functions/engineering/transcendental/erf.rs b/base/src/functions/engineering/transcendental/erf.rs new file mode 100644 index 0000000..9c56a4c --- /dev/null +++ b/base/src/functions/engineering/transcendental/erf.rs @@ -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 + } +} diff --git a/base/src/functions/engineering/transcendental/mod.rs b/base/src/functions/engineering/transcendental/mod.rs new file mode 100644 index 0000000..9a90a48 --- /dev/null +++ b/base/src/functions/engineering/transcendental/mod.rs @@ -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; diff --git a/base/src/functions/engineering/transcendental/test_bessel.rs b/base/src/functions/engineering/transcendental/test_bessel.rs new file mode 100644 index 0000000..123997d --- /dev/null +++ b/base/src/functions/engineering/transcendental/test_bessel.rs @@ -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})" + ); + } +} diff --git a/base/src/functions/financial.rs b/base/src/functions/financial.rs new file mode 100644 index 0000000..9d4ff81 --- /dev/null +++ b/base/src/functions/financial.rs @@ -0,0 +1,1884 @@ +use chrono::Datelike; + +use crate::{ + calc_result::{CalcResult, CellReference}, + constants::{LAST_COLUMN, LAST_ROW}, + expressions::{parser::Node, token::Error}, + formatter::dates::from_excel_date, + model::Model, +}; + +use super::financial_util::{compute_irr, compute_npv, compute_rate, compute_xirr, compute_xnpv}; + +// See: +// https://github.com/apache/openoffice/blob/c014b5f2b55cff8d4b0c952d5c16d62ecde09ca1/main/scaddins/source/analysis/financial.cxx + +// FIXME: Is this enough? +fn is_valid_date(date: f64) -> bool { + date > 0.0 +} + +fn is_less_than_one_year(start_date: i64, end_date: i64) -> bool { + if end_date - start_date < 365 { + return true; + } + let end = from_excel_date(end_date); + let start = from_excel_date(start_date); + let end_year = end.year(); + let start_year = start.year(); + if end_year == start_year { + return true; + } + if end_year != start_year + 1 { + return false; + } + let start_month = start.month(); + let end_month = end.month(); + if end_month < start_month { + return true; + } + if end_month > start_month { + return false; + } + // we are one year later same month + let start_day = start.day(); + let end_day = end.day(); + end_day <= start_day +} + +fn compute_payment( + rate: f64, + nper: f64, + pv: f64, + fv: f64, + period_start: bool, +) -> Result { + if rate == 0.0 { + if nper == 0.0 { + return Err((Error::NUM, "Period count must be non zero".to_string())); + } + return Ok(-(pv + fv) / nper); + } + if rate <= -1.0 { + return Err((Error::NUM, "Rate must be > -1".to_string())); + }; + let rate_nper = if nper == 0.0 { + 1.0 + } else { + (1.0 + rate).powf(nper) + }; + let result = if period_start { + // type = 1 + (fv + pv * rate_nper) * rate / ((1.0 + rate) * (1.0 - rate_nper)) + } else { + (fv * rate + pv * rate * rate_nper) / (1.0 - rate_nper) + }; + if result.is_nan() || result.is_infinite() { + return Err((Error::NUM, "Invalid result".to_string())); + } + Ok(result) +} + +fn compute_future_value( + rate: f64, + nper: f64, + pmt: f64, + pv: f64, + period_start: bool, +) -> Result { + if rate == 0.0 { + return Ok(-pv - pmt * nper); + } + + let rate_nper = (1.0 + rate).powf(nper); + let fv = if period_start { + // type = 1 + -pv * rate_nper - pmt * (1.0 + rate) * (rate_nper - 1.0) / rate + } else { + -pv * rate_nper - pmt * (rate_nper - 1.0) / rate + }; + if fv.is_nan() { + return Err((Error::NUM, "Invalid result".to_string())); + } + if !fv.is_finite() { + return Err((Error::DIV, "Divide by zero".to_string())); + } + Ok(fv) +} + +fn compute_ipmt( + rate: f64, + period: f64, + period_count: f64, + present_value: f64, + future_value: f64, + period_start: bool, +) -> Result { + // http://www.staff.city.ac.uk/o.s.kerr/CompMaths/WSheet4.pdf + // https://www.experts-exchange.com/articles/1948/A-Guide-to-the-PMT-FV-IPMT-and-PPMT-Functions.html + // type = 0 (end of period) + // impt = -[(1+rate)^(period-1)*(pv*rate+pmt)-pmt] + // ipmt = FV(rate, period-1, payment, pv, type) * rate + // type = 1 (beginning of period) + // ipmt = (FV(rate, period-2, payment, pv, type) - payment) * rate + let payment = compute_payment( + rate, + period_count, + present_value, + future_value, + period_start, + )?; + if period < 1.0 || period >= period_count + 1.0 { + return Err(( + Error::NUM, + format!("Period must be between 1 and {}", period_count + 1.0), + )); + } + if period == 1.0 && period_start { + Ok(0.0) + } else { + let p = if period_start { + period - 2.0 + } else { + period - 1.0 + }; + let c = if period_start { -payment } else { 0.0 }; + let fv = compute_future_value(rate, p, payment, present_value, period_start)?; + Ok((fv + c) * rate) + } +} + +fn compute_ppmt( + rate: f64, + period: f64, + period_count: f64, + present_value: f64, + future_value: f64, + period_start: bool, +) -> Result { + let payment = compute_payment( + rate, + period_count, + present_value, + future_value, + period_start, + )?; + // It's a bit unfortunate that the first thing compute_ipmt does is compute_payment again + let ipmt = compute_ipmt( + rate, + period, + period_count, + present_value, + future_value, + period_start, + )?; + Ok(payment - ipmt) +} + +// These formulas revolve around compound interest and annuities. +// The financial functions pv, rate, nper, pmt and fv: +// rate = interest rate per period +// nper (number of periods) = loan term +// pv (present value) = loan amount +// fv (future value) = cash balance after last payment. Default is 0 +// type = the annuity type indicates when payments are due +// * 0 (default) Payments are made at the end of the period +// * 1 Payments are made at the beginning of the period (like a lease or rent) +// The variable period_start is true if type is 1 +// They are linked by the formulas: +// If rate != 0 +// $pv*(1+rate)^nper+pmt*(1+rate*type)*((1+rate)^nper-1)/rate+fv=0$ +// If rate = 0 +// $pmt*nper+pv+fv=0$ +// All, except for rate are easily solvable in terms of the others. +// In these formulas the payment (pmt) is normally negative + +impl Model { + // FIXME: These three functions (get_array_of_numbers..) need to be refactored + // They are really similar expect for small issues + fn get_array_of_numbers( + &mut self, + arg: &Node, + cell: &CellReference, + ) -> Result, CalcResult> { + let mut values = Vec::new(); + match self.evaluate_node_in_context(arg, *cell) { + CalcResult::Number(value) => values.push(value), + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return Err(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) => { + values.push(value); + } + error @ CalcResult::Error { .. } => return Err(error), + _ => { + // We ignore booleans and strings + } + } + } + } + } + error @ CalcResult::Error { .. } => return Err(error), + _ => { + // We ignore booleans and strings + } + }; + Ok(values) + } + + fn get_array_of_numbers_xpnv( + &mut self, + arg: &Node, + cell: &CellReference, + error: Error, + ) -> Result, CalcResult> { + let mut values = Vec::new(); + match self.evaluate_node_in_context(arg, *cell) { + CalcResult::Number(value) => values.push(value), + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return Err(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) => { + values.push(value); + } + error @ CalcResult::Error { .. } => return Err(error), + CalcResult::EmptyCell => { + return Err(CalcResult::new_error( + Error::NUM, + *cell, + "Expected number".to_string(), + )); + } + _ => { + return Err(CalcResult::new_error( + error, + *cell, + "Expected number".to_string(), + )); + } + } + } + } + } + error @ CalcResult::Error { .. } => return Err(error), + _ => { + return Err(CalcResult::new_error( + error, + *cell, + "Expected number".to_string(), + )); + } + }; + Ok(values) + } + + fn get_array_of_numbers_xirr( + &mut self, + arg: &Node, + cell: &CellReference, + ) -> Result, CalcResult> { + let mut values = Vec::new(); + match self.evaluate_node_in_context(arg, *cell) { + CalcResult::Range { left, right } => { + if left.sheet != right.sheet { + return Err(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) => { + values.push(value); + } + error @ CalcResult::Error { .. } => return Err(error), + CalcResult::EmptyCell => values.push(0.0), + _ => { + return Err(CalcResult::new_error( + Error::VALUE, + *cell, + "Expected number".to_string(), + )); + } + } + } + } + } + error @ CalcResult::Error { .. } => return Err(error), + _ => { + return Err(CalcResult::new_error( + Error::VALUE, + *cell, + "Expected number".to_string(), + )); + } + }; + Ok(values) + } + + /// PMT(rate, nper, pv, [fv], [type]) + pub(crate) fn fn_pmt(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(3..=5).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // number of periods + let nper = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // present value + let pv = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // future_value + let fv = if arg_count > 3 { + match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.0 + }; + let period_start = if arg_count > 4 { + match self.get_number(&args[4], cell) { + Ok(f) => f != 0.0, + Err(s) => return s, + } + } else { + // at the end of the period + false + }; + match compute_payment(rate, nper, pv, fv, period_start) { + Ok(p) => CalcResult::Number(p), + Err(error) => CalcResult::Error { + error: error.0, + origin: cell, + message: error.1, + }, + } + } + + // PV(rate, nper, pmt, [fv], [type]) + pub(crate) fn fn_pv(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(3..=5).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // nper + let period_count = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // pmt + let payment = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // fv + let future_value = if arg_count > 3 { + match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.0 + }; + let period_start = if arg_count > 4 { + match self.get_number(&args[4], cell) { + Ok(f) => f != 0.0, + Err(s) => return s, + } + } else { + // at the end of the period + false + }; + if rate == 0.0 { + return CalcResult::Number(-future_value - payment * period_count); + } + if rate == -1.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Rate must be != -1".to_string(), + }; + }; + let rate_nper = (1.0 + rate).powf(period_count); + let result = if period_start { + // type = 1 + -(future_value * rate + payment * (1.0 + rate) * (rate_nper - 1.0)) / (rate * rate_nper) + } else { + (-future_value * rate - payment * (rate_nper - 1.0)) / (rate * rate_nper) + }; + if result.is_nan() || result.is_infinite() { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Invalid result".to_string(), + }; + } + + CalcResult::Number(result) + } + + // RATE(nper, pmt, pv, [fv], [type], [guess]) + pub(crate) fn fn_rate(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(3..=5).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let nper = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let pmt = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let pv = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // fv + let fv = if arg_count > 3 { + match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.0 + }; + let annuity_type = if arg_count > 4 { + match self.get_number(&args[4], cell) { + Ok(f) => i32::from(f != 0.0), + Err(s) => return s, + } + } else { + // at the end of the period + 0 + }; + + let guess = if arg_count > 5 { + match self.get_number(&args[5], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.1 + }; + + match compute_rate(pv, fv, nper, pmt, annuity_type, guess) { + Ok(f) => CalcResult::Number(f), + Err(error) => CalcResult::Error { + error: error.0, + origin: cell, + message: error.1, + }, + } + } + + // NPER(rate,pmt,pv,[fv],[type]) + pub(crate) fn fn_nper(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(3..=5).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // pmt + let payment = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // pv + let present_value = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // fv + let future_value = if arg_count > 3 { + match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.0 + }; + let period_start = if arg_count > 4 { + match self.get_number(&args[4], cell) { + Ok(f) => f != 0.0, + Err(s) => return s, + } + } else { + // at the end of the period + false + }; + if rate == 0.0 { + if payment == 0.0 { + return CalcResult::Error { + error: Error::DIV, + origin: cell, + message: "Divide by zero".to_string(), + }; + } + return CalcResult::Number(-(future_value + present_value) / payment); + } + if rate < -1.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Rate must be > -1".to_string(), + }; + }; + let rate_nper = if period_start { + // type = 1 + if payment != 0.0 { + let term = payment * (1.0 + rate) / rate; + (1.0 - future_value / term) / (1.0 + present_value / term) + } else { + -future_value / present_value + } + } else { + // type = 0 + if payment != 0.0 { + let term = payment / rate; + (1.0 - future_value / term) / (1.0 + present_value / term) + } else { + -future_value / present_value + } + }; + if rate_nper <= 0.0 { + return CalcResult::Error { + error: Error::NUM, + origin: cell, + message: "Cannot compute.".to_string(), + }; + } + let result = rate_nper.ln() / (1.0 + rate).ln(); + CalcResult::Number(result) + } + + // FV(rate, nper, pmt, [pv], [type]) + pub(crate) fn fn_fv(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(3..=5).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // number of periods + let nper = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // payment + let pmt = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // present value + let pv = if arg_count > 3 { + match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.0 + }; + let period_start = if arg_count > 4 { + match self.get_number(&args[4], cell) { + Ok(f) => f != 0.0, + Err(s) => return s, + } + } else { + // at the end of the period + false + }; + match compute_future_value(rate, nper, pmt, pv, period_start) { + Ok(f) => CalcResult::Number(f), + Err(error) => CalcResult::Error { + error: error.0, + origin: cell, + message: error.1, + }, + } + } + + // IPMT(rate, per, nper, pv, [fv], [type]) + pub(crate) fn fn_ipmt(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(4..=6).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // per + let period = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // nper + let period_count = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // pv + let present_value = match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // fv + let future_value = if arg_count > 4 { + match self.get_number(&args[4], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.0 + }; + let period_start = if arg_count > 5 { + match self.get_number(&args[5], cell) { + Ok(f) => f != 0.0, + Err(s) => return s, + } + } else { + // at the end of the period + false + }; + let ipmt = match compute_ipmt( + rate, + period, + period_count, + present_value, + future_value, + period_start, + ) { + Ok(f) => f, + Err(error) => { + return CalcResult::Error { + error: error.0, + origin: cell, + message: error.1, + } + } + }; + CalcResult::Number(ipmt) + } + + // PPMT(rate, per, nper, pv, [fv], [type]) + pub(crate) fn fn_ppmt(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(4..=6).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // per + let period = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // nper + let period_count = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // pv + let present_value = match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // fv + let future_value = if arg_count > 4 { + match self.get_number(&args[4], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.0 + }; + let period_start = if arg_count > 5 { + match self.get_number(&args[5], cell) { + Ok(f) => f != 0.0, + Err(s) => return s, + } + } else { + // at the end of the period + false + }; + + let ppmt = match compute_ppmt( + rate, + period, + period_count, + present_value, + future_value, + period_start, + ) { + Ok(f) => f, + Err(error) => { + return CalcResult::Error { + error: error.0, + origin: cell, + message: error.1, + } + } + }; + CalcResult::Number(ppmt) + } + + // NPV(rate, value1, [value2],...) + // npv = Sum[value[i]/(1+rate)^i, {i, 1, n}] + pub(crate) fn fn_npv(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if arg_count < 2 { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let mut values = Vec::new(); + for arg in &args[1..] { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(value) => values.push(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) => { + values.push(value); + } + error @ CalcResult::Error { .. } => return error, + _ => { + // We ignore booleans and strings + } + } + } + } + } + error @ CalcResult::Error { .. } => return error, + _ => { + // We ignore booleans and strings + } + }; + } + match compute_npv(rate, &values) { + Ok(f) => CalcResult::Number(f), + Err(error) => CalcResult::new_error(error.0, cell, error.1), + } + } + + // Returns the internal rate of return for a series of cash flows represented by the numbers + // in values. + // These cash flows do not have to be even, as they would be for an annuity. + // However, the cash flows must occur at regular intervals, such as monthly or annually. + // The internal rate of return is the interest rate received for an investment consisting + // of payments (negative values) and income (positive values) that occur at regular periods + + // IRR(values, [guess]) + pub(crate) fn fn_irr(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if arg_count > 2 || arg_count == 0 { + return CalcResult::new_args_number_error(cell); + } + let values = match self.get_array_of_numbers(&args[0], &cell) { + Ok(s) => s, + Err(error) => return error, + }; + let guess = if arg_count == 2 { + match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.1 + }; + match compute_irr(&values, guess) { + Ok(f) => CalcResult::Number(f), + Err(error) => CalcResult::Error { + error: error.0, + origin: cell, + message: error.1, + }, + } + } + + // XNPV(rate, values, dates) + pub(crate) fn fn_xnpv(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(2..=3).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let values = match self.get_array_of_numbers_xpnv(&args[1], &cell, Error::NUM) { + Ok(s) => s, + Err(error) => return error, + }; + let dates = match self.get_array_of_numbers_xpnv(&args[2], &cell, Error::VALUE) { + Ok(s) => s, + Err(error) => return error, + }; + // Decimal points on dates are truncated + let dates: Vec = dates.iter().map(|s| s.floor()).collect(); + let values_count = values.len(); + // If values and dates contain a different number of values, XNPV returns the #NUM! error value. + if values_count != dates.len() { + return CalcResult::new_error( + Error::NUM, + cell, + "Values and dates must be the same length".to_string(), + ); + } + if values_count == 0 { + return CalcResult::new_error(Error::NUM, cell, "Not enough values".to_string()); + } + let first_date = dates[0]; + for date in &dates { + if !is_valid_date(*date) { + // Excel docs claim that if any number in dates is not a valid date, + // XNPV returns the #VALUE! error value, but it seems to return #VALUE! + return CalcResult::new_error( + Error::NUM, + cell, + "Invalid number for date".to_string(), + ); + } + // If any number in dates precedes the starting date, XNPV returns the #NUM! error value. + if date < &first_date { + return CalcResult::new_error( + Error::NUM, + cell, + "Date precedes the starting date".to_string(), + ); + } + } + // It seems Excel returns #NUM! if rate < 0, this is only necessary if r <= -1 + if rate <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "rate needs to be > 0".to_string()); + } + match compute_xnpv(rate, &values, &dates) { + Ok(f) => CalcResult::Number(f), + Err((error, message)) => CalcResult::new_error(error, cell, message), + } + } + + // XIRR(values, dates, [guess]) + pub(crate) fn fn_xirr(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(2..=3).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let values = match self.get_array_of_numbers_xirr(&args[0], &cell) { + Ok(s) => s, + Err(error) => return error, + }; + let dates = match self.get_array_of_numbers_xirr(&args[1], &cell) { + Ok(s) => s, + Err(error) => return error, + }; + let guess = if arg_count == 3 { + match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + 0.1 + }; + // Decimal points on dates are truncated + let dates: Vec = dates.iter().map(|s| s.floor()).collect(); + let values_count = values.len(); + // If values and dates contain a different number of values, XNPV returns the #NUM! error value. + if values_count != dates.len() { + return CalcResult::new_error( + Error::NUM, + cell, + "Values and dates must be the same length".to_string(), + ); + } + if values_count == 0 { + return CalcResult::new_error(Error::NUM, cell, "Not enough values".to_string()); + } + let first_date = dates[0]; + for date in &dates { + if !is_valid_date(*date) { + return CalcResult::new_error( + Error::NUM, + cell, + "Invalid number for date".to_string(), + ); + } + // If any number in dates precedes the starting date, XIRR returns the #NUM! error value. + if date < &first_date { + return CalcResult::new_error( + Error::NUM, + cell, + "Date precedes the starting date".to_string(), + ); + } + } + match compute_xirr(&values, &dates, guess) { + Ok(f) => CalcResult::Number(f), + Err((error, message)) => CalcResult::Error { + error, + origin: cell, + message, + }, + } + } + + // MIRR(values, finance_rate, reinvest_rate) + // The formula is: + // $$ (-NPV(r1, v_p) * (1+r1)^y)/(NPV(r2, v_n)*(1+r2))^(1/y)-1$$ + // where: + // $r1$ is the reinvest_rate, $r2$ the finance_rate + // $v_p$ the vector of positive values + // $v_n$ the vector of negative values + // and $y$ is dimension of $v$ - 1 (number of years) + pub(crate) fn fn_mirr(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let values = match self.get_array_of_numbers(&args[0], &cell) { + Ok(s) => s, + Err(error) => return error, + }; + let finance_rate = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let reinvest_rate = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let mut positive_values = Vec::new(); + let mut negative_values = Vec::new(); + let mut last_negative_index = -1; + for (index, &value) in values.iter().enumerate() { + let (p, n) = if value >= 0.0 { + (value, 0.0) + } else { + last_negative_index = index as i32; + (0.0, value) + }; + positive_values.push(p); + negative_values.push(n); + } + if last_negative_index == -1 { + return CalcResult::new_error( + Error::DIV, + cell, + "Invalid data for MIRR function".to_string(), + ); + } + // We do a bit of analysis if the rates are -1 as there are some cancellations + // It is probably not important. + let years = values.len() as f64; + let top = if reinvest_rate == -1.0 { + // This is finite + match positive_values.last() { + Some(f) => *f, + None => 0.0, + } + } else { + match compute_npv(reinvest_rate, &positive_values) { + Ok(npv) => -npv * ((1.0 + reinvest_rate).powf(years)), + Err((error, message)) => { + return CalcResult::Error { + error, + origin: cell, + message, + } + } + } + }; + let bottom = if finance_rate == -1.0 { + if last_negative_index == 0 { + // This is still finite + negative_values[last_negative_index as usize] + } else { + // or -Infinity depending of the sign in the last_negative_index coef. + // But it is irrelevant for the calculation + f64::INFINITY + } + } else { + match compute_npv(finance_rate, &negative_values) { + Ok(npv) => npv * (1.0 + finance_rate), + Err((error, message)) => { + return CalcResult::Error { + error, + origin: cell, + message, + } + } + } + }; + + let result = (top / bottom).powf(1.0 / (years - 1.0)) - 1.0; + if result.is_infinite() { + return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); + } + if result.is_nan() { + return CalcResult::new_error(Error::NUM, cell, "Invalid data for MIRR".to_string()); + } + CalcResult::Number(result) + } + + // ISPMT(rate, per, nper, pv) + // Formula is: + // $$pv*rate*\left(\frac{per}{nper}-1\right)$$ + pub(crate) fn fn_ispmt(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 4 { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let per = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let nper = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let pv = match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if nper == 0.0 { + return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); + } + CalcResult::Number(pv * rate * (per / nper - 1.0)) + } + + // RRI(nper, pv, fv) + // Formula is + // $$ \left(\frac{fv}{pv}\right)^{\frac{1}{nper}}-1 $$ + pub(crate) fn fn_rri(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let nper = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let pv = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let fv = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if nper <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "nper should be >0".to_string()); + } + if pv == 0.0 { + // Note error is NUM not DIV/0 also bellow + return CalcResult::new_error(Error::NUM, cell, "Division by 0".to_string()); + } + let result = (fv / pv).powf(1.0 / nper) - 1.0; + if result.is_infinite() { + return CalcResult::new_error(Error::NUM, cell, "Division by 0".to_string()); + } + if result.is_nan() { + return CalcResult::new_error(Error::NUM, cell, "Invalid data for RRI".to_string()); + } + + CalcResult::Number(result) + } + + // SLN(cost, salvage, life) + // Formula is: + // $$ \frac{cost-salvage}{life} $$ + pub(crate) fn fn_sln(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let cost = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let salvage = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let life = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if life == 0.0 { + return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); + } + let result = (cost - salvage) / life; + + CalcResult::Number(result) + } + + // SYD(cost, salvage, life, per) + // Formula is: + // $$ \frac{(cost-salvage)*(life-per+1)*2}{life*(life+1)} $$ + pub(crate) fn fn_syd(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 4 { + return CalcResult::new_args_number_error(cell); + } + let cost = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let salvage = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let life = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let per = match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if life == 0.0 { + return CalcResult::new_error(Error::NUM, cell, "Division by 0".to_string()); + } + if per > life || per <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "per should be <= life".to_string()); + } + let result = ((cost - salvage) * (life - per + 1.0) * 2.0) / (life * (life + 1.0)); + + CalcResult::Number(result) + } + + // NOMINAL(effective_rate, npery) + // Formula is: + // $$ n\times\left(\left(1+r\right)^{\frac{1}{n}}-1\right) $$ + // where: + // $r$ is the effective interest rate + // $n$ is the number of periods per year + pub(crate) fn fn_nominal(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let effect_rate = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let npery = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + if effect_rate <= 0.0 || npery < 1.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid arguments".to_string()); + } + let result = ((1.0 + effect_rate).powf(1.0 / npery) - 1.0) * npery; + if result.is_infinite() { + return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); + } + if result.is_nan() { + return CalcResult::new_error(Error::NUM, cell, "Invalid data for RRI".to_string()); + } + + CalcResult::Number(result) + } + + // EFFECT(nominal_rate, npery) + // Formula is: + // $$ \left(1+\frac{r}{n}\right)^n-1 $$ + // where: + // $r$ is the nominal interest rate + // $n$ is the number of periods per year + pub(crate) fn fn_effect(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let nominal_rate = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let npery = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f.floor(), + Err(s) => return s, + }; + if nominal_rate <= 0.0 || npery < 1.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid arguments".to_string()); + } + let result = (1.0 + nominal_rate / npery).powf(npery) - 1.0; + if result.is_infinite() { + return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); + } + if result.is_nan() { + return CalcResult::new_error(Error::NUM, cell, "Invalid data for RRI".to_string()); + } + + CalcResult::Number(result) + } + + // PDURATION(rate, pv, fv) + // Formula is: + // $$ \frac{log(fv) - log(pv)}{log(1+r)} $$ + // where: + // * $r$ is the interest rate per period + // * $pv$ is the present value of the investment + // * $fv$ is the desired future value of the investment + pub(crate) fn fn_pduration(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let pv = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let fv = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if fv <= 0.0 || pv <= 0.0 || rate <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid arguments".to_string()); + } + let result = (fv.ln() - pv.ln()) / ((1.0 + rate).ln()); + if result.is_infinite() { + return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); + } + if result.is_nan() { + return CalcResult::new_error(Error::NUM, cell, "Invalid data for RRI".to_string()); + } + + CalcResult::Number(result) + } + + /// This next three functions deal with Treasure Bills or T-Bills for short + /// They are zero-coupon that mature in one year or less. + /// Definitions: + /// $r$ be the discount rate + /// $v$ the face value of the Bill + /// $p$ the price of the Bill + /// $d_m$ is the number of days from the settlement to maturity + /// Then: + /// $$ p = v \times\left(1-\frac{d_m}{r}\right) $$ + /// If d_m is less than 183 days the he Bond Equivalent Yield (BEY, here $y$) is given by: + /// $$ y = \frac{F - B}{M}\times \frac{365}{d_m} = \frac{365\times r}{360-r\times d_m} + /// If d_m>= 183 days things are a bit more complicated. + /// Let $d_e = d_m - 365/2$ if $d_m <= 365$ or $d_e = 183$ if $d_m = 366$. + /// $$ v = p\times \left(1+\frac{y}{2}\right)\left(1+d_e\times\frac{y}{365}\right) $$ + /// Together with the previous relation of $p$ and $v$ gives us a quadratic equation for $y$. + + // TBILLEQ(settlement, maturity, discount) + pub(crate) fn fn_tbilleq(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let settlement = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let maturity = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let discount = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if !is_valid_date(settlement) || !is_valid_date(maturity) { + return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()); + } + if settlement > maturity { + return CalcResult::new_error( + Error::NUM, + cell, + "settlement should be <= maturity".to_string(), + ); + } + if !is_less_than_one_year(settlement as i64, maturity as i64) { + return CalcResult::new_error( + Error::NUM, + cell, + "maturity <= settlement + year".to_string(), + ); + } + if discount <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "discount should be >0".to_string()); + } + // days to maturity + let d_m = maturity - settlement; + let result = if d_m < 183.0 { + 365.0 * discount / (360.0 - discount * d_m) + } else { + // Equation here is: + // (1-days*rate/360)*(1+y/2)*(1+d_extra*y/year)=1 + let year = if d_m == 366.0 { 366.0 } else { 365.0 }; + let d_extra = d_m - year / 2.0; + let alpha = 1.0 - d_m * discount / 360.0; + let beta = 0.5 + d_extra / year; + // ay^2+by+c=0 + let a = d_extra * alpha / (year * 2.0); + let b = alpha * beta; + let c = alpha - 1.0; + (-b + (b * b - 4.0 * a * c).sqrt()) / (2.0 * a) + }; + if result.is_infinite() { + return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); + } + if result.is_nan() { + return CalcResult::new_error(Error::NUM, cell, "Invalid data for RRI".to_string()); + } + + CalcResult::Number(result) + } + + // TBILLPRICE(settlement, maturity, discount) + pub(crate) fn fn_tbillprice(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let settlement = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let maturity = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let discount = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if !is_valid_date(settlement) || !is_valid_date(maturity) { + return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()); + } + if settlement > maturity { + return CalcResult::new_error( + Error::NUM, + cell, + "settlement should be <= maturity".to_string(), + ); + } + if !is_less_than_one_year(settlement as i64, maturity as i64) { + return CalcResult::new_error( + Error::NUM, + cell, + "maturity <= settlement + year".to_string(), + ); + } + if discount <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "discount should be >0".to_string()); + } + // days to maturity + let d_m = maturity - settlement; + let result = 100.0 * (1.0 - discount * d_m / 360.0); + if result.is_infinite() { + return CalcResult::new_error(Error::DIV, cell, "Division by 0".to_string()); + } + if result.is_nan() || result < 0.0 { + return CalcResult::new_error(Error::NUM, cell, "Invalid data for RRI".to_string()); + } + + CalcResult::Number(result) + } + + // TBILLYIELD(settlement, maturity, pr) + pub(crate) fn fn_tbillyield(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let settlement = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let maturity = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let pr = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if !is_valid_date(settlement) || !is_valid_date(maturity) { + return CalcResult::new_error(Error::NUM, cell, "Invalid date".to_string()); + } + if settlement > maturity { + return CalcResult::new_error( + Error::NUM, + cell, + "settlement should be <= maturity".to_string(), + ); + } + if !is_less_than_one_year(settlement as i64, maturity as i64) { + return CalcResult::new_error( + Error::NUM, + cell, + "maturity <= settlement + year".to_string(), + ); + } + if pr <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "discount should be >0".to_string()); + } + let days = maturity - settlement; + let result = (100.0 - pr) * 360.0 / (pr * days); + + CalcResult::Number(result) + } + + // DOLLARDE(fractional_dollar, fraction) + pub(crate) fn fn_dollarde(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let fractional_dollar = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let mut fraction = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if fraction < 0.0 { + return CalcResult::new_error(Error::NUM, cell, "fraction should be >= 1".to_string()); + } + if fraction < 1.0 { + // this is not necessarily DIV/0 + return CalcResult::new_error(Error::DIV, cell, "fraction should be >= 1".to_string()); + } + fraction = fraction.trunc(); + while fraction > 10.0 { + fraction /= 10.0; + } + let t = fractional_dollar.trunc(); + let result = t + (fractional_dollar - t) * 10.0 / fraction; + CalcResult::Number(result) + } + + // DOLLARFR(decimal_dollar, fraction) + pub(crate) fn fn_dollarfr(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let decimal_dollar = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let mut fraction = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + if fraction < 0.0 { + return CalcResult::new_error(Error::NUM, cell, "fraction should be >= 1".to_string()); + } + if fraction < 1.0 { + // this is not necessarily DIV/0 + return CalcResult::new_error(Error::DIV, cell, "fraction should be >= 1".to_string()); + } + fraction = fraction.trunc(); + while fraction > 10.0 { + fraction /= 10.0; + } + let t = decimal_dollar.trunc(); + let result = t + (decimal_dollar - t) * fraction / 10.0; + CalcResult::Number(result) + } + + // CUMIPMT(rate, nper, pv, start_period, end_period, type) + pub(crate) fn fn_cumipmt(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 6 { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let nper = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let pv = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let start_period = match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f.ceil() as i32, + Err(s) => return s, + }; + let end_period = match self.get_number_no_bools(&args[4], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + }; + // 0 at the end of the period, 1 at the beginning of the period + let period_type = match self.get_number_no_bools(&args[5], cell) { + Ok(f) => { + if f == 0.0 { + false + } else if f == 1.0 { + true + } else { + return CalcResult::new_error( + Error::NUM, + cell, + "invalid period type".to_string(), + ); + } + } + Err(s) => return s, + }; + if start_period > end_period { + return CalcResult::new_error( + Error::NUM, + cell, + "start period should come before end period".to_string(), + ); + } + if rate <= 0.0 || nper <= 0.0 || pv <= 0.0 || start_period < 1 { + return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string()); + } + let mut result = 0.0; + for period in start_period..=end_period { + result += match compute_ipmt(rate, period as f64, nper, pv, 0.0, period_type) { + Ok(f) => f, + Err(error) => { + return CalcResult::Error { + error: error.0, + origin: cell, + message: error.1, + } + } + } + } + CalcResult::Number(result) + } + + // CUMPRINC(rate, nper, pv, start_period, end_period, type) + pub(crate) fn fn_cumprinc(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 6 { + return CalcResult::new_args_number_error(cell); + } + let rate = match self.get_number_no_bools(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let nper = match self.get_number_no_bools(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let pv = match self.get_number_no_bools(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let start_period = match self.get_number_no_bools(&args[3], cell) { + Ok(f) => f.ceil() as i32, + Err(s) => return s, + }; + let end_period = match self.get_number_no_bools(&args[4], cell) { + Ok(f) => f.trunc() as i32, + Err(s) => return s, + }; + // 0 at the end of the period, 1 at the beginning of the period + let period_type = match self.get_number_no_bools(&args[5], cell) { + Ok(f) => { + if f == 0.0 { + false + } else if f == 1.0 { + true + } else { + return CalcResult::new_error( + Error::NUM, + cell, + "invalid period type".to_string(), + ); + } + } + Err(s) => return s, + }; + if start_period > end_period { + return CalcResult::new_error( + Error::NUM, + cell, + "start period should come before end period".to_string(), + ); + } + if rate <= 0.0 || nper <= 0.0 || pv <= 0.0 || start_period < 1 { + return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string()); + } + let mut result = 0.0; + for period in start_period..=end_period { + result += match compute_ppmt(rate, period as f64, nper, pv, 0.0, period_type) { + Ok(f) => f, + Err(error) => { + return CalcResult::Error { + error: error.0, + origin: cell, + message: error.1, + } + } + } + } + CalcResult::Number(result) + } + + // DDB(cost, salvage, life, period, [factor]) + pub(crate) fn fn_ddb(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(4..=5).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let cost = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let salvage = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let life = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let period = match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + }; + // The rate at which the balance declines. + let factor = if arg_count > 4 { + match self.get_number_no_bools(&args[4], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + // If factor is omitted, it is assumed to be 2 (the double-declining balance method). + 2.0 + }; + if period > life || cost < 0.0 || salvage < 0.0 || period <= 0.0 || factor <= 0.0 { + return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string()); + }; + // let period_trunc = period.floor() as i32; + let mut rate = factor / life; + if rate > 1.0 { + rate = 1.0 + }; + let value = if rate == 1.0 { + if period == 1.0 { + cost + } else { + 0.0 + } + } else { + cost * (1.0 - rate).powf(period - 1.0) + }; + let new_value = cost * (1.0 - rate).powf(period); + let result = f64::max(value - f64::max(salvage, new_value), 0.0); + CalcResult::Number(result) + } + + // DB(cost, salvage, life, period, [month]) + pub(crate) fn fn_db(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(4..=5).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let cost = match self.get_number(&args[0], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let salvage = match self.get_number(&args[1], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let life = match self.get_number(&args[2], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let period = match self.get_number(&args[3], cell) { + Ok(f) => f, + Err(s) => return s, + }; + let month = if arg_count > 4 { + match self.get_number_no_bools(&args[4], cell) { + Ok(f) => f.trunc(), + Err(s) => return s, + } + } else { + 12.0 + }; + if month == 12.0 && period > life + || (period > life + 1.0) + || month <= 0.0 + || month > 12.0 + || period <= 0.0 + || cost < 0.0 + { + return CalcResult::new_error(Error::NUM, cell, "invalid parameters".to_string()); + }; + if cost == 0.0 { + return CalcResult::Number(0.0); + } + // rounded to three decimal places + // FIXME: We should have utilities for this (see to_precision) + let rate = f64::round((1.0 - f64::powf(salvage / cost, 1.0 / life)) * 1000.0) / 1000.0; + + let mut result = cost * rate * month / 12.0; + + let period = period.floor() as i32; + let life = life.floor() as i32; + + // Depreciation for the first and last periods is a special case. + if period == 1 { + return CalcResult::Number(result); + }; + + for _ in 0..period - 2 { + result += (cost - result) * rate; + } + + if period == life + 1 { + // last period + return CalcResult::Number((cost - result) * rate * (12.0 - month) / 12.0); + } + + CalcResult::Number(rate * (cost - result)) + } +} diff --git a/base/src/functions/financial_util.rs b/base/src/functions/financial_util.rs new file mode 100644 index 0000000..3bf143d --- /dev/null +++ b/base/src/functions/financial_util.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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())) +} diff --git a/base/src/functions/information.rs b/base/src/functions/information.rs new file mode 100644 index 0000000..1c76e38 --- /dev/null +++ b/base/src/functions/information.rs @@ -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(), + } + } +} diff --git a/base/src/functions/logical.rs b/base/src/functions/logical.rs new file mode 100644 index 0000000..5aa2347 --- /dev/null +++ b/base/src/functions/logical.rs @@ -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(), + } + } +} diff --git a/base/src/functions/lookup_and_reference.rs b/base/src/functions/lookup_and_reference.rs new file mode 100644 index 0000000..f0d6dde --- /dev/null +++ b/base/src/functions/lookup_and_reference.rs @@ -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 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 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 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 } + } +} diff --git a/base/src/functions/mathematical.rs b/base/src/functions/mathematical.rs new file mode 100644 index 0000000..d3be894 --- /dev/null +++ b/base/src/functions/mathematical.rs @@ -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()) + } +} diff --git a/base/src/functions/mod.rs b/base/src/functions/mod.rs new file mode 100644 index 0000000..642fa54 --- /dev/null +++ b/base/src/functions/mod.rs @@ -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 { + 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), + } + } +} diff --git a/base/src/functions/statistical.rs b/base/src/functions/statistical.rs new file mode 100644 index 0000000..ab59f15 --- /dev/null +++ b/base/src/functions/statistical.rs @@ -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::() { + 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::() { + 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::().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( + &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) + } +} diff --git a/base/src/functions/subtotal.rs b/base/src/functions/subtotal.rs new file mode 100644 index 0000000..4bef13a --- /dev/null +++ b/base/src/functions/subtotal.rs @@ -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, CalcResult> { + let mut result: Vec = 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) + } +} diff --git a/base/src/functions/text.rs b/base/src/functions/text.rs new file mode 100644 index 0000000..177ba76 --- /dev/null +++ b/base/src/functions/text.rs @@ -0,0 +1,1104 @@ +use crate::{ + calc_result::{CalcResult, CellReference}, + constants::{LAST_COLUMN, LAST_ROW}, + expressions::parser::Node, + expressions::token::Error, + formatter::format::{format_number, parse_formatted_number}, + model::Model, + number_format::to_precision, +}; + +use super::{ + text_util::{substitute, text_after, text_before, Case}, + util::from_wildcard_to_regex, +}; + +/// Finds the first instance of 'search_for' in text starting at char index start +fn find(search_for: &str, text: &str, start: usize) -> Option { + let ch = text.chars(); + let mut byte_index = 0; + for (char_index, c) in ch.enumerate() { + if char_index + 1 >= start && text[byte_index..].starts_with(search_for) { + return Some((char_index + 1) as i32); + } + byte_index += c.len_utf8(); + } + None +} + +/// You can use the wildcard characters — the question mark (?) and asterisk (*) — in the find_text argument. +/// * A question mark matches any single character. +/// * An asterisk matches any sequence of characters. +/// * If you want to find an actual question mark or asterisk, type a tilde (~) before the character. +fn search(search_for: &str, text: &str, start: usize) -> Option { + let re = match from_wildcard_to_regex(search_for, false) { + Ok(r) => r, + Err(_) => return None, + }; + + let ch = text.chars(); + let mut byte_index = 0; + for (char_index, c) in ch.enumerate() { + if char_index + 1 >= start { + if let Some(m) = re.find(&text[byte_index..]) { + return Some((text[0..(m.start() + byte_index)].chars().count() as i32) + 1); + } else { + return None; + } + } + byte_index += c.len_utf8(); + } + None +} + +impl Model { + pub(crate) fn fn_concat(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let mut result = "".to_string(); + for arg in args { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::String(value) => result = format!("{}{}", result, value), + CalcResult::Number(value) => result = format!("{}{}", result, value), + CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Boolean(value) => { + if value { + result = format!("{}TRUE", result); + } else { + result = format!("{}FALSE", result); + } + } + error @ CalcResult::Error { .. } => return error, + 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(value) => { + result = format!("{}{}", result, value); + } + CalcResult::Number(value) => { + result = format!("{}{}", result, value) + } + CalcResult::Boolean(value) => { + if value { + result = format!("{}TRUE", result); + } else { + result = format!("{}FALSE", result); + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::EmptyCell | CalcResult::EmptyArg => {} + CalcResult::Range { .. } => {} + } + } + } + } + }; + } + CalcResult::String(result) + } + pub(crate) fn fn_text(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() == 2 { + let value = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Number(f) => f, + CalcResult::String(s) => { + return CalcResult::String(s); + } + CalcResult::Boolean(b) => { + return CalcResult::Boolean(b); + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => 0.0, + }; + let format_code = match self.get_string(&args[1], cell) { + Ok(s) => s, + Err(s) => return s, + }; + let d = format_number(value, &format_code, &self.locale); + if let Some(_e) = d.error { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid format code".to_string(), + }; + } + CalcResult::String(d.text) + } else { + CalcResult::new_args_number_error(cell) + } + } + + /// FIND(find_text, within_text, [start_num]) + /// * FIND and FINDB are case sensitive and don't allow wildcard characters. + /// * If find_text is "" (empty text), FIND matches the first character in the search string (that is, the character numbered start_num or 1). + /// * Find_text cannot contain any wildcard characters. + /// * If find_text does not appear in within_text, FIND and FINDB return the #VALUE! error value. + /// * If start_num is not greater than zero, FIND and FINDB return the #VALUE! error value. + /// * If start_num is greater than the length of within_text, FIND and FINDB return the #VALUE! error value. + /// NB: FINDB is not implemented. It is the same as FIND function unless locale is a DBCS (Double Byte Character Set) + pub(crate) fn fn_find(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() < 2 || args.len() > 3 { + return CalcResult::new_args_number_error(cell); + } + let find_text = match self.get_string(&args[0], cell) { + Ok(s) => s, + Err(s) => return s, + }; + let within_text = match self.get_string(&args[1], cell) { + Ok(s) => s, + Err(s) => return s, + }; + let start_num = if args.len() == 3 { + match self.get_number(&args[2], cell) { + Ok(s) => s.floor(), + Err(s) => return s, + } + } else { + 1.0 + }; + + if start_num < 1.0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Start num must be >= 1".to_string(), + }; + } + let start_num = start_num as usize; + + if start_num > within_text.len() { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Start num greater than length".to_string(), + }; + } + if let Some(s) = find(&find_text, &within_text, start_num) { + CalcResult::Number(s as f64) + } else { + CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Text not found".to_string(), + } + } + } + + /// Same API as FIND but: + /// * Allows wildcards + /// * It is case insensitive + /// SEARCH(find_text, within_text, [start_num]) + pub(crate) fn fn_search(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() < 2 || args.len() > 3 { + return CalcResult::new_args_number_error(cell); + } + let find_text = match self.get_string(&args[0], cell) { + Ok(s) => s, + Err(s) => return s, + }; + let within_text = match self.get_string(&args[1], cell) { + Ok(s) => s, + Err(s) => return s, + }; + let start_num = if args.len() == 3 { + match self.get_number(&args[2], cell) { + Ok(s) => s.floor(), + Err(s) => return s, + } + } else { + 1.0 + }; + + if start_num < 1.0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Start num must be >= 1".to_string(), + }; + } + let start_num = start_num as usize; + + if start_num > within_text.len() { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Start num greater than length".to_string(), + }; + } + // SEARCH is case insensitive + if let Some(s) = search( + &find_text.to_lowercase(), + &within_text.to_lowercase(), + start_num, + ) { + CalcResult::Number(s as f64) + } else { + CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Text not found".to_string(), + } + } + } + + // LEN, LEFT, RIGHT, MID, LOWER, UPPER, TRIM + pub(crate) fn fn_len(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() == 1 { + let s = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Number(v) => format!("{}", v), + CalcResult::String(v) => v, + CalcResult::Boolean(b) => { + if b { + "TRUE".to_string() + } else { + "FALSE".to_string() + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + }; + return CalcResult::Number(s.chars().count() as f64); + } + CalcResult::new_args_number_error(cell) + } + + pub(crate) fn fn_trim(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() == 1 { + let s = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Number(v) => format!("{}", v), + CalcResult::String(v) => v, + CalcResult::Boolean(b) => { + if b { + "TRUE".to_string() + } else { + "FALSE".to_string() + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + }; + return CalcResult::String(s.trim().to_owned()); + } + CalcResult::new_args_number_error(cell) + } + + pub(crate) fn fn_lower(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() == 1 { + let s = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Number(v) => format!("{}", v), + CalcResult::String(v) => v, + CalcResult::Boolean(b) => { + if b { + "TRUE".to_string() + } else { + "FALSE".to_string() + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + }; + return CalcResult::String(s.to_lowercase()); + } + CalcResult::new_args_number_error(cell) + } + + pub(crate) fn fn_upper(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() == 1 { + let s = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Number(v) => format!("{}", v), + CalcResult::String(v) => v, + CalcResult::Boolean(b) => { + if b { + "TRUE".to_string() + } else { + "FALSE".to_string() + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + }; + return CalcResult::String(s.to_uppercase()); + } + CalcResult::new_args_number_error(cell) + } + + pub(crate) fn fn_left(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() > 2 || args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let s = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Number(v) => format!("{}", v), + CalcResult::String(v) => v, + CalcResult::Boolean(b) => { + if b { + "TRUE".to_string() + } else { + "FALSE".to_string() + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + }; + let num_chars = if args.len() == 2 { + match self.evaluate_node_in_context(&args[1], cell) { + CalcResult::Number(v) => { + if v < 0.0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Number must be >= 0".to_string(), + }; + } + v.floor() as usize + } + CalcResult::Boolean(_) | CalcResult::String(_) => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Expecting number".to_string(), + }; + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => 0, + } + } else { + 1 + }; + let mut result = "".to_string(); + for (index, ch) in s.chars().enumerate() { + if index >= num_chars { + break; + } + result.push(ch); + } + CalcResult::String(result) + } + + pub(crate) fn fn_right(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() > 2 || args.is_empty() { + return CalcResult::new_args_number_error(cell); + } + let s = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Number(v) => format!("{}", v), + CalcResult::String(v) => v, + CalcResult::Boolean(b) => { + if b { + "TRUE".to_string() + } else { + "FALSE".to_string() + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + }; + let num_chars = if args.len() == 2 { + match self.evaluate_node_in_context(&args[1], cell) { + CalcResult::Number(v) => { + if v < 0.0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Number must be >= 0".to_string(), + }; + } + v.floor() as usize + } + CalcResult::Boolean(_) | CalcResult::String(_) => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Expecting number".to_string(), + }; + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => 0, + } + } else { + 1 + }; + let mut result = "".to_string(); + for (index, ch) in s.chars().rev().enumerate() { + if index >= num_chars { + break; + } + result.push(ch); + } + return CalcResult::String(result.chars().rev().collect::()); + } + + pub(crate) fn fn_mid(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 3 { + return CalcResult::new_args_number_error(cell); + } + let s = match self.evaluate_node_in_context(&args[0], cell) { + CalcResult::Number(v) => format!("{}", v), + CalcResult::String(v) => v, + CalcResult::Boolean(b) => { + if b { + "TRUE".to_string() + } else { + "FALSE".to_string() + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => "".to_string(), + }; + let start_num = match self.evaluate_node_in_context(&args[1], cell) { + CalcResult::Number(v) => { + if v < 1.0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Number must be >= 1".to_string(), + }; + } + v.floor() as usize + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + _ => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Expecting number".to_string(), + }; + } + }; + let num_chars = match self.evaluate_node_in_context(&args[2], cell) { + CalcResult::Number(v) => { + if v < 0.0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Number must be >= 0".to_string(), + }; + } + v.floor() as usize + } + CalcResult::String(_) => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Expecting number".to_string(), + }; + } + CalcResult::Boolean(_) => { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Expecting number".to_string(), + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::Range { .. } => { + // Implicit Intersection not implemented + return CalcResult::Error { + error: Error::NIMPL, + origin: cell, + message: "Implicit Intersection not implemented".to_string(), + }; + } + CalcResult::EmptyCell | CalcResult::EmptyArg => 0, + }; + let mut result = "".to_string(); + let mut count: usize = 0; + for (index, ch) in s.chars().enumerate() { + if count >= num_chars { + break; + } + if index + 1 >= start_num { + result.push(ch); + count += 1; + } + } + CalcResult::String(result) + } + + // REPT(text, number_times) + pub(crate) fn fn_rept(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let text = match self.get_string(&args[0], cell) { + Ok(s) => s, + Err(error) => return error, + }; + let number_times = match self.get_number(&args[1], cell) { + Ok(f) => f.floor() as i32, + Err(s) => return s, + }; + let text_len = text.len() as i32; + + // We normally don't follow Excel's sometimes archaic size's restrictions + // But this might be a security issue + if text_len * number_times > 32767 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "number times too high".to_string(), + }; + } + if number_times < 0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "number times too high".to_string(), + }; + } + if number_times == 0 { + return CalcResult::String("".to_string()); + } + CalcResult::String(text.repeat(number_times as usize)) + } + + // TEXTAFTER(text, delimiter, [instance_num], [match_mode], [match_end], [if_not_found]) + pub(crate) fn fn_textafter(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(2..=6).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let text = match self.get_string(&args[0], cell) { + Ok(s) => s, + Err(error) => return error, + }; + let delimiter = match self.get_string(&args[1], cell) { + Ok(s) => s, + Err(error) => return error, + }; + let instance_num = if arg_count > 2 { + match self.get_number(&args[2], cell) { + Ok(f) => f.floor() as i32, + Err(s) => return s, + } + } else { + 1 + }; + let match_mode = if arg_count > 3 { + match self.get_number(&args[3], cell) { + Ok(f) => { + if f == 0.0 { + Case::Sensitive + } else { + Case::Insensitive + } + } + Err(s) => return s, + } + } else { + Case::Sensitive + }; + + let match_end = if arg_count > 4 { + match self.get_number(&args[4], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + // disabled by default + // the delimiter is specified in the formula + 0.0 + }; + if instance_num == 0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "instance_num must be <> 0".to_string(), + }; + } + if delimiter.len() > text.len() { + // so this is fun(!) + // if the function was provided with two arguments is a #VALUE! + // if it had more is a #N/A (irrespective of their values) + if arg_count > 2 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "The delimiter is longer than the text is trying to match".to_string(), + }; + } else { + return CalcResult::Error { + error: Error::NA, + origin: cell, + message: "The delimiter is longer than the text is trying to match".to_string(), + }; + } + } + if match_end != 0.0 && match_end != 1.0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "argument must be 0 or 1".to_string(), + }; + }; + match text_after(&text, &delimiter, instance_num, match_mode) { + Some(s) => CalcResult::String(s), + None => { + if match_end == 1.0 { + if instance_num == 1 { + return CalcResult::String("".to_string()); + } else if instance_num == -1 { + return CalcResult::String(text); + } + } + if arg_count == 6 { + // An empty cell is converted to empty string (not 0) + match self.evaluate_node_in_context(&args[5], cell) { + CalcResult::EmptyCell => CalcResult::String("".to_string()), + result => result, + } + } else { + CalcResult::Error { + error: Error::NA, + origin: cell, + message: "Value not found".to_string(), + } + } + } + } + } + + pub(crate) fn fn_textbefore(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(2..=6).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let text = match self.get_string(&args[0], cell) { + Ok(s) => s, + Err(error) => return error, + }; + let delimiter = match self.get_string(&args[1], cell) { + Ok(s) => s, + Err(error) => return error, + }; + let instance_num = if arg_count > 2 { + match self.get_number(&args[2], cell) { + Ok(f) => f.floor() as i32, + Err(s) => return s, + } + } else { + 1 + }; + let match_mode = if arg_count > 3 { + match self.get_number(&args[3], cell) { + Ok(f) => { + if f == 0.0 { + Case::Sensitive + } else { + Case::Insensitive + } + } + Err(s) => return s, + } + } else { + Case::Sensitive + }; + + let match_end = if arg_count > 4 { + match self.get_number(&args[4], cell) { + Ok(f) => f, + Err(s) => return s, + } + } else { + // disabled by default + // the delimiter is specified in the formula + 0.0 + }; + if instance_num == 0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "instance_num must be <> 0".to_string(), + }; + } + if delimiter.len() > text.len() { + // so this is fun(!) + // if the function was provided with two arguments is a #VALUE! + // if it had more is a #N/A (irrespective of their values) + if arg_count > 2 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "The delimiter is longer than the text is trying to match".to_string(), + }; + } else { + return CalcResult::Error { + error: Error::NA, + origin: cell, + message: "The delimiter is longer than the text is trying to match".to_string(), + }; + } + } + if match_end != 0.0 && match_end != 1.0 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "argument must be 0 or 1".to_string(), + }; + }; + match text_before(&text, &delimiter, instance_num, match_mode) { + Some(s) => CalcResult::String(s), + None => { + if match_end == 1.0 { + if instance_num == -1 { + return CalcResult::String("".to_string()); + } else if instance_num == 1 { + return CalcResult::String(text); + } + } + if arg_count == 6 { + // An empty cell is converted to empty string (not 0) + match self.evaluate_node_in_context(&args[5], cell) { + CalcResult::EmptyCell => CalcResult::String("".to_string()), + result => result, + } + } else { + CalcResult::Error { + error: Error::NA, + origin: cell, + message: "Value not found".to_string(), + } + } + } + } + } + + // TEXTJOIN(delimiter, ignore_empty, text1, [text2], …) + pub(crate) fn fn_textjoin(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if arg_count < 3 { + return CalcResult::new_args_number_error(cell); + } + let delimiter = match self.get_string(&args[0], cell) { + Ok(s) => s, + Err(error) => return error, + }; + let ignore_empty = match self.get_boolean(&args[1], cell) { + Ok(b) => b, + Err(error) => return error, + }; + let mut values = Vec::new(); + for arg in &args[2..] { + match self.evaluate_node_in_context(arg, cell) { + CalcResult::Number(value) => values.push(format!("{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) => { + values.push(format!("{value}")); + } + CalcResult::String(value) => values.push(value), + CalcResult::Boolean(value) => { + if value { + values.push("TRUE".to_string()) + } else { + values.push("FALSE".to_string()) + } + } + CalcResult::EmptyCell => { + if !ignore_empty { + values.push("".to_string()) + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::EmptyArg | CalcResult::Range { .. } => {} + } + } + } + } + error @ CalcResult::Error { .. } => return error, + CalcResult::String(value) => values.push(value), + CalcResult::Boolean(value) => { + if value { + values.push("TRUE".to_string()) + } else { + values.push("FALSE".to_string()) + } + } + CalcResult::EmptyCell => { + if !ignore_empty { + values.push("".to_string()) + } + } + CalcResult::EmptyArg => {} + }; + } + let result = values.join(&delimiter); + CalcResult::String(result) + } + + // SUBSTITUTE(text, old_text, new_text, [instance_num]) + pub(crate) fn fn_substitute(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if !(2..=4).contains(&arg_count) { + return CalcResult::new_args_number_error(cell); + } + let text = match self.get_string(&args[0], cell) { + Ok(s) => s, + Err(error) => return error, + }; + let old_text = match self.get_string(&args[1], cell) { + Ok(s) => s, + Err(error) => return error, + }; + let new_text = match self.get_string(&args[2], cell) { + Ok(s) => s, + Err(error) => return error, + }; + let instance_num = if arg_count > 3 { + match self.get_number(&args[3], cell) { + Ok(f) => Some(f.floor() as i32), + Err(s) => return s, + } + } else { + // means every instance is replaced + None + }; + if let Some(num) = instance_num { + if num < 1 { + return CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid value".to_string(), + }; + } + if old_text.is_empty() { + return CalcResult::String(text); + } + CalcResult::String(substitute(&text, &old_text, &new_text, num)) + } else { + if old_text.is_empty() { + return CalcResult::String(text); + } + CalcResult::String(text.replace(&old_text, &new_text)) + } + } + pub(crate) fn fn_concatenate(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + let arg_count = args.len(); + if arg_count == 0 { + return CalcResult::new_args_number_error(cell); + } + let mut text_array = Vec::new(); + for arg in args { + let text = match self.get_string(arg, cell) { + Ok(s) => s, + Err(error) => return error, + }; + text_array.push(text) + } + CalcResult::String(text_array.join("")) + } + + pub(crate) fn fn_exact(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 2 { + return CalcResult::new_args_number_error(cell); + } + let result1 = &self.evaluate_node_in_context(&args[0], cell); + let result2 = &self.evaluate_node_in_context(&args[1], cell); + // FIXME: Implicit intersection + if let (CalcResult::Number(number1), CalcResult::Number(number2)) = (result1, result2) { + // In Excel two numbers are the same if they are the same up to 15 digits. + CalcResult::Boolean(to_precision(*number1, 15) == to_precision(*number2, 15)) + } else { + let string1 = match self.cast_to_string(result1.clone(), cell) { + Ok(s) => s, + Err(error) => return error, + }; + let string2 = match self.cast_to_string(result2.clone(), cell) { + Ok(s) => s, + Err(error) => return error, + }; + CalcResult::Boolean(string1 == string2) + } + } + // VALUE(text) + pub(crate) fn fn_value(&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(text) => { + let currencies = vec!["$", "€"]; + if let Ok((value, _)) = parse_formatted_number(&text, ¤cies) { + return CalcResult::Number(value); + }; + CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid number".to_string(), + } + } + CalcResult::Number(f) => CalcResult::Number(f), + CalcResult::Boolean(_) => CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid number".to_string(), + }, + error @ CalcResult::Error { .. } => error, + CalcResult::Range { .. } => { + // TODO Implicit Intersection + CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid number".to_string(), + } + } + CalcResult::EmptyCell | CalcResult::EmptyArg => CalcResult::Number(0.0), + } + } + + pub(crate) fn fn_t(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 1 { + return CalcResult::new_args_number_error(cell); + } + // FIXME: Implicit intersection + let result = self.evaluate_node_in_context(&args[0], cell); + match result { + CalcResult::String(_) => result, + error @ CalcResult::Error { .. } => error, + _ => CalcResult::String("".to_string()), + } + } + + // VALUETOTEXT(value) + pub(crate) fn fn_valuetotext(&mut self, args: &[Node], cell: CellReference) -> CalcResult { + if args.len() != 1 { + return CalcResult::new_args_number_error(cell); + } + let text = match self.get_string(&args[0], cell) { + Ok(s) => s, + Err(error) => match error { + CalcResult::Error { error, .. } => error.to_string(), + _ => "".to_string(), + }, + }; + CalcResult::String(text) + } +} diff --git a/base/src/functions/text_util.rs b/base/src/functions/text_util.rs new file mode 100644 index 0000000..f2b6744 --- /dev/null +++ b/base/src/functions/text_util.rs @@ -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 { + 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 { + 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()) + ); + } +} diff --git a/base/src/functions/util.rs b/base/src/functions/util.rs new file mode 100644 index 0000000..96f1fb8 --- /dev/null +++ b/base/src/functions/util.rs @@ -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 { + // 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::() { + 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 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::() { + 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::() { + 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::() { + Box::new(move |x| result_is_not_equal_to_number(x, f)) + } else if let Ok(b) = v.to_lowercase().parse::() { + 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::() { + 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::() { + 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::() { + Box::new(move |x| result_is_equal_to_number(x, f)) + } else if let Ok(b) = v.to_lowercase().parse::() { + 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), + } +} diff --git a/base/src/functions/xlookup.rs b/base/src/functions/xlookup.rs new file mode 100644 index 0000000..991d836 --- /dev/null +++ b/base/src/functions/xlookup.rs @@ -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 { + 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 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(), + }, + } + } +} diff --git a/base/src/implicit_intersection.rs b/base/src/implicit_intersection.rs new file mode 100644 index 0000000..af3e46e --- /dev/null +++ b/base/src/implicit_intersection.rs @@ -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 { + 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 +} diff --git a/base/src/language/language.json b/base/src/language/language.json new file mode 100644 index 0000000..7e173a0 --- /dev/null +++ b/base/src/language/language.json @@ -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!" + } + } +} diff --git a/base/src/language/mod.rs b/base/src/language/mod.rs new file mode 100644 index 0000000..92ed7a0 --- /dev/null +++ b/base/src/language/mod.rs @@ -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> = 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) +} diff --git a/base/src/lib.rs b/base/src/lib.rs new file mode 100644 index 0000000..ee8d351 --- /dev/null +++ b/base/src/lib.rs @@ -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; diff --git a/base/src/locale/locales.json b/base/src/locale/locales.json new file mode 100644 index 0000000..8a53ae2 --- /dev/null +++ b/base/src/locale/locales.json @@ -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":"$"}}} \ No newline at end of file diff --git a/base/src/locale/mod.rs b/base/src/locale/mod.rs new file mode 100644 index 0000000..328ac43 --- /dev/null +++ b/base/src/locale/mod.rs @@ -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, + pub day_names_short: Vec, + pub months: Vec, + pub months_short: Vec, + pub months_letter: Vec, +} + +#[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, + #[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, + #[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> = 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) +} diff --git a/base/src/mock_time.rs b/base/src/mock_time.rs new file mode 100644 index 0000000..3e03034 --- /dev/null +++ b/base/src/mock_time.rs @@ -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 = 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); +} diff --git a/base/src/model.rs b/base/src/model.rs new file mode 100644 index 0000000..2533d6a --- /dev/null +++ b/base/src/model.rs @@ -0,0 +1,1507 @@ +use serde::{Deserialize, Serialize}; +use serde_json::json; + +use std::collections::HashMap; +use std::vec::Vec; + +use crate::{ + calc_result::{CalcResult, CellReference, Range}, + cell::CellValue, + constants, + expressions::token::{Error, OpCompare, OpProduct, OpSum, OpUnary}, + expressions::{ + parser::move_formula::{move_formula, MoveContext}, + token::get_error_by_name, + types::*, + utils::{self, is_valid_row}, + }, + expressions::{ + parser::{ + stringify::{to_rc_format, to_string}, + Node, Parser, + }, + utils::is_valid_column_number, + }, + formatter::{ + format::{format_number, parse_formatted_number}, + lexer::is_likely_date_number_format, + }, + functions::util::compare_values, + implicit_intersection::implicit_intersection, + language::{get_language, Language}, + locale::{get_locale, Currency, Locale}, + types::*, + utils as common, +}; + +pub use chrono_tz::Tz; + +#[cfg(test)] +pub use crate::mock_time::get_milliseconds_since_epoch; + +#[cfg(not(test))] +#[cfg(not(target_arch = "wasm32"))] +pub fn get_milliseconds_since_epoch() -> i64 { + use std::time::{SystemTime, UNIX_EPOCH}; + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("problem with system time") + .as_millis() as i64 +} + +#[cfg(not(test))] +#[cfg(target_arch = "wasm32")] +pub fn get_milliseconds_since_epoch() -> i64 { + use js_sys::Date; + Date::now() as i64 +} + +#[derive(Clone)] +pub enum CellState { + Evaluated, + Evaluating, +} + +#[derive(Debug, Clone)] +pub enum ParsedDefinedName { + CellReference(CellReference), + RangeReference(Range), + InvalidDefinedNameFormula, + // TODO: Support constants in defined names + // TODO: Support formulas in defined names + // TODO: Support tables in defined names +} + +/// A model includes: +/// * A Workbook: An internal representation of and Excel workbook +/// * Parsed Formulas: All the formulas in the workbook are parsed here (runtime only) +/// * A list of cells with its status (evaluating, evaluated, not evaluated) +#[derive(Clone)] +pub struct Model { + pub workbook: Workbook, + pub parsed_formulas: Vec>, + pub parsed_defined_names: HashMap<(Option, String), ParsedDefinedName>, + pub parser: Parser, + pub cells: HashMap<(u32, i32, i32), CellState>, + pub locale: Locale, + pub language: Language, + pub tz: Tz, +} + +pub struct CellIndex { + pub index: u32, + pub row: i32, + pub column: i32, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Style { + pub alignment: Option, + pub num_fmt: String, + pub fill: Fill, + pub font: Font, + pub border: Border, + pub quote_prefix: bool, +} + +impl Model { + pub(crate) fn evaluate_node_with_reference( + &mut self, + node: &Node, + cell: CellReference, + ) -> CalcResult { + match node { + Node::ReferenceKind { + sheet_name: _, + sheet_index, + absolute_row, + absolute_column, + row, + column, + } => { + let mut row1 = *row; + let mut column1 = *column; + if !absolute_row { + row1 += cell.row; + } + if !absolute_column { + column1 += cell.column; + } + CalcResult::Range { + left: CellReference { + sheet: *sheet_index, + row: row1, + column: column1, + }, + right: CellReference { + sheet: *sheet_index, + row: row1, + column: column1, + }, + } + } + Node::RangeKind { + sheet_name: _, + sheet_index, + absolute_row1, + absolute_column1, + row1, + column1, + absolute_row2, + absolute_column2, + row2, + column2, + } => { + let mut row_left = *row1; + let mut column_left = *column1; + if !absolute_row1 { + row_left += cell.row; + } + if !absolute_column1 { + column_left += cell.column; + } + let mut row_right = *row2; + let mut column_right = *column2; + if !absolute_row2 { + row_right += cell.row; + } + if !absolute_column2 { + column_right += cell.column; + } + // FIXME: HACK. The parser is currently parsing Sheet3!A1:A10 as Sheet3!A1:(present sheet)!A10 + CalcResult::Range { + left: CellReference { + sheet: *sheet_index, + row: row_left, + column: column_left, + }, + right: CellReference { + sheet: *sheet_index, + row: row_right, + column: column_right, + }, + } + } + _ => self.evaluate_node_in_context(node, cell), + } + } + + fn get_range(&mut self, left: &Node, right: &Node, cell: CellReference) -> CalcResult { + let left_result = self.evaluate_node_with_reference(left, cell); + let right_result = self.evaluate_node_with_reference(right, cell); + match (left_result, right_result) { + ( + CalcResult::Range { + left: left1, + right: right1, + }, + CalcResult::Range { + left: left2, + right: right2, + }, + ) => { + if left1.row == right1.row + && left1.column == right1.column + && left2.row == right2.row + && left2.column == right2.column + { + return CalcResult::Range { + left: left1, + right: right2, + }; + } + CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid range".to_string(), + } + } + _ => CalcResult::Error { + error: Error::VALUE, + origin: cell, + message: "Invalid range".to_string(), + }, + } + } + + pub(crate) fn evaluate_node_in_context( + &mut self, + node: &Node, + cell: CellReference, + ) -> CalcResult { + use Node::*; + match node { + OpSumKind { kind, left, right } => { + // In the future once the feature try trait stabilizes we could use the '?' operator for this :) + // See: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=236044e8321a1450988e6ffe5a27dab5 + let l = match self.get_number(left, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + let r = match self.get_number(right, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + let result = match kind { + OpSum::Add => l + r, + OpSum::Minus => l - r, + }; + CalcResult::Number(result) + } + NumberKind(value) => CalcResult::Number(*value), + StringKind(value) => CalcResult::String(value.replace(r#""""#, r#"""#)), + BooleanKind(value) => CalcResult::Boolean(*value), + ReferenceKind { + sheet_name: _, + sheet_index, + absolute_row, + absolute_column, + row, + column, + } => { + let mut row1 = *row; + let mut column1 = *column; + if !absolute_row { + row1 += cell.row; + } + if !absolute_column { + column1 += cell.column; + } + self.evaluate_cell(CellReference { + sheet: *sheet_index, + row: row1, + column: column1, + }) + } + WrongReferenceKind { .. } => { + CalcResult::new_error(Error::REF, cell, "Wrong reference".to_string()) + } + OpRangeKind { left, right } => self.get_range(left, right, cell), + WrongRangeKind { .. } => { + CalcResult::new_error(Error::REF, cell, "Wrong range".to_string()) + } + RangeKind { + sheet_index, + row1, + column1, + row2, + column2, + absolute_column1, + absolute_row2, + absolute_row1, + absolute_column2, + sheet_name: _, + } => CalcResult::Range { + left: CellReference { + sheet: *sheet_index, + row: if *absolute_row1 { + *row1 + } else { + *row1 + cell.row + }, + column: if *absolute_column1 { + *column1 + } else { + *column1 + cell.column + }, + }, + right: CellReference { + sheet: *sheet_index, + row: if *absolute_row2 { + *row2 + } else { + *row2 + cell.row + }, + column: if *absolute_column2 { + *column2 + } else { + *column2 + cell.column + }, + }, + }, + OpConcatenateKind { left, right } => { + let l = match self.get_string(left, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + let r = match self.get_string(right, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + let result = format!("{}{}", l, r); + CalcResult::String(result) + } + OpProductKind { kind, left, right } => { + let l = match self.get_number(left, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + let r = match self.get_number(right, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + let result = match kind { + OpProduct::Times => l * r, + OpProduct::Divide => { + if r == 0.0 { + return CalcResult::new_error( + Error::DIV, + cell, + "Divide by Zero".to_string(), + ); + } + l / r + } + }; + CalcResult::Number(result) + } + OpPowerKind { left, right } => { + let l = match self.get_number(left, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + let r = match self.get_number(right, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + // Deal with errors properly + CalcResult::Number(l.powf(r)) + } + FunctionKind { kind, args } => self.evaluate_function(kind, args, cell), + InvalidFunctionKind { name, args: _ } => { + CalcResult::new_error(Error::ERROR, cell, format!("Invalid function: {}", name)) + } + ArrayKind(_) => { + // TODO: NOT IMPLEMENTED + CalcResult::new_error(Error::NIMPL, cell, "Arrays not implemented".to_string()) + } + VariableKind(defined_name) => { + let parsed_defined_name = self + .parsed_defined_names + .get(&(Some(cell.sheet), defined_name.to_lowercase())) // try getting local defined name + .or_else(|| { + self.parsed_defined_names + .get(&(None, defined_name.to_lowercase())) + }); // fallback to global + + if let Some(parsed_defined_name) = parsed_defined_name { + match parsed_defined_name { + ParsedDefinedName::CellReference(reference) => { + self.evaluate_cell(*reference) + } + ParsedDefinedName::RangeReference(range) => CalcResult::Range { + left: range.left, + right: range.right, + }, + ParsedDefinedName::InvalidDefinedNameFormula => CalcResult::new_error( + Error::NIMPL, + cell, + format!("Defined name \"{}\" is not a reference.", defined_name), + ), + } + } else { + CalcResult::new_error( + Error::NAME, + cell, + format!("Defined name \"{}\" not found.", defined_name), + ) + } + } + CompareKind { kind, left, right } => { + let l = self.evaluate_node_in_context(left, cell); + if l.is_error() { + return l; + } + let r = self.evaluate_node_in_context(right, cell); + if r.is_error() { + return r; + } + let compare = compare_values(&l, &r); + match kind { + OpCompare::Equal => { + if compare == 0 { + CalcResult::Boolean(true) + } else { + CalcResult::Boolean(false) + } + } + OpCompare::LessThan => { + if compare == -1 { + CalcResult::Boolean(true) + } else { + CalcResult::Boolean(false) + } + } + OpCompare::GreaterThan => { + if compare == 1 { + CalcResult::Boolean(true) + } else { + CalcResult::Boolean(false) + } + } + OpCompare::LessOrEqualThan => { + if compare < 1 { + CalcResult::Boolean(true) + } else { + CalcResult::Boolean(false) + } + } + OpCompare::GreaterOrEqualThan => { + if compare > -1 { + CalcResult::Boolean(true) + } else { + CalcResult::Boolean(false) + } + } + OpCompare::NonEqual => { + if compare != 0 { + CalcResult::Boolean(true) + } else { + CalcResult::Boolean(false) + } + } + } + } + UnaryKind { kind, right } => { + let r = match self.get_number(right, cell) { + Ok(f) => f, + Err(s) => { + return s; + } + }; + match kind { + OpUnary::Minus => CalcResult::Number(-r), + OpUnary::Percentage => CalcResult::Number(r / 100.0), + } + } + ErrorKind(kind) => CalcResult::new_error(kind.clone(), cell, "".to_string()), + ParseErrorKind { + formula, + message, + position: _, + } => CalcResult::new_error( + Error::ERROR, + cell, + format!("Error parsing {}: {}", formula, message), + ), + EmptyArgKind => CalcResult::EmptyArg, + } + } + + fn cell_reference_to_string(&self, cell_reference: &CellReference) -> Result { + let sheet = self.workbook.worksheet(cell_reference.sheet)?; + let column = utils::number_to_column(cell_reference.column) + .ok_or_else(|| "Invalid column".to_string())?; + if !is_valid_row(cell_reference.row) { + return Err("Invalid row".to_string()); + } + Ok(format!("{}!{}{}", sheet.name, column, cell_reference.row)) + } + /// Sets `result` in the cell given by `sheet` sheet index, row and column + /// Note that will panic if the cell does not exist + /// It will do nothing if the cell does not have a formula + fn set_cell_value(&mut self, cell_reference: CellReference, result: &CalcResult) { + let CellReference { sheet, column, row } = cell_reference; + let cell = &self.workbook.worksheets[sheet as usize].sheet_data[&row][&column]; + let s = cell.get_style(); + if let Some(f) = cell.get_formula() { + match result { + CalcResult::Number(value) => { + // safety belt + if value.is_nan() || value.is_infinite() { + // This should never happen, is there a way we can log this events? + return self.set_cell_value( + cell_reference, + &CalcResult::Error { + error: Error::NUM, + origin: cell_reference, + message: "".to_string(), + }, + ); + } + *self.workbook.worksheets[sheet as usize] + .sheet_data + .get_mut(&row) + .expect("expected a row") + .get_mut(&column) + .expect("expected a column") = Cell::CellFormulaNumber { f, s, v: *value }; + } + CalcResult::String(value) => { + *self.workbook.worksheets[sheet as usize] + .sheet_data + .get_mut(&row) + .expect("expected a row") + .get_mut(&column) + .expect("expected a column") = Cell::CellFormulaString { + f, + s, + v: value.clone(), + }; + } + CalcResult::Boolean(value) => { + *self.workbook.worksheets[sheet as usize] + .sheet_data + .get_mut(&row) + .expect("expected a row") + .get_mut(&column) + .expect("expected a column") = Cell::CellFormulaBoolean { f, s, v: *value }; + } + CalcResult::Error { + error, + origin, + message, + } => { + let o = match self.cell_reference_to_string(origin) { + Ok(s) => s, + Err(_) => "".to_string(), + }; + *self.workbook.worksheets[sheet as usize] + .sheet_data + .get_mut(&row) + .expect("expected a row") + .get_mut(&column) + .expect("expected a column") = Cell::CellFormulaError { + f, + s, + o, + m: message.to_string(), + ei: error.clone(), + }; + } + CalcResult::Range { left, right } => { + let range = Range { + left: *left, + right: *right, + }; + if let Some(intersection_cell) = implicit_intersection(&cell_reference, &range) + { + let v = self.evaluate_cell(intersection_cell); + self.set_cell_value(cell_reference, &v); + } else { + let o = match self.cell_reference_to_string(&cell_reference) { + Ok(s) => s, + Err(_) => "".to_string(), + }; + *self.workbook.worksheets[sheet as usize] + .sheet_data + .get_mut(&row) + .expect("expected a row") + .get_mut(&column) + .expect("expected a column") = Cell::CellFormulaError { + f, + s, + o, + m: "Invalid reference".to_string(), + ei: Error::VALUE, + }; + } + } + CalcResult::EmptyCell | CalcResult::EmptyArg => { + *self.workbook.worksheets[sheet as usize] + .sheet_data + .get_mut(&row) + .expect("expected a row") + .get_mut(&column) + .expect("expected a column") = Cell::CellFormulaNumber { f, s, v: 0.0 }; + } + } + } + } + + pub fn set_sheet_color(&mut self, sheet: u32, color: &str) -> Result<(), String> { + let worksheet = self.workbook.worksheet_mut(sheet)?; + if color.is_empty() { + worksheet.color = None; + return Ok(()); + } else if common::is_valid_hex_color(color) { + worksheet.color = Some(color.to_string()); + return Ok(()); + } + Err(format!("Invalid color: {}", color)) + } + + fn get_cell_value(&self, cell: &Cell, cell_reference: CellReference) -> CalcResult { + use Cell::*; + match cell { + EmptyCell { .. } => CalcResult::EmptyCell, + BooleanCell { v, .. } => CalcResult::Boolean(*v), + NumberCell { v, .. } => CalcResult::Number(*v), + ErrorCell { ei, .. } => { + let message = ei.to_localized_error_string(&self.language); + CalcResult::new_error(ei.clone(), cell_reference, message) + } + SharedString { si, .. } => { + if let Some(s) = self.workbook.shared_strings.get(*si as usize) { + CalcResult::String(s.clone()) + } else { + let message = "Invalid shared string".to_string(); + CalcResult::new_error(Error::ERROR, cell_reference, message) + } + } + CellFormula { .. } => CalcResult::Error { + error: Error::ERROR, + origin: cell_reference, + message: "Unevaluated formula".to_string(), + }, + CellFormulaBoolean { v, .. } => CalcResult::Boolean(*v), + CellFormulaNumber { v, .. } => CalcResult::Number(*v), + CellFormulaString { v, .. } => CalcResult::String(v.clone()), + CellFormulaError { ei, o, m, .. } => { + if let Some(cell_reference) = self.parse_reference(o) { + CalcResult::new_error(ei.clone(), cell_reference, m.clone()) + } else { + CalcResult::Error { + error: ei.clone(), + origin: cell_reference, + message: ei.to_localized_error_string(&self.language), + } + } + } + } + } + + /// Returns true if cell is completely empty. + /// Cell with formula that evaluates to empty string is not considered empty. + pub fn is_empty_cell(&self, sheet: u32, row: i32, column: i32) -> Result { + let worksheet = self.workbook.worksheet(sheet)?; + worksheet.is_empty_cell(row, column) + } + + pub(crate) fn evaluate_cell(&mut self, cell_reference: CellReference) -> CalcResult { + let row_data = match self.workbook.worksheets[cell_reference.sheet as usize] + .sheet_data + .get(&cell_reference.row) + { + Some(r) => r, + None => return CalcResult::EmptyCell, + }; + let cell = match row_data.get(&cell_reference.column) { + Some(c) => c, + None => { + return CalcResult::EmptyCell; + } + }; + + match cell.get_formula() { + Some(f) => { + let key = ( + cell_reference.sheet, + cell_reference.row, + cell_reference.column, + ); + match self.cells.get(&key) { + Some(CellState::Evaluating) => { + return CalcResult::new_error( + Error::CIRC, + cell_reference, + "Circular reference detected".to_string(), + ); + } + Some(CellState::Evaluated) => { + return self.get_cell_value(cell, cell_reference); + } + _ => { + // mark cell as being evaluated + self.cells.insert(key, CellState::Evaluating); + } + } + let node = &self.parsed_formulas[cell_reference.sheet as usize][f as usize].clone(); + let result = self.evaluate_node_in_context(node, cell_reference); + self.set_cell_value(cell_reference, &result); + // mark cell as evaluated + self.cells.insert(key, CellState::Evaluated); + result + } + None => self.get_cell_value(cell, cell_reference), + } + } + + pub(crate) fn get_sheet_index_by_name(&self, name: &str) -> Option { + let worksheets = &self.workbook.worksheets; + for (index, worksheet) in worksheets.iter().enumerate() { + if worksheet.get_name().to_uppercase() == name.to_uppercase() { + return Some(index as u32); + } + } + None + } + + // Public API + /// Returns a model from a String representation of a workbook + pub fn from_json(s: &str) -> Result { + let workbook: Workbook = + serde_json::from_str(s).map_err(|_| "Error parsing workbook".to_string())?; + Model::from_workbook(workbook) + } + + pub fn from_workbook(workbook: Workbook) -> Result { + let parsed_formulas = Vec::new(); + let worksheets = &workbook.worksheets; + + let worksheet_names = worksheets.iter().map(|s| s.get_name()).collect(); + + // add all tables + // let mut tables = Vec::new(); + // for worksheet in worksheets { + // let mut tables_in_sheet = HashMap::new(); + // for table in &worksheet.tables { + // tables_in_sheet.insert(table.name.clone(), table.clone()); + // } + // tables.push(tables_in_sheet); + // } + let parser = Parser::new(worksheet_names, workbook.tables.clone()); + let cells = HashMap::new(); + let locale = get_locale(&workbook.settings.locale) + .map_err(|_| "Invalid locale".to_string())? + .clone(); + let tz: Tz = workbook + .settings + .tz + .parse() + .map_err(|_| format!("Invalid timezone: {}", workbook.settings.tz))?; + + // 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, + language, + locale, + tz, + }; + + model.parse_formulas(); + model.parse_defined_names(); + + Ok(model) + } + + /// Parses a reference like "Sheet1!B4" into {0, 2, 4} + pub fn parse_reference(&self, s: &str) -> Option { + let bytes = s.as_bytes(); + let mut sheet_name = "".to_string(); + let mut column = "".to_string(); + let mut row = "".to_string(); + let mut state = "sheet"; // "sheet", "col", "row" + for &byte in bytes { + match state { + "sheet" => { + if byte == b'!' { + state = "col" + } else { + sheet_name.push(byte as char); + } + } + "col" => { + if byte.is_ascii_alphabetic() { + column.push(byte as char); + } else { + state = "row"; + row.push(byte as char); + } + } + _ => { + row.push(byte as char); + } + } + } + let sheet = match self.get_sheet_index_by_name(&sheet_name) { + Some(s) => s, + None => return None, + }; + let row = match row.parse::() { + Ok(r) => r, + Err(_) => return None, + }; + if !(1..=constants::LAST_ROW).contains(&row) { + return None; + } + + let column = match utils::column_to_number(&column) { + Ok(column) => { + if is_valid_column_number(column) { + column + } else { + return None; + } + } + Err(_) => return None, + }; + + Some(CellReference { sheet, row, column }) + } + + /// moves the value in area from source to target. + pub fn move_cell_value_to_area( + &mut self, + value: &str, + source: &CellReferenceIndex, + target: &CellReferenceIndex, + area: &Area, + ) -> Result { + let source_sheet_name = self + .workbook + .worksheet(source.sheet) + .map_err(|e| format!("Could not find source worksheet: {}", e))? + .get_name(); + if source.sheet != area.sheet { + return Err("Source and area are in different sheets".to_string()); + } + if source.row < area.row || source.row >= area.row + area.height { + return Err("Source is outside the area".to_string()); + } + if source.column < area.column || source.column >= area.column + area.width { + return Err("Source is outside the area".to_string()); + } + let target_sheet_name = self + .workbook + .worksheet(target.sheet) + .map_err(|e| format!("Could not find target worksheet: {}", e))? + .get_name(); + if let Some(formula) = value.strip_prefix('=') { + let cell_reference = CellReferenceRC { + sheet: source_sheet_name.to_owned(), + row: source.row, + column: source.column, + }; + let formula_str = move_formula( + &self.parser.parse(formula, &Some(cell_reference)), + &MoveContext { + source_sheet_name: &source_sheet_name, + row: source.row, + column: source.column, + area, + target_sheet_name: &target_sheet_name, + row_delta: target.row - source.row, + column_delta: target.column - source.column, + }, + ); + Ok(format!("={}", formula_str)) + } else { + Ok(value.to_string()) + } + } + + /// 'Extends' the value from cell [sheet, row, column] to [target_row, target_column] + pub fn extend_to( + &self, + sheet: u32, + row: i32, + column: i32, + target_row: i32, + target_column: i32, + ) -> Result { + let cell = self.workbook.worksheet(sheet)?.cell(row, column); + let result = match cell { + Some(cell) => match cell.get_formula() { + None => cell.get_text(&self.workbook.shared_strings, &self.language), + Some(i) => { + let formula = &self.parsed_formulas[sheet as usize][i as usize]; + let cell_ref = CellReferenceRC { + sheet: self.workbook.worksheets[sheet as usize].get_name(), + row: target_row, + column: target_column, + }; + format!("={}", to_string(formula, &cell_ref)) + } + }, + None => "".to_string(), + }; + Ok(result) + } + + /// 'Extends' value from cell [sheet, row, column] to [target_row, target_column] + pub fn extend_copied_value( + &mut self, // FIXME: weird that it must be mutable + value: &str, + source_sheet_name: &str, + source: &CellReferenceIndex, + target: &CellReferenceIndex, + ) -> Result { + let target_sheet_name = match self.workbook.worksheets.get(target.sheet as usize) { + Some(ws) => ws.get_name(), + None => { + return Err("Invalid worksheet index".to_owned()); + } + }; + if let Some(formula_str) = value.strip_prefix('=') { + let cell_reference = CellReferenceRC { + sheet: source_sheet_name.to_string(), + row: source.row, + column: source.column, + }; + let formula = &self.parser.parse(formula_str, &Some(cell_reference)); + let cell_reference = CellReferenceRC { + sheet: target_sheet_name, + row: target.row, + column: target.column, + }; + return Ok(format!("={}", to_string(formula, &cell_reference))); + }; + Ok(value.to_string()) + } + + pub fn cell_formula( + &self, + sheet: u32, + row: i32, + column: i32, + ) -> Result, String> { + let worksheet = self.workbook.worksheet(sheet)?; + Ok(worksheet.cell(row, column).and_then(|cell| { + cell.get_formula().map(|formula_index| { + let formula = &self.parsed_formulas[sheet as usize][formula_index as usize]; + let cell_ref = CellReferenceRC { + sheet: worksheet.get_name(), + row, + column, + }; + format!("={}", to_string(formula, &cell_ref)) + }) + })) + } + + /// Updates the value of a cell with some text + /// It does not change the style unless needs to add "quoting" + pub fn update_cell_with_text(&mut self, sheet: u32, row: i32, column: i32, value: &str) { + let style_index = self.get_cell_style_index(sheet, row, column); + let new_style_index; + if common::value_needs_quoting(value, &self.language) { + new_style_index = self + .workbook + .styles + .get_style_with_quote_prefix(style_index); + } else if self.workbook.styles.style_is_quote_prefix(style_index) { + new_style_index = self + .workbook + .styles + .get_style_without_quote_prefix(style_index); + } else { + new_style_index = style_index; + } + self.set_cell_with_string(sheet, row, column, value, new_style_index); + } + + /// Updates the value of a cell with a boolean value + /// It does not change the style + pub fn update_cell_with_bool(&mut self, sheet: u32, row: i32, column: i32, value: bool) { + let style_index = self.get_cell_style_index(sheet, row, column); + let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) { + self.workbook + .styles + .get_style_without_quote_prefix(style_index) + } else { + style_index + }; + let worksheet = &mut self.workbook.worksheets[sheet as usize]; + worksheet.set_cell_with_boolean(row, column, value, new_style_index); + } + + /// Updates the value of a cell with a number + /// It does not change the style + pub fn update_cell_with_number(&mut self, sheet: u32, row: i32, column: i32, value: f64) { + let style_index = self.get_cell_style_index(sheet, row, column); + let new_style_index = if self.workbook.styles.style_is_quote_prefix(style_index) { + self.workbook + .styles + .get_style_without_quote_prefix(style_index) + } else { + style_index + }; + let worksheet = &mut self.workbook.worksheets[sheet as usize]; + worksheet.set_cell_with_number(row, column, value, new_style_index); + } + + /// Updates the formula of given cell + /// It does not change the style unless needs to add "quoting" + /// Expects the formula to start with "=" + pub fn update_cell_with_formula( + &mut self, + sheet: u32, + row: i32, + column: i32, + formula: String, + ) -> Result<(), String> { + let mut style_index = self.get_cell_style_index(sheet, row, column); + if self.workbook.styles.style_is_quote_prefix(style_index) { + style_index = self + .workbook + .styles + .get_style_without_quote_prefix(style_index); + } + let formula = formula + .strip_prefix('=') + .ok_or_else(|| format!("\"{formula}\" is not a valid formula"))?; + self.set_cell_with_formula(sheet, row, column, formula, style_index)?; + Ok(()) + } + + /// Sets a cell parametrized by (`sheet`, `row`, `column`) with `value` + /// This mimics a user entering a value on a cell. + /// If you enter a currency `$100` it will set as a number and update the style + /// Note that for currencies/percentage there is only one possible style + /// The value is always a string, so we need to try to cast it into numbers/booleans/errors + pub fn set_user_input(&mut self, sheet: u32, row: i32, column: i32, value: String) { + // If value starts with "'" then we force the style to be quote_prefix + let style_index = self.get_cell_style_index(sheet, row, column); + if let Some(new_value) = value.strip_prefix('\'') { + // First check if it needs quoting + let new_style = if common::value_needs_quoting(new_value, &self.language) { + self.workbook + .styles + .get_style_with_quote_prefix(style_index) + } else { + style_index + }; + self.set_cell_with_string(sheet, row, column, new_value, new_style); + } else { + let mut new_style_index = style_index; + if self.workbook.styles.style_is_quote_prefix(style_index) { + new_style_index = self + .workbook + .styles + .get_style_without_quote_prefix(style_index); + } + if let Some(formula) = value.strip_prefix('=') { + let formula_index = self + .set_cell_with_formula(sheet, row, column, formula, new_style_index) + .expect("could not set the cell formula"); + // Update the style if needed + let cell = CellReference { sheet, row, column }; + let parsed_formula = &self.parsed_formulas[sheet as usize][formula_index as usize]; + if let Some(units) = self.compute_node_units(parsed_formula, &cell) { + let new_style_index = self + .workbook + .styles + .get_style_with_format(new_style_index, &units.get_num_fmt()); + let style = self.workbook.styles.get_style(new_style_index); + self.set_cell_style(sheet, row, column, &style) + .expect("Failed setting the style"); + } + } else { + let worksheets = &mut self.workbook.worksheets; + let worksheet = &mut worksheets[sheet as usize]; + + // The list of currencies is '$', '€' and the local currency + let mut currencies = vec!["$", "€"]; + let currency = &self.locale.currency.symbol; + if !currencies.iter().any(|e| e == currency) { + currencies.push(currency); + } + // We try to parse as number + if let Ok((v, number_format)) = parse_formatted_number(&value, ¤cies) { + if let Some(num_fmt) = number_format { + // Should not apply the format in the following cases: + // - we assign a date to already date-formatted cell + let should_apply_format = !(is_likely_date_number_format( + &self.workbook.styles.get_style(new_style_index).num_fmt, + ) && is_likely_date_number_format(&num_fmt)); + if should_apply_format { + new_style_index = self + .workbook + .styles + .get_style_with_format(new_style_index, &num_fmt); + } + } + worksheet.set_cell_with_number(row, column, v, new_style_index); + return; + } + // We try to parse as boolean + if let Ok(v) = value.to_lowercase().parse::() { + worksheet.set_cell_with_boolean(row, column, v, new_style_index); + return; + } + // Check is it is error value + let upper = value.to_uppercase(); + match get_error_by_name(&upper, &self.language) { + Some(error) => { + worksheet.set_cell_with_error(row, column, error, new_style_index); + } + None => { + self.set_cell_with_string(sheet, row, column, &value, new_style_index); + } + } + } + } + } + + fn set_cell_with_formula( + &mut self, + sheet: u32, + row: i32, + column: i32, + formula: &str, + style: i32, + ) -> Result { + let worksheet = self.workbook.worksheet_mut(sheet)?; + let cell_reference = CellReferenceRC { + sheet: worksheet.get_name(), + row, + column, + }; + let shared_formulas = &mut worksheet.shared_formulas; + let mut parsed_formula = self.parser.parse(formula, &Some(cell_reference.clone())); + // If the formula fails to parse try adding a parenthesis + // SUM(A1:A3 => SUM(A1:A3) + if let Node::ParseErrorKind { .. } = parsed_formula { + let new_parsed_formula = self + .parser + .parse(&format!("{})", formula), &Some(cell_reference)); + match new_parsed_formula { + Node::ParseErrorKind { .. } => {} + _ => parsed_formula = new_parsed_formula, + } + } + + let s = to_rc_format(&parsed_formula); + let mut formula_index: i32 = -1; + if let Some(index) = shared_formulas.iter().position(|x| x == &s) { + formula_index = index as i32; + } + if formula_index == -1 { + shared_formulas.push(s); + self.parsed_formulas[sheet as usize].push(parsed_formula); + formula_index = (shared_formulas.len() as i32) - 1; + } + worksheet.set_cell_with_formula(row, column, formula_index, style); + Ok(formula_index) + } + + fn set_cell_with_string(&mut self, sheet: u32, row: i32, column: i32, value: &str, style: i32) { + // Interestingly, `self.workbook.worksheet()` cannot be used because it would create two + // mutable borrows of worksheet. However, I suspect that lexical lifetimes silently help + // here, so there is no issue with inlined call. + let worksheets = &mut self.workbook.worksheets; + let worksheet = &mut worksheets[sheet as usize]; + let shared_strings = &mut self.workbook.shared_strings; + let index = shared_strings.iter().position(|r| r == value); + match index { + Some(string_index) => { + worksheet.set_cell_with_string(row, column, string_index as i32, style); + } + None => { + shared_strings.push(value.to_string()); + let string_index = shared_strings.len() as i32 - 1; + worksheet.set_cell_with_string(row, column, string_index, style); + } + } + } + + // FIXME: Can't put it in Workbook, because language is outside of workbook, sic! + /// Gets the Excel Value (Bool, Number, String) of a cell + pub fn get_cell_value_by_ref(&self, cell_ref: &str) -> Result { + let cell_reference = match self.parse_reference(cell_ref) { + Some(c) => c, + None => return Err(format!("Error parsing reference: '{cell_ref}'")), + }; + let sheet_index = cell_reference.sheet; + let column = cell_reference.column; + let row = cell_reference.row; + + self.get_cell_value_by_index(sheet_index, row, column) + } + + // FIXME: Can't put it in Workbook, because language is outside of workbook, sic! + pub fn get_cell_value_by_index( + &self, + sheet_index: u32, + row: i32, + column: i32, + ) -> Result { + let cell = self + .workbook + .worksheet(sheet_index)? + .cell(row, column) + .cloned() + .unwrap_or_default(); + let cell_value = cell.value(&self.workbook.shared_strings, &self.language); + Ok(cell_value) + } + + // FIXME: Can't put it in Workbook, because locale and language are outside of workbook, sic! + pub fn formatted_cell_value( + &self, + sheet_index: u32, + row: i32, + column: i32, + ) -> Result { + let format = self.get_style_for_cell(sheet_index, row, column).num_fmt; + let cell = self + .workbook + .worksheet(sheet_index)? + .cell(row, column) + .cloned() + .unwrap_or_default(); + let formatted_value = + cell.formatted_value(&self.workbook.shared_strings, &self.language, |value| { + format_number(value, &format, &self.locale).text + }); + Ok(formatted_value) + } + + /// Returns a list of all cells + pub fn get_all_cells(&self) -> Vec { + let mut cells = Vec::new(); + for (index, sheet) in self.workbook.worksheets.iter().enumerate() { + let mut sorted_rows: Vec<_> = sheet.sheet_data.keys().collect(); + sorted_rows.sort_unstable(); + for row in sorted_rows { + let row_data = &sheet.sheet_data[row]; + let mut sorted_columns: Vec<_> = row_data.keys().collect(); + sorted_columns.sort_unstable(); + for column in sorted_columns { + cells.push(CellIndex { + index: index as u32, + row: *row, + column: *column, + }); + } + } + } + cells + } + + /// Evaluates the model with a top-down recursive algorithm + pub fn evaluate(&mut self) { + // clear all computation artifacts + self.cells.clear(); + + let cells = self.get_all_cells(); + + for cell in cells { + self.evaluate_cell(CellReference { + sheet: cell.index, + row: cell.row, + column: cell.column, + }); + } + } + + /// Sets cell to empty. Can be used to delete value without affecting style. + pub fn set_cell_empty(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { + let worksheet = self.workbook.worksheet_mut(sheet)?; + worksheet.set_cell_empty(row, column); + Ok(()) + } + + /// Deletes a cell by removing it from worksheet data. + pub fn delete_cell(&mut self, sheet: u32, row: i32, column: i32) -> Result<(), String> { + let worksheet = self.workbook.worksheet_mut(sheet)?; + + let sheet_data = &mut worksheet.sheet_data; + if let Some(row_data) = sheet_data.get_mut(&row) { + row_data.remove(&column); + } + + Ok(()) + } + + // FIXME: expect + pub fn get_cell_style_index(&self, sheet: u32, row: i32, column: i32) -> i32 { + // First check the cell, then row, the column + let cell = self + .workbook + .worksheet(sheet) + .expect("Invalid sheet") + .cell(row, column); + match cell { + Some(cell) => cell.get_style(), + None => { + let rows = &self.workbook.worksheets[sheet as usize].rows; + for r in rows { + if r.r == row { + if r.custom_format { + return r.s; + } else { + break; + } + } + } + let cols = &self.workbook.worksheets[sheet as usize].cols; + for c in cols.iter() { + let min = c.min; + let max = c.max; + if column >= min && column <= max { + return c.style.unwrap_or(0); + } + } + 0 + } + } + } + + pub fn get_style_for_cell(&self, sheet: u32, row: i32, column: i32) -> Style { + self.workbook + .styles + .get_style(self.get_cell_style_index(sheet, row, column)) + } + + /// Returns a JSON string of the workbook + pub fn to_json_str(&self) -> String { + match serde_json::to_string(&self.workbook) { + Ok(s) => s, + Err(_) => { + // TODO, is this branch possible at all? + json!({"error": "Error stringifying workbook"}).to_string() + } + } + } + + /// Returns markup representation of the given `sheet`. + pub fn sheet_markup(&self, sheet: u32) -> Result { + let worksheet = self.workbook.worksheet(sheet)?; + let dimension = worksheet.dimension(); + + let mut rows = Vec::new(); + + for row in 1..(dimension.max_row + 1) { + let mut row_markup: Vec = Vec::new(); + + for column in 1..(dimension.max_column + 1) { + let mut cell_markup = match self.cell_formula(sheet, row, column)? { + Some(formula) => formula, + None => self.formatted_cell_value(sheet, row, column)?, + }; + let style = self.get_style_for_cell(sheet, row, column); + if style.font.b { + cell_markup = format!("**{cell_markup}**") + } + row_markup.push(cell_markup); + } + + rows.push(row_markup.join("|")); + } + + Ok(rows.join("\n")) + } + + pub fn set_currency(&mut self, iso: &str) -> Result<(), &str> { + // TODO: Add a full list + let symbol = if iso == "USD" { + "$" + } else if iso == "EUR" { + "€" + } else if iso == "GBP" { + "£" + } else if iso == "JPY" { + "¥" + } else { + return Err("Unsupported currency"); + }; + self.locale.currency = Currency { + symbol: symbol.to_string(), + iso: iso.to_string(), + }; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::util::new_empty_model; + + #[test] + fn test_cell_reference_to_string() { + let model = new_empty_model(); + let reference = CellReference { + sheet: 0, + row: 32, + column: 16, + }; + assert_eq!( + model.cell_reference_to_string(&reference), + Ok("Sheet1!P32".to_string()) + ) + } + + #[test] + fn test_cell_reference_to_string_invalid_worksheet() { + let model = new_empty_model(); + let reference = CellReference { + sheet: 10, + row: 1, + column: 1, + }; + assert_eq!( + model.cell_reference_to_string(&reference), + Err("Invalid sheet index".to_string()) + ) + } + + #[test] + fn test_cell_reference_to_string_invalid_column() { + let model = new_empty_model(); + let reference = CellReference { + sheet: 0, + row: 1, + column: 20_000, + }; + assert_eq!( + model.cell_reference_to_string(&reference), + Err("Invalid column".to_string()) + ) + } + + #[test] + fn test_cell_reference_to_string_invalid_row() { + let model = new_empty_model(); + let reference = CellReference { + sheet: 0, + row: 2_000_000, + column: 1, + }; + assert_eq!( + model.cell_reference_to_string(&reference), + Err("Invalid row".to_string()) + ) + } + + #[test] + fn test_get_cell() { + let mut model = new_empty_model(); + model._set("A1", "35"); + model._set("A2", ""); + let worksheet = model.workbook.worksheet(0).expect("Invalid sheet"); + + assert_eq!( + worksheet.cell(1, 1), + Some(&Cell::NumberCell { v: 35.0, s: 0 }) + ); + + assert_eq!( + worksheet.cell(2, 1), + Some(&Cell::SharedString { si: 0, s: 0 }) + ); + assert_eq!(worksheet.cell(3, 1), None) + } + + #[test] + fn test_get_cell_invalid_sheet() { + let model = new_empty_model(); + assert_eq!( + model.workbook.worksheet(5), + Err("Invalid sheet index".to_string()), + ) + } +} diff --git a/base/src/new_empty.rs b/base/src/new_empty.rs new file mode 100644 index 0000000..1bd4e3c --- /dev/null +++ b/base/src/new_empty.rs @@ -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, + ) -> 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 { + 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 { + 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 + } +} diff --git a/base/src/number_format.rs b/base/src/number_format.rs new file mode 100644 index 0000000..6fcf295 --- /dev/null +++ b/base/src/number_format.rs @@ -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 { + 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::() + .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::().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) +} diff --git a/base/src/styles.rs b/base/src/styles.rs new file mode 100644 index 0000000..ba00256 --- /dev/null +++ b/base/src/styles.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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(()) + } +} diff --git a/base/src/test/engineering/mod.rs b/base/src/test/engineering/mod.rs new file mode 100644 index 0000000..4e109c8 --- /dev/null +++ b/base/src/test/engineering/mod.rs @@ -0,0 +1,6 @@ +mod test_bessel; +mod test_bit_operations; +mod test_complex; +mod test_convert; +mod test_misc; +mod test_number_basis; diff --git a/base/src/test/engineering/test_bessel.rs b/base/src/test/engineering/test_bessel.rs new file mode 100644 index 0000000..e0dd759 --- /dev/null +++ b/base/src/test/engineering/test_bessel.rs @@ -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!"); +} diff --git a/base/src/test/engineering/test_bit_operations.rs b/base/src/test/engineering/test_bit_operations.rs new file mode 100644 index 0000000..dffb58e --- /dev/null +++ b/base/src/test/engineering/test_bit_operations.rs @@ -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!"); +} diff --git a/base/src/test/engineering/test_complex.rs b/base/src/test/engineering/test_complex.rs new file mode 100644 index 0000000..19dd430 --- /dev/null +++ b/base/src/test/engineering/test_complex.rs @@ -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"))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); +} diff --git a/base/src/test/test_circular_references.rs b/base/src/test/test_circular_references.rs new file mode 100644 index 0000000..b3139b4 --- /dev/null +++ b/base/src/test/test_circular_references.rs @@ -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!"); +} diff --git a/base/src/test/test_column_width.rs b/base/src/test/test_column_width.rs new file mode 100644 index 0000000..beee9cd --- /dev/null +++ b/base/src/test/test_column_width.rs @@ -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); +} diff --git a/base/src/test/test_criteria.rs b/base/src/test/test_criteria.rs new file mode 100644 index 0000000..07109cb --- /dev/null +++ b/base/src/test/test_criteria.rs @@ -0,0 +1,86 @@ +use crate::calc_result::CalcResult; +use crate::functions::util::build_criteria; + +// Tests for build_criteria +// ------------------------ +// +// Note that any test here is mostly for documentation purposes. +// A real test must be done in Excel. +// +// `build_criteria` takes a string ('criteria') and returns a function ('fn_criteria') that takes a CalcResult and returns a boolean. +// +// For instance if criteria is "123" we want all cells that contain the number "123". Then +// +// let fn_criteria = build_criteria(&CalcResult::Number(123)); +// +// Then fn_criteria(calc_result) will return true every time calc_result is the number "123" +// +// There are different types of criteria +// +// * We want the cells that are equal to a value (say a number, string, bool or an error). +// We can build those with a calc_result of the type (i.e CalcResult::Number(123)) +// or we can use a string preceded by "=" like CalcResult::String("=123") +// * We can use inequality signs "<", ">", "<=", ">=" or "<>" +// * If you use "=" or "<>" you can use wildcards (like "=*brown") +// +// All of them are case insensitive. + +#[test] +fn test_build_criteria_is_number() { + let c = CalcResult::Number(42.0); + let fn_criteria = build_criteria(&c); + assert!(fn_criteria(&CalcResult::Number(42.0))); + assert!(fn_criteria(&CalcResult::String("42".to_string()))); + assert!(fn_criteria(&CalcResult::String("42.00".to_string()))); + assert!(!fn_criteria(&CalcResult::Number(2.0))); + + let c = CalcResult::String("=42".to_string()); + let fn_criteria = build_criteria(&c); + assert!(fn_criteria(&CalcResult::Number(42.0))); + assert!(fn_criteria(&CalcResult::String("42".to_string()))); + assert!(fn_criteria(&CalcResult::String("42.00".to_string()))); + assert!(!fn_criteria(&CalcResult::Number(2.0))); +} + +#[test] +fn test_build_criteria_is_bool() { + let c = CalcResult::Boolean(true); + let fn_criteria = build_criteria(&c); + assert!(fn_criteria(&CalcResult::Boolean(true))); + assert!(!fn_criteria(&CalcResult::String("true".to_string()))); + assert!(!fn_criteria(&CalcResult::Number(1.0))); + + let c = CalcResult::String("=True".to_string()); + let fn_criteria = build_criteria(&c); + assert!(fn_criteria(&CalcResult::Boolean(true))); + assert!(!fn_criteria(&CalcResult::String("true".to_string()))); + assert!(!fn_criteria(&CalcResult::Number(1.0))); +} + +#[test] +fn test_build_criteria_is_less_than() { + let c = CalcResult::String("<100".to_string()); + let fn_criteria = build_criteria(&c); + assert!(!fn_criteria(&CalcResult::Boolean(true))); + assert!(!fn_criteria(&CalcResult::String("23".to_string()))); + assert!(fn_criteria(&CalcResult::Number(1.0))); + assert!(!fn_criteria(&CalcResult::Number(101.0))); +} + +#[test] +fn test_build_criteria_is_less_wildcard() { + let c = CalcResult::String("=D* G*".to_string()); + let fn_criteria = build_criteria(&c); + assert!(fn_criteria(&CalcResult::String( + "Diarmuid Glynn".to_string() + ))); + assert!(fn_criteria(&CalcResult::String( + "Daniel Gonzalez".to_string() + ))); + assert!(!fn_criteria(&CalcResult::String( + "DanielGonzalez".to_string() + ))); + assert!(!fn_criteria(&CalcResult::String( + " Daniel Gonzalez".to_string() + ))); +} diff --git a/base/src/test/test_currency.rs b/base/src/test/test_currency.rs new file mode 100644 index 0000000..32e951e --- /dev/null +++ b/base/src/test/test_currency.rs @@ -0,0 +1,24 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_cell_currency_dollar() { + let mut model = new_empty_model(); + model._set("A1", "=PMT(8/1200,10,10000)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), "-$1,037.03"); + + assert!(model.set_currency("EUR").is_ok()); +} + +#[test] +fn test_cell_currency_euro() { + let mut model = new_empty_model(); + assert!(model.set_currency("EUR").is_ok()); + model._set("A1", "=PMT(8/1200,10,10000)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), "-€1,037.03"); +} diff --git a/base/src/test/test_date_and_time.rs b/base/src/test/test_date_and_time.rs new file mode 100644 index 0000000..8420e2d --- /dev/null +++ b/base/src/test/test_date_and_time.rs @@ -0,0 +1,215 @@ +#![allow(clippy::unwrap_used)] + +/// Here we add tests that cannot be done in Excel +/// Either because Excel does not have that feature (i.e. wrong number of arguments) +/// or because we differ from Excel throwing #NUM! on invalid dates +/// We can also enter examples that illustrate/document a part of the function +use crate::{cell::CellValue, test::util::new_empty_model}; + +#[test] +fn test_fn_date_arguments() { + let mut model = new_empty_model(); + + // Wrong number of arguments produce #ERROR! + // NB: Excel does not have this error, but does not let you enter wrong number of arguments in the UI + model._set("A1", "=DATE()"); + model._set("A2", "=DATE(1975)"); + model._set("A3", "=DATE(1975, 2)"); + model._set("A4", "=DATE(1975, 2, 10, 3)"); + + // Arguments are out of rage. This is against Excel + // Excel will actually compute a date by continuing to the next month, year... + // We throw #NUM! + model._set("A5", "=DATE(1975, -2, 10)"); + model._set("A6", "=DATE(1975, 2, -10)"); + model._set("A7", "=DATE(1975, 14, 10)"); + // February doesn't have 30 days + model._set("A8", "=DATE(1975, 2, 30)"); + + // 1975, a great year, wasn't a leap year + model._set("A9", "=DATE(1975, 2, 29)"); + // 1976 was + model._set("A10", "=DATE(1976, 2, 29)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + assert_eq!(model._get_text("A4"), *"#ERROR!"); + + assert_eq!(model._get_text("A5"), *"#NUM!"); + assert_eq!(model._get_text("A6"), *"#NUM!"); + assert_eq!(model._get_text("A7"), *"#NUM!"); + assert_eq!(model._get_text("A8"), *"#NUM!"); + + assert_eq!(model._get_text("A9"), *"#NUM!"); + assert_eq!(model._get_text("A10"), *"29/02/1976"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A10"), + Ok(CellValue::Number(27819.0)) + ); +} + +#[test] +fn test_date_out_of_range() { + let mut model = new_empty_model(); + + // month + model._set("A1", "=DATE(2022, 0, 10)"); + model._set("A2", "=DATE(2022, 13, 10)"); + + // day + model._set("B1", "=DATE(2042, 5, 0)"); + model._set("B2", "=DATE(2025, 5, 32)"); + + // year (actually years < 1900 don't really make sense) + model._set("C1", "=DATE(-1, 5, 5)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#NUM!"); + assert_eq!(model._get_text("A2"), *"#NUM!"); + assert_eq!(model._get_text("B1"), *"#NUM!"); + assert_eq!(model._get_text("B2"), *"#NUM!"); + + assert_eq!(model._get_text("C1"), *"#NUM!"); +} + +#[test] +fn test_year_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=YEAR()"); + model._set("A2", "=YEAR(27819)"); + model._set("A3", "=YEAR(27819, 3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"1976"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); +} + +#[test] +fn test_month_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=MONTH()"); + model._set("A2", "=MONTH(27819)"); + model._set("A3", "=MONTH(27819, 3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"2"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); +} + +#[test] +fn test_day_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=DAY()"); + model._set("A2", "=DAY(27819)"); + model._set("A3", "=DAY(27819, 3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"29"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); +} + +#[test] +fn test_day_small_serial() { + let mut model = new_empty_model(); + model._set("A1", "=DAY(-1)"); + model._set("A2", "=DAY(0)"); + model._set("A3", "=DAY(60)"); + + model._set("A4", "=DAY(61)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#NUM!"); + // This agrees with Google Docs and disagrees with Excel + assert_eq!(model._get_text("A2"), *"30"); + // Excel thinks is Feb 29, 1900 + assert_eq!(model._get_text("A3"), *"28"); + + // From now on everyone agrees + assert_eq!(model._get_text("A4"), *"1"); +} + +#[test] +fn test_month_small_serial() { + let mut model = new_empty_model(); + model._set("A1", "=MONTH(-1)"); + model._set("A2", "=MONTH(0)"); + model._set("A3", "=MONTH(60)"); + + model._set("A4", "=MONTH(61)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#NUM!"); + // This agrees with Google Docs and disagrees with Excel + assert_eq!(model._get_text("A2"), *"12"); + // We agree with Excel here (We are both in Feb) + assert_eq!(model._get_text("A3"), *"2"); + + // Same as Excel + assert_eq!(model._get_text("A4"), *"3"); +} + +#[test] +fn test_year_small_serial() { + let mut model = new_empty_model(); + model._set("A1", "=YEAR(-1)"); + model._set("A2", "=YEAR(0)"); + model._set("A3", "=YEAR(60)"); + + model._set("A4", "=YEAR(61)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#NUM!"); + // This agrees with Google Docs and disagrees with Excel + assert_eq!(model._get_text("A2"), *"1899"); + + assert_eq!(model._get_text("A3"), *"1900"); + + // Same as Excel + assert_eq!(model._get_text("A4"), *"1900"); +} + +#[test] +fn test_date_early_dates() { + let mut model = new_empty_model(); + model._set("A1", "=DATE(1900, 1, 1)"); + model._set("A2", "=DATE(1900, 2, 28)"); + model._set("B2", "=DATE(1900, 2, 29)"); + model._set("A3", "=DATE(1900, 3, 1)"); + + model.evaluate(); + + // This is 1 in Excel, we agree with Google Docs + assert_eq!(model._get_text("A1"), *"01/01/1900"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(2.0)) + ); + + // 1900 was not a leap year, this is a bug in EXCEL + // This would be 60 in Excel + assert_eq!(model._get_text("A2"), *"28/02/1900"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A2"), + Ok(CellValue::Number(60.0)) + ); + assert_eq!(model._get_text("B2"), *"#NUM!"); + + // This agrees with Excel from he onward + assert_eq!(model._get_text("A3"), *"01/03/1900"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A3"), + Ok(CellValue::Number(61.0)) + ); +} diff --git a/base/src/test/test_error_propagation.rs b/base/src/test/test_error_propagation.rs new file mode 100644 index 0000000..fed0a95 --- /dev/null +++ b/base/src/test/test_error_propagation.rs @@ -0,0 +1,39 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; +use crate::types::Cell; + +#[test] +fn test_simple_error_propagation() { + let mut model = new_empty_model(); + model._set("A1", "=1/0"); + model._set("A2", "=2+A1"); + model._set("A3", "=C2+A2"); + model.evaluate(); + match model._get_cell("Sheet1!A3") { + Cell::CellFormulaError { o, .. } => { + assert_eq!(o, "Sheet1!A1"); + } + _ => panic!("Unreachable"), + } +} + +#[test] +fn test_simple_errors() { + let mut model = new_empty_model(); + model._set("A1", "#CALC!"); + model._set("A2", "#SPILL!"); + model._set("A3", "#OTHER!"); + + model._set("B1", "=ISERROR(A1)"); + model._set("B2", "=ISERROR(A2)"); + model._set("B3", "=ISERROR(A3)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), "#CALC!"); + assert_eq!(model._get_text("A2"), "#SPILL!"); + assert_eq!(model._get_text("A3"), "#OTHER!"); + assert_eq!(model._get_text("B1"), "TRUE"); + assert_eq!(model._get_text("B2"), "TRUE"); + assert_eq!(model._get_text("B3"), "FALSE"); +} diff --git a/base/src/test/test_escape_quotes.rs b/base/src/test/test_escape_quotes.rs new file mode 100644 index 0000000..0047ae2 --- /dev/null +++ b/base/src/test/test_escape_quotes.rs @@ -0,0 +1,12 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn escape_quotes() { + let mut model = new_empty_model(); + model._set("A1", r#"="TEST""ABC""#); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *r#"TEST"ABC"#); +} diff --git a/base/src/test/test_fn_average.rs b/base/src/test/test_fn_average.rs new file mode 100644 index 0000000..1a10f13 --- /dev/null +++ b/base/src/test/test_fn_average.rs @@ -0,0 +1,31 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_average_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=AVERAGE()"); + model._set("A2", "=AVERAGEA()"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); +} + +#[test] +fn test_fn_average_minimal() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "2"); + model._set("B3", "3"); + model._set("B4", "'2"); + // B5 is empty + model._set("B6", "true"); + model._set("A1", "=AVERAGE(B1:B6)"); + model._set("A2", "=AVERAGEA(B1:B6)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"2"); + assert_eq!(model._get_text("A2"), *"1.4"); +} diff --git a/base/src/test/test_fn_averageifs.rs b/base/src/test/test_fn_averageifs.rs new file mode 100644 index 0000000..25ea1d2 --- /dev/null +++ b/base/src/test/test_fn_averageifs.rs @@ -0,0 +1,40 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_averageifs_arguments() { + let mut model = new_empty_model(); + + // Incorrect number of arguments + model._set("A1", "=AVERAGEIFS()"); + model._set("A2", "=AVERAGEIFS(B2:B9)"); + model._set("A3", "=AVERAGEIFS(B2:B9,C2:C9)"); + model._set("A4", "=AVERAGEIFS(B2:B9,C2:C9,\"=A*\",D2:D9)"); + + // Correct (Sum everything in column 'B' if column 'C' starts with "A") + model._set("A5", "=AVERAGEIFS(B2:B9,C2:C9,\"=A*\")"); + + // Data + model._set("B2", "5"); + model._set("B3", "4"); + model._set("B4", "15"); + model._set("B5", "22"); + model._set("B6", "=NA()"); + model._set("C2", "Apples"); + model._set("C3", "Bananas"); + model._set("C4", "Almonds"); + model._set("C5", "Yoni"); + model._set("C6", "Mandarin"); + + model.evaluate(); + + // Error (Incorrect number of arguments) + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + assert_eq!(model._get_text("A4"), *"#ERROR!"); + + // Correct + assert_eq!(model._get_text("A5"), *"10"); +} diff --git a/base/src/test/test_fn_choose.rs b/base/src/test/test_fn_choose.rs new file mode 100644 index 0000000..6864794 --- /dev/null +++ b/base/src/test/test_fn_choose.rs @@ -0,0 +1,50 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_choose_args_number() { + let mut model = new_empty_model(); + model._set("A1", "=CHOOSE()"); + model._set("A2", "=CHOOSE(1)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); +} + +#[test] +fn test_fn_choose_incorrect_index() { + let mut model = new_empty_model(); + model._set("A1", "=CHOOSE(-1, 42)"); + model._set("A2", "=CHOOSE(0, 42)"); + model._set("A3", "=CHOOSE(1, 42)"); + model._set("A4", "=CHOOSE(2, 42)"); + model._set("B1", "TEST"); + model._set("A5", "=CHOOSE(B1, 42)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#VALUE!"); + assert_eq!(model._get_text("A2"), *"#VALUE!"); + assert_eq!(model._get_text("A3"), *"42"); + assert_eq!(model._get_text("A4"), *"#VALUE!"); + assert_eq!(model._get_text("A5"), *"#VALUE!"); +} + +#[test] +fn test_fn_choose_basic_tests() { + let mut model = new_empty_model(); + model._set("B1", "1"); + model._set("B2", "2"); + model._set("B3", "3"); + model._set("A1", "=CHOOSE(3.1, B1, B2, B3)"); + model._set("A2", "=SUM(B1:CHOOSE(1, B1, B2, B3))"); + model._set("A3", "=SUM(CHOOSE(3, B1:B1, B1:B2, B1:B3))"); + model._set("A4", "=CHOOSE(3,\"Wide\",115,\"world\",8)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"3"); + assert_eq!(model._get_text("A2"), *"1"); + assert_eq!(model._get_text("A3"), *"6"); + assert_eq!(model._get_text("A4"), *"world"); +} diff --git a/base/src/test/test_fn_concatenate.rs b/base/src/test/test_fn_concatenate.rs new file mode 100644 index 0000000..727fe95 --- /dev/null +++ b/base/src/test/test_fn_concatenate.rs @@ -0,0 +1,34 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn fn_concatenate_args_number() { + let mut model = new_empty_model(); + model._set("A1", "=CONCATENATE()"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); +} + +#[test] +fn fn_concatenate() { + let mut model = new_empty_model(); + model._set("A1", "Hello"); + model._set("A2", " my "); + model._set("A3", "World"); + + model._set("B1", r#"=CONCATENATE(A1, A2, A3, "!")"#); + // This will break once we implement the implicit intersection operator + // It should be: + // model._set("B2", r#"=CONCATENATE(@A1:A3, "!")"#); + model._set("B2", r#"=CONCATENATE(A1:A3, "!")"#); + model._set("B3", r#"=CONCAT(A1:A3, "!")"#); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"Hello my World!"); + assert_eq!(model._get_text("B2"), *" my !"); + assert_eq!(model._get_text("B3"), *"Hello my World!"); +} diff --git a/base/src/test/test_fn_count.rs b/base/src/test/test_fn_count.rs new file mode 100644 index 0000000..df8a60c --- /dev/null +++ b/base/src/test/test_fn_count.rs @@ -0,0 +1,37 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_count_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=COUNT()"); + model._set("A2", "=COUNTA()"); + model._set("A3", "=COUNTBLANK()"); + model._set("A4", "=COUNTBLANK(C1:D1, H3:H4)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + assert_eq!(model._get_text("A4"), *"#ERROR!"); +} + +#[test] +fn test_fn_count_minimal() { + let mut model = new_empty_model(); + model._set("B1", "3.1415926"); + model._set("B2", "Tomorrow's the day my bride's gonna come"); + model._set("B3", ""); + model._set("A1", "=COUNT(B1:B5)"); + model._set("A2", "=COUNTA(B1:B5)"); + model._set("A3", "=COUNTBLANK(B1:B5)"); + model.evaluate(); + + // There is only one number + assert_eq!(model._get_text("A1"), *"1"); + // Thre are three non-empty cells + assert_eq!(model._get_text("A2"), *"3"); + // There are 3 blank cells B4, B5 and B3 that contains the empty string + assert_eq!(model._get_text("A3"), *"3"); +} diff --git a/base/src/test/test_fn_exact.rs b/base/src/test/test_fn_exact.rs new file mode 100644 index 0000000..0d03cc7 --- /dev/null +++ b/base/src/test/test_fn_exact.rs @@ -0,0 +1,31 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn fn_exact_args_number() { + let mut model = new_empty_model(); + + model._set("A1", "=EXACT(1)"); + model._set("A2", "=EXACT(1, 1, 1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); +} + +#[test] +fn fn_exact() { + let mut model = new_empty_model(); + + model._set("A1", "=EXACT(2.3, 2.3)"); + model._set("A2", r#"=EXACT(2.3, "2.3")"#); + model._set("A3", r#"=EXACT("Hello", "hello")"#); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"TRUE"); + assert_eq!(model._get_text("A2"), *"TRUE"); + assert_eq!(model._get_text("A3"), *"FALSE"); +} diff --git a/base/src/test/test_fn_financial.rs b/base/src/test/test_fn_financial.rs new file mode 100644 index 0000000..a5f31f0 --- /dev/null +++ b/base/src/test/test_fn_financial.rs @@ -0,0 +1,470 @@ +#![allow(clippy::unwrap_used)] + +use crate::{cell::CellValue, test::util::new_empty_model}; + +#[test] +fn fn_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=PMT()"); + model._set("A2", "=PMT(1,1)"); + model._set("A3", "=PMT(1,1,1,1,1,1)"); + + model._set("B1", "=FV()"); + model._set("B2", "=FV(1,1)"); + model._set("B3", "=FV(1,1,1,1,1,1)"); + + model._set("C1", "=PV()"); + model._set("C2", "=PV(1,1)"); + model._set("C3", "=PV(1,1,1,1,1,1)"); + + model._set("D1", "=NPER()"); + model._set("D2", "=NPER(1,1)"); + model._set("D3", "=NPER(1,1,1,1,1,1)"); + + model._set("E1", "=RATE()"); + model._set("E2", "=RATE(1,1)"); + model._set("E3", "=RATE(1,1,1,1,1,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + + assert_eq!(model._get_text("B1"), *"#ERROR!"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); + assert_eq!(model._get_text("C3"), *"#ERROR!"); + + assert_eq!(model._get_text("D1"), *"#ERROR!"); + assert_eq!(model._get_text("D2"), *"#ERROR!"); + assert_eq!(model._get_text("D3"), *"#ERROR!"); + + assert_eq!(model._get_text("E1"), *"#ERROR!"); + assert_eq!(model._get_text("E2"), *"#ERROR!"); + assert_eq!(model._get_text("E3"), *"#ERROR!"); +} + +#[test] +fn fn_impmt_ppmt_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=IPMT()"); + model._set("A2", "=IPMT(1,1,1)"); + model._set("A3", "=IPMT(1,1,1,1,1,1,1)"); + + model._set("B1", "=PPMT()"); + model._set("B2", "=PPMT(1,1,1)"); + model._set("B3", "=PPMT(1,1,1,1,1,1,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + + assert_eq!(model._get_text("B1"), *"#ERROR!"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); +} + +#[test] +fn fn_irr_npv_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=NPV()"); + model._set("A2", "=NPV(1,1)"); + + model._set("C1", "-2"); // v0 + model._set("C2", "5"); // v1 + model._set("B1", "=IRR()"); + model._set("B3", "=IRR(1, 2, 3, 4)"); + // r such that v0 + v1/(1+r) = 0 + // r = -v1/v0 - 1 + model._set("B4", "=IRR(C1:C2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"$0.50"); + + assert_eq!(model._get_text("B1"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); + // r = 5/2-1 = 1.5 + assert_eq!(model._get_text("B4"), *"150%"); +} + +#[test] +fn fn_mirr() { + let mut model = new_empty_model(); + model._set("A2", "-120000"); + model._set("A3", "39000"); + model._set("A4", "30000"); + model._set("A5", "21000"); + model._set("A6", "37000"); + model._set("A7", "46000"); + model._set("A8", "0.1"); + model._set("A9", "0.12"); + + model._set("B1", "=MIRR(A2:A7, A8, A9)"); + model._set("B2", "=MIRR(A2:A5, A8, A9)"); + + model.evaluate(); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!B1"), + Ok(CellValue::Number(0.1260941303659051)) + ); + assert_eq!(model._get_text("B1"), "13%"); + assert_eq!(model._get_text("B2"), "-5%"); +} + +#[test] +fn fn_mirr_div_0() { + // This test produces #DIV/0! in Excel (but it is incorrect) + let mut model = new_empty_model(); + model._set("A2", "-30"); + model._set("A3", "-20"); + model._set("A4", "-10"); + model._set("A5", "5"); + model._set("A6", "5"); + model._set("A7", "5"); + model._set("A8", "-1"); + model._set("A9", "2"); + + model._set("B1", "=MIRR(A2:A7, A8, A9)"); + + model.evaluate(); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!B1"), + Ok(CellValue::Number(-1.0)) + ); + assert_eq!(model._get_text("B1"), "-100%"); +} + +#[test] +fn fn_ispmt() { + let mut model = new_empty_model(); + model._set("A1", "1"); // rate + model._set("A2", "2"); // per + model._set("A3", "5"); // nper + model._set("A4", "4"); // pv + + model._set("B1", "=ISPMT(A1, A2, A3, A4)"); + model._set("B2", "=ISPMT(A1, A2, A3, A4, 1)"); + model._set("B3", "=ISPMT(A1, A2, A3)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "-2.4"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); +} + +#[test] +fn fn_rri() { + let mut model = new_empty_model(); + model._set("A1", "1"); // nper + model._set("A2", "2"); // pv + model._set("A3", "3"); // fv + + model._set("B1", "=RRI(A1, A2, A3)"); + model._set("B2", "=RRI(A1, A2)"); + model._set("B3", "=RRI(A1, A2, A3, 1)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "0.5"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); +} + +#[test] +fn fn_sln() { + let mut model = new_empty_model(); + model._set("A1", "1"); // cost + model._set("A2", "2"); // salvage + model._set("A3", "3"); // life + + model._set("B1", "=SLN(A1, A2, A3)"); + model._set("B2", "=SLN(A1, A2)"); + model._set("B3", "=SLN(A1, A2, A3, 1)"); + + model.evaluate(); + + assert_eq!( + model.get_cell_value_by_ref("Sheet1!B1"), + Ok(CellValue::Number(-1.0 / 3.0)) + ); + assert_eq!(model._get_text("B1"), "-$0.33"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); +} + +#[test] +fn fn_syd() { + let mut model = new_empty_model(); + model._set("A1", "100"); // cost + model._set("A2", "5"); // salvage + model._set("A3", "20"); // life + model._set("A4", "10"); // periods + + model._set("B1", "=SYD(A1, A2, A3, A4)"); + model._set("B2", "=SYD(A1, A2, A3)"); + model._set("B3", "=SYD(A1, A2, A3, A4, 1)"); + + model.evaluate(); + + assert_eq!( + model.get_cell_value_by_ref("Sheet1!B1"), + Ok(CellValue::Number(4.976190476190476)) + ); + assert_eq!(model._get_text("B1"), "$4.98"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); +} + +#[test] +fn fn_effect() { + let mut model = new_empty_model(); + model._set("A1", "2"); // rate + model._set("A2", "1"); // periods + + model._set("B1", "=EFFECT(A1, A2)"); + model._set("B2", "=EFFECT(A1)"); + model._set("B3", "=EFFECT(A1, A2, A3)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "2"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); +} + +#[test] +fn fn_nominal() { + let mut model = new_empty_model(); + model._set("A1", "2"); // rate + model._set("A2", "1"); // periods + + model._set("B1", "=NOMINAL(A1, A2)"); + model._set("B2", "=NOMINAL(A1)"); + model._set("B3", "=NOMINAL(A1, A2, A3)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "2"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); +} + +#[test] +fn fn_db() { + let mut model = new_empty_model(); + model._set("A2", "$1,000,000"); // cost + model._set("A3", "$100,000"); // salvage + model._set("A4", "6"); // life + + model._set("B1", "=DB(A2,A3,A4,1,7)"); + model._set("B2", "=DB(A2,A3,A4,2,7)"); + model._set("B3", "=DB(A2,A3,A4,3,7)"); + model._set("B4", "=DB(A2,A3,A4,4,7)"); + model._set("B5", "=DB(A2,A3,A4,5,7)"); + model._set("B6", "=DB(A2,A3,A4,6,7)"); + model._set("B7", "=DB(A2,A3,A4,7,7)"); + + model._set("C1", "=DB(A2,A3,A4,7,7,1)"); + model._set("C2", "=DB(A2,A3,A4)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "$186,083.33"); + assert_eq!(model._get_text("B2"), "$259,639.42"); + assert_eq!(model._get_text("B3"), "$176,814.44"); + assert_eq!(model._get_text("B4"), "$120,410.64"); + assert_eq!(model._get_text("B5"), "$81,999.64"); + assert_eq!(model._get_text("B6"), "$55,841.76"); + assert_eq!(model._get_text("B7"), "$15,845.10"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); +} + +#[test] +fn fn_ddb() { + let mut model = new_empty_model(); + model._set("A2", "$2,400"); // cost + model._set("A3", "$300"); // salvage + model._set("A4", "10"); // life + + model._set("B1", "=DDB(A2,A3,A4*365,1)"); + model._set("B2", "=DDB(A2,A3,A4*12,1,2)"); + model._set("B3", "=DDB(A2,A3,A4,1,2)"); + model._set("B4", "=DDB(A2,A3,A4,2,1.5)"); + model._set("B5", "=DDB(A2,A3,A4,10)"); + + model._set("C1", "=DB(A2,A3,A4,7,7,1)"); + model._set("C2", "=DB(A2,A3,A4)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "$1.32"); + assert_eq!(model._get_text("B2"), "$40.00"); + assert_eq!(model._get_text("B3"), "$480.00"); + assert_eq!(model._get_text("B4"), "$306.00"); + assert_eq!(model._get_text("B5"), "$22.12"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); +} + +#[test] +fn fn_tbilleq() { + let mut model = new_empty_model(); + model._set("A2", "=DATE(2008, 3, 31)"); // settlement date + model._set("A3", "=DATE(2008, 6, 1)"); // maturity date + model._set("A4", "9.14%"); + + model._set("B1", "=TBILLEQ(A2,A3,A4)"); + + model._set("C1", "=TBILLEQ(A2,A3)"); + model._set("C2", "=TBILLEQ(A2,A3,A4,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "9.42%"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); +} + +#[test] +fn fn_tbillprice() { + let mut model = new_empty_model(); + model._set("A2", "=DATE(2008, 3, 31)"); // settlement date + model._set("A3", "=DATE(2008, 6, 1)"); // maturity date + model._set("A4", "9.0%"); + + model._set("B1", "=TBILLPRICE(A2,A3,A4)"); + + model._set("C1", "=TBILLPRICE(A2,A3)"); + model._set("C2", "=TBILLPRICE(A2,A3,A4,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "$98.45"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); +} + +#[test] +fn fn_tbillyield() { + let mut model = new_empty_model(); + model._set("A2", "=DATE(2008, 3, 31)"); // settlement date + model._set("A3", "=DATE(2008, 6, 1)"); // maturity date + model._set("A4", "$98.45"); + + model._set("B1", "=TBILLYIELD(A2,A3,A4)"); + + model._set("C1", "=TBILLYIELD(A2,A3)"); + model._set("C2", "=TBILLYIELD(A2,A3,A4,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "9.14%"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); +} + +#[test] +fn fn_dollarde() { + let mut model = new_empty_model(); + model._set("A1", "=DOLLARDE(1.02, 16)"); + model._set("A2", "=DOLLARDE(1.1, 32)"); + + model._set("C1", "=DOLLARDE(1.1)"); + model._set("C2", "=DOLLARDE(1.1, 32, 1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), "1.125"); + assert_eq!(model._get_text("A2"), "1.3125"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); +} + +#[test] +fn fn_dollarfr() { + let mut model = new_empty_model(); + model._set("A1", "=DOLLARFR(1.125,16)"); + model._set("A2", "=DOLLARFR(1.125,32)"); + + model._set("C1", "=DOLLARFR(1.1)"); + model._set("C2", "=DOLLARFR(1.1, 32, 1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), "1.02"); + assert_eq!(model._get_text("A2"), "1.04"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); +} + +#[test] +fn fn_cumipmt() { + let mut model = new_empty_model(); + model._set("A2", "9%"); // annual interest rate + model._set("A3", "30"); // years of the load + model._set("A4", "$125,000"); // present value + + model._set("B1", "=CUMIPMT(A2/12,A3*12,A4,13,24,0)"); + model._set("B2", "=CUMIPMT(A2/12,A3*12,A4,1,1,0)"); + + model._set("C1", "=CUMIPMT(A2/12,A3*12,A4,1,1,0,1)"); + model._set("C2", "=CUMIPMT(A2/12,A3*12,A4,1,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "-$11,135.23"); + assert_eq!(model._get_text("B2"), "-$937.50"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); +} + +#[test] +fn fn_cumprinc() { + let mut model = new_empty_model(); + model._set("A2", "9%"); // annual interest rate + model._set("A3", "30"); // years of the load + model._set("A4", "$125,000"); // present value + + model._set("B1", "=CUMPRINC(A2/12,A3*12,A4,13,24,0)"); + model._set("B2", "=CUMPRINC(A2/12,A3*12,A4,1,1,0)"); + + model._set("C1", "=CUMPRINC(A2/12,A3*12,A4,1,1,0,1)"); + model._set("C2", "=CUMPRINC(A2/12,A3*12,A4,1,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "-$934.11"); + assert_eq!(model._get_text("B2"), "-$68.28"); + + assert_eq!(model._get_text("C1"), *"#ERROR!"); + assert_eq!(model._get_text("C2"), *"#ERROR!"); +} + +#[test] +fn fn_db_misc() { + let mut model = new_empty_model(); + + model._set("B1", "=DB(0,10,1,2,2)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), "$0.00"); +} diff --git a/base/src/test/test_fn_if.rs b/base/src/test/test_fn_if.rs new file mode 100644 index 0000000..201a53f --- /dev/null +++ b/base/src/test/test_fn_if.rs @@ -0,0 +1,36 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn fn_if_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=IF()"); + model._set("A2", "=IF(1, 2, 3, 4)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); +} + +#[test] +fn fn_if_2_args() { + let mut model = new_empty_model(); + model._set("A1", "=IF(2 > 3, TRUE)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"FALSE"); +} + +#[test] +fn fn_if_missing_args() { + let mut model = new_empty_model(); + model._set("A1", "=IF(2 > 3, TRUE, )"); + model._set("A2", "=IF(2 > 3, , 5)"); + model._set("A3", "=IF(2 < 3, , 5)"); + + model.evaluate(); + + // assert_eq!(model._get_text("A1"), *"0"); + assert_eq!(model._get_text("A2"), *"5"); + assert_eq!(model._get_text("A3"), *"0"); +} diff --git a/base/src/test/test_fn_maxifs.rs b/base/src/test/test_fn_maxifs.rs new file mode 100644 index 0000000..3e0c7ed --- /dev/null +++ b/base/src/test/test_fn_maxifs.rs @@ -0,0 +1,40 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_maxifs_arguments() { + let mut model = new_empty_model(); + + // Incorrect number of arguments + model._set("A1", "=MAXIFS()"); + model._set("A2", "=MAXIFS(B2:B9)"); + model._set("A3", "=MAXIFS(B2:B9,C2:C9)"); + model._set("A4", "=MAXIFS(B2:B9,C2:C9,\"=A*\",D2:D9)"); + + // Correct (Sum everything in column 'B' if column 'C' starts with "A") + model._set("A5", "=MAXIFS(B2:B9,C2:C9,\"=A*\")"); + + // Data + model._set("B2", "5"); + model._set("B3", "4"); + model._set("B4", "15"); + model._set("B5", "22"); + model._set("B6", "=NA()"); + model._set("C2", "Apples"); + model._set("C3", "Bananas"); + model._set("C4", "Almonds"); + model._set("C5", "Yoni"); + model._set("C6", "Mandarin"); + + model.evaluate(); + + // Error (Incorrect number of arguments) + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + assert_eq!(model._get_text("A4"), *"#ERROR!"); + + // Correct + assert_eq!(model._get_text("A5"), *"15"); +} diff --git a/base/src/test/test_fn_minifs.rs b/base/src/test/test_fn_minifs.rs new file mode 100644 index 0000000..8b4bb66 --- /dev/null +++ b/base/src/test/test_fn_minifs.rs @@ -0,0 +1,40 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_minifs_arguments() { + let mut model = new_empty_model(); + + // Incorrect number of arguments + model._set("A1", "=MINIFS()"); + model._set("A2", "=MINIFS(B2:B9)"); + model._set("A3", "=MINIFS(B2:B9,C2:C9)"); + model._set("A4", "=MINIFS(B2:B9,C2:C9,\"=A*\",D2:D9)"); + + // Correct (Sum everything in column 'B' if column 'C' starts with "A") + model._set("A5", "=MINIFS(B2:B9,C2:C9,\"=A*\")"); + + // Data + model._set("B2", "5"); + model._set("B3", "4"); + model._set("B4", "15"); + model._set("B5", "22"); + model._set("B6", "=NA()"); + model._set("C2", "Apples"); + model._set("C3", "Bananas"); + model._set("C4", "Almonds"); + model._set("C5", "Yoni"); + model._set("C6", "Mandarin"); + + model.evaluate(); + + // Error (Incorrect number of arguments) + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + assert_eq!(model._get_text("A4"), *"#ERROR!"); + + // Correct + assert_eq!(model._get_text("A5"), *"5"); +} diff --git a/base/src/test/test_fn_offset.rs b/base/src/test/test_fn_offset.rs new file mode 100644 index 0000000..fcfcf10 --- /dev/null +++ b/base/src/test/test_fn_offset.rs @@ -0,0 +1,19 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn fn_offset_reference() { + let mut model = new_empty_model(); + model._set("B1", "12"); + model._set("B2", "13"); + model._set("B3", "15"); + + model._set("A1", "=SUM(B1:OFFSET($B$1,3,0))"); + model._set("A2", "=SUM(OFFSET(A1, 1, 1):B3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"40"); + assert_eq!(model._get_text("A2"), *"28"); +} diff --git a/base/src/test/test_fn_product.rs b/base/src/test/test_fn_product.rs new file mode 100644 index 0000000..20dfc3d --- /dev/null +++ b/base/src/test/test_fn_product.rs @@ -0,0 +1,15 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_product_arguments() { + let mut model = new_empty_model(); + + // Incorrect number of arguments + model._set("A1", "=PRODUCT()"); + + model.evaluate(); + // Error (Incorrect number of arguments) + assert_eq!(model._get_text("A1"), *"#ERROR!"); +} diff --git a/base/src/test/test_fn_rept.rs b/base/src/test/test_fn_rept.rs new file mode 100644 index 0000000..f6ed3ac --- /dev/null +++ b/base/src/test/test_fn_rept.rs @@ -0,0 +1,29 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn simple_cases() { + let mut model = new_empty_model(); + model._set("A1", "Well"); + + model._set("B1", "=REPT(A1, 3)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"WellWellWell"); +} + +#[test] +fn wrong_number_of_arguments() { + let mut model = new_empty_model(); + model._set("A1", "Well"); + + model._set("B1", "=REPT(A1)"); + model._set("B2", "=REPT(A1,3,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"#ERROR!"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); +} diff --git a/base/src/test/test_fn_sum.rs b/base/src/test/test_fn_sum.rs new file mode 100644 index 0000000..fa680d0 --- /dev/null +++ b/base/src/test/test_fn_sum.rs @@ -0,0 +1,19 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_sum_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=SUM()"); + model._set("A2", "=SUM(1, 2, 3)"); + model._set("A3", "=SUM(1, )"); + model._set("A4", "=SUM(1, , 3)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"6"); + assert_eq!(model._get_text("A3"), *"1"); + assert_eq!(model._get_text("A4"), *"4"); +} diff --git a/base/src/test/test_fn_sumifs.rs b/base/src/test/test_fn_sumifs.rs new file mode 100644 index 0000000..55922d5 --- /dev/null +++ b/base/src/test/test_fn_sumifs.rs @@ -0,0 +1,40 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_sumifs_arguments() { + let mut model = new_empty_model(); + + // Incorrect number of arguments + model._set("A1", "=SUMIFS()"); + model._set("A2", "=SUMIFS(B2:B9)"); + model._set("A3", "=SUMIFS(B2:B9,C2:C9)"); + model._set("A4", "=SUMIFS(B2:B9,C2:C9,\"=A*\",D2:D9)"); + + // Correct (Sum everything in column 'B' if column 'C' starts with "A") + model._set("A5", "=SUMIFS(B2:B9,C2:C9,\"=A*\")"); + + // Data + model._set("B2", "5"); + model._set("B3", "4"); + model._set("B4", "15"); + model._set("B5", "22"); + model._set("B6", "=NA()"); + model._set("C2", "Apples"); + model._set("C3", "Bananas"); + model._set("C4", "Almonds"); + model._set("C5", "Yoni"); + model._set("C6", "Mandarin"); + + model.evaluate(); + + // Error (Incorrect number of arguments) + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + assert_eq!(model._get_text("A4"), *"#ERROR!"); + + // Correct + assert_eq!(model._get_text("A5"), *"20"); +} diff --git a/base/src/test/test_fn_textbefore.rs b/base/src/test/test_fn_textbefore.rs new file mode 100644 index 0000000..85aa3ec --- /dev/null +++ b/base/src/test/test_fn_textbefore.rs @@ -0,0 +1,29 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn simple_cases() { + let mut model = new_empty_model(); + model._set("A1", "Brian Wiles"); + model._set("A2", "Jeden,dwa,trzy,cztery"); + + model._set("B1", "=TEXTAFTER(A1, \" \")"); + model._set("B2", "=TEXTAFTER(A2, \",\")"); + model._set("C2", "=TEXTAFTER(A2, \",\", 2)"); + + model._set("H1", "=TEXTBEFORE(A1, \" \")"); + model._set("H2", "=TEXTBEFORE(A2, \",\")"); + model._set("I2", "=_xlfn.TEXTBEFORE(A2, \",\", 2)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"Wiles"); + assert_eq!(model._get_text("B2"), *"dwa,trzy,cztery"); + assert_eq!(model._get_text("C2"), *"trzy,cztery"); + + assert_eq!(model._get_text("H1"), *"Brian"); + assert_eq!(model._get_text("H2"), *"Jeden"); + assert_eq!(model._get_text("I2"), *"Jeden,dwa"); + assert_eq!(model._get_formula("I2"), *"=TEXTBEFORE(A2,\",\",2)"); +} diff --git a/base/src/test/test_fn_textjoin.rs b/base/src/test/test_fn_textjoin.rs new file mode 100644 index 0000000..57dc470 --- /dev/null +++ b/base/src/test/test_fn_textjoin.rs @@ -0,0 +1,40 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn simple_cases() { + let mut model = new_empty_model(); + model._set("A1", "Monday"); + model._set("A2", "Tuesday"); + model._set("A3", "Wednesday"); + + model._set("B1", "=TEXTJOIN(\", \", TRUE, A1:A3)"); + model._set("B2", "=TEXTJOIN(\" and \", TRUE, A1:A3)"); + // This formula might have the _xlfn. prefix + model._set("B3", "=_xlfn.TEXTJOIN(\" or \", , A1:A3)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"Monday, Tuesday, Wednesday"); + assert_eq!(model._get_text("B2"), *"Monday and Tuesday and Wednesday"); + assert_eq!(model._get_text("B3"), *"Monday or Tuesday or Wednesday"); + // Our text version removes the prefix, of course (and some white spaces) + assert_eq!(model._get_formula("B3"), *"=TEXTJOIN(\" or \",,A1:A3)"); +} + +#[test] +fn wrong_number_of_arguments() { + let mut model = new_empty_model(); + model._set("A1", "Monday"); + model._set("A2", "Tuesday"); + model._set("A3", "Wednesday"); + + model._set("B1", "=TEXTJOIN(\", \", TRUE)"); + model._set("B2", "=TEXTJOIN(\" and \", A1:A3)"); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"#ERROR!"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); +} diff --git a/base/src/test/test_fn_type.rs b/base/src/test/test_fn_type.rs new file mode 100644 index 0000000..9dbc411 --- /dev/null +++ b/base/src/test/test_fn_type.rs @@ -0,0 +1,15 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn fn_type_array() { + let mut model = new_empty_model(); + model._set("A1", "=TYPE()"); + model._set("A2", "=TYPE(A1:C30)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"64"); +} diff --git a/base/src/test/test_forward_references.rs b/base/src/test/test_forward_references.rs new file mode 100644 index 0000000..0b8aa23 --- /dev/null +++ b/base/src/test/test_forward_references.rs @@ -0,0 +1,121 @@ +#![allow(clippy::unwrap_used)] + +use crate::expressions::types::{Area, CellReferenceIndex}; +use crate::test::util::new_empty_model; + +#[test] +fn test_forward_references() { + let mut model = new_empty_model(); + + // test single ref changed nd not changed + model._set("H8", "=F6*G9"); + // tests areas + model._set("H9", "=SUM(D4:F6)"); + // absolute coordinates + model._set("H10", "=$F$6"); + // area larger than the source area + model._set("H11", "=SUM(D3:F6)"); + // Test arguments and concat + model._set("H12", "=SUM(F6, D4:F6) & D4"); + // Test range operator. This is syntax error for now. + // model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))"); + // Test operations + model._set("H14", "=-D4+D5*F6/F5"); + + model.evaluate(); + + // Source Area is D4:F6 + let source_area = &Area { + sheet: 0, + row: 4, + column: 4, + width: 3, + height: 3, + }; + + // We paste in B10 + let target_row = 10; + let target_column = 2; + let result = model.forward_references( + source_area, + &CellReferenceIndex { + sheet: 0, + row: target_row, + column: target_column, + }, + ); + assert!(result.is_ok()); + model.evaluate(); + + assert_eq!(model._get_formula("H8"), "=D12*G9"); + assert_eq!(model._get_formula("H9"), "=SUM(B10:D12)"); + assert_eq!(model._get_formula("H10"), "=$D$12"); + + assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)"); + assert_eq!(model._get_formula("H12"), "=SUM(D12,B10:D12)&B10"); + // assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))"); + assert_eq!(model._get_formula("H14"), "=-B10+B11*D12/D11"); +} + +#[test] +fn test_different_sheet() { + let mut model = new_empty_model(); + + // test single ref changed not changed + model._set("H8", "=F6*G9"); + // tests areas + model._set("H9", "=SUM(D4:F6)"); + // absolute coordinates + model._set("H10", "=$F$6"); + // area larger than the source area + model._set("H11", "=SUM(D3:F6)"); + // Test arguments and concat + model._set("H12", "=SUM(F6, D4:F6) & D4"); + // Test range operator. This is syntax error for now. + // model._set("H13", "=SUM(D4:INDEX(D4:F5,4,2))"); + // Test operations + model._set("H14", "=-D4+D5*F6/F5"); + + // Adds a new sheet + assert!(model.add_sheet("Sheet2").is_ok()); + + model.evaluate(); + + // Source Area is D4:F6 + let source_area = &Area { + sheet: 0, + row: 4, + column: 4, + width: 3, + height: 3, + }; + + // We paste in Sheet2!B10 + let target_row = 10; + let target_column = 2; + let result = model.forward_references( + source_area, + &CellReferenceIndex { + sheet: 1, + row: target_row, + column: target_column, + }, + ); + assert!(result.is_ok()); + model.evaluate(); + + assert_eq!(model._get_formula("H8"), "=Sheet2!D12*G9"); + assert_eq!(model._get_formula("H9"), "=SUM(Sheet2!B10:D12)"); + assert_eq!(model._get_formula("H10"), "=Sheet2!$D$12"); + + assert_eq!(model._get_formula("H11"), "=SUM(D3:F6)"); + assert_eq!( + model._get_formula("H12"), + "=SUM(Sheet2!D12,Sheet2!B10:D12)&Sheet2!B10" + ); + // assert_eq!(model._get_formula("H13"), "=SUM(B10:INDEX(B10:D11,4,2))"); + assert_eq!( + model._get_formula("H14"), + "=-Sheet2!B10+Sheet2!B11*Sheet2!D12/Sheet2!D11" + ); +} diff --git a/base/src/test/test_frozen_rows_columns.rs b/base/src/test/test_frozen_rows_columns.rs new file mode 100644 index 0000000..57825a9 --- /dev/null +++ b/base/src/test/test_frozen_rows_columns.rs @@ -0,0 +1,54 @@ +#![allow(clippy::unwrap_used)] + +use crate::constants::{LAST_COLUMN, LAST_ROW}; +use crate::test::util::new_empty_model; + +#[test] +fn test_empty_model() { + let mut model = new_empty_model(); + let worksheet = model.workbook.worksheet_mut(0).unwrap(); + assert_eq!(worksheet.frozen_rows, 0); + assert_eq!(worksheet.frozen_columns, 0); + + let e = worksheet.set_frozen_rows(3); + assert!(e.is_ok()); + assert_eq!(worksheet.frozen_rows, 3); + assert_eq!(worksheet.frozen_columns, 0); + + let e = worksheet.set_frozen_columns(53); + assert!(e.is_ok()); + assert_eq!(worksheet.frozen_rows, 3); + assert_eq!(worksheet.frozen_columns, 53); + + // Set them back to zero + let e = worksheet.set_frozen_rows(0); + assert!(e.is_ok()); + let e = worksheet.set_frozen_columns(0); + assert!(e.is_ok()); + assert_eq!(worksheet.frozen_rows, 0); + assert_eq!(worksheet.frozen_columns, 0); +} + +#[test] +fn test_invalid_rows_columns() { + let mut model = new_empty_model(); + let worksheet = model.workbook.worksheet_mut(0).unwrap(); + + assert_eq!( + worksheet.set_frozen_rows(-3), + Err("Frozen rows cannot be negative".to_string()) + ); + assert_eq!( + worksheet.set_frozen_columns(-5), + Err("Frozen columns cannot be negative".to_string()) + ); + + assert_eq!( + worksheet.set_frozen_rows(LAST_ROW), + Err("Too many rows".to_string()) + ); + assert_eq!( + worksheet.set_frozen_columns(LAST_COLUMN), + Err("Too many columns".to_string()) + ); +} diff --git a/base/src/test/test_general.rs b/base/src/test/test_general.rs new file mode 100644 index 0000000..ada2ea8 --- /dev/null +++ b/base/src/test/test_general.rs @@ -0,0 +1,484 @@ +#![allow(clippy::unwrap_used)] + +use crate::cell::CellValue; + +use crate::number_format::to_excel_precision_str; + +use crate::test::util::new_empty_model; + +#[test] +fn test_empty_model() { + let model = new_empty_model(); + let names = model.workbook.get_worksheet_names(); + assert_eq!(names.len(), 1); + assert_eq!(names[0], "Sheet1"); +} + +#[test] +fn test_model_simple_evaluation() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "= 1 + 3".to_string()); + model.evaluate(); + let result = model._get_text_at(0, 1, 1); + assert_eq!(result, *"4"); + let result = model._get_formula("A1"); + assert_eq!(result, *"=1+3"); +} + +#[test] +fn test_model_simple_evaluation_order() { + let mut model = new_empty_model(); + model._set("A1", "=1/2/3"); + model._set("A2", "=(1/2)/3"); + model._set("A3", "=1/(2/3)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"0.166666667"); + assert_eq!(model._get_text("A2"), *"0.166666667"); + assert_eq!(model._get_text("A3"), *"1.5"); + // Unnecessary parenthesis are lost + assert_eq!(model._get_formula("A2"), *"=1/2/3"); + assert_eq!(model._get_formula("A3"), *"=1/(2/3)"); +} + +#[test] +fn test_model_invalid_formula() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "= 1 +".to_string()); + model.evaluate(); + let result = model._get_text_at(0, 1, 1); + assert_eq!(result, *"#ERROR!"); + let result = model._get_formula("A1"); + assert_eq!(result, *"= 1 +"); +} + +#[test] +fn test_model_dependencies() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "23".to_string()); // A1 + model.set_user_input(0, 1, 2, "= A1* 2-4".to_string()); // B1 + model.evaluate(); + let result = model._get_text_at(0, 1, 1); + assert_eq!(result, *"23"); + assert!(!model._has_formula("A1")); + let result = model._get_text_at(0, 1, 2); + assert_eq!(result, *"42"); + let result = model._get_formula("B1"); + assert_eq!(result, *"=A1*2-4"); + + model.set_user_input(0, 2, 1, "=SUM(A1, B1)".to_string()); // A2 + model.evaluate(); + let result = model._get_text_at(0, 2, 1); + assert_eq!(result, *"65"); +} + +#[test] +fn test_model_strings() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "Hello World".to_string()); + model.set_user_input(0, 1, 2, "=A1".to_string()); + model.evaluate(); + let result = model._get_text_at(0, 1, 1); + assert_eq!(result, *"Hello World"); + let result = model._get_text_at(0, 1, 2); + assert_eq!(result, *"Hello World"); +} + +#[test] +fn test_get_sheet_index_by_sheet_id() { + let mut model = new_empty_model(); + model.new_sheet(); + + assert_eq!(model.get_sheet_index_by_sheet_id(1), Some(0)); + assert_eq!(model.get_sheet_index_by_sheet_id(2), Some(1)); + assert_eq!(model.get_sheet_index_by_sheet_id(1337), None); +} + +#[test] +fn test_set_row_height() { + let mut model = new_empty_model(); + let worksheet = model.workbook.worksheet_mut(0).unwrap(); + worksheet.set_row_height(5, 25.0).unwrap(); + let worksheet = model.workbook.worksheet(0).unwrap(); + assert!((25.0 - worksheet.row_height(5).unwrap()).abs() < f64::EPSILON); + + let worksheet = model.workbook.worksheet_mut(0).unwrap(); + worksheet.set_row_height(5, 5.0).unwrap(); + let worksheet = model.workbook.worksheet(0).unwrap(); + assert!((5.0 - worksheet.row_height(5).unwrap()).abs() < f64::EPSILON); +} + +#[test] +fn test_to_excel_precision_str() { + struct TestCase<'a> { + value: f64, + str: &'a str, + } + let test_cases = vec![ + TestCase { + value: 2e-23, + str: "2e-23", + }, + TestCase { + value: 42.0, + str: "42", + }, + TestCase { + value: 200.0e-23, + str: "2e-21", + }, + TestCase { + value: -200e-23, + str: "-2e-21", + }, + TestCase { + value: 10.002, + str: "10.002", + }, + TestCase { + value: f64::INFINITY, + str: "inf", + }, + TestCase { + value: f64::NAN, + str: "NaN", + }, + ]; + for test_case in test_cases { + let str = to_excel_precision_str(test_case.value); + assert_eq!(str, test_case.str); + } +} + +#[test] +fn test_booleans() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "true".to_string()); + model.set_user_input(0, 2, 1, "TRUE".to_string()); + model.set_user_input(0, 3, 1, "True".to_string()); + model.set_user_input(0, 4, 1, "false".to_string()); + model.set_user_input(0, 5, 1, "FALSE".to_string()); + model.set_user_input(0, 6, 1, "False".to_string()); + + model.set_user_input(0, 1, 2, "=ISLOGICAL(A1)".to_string()); + model.set_user_input(0, 2, 2, "=ISLOGICAL(A2)".to_string()); + model.set_user_input(0, 3, 2, "=ISLOGICAL(A3)".to_string()); + model.set_user_input(0, 4, 2, "=ISLOGICAL(A4)".to_string()); + model.set_user_input(0, 5, 2, "=ISLOGICAL(A5)".to_string()); + model.set_user_input(0, 6, 2, "=ISLOGICAL(A6)".to_string()); + + model.set_user_input(0, 1, 5, "=IF(false, True, FALSe)".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text_at(0, 1, 1), *"TRUE"); + assert_eq!(model._get_text_at(0, 2, 1), *"TRUE"); + assert_eq!(model._get_text_at(0, 3, 1), *"TRUE"); + + assert_eq!(model._get_text_at(0, 4, 1), *"FALSE"); + assert_eq!(model._get_text_at(0, 5, 1), *"FALSE"); + assert_eq!(model._get_text_at(0, 6, 1), *"FALSE"); + + assert_eq!(model._get_text_at(0, 1, 2), *"TRUE"); + assert_eq!(model._get_text_at(0, 2, 2), *"TRUE"); + assert_eq!(model._get_text_at(0, 3, 2), *"TRUE"); + assert_eq!(model._get_text_at(0, 4, 2), *"TRUE"); + assert_eq!(model._get_text_at(0, 5, 2), *"TRUE"); + assert_eq!(model._get_text_at(0, 6, 2), *"TRUE"); + + assert_eq!(model._get_formula("E1"), *"=IF(FALSE,TRUE,FALSE)"); +} + +#[test] +fn test_set_cell_style() { + let mut model = new_empty_model(); + let mut style = model.get_style_for_cell(0, 1, 1); + assert!(!style.font.b); + + style.font.b = true; + assert!(model.set_cell_style(0, 1, 1, &style).is_ok()); + + let mut style = model.get_style_for_cell(0, 1, 1); + assert!(style.font.b); + + style.font.b = false; + assert!(model.set_cell_style(0, 1, 1, &style).is_ok()); + + let style = model.get_style_for_cell(0, 1, 1); + assert!(!style.font.b); +} + +#[test] +fn test_copy_cell_style() { + let mut model = new_empty_model(); + + let mut style = model.get_style_for_cell(0, 1, 1); + style.font.b = true; + assert!(model.set_cell_style(0, 1, 1, &style).is_ok()); + + let mut style = model.get_style_for_cell(0, 1, 2); + style.font.i = true; + assert!(model.set_cell_style(0, 1, 2, &style).is_ok()); + + assert!(model.copy_cell_style((0, 1, 1), (0, 1, 2)).is_ok()); + + let style = model.get_style_for_cell(0, 1, 1); + assert!(style.font.b); + assert!(!style.font.i); + + let style = model.get_style_for_cell(0, 1, 2); + assert!(style.font.b); + assert!(!style.font.i); +} + +#[test] +fn test_get_cell_style_index() { + let mut model = new_empty_model(); + + let mut style = model.get_style_for_cell(0, 1, 1); + let style_index = model.get_cell_style_index(0, 1, 1); + assert_eq!(style_index, 0); + assert!(!style.font.b); + + style.font.b = true; + assert!(model.set_cell_style(0, 1, 1, &style).is_ok()); + + let style_index = model.get_cell_style_index(0, 1, 1); + assert_eq!(style_index, 1); +} + +#[test] +fn test_model_set_cells_with_values_styles() { + let mut model = new_empty_model(); + // Inputs + model.set_user_input(0, 1, 1, "21".to_string()); // A1 + model.set_user_input(0, 2, 1, "2".to_string()); // A2 + + let style_index = model.get_cell_style_index(0, 1, 1); + assert_eq!(style_index, 0); + let mut style = model.get_style_for_cell(0, 1, 1); + style.font.b = true; + assert!(model.set_cell_style(0, 1, 1, &style).is_ok()); + assert!(model.set_cell_style(0, 2, 1, &style).is_ok()); + let style_index = model.get_cell_style_index(0, 1, 1); + assert_eq!(style_index, 1); + let style_index = model.get_cell_style_index(0, 2, 1); + assert_eq!(style_index, 1); + + model.update_cell_with_number(0, 1, 2, 1.0); + model.update_cell_with_number(0, 2, 1, 2.0); + + model.evaluate(); + + // Styles are not modified + let style_index = model.get_cell_style_index(0, 1, 1); + assert_eq!(style_index, 1); + let style_index = model.get_cell_style_index(0, 2, 1); + assert_eq!(style_index, 1); +} + +#[test] +fn test_style_fmt_id() { + let mut model = new_empty_model(); + + let mut style = model.get_style_for_cell(0, 1, 1); + style.num_fmt = "#.##".to_string(); + assert!(model.set_cell_style(0, 1, 1, &style).is_ok()); + let style = model.get_style_for_cell(0, 1, 1); + assert_eq!(style.num_fmt, "#.##"); + + let mut style = model.get_style_for_cell(0, 10, 1); + style.num_fmt = "$$#,##0.0000".to_string(); + assert!(model.set_cell_style(0, 10, 1, &style).is_ok()); + let style = model.get_style_for_cell(0, 10, 1); + assert_eq!(style.num_fmt, "$$#,##0.0000"); + + // Make sure old style is not touched + let style = model.get_style_for_cell(0, 1, 1); + assert_eq!(style.num_fmt, "#.##"); +} + +#[test] +fn test_set_sheet_color() { + let mut model = new_empty_model(); + assert_eq!(model.workbook.worksheet(0).unwrap().color, None); + assert!(model.set_sheet_color(0, "#FFFAAA").is_ok()); + + // Test new tab color is properly set + assert_eq!( + model.workbook.worksheet(0).unwrap().color, + Some("#FFFAAA".to_string()) + ); + + // Test we can remove it + assert!(model.set_sheet_color(0, "").is_ok()); + assert_eq!(model.workbook.worksheet(0).unwrap().color, None); +} + +#[test] +fn test_set_sheet_color_invalid_sheet() { + let mut model = new_empty_model(); + assert_eq!( + model.set_sheet_color(10, "#FFFAAA"), + Err("Invalid sheet index".to_string()) + ); +} + +#[test] +fn test_set_sheet_color_invalid() { + let mut model = new_empty_model(); + // Boundaries + assert!(model.set_sheet_color(0, "#FFFFFF").is_ok()); + assert!(model.set_sheet_color(0, "#000000").is_ok()); + + assert_eq!( + model.set_sheet_color(0, "#FFF"), + Err("Invalid color: #FFF".to_string()) + ); + assert_eq!( + model.set_sheet_color(0, "-#FFF"), + Err("Invalid color: -#FFF".to_string()) + ); + assert_eq!( + model.set_sheet_color(0, "#-FFF"), + Err("Invalid color: #-FFF".to_string()) + ); + assert_eq!( + model.set_sheet_color(0, "2FFFFFF"), + Err("Invalid color: 2FFFFFF".to_string()) + ); + assert_eq!( + model.set_sheet_color(0, "#FFFFFF1"), + Err("Invalid color: #FFFFFF1".to_string()) + ); +} + +#[test] +fn set_input_autocomplete() { + let mut model = new_empty_model(); + model._set("A1", "1"); + model._set("A2", "2"); + model.set_user_input(0, 3, 1, "=SUM(A1:A2".to_string()); + // This will fail anyway + model.set_user_input(0, 4, 1, "=SUM(A1*".to_string()); + model.evaluate(); + + assert_eq!(model._get_formula("A3"), "=SUM(A1:A2)"); + assert_eq!(model._get_text("A3"), "3"); + + assert_eq!(model._get_formula("A4"), "=SUM(A1*"); + assert_eq!(model._get_text("A4"), "#ERROR!"); +} + +#[test] +fn test_get_cell_value_by_ref() { + let mut model = new_empty_model(); + model._set("A1", "1"); + model._set("A2", "2"); + model.evaluate(); + + // Correct + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(1.0)) + ); + + // You need to specify full reference + assert_eq!( + model.get_cell_value_by_ref("A1"), + Err("Error parsing reference: 'A1'".to_string()) + ); + + // Error, it has a trailing space + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1 "), + Err("Error parsing reference: 'Sheet1!A1 '".to_string()) + ); +} + +#[test] +fn test_get_formatted_cell_value() { + let mut model = new_empty_model(); + model._set("A1", "foobar"); + model._set("A2", "true"); + model._set("A3", ""); + model._set("A4", "123.456"); + model._set("A5", "123.456"); + + // change A5 format + let mut style = model.get_style_for_cell(0, 5, 1); + style.num_fmt = "$#,##0.00".to_string(); + model.set_cell_style(0, 5, 1, &style).unwrap(); + + model.evaluate(); + + assert_eq!(model.formatted_cell_value(0, 1, 1).unwrap(), "foobar"); + assert_eq!(model.formatted_cell_value(0, 2, 1).unwrap(), "TRUE"); + assert_eq!(model.formatted_cell_value(0, 3, 1).unwrap(), ""); + assert_eq!(model.formatted_cell_value(0, 4, 1).unwrap(), "123.456"); + assert_eq!(model.formatted_cell_value(0, 5, 1).unwrap(), "$123.46"); +} + +#[test] +fn test_cell_formula() { + let mut model = new_empty_model(); + model._set("A1", "=1+2+3"); + model._set("A2", "foobar"); + model.evaluate(); + + assert_eq!( + model.cell_formula(0, 1, 1), // A1 + Ok(Some("=1+2+3".to_string())), + ); + assert_eq!( + model.cell_formula(0, 2, 1), // A2 + Ok(None), + ); + assert_eq!( + model.cell_formula(0, 3, 1), // A3 - empty cell + Ok(None), + ); + + assert_eq!( + model.cell_formula(42, 1, 1), + Err("Invalid sheet index".to_string()), + ); +} + +#[test] +fn test_xlfn() { + let mut model = new_empty_model(); + model._set("A1", "=_xlfn.SIN(1)"); + model._set("A2", "=_xlfn.SINY(1)"); + model._set("A3", "=_xlfn.CONCAT(3, 4.0)"); + model.evaluate(); + // Only modern formulas strip the '_xlfn.' + assert_eq!( + model.cell_formula(0, 1, 1).unwrap(), + Some("=_xlfn.SIN(1)".to_string()) + ); + // unknown formulas keep the '_xlfn.' prefix + assert_eq!( + model.cell_formula(0, 2, 1).unwrap(), + Some("=_xlfn.SINY(1)".to_string()) + ); + assert_eq!( + model.cell_formula(0, 3, 1).unwrap(), + Some("=CONCAT(3,4)".to_string()) + ); +} + +#[test] +fn test_letter_case() { + let mut model = new_empty_model(); + model._set("A1", "=sin(1)"); + model._set("A2", "=sIn(2)"); + model.evaluate(); + assert_eq!( + model.cell_formula(0, 1, 1).unwrap(), + Some("=SIN(1)".to_string()) + ); + assert_eq!( + model.cell_formula(0, 2, 1).unwrap(), + Some("=SIN(2)".to_string()) + ); +} diff --git a/base/src/test/test_math.rs b/base/src/test/test_math.rs new file mode 100644 index 0000000..1c08449 --- /dev/null +++ b/base/src/test/test_math.rs @@ -0,0 +1,29 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_sqrt_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=SQRT(4)"); + model._set("A2", "=SQRT()"); + model._set("A3", "=SQRT(4, 4)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"2"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); +} + +#[test] +fn test_fn_sqrtpi_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=SQRTPI()"); + model._set("A2", "=SQRTPI(4, 4)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); +} diff --git a/base/src/test/test_metadata.rs b/base/src/test/test_metadata.rs new file mode 100644 index 0000000..5d5bbfd --- /dev/null +++ b/base/src/test/test_metadata.rs @@ -0,0 +1,13 @@ +use crate::test::util::new_empty_model; + +#[test] +fn test_metadata_new_model() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "5.5".to_string()); + model.evaluate(); + let metadata = &model.workbook.metadata; + assert_eq!(metadata.application, "IronCalc Sheets"); + // FIXME: This will need to be updated once we fix versioning + assert_eq!(metadata.app_version, "10.0000"); + assert_eq!(metadata.last_modified, "2022-11-08T11:13:28Z"); +} diff --git a/base/src/test/test_model_delete_cell.rs b/base/src/test/test_model_delete_cell.rs new file mode 100644 index 0000000..8ba8472 --- /dev/null +++ b/base/src/test/test_model_delete_cell.rs @@ -0,0 +1,54 @@ +#![allow(clippy::unwrap_used)] +use crate::test::util::new_empty_model; + +#[test] +fn test_delete_cell_non_existing_sheet() { + let mut model = new_empty_model(); + assert_eq!( + model.delete_cell(13, 1, 1), + Err("Invalid sheet index".to_string()) + ); +} + +#[test] +fn test_delete_cell_unset_cell() { + let mut model = new_empty_model(); + assert!(model.delete_cell(0, 1, 1).is_ok()); +} + +#[test] +fn test_delete_cell_with_value() { + let mut model = new_empty_model(); + model._set("A1", "hello"); + model.evaluate(); + + assert_eq!(model._get_text_at(0, 1, 1), "hello"); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); + + model.delete_cell(0, 1, 1).unwrap(); + model.evaluate(); + + assert_eq!(model._get_text_at(0, 1, 1), ""); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true)); +} + +#[test] +fn test_delete_cell_referenced_elsewhere() { + let mut model = new_empty_model(); + model._set("A1", "35"); + model._set("A2", "=2*A1"); + model.evaluate(); + + assert_eq!(model._get_text_at(0, 1, 1), "35"); + assert_eq!(model._get_text_at(0, 2, 1), "70"); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); + assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false)); + + model.delete_cell(0, 1, 1).unwrap(); + model.evaluate(); + + assert_eq!(model._get_text_at(0, 1, 1), ""); + assert_eq!(model._get_text_at(0, 2, 1), "0"); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true)); + assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false)); +} diff --git a/base/src/test/test_model_is_empty_cell.rs b/base/src/test/test_model_is_empty_cell.rs new file mode 100644 index 0000000..888823d --- /dev/null +++ b/base/src/test/test_model_is_empty_cell.rs @@ -0,0 +1,55 @@ +#![allow(clippy::unwrap_used)] +use crate::test::util::new_empty_model; + +#[test] +fn test_is_empty_cell_non_existing_sheet() { + let model = new_empty_model(); + assert_eq!( + model.is_empty_cell(13, 1, 1), + Err("Invalid sheet index".to_string()) + ); +} + +#[test] +fn test_is_empty_cell() { + let mut model = new_empty_model(); + assert!(model.is_empty_cell(0, 3, 1).unwrap()); + model.set_user_input(0, 3, 1, "Hello World".to_string()); + assert!(!model.is_empty_cell(0, 3, 1).unwrap()); + model.set_cell_empty(0, 3, 1).unwrap(); + assert!(model.is_empty_cell(0, 3, 1).unwrap()); +} + +#[test] +fn test_is_empty_cell_unset_cell() { + let model = new_empty_model(); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true)); +} + +#[test] +fn test_is_empty_cell_with_value() { + let mut model = new_empty_model(); + model._set("A1", "hello"); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); +} + +#[test] +fn test_is_empty_cell_empty_string_not_empty() { + let mut model = new_empty_model(); + model._set("A1", ""); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); +} + +#[test] +fn test_is_empty_cell_formula_that_evaluates_to_empty_string() { + let mut model = new_empty_model(); + model._set("A1", "=A2"); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); +} + +#[test] +fn test_is_empty_cell_formula_that_evaluates_to_zero() { + let mut model = new_empty_model(); + model._set("A1", "=2*A2"); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); +} diff --git a/base/src/test/test_model_set_cell_empty.rs b/base/src/test/test_model_set_cell_empty.rs new file mode 100644 index 0000000..decb0fb --- /dev/null +++ b/base/src/test/test_model_set_cell_empty.rs @@ -0,0 +1,57 @@ +#![allow(clippy::unwrap_used)] +use crate::test::util::new_empty_model; + +#[test] +fn test_set_cell_empty_non_existing_sheet() { + let mut model = new_empty_model(); + assert_eq!( + model.set_cell_empty(13, 1, 1), + Err("Invalid sheet index".to_string()) + ); +} + +#[test] +fn test_set_cell_empty_unset_cell() { + let mut model = new_empty_model(); + model.set_cell_empty(0, 1, 1).unwrap(); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true)); + model.evaluate(); + assert_eq!(model._get_text_at(0, 1, 1), ""); +} + +#[test] +fn test_set_cell_empty_with_value() { + let mut model = new_empty_model(); + model._set("A1", "hello"); + model.evaluate(); + + assert_eq!(model._get_text_at(0, 1, 1), "hello"); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); + + model.set_cell_empty(0, 1, 1).unwrap(); + model.evaluate(); + + assert_eq!(model._get_text_at(0, 1, 1), ""); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true)); +} + +#[test] +fn test_set_cell_empty_referenced_elsewhere() { + let mut model = new_empty_model(); + model._set("A1", "35"); + model._set("A2", "=2*A1"); + model.evaluate(); + + assert_eq!(model._get_text_at(0, 1, 1), "35"); + assert_eq!(model._get_text_at(0, 2, 1), "70"); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(false)); + assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false)); + + model.set_cell_empty(0, 1, 1).unwrap(); + model.evaluate(); + + assert_eq!(model._get_text_at(0, 1, 1), ""); + assert_eq!(model._get_text_at(0, 2, 1), "0"); + assert_eq!(model.is_empty_cell(0, 1, 1), Ok(true)); + assert_eq!(model.is_empty_cell(0, 2, 1), Ok(false)); +} diff --git a/base/src/test/test_move_formula.rs b/base/src/test/test_move_formula.rs new file mode 100644 index 0000000..fd44bb8 --- /dev/null +++ b/base/src/test/test_move_formula.rs @@ -0,0 +1,230 @@ +#![allow(clippy::unwrap_used)] + +use crate::expressions::types::{Area, CellReferenceIndex}; +use crate::test::util::new_empty_model; + +#[test] +fn test_move_formula() { + let mut model = new_empty_model(); + + let source = &CellReferenceIndex { + sheet: 0, + column: 1, + row: 1, + }; + let value = "=A2+3"; + let target = &CellReferenceIndex { + sheet: 0, + column: 10, + row: 10, + }; + + // if we move just one point formula does ot change + let area = &Area { + sheet: 0, + row: 1, + column: 1, + width: 1, + height: 1, + }; + let t = model.move_cell_value_to_area(value, source, target, area); + assert!(t.is_ok()); + assert_eq!(t.unwrap(), "=A2+3"); + + // if we move a 2x2 square formula does change + let area = &Area { + sheet: 0, + row: 1, + column: 1, + width: 2, + height: 2, + }; + let t = model.move_cell_value_to_area(value, source, target, area); + assert!(t.is_ok()); + assert_eq!(t.unwrap(), "=J11+3"); +} + +#[test] +fn test_move_formula_wrong_args() { + let mut model = new_empty_model(); + let t = model.add_sheet("Sheet2"); + assert!(t.is_ok()); + + let source = &CellReferenceIndex { + sheet: 0, + column: 5, + row: 5, + }; + let value = "=A2+3"; + let target = &CellReferenceIndex { + sheet: 0, + column: 10, + row: 10, + }; + + // different sheet + { + let area = &Area { + sheet: 1, + row: 5, + column: 5, + width: 1, + height: 1, + }; + let t = model.move_cell_value_to_area(value, source, target, area); + assert_eq!( + t, + Err("Source and area are in different sheets".to_string()) + ); + } + + // not in area + { + let area = &Area { + sheet: 0, + row: 6, + column: 4, + width: 5, + height: 5, + }; + let t = model.move_cell_value_to_area(value, source, target, area); + assert_eq!(t, Err("Source is outside the area".to_string())); + } + + { + let area = &Area { + sheet: 0, + row: 1, + column: 4, + width: 5, + height: 2, + }; + let t = model.move_cell_value_to_area(value, source, target, area); + assert_eq!(t, Err("Source is outside the area".to_string())); + } + + { + let area = &Area { + sheet: 0, + row: 1, + column: 6, + width: 20, + height: 5, + }; + let t = model.move_cell_value_to_area(value, source, target, area); + assert_eq!(t, Err("Source is outside the area".to_string())); + } + + // Invalid sheet indexes + assert_eq!( + model.move_cell_value_to_area( + value, + &CellReferenceIndex { + sheet: 0, + row: 1, + column: 4, + }, + &CellReferenceIndex { + sheet: 16, + row: 1, + column: 1, + }, + &Area { + sheet: 0, + row: 1, + column: 4, + width: 5, + height: 2, + } + ), + Err("Could not find target worksheet: Invalid sheet index".to_string()) + ); + assert_eq!( + model.move_cell_value_to_area( + value, + &CellReferenceIndex { + sheet: 3, + column: 1, + row: 1, + }, + target, + &Area { + sheet: 3, + row: 1, + column: 1, + width: 5, + height: 5, + }, + ), + Err("Could not find source worksheet: Invalid sheet index".to_string()) + ); +} + +#[test] +fn test_move_formula_rectangle() { + let mut model = new_empty_model(); + + let value = "=B2+C2"; + let target = &CellReferenceIndex { + sheet: 0, + column: 10, + row: 10, + }; + + // if we move just one point formula does not change + let area = &Area { + sheet: 0, + row: 1, + column: 1, + width: 2, + height: 20, + }; + assert!(model + .move_cell_value_to_area( + value, + &CellReferenceIndex { + sheet: 0, + column: 3, + row: 1, + }, + target, + area + ) + .is_err()); + assert!(model + .move_cell_value_to_area( + value, + &CellReferenceIndex { + sheet: 0, + column: 2, + row: 1, + }, + target, + area + ) + .is_ok()); + assert!(model + .move_cell_value_to_area( + value, + &CellReferenceIndex { + sheet: 0, + column: 1, + row: 20, + }, + target, + area + ) + .is_ok()); + assert!(model + .move_cell_value_to_area( + value, + &CellReferenceIndex { + sheet: 0, + column: 1, + row: 21, + }, + target, + area + ) + .is_err()); +} diff --git a/base/src/test/test_number_format.rs b/base/src/test/test_number_format.rs new file mode 100644 index 0000000..ae4cc04 --- /dev/null +++ b/base/src/test/test_number_format.rs @@ -0,0 +1,16 @@ +#![allow(clippy::unwrap_used)] + +use crate::number_format::format_number; + +#[test] +fn test_simple_format() { + let formatted = format_number(2.3, "General", "en"); + assert_eq!(formatted.text, "2.3".to_string()); +} + +#[test] +#[ignore = "not yet implemented"] +fn test_wrong_locale() { + let formatted = format_number(2.3, "General", "ens"); + assert_eq!(formatted.text, "#ERROR!".to_string()); +} diff --git a/base/src/test/test_percentage.rs b/base/src/test/test_percentage.rs new file mode 100644 index 0000000..ff96afb --- /dev/null +++ b/base/src/test/test_percentage.rs @@ -0,0 +1,19 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn simple_example() { + let mut model = new_empty_model(); + + model._set("A1", "220"); + model._set("B1", "=A1*10%"); + model._set("C1", "=SIN(A1)%"); + + model.evaluate(); + + assert_eq!(model._get_formula("B1"), *"=A1*10%"); + assert_eq!(model._get_text("B1"), *"22"); + assert_eq!(model._get_formula("C1"), *"=SIN(A1)%"); + assert_eq!(model._get_text("C1"), *"0.000883987"); +} diff --git a/base/src/test/test_quote_prefix.rs b/base/src/test/test_quote_prefix.rs new file mode 100644 index 0000000..455e673 --- /dev/null +++ b/base/src/test/test_quote_prefix.rs @@ -0,0 +1,146 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_quote_prefix_formula() { + let mut model = new_empty_model(); + model._set("A1", "'= 1 + 3"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"= 1 + 3"); + assert!(!model._has_formula("A1")); +} + +#[test] +fn test_quote_prefix_number() { + let mut model = new_empty_model(); + model._set("A1", "'13"); + model._set("A2", "=ISNUMBER(A1)"); + model._set("A3", "=A1+1"); + model._set("A4", "=ISNUMBER(A3)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"13"); + assert!(!model._has_formula("A1")); + + assert_eq!(model._get_text("A2"), *"FALSE"); + assert_eq!(model._get_text("A3"), *"14"); + + assert_eq!(model._get_text("A4"), *"TRUE"); +} + +#[test] +fn test_quote_prefix_error() { + let mut model = new_empty_model(); + model._set("A1", "'#N/A"); + model._set("A2", "=ISERROR(A1)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"#N/A"); + + assert_eq!(model._get_text("A2"), *"FALSE"); +} + +#[test] +fn test_quote_prefix_boolean() { + let mut model = new_empty_model(); + model._set("A1", "'FALSE"); + model._set("A2", "=ISTEXT(A1)"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"FALSE"); + + assert_eq!(model._get_text("A2"), *"TRUE"); +} + +#[test] +fn test_quote_prefix_enter() { + let mut model = new_empty_model(); + model._set("A1", "'123"); + model._set("A2", "=ISTEXT(A1)"); + model.evaluate(); + // We introduce a value with a "quote prefix" index + model.set_user_input(0, 1, 3, "'=A1".to_string()); + model.evaluate(); + assert_eq!(model._get_text("C1"), *"=A1"); + + // But if we enter with a quote_prefix but without the "'" it won't be quote_prefix + model.set_user_input(0, 1, 4, "=A1".to_string()); + model.evaluate(); + assert_eq!(model._get_text("D1"), *"123"); +} + +#[test] +fn test_quote_prefix_reenter() { + let mut model = new_empty_model(); + model._set("A1", "'123"); + model._set("A2", "=ISTEXT(A1)"); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"TRUE"); + // We introduce a value with a "quote prefix" index + model.set_user_input(0, 1, 1, "123".to_string()); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"FALSE"); +} + +#[test] +fn test_update_cell_quote() { + let mut model = new_empty_model(); + model.update_cell_with_text(0, 1, 1, "= 1 + 3"); + model.evaluate(); + assert_eq!(model._get_text("A1"), *"= 1 + 3"); + assert!(!model._has_formula("A1")); +} + +#[test] +fn test_update_quote_prefix_reenter() { + let mut model = new_empty_model(); + model.update_cell_with_text(0, 1, 1, "123"); + model._set("A2", "=ISTEXT(A1)"); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"TRUE"); + // We reenter as a number + model.update_cell_with_number(0, 1, 1, 123.0); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"FALSE"); +} + +#[test] +fn test_update_quote_prefix_reenter_bool() { + let mut model = new_empty_model(); + model.update_cell_with_text(0, 1, 1, "TRUE"); + model._set("A2", "=ISTEXT(A1)"); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"TRUE"); + // We enter a bool + model.update_cell_with_bool(0, 1, 1, true); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"FALSE"); +} + +#[test] +fn test_update_quote_prefix_reenter_text() { + let mut model = new_empty_model(); + model.update_cell_with_text(0, 1, 1, "123"); + model._set("A2", "=ISTEXT(A1)"); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"TRUE"); + assert!(model.get_style_for_cell(0, 1, 1).quote_prefix); + // We enter a string + model.update_cell_with_text(0, 1, 1, "Hello"); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"TRUE"); + assert!(!model.get_style_for_cell(0, 1, 1).quote_prefix); +} + +#[test] +fn test_update_quote_prefix_reenter_text_2() { + let mut model = new_empty_model(); + model.update_cell_with_text(0, 1, 1, "123"); + model._set("A2", "=ISTEXT(A1)"); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"TRUE"); + assert!(model.get_style_for_cell(0, 1, 1).quote_prefix); + // We enter another number + model.update_cell_with_text(0, 1, 1, "42"); + model.evaluate(); + assert_eq!(model._get_text("A2"), *"TRUE"); + assert!(model.get_style_for_cell(0, 1, 1).quote_prefix); +} diff --git a/base/src/test/test_set_user_input.rs b/base/src/test/test_set_user_input.rs new file mode 100644 index 0000000..c49780b --- /dev/null +++ b/base/src/test/test_set_user_input.rs @@ -0,0 +1,480 @@ +#![allow(clippy::unwrap_used)] + +use crate::{cell::CellValue, test::util::new_empty_model}; + +#[test] +fn test_currencies() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "$100.348".to_string()); + model.set_user_input(0, 1, 2, "=ISNUMBER(A1)".to_string()); + + model.set_user_input(0, 2, 1, "$ 100.348".to_string()); + model.set_user_input(0, 2, 2, "=ISNUMBER(A2)".to_string()); + + model.set_user_input(0, 3, 1, "100$".to_string()); + model.set_user_input(0, 3, 2, "=ISNUMBER(A3)".to_string()); + + model.set_user_input(0, 4, 1, "3.1415926$".to_string()); + + model.evaluate(); + + // two decimal rounded up + assert_eq!(model._get_text("A1"), "$100.35"); + assert_eq!(model._get_text("B1"), *"TRUE"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(100.348)) + ); + // No space + assert_eq!(model._get_text("A2"), "$100.35"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A2"), + Ok(CellValue::Number(100.348)) + ); + assert_eq!(model._get_text("B2"), *"TRUE"); + + // Dollar is on the right + assert_eq!(model._get_text("A3"), "100$"); + assert_eq!(model._get_text("B3"), *"TRUE"); + + assert_eq!(model._get_text("A4"), "3.14$"); +} + +#[test] +fn scientific() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "3e-4".to_string()); + model.set_user_input(0, 2, 1, "5e-4$".to_string()); + model.set_user_input(0, 3, 1, "6e-4%".to_string()); + + model.evaluate(); + + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(0.0003)) + ); + assert_eq!(model._get_text("Sheet1!A1"), "3.00E-04"); + assert_eq!(model._get_text("Sheet1!A2"), "5.00E-04"); + assert_eq!(model._get_text("Sheet1!A3"), "6.00E-06"); +} + +#[test] +fn test_percentage() { + let mut model = new_empty_model(); + model.set_user_input(0, 10, 1, "50%".to_string()); + model.set_user_input(0, 10, 2, "=ISNUMBER(A10)".to_string()); + model.set_user_input(0, 11, 1, "55.759%".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("B10"), *"TRUE"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A10"), + Ok(CellValue::Number(0.5)) + ); + // Two decimal places + assert_eq!(model._get_text("A11"), "55.76%"); +} + +#[test] +fn test_percentage_ops() { + let mut model = new_empty_model(); + model._set("A1", "5%"); + model._set("A2", "20%"); + model.set_user_input(0, 3, 1, "=A1+A2".to_string()); + model.set_user_input(0, 4, 1, "=A1*A2".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("A3"), *"25%"); + assert_eq!(model._get_text("A4"), *"1.00%"); +} + +#[test] +fn test_numbers() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "1,000,000".to_string()); + + model.set_user_input(0, 20, 1, "50,123.549".to_string()); + model.set_user_input(0, 21, 1, "50,12.549".to_string()); + model.set_user_input(0, 22, 1, "1,234567".to_string()); + + model.evaluate(); + + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(1000000.0)) + ); + + // Two decimal places + assert_eq!(model._get_text("A20"), "50,123.55"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A20"), + Ok(CellValue::Number(50123.549)) + ); + + // This is a string + assert_eq!(model._get_text("A21"), "50,12.549"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A21"), + Ok(CellValue::String("50,12.549".to_string())) + ); + + // Commas in all places + assert_eq!(model._get_text("A22"), "1,234,567"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A22"), + Ok(CellValue::Number(1234567.0)) + ); +} + +#[test] +fn test_negative_numbers() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "-100".to_string()); + + model.evaluate(); + + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(-100.0)) + ); +} + +#[test] +fn test_negative_currencies() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "-$100".to_string()); + model.set_user_input(0, 2, 1, "-$99.123".to_string()); + // This is valid! + model.set_user_input(0, 3, 1, "$-345".to_string()); + + model.set_user_input(0, 1, 2, "-200$".to_string()); + model.set_user_input(0, 2, 2, "-92.689$".to_string()); + // This is valid! + model.set_user_input(0, 3, 2, "-22$".to_string()); + + model.evaluate(); + + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(-100.0)) + ); + assert_eq!(model._get_text("A1"), *"-$100"); + assert_eq!(model._get_text("A2"), *"-$99.12"); + assert_eq!(model._get_text("A3"), *"-$345"); + + assert_eq!(model._get_text("B1"), *"-200$"); + assert_eq!(model._get_text("B2"), *"-92.69$"); + assert_eq!(model._get_text("B3"), *"-22$"); +} + +#[test] +fn test_formulas() { + let mut model = new_empty_model(); + model._set("A1", "$100"); + model._set("A2", "$200"); + model.set_user_input(0, 3, 1, "=A1+A2".to_string()); + model.set_user_input(0, 4, 1, "=SUM(A1:A3)".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("A3"), *"$300"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A3"), + Ok(CellValue::Number(300.0)) + ); + assert_eq!(model._get_text("A4"), *"$600"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A4"), + Ok(CellValue::Number(600.0)) + ); +} + +#[test] +fn test_product() { + let mut model = new_empty_model(); + model._set("A1", "$100"); + model._set("A2", "$5"); + model._set("A3", "4"); + + model.set_user_input(0, 1, 2, "=A1*A2".to_string()); + model.set_user_input(0, 2, 2, "=A1*A3".to_string()); + model.set_user_input(0, 3, 2, "=A1*3".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"500"); + assert_eq!(model._get_text("B2"), *"$400"); + assert_eq!(model._get_text("B3"), *"$300"); +} + +#[test] +fn test_division() { + let mut model = new_empty_model(); + model._set("A1", "$100"); + model._set("A2", "$5"); + model._set("A3", "4"); + + model.set_user_input(0, 1, 2, "=A1/A2".to_string()); + model.set_user_input(0, 2, 2, "=A1/A3".to_string()); + model.set_user_input(0, 3, 2, "=A1/2".to_string()); + model.set_user_input(0, 4, 2, "=100/A2".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"20"); + assert_eq!(model._get_text("B2"), *"$25"); + assert_eq!(model._get_text("B3"), *"$50"); + assert_eq!(model._get_text("B4"), *"20"); +} + +#[test] +fn test_some_complex_examples() { + let mut model = new_empty_model(); + // $3.00 / 2 = $1.50 + model._set("A1", "$3.00"); + model._set("A2", "2"); + model.set_user_input(0, 3, 1, "=A1/A2".to_string()); + + // $3 / 2 = $1 + model._set("B1", "$3"); + model._set("B2", "2"); + model.set_user_input(0, 3, 2, "=B1/B2".to_string()); + + // $5.00 * 25% = 25% * $5.00 = $1.25 + model._set("C1", "$5.00"); + model._set("C2", "25%"); + model.set_user_input(0, 3, 3, "=C1*C2".to_string()); + model.set_user_input(0, 4, 3, "=C2*C1".to_string()); + + // $5 * 75% = 75% * $5 = $1 + model._set("D1", "$5"); + model._set("D2", "75%"); + model.set_user_input(0, 3, 4, "=D1*D2".to_string()); + model.set_user_input(0, 4, 4, "=D2*D1".to_string()); + + // $10 + $9.99 = $9.99 + $10 = $19.99 + model._set("E1", "$10"); + model._set("E2", "$9.99"); + model.set_user_input(0, 3, 5, "=E1+E2".to_string()); + model.set_user_input(0, 4, 5, "=E2+E1".to_string()); + + // $2 * 2 = 2 * $2 = $4 + model._set("F1", "$2"); + model._set("F2", "2"); + model.set_user_input(0, 3, 6, "=F1*F2".to_string()); + model.set_user_input(0, 4, 6, "=F2*F1".to_string()); + + // $2.50 * 2 = 2 * $2.50 = $5.00 + model._set("G1", "$2.50"); + model._set("G2", "2"); + model.set_user_input(0, 3, 7, "=G1*G2".to_string()); + model.set_user_input(0, 4, 7, "=G2*G1".to_string()); + + // $2 * 2.5 = 2.5 * $2 = $5 + model._set("H1", "$2"); + model._set("H2", "2.5"); + model.set_user_input(0, 3, 8, "=H1*H2".to_string()); + model.set_user_input(0, 4, 8, "=H2*H1".to_string()); + + // 10% * 1,000 = 1,000 * 10% = 100 + model._set("I1", "10%"); + model._set("I2", "1,000"); + model.set_user_input(0, 3, 9, "=I1*I2".to_string()); + model.set_user_input(0, 4, 9, "=I2*I1".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("A3"), *"$1.50"); + + assert_eq!(model._get_text("B3"), *"$2"); + + assert_eq!(model._get_text("C3"), *"$1.25"); + assert_eq!(model._get_text("C4"), *"$1.25"); + + assert_eq!(model._get_text("D3"), *"$3.75"); + assert_eq!(model._get_text("D4"), *"$3.75"); + + assert_eq!(model._get_text("E3"), *"$19.99"); + assert_eq!(model._get_text("E4"), *"$19.99"); + + assert_eq!(model._get_text("F3"), *"$4"); + assert_eq!(model._get_text("F4"), *"$4"); + + assert_eq!(model._get_text("G3"), *"$5.00"); + assert_eq!(model._get_text("G4"), *"$5.00"); + + assert_eq!(model._get_text("H3"), *"$5"); + assert_eq!(model._get_text("H4"), *"$5"); + + assert_eq!(model._get_text("I3"), *"100"); + assert_eq!(model._get_text("I4"), *"100"); +} + +#[test] +fn test_financial_functions() { + // Some functions imply a currency formatting even on error + let mut model = new_empty_model(); + model._set("A2", "8%"); + model._set("A3", "10"); + model._set("A4", "$10,000"); + + model.set_user_input(0, 5, 1, "=PMT(A2/12,A3,A4)".to_string()); + model.set_user_input(0, 6, 1, "=PMT(A2/12,A3,A4,,1)".to_string()); + model.set_user_input(0, 7, 1, "=PMT(0.2, 3, -200)".to_string()); + + model.evaluate(); + + // This two are negative numbers + assert_eq!(model._get_text("A5"), *"-$1,037.03"); + assert_eq!(model._get_text("A6"), *"-$1,030.16"); + // This is a positive number + assert_eq!(model._get_text("A7"), *"$94.95"); +} + +#[test] +fn test_sum_function() { + let mut model = new_empty_model(); + model._set("A1", "$100"); + model._set("A2", "$300"); + + model.set_user_input(0, 1, 2, "=SUM(A:A)".to_string()); + model.set_user_input(0, 2, 2, "=SUM(A1:A2)".to_string()); + model.set_user_input(0, 3, 2, "=SUM(A1, A2, A3)".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"$400"); + assert_eq!(model._get_text("B2"), *"$400"); + assert_eq!(model._get_text("B3"), *"$400"); +} + +#[test] +fn test_number() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "3".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"3"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(3.0)) + ); +} + +#[test] +fn test_currencies_eur_prefix() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "€100.348".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), "€100.35"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(100.348)) + ); +} + +#[test] +fn test_currencies_eur_suffix() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "100.348€".to_string()); + model.set_user_input(0, 2, 1, "25€".to_string()); + + // negatives + model.set_user_input(0, 1, 2, "-123.348€".to_string()); + model.set_user_input(0, 2, 2, "-42€".to_string()); + + // with a space + model.set_user_input(0, 1, 3, "101.348 €".to_string()); + model.set_user_input(0, 2, 3, "26 €".to_string()); + + model.set_user_input(0, 1, 4, "-12.348 €".to_string()); + model.set_user_input(0, 2, 4, "-45 €".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), "100.35€"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(100.348)) + ); + assert_eq!(model._get_text("A2"), "25€"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A2"), + Ok(CellValue::Number(25.0)) + ); + + assert_eq!(model._get_text("B1"), "-123.35€"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!B1"), + Ok(CellValue::Number(-123.348)) + ); + assert_eq!(model._get_text("B2"), "-42€"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!B2"), + Ok(CellValue::Number(-42.0)) + ); + + // with a space + assert_eq!(model._get_text("C1"), "101.35€"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!C1"), + Ok(CellValue::Number(101.348)) + ); + assert_eq!(model._get_text("C2"), "26€"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!C2"), + Ok(CellValue::Number(26.0)) + ); + + assert_eq!(model._get_text("D1"), "-12.35€"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!D1"), + Ok(CellValue::Number(-12.348)) + ); + assert_eq!(model._get_text("D2"), "-45€"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!D2"), + Ok(CellValue::Number(-45.0)) + ); +} + +#[test] +fn test_sum_function_eur() { + let mut model = new_empty_model(); + model._set("A1", "€100"); + model._set("A2", "€300"); + + model.set_user_input(0, 1, 2, "=SUM(A:A)".to_string()); + model.set_user_input(0, 2, 2, "=SUM(A1:A2)".to_string()); + model.set_user_input(0, 3, 2, "=SUM(A1, A2, A3)".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("B1"), *"€400"); + assert_eq!(model._get_text("B2"), *"€400"); + assert_eq!(model._get_text("B3"), *"€400"); +} + +#[test] +fn input_dates() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "3/4/2025".to_string()); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), "3/4/2025"); + assert_eq!( + model.get_cell_value_by_ref("Sheet1!A1"), + Ok(CellValue::Number(45750.0)) + ); + + // further date assignments do not change the format + model.set_user_input(0, 1, 1, "08-08-2028".to_string()); + model.evaluate(); + assert_eq!(model._get_text("A1"), "8/8/2028"); +} diff --git a/base/src/test/test_sheet_markup.rs b/base/src/test/test_sheet_markup.rs new file mode 100644 index 0000000..460244b --- /dev/null +++ b/base/src/test/test_sheet_markup.rs @@ -0,0 +1,27 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_sheet_markup() { + let mut model = new_empty_model(); + model._set("A1", "Item"); + model._set("B1", "Cost"); + model._set("A2", "Rent"); + model._set("B2", "$600"); + model._set("A3", "Electricity"); + model._set("B3", "$200"); + model._set("A4", "Total"); + model._set("B4", "=SUM(B2:B3)"); + + let mut style = model.get_style_for_cell(0, 1, 1); + style.font.b = true; + model.set_cell_style(0, 1, 1, &style).unwrap(); + model.set_cell_style(0, 1, 2, &style).unwrap(); + model.set_cell_style(0, 4, 1, &style).unwrap(); + + assert_eq!( + model.sheet_markup(0), + Ok("**Item**|**Cost**\nRent|$600\nElectricity|$200\n**Total**|=SUM(B2:B3)".to_string()), + ) +} diff --git a/base/src/test/test_sheets.rs b/base/src/test/test_sheets.rs new file mode 100644 index 0000000..6d73171 --- /dev/null +++ b/base/src/test/test_sheets.rs @@ -0,0 +1,238 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_add_remove_sheets() { + let mut model = new_empty_model(); + model._set("A1", "7"); + model._set("A2", "=Sheet2!C3"); + model.evaluate(); + assert_eq!(model.workbook.get_worksheet_names(), ["Sheet1"]); + assert_eq!(model._get_text("A2"), "#REF!"); + + // Add a sheet + model.new_sheet(); + assert_eq!(model.workbook.get_worksheet_names(), ["Sheet1", "Sheet2"]); + assert_eq!(model._get_text("A2"), "0"); + model._set("Sheet2!A1", "=Sheet1!A1"); + model.evaluate(); + assert_eq!(model._get_text("Sheet2!A1"), "7"); + + // Rename the first sheet + let r = model.rename_sheet("Sheet1", "Ricci"); + assert!(r.is_ok()); + + assert_eq!(model.workbook.get_worksheet_names(), ["Ricci", "Sheet2"]); + assert_eq!(model._get_text("Sheet2!A1"), "7"); + assert_eq!(model._get_formula("Sheet2!A1"), "=Ricci!A1"); + + // Remove the first sheet + let r = model.delete_sheet_by_name("Ricci"); + assert!(r.is_ok()); + assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2"]); + assert_eq!(model._get_text("Sheet2!A1"), "#REF!"); +} + +#[test] +fn test_rename_delete_to_existing() { + let mut model = new_empty_model(); + model.new_sheet(); + // Cannot rename to an existing one + let r = model.rename_sheet("Sheet1", "Sheet2"); + assert!(r.is_err()); + + // Not every name is valid + let r = model.rename_sheet("Sheet1", "Invalid[]"); + assert!(r.is_err()); + + // Cannot delete something that does not exist + let r = model.delete_sheet_by_name("NonExists"); + assert!(r.is_err()); +} + +#[test] +fn test_rename_one_sheet() { + let mut model = new_empty_model(); + let r = model.rename_sheet("Sheet1", "Sheet2"); + assert!(r.is_ok()); + model.new_sheet(); + assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2", "Sheet1"]); +} + +#[test] +fn test_rename_and_formula() { + let mut model = new_empty_model(); + model._set("A1", "=A2*3"); + model._set("A2", "42"); + model.evaluate(); + let r = model.rename_sheet("Sheet1", "Sheet2"); + assert!(r.is_ok()); + model.new_sheet(); + assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2", "Sheet1"]); + model._set("Sheet2!A3", "= A1 * 3"); + model.evaluate(); + assert_eq!(model._get_formula("Sheet2!A3"), "=A1*3"); +} + +#[test] +fn test_correct_quoting() { + let mut model = new_empty_model(); + model.new_sheet(); + model._set("Sheet2!B3", "400"); + model._set("A1", "=Sheet2!B3*2"); + model.evaluate(); + assert_eq!(model._get_text("A1"), "800"); + let r = model.rename_sheet("Sheet2", "New Sheet"); + assert!(r.is_ok()); + assert_eq!(model._get_text("A1"), "800"); + assert_eq!(model._get_formula("A1"), "='New Sheet'!B3*2") +} + +#[test] +fn test_cannot_delete_last_sheet() { + let mut model = new_empty_model(); + let r = model.delete_sheet_by_name("Sheet1"); + assert_eq!(r, Err("Cannot delete only sheet".to_string())); + model.new_sheet(); + + let r = model.delete_sheet_by_name("Sheet10"); + assert_eq!(r, Err("Sheet not found".to_string())); + + let r = model.delete_sheet_by_name("Sheet1"); + assert!(r.is_ok()); +} + +#[test] +fn test_ranges() { + let mut model = new_empty_model(); + model._set("A1", "=SUM(Sheet2!A1:C3)*Sheet3!A2"); + model.evaluate(); + assert_eq!(model._get_text("A1"), "#REF!"); + model.new_sheet(); + assert_eq!(model._get_text("A1"), "#REF!"); + model.new_sheet(); + assert_eq!(model._get_text("A1"), "0"); + + model._set("Sheet3!A2", "42"); + model._set("Sheet2!A1", "2"); + model.evaluate(); + assert_eq!(model._get_text("A1"), "84"); + let r = model.rename_sheet("Sheet2", "Other Sheet"); + assert!(r.is_ok()); + assert_eq!( + model._get_formula("A1"), + "=SUM('Other Sheet'!A1:C3)*Sheet3!A2" + ); +} + +#[test] +fn test_insert_sheet() { + // Set a formula with a wrong sheet + let mut model = new_empty_model(); + model._set("A1", "=Bacchus!A3"); + model._set("A2", "=Dionysus!A3"); + model.evaluate(); + assert_eq!(model._get_text("A1"), "#REF!"); + assert_eq!(model._get_text("A2"), "#REF!"); + + // Insert the sheet at the end and check the formula + assert!(model.insert_sheet("Bacchus", 1, None).is_ok()); + model.set_user_input(1, 3, 1, "42".to_string()); + model.evaluate(); + assert_eq!(model._get_text("A1"), "42"); + assert_eq!(model._get_text("A2"), "#REF!"); + + // Insert a sheet in between the other two + assert!(model.insert_sheet("Dionysus", 1, None).is_ok()); + model.set_user_input(1, 3, 1, "111".to_string()); + model.evaluate(); + assert_eq!(model._get_text("A1"), "42"); + assert_eq!(model._get_text("A2"), "111"); + assert_eq!( + model.workbook.get_worksheet_names(), + ["Sheet1", "Dionysus", "Bacchus"] + ); + + // Insert a sheet out of bounds + assert!(model.insert_sheet("OutOfBounds", 4, None).is_err()); + model.evaluate(); + assert_eq!( + model.workbook.get_worksheet_names(), + ["Sheet1", "Dionysus", "Bacchus"] + ); + + // Insert at the beginning + assert!(model.insert_sheet("FirstSheet", 0, None).is_ok()); + model.evaluate(); + assert_eq!( + model.workbook.get_worksheet_names(), + ["FirstSheet", "Sheet1", "Dionysus", "Bacchus"] + ); +} + +#[test] +fn test_rename_sheet() { + let mut model = new_empty_model(); + model.new_sheet(); + model._set("A1", "=NewSheet!A3"); + model.set_user_input(1, 3, 1, "25".to_string()); + model.evaluate(); + assert_eq!(model._get_text("A1"), "#REF!"); + assert!(model.rename_sheet("Sheet2", "NewSheet").is_ok()); + model.evaluate(); + assert_eq!(model._get_text("A1"), "25"); +} + +#[test] +fn test_rename_sheet_by_index() { + let mut model = new_empty_model(); + model.new_sheet(); + model._set("A1", "=NewSheet!A1"); + model.set_user_input(1, 1, 1, "25".to_string()); + model.evaluate(); + assert_eq!(model._get_text("A1"), "#REF!"); + assert!(model.rename_sheet_by_index(1, "NewSheet").is_ok()); + model.evaluate(); + assert_eq!(model._get_text("A1"), "25"); +} + +#[test] +fn test_rename_sheet_by_index_error() { + let mut model = new_empty_model(); + model.new_sheet(); + assert!(model.rename_sheet_by_index(0, "OldSheet").is_ok()); + assert!(model.rename_sheet_by_index(2, "NewSheet").is_err()); +} + +#[test] +fn test_delete_sheet_by_index() { + let mut model = new_empty_model(); + model._set("A1", "7"); + model._set("A2", "=Sheet2!C3"); + model.evaluate(); + assert_eq!(model.workbook.get_worksheet_names(), ["Sheet1"]); + assert_eq!(model._get_text("A2"), "#REF!"); + + // Add a sheet + model.new_sheet(); + assert_eq!(model.workbook.get_worksheet_names(), ["Sheet1", "Sheet2"]); + assert_eq!(model._get_text("A2"), "0"); + model._set("Sheet2!A1", "=Sheet1!A1"); + model.evaluate(); + assert_eq!(model._get_text("Sheet2!A1"), "7"); + + // Rename the first sheet + let r = model.rename_sheet("Sheet1", "Ricci"); + assert!(r.is_ok()); + + assert_eq!(model.workbook.get_worksheet_names(), ["Ricci", "Sheet2"]); + assert_eq!(model._get_text("Sheet2!A1"), "7"); + assert_eq!(model._get_formula("Sheet2!A1"), "=Ricci!A1"); + + // Remove the first sheet + let r = model.delete_sheet_by_name("Ricci"); + assert!(r.is_ok()); + assert_eq!(model.workbook.get_worksheet_names(), ["Sheet2"]); + assert_eq!(model._get_text("Sheet2!A1"), "#REF!"); +} diff --git a/base/src/test/test_styles.rs b/base/src/test/test_styles.rs new file mode 100644 index 0000000..c6f41f9 --- /dev/null +++ b/base/src/test/test_styles.rs @@ -0,0 +1,64 @@ +#![allow(clippy::unwrap_used)] + +use crate::model::Style; +use crate::test::util::new_empty_model; + +#[test] +fn test_model_set_cells_with_values_styles() { + let mut model = new_empty_model(); + // Inputs + model.set_user_input(0, 1, 1, "21".to_string()); // A1 + model.set_user_input(0, 2, 1, "42".to_string()); // A2 + + let style_base = model.get_style_for_cell(0, 1, 1); + let mut style = style_base.clone(); + style.font.b = true; + style.num_fmt = "#,##0.00".to_string(); + assert!(model.set_cell_style(0, 1, 1, &style).is_ok()); + + let mut style = style_base; + style.num_fmt = "#,##0.00".to_string(); + assert!(model.set_cell_style(0, 2, 1, &style).is_ok()); + let style: Style = model.get_style_for_cell(0, 2, 1); + assert_eq!(style.num_fmt, "#,##0.00".to_string()); +} + +#[test] +fn test_named_styles() { + let mut model = new_empty_model(); + model._set("A1", "42"); + let mut style = model.get_style_for_cell(0, 1, 1); + style.font.b = true; + assert!(model.set_cell_style(0, 1, 1, &style).is_ok()); + let bold_style_index = model.get_cell_style_index(0, 1, 1); + let e = model + .workbook + .styles + .add_named_cell_style("bold", bold_style_index); + assert!(e.is_ok()); + model._set("A2", "420"); + let a2_style_index = model.get_cell_style_index(0, 2, 1); + assert!(a2_style_index != bold_style_index); + let e = model.set_cell_style_by_name(0, 2, 1, "bold"); + assert!(e.is_ok()); + assert_eq!(model.get_cell_style_index(0, 2, 1), bold_style_index); +} + +#[test] +fn test_create_named_style() { + let mut model = new_empty_model(); + model._set("A1", "42"); + + let mut style = model.get_style_for_cell(0, 1, 1); + assert!(!style.font.b); + + style.font.b = true; + let e = model.workbook.styles.create_named_style("bold", &style); + assert!(e.is_ok()); + + let e = model.set_cell_style_by_name(0, 1, 1, "bold"); + assert!(e.is_ok()); + + let style = model.get_style_for_cell(0, 1, 1); + assert!(style.font.b); +} diff --git a/base/src/test/test_today.rs b/base/src/test/test_today.rs new file mode 100644 index 0000000..a3b541b --- /dev/null +++ b/base/src/test/test_today.rs @@ -0,0 +1,50 @@ +#![allow(clippy::unwrap_used)] + +use crate::mock_time; +use crate::model::Model; +use crate::test::util::new_empty_model; + +// 14:44 20 Mar 2023 Berlin +const TIMESTAMP_2023: i64 = 1679319865208; + +#[test] +fn today_basic() { + let mut model = new_empty_model(); + model._set("A1", "=TODAY()"); + model._set("A2", "=TEXT(A1, \"yyyy/m/d\")"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"08/11/2022"); + assert_eq!(model._get_text("A2"), *"2022/11/8"); +} + +#[test] +fn today_with_wrong_tz() { + let model = Model::new_empty("model", "en", "Wrong Timezone"); + assert!(model.is_err()); +} + +#[test] +fn now_basic_utc() { + mock_time::set_mock_time(TIMESTAMP_2023); + let mut model = Model::new_empty("model", "en", "UTC").unwrap(); + model._set("A1", "=TODAY()"); + model._set("A2", "=NOW()"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"20/03/2023"); + assert_eq!(model._get_text("A2"), *"45005.572511574"); +} + +#[test] +fn now_basic_europe_berlin() { + mock_time::set_mock_time(TIMESTAMP_2023); + let mut model = Model::new_empty("model", "en", "Europe/Berlin").unwrap(); + model._set("A1", "=TODAY()"); + model._set("A2", "=NOW()"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"20/03/2023"); + // This is UTC + 1 hour: 45005.572511574 + 1/24 + assert_eq!(model._get_text("A2"), *"45005.614178241"); +} diff --git a/base/src/test/test_trigonometric.rs b/base/src/test/test_trigonometric.rs new file mode 100644 index 0000000..416776e --- /dev/null +++ b/base/src/test/test_trigonometric.rs @@ -0,0 +1,98 @@ +#![allow(clippy::unwrap_used)] + +use crate::test::util::new_empty_model; + +#[test] +fn test_fn_pi_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=PI(1)"); + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); +} + +#[test] +fn test_fn_atan2_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=ATAN2(1)"); + model._set("A2", "=ATAN2(1,1)"); + model._set("A3", "=ATAN2(1,1,1)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"0.785398163"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); +} + +#[test] +fn test_fn_trigonometric_arguments() { + let mut model = new_empty_model(); + model._set("A1", "=SIN()"); + model._set("A2", "=COS()"); + model._set("A3", "=TAN()"); + + model._set("A5", "=ASIN()"); + model._set("A6", "=ACOS()"); + model._set("A7", "=ATAN()"); + + model._set("A9", "=SINH()"); + model._set("A10", "=COSH()"); + model._set("A11", "=TANH()"); + + model._set("A13", "=ASINH()"); + model._set("A14", "=ACOSH()"); + model._set("A15", "=ATANH()"); + + model._set("B1", "=SIN(1,2)"); + model._set("B2", "=COS(1,2)"); + model._set("B3", "=TAN(1,2)"); + + model._set("B5", "=ASIN(1,2)"); + model._set("B6", "=ACOS(1,2)"); + model._set("B7", "=ATAN(1,2)"); + + model._set("B9", "=SINH(1,2)"); + model._set("B10", "=COSH(1,2)"); + model._set("B11", "=TANH(1,2)"); + + model._set("B13", "=ASINH(1,2)"); + model._set("B14", "=ACOSH(1,2)"); + model._set("B15", "=ATANH(1,2)"); + + model.evaluate(); + + assert_eq!(model._get_text("A1"), *"#ERROR!"); + assert_eq!(model._get_text("A2"), *"#ERROR!"); + assert_eq!(model._get_text("A3"), *"#ERROR!"); + + assert_eq!(model._get_text("A5"), *"#ERROR!"); + assert_eq!(model._get_text("A6"), *"#ERROR!"); + assert_eq!(model._get_text("A7"), *"#ERROR!"); + + assert_eq!(model._get_text("A9"), *"#ERROR!"); + assert_eq!(model._get_text("A10"), *"#ERROR!"); + assert_eq!(model._get_text("A11"), *"#ERROR!"); + + assert_eq!(model._get_text("B1"), *"#ERROR!"); + assert_eq!(model._get_text("B2"), *"#ERROR!"); + assert_eq!(model._get_text("B3"), *"#ERROR!"); + + assert_eq!(model._get_text("B5"), *"#ERROR!"); + assert_eq!(model._get_text("B6"), *"#ERROR!"); + assert_eq!(model._get_text("B7"), *"#ERROR!"); + + assert_eq!(model._get_text("B9"), *"#ERROR!"); + assert_eq!(model._get_text("B10"), *"#ERROR!"); + assert_eq!(model._get_text("B11"), *"#ERROR!"); +} + +#[test] +fn test_fn_tan_pi2() { + let mut model = new_empty_model(); + model._set("A1", "=TAN(PI()/2)"); + model.evaluate(); + + // This is consistent with IEEE 754 but inconsistent with Excel + assert_eq!(model._get_text("A1"), *"1.63312E+16"); +} diff --git a/base/src/test/test_worksheet.rs b/base/src/test/test_worksheet.rs new file mode 100644 index 0000000..bb3670d --- /dev/null +++ b/base/src/test/test_worksheet.rs @@ -0,0 +1,275 @@ +#![allow(clippy::unwrap_used)] + +use crate::{ + constants::{LAST_COLUMN, LAST_ROW}, + test::util::new_empty_model, + worksheet::{NavigationDirection, WorksheetDimension}, +}; + +#[test] +fn test_worksheet_dimension_empty_sheet() { + let model = new_empty_model(); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 1, + min_column: 1, + max_row: 1, + max_column: 1 + } + ); +} + +#[test] +fn test_worksheet_dimension_single_cell() { + let mut model = new_empty_model(); + model._set("W11", "1"); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 11, + min_column: 23, + max_row: 11, + max_column: 23 + } + ); +} + +#[test] +fn test_worksheet_dimension_single_cell_set_empty() { + let mut model = new_empty_model(); + model._set("W11", "1"); + model.set_cell_empty(0, 11, 23).unwrap(); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 11, + min_column: 23, + max_row: 11, + max_column: 23 + } + ); +} + +#[test] +fn test_worksheet_dimension_single_cell_deleted() { + let mut model = new_empty_model(); + model._set("W11", "1"); + model.delete_cell(0, 11, 23).unwrap(); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 1, + min_column: 1, + max_row: 1, + max_column: 1 + } + ); +} + +#[test] +fn test_worksheet_dimension_multiple_cells() { + let mut model = new_empty_model(); + model._set("W11", "1"); + model._set("E11", "1"); + model._set("AA17", "1"); + model._set("G17", "1"); + model._set("B19", "1"); + model.delete_cell(0, 11, 23).unwrap(); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 11, + min_column: 2, + max_row: 19, + max_column: 27 + } + ); +} + +#[test] +fn test_worksheet_dimension_progressive() { + let mut model = new_empty_model(); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 1, + min_column: 1, + max_row: 1, + max_column: 1 + } + ); + + model.set_user_input(0, 30, 50, "Hello World".to_string()); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 30, + min_column: 50, + max_row: 30, + max_column: 50 + } + ); + + model.set_user_input(0, 10, 15, "Hello World".to_string()); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 10, + min_column: 15, + max_row: 30, + max_column: 50 + } + ); + + model.set_user_input(0, 5, 25, "Hello World".to_string()); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 5, + min_column: 15, + max_row: 30, + max_column: 50 + } + ); + + model.set_user_input(0, 10, 250, "Hello World".to_string()); + assert_eq!( + model.workbook.worksheet(0).unwrap().dimension(), + WorksheetDimension { + min_row: 5, + min_column: 15, + max_row: 30, + max_column: 250 + } + ); +} + +#[test] +fn test_worksheet_navigate_to_edge_in_direction() { + let inline_spreadsheet = [ + [0, 0, 0, 0, 0, 0, 0, 0], // row 1 + [0, 1, 0, 1, 1, 1, 0, 1], // row 2 + [0, 1, 0, 1, 1, 0, 0, 0], // row 3 + [0, 1, 0, 1, 1, 0, 0, 0], // row 4 + [0, 0, 0, 1, 0, 0, 0, 0], // row 5 + [0, 1, 1, 0, 1, 0, 0, 0], // row 6 + [0, 0, 0, 0, 0, 0, 0, 0], // row 7 + ]; + // 1, 2, 3, 4, 5, 6, 7, 8 - columns + + let mut model = new_empty_model(); + for (row_index, row) in inline_spreadsheet.into_iter().enumerate() { + for (column_index, value) in row.into_iter().enumerate() { + if value != 0 { + model.update_cell_with_number( + 0, + (row_index as i32) + 1, + (column_index as i32) + 1, + value.into(), + ); + } + } + } + + let worksheet = model.workbook.worksheet(0).unwrap(); + + // Simple alias for readability of tests + let navigate = |row, column, direction| { + worksheet + .navigate_to_edge_in_direction(row, column, direction) + .unwrap() + }; + + assert_eq!(navigate(1, 1, NavigationDirection::Up), (1, 1)); + assert_eq!(navigate(1, 1, NavigationDirection::Left), (1, 1)); + assert_eq!(navigate(1, 1, NavigationDirection::Down), (LAST_ROW, 1)); + assert_eq!(navigate(1, 1, NavigationDirection::Right), (1, LAST_COLUMN)); + + assert_eq!(navigate(LAST_ROW, 1, NavigationDirection::Up), (1, 1)); + assert_eq!( + navigate(LAST_ROW, 1, NavigationDirection::Left), + (LAST_ROW, 1) + ); + assert_eq!( + navigate(LAST_ROW, 1, NavigationDirection::Down), + (LAST_ROW, 1) + ); + assert_eq!( + navigate(LAST_ROW, 1, NavigationDirection::Right), + (LAST_ROW, LAST_COLUMN) + ); + + assert_eq!( + navigate(1, LAST_COLUMN, NavigationDirection::Up), + (1, LAST_COLUMN) + ); + assert_eq!(navigate(1, LAST_COLUMN, NavigationDirection::Left), (1, 1)); + assert_eq!( + navigate(1, LAST_COLUMN, NavigationDirection::Down), + (LAST_ROW, LAST_COLUMN) + ); + assert_eq!( + navigate(1, LAST_COLUMN, NavigationDirection::Right), + (1, LAST_COLUMN) + ); + + assert_eq!( + navigate(LAST_ROW, LAST_COLUMN, NavigationDirection::Up), + (1, LAST_COLUMN) + ); + assert_eq!( + navigate(LAST_ROW, LAST_COLUMN, NavigationDirection::Left), + (LAST_ROW, 1) + ); + assert_eq!( + navigate(LAST_ROW, LAST_COLUMN, NavigationDirection::Down), + (LAST_ROW, LAST_COLUMN) + ); + assert_eq!( + navigate(LAST_ROW, LAST_COLUMN, NavigationDirection::Right), + (LAST_ROW, LAST_COLUMN) + ); + + // Direction = right + assert_eq!(navigate(2, 1, NavigationDirection::Right), (2, 2)); + assert_eq!(navigate(2, 2, NavigationDirection::Right), (2, 4)); + assert_eq!(navigate(2, 4, NavigationDirection::Right), (2, 6)); + assert_eq!(navigate(2, 6, NavigationDirection::Right), (2, 8)); + assert_eq!(navigate(2, 8, NavigationDirection::Right), (2, LAST_COLUMN)); + + assert_eq!(navigate(2, 3, NavigationDirection::Right), (2, 4)); + assert_eq!(navigate(5, 1, NavigationDirection::Right), (5, 4)); + assert_eq!(navigate(5, 2, NavigationDirection::Right), (5, 4)); + + // Direction = left + assert_eq!(navigate(2, LAST_COLUMN, NavigationDirection::Left), (2, 8)); + assert_eq!(navigate(2, 8, NavigationDirection::Left), (2, 6)); + assert_eq!(navigate(2, 6, NavigationDirection::Left), (2, 4)); + assert_eq!(navigate(2, 4, NavigationDirection::Left), (2, 2)); + assert_eq!(navigate(2, 2, NavigationDirection::Left), (2, 1)); + + assert_eq!(navigate(2, 3, NavigationDirection::Left), (2, 2)); + assert_eq!(navigate(5, 8, NavigationDirection::Left), (5, 4)); + assert_eq!(navigate(5, 7, NavigationDirection::Left), (5, 4)); + + // Direction = down + assert_eq!(navigate(1, 5, NavigationDirection::Down), (2, 5)); + assert_eq!(navigate(2, 5, NavigationDirection::Down), (4, 5)); + assert_eq!(navigate(4, 5, NavigationDirection::Down), (6, 5)); + assert_eq!(navigate(6, 5, NavigationDirection::Down), (LAST_ROW, 5)); + + assert_eq!(navigate(2, 3, NavigationDirection::Down), (6, 3)); + assert_eq!(navigate(3, 3, NavigationDirection::Down), (6, 3)); + assert_eq!(navigate(5, 3, NavigationDirection::Down), (6, 3)); + + // Direction = up + assert_eq!(navigate(LAST_ROW, 5, NavigationDirection::Up), (6, 5)); + assert_eq!(navigate(6, 5, NavigationDirection::Up), (4, 5)); + assert_eq!(navigate(4, 5, NavigationDirection::Up), (2, 5)); + assert_eq!(navigate(2, 5, NavigationDirection::Up), (1, 5)); + + assert_eq!(navigate(7, 3, NavigationDirection::Up), (6, 3)); + assert_eq!(navigate(8, 3, NavigationDirection::Up), (6, 3)); + assert_eq!(navigate(9, 3, NavigationDirection::Up), (6, 3)); +} diff --git a/base/src/test/util.rs b/base/src/test/util.rs new file mode 100644 index 0000000..c1b2d55 --- /dev/null +++ b/base/src/test/util.rs @@ -0,0 +1,52 @@ +#![allow(clippy::unwrap_used)] + +use crate::calc_result::CellReference; +use crate::model::Model; +use crate::types::Cell; + +pub fn new_empty_model() -> Model { + Model::new_empty("model", "en", "UTC").unwrap() +} + +impl Model { + fn _parse_reference(&self, cell: &str) -> CellReference { + if cell.contains('!') { + self.parse_reference(cell).unwrap() + } else { + self.parse_reference(&format!("Sheet1!{}", cell)).unwrap() + } + } + pub fn _set(&mut self, cell: &str, value: &str) { + let cell_reference = self._parse_reference(cell); + let column = cell_reference.column; + let row = cell_reference.row; + self.set_user_input(cell_reference.sheet, row, column, value.to_string()); + } + pub fn _has_formula(&self, cell: &str) -> bool { + self._get_formula_opt(cell).is_some() + } + pub fn _get_formula(&self, cell: &str) -> String { + self._get_formula_opt(cell).unwrap_or_default() + } + fn _get_formula_opt(&self, cell: &str) -> Option { + let cell_reference = self._parse_reference(cell); + let column = cell_reference.column; + let row = cell_reference.row; + self.cell_formula(cell_reference.sheet, row, column) + .unwrap() + } + pub fn _get_text_at(&self, sheet: u32, row: i32, column: i32) -> String { + self.formatted_cell_value(sheet, row, column).unwrap() + } + pub fn _get_text(&self, cell: &str) -> String { + let CellReference { sheet, row, column } = self._parse_reference(cell); + self._get_text_at(sheet, row, column) + } + pub fn _get_cell(&self, cell: &str) -> &Cell { + let cell_reference = self._parse_reference(cell); + let worksheet = self.workbook.worksheet(cell_reference.sheet).unwrap(); + worksheet + .cell(cell_reference.row, cell_reference.column) + .unwrap() + } +} diff --git a/base/src/types.rs b/base/src/types.rs new file mode 100644 index 0000000..7024de8 --- /dev/null +++ b/base/src/types.rs @@ -0,0 +1,653 @@ +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, fmt::Display}; + +use crate::expressions::token::Error; + +// Useful for `#[serde(default = "default_as_true")]` +fn default_as_true() -> bool { + true +} + +fn default_as_false() -> bool { + false +} + +// Useful for `#[serde(skip_serializing_if = "is_true")]` +fn is_true(b: &bool) -> bool { + *b +} + +fn is_false(b: &bool) -> bool { + !*b +} + +fn is_zero(num: &i32) -> bool { + *num == 0 +} + +fn is_default_alignment(o: &Option) -> bool { + o.is_none() || *o == Some(Alignment::default()) +} + +fn hashmap_is_empty(h: &HashMap) -> bool { + h.values().len() == 0 +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Metadata { + pub application: String, + pub app_version: String, + pub creator: String, + pub last_modified_by: String, + pub created: String, // "2020-08-06T21:20:53Z", + pub last_modified: String, //"2020-11-20T16:24:35" +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct WorkbookSettings { + pub tz: String, + pub locale: String, +} +/// An internal representation of an IronCalc Workbook +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +#[serde(deny_unknown_fields)] +pub struct Workbook { + pub shared_strings: Vec, + pub defined_names: Vec, + pub worksheets: Vec, + pub styles: Styles, + pub name: String, + pub settings: WorkbookSettings, + pub metadata: Metadata, + #[serde(default)] + #[serde(skip_serializing_if = "hashmap_is_empty")] + pub tables: HashMap, +} + +/// A defined name. The `sheet_id` is the sheet index in case the name is local +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct DefinedName { + pub name: String, + pub formula: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub sheet_id: Option, +} + +// TODO: Move to worksheet.rs make frozen_rows/columns private and u32 +/// Internal representation of a worksheet Excel object + +/// * state: +/// 18.18.68 ST_SheetState (Sheet Visibility Types) +/// hidden, veryHidden, visible +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum SheetState { + Visible, + Hidden, + VeryHidden, +} + +impl Display for SheetState { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + SheetState::Visible => write!(formatter, "visible"), + SheetState::Hidden => write!(formatter, "hidden"), + SheetState::VeryHidden => write!(formatter, "veryHidden"), + } + } +} + +/// Internal representation of a worksheet Excel object +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct Worksheet { + pub dimension: String, + pub cols: Vec, + pub rows: Vec, + pub name: String, + pub sheet_data: SheetData, + pub shared_formulas: Vec, + pub sheet_id: u32, + pub state: SheetState, + #[serde(skip_serializing_if = "Option::is_none")] + pub color: Option, + pub merge_cells: Vec, + pub comments: Vec, + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub frozen_rows: i32, + #[serde(default)] + #[serde(skip_serializing_if = "is_zero")] + pub frozen_columns: i32, +} + +/// Internal representation of Excel's sheet_data +/// It is row first and because of this all of our API's should be row first +pub type SheetData = HashMap>; + +// ECMA-376-1:2016 section 18.3.1.73 +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct Row { + /// Row index + pub r: i32, + pub height: f64, + pub custom_format: bool, + pub custom_height: bool, + pub s: i32, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub hidden: bool, +} + +// ECMA-376-1:2016 section 18.3.1.13 +#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)] +pub struct Col { + // Column definitions are defined on ranges, unlike rows which store unique, per-row entries. + /// First column affected by this record. Settings apply to column in \[min, max\] range. + pub min: i32, + /// Last column affected by this record. Settings apply to column in \[min, max\] range. + pub max: i32, + + pub width: f64, + pub custom_width: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub style: Option, +} + +/// Cell type enum matching Excel TYPE() function values. +#[derive(Debug, Eq, PartialEq)] +pub enum CellType { + Number = 1, + Text = 2, + LogicalValue = 4, + ErrorValue = 16, + Array = 64, + CompoundData = 128, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +#[serde(tag = "t", deny_unknown_fields)] +pub enum Cell { + #[serde(rename = "empty")] + EmptyCell { s: i32 }, + #[serde(rename = "b")] + BooleanCell { v: bool, s: i32 }, + #[serde(rename = "n")] + NumberCell { v: f64, s: i32 }, + // Maybe we should not have this type. In Excel this is just a string + #[serde(rename = "e")] + ErrorCell { ei: Error, s: i32 }, + // Always a shared string + #[serde(rename = "s")] + SharedString { si: i32, s: i32 }, + // Non evaluated Formula + #[serde(rename = "u")] + CellFormula { f: i32, s: i32 }, + #[serde(rename = "fb")] + CellFormulaBoolean { f: i32, v: bool, s: i32 }, + #[serde(rename = "fn")] + CellFormulaNumber { f: i32, v: f64, s: i32 }, + // always inline string + #[serde(rename = "str")] + CellFormulaString { f: i32, v: String, s: i32 }, + #[serde(rename = "fe")] + CellFormulaError { + f: i32, + ei: Error, + s: i32, + // Origin: Sheet3!C4 + o: String, + // Error Message: "Not implemented function" + m: String, + }, + // TODO: Array formulas +} + +impl Default for Cell { + fn default() -> Self { + Cell::EmptyCell { s: 0 } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Comment { + pub text: String, + pub author_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub author_id: Option, + pub cell_ref: String, +} + +// ECMA-376-1:2016 section 18.5.1.2 +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Table { + pub name: String, + pub display_name: String, + pub sheet_name: String, + pub reference: String, + pub totals_row_count: u32, + pub header_row_count: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_row_dxf_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data_dxf_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub totals_row_dxf_id: Option, + pub columns: Vec, + pub style_info: TableStyleInfo, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub has_filters: bool, +} + +// totals_row_label vs totals_row_function might be mutually exclusive. Use an enum? +// the totals_row_function is an enum not String methinks +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct TableColumn { + pub id: u32, + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub totals_row_label: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub header_row_dxf_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub data_dxf_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub totals_row_dxf_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub totals_row_function: Option, +} + +impl Default for TableColumn { + fn default() -> Self { + TableColumn { + id: 0, + name: "Column".to_string(), + totals_row_label: None, + totals_row_function: None, + data_dxf_id: None, + header_row_dxf_id: None, + totals_row_dxf_id: None, + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] +pub struct TableStyleInfo { + #[serde(skip_serializing_if = "Option::is_none")] + pub name: Option, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub show_first_column: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub show_last_column: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub show_row_stripes: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub show_column_stripes: bool, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Styles { + pub num_fmts: Vec, + pub fonts: Vec, + pub fills: Vec, + pub borders: Vec, + pub cell_style_xfs: Vec, + pub cell_xfs: Vec, + pub cell_styles: Vec, +} + +impl Default for Styles { + fn default() -> Self { + Styles { + num_fmts: vec![], + fonts: vec![Default::default()], + fills: vec![Default::default()], + borders: vec![Default::default()], + cell_style_xfs: vec![Default::default()], + cell_xfs: vec![Default::default()], + cell_styles: vec![Default::default()], + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct NumFmt { + pub num_fmt_id: i32, + pub format_code: String, +} + +impl Default for NumFmt { + fn default() -> Self { + NumFmt { + num_fmt_id: 0, + format_code: "general".to_string(), + } + } +} + +// ST_FontScheme simple type (§18.18.33). +// Usually major fonts are used for styles like headings, +// and minor fonts are used for body and paragraph text. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "lowercase")] +#[derive(Default)] +pub enum FontScheme { + #[default] + Minor, + Major, + None, +} + +impl Display for FontScheme { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + FontScheme::Minor => write!(formatter, "minor"), + FontScheme::Major => write!(formatter, "major"), + FontScheme::None => write!(formatter, "none"), + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Font { + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub strike: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub u: bool, // seems that Excel supports a bit more - double underline / account underline etc. + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub b: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub i: bool, + pub sz: i32, + pub color: Option, + pub name: String, + // This is the font family fallback + // 1 -> serif + // 2 -> sans serif + // 3 -> monospaced + // ... + pub family: i32, + pub scheme: FontScheme, +} + +impl Default for Font { + fn default() -> Self { + Font { + strike: false, + u: false, + b: false, + i: false, + sz: 11, + color: Some("#000000".to_string()), + name: "Calibri".to_string(), + family: 2, + scheme: FontScheme::Minor, + } + } +} + +// TODO: Maybe use an enum for the pattern_type values here? +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Fill { + pub pattern_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub fg_color: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bg_color: Option, +} + +impl Default for Fill { + fn default() -> Self { + Fill { + pattern_type: "none".to_string(), + fg_color: Default::default(), + bg_color: Default::default(), + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum HorizontalAlignment { + Center, + CenterContinuous, + Distributed, + Fill, + General, + Justify, + Left, + Right, +} + +// Note that alignment in "General" depends on type +impl Default for HorizontalAlignment { + fn default() -> Self { + Self::General + } +} + +impl HorizontalAlignment { + fn is_default(&self) -> bool { + self == &HorizontalAlignment::default() + } +} + +// FIXME: Is there a way to generate this automatically? +impl Display for HorizontalAlignment { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + HorizontalAlignment::Center => write!(formatter, "center"), + HorizontalAlignment::CenterContinuous => write!(formatter, "centerContinuous"), + HorizontalAlignment::Distributed => write!(formatter, "distributed"), + HorizontalAlignment::Fill => write!(formatter, "fill"), + HorizontalAlignment::General => write!(formatter, "general"), + HorizontalAlignment::Justify => write!(formatter, "justify"), + HorizontalAlignment::Left => write!(formatter, "left"), + HorizontalAlignment::Right => write!(formatter, "right"), + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum VerticalAlignment { + Bottom, + Center, + Distributed, + Justify, + Top, +} + +impl VerticalAlignment { + fn is_default(&self) -> bool { + self == &VerticalAlignment::default() + } +} + +impl Default for VerticalAlignment { + fn default() -> Self { + Self::Bottom + } +} + +impl Display for VerticalAlignment { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + VerticalAlignment::Bottom => write!(formatter, "bottom"), + VerticalAlignment::Center => write!(formatter, "center"), + VerticalAlignment::Distributed => write!(formatter, "distributed"), + VerticalAlignment::Justify => write!(formatter, "justify"), + VerticalAlignment::Top => write!(formatter, "top"), + } + } +} + +// 1762 +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] +pub struct Alignment { + #[serde(default)] + #[serde(skip_serializing_if = "HorizontalAlignment::is_default")] + pub horizontal: HorizontalAlignment, + #[serde(skip_serializing_if = "VerticalAlignment::is_default")] + #[serde(default)] + pub vertical: VerticalAlignment, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub wrap_text: bool, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct CellStyleXfs { + pub num_fmt_id: i32, + pub font_id: i32, + pub fill_id: i32, + pub border_id: i32, + #[serde(default = "default_as_true")] + #[serde(skip_serializing_if = "is_true")] + pub apply_number_format: bool, + #[serde(default = "default_as_true")] + #[serde(skip_serializing_if = "is_true")] + pub apply_border: bool, + #[serde(default = "default_as_true")] + #[serde(skip_serializing_if = "is_true")] + pub apply_alignment: bool, + #[serde(default = "default_as_true")] + #[serde(skip_serializing_if = "is_true")] + pub apply_protection: bool, + #[serde(default = "default_as_true")] + #[serde(skip_serializing_if = "is_true")] + pub apply_font: bool, + #[serde(default = "default_as_true")] + #[serde(skip_serializing_if = "is_true")] + pub apply_fill: bool, +} + +impl Default for CellStyleXfs { + fn default() -> Self { + CellStyleXfs { + num_fmt_id: 0, + font_id: 0, + fill_id: 0, + border_id: 0, + apply_number_format: true, + apply_border: true, + apply_alignment: true, + apply_protection: true, + apply_font: true, + apply_fill: true, + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] +pub struct CellXfs { + pub xf_id: i32, + pub num_fmt_id: i32, + pub font_id: i32, + pub fill_id: i32, + pub border_id: i32, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub apply_number_format: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub apply_border: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub apply_alignment: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub apply_protection: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub apply_font: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub apply_fill: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub quote_prefix: bool, + #[serde(skip_serializing_if = "is_default_alignment")] + pub alignment: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct CellStyles { + pub name: String, + pub xf_id: i32, + pub builtin_id: i32, +} + +impl Default for CellStyles { + fn default() -> Self { + CellStyles { + name: "normal".to_string(), + xf_id: 0, + builtin_id: 0, + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +#[serde(rename_all = "lowercase")] +pub enum BorderStyle { + Thin, + Medium, + Thick, + Double, + Dotted, + SlantDashDot, + MediumDashed, + MediumDashDotDot, + MediumDashDot, +} + +impl Display for BorderStyle { + fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + BorderStyle::Thin => write!(formatter, "thin"), + BorderStyle::Thick => write!(formatter, "thick"), + BorderStyle::SlantDashDot => write!(formatter, "slantdashdot"), + BorderStyle::MediumDashed => write!(formatter, "mediumdashed"), + BorderStyle::MediumDashDotDot => write!(formatter, "mediumdashdotdot"), + BorderStyle::MediumDashDot => write!(formatter, "mediumdashdot"), + BorderStyle::Medium => write!(formatter, "medium"), + BorderStyle::Double => write!(formatter, "double"), + BorderStyle::Dotted => write!(formatter, "dotted"), + } + } +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct BorderItem { + pub style: BorderStyle, + pub color: Option, +} + +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)] +pub struct Border { + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub diagonal_up: bool, + #[serde(default = "default_as_false")] + #[serde(skip_serializing_if = "is_false")] + pub diagonal_down: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub left: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub right: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub top: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub bottom: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub diagonal: Option, +} diff --git a/base/src/units.rs b/base/src/units.rs new file mode 100644 index 0000000..2cd06c0 --- /dev/null +++ b/base/src/units.rs @@ -0,0 +1,364 @@ +use crate::{ + calc_result::CellReference, + expressions::{parser::Node, token::OpProduct}, + formatter::parser::{ParsePart, Parser}, + functions::Function, + model::Model, +}; + +pub enum Units { + Number { + group_separator: bool, + precision: i32, + num_fmt: String, + }, + Currency { + group_separator: bool, + precision: i32, + num_fmt: String, + currency: String, + }, + Percentage { + group_separator: bool, + precision: i32, + num_fmt: String, + }, + Date(String), +} + +impl Units { + pub fn get_num_fmt(&self) -> String { + match self { + Units::Number { num_fmt, .. } => num_fmt.to_string(), + Units::Currency { num_fmt, .. } => num_fmt.to_string(), + Units::Percentage { num_fmt, .. } => num_fmt.to_string(), + Units::Date(num_fmt) => num_fmt.to_string(), + } + } + pub fn get_precision(&self) -> i32 { + match self { + Units::Number { precision, .. } => *precision, + Units::Currency { precision, .. } => *precision, + Units::Percentage { precision, .. } => *precision, + Units::Date(_) => 0, + } + } +} + +fn get_units_from_format_string(num_fmt: &str) -> Option { + let mut parser = Parser::new(num_fmt); + parser.parse(); + // We only care about the first part (positive number) + match &parser.parts[0] { + ParsePart::Number(part) => { + if part.percent > 0 { + Some(Units::Percentage { + num_fmt: num_fmt.to_string(), + group_separator: part.use_thousands, + precision: part.precision, + }) + } else if num_fmt.contains('$') { + Some(Units::Currency { + num_fmt: num_fmt.to_string(), + group_separator: part.use_thousands, + precision: part.precision, + currency: "$".to_string(), + }) + } else if num_fmt.contains('€') { + Some(Units::Currency { + num_fmt: num_fmt.to_string(), + group_separator: part.use_thousands, + precision: part.precision, + currency: "€".to_string(), + }) + } else { + Some(Units::Number { + num_fmt: num_fmt.to_string(), + group_separator: part.use_thousands, + precision: part.precision, + }) + } + } + ParsePart::Date(_) => Some(Units::Date(num_fmt.to_string())), + ParsePart::Error(_) => None, + ParsePart::General(_) => None, + } +} + +impl Model { + fn compute_cell_units(&self, cell_reference: &CellReference) -> Option { + let style = &self.get_style_for_cell( + cell_reference.sheet, + cell_reference.row, + cell_reference.column, + ); + get_units_from_format_string(&style.num_fmt) + } + + pub(crate) fn compute_node_units(&self, node: &Node, cell: &CellReference) -> Option { + match node { + Node::ReferenceKind { + sheet_name: _, + sheet_index, + absolute_row, + absolute_column, + row, + column, + } => { + let mut row1 = *row; + let mut column1 = *column; + if !absolute_row { + row1 += cell.row; + } + if !absolute_column { + column1 += cell.column; + } + self.compute_cell_units(&CellReference { + sheet: *sheet_index, + row: row1, + column: column1, + }) + } + Node::RangeKind { + sheet_name: _, + sheet_index, + absolute_row1, + absolute_column1, + row1, + column1, + absolute_row2: _, + absolute_column2: _, + row2: _, + column2: _, + } => { + // We return the unit of the first element + let mut row1 = *row1; + let mut column1 = *column1; + if !absolute_row1 { + row1 += cell.row; + } + if !absolute_column1 { + column1 += cell.column; + } + self.compute_cell_units(&CellReference { + sheet: *sheet_index, + row: row1, + column: column1, + }) + } + Node::OpSumKind { + kind: _, + left, + right, + } => { + let left_units = self.compute_node_units(left, cell); + let right_units = self.compute_node_units(right, cell); + match (&left_units, &right_units) { + (Some(_), None) => left_units, + (None, Some(_)) => right_units, + (Some(l), Some(r)) => { + if l.get_precision() < r.get_precision() { + right_units + } else { + left_units + } + } + (None, None) => None, + } + } + Node::OpProductKind { kind, left, right } => { + let left_units = self.compute_node_units(left, cell); + let right_units = self.compute_node_units(right, cell); + match (&left_units, &right_units) { + ( + Some(Units::Percentage { precision: l, .. }), + Some(Units::Percentage { precision: r, .. }), + ) => { + if l > r { + left_units + } else { + if *r > 1 { + return right_units; + } + // When multiplying percentage we want at least two decimal places + Some(Units::Percentage { + group_separator: false, + precision: 2, + num_fmt: "0.00%".to_string(), + }) + } + } + ( + Some(Units::Currency { + currency, + precision, + .. + }), + Some(Units::Percentage { .. }), + ) => { + match kind { + OpProduct::Divide => None, + OpProduct::Times => { + if *precision > 1 { + return left_units; + } + // This is tricky, we need at least 2 digit precision + // but I do not want to mess with the num_fmt string + Some(Units::Currency { + currency: currency.to_string(), + group_separator: true, + precision: 2, + num_fmt: format!("{currency}#,##0.00"), + }) + } + } + } + ( + Some(Units::Percentage { .. }), + Some(Units::Currency { + precision, + currency, + .. + }), + ) => { + match kind { + OpProduct::Divide => None, + OpProduct::Times => { + if *precision > 1 { + return right_units; + } + // This is tricky, we need at least 2 digit precision + // but I do not want to mess with the num_fmt string + Some(Units::Currency { + currency: currency.to_string(), + group_separator: true, + precision: 2, + num_fmt: format!("{currency}#,##0.00"), + }) + } + } + } + (Some(Units::Percentage { .. }), _) => right_units, + (_, Some(Units::Percentage { .. })) => match kind { + OpProduct::Divide => None, + OpProduct::Times => left_units, + }, + (None, _) => match kind { + OpProduct::Divide => None, + OpProduct::Times => right_units, + }, + (_, None) => left_units, + ( + Some(Units::Number { precision: l, .. }), + Some(Units::Number { precision: r, .. }), + ) => { + if l > r { + left_units + } else { + right_units + } + } + (Some(Units::Number { .. }), _) => match kind { + OpProduct::Divide => None, + OpProduct::Times => right_units, + }, + (_, Some(Units::Number { .. })) => left_units, + _ => None, + } + } + Node::FunctionKind { kind, args } => self.compute_function_units(kind, args, cell), + Node::UnaryKind { kind: _, right } => { + // What happens if kind => OpUnary::Percentage? + self.compute_node_units(right, cell) + } + // The rest of the nodes return None + Node::BooleanKind(_) => None, + Node::NumberKind(_) => None, + Node::StringKind(_) => None, + Node::WrongReferenceKind { .. } => None, + Node::WrongRangeKind { .. } => None, + Node::OpRangeKind { .. } => None, + Node::OpConcatenateKind { .. } => None, + Node::ErrorKind(_) => None, + Node::ParseErrorKind { .. } => None, + Node::EmptyArgKind => None, + Node::InvalidFunctionKind { .. } => None, + Node::ArrayKind(_) => None, + Node::VariableKind(_) => None, + Node::CompareKind { .. } => None, + Node::OpPowerKind { .. } => None, + } + } + + fn compute_function_units( + &self, + kind: &Function, + args: &[Node], + cell: &CellReference, + ) -> Option { + match kind { + Function::Sum => self.units_fn_sum_like(args, cell), + Function::Average => self.units_fn_sum_like(args, cell), + Function::Pmt => self.units_fn_currency(args, cell), + Function::Nper => self.units_fn_currency(args, cell), + Function::Npv => self.units_fn_currency(args, cell), + Function::Irr => self.units_fn_percentage(args, cell), + Function::Mirr => self.units_fn_percentage(args, cell), + Function::Sln => self.units_fn_currency(args, cell), + Function::Syd => self.units_fn_currency(args, cell), + Function::Db => self.units_fn_currency(args, cell), + Function::Ddb => self.units_fn_currency(args, cell), + Function::Cumipmt => self.units_fn_currency(args, cell), + Function::Cumprinc => self.units_fn_currency(args, cell), + Function::Tbilleq => self.units_fn_percentage_2(args, cell), + Function::Tbillprice => self.units_fn_currency(args, cell), + Function::Tbillyield => self.units_fn_percentage_2(args, cell), + Function::Date => self.units_fn_dates(args, cell), + Function::Today => self.units_fn_dates(args, cell), + _ => None, + } + } + + fn units_fn_sum_like(&self, args: &[Node], cell: &CellReference) -> Option { + // We return the unit of the first argument + if !args.is_empty() { + return self.compute_node_units(&args[0], cell); + } + None + } + + fn units_fn_currency(&self, _args: &[Node], _cell: &CellReference) -> Option { + let currency_symbol = &self.locale.currency.symbol; + let standard_format = &self.locale.numbers.currency_formats.standard; + let num_fmt = standard_format.replace('¤', currency_symbol); + // The "space" in the cldr is a weird space. + let num_fmt = num_fmt.replace(' ', " "); + Some(Units::Currency { + num_fmt, + group_separator: true, + precision: 2, + currency: currency_symbol.to_string(), + }) + } + + fn units_fn_percentage(&self, _args: &[Node], _cell: &CellReference) -> Option { + Some(Units::Percentage { + group_separator: false, + precision: 0, + num_fmt: "0%".to_string(), + }) + } + + fn units_fn_percentage_2(&self, _args: &[Node], _cell: &CellReference) -> Option { + Some(Units::Percentage { + group_separator: false, + precision: 2, + num_fmt: "0.00%".to_string(), + }) + } + + fn units_fn_dates(&self, _args: &[Node], _cell: &CellReference) -> Option { + // TODO: update locale and use it here + Some(Units::Date("dd/mm/yyyy".to_string())) + } +} diff --git a/base/src/utils.rs b/base/src/utils.rs new file mode 100644 index 0000000..121e3fa --- /dev/null +++ b/base/src/utils.rs @@ -0,0 +1,369 @@ +use crate::expressions::token::get_error_by_name; +use crate::language::Language; + +use crate::{ + calc_result::CellReference, + expressions::{ + lexer::{Lexer, LexerMode}, + token::TokenType, + }, + language::get_language, + locale::Locale, +}; + +#[derive(Debug, Eq, PartialEq)] +pub enum ParsedReference { + CellReference(CellReference), + Range(CellReference, CellReference), +} + +impl ParsedReference { + /// Parses reference in formula format. For example: `Sheet1!A1`, `Sheet1!$A$1:$B$9`. + /// Absolute references (`$`) do not affect parsing. + /// + /// # Arguments + /// + /// * `sheet_index_context` - if available, sheet index can be provided so references + /// without explicit sheet name can be recognized + /// * `reference` - text string to parse as reference + /// * `locale` - locale that will be used to set-up parser + /// * `get_sheet_index_by_name` - function that allows to translate sheet name to index + pub(crate) fn parse_reference_formula Option>( + sheet_index_context: Option, + reference: &str, + locale: &Locale, + get_sheet_index_by_name: F, + ) -> Result { + let language = get_language("en").expect(""); + let mut lexer = Lexer::new(reference, LexerMode::A1, locale, language); + + let reference_token = lexer.next_token(); + let eof_token = lexer.next_token(); + + if TokenType::EOF != eof_token { + return Err("Invalid reference. Expected only one token.".to_string()); + } + + match reference_token { + TokenType::Reference { + sheet: sheet_name, + column: column_id, + row: row_id, + .. + } => { + let sheet_index; + if let Some(name) = sheet_name { + match get_sheet_index_by_name(&name) { + Some(i) => sheet_index = i, + None => { + return Err(format!( + "Invalid reference. Sheet \"{}\" could not be found.", + name.as_str(), + )); + } + } + } else if let Some(sheet_index_context) = sheet_index_context { + sheet_index = sheet_index_context; + } else { + return Err( + "Reference doesn't contain sheet name and relative cell is not known." + .to_string(), + ); + } + + Ok(ParsedReference::CellReference(CellReference { + sheet: sheet_index, + row: row_id, + column: column_id, + })) + } + TokenType::Range { + sheet: sheet_name, + left, + right, + } => { + let sheet_index; + if let Some(name) = sheet_name { + match get_sheet_index_by_name(&name) { + Some(i) => sheet_index = i, + None => { + return Err(format!( + "Invalid reference. Sheet \"{}\" could not be found.", + name.as_str(), + )); + } + } + } else if let Some(sheet_index_context) = sheet_index_context { + sheet_index = sheet_index_context; + } else { + return Err( + "Reference doesn't contain sheet name and relative cell is not known." + .to_string(), + ); + } + + Ok(ParsedReference::Range( + CellReference { + sheet: sheet_index, + row: left.row, + column: left.column, + }, + CellReference { + sheet: sheet_index, + row: right.row, + column: right.column, + }, + )) + } + _ => Err("Invalid reference. First token is not a reference.".to_string()), + } + } +} + +/// Returns true if the string value could be interpreted as: +/// * a formula +/// * a number +/// * a boolean +/// * an error (i.e "#VALUE!") +pub(crate) fn value_needs_quoting(value: &str, language: &Language) -> bool { + value.starts_with('=') + || value.parse::().is_ok() + || value.to_lowercase().parse::().is_ok() + || get_error_by_name(&value.to_uppercase(), language).is_some() +} + +/// Valid hex colors are #FFAABB +/// #fff is not valid +pub(crate) fn is_valid_hex_color(color: &str) -> bool { + if color.chars().count() != 7 { + return false; + } + if !color.starts_with('#') { + return false; + } + if let Ok(z) = i32::from_str_radix(&color[1..], 16) { + if (0..=0xffffff).contains(&z) { + return true; + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::language::get_language; + use crate::locale::{get_locale, Locale}; + + fn get_test_locale() -> &'static Locale { + #![allow(clippy::unwrap_used)] + get_locale("en").unwrap() + } + + fn get_sheet_index_by_name(sheet_names: &[&str], name: &str) -> Option { + sheet_names + .iter() + .position(|&sheet_name| sheet_name == name) + .map(|index| index as u32) + } + + #[test] + fn test_parse_cell_references() { + let locale = get_test_locale(); + let sheet_names = vec!["Sheet1", "Sheet2", "Sheet3"]; + + assert_eq!( + ParsedReference::parse_reference_formula(Some(7), "A1", locale, |name| { + get_sheet_index_by_name(&sheet_names, name) + },), + Ok(ParsedReference::CellReference(CellReference { + sheet: 7, + row: 1, + column: 1, + })), + ); + + assert_eq!( + ParsedReference::parse_reference_formula(None, "Sheet1!A1", locale, |name| { + get_sheet_index_by_name(&sheet_names, name) + },), + Ok(ParsedReference::CellReference(CellReference { + sheet: 0, + row: 1, + column: 1, + })), + ); + + assert_eq!( + ParsedReference::parse_reference_formula(None, "Sheet1!$A$1", locale, |name| { + get_sheet_index_by_name(&sheet_names, name) + },), + Ok(ParsedReference::CellReference(CellReference { + sheet: 0, + row: 1, + column: 1, + })), + ); + + assert_eq!( + ParsedReference::parse_reference_formula(None, "Sheet2!$A$1", locale, |name| { + get_sheet_index_by_name(&sheet_names, name) + },), + Ok(ParsedReference::CellReference(CellReference { + sheet: 1, + row: 1, + column: 1, + })), + ); + } + + #[test] + fn test_parse_range_references() { + let locale = get_test_locale(); + let sheet_names = vec!["Sheet1", "Sheet2", "Sheet3"]; + + assert_eq!( + ParsedReference::parse_reference_formula(Some(5), "A1:A2", locale, |name| { + get_sheet_index_by_name(&sheet_names, name) + },), + Ok(ParsedReference::Range( + CellReference { + sheet: 5, + column: 1, + row: 1, + }, + CellReference { + sheet: 5, + column: 1, + row: 2, + }, + )), + ); + + assert_eq!( + ParsedReference::parse_reference_formula(None, "Sheet1!$A$1:$B$10", locale, |name| { + get_sheet_index_by_name(&sheet_names, name) + },), + Ok(ParsedReference::Range( + CellReference { + sheet: 0, + row: 1, + column: 1, + }, + CellReference { + sheet: 0, + row: 10, + column: 2, + }, + )), + ); + + assert_eq!( + ParsedReference::parse_reference_formula(None, "Sheet2!AA1:E$11", locale, |name| { + get_sheet_index_by_name(&sheet_names, name) + },), + Ok(ParsedReference::Range( + CellReference { + sheet: 1, + row: 1, + column: 27, + }, + CellReference { + sheet: 1, + row: 11, + column: 5, + }, + )), + ); + } + + #[test] + fn test_error_reject_assignments() { + let locale = get_test_locale(); + let sheet_index = Some(1); + assert_eq!( + ParsedReference::parse_reference_formula(sheet_index, "=A1", locale, |_| Some(1)), + Err("Invalid reference. Expected only one token.".to_string()), + ); + assert_eq!( + ParsedReference::parse_reference_formula(sheet_index, "=$A$1", locale, |_| { Some(1) }), + Err("Invalid reference. Expected only one token.".to_string()), + ); + assert_eq!( + ParsedReference::parse_reference_formula(None, "=Sheet1!A1", locale, |_| Some(1)), + Err("Invalid reference. Expected only one token.".to_string()), + ); + } + + #[test] + fn test_error_reject_formulas_without_equal_sign() { + let locale = get_test_locale(); + assert_eq!( + ParsedReference::parse_reference_formula(None, "SUM", locale, |_| Some(1)), + Err("Invalid reference. First token is not a reference.".to_string()), + ); + assert_eq!( + ParsedReference::parse_reference_formula(None, "SUM(A1:A2)", locale, |_| Some(1)), + Err("Invalid reference. Expected only one token.".to_string()), + ); + } + + #[test] + fn test_error_reject_without_sheet_and_relative_cell() { + let locale = get_test_locale(); + assert_eq!( + ParsedReference::parse_reference_formula(None, "A1", locale, |_| Some(1)), + Err("Reference doesn't contain sheet name and relative cell is not known.".to_string()), + ); + assert_eq!( + ParsedReference::parse_reference_formula(None, "A1:A2", locale, |_| Some(1)), + Err("Reference doesn't contain sheet name and relative cell is not known.".to_string()), + ); + } + + #[test] + fn test_error_unrecognized_sheet_name() { + let locale = get_test_locale(); + assert_eq!( + ParsedReference::parse_reference_formula(None, "SheetName!A1", locale, |_| None), + Err("Invalid reference. Sheet \"SheetName\" could not be found.".to_string()), + ); + assert_eq!( + ParsedReference::parse_reference_formula(None, "SheetName2!A1:A4", locale, |_| None), + Err("Invalid reference. Sheet \"SheetName2\" could not be found.".to_string()), + ); + } + + #[test] + fn test_value_needs_quoting() { + let en_language = get_language("en").expect("en language expected"); + + assert!(!value_needs_quoting("", en_language)); + assert!(!value_needs_quoting("hello", en_language)); + + assert!(value_needs_quoting("12", en_language)); + assert!(value_needs_quoting("true", en_language)); + assert!(value_needs_quoting("False", en_language)); + + assert!(value_needs_quoting("=A1", en_language)); + + assert!(value_needs_quoting("#REF!", en_language)); + assert!(value_needs_quoting("#NAME?", en_language)); + } + + #[test] + fn test_is_valid_hex_color() { + assert!(is_valid_hex_color("#000000")); + assert!(is_valid_hex_color("#ffffff")); + + assert!(!is_valid_hex_color("000000")); + assert!(!is_valid_hex_color("ffffff")); + + assert!(!is_valid_hex_color("#gggggg")); + + // Not obvious cases unrecognized as colors + assert!(!is_valid_hex_color("#ffffff ")); + assert!(!is_valid_hex_color("#fff")); // CSS shorthand + assert!(!is_valid_hex_color("#ffffff00")); // with alpha channel + } +} diff --git a/base/src/workbook.rs b/base/src/workbook.rs new file mode 100644 index 0000000..791acb1 --- /dev/null +++ b/base/src/workbook.rs @@ -0,0 +1,30 @@ +use std::vec::Vec; + +use crate::types::*; + +impl Workbook { + pub fn get_worksheet_names(&self) -> Vec { + self.worksheets + .iter() + .map(|worksheet| worksheet.get_name()) + .collect() + } + pub fn get_worksheet_ids(&self) -> Vec { + self.worksheets + .iter() + .map(|worksheet| worksheet.get_sheet_id()) + .collect() + } + + pub fn worksheet(&self, worksheet_index: u32) -> Result<&Worksheet, String> { + self.worksheets + .get(worksheet_index as usize) + .ok_or_else(|| "Invalid sheet index".to_string()) + } + + pub fn worksheet_mut(&mut self, worksheet_index: u32) -> Result<&mut Worksheet, String> { + self.worksheets + .get_mut(worksheet_index as usize) + .ok_or_else(|| "Invalid sheet index".to_string()) + } +} diff --git a/base/src/worksheet.rs b/base/src/worksheet.rs new file mode 100644 index 0000000..974e1a3 --- /dev/null +++ b/base/src/worksheet.rs @@ -0,0 +1,630 @@ +use crate::constants::{self, LAST_COLUMN, LAST_ROW}; +use crate::expressions::types::CellReferenceIndex; +use crate::expressions::utils::{is_valid_column_number, is_valid_row}; +use crate::{expressions::token::Error, types::*}; + +use std::collections::HashMap; + +#[derive(Debug, PartialEq, Eq)] +pub struct WorksheetDimension { + pub min_row: i32, + pub max_row: i32, + pub min_column: i32, + pub max_column: i32, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum NavigationDirection { + Left, + Right, + Up, + Down, +} + +impl Worksheet { + pub fn get_name(&self) -> String { + self.name.clone() + } + + pub fn get_sheet_id(&self) -> u32 { + self.sheet_id + } + + pub fn set_name(&mut self, name: &str) { + self.name = name.to_string(); + } + + pub fn cell(&self, row: i32, column: i32) -> Option<&Cell> { + self.sheet_data.get(&row)?.get(&column) + } + + pub(crate) fn cell_mut(&mut self, row: i32, column: i32) -> Option<&mut Cell> { + self.sheet_data.get_mut(&row)?.get_mut(&column) + } + + fn update_cell(&mut self, row: i32, column: i32, new_cell: Cell) { + match self.sheet_data.get_mut(&row) { + Some(column_data) => match column_data.get(&column) { + Some(_cell) => { + column_data.insert(column, new_cell); + } + None => { + column_data.insert(column, new_cell); + } + }, + None => { + let mut column_data = HashMap::new(); + column_data.insert(column, new_cell); + self.sheet_data.insert(row, column_data); + } + } + } + + // TODO [MVP]: Pass the cell style from the model + // See: get_style_for_cell + fn get_row_column_style(&self, row_index: i32, column_index: i32) -> i32 { + let rows = &self.rows; + for row in rows { + if row.r == row_index { + if row.custom_format { + return row.s; + } else { + break; + } + } + } + let cols = &self.cols; + for column in cols.iter() { + let min = column.min; + let max = column.max; + if column_index >= min && column_index <= max { + return column.style.unwrap_or(0); + } + } + 0 + } + + pub fn get_style(&self, row: i32, column: i32) -> i32 { + match self.sheet_data.get(&row) { + Some(column_data) => match column_data.get(&column) { + Some(cell) => cell.get_style(), + None => self.get_row_column_style(row, column), + }, + None => self.get_row_column_style(row, column), + } + } + + pub fn set_style(&mut self, style_index: i32) -> Result<(), String> { + self.cols = vec![Col { + min: 1, + max: constants::LAST_COLUMN, + width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR, + custom_width: true, + style: Some(style_index), + }]; + Ok(()) + } + + pub fn set_column_style(&mut self, column: i32, style_index: i32) -> Result<(), String> { + let cols = &mut self.cols; + let col = Col { + min: column, + max: column, + width: constants::DEFAULT_COLUMN_WIDTH / constants::COLUMN_WIDTH_FACTOR, + custom_width: true, + style: Some(style_index), + }; + let mut index = 0; + let mut split = false; + for c in cols.iter_mut() { + let min = c.min; + let max = c.max; + if min <= column && column <= max { + if min == column && max == column { + c.style = Some(style_index); + return Ok(()); + } else { + // We need to split the result + split = true; + break; + } + } + if column < min { + // We passed, we should insert at index + break; + } + index += 1; + } + if split { + let min = cols[index].min; + let max = cols[index].max; + let pre = Col { + min, + max: column - 1, + width: cols[index].width, + custom_width: cols[index].custom_width, + style: cols[index].style, + }; + let post = Col { + min: column + 1, + max, + width: cols[index].width, + custom_width: cols[index].custom_width, + style: cols[index].style, + }; + cols.remove(index); + if column != max { + cols.insert(index, post); + } + cols.insert(index, col); + if column != min { + cols.insert(index, pre); + } + } else { + cols.insert(index, col); + } + Ok(()) + } + + pub fn set_row_style(&mut self, row: i32, style_index: i32) -> Result<(), String> { + for r in self.rows.iter_mut() { + if r.r == row { + r.s = style_index; + r.custom_format = true; + return Ok(()); + } + } + self.rows.push(Row { + height: constants::DEFAULT_ROW_HEIGHT / constants::ROW_HEIGHT_FACTOR, + r: row, + custom_format: true, + custom_height: true, + s: style_index, + hidden: false, + }); + Ok(()) + } + + pub fn set_cell_style(&mut self, row: i32, column: i32, style_index: i32) { + match self.cell_mut(row, column) { + Some(cell) => { + cell.set_style(style_index); + } + None => { + self.set_cell_empty_with_style(row, column, style_index); + } + } + + // TODO: cleanup check if the old cell style is still in use + } + + pub fn set_cell_with_formula(&mut self, row: i32, column: i32, index: i32, style: i32) { + let cell = Cell::new_formula(index, style); + self.update_cell(row, column, cell); + } + + pub fn set_cell_with_number(&mut self, row: i32, column: i32, value: f64, style: i32) { + let cell = Cell::new_number(value, style); + self.update_cell(row, column, cell); + } + + pub fn set_cell_with_string(&mut self, row: i32, column: i32, index: i32, style: i32) { + let cell = Cell::new_string(index, style); + self.update_cell(row, column, cell); + } + + pub fn set_cell_with_boolean(&mut self, row: i32, column: i32, value: bool, style: i32) { + let cell = Cell::new_boolean(value, style); + self.update_cell(row, column, cell); + } + + pub fn set_cell_with_error(&mut self, row: i32, column: i32, error: Error, style: i32) { + let cell = Cell::new_error(error, style); + self.update_cell(row, column, cell); + } + + pub fn set_cell_empty(&mut self, row: i32, column: i32) { + let s = self.get_style(row, column); + let cell = Cell::EmptyCell { s }; + self.update_cell(row, column, cell); + } + + pub fn set_cell_empty_with_style(&mut self, row: i32, column: i32, style: i32) { + let cell = Cell::EmptyCell { s: style }; + self.update_cell(row, column, cell); + } + + pub fn set_frozen_rows(&mut self, frozen_rows: i32) -> Result<(), String> { + if frozen_rows < 0 { + return Err("Frozen rows cannot be negative".to_string()); + } else if frozen_rows >= constants::LAST_ROW { + return Err("Too many rows".to_string()); + } + self.frozen_rows = frozen_rows; + Ok(()) + } + + pub fn set_frozen_columns(&mut self, frozen_columns: i32) -> Result<(), String> { + if frozen_columns < 0 { + return Err("Frozen columns cannot be negative".to_string()); + } else if frozen_columns >= constants::LAST_COLUMN { + return Err("Too many columns".to_string()); + } + self.frozen_columns = frozen_columns; + Ok(()) + } + + /// Changes the height of a row. + /// * If the row does not a have a style we add it. + /// * If it has we modify the height and make sure it is applied. + /// Fails if column index is outside allowed range. + pub fn set_row_height(&mut self, row: i32, height: f64) -> Result<(), String> { + if !is_valid_row(row) { + return Err(format!("Row number '{row}' is not valid.")); + } + + let rows = &mut self.rows; + for r in rows.iter_mut() { + if r.r == row { + r.height = height / constants::ROW_HEIGHT_FACTOR; + r.custom_height = true; + return Ok(()); + } + } + rows.push(Row { + height: height / constants::ROW_HEIGHT_FACTOR, + r: row, + custom_format: false, + custom_height: true, + s: 0, + hidden: false, + }); + Ok(()) + } + /// Changes the width of a column. + /// * If the column does not a have a width we simply add it + /// * If it has, it might be part of a range and we ned to split the range. + /// Fails if column index is outside allowed range. + pub fn set_column_width(&mut self, column: i32, width: f64) -> Result<(), String> { + if !is_valid_column_number(column) { + return Err(format!("Column number '{column}' is not valid.")); + } + let cols = &mut self.cols; + let mut col = Col { + min: column, + max: column, + width: width / constants::COLUMN_WIDTH_FACTOR, + custom_width: true, + style: None, + }; + let mut index = 0; + let mut split = false; + for c in cols.iter_mut() { + let min = c.min; + let max = c.max; + if min <= column && column <= max { + if min == column && max == column { + c.width = width / constants::COLUMN_WIDTH_FACTOR; + return Ok(()); + } else { + // We need to split the result + split = true; + break; + } + } + if column < min { + // We passed, we should insert at index + break; + } + index += 1; + } + if split { + let min = cols[index].min; + let max = cols[index].max; + let pre = Col { + min, + max: column - 1, + width: cols[index].width, + custom_width: cols[index].custom_width, + style: cols[index].style, + }; + let post = Col { + min: column + 1, + max, + width: cols[index].width, + custom_width: cols[index].custom_width, + style: cols[index].style, + }; + col.style = cols[index].style; + cols.remove(index); + if column != max { + cols.insert(index, post); + } + cols.insert(index, col); + if column != min { + cols.insert(index, pre); + } + } else { + cols.insert(index, col); + } + Ok(()) + } + + /// Return the width of a column in pixels + pub fn column_width(&self, column: i32) -> Result { + if !is_valid_column_number(column) { + return Err(format!("Column number '{column}' is not valid.")); + } + + let cols = &self.cols; + for col in cols { + let min = col.min; + let max = col.max; + if column >= min && column <= max { + if col.custom_width { + return Ok(col.width * constants::COLUMN_WIDTH_FACTOR); + } else { + break; + } + } + } + Ok(constants::DEFAULT_COLUMN_WIDTH) + } + + // Returns non empty cells in a column + pub fn column_cell_references(&self, column: i32) -> Result, String> { + let mut column_cell_references: Vec = Vec::new(); + if !is_valid_column_number(column) { + return Err(format!("Column number '{column}' is not valid.")); + } + + for row in self.sheet_data.keys() { + if self.cell(*row, column).is_some() { + column_cell_references.push(CellReferenceIndex { + sheet: self.sheet_id, + row: *row, + column, + }); + } + } + Ok(column_cell_references) + } + + /// Returns the height of a row in pixels + pub fn row_height(&self, row: i32) -> Result { + if !is_valid_row(row) { + return Err(format!("Row number '{row}' is not valid.")); + } + + let rows = &self.rows; + for r in rows { + if r.r == row { + return Ok(r.height * constants::ROW_HEIGHT_FACTOR); + } + } + Ok(constants::DEFAULT_ROW_HEIGHT) + } + + /// Returns non empty cells in a row + pub fn row_cell_references(&self, row: i32) -> Result, String> { + let mut row_cell_references: Vec = Vec::new(); + if !is_valid_row(row) { + return Err(format!("Row number '{row}' is not valid.")); + } + + for (row_index, columns) in self.sheet_data.iter() { + if *row_index == row { + for column in columns.keys() { + row_cell_references.push(CellReferenceIndex { + sheet: self.sheet_id, + row, + column: *column, + }) + } + } + } + Ok(row_cell_references) + } + + /// Returns non empty cells + pub fn cell_references(&self) -> Result, String> { + let mut cell_references: Vec = Vec::new(); + for (row, columns) in self.sheet_data.iter() { + for column in columns.keys() { + cell_references.push(CellReferenceIndex { + sheet: self.sheet_id, + row: *row, + column: *column, + }) + } + } + Ok(cell_references) + } + + /// Calculates dimension of the sheet. This function isn't cheap to calculate. + pub fn dimension(&self) -> WorksheetDimension { + // FIXME: It's probably better to just track the size as operations happen. + if self.sheet_data.is_empty() { + return WorksheetDimension { + min_row: 1, + max_row: 1, + min_column: 1, + max_column: 1, + }; + } + + let mut row_range: Option<(i32, i32)> = None; + let mut column_range: Option<(i32, i32)> = None; + + for (row_index, columns) in &self.sheet_data { + row_range = if let Some((current_min, current_max)) = row_range { + Some((current_min.min(*row_index), current_max.max(*row_index))) + } else { + Some((*row_index, *row_index)) + }; + + for column_index in columns.keys() { + column_range = if let Some((current_min, current_max)) = column_range { + Some(( + current_min.min(*column_index), + current_max.max(*column_index), + )) + } else { + Some((*column_index, *column_index)) + } + } + } + + let dimension = if let Some((min_row, max_row)) = row_range { + if let Some((min_column, max_column)) = column_range { + Some(WorksheetDimension { + min_row, + min_column, + max_row, + max_column, + }) + } else { + None + } + } else { + None + }; + + dimension.unwrap_or(WorksheetDimension { + min_row: 1, + max_row: 1, + min_column: 1, + max_column: 1, + }) + } + + /// Returns true if cell is completely empty. + /// Cell with formula that evaluates to empty string is not considered empty. + pub fn is_empty_cell(&self, row: i32, column: i32) -> Result { + if !is_valid_column_number(column) || !is_valid_row(row) { + return Err("Row or column is outside valid range.".to_string()); + } + + let is_empty = if let Some(data_row) = self.sheet_data.get(&row) { + if let Some(cell) = data_row.get(&column) { + matches!(cell, Cell::EmptyCell { .. }) + } else { + true + } + } else { + true + }; + + Ok(is_empty) + } + + /// It provides convenient method for user navigation in the spreadsheet by jumping to edges. + /// Spreadsheet engines usually allow this method of navigation by using CTRL+arrows. + /// Behaviour summary: + /// - if starting cell is empty then find first non empty cell in given direction + /// - if starting cell is not empty, and neighbour in given direction is empty, then find + /// first non empty cell in given direction + /// - if starting cell is not empty, and neighbour in given direction is also not empty, then + /// find last non empty cell in given direction + pub fn navigate_to_edge_in_direction( + &self, + row: i32, + column: i32, + direction: NavigationDirection, + ) -> Result<(i32, i32), String> { + if !is_valid_column_number(column) || !is_valid_row(row) { + return Err("Row or column is outside valid range.".to_string()); + } + + let start_cell = (row, column); + let neighbour_cell = if let Some(cell) = step_in_direction(start_cell, direction) { + cell + } else { + return Ok((start_cell.0, start_cell.1)); + }; + + if self.is_empty_cell(start_cell.0, start_cell.1)? { + // Find first non-empty cell or move to the end. + let found_cells = walk_in_direction(start_cell, direction, |(row, column)| { + Ok(!self.is_empty_cell(row, column)?) + })?; + Ok(match found_cells.found_cell { + Some(cell) => cell, + None => found_cells.previous_cell, + }) + } else { + // Neighbour cell is empty => find FIRST that is NOT empty + // Neighbour cell is not empty => find LAST that is NOT empty in sequence + if self.is_empty_cell(neighbour_cell.0, neighbour_cell.1)? { + let found_cells = walk_in_direction(start_cell, direction, |(row, column)| { + Ok(!self.is_empty_cell(row, column)?) + })?; + Ok(match found_cells.found_cell { + Some(cell) => cell, + None => found_cells.previous_cell, + }) + } else { + let found_cells = walk_in_direction(start_cell, direction, |(row, column)| { + self.is_empty_cell(row, column) + })?; + Ok(found_cells.previous_cell) + } + } + } +} + +struct WalkFoundCells { + /// If cell is found, it contains coordinates of the cell, otherwise None + found_cell: Option<(i32, i32)>, + /// Previous cell in chain relative to `found_cell`. + /// If `found_cell` is None then it's last considered cell. + previous_cell: (i32, i32), +} + +/// Walks in direction until condition is met or boundary reached. +/// Returns tuple `(current_cell, previous_cell)`. `current_cell` is either None or passes predicate +fn walk_in_direction( + start_cell: (i32, i32), + direction: NavigationDirection, + predicate: F, +) -> Result +where + F: Fn((i32, i32)) -> Result, +{ + let mut previous_cell = start_cell; + let mut current_cell = step_in_direction(start_cell, direction); + while let Some(cell) = current_cell { + if !predicate((cell.0, cell.1))? { + previous_cell = cell; + current_cell = step_in_direction(cell, direction); + } else { + break; + } + } + Ok(WalkFoundCells { + found_cell: current_cell, + previous_cell, + }) +} + +/// Returns coordinate of cell in given direction from given cell. +/// Returns `None` if steps over the edge. +fn step_in_direction( + (row, column): (i32, i32), + direction: NavigationDirection, +) -> Option<(i32, i32)> { + if (row == 1 && direction == NavigationDirection::Up) + || (row == LAST_ROW && direction == NavigationDirection::Down) + || (column == 1 && direction == NavigationDirection::Left) + || (column == LAST_COLUMN && direction == NavigationDirection::Right) + { + return None; + } + + Some(match direction { + NavigationDirection::Left => (row, column - 1), + NavigationDirection::Right => (row, column + 1), + NavigationDirection::Up => (row - 1, column), + NavigationDirection::Down => (row + 1, column), + }) +} diff --git a/generate_locale/.gitignore b/generate_locale/.gitignore new file mode 100644 index 0000000..c8b241f --- /dev/null +++ b/generate_locale/.gitignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/generate_locale/Cargo.lock b/generate_locale/Cargo.lock new file mode 100644 index 0000000..a972d74 --- /dev/null +++ b/generate_locale/Cargo.lock @@ -0,0 +1,283 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "clap" +version = "3.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "generate_locale" +version = "0.1.0" +dependencies = [ + "clap", + "serde", + "serde_json", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" + +[[package]] +name = "libc" +version = "0.2.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" + +[[package]] +name = "once_cell" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" + +[[package]] +name = "os_str_bytes" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "serde" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.144" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" + +[[package]] +name = "unicode-ident" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/generate_locale/Cargo.toml b/generate_locale/Cargo.toml new file mode 100644 index 0000000..62cabe1 --- /dev/null +++ b/generate_locale/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "generate_locale" +version = "0.1.0" +authors = ["Nicolás Hatcher "] +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" +clap = { version = "3.2.22", features = ["derive"] } diff --git a/generate_locale/README.md b/generate_locale/README.md new file mode 100644 index 0000000..12c496d --- /dev/null +++ b/generate_locale/README.md @@ -0,0 +1,26 @@ +# Generate Locale + +This is a small util to generate locales for IronCalc. + +To build + +```bash +$ cargo build --release +``` + +To run it you will need a checkout of the [CLDR json repo](https://github.com/unicode-org/cldr-json) + +```bash +$ generate_locale --locales= --cldr-dir= --output= +``` + +Further information: + +http://cldr.unicode.org/ + + +## TODO: + +* Add tests +* Checkout whole folder? + diff --git a/generate_locale/locales_list.json b/generate_locale/locales_list.json new file mode 100644 index 0000000..4544157 --- /dev/null +++ b/generate_locale/locales_list.json @@ -0,0 +1,3 @@ +[ + "en", "en-GB", "de", "es" +] \ No newline at end of file diff --git a/generate_locale/src/constants.rs b/generate_locale/src/constants.rs new file mode 100644 index 0000000..f4dbc66 --- /dev/null +++ b/generate_locale/src/constants.rs @@ -0,0 +1,78 @@ +use serde::{Deserialize, Serialize}; + +pub const LOCAL_TYPE: &str = "modern"; // or "full" + +#[derive(Serialize, Deserialize)] +pub struct Locale { + pub dates: Dates, + pub numbers: NumbersProperties, + pub currency: Currency +} + +#[derive(Serialize, Deserialize)] +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)] +pub struct Dates { + pub day_names: Vec, + pub day_names_short: Vec, + pub months: Vec, + pub months_short: Vec, + pub months_letter: Vec, +} + +#[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, + #[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, + #[serde(rename = "accounting-noCurrency")] + pub accounting_no_currency: String, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct DecimalFormats { + pub standard: String, +} diff --git a/generate_locale/src/dates.rs b/generate_locale/src/dates.rs new file mode 100644 index 0000000..0229ced --- /dev/null +++ b/generate_locale/src/dates.rs @@ -0,0 +1,83 @@ +use std::collections::HashMap; +use std::fs; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::constants::{Dates, LOCAL_TYPE}; + +#[derive(Serialize, Deserialize)] +struct CaGCalendarsFormat { + format: HashMap>, +} +#[derive(Serialize, Deserialize)] +struct CaGCalendarsII { + months: CaGCalendarsFormat, + days: CaGCalendarsFormat, +} +#[derive(Serialize, Deserialize)] +struct CaGCalendarsI { + gregorian: CaGCalendarsII, +} + +#[derive(Serialize, Deserialize)] +struct CaGCalendars { + calendars: CaGCalendarsI, +} + +#[derive(Serialize, Deserialize)] +struct CaGId { + identity: Value, + dates: CaGCalendars, +} + +#[derive(Serialize, Deserialize)] +struct CaGregorian { + main: HashMap, +} + +pub fn get_dates_formatting(cldr_dir: &str, locale_id: &str) -> Result { + let calendar_file = format!( + "{}cldr-json/cldr-dates-{}/main/{}/ca-gregorian.json", + cldr_dir, LOCAL_TYPE, locale_id + ); + + let contents = + fs::read_to_string(calendar_file).or(Err("Failed reading 'ca-gregorian' file"))?; + let ca_gregorian: CaGregorian = + serde_json::from_str(&contents).or(Err("Failed parsing 'ca-gregorian' file"))?; + let gregorian = &ca_gregorian.main[locale_id].dates.calendars.gregorian; + // See: http://cldr.unicode.org/translation/date-time-1/date-time-patterns + // for the difference between stand-alone and format. We will use only the format mode + let months_format = &gregorian.months.format; + let days_format = &gregorian.days.format; + let mut day_names = vec![]; + let mut day_names_short = vec![]; + + let mut months = vec![]; + let mut months_short = vec![]; + let mut months_letter = vec![]; + + let month_index = vec![ + "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", + ]; + for index in month_index { + months_letter.push(months_format["narrow"][index].to_owned()); + months_short.push(months_format["abbreviated"][index].to_owned()); + months.push(months_format["wide"][index].to_owned()); + } + + let day_index = vec!["sun", "mon", "tue", "wed", "thu", "fri", "sat"]; + for day in day_index { + day_names_short.push(days_format["abbreviated"][day].to_owned()); + day_names.push(days_format["wide"][day].to_owned()); + } + + Ok(Dates { + day_names, + day_names_short, + months, + months_short, + months_letter, + }) +} diff --git a/generate_locale/src/main.rs b/generate_locale/src/main.rs new file mode 100644 index 0000000..b6fd6bf --- /dev/null +++ b/generate_locale/src/main.rs @@ -0,0 +1,61 @@ +use std::fs; +use std::{collections::HashMap, io::Write, path::PathBuf}; + +use constants::{Locale, Currency}; + +use clap::Parser; +use numbers::get_numbers_formatting; + +mod constants; +mod dates; +mod numbers; +mod util; + +use dates::get_dates_formatting; +use util::get_all_locales_id; + +#[derive(Parser)] +#[clap(author, version, about, long_about = None)] +pub struct Opt { + /// File with the list of required locales + #[clap(long, value_parser)] + locales: Option, + + /// Folder with the cldr data + #[clap(long, value_parser)] + cldr_dir: String, + + /// output json file with all locale info + #[clap(long, value_parser)] + output: PathBuf, +} + +fn main() -> Result<(), String> { + let opt = Opt::from_args(); + let cldr_dir = opt.cldr_dir; + let locales_list: Vec = if let Some(locales_path) = opt.locales { + let contents = fs::read_to_string(locales_path).or(Err("Failed reading file"))?; + serde_json::from_str(&contents).or(Err("Failed parsing file"))? + } else { + get_all_locales_id(&cldr_dir) + }; + + let mut locales = HashMap::new(); + + for locale_id in locales_list { + let dates = get_dates_formatting(&cldr_dir, &locale_id)?; + let numbers = get_numbers_formatting(&cldr_dir, &locale_id)?; + // HACK: the currency is not a part of the cldr locale + // We just stick here one and make this adaptable in the calc module for now + let currency = Currency { + iso: "USD".to_string(), + symbol: "$".to_string() + }; + locales.insert(locale_id, Locale { dates, numbers, currency }); + } + + let s = serde_json::to_string(&locales).or(Err("Failed to stringify data"))?; + let mut f = fs::File::create(opt.output).or(Err("Failed to create file"))?; + f.write_all(s.as_bytes()).or(Err("Failed writing"))?; + Ok(()) +} diff --git a/generate_locale/src/numbers.rs b/generate_locale/src/numbers.rs new file mode 100644 index 0000000..fedbdab --- /dev/null +++ b/generate_locale/src/numbers.rs @@ -0,0 +1,65 @@ +use std::collections::HashMap; +use std::fs; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::constants::{NumbersProperties, LOCAL_TYPE}; + +#[derive(Serialize, Deserialize)] +struct CaGCalendarsFormat { + format: HashMap>, +} +#[derive(Serialize, Deserialize)] +struct CaGCalendarsII { + months: CaGCalendarsFormat, + days: CaGCalendarsFormat, +} + +#[derive(Serialize, Deserialize)] +struct NumbersJSONId { + identity: Value, + numbers: NumbersProperties, +} + +#[derive(Serialize, Deserialize)] +struct NumbersJSON { + main: HashMap, +} + +pub fn get_numbers_formatting( + cldr_dir: &str, + locale_id: &str, +) -> Result { + let numbers_file = format!( + "{}cldr-json/cldr-numbers-{}/main/{}/numbers.json", + cldr_dir, LOCAL_TYPE, locale_id + ); + + let contents = fs::read_to_string(numbers_file).or(Err("Failed reading 'numbers.json'"))?; + let numbers_json: &NumbersJSON = + &serde_json::from_str(&contents).or(Err("Failed parsing 'numbers.json' file"))?; + // Grouping is either + // * #,##,##0.### (indian way) + // * #,##0.### (standard) + // * 0.###### (posix) + // anything else is an error + let grouping_str = &numbers_json.main[locale_id] + .numbers + .decimal_formats + .standard; + let _grouping = if grouping_str == "#,##0.###" { + "standard" + } else if grouping_str == "#,##,##0.###" { + "indian" + } else if grouping_str == "0.######" { + "posix" + } else { + let message = format!( + "Unexpected grouping {} in locale {}", + grouping_str, locale_id + ); + return Err(message); + }; + Ok(numbers_json.main[locale_id].numbers.clone()) +} diff --git a/generate_locale/src/util.rs b/generate_locale/src/util.rs new file mode 100644 index 0000000..efe355e --- /dev/null +++ b/generate_locale/src/util.rs @@ -0,0 +1,26 @@ +use std::fs; + +use crate::constants::LOCAL_TYPE; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +struct AlI { + modern: Vec, + full: Vec, +} +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct AvailableLocales { + available_locales: AlI, +} + +pub fn get_all_locales_id(cldr_dir: &str) -> Vec { + let al_file = format!("{}cldr-json/cldr-core/availableLocales.json", cldr_dir); + let contents = fs::read_to_string(al_file).unwrap(); + let locales: AvailableLocales = serde_json::from_str(&contents).unwrap(); + if LOCAL_TYPE == "modern" { + locales.available_locales.modern + } else { + locales.available_locales.full + } +} diff --git a/xlsx/.gitignore b/xlsx/.gitignore new file mode 100644 index 0000000..c8b241f --- /dev/null +++ b/xlsx/.gitignore @@ -0,0 +1 @@ +target/* \ No newline at end of file diff --git a/xlsx/Cargo.lock b/xlsx/Cargo.lock new file mode 100644 index 0000000..4519a4a --- /dev/null +++ b/xlsx/Cargo.lock @@ -0,0 +1,716 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[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 = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[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 = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[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" +version = "0.1.0" +dependencies = [ + "chrono", + "ironcalc_base", + "itertools", + "roxmltree", + "serde", + "serde_json", + "thiserror", + "uuid", + "zip", +] + +[[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 = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[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 = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", +] + +[[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 = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[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 = "roxmltree" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf7d7b1ea646d380d0e8153158063a6da7efe30ddbf3184042848e3f8a6f671" +dependencies = [ + "xmlparser", +] + +[[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 = "thiserror" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "uuid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ad59a7560b41a70d191093a945f0b87bc1deeda46fb237479708a1d6b6cdfc" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[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 = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[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" + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "zip" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +dependencies = [ + "byteorder", + "bzip2", + "crc32fast", + "flate2", + "thiserror", + "time", +] diff --git a/xlsx/Cargo.toml b/xlsx/Cargo.toml new file mode 100644 index 0000000..8af2c6d --- /dev/null +++ b/xlsx/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "ironcalc" +version = "0.1.0" +authors = ["Nicolás Hatcher "] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +zip = "0.5" +roxmltree = "0.13.0" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +# Uses `../../engine/base` when used locally, and uses +# the inicated version from crates.io when published. +# https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#multiple-locations +ironcalc_base = { path = "../base", version = "0.1.0" } +itertools = "0.10.5" +chrono = "0.4" + +[dev-dependencies] +uuid = { version = "1.2.2", features = ["serde", "v4"] } + +[lib] +name = "ironcalc" +path = "src/lib.rs" + +[[bin]] +name = "test" +path = "src/bin/test.rs" diff --git a/xlsx/README.md b/xlsx/README.md new file mode 100644 index 0000000..6083f4d --- /dev/null +++ b/xlsx/README.md @@ -0,0 +1,17 @@ +# IronCalc + +## 📚 About + +Xlsx importer and exporter for the IronCalc engine. + +## 🚴 Usage + +The command + +``` +cargo build --release +``` + +Will produce a binary: + +- `/target/release/test` you can use to test that IronCalc computes the same results as Excel on a particular file diff --git a/xlsx/documentation/README.md b/xlsx/documentation/README.md new file mode 100644 index 0000000..f8f10aa --- /dev/null +++ b/xlsx/documentation/README.md @@ -0,0 +1,64 @@ +Documentation +============= + +An `xlsx` is a zip file containing a set of folders and `xml` files. The IronCalc json structure mimics the relevant parts of the Excel zip. +Although the xlsx structure is quite complicated, it's essentials regarding the spreadsheet technology are easier to grasp. + +The simplest workbook folder structure might look like this: + +``` +docProps + app.xml + core.xml + +_rels + .rels + +xl + _rels + workbook.xml.rels + theme + theme1.xml + worksheets + sheet1.xml + calcChain.xml + styles.xml + workbook.xml + sharedStrings.xml + +[Content_Types].xml +``` + +Note that more complicated workbooks will have many more files and folders. +For instance charts, pivot tables, comments, tables,... + +The relevant json structure in IronCalc will be: + + +```json +{ + "name": "Workbook1", + "defined_names": [], + "shared_strings": [], + "worksheets": [], + "styles": { + "num_fmts": [], + "fonts": [], + "fills": [], + "borders": [], + "cell_style_xfs": [], + "cell_styles" : [], + "cell_xfs": [] + } +} +``` + +Note that there is not a 1-1 correspondence but there is a close resemblance. + + + +SpreadsheetML +------------- +International standard (Four edition 2016-11-01): ECMA-376, ISO/IEC 29500-1 +* [iso](https://standards.iso.org/ittf/PubliclyAvailableStandards/c071691_ISO_IEC_29500-1_2016.zip) +* [ecma](http://www.ecma-international.org/publications/standards/Ecma-376.htm) \ No newline at end of file diff --git a/xlsx/documentation/sharedStrings.md b/xlsx/documentation/sharedStrings.md new file mode 100644 index 0000000..30818f4 --- /dev/null +++ b/xlsx/documentation/sharedStrings.md @@ -0,0 +1,67 @@ +Shared Strings +============== + +In Excel the type of a cell that contains a string can be of one of three cases: +(see section 18.18.11 ST_CellType (Cell Type)) + +* 's' (A shared string) +* 'str' (A formula string) +* 'inlineStr' (An inline string) + +This file holds a list of the shared strings. The following example contains two strings: + +* Cell A1 +* Cell A2 + +The second contains some internal formatting that in IronCalc is lost. + +```xml + + + + + Cell A1 + + + + + + + + + + + Cell + + + + + + + + + + + + + + + + + + + + + A2 + + + +``` + +This will result in IronCalc in `shared_strings: ["Cell A1", "Cell A2"]`. + +Note that the formatting we are loosing is different formatting within a cell. We can still format and style the full contents of a cell. + +In this example there are two strings (`uniqueCount=2`) in the list but those strings are present in 6 cell across the workbook (`count=6`). Those parameters are not kept in IronCalc. + +Another issue (a corner case) we will have in IronCalc is that we might end have repeated shared string in the list if the original Excel file has the same content is two cells with different formatting. That will mean that we end up using more memory than we need to but will not result in an error. \ No newline at end of file diff --git a/xlsx/documentation/workbook.md b/xlsx/documentation/workbook.md new file mode 100644 index 0000000..e63a9f8 --- /dev/null +++ b/xlsx/documentation/workbook.md @@ -0,0 +1,68 @@ +workbook.xlm: worksheets, define names and relationships +======================================================== + +The most important thing we will find in `workbook.xml` is a list of sheets and a list of defined names + + +For example the list of sheets might be something like: + +```xml + + + + + + + + + + + +``` + +The order is the order they will appear in the workbook. `sheetId` identifies the sheet and does not change if we reorder the sheets. + +This example has three defined names. Those that have a `localSheetId` attribute are scoped to a sheet. Note that the `localSheetId` refers to the order in the sheet list (0-indexed) and not the `sheetId`. + +A sheet can hve one of three states: + +* visible +* hidden +* very hidden + +To understand what file belongs to each sheet we have to do a bit of work. we will also understand the sheet "`Chart1`" is not a spreadsheet that we what to import but a "chart" sheet. +This is where the relationships file comes in (xl/_rels/workbook.xml.rels). In our case it is something like: + +```xml + + + + + + + + + + + + + + + + +``` + +The `r:id` attribute in the sheet list links the sheet to this relationships file. For instance the sheet "shared" has an relationships id "rIdr5" that links to the file "`worksheets/sheet4.xml`" that is of type "worksheet". +Note that the second sheet "Chart" has id `rId2` that links to the file "`chartsheets/sheet1.xml`" and is of type "chartsheet". In IronCalc we ignore those sheets. + +```xml + + shared!$G$5 + Sheet1!$I$6 + Second!$B$1:$B$9 + Sheet1!$A$16:$A$18 + Sheet1!$C$14 + +``` + +So `answer2` is scoped to `Sheet1` and `answer` is scoped to `shared`. \ No newline at end of file diff --git a/xlsx/documentation/worksheets.md b/xlsx/documentation/worksheets.md new file mode 100644 index 0000000..2369bc5 --- /dev/null +++ b/xlsx/documentation/worksheets.md @@ -0,0 +1,61 @@ +Worksheets +========== + +All the sheets in the workbook are in `xl/worksheets/sheet*.xlm` and represent the single most important files for us. + +An example, ignoring for now the most important part `sheetData` + +```xml + + + + + + + + + + + + + + +... + + + + + + + + + +``` + +For this file we can read the columns information, the sheet data and merged cells. +For now everything else is ignored and lost in IronCalc. + +The sheetData is organized by rows: + +```xml + + + + 0 + + + 1 + + + + + 222 + + + 2 + + + +``` + +In IronCalc the `spans` (an Excel optimization) is not used. The `dyDescent` property is also ignore in `IronCalc`, \ No newline at end of file diff --git a/xlsx/src/bin/test.rs b/xlsx/src/bin/test.rs new file mode 100644 index 0000000..2b094fa --- /dev/null +++ b/xlsx/src/bin/test.rs @@ -0,0 +1,34 @@ +//! Tests an Excel xlsx file. +//! Returns a list of differences in json format. +//! Saves an IronCalc version +//! This is primary for QA internal testing and will be superseded by an official +//! IronCalc CLI. +//! +//! Usage: test file.xlsx + +use std::path; + +use ironcalc::{compare::test_file, export::save_to_xlsx, import::load_model_from_xlsx}; + +fn main() { + let args: Vec<_> = std::env::args().collect(); + if args.len() != 2 { + panic!("Usage: {} ", args[0]); + } + // first test the file + let file_name = &args[1]; + println!("Testing file: {file_name}"); + if let Err(message) = test_file(file_name) { + println!("{}", message); + panic!("Model was evaluated inconsistently with XLSX data.") + } + + // save a copy my_xlsx_file.xlsx => my_xlsx_file.output.xlsx + let file_path = path::Path::new(file_name); + let base_name = file_path.file_stem().unwrap().to_str().unwrap(); + let output_file_name = &format!("{base_name}.output.xlsx"); + let mut model = load_model_from_xlsx(file_name, "en", "UTC").unwrap(); + model.evaluate(); + println!("Saving result as: {output_file_name}. Please open with Excel and test."); + save_to_xlsx(&model, output_file_name).unwrap(); +} diff --git a/xlsx/src/compare.rs b/xlsx/src/compare.rs new file mode 100644 index 0000000..a83abe2 --- /dev/null +++ b/xlsx/src/compare.rs @@ -0,0 +1,206 @@ +use std::path::Path; + +use ironcalc_base::cell::CellValue; +use ironcalc_base::types::*; +use ironcalc_base::{expressions::utils::number_to_column, model::Model}; + +use crate::export::save_to_xlsx; +use crate::import::load_model_from_xlsx; + +pub struct CompareError { + message: String, +} + +type CompareResult = std::result::Result; + +pub struct Diff { + pub sheet_name: String, + pub row: i32, + pub column: i32, + pub value1: Cell, + pub value2: Cell, + pub reason: String, +} + +// TODO use f64::EPSILON +const EPS: f64 = 5e-8; +// const EPS: f64 = f64::EPSILON; + +fn numbers_are_close(x: f64, y: f64, eps: f64) -> bool { + let norm = (x * x + y * y).sqrt(); + if norm == 0.0 { + return true; + } + let d = f64::abs(x - y); + if d < eps { + return true; + } + d / norm < eps +} +/// Compares two Models in the internal representation and returns a list of differences +pub fn compare(model1: &Model, model2: &Model) -> CompareResult> { + let ws1 = model1.workbook.get_worksheet_names(); + let ws2 = model2.workbook.get_worksheet_names(); + if ws1.len() != ws2.len() { + return Err(CompareError { + message: "Different number of sheets".to_string(), + }); + } + let eps = if let Ok(CellValue::Number(v)) = model1.get_cell_value_by_ref("METADATA!A1") { + v + } else { + EPS + }; + let mut diffs = Vec::new(); + let cells = model1.get_all_cells(); + for cell in cells { + let sheet = cell.index; + let row = cell.row; + let column = cell.column; + let cell1 = &model1 + .workbook + .worksheet(sheet) + .unwrap() + .cell(row, column) + .cloned() + .unwrap_or_default(); + let cell2 = &model2 + .workbook + .worksheet(sheet) + .unwrap() + .cell(row, column) + .cloned() + .unwrap_or_default(); + match (cell1, cell2) { + (Cell::EmptyCell { .. }, Cell::EmptyCell { .. }) => {} + (Cell::NumberCell { .. }, Cell::NumberCell { .. }) => {} + (Cell::BooleanCell { .. }, Cell::BooleanCell { .. }) => {} + (Cell::ErrorCell { .. }, Cell::ErrorCell { .. }) => {} + (Cell::SharedString { .. }, Cell::SharedString { .. }) => {} + ( + Cell::CellFormulaNumber { v: value1, .. }, + Cell::CellFormulaNumber { v: value2, .. }, + ) => { + if !numbers_are_close(*value1, *value2, eps) { + diffs.push(Diff { + sheet_name: ws1[cell.index as usize].clone(), + row, + column, + value1: cell1.clone(), + value2: cell2.clone(), + reason: "Numbers are different".to_string(), + }); + } + } + ( + Cell::CellFormulaString { v: value1, .. }, + Cell::CellFormulaString { v: value2, .. }, + ) => { + // FIXME: We should compare the actual value, not just the index + if value1 != value2 { + diffs.push(Diff { + sheet_name: ws1[cell.index as usize].clone(), + row, + column, + value1: cell1.clone(), + value2: cell2.clone(), + reason: "Strings are different".to_string(), + }); + } + } + ( + Cell::CellFormulaBoolean { v: value1, .. }, + Cell::CellFormulaBoolean { v: value2, .. }, + ) => { + // FIXME: We should compare the actual value, not just the index + if value1 != value2 { + diffs.push(Diff { + sheet_name: ws1[cell.index as usize].clone(), + row, + column, + value1: cell1.clone(), + value2: cell2.clone(), + reason: "Booleans are different".to_string(), + }); + } + } + ( + Cell::CellFormulaError { ei: index1, .. }, + Cell::CellFormulaError { ei: index2, .. }, + ) => { + // FIXME: We should compare the actual value, not just the index + if index1 != index2 { + diffs.push(Diff { + sheet_name: ws1[cell.index as usize].clone(), + row, + column, + value1: cell1.clone(), + value2: cell2.clone(), + reason: "Errors are different".to_string(), + }); + } + } + (_, _) => { + diffs.push(Diff { + sheet_name: ws1[cell.index as usize].clone(), + row, + column, + value1: cell1.clone(), + value2: cell2.clone(), + reason: "Types are different".to_string(), + }); + } + } + } + Ok(diffs) +} + +pub(crate) fn compare_models(m1: &Model, m2: &Model) -> Result<(), String> { + match compare(m1, m2) { + Ok(diffs) => { + if diffs.is_empty() { + Ok(()) + } else { + let mut message = "".to_string(); + for diff in diffs { + message = format!( + "{}\n.Diff: {}!{}{}, value1: {}, value2 {}\n {}", + message, + diff.sheet_name, + number_to_column(diff.column).unwrap(), + diff.row, + serde_json::to_string(&diff.value1).unwrap(), + serde_json::to_string(&diff.value2).unwrap(), + diff.reason + ); + } + Err(format!("Models are different: {}", message)) + } + } + Err(r) => Err(format!("Models are different: {}", r.message)), + } +} + +/// Tests that file in file_path produces the same results in Excel and in IronCalc. +pub fn test_file(file_path: &str) -> Result<(), String> { + let model1 = load_model_from_xlsx(file_path, "en", "UTC").unwrap(); + let mut model2 = load_model_from_xlsx(file_path, "en", "UTC").unwrap(); + model2.evaluate(); + compare_models(&model1, &model2) +} + +/// Tests that file in file_path can be converted to xlsx and read again +pub fn test_load_and_saving(file_path: &str, temp_dir_name: &Path) -> Result<(), String> { + let model1 = load_model_from_xlsx(file_path, "en", "UTC").unwrap(); + + let base_name = Path::new(file_path).file_name().unwrap().to_str().unwrap(); + + let temp_path_buff = temp_dir_name.join(base_name); + let temp_file_path = &format!("{}.xlsx", temp_path_buff.to_str().unwrap()); + // test can save + save_to_xlsx(&model1, temp_file_path).unwrap(); + // test can open + let mut model2 = load_model_from_xlsx(temp_file_path, "en", "UTC").unwrap(); + model2.evaluate(); + compare_models(&model1, &model2) +} diff --git a/xlsx/src/error.rs b/xlsx/src/error.rs new file mode 100644 index 0000000..451a04c --- /dev/null +++ b/xlsx/src/error.rs @@ -0,0 +1,89 @@ +use std::io; +use std::num::{ParseFloatError, ParseIntError}; +use thiserror::Error; +use zip::result::ZipError; + +#[derive(Error, Debug, PartialEq, Eq)] +pub enum XlsxError { + #[error("I/O Error: {0}")] + IO(String), + #[error("Zip Error: {0}")] + Zip(String), + #[error("XML Error: {0}")] + Xml(String), + #[error("{0}")] + Workbook(String), + #[error("Evaluation Error: {}", .0.join("; "))] + Evaluation(Vec), + #[error("Comparison Error: {0}")] + Comparison(String), + #[error("Not Implemented Error: {0}")] + NotImplemented(String), +} + +impl From for XlsxError { + fn from(error: io::Error) -> Self { + XlsxError::IO(error.to_string()) + } +} + +impl From for XlsxError { + fn from(error: ZipError) -> Self { + XlsxError::Zip(error.to_string()) + } +} + +impl From for XlsxError { + fn from(error: ParseIntError) -> Self { + XlsxError::Xml(error.to_string()) + } +} + +impl From for XlsxError { + fn from(error: ParseFloatError) -> Self { + XlsxError::Xml(error.to_string()) + } +} + +impl From for XlsxError { + fn from(error: roxmltree::Error) -> Self { + XlsxError::Xml(error.to_string()) + } +} + +impl XlsxError { + pub fn user_message(&self) -> String { + match &self { + XlsxError::IO(_) | XlsxError::Workbook(_) => self.to_string(), + XlsxError::Zip(_) | XlsxError::Xml(_) => { + "IronCalc can only open workbooks created by Microsoft Excel. \ + Can you open this file with Excel, save it to a new file, \ + and then open that new file with IronCalc? If you've already tried this, \ + then send this workbook to support@ironcalc.com and our engineering team \ + will work with you to fix the issue." + .to_string() + } + XlsxError::NotImplemented(error) => format!( + "IronCalc cannot open this workbook due to the following unsupported features: \ + {error}. You can either re-implement these parts of your workbook using features \ + supported by IronCalc, or you can send this workbook to support@ironcalc.com \ + and our engineering team will work with you to fix the issue.", + ), + XlsxError::Evaluation(errors) => format!( + "IronCalc could not evaluate this workbook without errors. This may indicate a bug or missing feature \ + in the IronCalc spreadsheet calculation engine. Please contact support@ironcalc.com, share the entirety \ + of this error message and the relevant workbook, and we will work with you to resolve the issue. \ + Detailed error message:\n{}", + errors.join("\n") + ), + XlsxError::Comparison(error) => format!( + "IronCalc produces different results when evaluating the workbook \ + than those already present in the workbook. This may indicate a bug or missing \ + feature in the IronCalc spreadsheet calculation engine. Please contact \ + support@ironcalc.com, share the entirety of this error message and the relevant \ + workbook, and we will work with you to resolve the issue. \ + Detailed error message:\n{error}" + ), + } + } +} diff --git a/xlsx/src/export/_rels.rs b/xlsx/src/export/_rels.rs new file mode 100644 index 0000000..fecb385 --- /dev/null +++ b/xlsx/src/export/_rels.rs @@ -0,0 +1,6 @@ +use ironcalc_base::types::Workbook; + +pub(crate) fn get_dot_rels(_: &Workbook) -> String { + r#" +"#.to_owned() +} diff --git a/xlsx/src/export/doc_props.rs b/xlsx/src/export/doc_props.rs new file mode 100644 index 0000000..4726df0 --- /dev/null +++ b/xlsx/src/export/doc_props.rs @@ -0,0 +1,69 @@ +use chrono::NaiveDateTime; +use ironcalc_base::{ + new_empty::{APPLICATION, APP_VERSION, IRONCALC_USER}, + types::Workbook, +}; + +use crate::error::XlsxError; + +// Application-Defined File Properties part +pub(crate) fn get_app_xml(_: &Workbook) -> String { + // contains application name and version + + // The next few are not needed: + // security. It is password protected (not implemented) + // Scale + // Titles of parts + + format!( + " +\ + {}\ + {}\ +", + APPLICATION, APP_VERSION + ) +} + +// Core File Properties part +pub(crate) fn get_core_xml(workbook: &Workbook, milliseconds: i64) -> Result { + // contains the name of the creator, last modified and date + let metadata = &workbook.metadata; + let creator = metadata.creator.to_string(); + let last_modified_by = IRONCALC_USER.to_string(); + let created = metadata.created.to_string(); + // FIXME add now + + let seconds = milliseconds / 1000; + let dt = match NaiveDateTime::from_timestamp_opt(seconds, 0) { + Some(s) => s, + None => { + return Err(XlsxError::Xml(format!( + "Invalid timestamp: {}", + milliseconds + ))) + } + }; + let last_modified = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(); + Ok(format!( + " + \ +\ +{}\ +\ +\ +{}\ +\ +{}\ +{}\ +\ +\ +", + creator, last_modified_by, created, last_modified + )) +} diff --git a/xlsx/src/export/escape.rs b/xlsx/src/export/escape.rs new file mode 100644 index 0000000..cf051e7 --- /dev/null +++ b/xlsx/src/export/escape.rs @@ -0,0 +1,99 @@ +// Taken from : + +// https://docs.rs/xml-rs/latest/src/xml/escape.rs.html#1-125 + +//! Contains functions for performing XML special characters escaping. + +use std::borrow::Cow; + +enum Value { + Char(char), + Str(&'static str), +} + +fn escape_char(c: char) -> Value { + match c { + '<' => Value::Str("<"), + '>' => Value::Str(">"), + '"' => Value::Str("""), + '\'' => Value::Str("'"), + '&' => Value::Str("&"), + '\n' => Value::Str(" "), + '\r' => Value::Str(" "), + _ => Value::Char(c), + } +} + +enum Process<'a> { + Borrowed(&'a str), + Owned(String), +} + +impl<'a> Process<'a> { + fn process(&mut self, (i, next): (usize, Value)) { + match next { + Value::Str(s) => match *self { + Process::Owned(ref mut o) => o.push_str(s), + Process::Borrowed(b) => { + let mut r = String::with_capacity(b.len() + s.len()); + r.push_str(&b[..i]); + r.push_str(s); + *self = Process::Owned(r); + } + }, + Value::Char(c) => match *self { + Process::Borrowed(_) => {} + Process::Owned(ref mut o) => o.push(c), + }, + } + } + + fn into_result(self) -> Cow<'a, str> { + match self { + Process::Borrowed(b) => Cow::Borrowed(b), + Process::Owned(o) => Cow::Owned(o), + } + } +} + +impl<'a> Extend<(usize, Value)> for Process<'a> { + fn extend>(&mut self, it: I) { + for v in it.into_iter() { + self.process(v); + } + } +} + +/// Performs escaping of common XML characters inside an attribute value. +/// +/// This function replaces several important markup characters with their +/// entity equivalents: +/// +/// * `<` → `<` +/// * `>` → `>` +/// * `"` → `"` +/// * `'` → `'` +/// * `&` → `&` +/// +/// The resulting string is safe to use inside XML attribute values. +/// +/// Does not perform allocations if the given string does not contain escapable characters. +pub fn escape_xml(s: &str) -> Cow { + let mut p = Process::Borrowed(s); + p.extend(s.char_indices().map(|(ind, c)| (ind, escape_char(c)))); + p.into_result() +} + +// A simpler function that allocates memory for each replacement +// fn escape_xml(value: &str) -> String { +// value +// .replace('&', "&") +// .replace('<', "<") +// .replace('>', ">") +// .replace('"', """) +// .replace('\'', "'") +// } + +// See also: +// https://docs.rs/shell-escape/0.1.5/src/shell_escape/lib.rs.html#17-23 +// https://aaronerhardt.github.io/docs/relm4/src/quick_xml/escapei.rs.html#69-106 diff --git a/xlsx/src/export/mod.rs b/xlsx/src/export/mod.rs new file mode 100644 index 0000000..77ff3fe --- /dev/null +++ b/xlsx/src/export/mod.rs @@ -0,0 +1,138 @@ +mod _rels; +mod doc_props; +mod escape; +mod shared_strings; +mod styles; +mod workbook; +mod workbook_xml_rels; +mod worksheets; +mod xml_constants; + +use std::io::BufWriter; +use std::{ + fs, + io::{Seek, Write}, +}; + +use ironcalc_base::expressions::utils::number_to_column; +use ironcalc_base::model::{get_milliseconds_since_epoch, Model}; +use ironcalc_base::types::Workbook; + +use self::xml_constants::XML_DECLARATION; + +use crate::error::XlsxError; + +#[cfg(test)] +mod test; + +fn get_content_types_xml(workbook: &Workbook) -> String { + // A list of all files in the zip + let mut content = vec![ + r#""#.to_string(), + r#""#.to_string(), + r#""#.to_string(), + r#""#.to_string(), + ]; + for worksheet in 0..workbook.worksheets.len() { + let sheet = format!( + r#""#, + worksheet + 1 + ); + content.push(sheet); + } + // we skip the theme and calcChain + // r#""#, + // r#""#, + content.extend([ + r#""#.to_string(), + r#""#.to_string(), + r#""#.to_string(), + r#""#.to_string(), + r#""#.to_string(), + ]); + format!("{XML_DECLARATION}\n{}", content.join("")) +} + +/// Exports a model to an xlsx file +pub fn save_to_xlsx(model: &Model, file_name: &str) -> Result<(), XlsxError> { + let file_path = std::path::Path::new(&file_name); + if file_path.exists() { + return Err(XlsxError::IO(format!("file {} already exists", file_name))); + } + let file = fs::File::create(file_path).unwrap(); + let writer = BufWriter::new(file); + save_xlsx_to_writer(model, writer)?; + + Ok(()) +} + +pub fn save_xlsx_to_writer(model: &Model, writer: W) -> Result { + let workbook = &model.workbook; + let mut zip = zip::ZipWriter::new(writer); + + let options = + zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); + + // root folder + zip.start_file("[Content_Types].xml", options)?; + zip.write_all(get_content_types_xml(workbook).as_bytes())?; + + zip.add_directory("docProps", options)?; + zip.start_file("docProps/app.xml", options)?; + zip.write_all(doc_props::get_app_xml(workbook).as_bytes())?; + zip.start_file("docProps/core.xml", options)?; + let milliseconds = get_milliseconds_since_epoch(); + zip.write_all(doc_props::get_core_xml(workbook, milliseconds)?.as_bytes())?; + + // Package-relationship item + zip.add_directory("_rels", options)?; + zip.start_file("_rels/.rels", options)?; + zip.write_all(_rels::get_dot_rels(workbook).as_bytes())?; + + zip.add_directory("xl", options)?; + zip.start_file("xl/sharedStrings.xml", options)?; + zip.write_all(shared_strings::get_shared_strings_xml(workbook).as_bytes())?; + zip.start_file("xl/styles.xml", options)?; + zip.write_all(styles::get_styles_xml(workbook).as_bytes())?; + zip.start_file("xl/workbook.xml", options)?; + zip.write_all(workbook::get_workbook_xml(workbook).as_bytes())?; + + zip.add_directory("xl/_rels", options)?; + zip.start_file("xl/_rels/workbook.xml.rels", options)?; + zip.write_all(workbook_xml_rels::get_workbook_xml_rels(workbook).as_bytes())?; + + zip.add_directory("xl/worksheets", options)?; + for (sheet_index, worksheet) in workbook.worksheets.iter().enumerate() { + let id = sheet_index + 1; + zip.start_file(&format!("xl/worksheets/sheet{id}.xml"), options)?; + let dimension = model + .workbook + .worksheet(sheet_index as u32) + .unwrap() + .dimension(); + let column_min_str = number_to_column(dimension.min_column).unwrap(); + let column_max_str = number_to_column(dimension.max_column).unwrap(); + let min_row = dimension.min_row; + let max_row = dimension.max_row; + let sheet_dimension_str = &format!("{column_min_str}{min_row}:{column_max_str}{max_row}"); + zip.write_all( + worksheets::get_worksheet_xml( + worksheet, + &model.parsed_formulas[sheet_index], + sheet_dimension_str, + ) + .as_bytes(), + )?; + } + + let writer = zip.finish()?; + Ok(writer) +} + +/// Exports an internal representation of a workbook into an equivalent IronCalc json format +pub fn save_to_json(workbook: Workbook, output: &str) { + let s = serde_json::to_string(&workbook).unwrap(); + let file_path = std::path::Path::new(output); + let mut file = fs::File::create(file_path).unwrap(); + file.write_all(s.as_bytes()).unwrap(); +} diff --git a/xlsx/src/export/shared_strings.rs b/xlsx/src/export/shared_strings.rs new file mode 100644 index 0000000..5f57104 --- /dev/null +++ b/xlsx/src/export/shared_strings.rs @@ -0,0 +1,16 @@ +use ironcalc_base::types::Workbook; + +use super::{escape::escape_xml, xml_constants::XML_DECLARATION}; + +pub(crate) fn get_shared_strings_xml(model: &Workbook) -> String { + let mut shared_strings: Vec = vec![]; + let count = &model.shared_strings.len(); + let unique_count = &model.shared_strings.len(); + for shared_string in &model.shared_strings { + shared_strings.push(format!("{}", escape_xml(shared_string))); + } + format!("{}\n\ + \ + {}\ + ", XML_DECLARATION, shared_strings.join("")) +} diff --git a/xlsx/src/export/styles.rs b/xlsx/src/export/styles.rs new file mode 100644 index 0000000..961d41b --- /dev/null +++ b/xlsx/src/export/styles.rs @@ -0,0 +1,282 @@ +use ironcalc_base::types::{ + Alignment, BorderItem, HorizontalAlignment, Styles, VerticalAlignment, Workbook, +}; + +use super::{escape::escape_xml, xml_constants::XML_DECLARATION}; + +fn get_fonts_xml(styles: &Styles) -> String { + let fonts = &styles.fonts; + let mut fonts_str: Vec = vec![]; + for font in fonts { + let size = format!("", font.sz); + let color = if let Some(some_color) = &font.color { + format!("", some_color.trim_start_matches('#')) + } else { + "".to_string() + }; + let name = format!("", escape_xml(&font.name)); + let bold = if font.b { "" } else { "" }; + let italic = if font.i { "" } else { "" }; + let underline = if font.u { "" } else { "" }; + let strike = if font.strike { "" } else { "" }; + let family = format!("", font.family); + let scheme = format!("", font.scheme); + fonts_str.push(format!( + "\ + {size}\ + {color}\ + {name}\ + {bold}\ + {italic}\ + {underline}\ + {strike}\ + {family}\ + {scheme}\ + " + )); + } + let font_count = fonts.len(); + format!( + "{}", + fonts_str.join("") + ) +} + +fn get_color_xml(color: &Option, name: &str) -> String { + // We blindly append FF at the beginning of these RGB color to make it ARGB + if let Some(some_color) = color { + format!("<{name} rgb=\"FF{}\"/>", some_color.trim_start_matches('#')) + } else { + "".to_string() + } +} + +fn get_fills_xml(styles: &Styles) -> String { + let fills = &styles.fills; + let mut fills_str: Vec = vec![]; + for fill in fills { + let pattern_type = &fill.pattern_type; + let fg_color = get_color_xml(&fill.fg_color, "fgColor"); + let bg_color = get_color_xml(&fill.bg_color, "bgColor"); + fills_str.push(format!( + "{fg_color}{bg_color}" + )); + } + let fill_count = fills.len(); + format!( + "{}", + fills_str.join("") + ) +} + +fn get_border_xml(border: &Option, name: &str) -> String { + if let Some(border_item) = border { + let color = get_color_xml(&border_item.color, "color"); + return format!("<{name} style=\"{}\">{color}", border_item.style); + } + format!("<{name}/>") +} + +fn get_borders_xml(styles: &Styles) -> String { + let borders = &styles.borders; + let mut borders_str: Vec = vec![]; + let border_count = borders.len(); + for border in borders { + // TODO: diagonal_up/diagonal_down? + let border_left = get_border_xml(&border.left, "left"); + let border_right = get_border_xml(&border.right, "right"); + let border_top = get_border_xml(&border.top, "top"); + let border_bottom = get_border_xml(&border.bottom, "bottom"); + let border_diagonal = get_border_xml(&border.diagonal, "diagonal"); + borders_str.push(format!( + "{border_left}{border_right}{border_top}{border_bottom}{border_diagonal}" + )); + } + format!( + "{}", + borders_str.join("") + ) +} + +// +// +// +fn get_cell_number_formats_xml(styles: &Styles) -> String { + let num_fmts = &styles.num_fmts; + let mut num_fmts_str: Vec = vec![]; + let num_fmt_count = num_fmts.len(); + for num_fmt in num_fmts { + let num_fmt_id = num_fmt.num_fmt_id; + let format_code = &num_fmt.format_code; + let format_code = escape_xml(format_code); + num_fmts_str.push(format!( + "" + )); + } + if num_fmt_count == 0 { + return "".to_string(); + } + format!( + "{}", + num_fmts_str.join("") + ) +} + +fn get_alignment(alignment: &Alignment) -> String { + let wrap_text = if alignment.wrap_text { + " wrapText=\"1\"" + } else { + "" + }; + let horizontal = if alignment.horizontal != HorizontalAlignment::default() { + format!(" horizontal=\"{}\"", alignment.horizontal) + } else { + "".to_string() + }; + let vertical = if alignment.vertical != VerticalAlignment::default() { + format!(" vertical=\"{}\"", alignment.vertical) + } else { + "".to_string() + }; + format!("") +} + +fn get_cell_style_xfs_xml(styles: &Styles) -> String { + let cell_style_xfs = &styles.cell_style_xfs; + let mut cell_style_str: Vec = vec![]; + for cell_style_xf in cell_style_xfs { + let border_id = cell_style_xf.border_id; + let fill_id = cell_style_xf.fill_id; + let font_id = cell_style_xf.font_id; + let num_fmt_id = cell_style_xf.num_fmt_id; + let apply_alignment_str = if cell_style_xf.apply_alignment { + r#" applyAlignment="1""# + } else { + "" + }; + let apply_font_str = if cell_style_xf.apply_font { + r#" applyFont="1""# + } else { + "" + }; + let apply_fill_str = if cell_style_xf.apply_fill { + r#" applyFill="1""# + } else { + "" + }; + cell_style_str.push(format!( + "" + )); + } + let style_count = cell_style_xfs.len(); + format!( + "{}", + cell_style_str.join("") + ) +} + +fn get_cell_xfs_xml(styles: &Styles) -> String { + let cell_xfs = &styles.cell_xfs; + let mut cell_xfs_str: Vec = vec![]; + for cell_xf in cell_xfs { + let xf_id = cell_xf.xf_id; + let border_id = cell_xf.border_id; + let fill_id = cell_xf.fill_id; + let font_id = cell_xf.font_id; + let num_fmt_id = cell_xf.num_fmt_id; + let quote_prefix_str = if cell_xf.quote_prefix { + r#" quotePrefix="1""# + } else { + "" + }; + let apply_alignment_str = if cell_xf.apply_alignment { + r#" applyAlignment="1""# + } else { + "" + }; + let apply_font_str = if cell_xf.apply_font { + r#" applyFont="1""# + } else { + "" + }; + let apply_fill_str = if cell_xf.apply_fill { + r#" applyFill="1""# + } else { + "" + }; + let properties = format!( + "xfId=\"{xf_id}\" \ + borderId=\"{border_id}\" \ + fillId=\"{fill_id}\" \ + fontId=\"{font_id}\" \ + numFmtId=\"{num_fmt_id}\"\ + {quote_prefix_str}\ + {apply_alignment_str}\ + {apply_font_str}\ + {apply_fill_str}" + ); + if let Some(alignment) = &cell_xf.alignment { + let alignment = get_alignment(alignment); + cell_xfs_str.push(format!("{alignment}")); + } else { + cell_xfs_str.push(format!("")); + } + } + let style_count = cell_xfs.len(); + format!( + "{}", + cell_xfs_str.join("") + ) +} + +// +fn get_cell_styles_xml(styles: &Styles) -> String { + let cell_styles = &styles.cell_styles; + let mut cell_styles_str: Vec = vec![]; + for cell_style in cell_styles { + let xf_id = cell_style.xf_id; + let name = &cell_style.name; + let name = escape_xml(name); + let builtin_id = cell_style.builtin_id; + cell_styles_str.push(format!( + "" + )); + } + let style_count = cell_styles.len(); + format!( + "{}", + cell_styles_str.join("") + ) +} + +pub(crate) fn get_styles_xml(model: &Workbook) -> String { + let styles = &model.styles; + let fonts = get_fonts_xml(styles); + let fills = get_fills_xml(styles); + let borders = get_borders_xml(styles); + let number_formats = get_cell_number_formats_xml(styles); + let cell_style_xfs = get_cell_style_xfs_xml(styles); + let cell_xfs = get_cell_xfs_xml(styles); + let cell_styles = get_cell_styles_xml(styles); + + format!( + "{XML_DECLARATION} +\ +{number_formats}\ +{fonts}\ +{fills}\ +{borders}\ +{cell_style_xfs}\ +{cell_xfs}\ +{cell_styles}\ +\ +" + ) +} diff --git a/xlsx/src/export/test/mod.rs b/xlsx/src/export/test/mod.rs new file mode 100644 index 0000000..7d02a45 --- /dev/null +++ b/xlsx/src/export/test/mod.rs @@ -0,0 +1,2 @@ +mod test_escape; +mod test_export; diff --git a/xlsx/src/export/test/test_escape.rs b/xlsx/src/export/test/test_escape.rs new file mode 100644 index 0000000..4ca0b39 --- /dev/null +++ b/xlsx/src/export/test/test_escape.rs @@ -0,0 +1,25 @@ +use crate::export::escape::escape_xml; + +#[test] +fn test_escape_xml() { + assert_eq!(escape_xml("all good"), "all good"); + assert_eq!(escape_xml("3 < 4"), "3 < 4"); + assert_eq!(escape_xml("3 > 4"), "3 > 4"); + assert_eq!(escape_xml("3 & 4"), "3 & 4"); + assert_eq!(escape_xml("3 && 4"), "3 && 4"); + assert_eq!(escape_xml("3 \"literal\" 4"), "3 "literal" 4"); + assert_eq!( + escape_xml("I don't 'know'"), + "I don't 'know'" + ); + assert_eq!( + escape_xml("This is <>&\"' say"), + "This is <>&"' say" + ); +} + +// '&' => "&" +// '<' "<") +// '>' => ">" +// '"' => """ +// '\'' => "'" diff --git a/xlsx/src/export/test/test_export.rs b/xlsx/src/export/test/test_export.rs new file mode 100644 index 0000000..f0ba99e --- /dev/null +++ b/xlsx/src/export/test/test_export.rs @@ -0,0 +1,134 @@ +use std::fs; + +use ironcalc_base::model::Model; + +use crate::error::XlsxError; +use crate::{export::save_to_xlsx, import::load_model_from_xlsx}; + +pub fn new_empty_model() -> Model { + Model::new_empty("model", "en", "UTC").unwrap() +} + +#[test] +fn test_values() { + let mut model = new_empty_model(); + // numbers + model.set_user_input(0, 1, 1, "123.456".to_string()); + // strings + model.set_user_input(0, 2, 1, "Hello world!".to_string()); + model.set_user_input(0, 3, 1, "Hello world!".to_string()); + model.set_user_input(0, 4, 1, "你好世界!".to_string()); + // booleans + model.set_user_input(0, 5, 1, "TRUE".to_string()); + model.set_user_input(0, 6, 1, "FALSE".to_string()); + // errors + model.set_user_input(0, 7, 1, "#VALUE!".to_string()); + + // noop + model.evaluate(); + + let temp_file_name = "temp_file_test_values.xlsx"; + save_to_xlsx(&model, temp_file_name).unwrap(); + + let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); + assert_eq!(model.formatted_cell_value(0, 1, 1).unwrap(), "123.456"); + assert_eq!(model.formatted_cell_value(0, 2, 1).unwrap(), "Hello world!"); + assert_eq!(model.formatted_cell_value(0, 3, 1).unwrap(), "Hello world!"); + assert_eq!(model.formatted_cell_value(0, 4, 1).unwrap(), "你好世界!"); + assert_eq!(model.formatted_cell_value(0, 5, 1).unwrap(), "TRUE"); + assert_eq!(model.formatted_cell_value(0, 6, 1).unwrap(), "FALSE"); + assert_eq!(model.formatted_cell_value(0, 7, 1).unwrap(), "#VALUE!"); + + fs::remove_file(temp_file_name).unwrap(); +} + +#[test] +fn test_formulas() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "5.5".to_string()); + model.set_user_input(0, 2, 1, "6.5".to_string()); + model.set_user_input(0, 3, 1, "7.5".to_string()); + + model.set_user_input(0, 1, 2, "=A1*2".to_string()); + model.set_user_input(0, 2, 2, "=A2*2".to_string()); + model.set_user_input(0, 3, 2, "=A3*2".to_string()); + model.set_user_input(0, 4, 2, "=SUM(A1:B3)".to_string()); + + model.evaluate(); + let temp_file_name = "temp_file_test_formulas.xlsx"; + save_to_xlsx(&model, temp_file_name).unwrap(); + + let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); + assert_eq!(model.formatted_cell_value(0, 1, 2).unwrap(), "11"); + assert_eq!(model.formatted_cell_value(0, 2, 2).unwrap(), "13"); + assert_eq!(model.formatted_cell_value(0, 3, 2).unwrap(), "15"); + assert_eq!(model.formatted_cell_value(0, 4, 2).unwrap(), "58.5"); + fs::remove_file(temp_file_name).unwrap(); +} + +#[test] +fn test_sheets() { + let mut model = new_empty_model(); + model.add_sheet("With space").unwrap(); + // xml escaped + model.add_sheet("Tango & Cash").unwrap(); + model.add_sheet("你好世界").unwrap(); + + // noop + model.evaluate(); + + let temp_file_name = "temp_file_test_sheets.xlsx"; + save_to_xlsx(&model, temp_file_name).unwrap(); + + let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); + assert_eq!( + model.workbook.get_worksheet_names(), + vec!["Sheet1", "With space", "Tango & Cash", "你好世界"] + ); + fs::remove_file(temp_file_name).unwrap(); +} + +#[test] +fn test_named_styles() { + let mut model = new_empty_model(); + model.set_user_input(0, 1, 1, "5.5".to_string()); + let mut style = model.get_style_for_cell(0, 1, 1); + style.font.b = true; + style.font.i = true; + assert!(model.set_cell_style(0, 1, 1, &style).is_ok()); + let bold_style_index = model.get_cell_style_index(0, 1, 1); + let e = model + .workbook + .styles + .add_named_cell_style("bold & italics", bold_style_index); + assert!(e.is_ok()); + + // noop + model.evaluate(); + + let temp_file_name = "temp_file_test_named_styles.xlsx"; + save_to_xlsx(&model, temp_file_name).unwrap(); + + let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); + assert!(model + .workbook + .styles + .get_style_index_by_name("bold & italics") + .is_ok()); + fs::remove_file(temp_file_name).unwrap(); +} + +#[test] +fn test_existing_file() { + let file_name = "existing_file.xlsx"; + fs::File::create(file_name).unwrap(); + + assert_eq!( + save_to_xlsx(&new_empty_model(), file_name), + Err(XlsxError::IO( + "file existing_file.xlsx already exists".to_string() + )), + ); + + fs::remove_file(file_name).unwrap(); +} diff --git a/xlsx/src/export/workbook.rs b/xlsx/src/export/workbook.rs new file mode 100644 index 0000000..6ccd6b8 --- /dev/null +++ b/xlsx/src/export/workbook.rs @@ -0,0 +1,91 @@ +//! + +//! A workbook is composed of workbook-level properties and a collection of 1 or more sheets. +//! The workbook part and corresponding properties comprise data +//! used to set application and workbook-level operational state. The workbook also serves to bind all the sheets +//! and child elements into an organized single file. The workbook XML attributes and elements include information +//! about what application last saved the file, where and how the windows of the workbook were positioned, and +//! an enumeration of the worksheets in the workbook. +//! This is the XML for the smallest possible (blank) workbook: +//! +//! +//! +//! +//! +//! +//! +//! Note that this workbook has a single sheet, named Sheet1. An Id for the sheet is required, and a relationship Id +//! pointing to the location of the sheet definition is also required. +//! +//! +//! +//! The most important objet of this part is a collection of all the sheets and all the defined names +//! of the workbook. +//! +//! It also may hold state properties like the selected tab + +//! # bookViews +//! + +use std::collections::HashMap; + +use ironcalc_base::types::{SheetState, Workbook}; + +use super::escape::escape_xml; +use super::xml_constants::XML_DECLARATION; + +pub(crate) fn get_workbook_xml(workbook: &Workbook) -> String { + // sheets + // + let mut sheets_str: Vec = vec![]; + let mut sheet_id_to_sheet_index: HashMap = HashMap::new(); + for (sheet_index, worksheet) in workbook.worksheets.iter().enumerate() { + let name = &worksheet.name; + let name = escape_xml(name); + let sheet_id = worksheet.sheet_id; + let state_str = match &worksheet.state { + SheetState::Visible => "", + SheetState::Hidden => " state=\"hidden\"", + SheetState::VeryHidden => " state=\"veryHidden\"", + }; + + sheets_str.push(format!( + "", + sheet_index + 1 + )); + sheet_id_to_sheet_index.insert(sheet_id, sheet_index as u32); + } + + // defined names + // shared!$G$5 + // Sheet1!$A$16:$A$18 + let mut defined_names_str: Vec = vec![]; + for defined_name in &workbook.defined_names { + let name = &defined_name.name; + let name = escape_xml(name); + let local_sheet_id = if let Some(sheet_id) = defined_name.sheet_id { + // In Excel the localSheetId is actually the index of the sheet. + let excel_local_sheet_id = sheet_id_to_sheet_index.get(&sheet_id).unwrap(); + format!(" localSheetId=\"{excel_local_sheet_id}\"") + } else { + "".to_string() + }; + let formula = escape_xml(&defined_name.formula); + defined_names_str.push(format!( + "{formula}" + )) + } + + let sheets = sheets_str.join(""); + let defined_names = defined_names_str.join(""); + format!("{XML_DECLARATION}\n\ + \ + \ + {sheets}\ + \ + \ + {defined_names}\ + \ + \ + ") +} diff --git a/xlsx/src/export/workbook_xml_rels.rs b/xlsx/src/export/workbook_xml_rels.rs new file mode 100644 index 0000000..4f7269d --- /dev/null +++ b/xlsx/src/export/workbook_xml_rels.rs @@ -0,0 +1,25 @@ +use ironcalc_base::types::Workbook; + +use super::xml_constants::{XML_DECLARATION, XML_WORKSHEET}; + +pub(crate) fn get_workbook_xml_rels(workbook: &Workbook) -> String { + let mut relationships_str: Vec = vec![]; + let worksheet_count = workbook.worksheets.len() + 1; + for id in 1..worksheet_count { + relationships_str.push(format!( + "" + )); + } + let mut id = worksheet_count; + relationships_str.push( + format!("") + ); + id += 1; + relationships_str.push( + format!("") + ); + format!( + "{XML_DECLARATION}\n{}", + relationships_str.join("") + ) +} diff --git a/xlsx/src/export/worksheets.rs b/xlsx/src/export/worksheets.rs new file mode 100644 index 0000000..f54ecfc --- /dev/null +++ b/xlsx/src/export/worksheets.rs @@ -0,0 +1,267 @@ +//! # A note on shared formulas +//! Although both Excel and IronCalc uses shared formulas they are used in a slightly different way that cannot be mapped 1-1 +//! In IronCalc _all_ formulas are shared and there is a list of shared formulas much like there is a list of shared strings. +//! In Excel the situation in more nuanced. A shared formula is shared amongst a rage of cells. +//! The top left cell would be the "mother" cell that would contain the shared formula: +//! +//! A4+C4 +//! 123 +//! +//! Cells in the range F4:F8 will then link to that formula like so: +//! +//! +//! 1 +//! +//! Formula in F6 would then be 'A6+C6' +use std::collections::HashMap; + +use itertools::Itertools; + +use ironcalc_base::{ + expressions::{ + parser::{stringify::to_excel_string, Node}, + types::CellReferenceRC, + utils::number_to_column, + }, + types::{Cell, Worksheet}, +}; + +use super::{escape::escape_xml, xml_constants::XML_DECLARATION}; + +fn get_cell_style_attribute(s: i32) -> String { + if s == 0 { + "".to_string() + } else { + format!(" s=\"{}\"", s) + } +} + +fn get_formula_attribute( + sheet_name: String, + row: i32, + column: i32, + parsed_formula: &Node, +) -> String { + let cell_ref = CellReferenceRC { + sheet: sheet_name, + row, + column, + }; + let formula = &to_excel_string(parsed_formula, &cell_ref); + escape_xml(formula).to_string() +} + +pub(crate) fn get_worksheet_xml( + worksheet: &Worksheet, + parsed_formulas: &[Node], + dimension: &str, +) -> String { + let mut sheet_data_str: Vec = vec![]; + let mut cols_str: Vec = vec![]; + + for col in &worksheet.cols { + // + let min = col.min; + let max = col.max; + let width = col.width; + let custom_width = i32::from(col.custom_width); + let column_style = match col.style { + Some(s) => format!(" style=\"{s}\""), + None => "".to_string(), + }; + cols_str.push(format!( + "" + )); + } + + // this is a bit of an overkill. A dictionary of the row styles by row_index + let mut row_style_dict = HashMap::new(); + for row in &worksheet.rows { + // { + // "height": 13, + // "r": 7, + // "custom_format": false, + // "custom_height": true, + // "s": 0 + // "hidden": false, + // }, + row_style_dict.insert(row.r, row.clone()); + } + + for (row_index, row_data) in worksheet.sheet_data.iter().sorted_by_key(|x| x.0) { + let mut row_data_str: Vec = vec![]; + for (column_index, cell) in row_data.iter().sorted_by_key(|x| x.0) { + let column_name = number_to_column(*column_index).unwrap(); + let cell_name = format!("{column_name}{row_index}"); + match cell { + Cell::EmptyCell { s } => { + // they only hold the style + let style = get_cell_style_attribute(*s); + row_data_str.push(format!("")); + } + Cell::BooleanCell { v, s } => { + // + // 1 + // + let b = i32::from(*v); + let style = get_cell_style_attribute(*s); + row_data_str.push(format!( + "{b}" + )); + } + Cell::NumberCell { v, s } => { + // Normally the type number is left out. Example: + // + // 3 + // + let style = get_cell_style_attribute(*s); + row_data_str.push(format!("{v}")); + } + Cell::ErrorCell { ei, s } => { + let style = get_cell_style_attribute(*s); + row_data_str.push(format!( + "{ei}" + )); + } + Cell::SharedString { si, s } => { + // Example: + // + // 5 + // + // Cell on A1 contains a string (t="s") of style="1". The string is the 6th in the list of shared strings + let style = get_cell_style_attribute(*s); + row_data_str.push(format!( + "{si}" + )); + } + Cell::CellFormula { f: _, s: _ } => { + panic!("Model needs to be evaluated before saving!"); + } + Cell::CellFormulaBoolean { f, v, s } => { + // + // ISTEXT(A5) + // 1 + // + let style = get_cell_style_attribute(*s); + + let formula = get_formula_attribute( + worksheet.get_name(), + *row_index, + *column_index, + &parsed_formulas[*f as usize], + ); + + let b = i32::from(*v); + row_data_str.push(format!( + "{formula}{b}" + )); + } + Cell::CellFormulaNumber { f, v, s } => { + // Note again type is skipped + // + // A5+C3 + // 123 + // + + let formula = get_formula_attribute( + worksheet.get_name(), + *row_index, + *column_index, + &parsed_formulas[*f as usize], + ); + let style = get_cell_style_attribute(*s); + + row_data_str.push(format!( + "{formula}{v}" + )); + } + Cell::CellFormulaString { f, v, s } => { + // + // CONCATENATE(A1, A2) + // Hello world! + // + let formula = get_formula_attribute( + worksheet.get_name(), + *row_index, + *column_index, + &parsed_formulas[*f as usize], + ); + let style = get_cell_style_attribute(*s); + + row_data_str.push(format!( + "{formula}{v}" + )); + } + Cell::CellFormulaError { + f, + ei, + s, + o: _, + m: _, + } => { + // + // A1/A3 + // #DIV/0! + // + let formula = get_formula_attribute( + worksheet.get_name(), + *row_index, + *column_index, + &parsed_formulas[*f as usize], + ); + let style = get_cell_style_attribute(*s); + row_data_str.push(format!( + "{formula}{ei}" + )); + } + } + } + let row_style_str = match row_style_dict.get(row_index) { + Some(row_style) => { + let hidden_str = if row_style.hidden { + r#" hidden="1""# + } else { + "" + }; + format!( + r#" s="{}" ht="{}" customHeight="{}" customFormat="{}"{}"#, + row_style.s, + row_style.height, + i32::from(row_style.custom_height), + i32::from(row_style.custom_format), + hidden_str, + ) + } + None => "".to_string(), + }; + sheet_data_str.push(format!( + "{}", + row_data_str.join("") + )) + } + let sheet_data = sheet_data_str.join(""); + let cols = cols_str.join(""); + let cols = if cols.is_empty() { + "".to_string() + } else { + format!("{cols}") + }; + + format!( + "{XML_DECLARATION} +\ + \ + \ + \ + \ + \ + \ + {cols}\ + \ + {sheet_data}\ + \ +" + ) +} diff --git a/xlsx/src/export/xml_constants.rs b/xlsx/src/export/xml_constants.rs new file mode 100644 index 0000000..459bd69 --- /dev/null +++ b/xlsx/src/export/xml_constants.rs @@ -0,0 +1,5 @@ +pub(crate) const XML_DECLARATION: &str = + r#""#; + +pub(crate) const XML_WORKSHEET: &str = + r#"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"#; diff --git a/xlsx/src/import/colors.rs b/xlsx/src/import/colors.rs new file mode 100644 index 0000000..b6cfd34 --- /dev/null +++ b/xlsx/src/import/colors.rs @@ -0,0 +1,257 @@ +use core::cmp::max; +use core::cmp::min; + +// https://gist.github.com/emanuel-sanabria-developer/5793377 +// https://github.com/ClosedXML/ClosedXML/wiki/Excel-Indexed-Colors + +// Warning: Excel uses a weird normalization for HSL colors (0, 255) +// We use a more standard one but our HSL numbers will not coincide with Excel's + +pub(crate) fn hex_to_rgb(h: &str) -> [i32; 3] { + let r = i32::from_str_radix(&h[1..3], 16).unwrap(); + let g = i32::from_str_radix(&h[3..5], 16).unwrap(); + let b = i32::from_str_radix(&h[5..7], 16).unwrap(); + [r, g, b] +} + +pub(crate) fn rgb_to_hex(rgb: [i32; 3]) -> String { + format!("#{:02X}{:02X}{:02X}", rgb[0], rgb[1], rgb[2]) +} + +pub(crate) fn rgb_to_hsl(rgb: [i32; 3]) -> [i32; 3] { + let r = rgb[0]; + let g = rgb[1]; + let b = rgb[2]; + let red = r as f64 / 255.0; + let green = g as f64 / 255.0; + let blue = b as f64 / 255.0; + let max_color = max(max(r, g), b); + let min_color = min(min(r, g), b); + let chroma = (max_color - min_color) as f64 / 255.0; + if chroma == 0.0 { + return [0, 0, (red * 100.0).round() as i32]; + } + + let hue; + let luminosity = (max_color + min_color) as f64 / (255.0 * 2.0); + let saturation = if luminosity > 0.5 { + 0.5 * chroma / (1.0 - luminosity) + } else { + 0.5 * chroma / luminosity + }; + if max_color == r { + if green >= blue { + hue = 60.0 * (green - blue) / chroma; + } else { + hue = ((green - blue) / chroma + 6.0) * 60.0; + } + } else if max_color == g { + hue = ((blue - red) / chroma + 2.0) * 60.0; + } else { + hue = ((red - green) / chroma + 4.0) * 60.0; + } + let hue = hue.round() as i32; + let saturation = (saturation * 100.0).round() as i32; + let luminosity = (luminosity * 100.0).round() as i32; + [hue, saturation, luminosity] +} + +fn hue_to_rgb(p: f64, q: f64, t: f64) -> f64 { + let mut c = t; + if c < 0.0 { + c += 1.0; + } + if c > 1.0 { + c -= 1.0; + } + if c < 1.0 / 6.0 { + return p + (q - p) * 6.0 * t; + }; + if c < 0.5 { + return q; + }; + if c < 2.0 / 3.0 { + return p + (q - p) * (2.0 / 3.0 - t) * 6.0; + }; + p +} + +pub(crate) fn hsl_to_rgb(hsl: [i32; 3]) -> [i32; 3] { + let hue = (hsl[0] as f64) / 360.0; + let saturation = (hsl[1] as f64) / 100.0; + let luminosity = (hsl[2] as f64) / 100.0; + let red; + let green; + let blue; + + if saturation == 0.0 { + // achromatic + red = luminosity * 255.0; + green = luminosity * 255.0; + blue = luminosity * 255.0; + } else { + let q = if luminosity < 0.5 { + luminosity * (1.0 + saturation) + } else { + luminosity + saturation - luminosity * saturation + }; + let p = 2.0 * luminosity - q; + red = 255.0 * hue_to_rgb(p, q, hue + 1.0 / 3.0); + green = 255.0 * hue_to_rgb(p, q, hue); + blue = 255.0 * hue_to_rgb(p, q, hue - 1.0 / 3.0); + } + [ + red.round() as i32, + green.round() as i32, + blue.round() as i32, + ] +} + +/* 18.8.3 bgColor tint algorithm */ +fn hex_with_tint_to_rgb(hex: &str, tint: f64) -> String { + if tint == 0.0 { + return hex.to_string(); + } + let mut hsl = rgb_to_hsl(hex_to_rgb(hex)); + let l = hsl[2] as f64; + if tint < 0.0 { + // Lum’ = Lum * (1.0 + tint) + hsl[2] = (l * (1.0 + tint)).round() as i32; + } else { + // HLSMAX here would be 100, for Excel 255 + // Lum‘ = Lum * (1.0-tint) + (HLSMAX – HLSMAX * (1.0-tint)) + hsl[2] = (l + (100.0 - l) * tint).round() as i32; + }; + rgb_to_hex(hsl_to_rgb(hsl)) +} + +pub fn get_themed_color(theme: i32, tint: f64) -> String { + let color_theme = [ + "#FFFFFF", "#000000", // "window", + "#E7E6E6", "#44546A", "#4472C4", "#ED7D31", "#A5A5A5", "#FFC000", "#5B9BD5", "#70AD47", + "#0563C1", "#954F72", + ]; + hex_with_tint_to_rgb(color_theme[theme as usize], tint) +} + +pub fn get_indexed_color(index: i32) -> String { + let color_list = [ + "#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", + "#000000", "#FFFFFF", "#FF0000", "#00FF00", "#0000FF", "#FFFF00", "#FF00FF", "#00FFFF", + "#800000", "#008000", "#000080", "#808000", "#800080", "#008080", "#C0C0C0", "#808080", + "#9999FF", "#993366", "#FFFFCC", "#CCFFFF", "#660066", "#FF8080", "#0066CC", "#CCCCFF", + "#000080", "#FF00FF", "#FFFF00", "#00FFFF", "#800080", "#800000", "#008080", "#0000FF", + "#00CCFF", "#CCFFFF", "#CCFFCC", "#FFFF99", "#99CCFF", "#FF99CC", "#CC99FF", "#FFCC99", + "#3366FF", "#33CCCC", "#99CC00", "#FFCC00", "#FF9900", "#FF6600", "#666699", "#969696", + "#003366", "#339966", "#003300", "#333300", "#993300", "#993366", "#333399", + "#333333", + // 64, Transparent) + ]; + if index > 63 { + return color_list[0].to_string(); + } + color_list[index as usize].to_string() +} + +#[cfg(test)] +mod tests { + use crate::import::colors::*; + + #[test] + fn test_known_colors() { + let color1 = get_themed_color(0, -0.05); + assert_eq!(color1, "#F2F2F2"); + + let color2 = get_themed_color(5, -0.25); + // Excel returns "#C65911" (rounding error) + assert_eq!(color2, "#C55911"); + + let color3 = get_themed_color(4, 0.6); + // Excel returns "#b4c6e7" (rounding error) + assert_eq!(color3, "#B5C8E8"); + } + + #[test] + fn test_rgb_hex() { + struct ColorTest { + hex: String, + rgb: [i32; 3], + hsl: [i32; 3], + } + let color_tests = [ + ColorTest { + hex: "#FFFFFF".to_string(), + rgb: [255, 255, 255], + hsl: [0, 0, 100], + }, + ColorTest { + hex: "#000000".to_string(), + rgb: [0, 0, 0], + hsl: [0, 0, 0], + }, + ColorTest { + hex: "#44546A".to_string(), + rgb: [68, 84, 106], + hsl: [215, 22, 34], + }, + ColorTest { + hex: "#E7E6E6".to_string(), + rgb: [231, 230, 230], + hsl: [0, 2, 90], + }, + ColorTest { + hex: "#4472C4".to_string(), + rgb: [68, 114, 196], + hsl: [218, 52, 52], + }, + ColorTest { + hex: "#ED7D31".to_string(), + rgb: [237, 125, 49], + hsl: [24, 84, 56], + }, + ColorTest { + hex: "#A5A5A5".to_string(), + rgb: [165, 165, 165], + hsl: [0, 0, 65], + }, + ColorTest { + hex: "#FFC000".to_string(), + rgb: [255, 192, 0], + hsl: [45, 100, 50], + }, + ColorTest { + hex: "#5B9BD5".to_string(), + rgb: [91, 155, 213], + hsl: [209, 59, 60], + }, + ColorTest { + hex: "#70AD47".to_string(), + rgb: [112, 173, 71], + hsl: [96, 42, 48], + }, + ColorTest { + hex: "#0563C1".to_string(), + rgb: [5, 99, 193], + hsl: [210, 95, 39], + }, + ColorTest { + hex: "#954F72".to_string(), + rgb: [149, 79, 114], + hsl: [330, 31, 45], + }, + ]; + for color in color_tests.iter() { + let rgb = color.rgb; + let hsl = color.hsl; + assert_eq!(rgb, hex_to_rgb(&color.hex)); + assert_eq!(hsl, rgb_to_hsl(rgb)); + assert_eq!(rgb_to_hex(rgb), color.hex); + // The round trip has rounding errors + // FIXME: We could also hardcode the hsl21 in the testcase + let rgb2 = hsl_to_rgb(hsl); + let diff = + (rgb2[0] - rgb[0]).abs() + (rgb2[1] - rgb[1]).abs() + (rgb2[2] - rgb[2]).abs(); + assert!(diff < 4); + } + } +} diff --git a/xlsx/src/import/metadata.rs b/xlsx/src/import/metadata.rs new file mode 100644 index 0000000..f69348a --- /dev/null +++ b/xlsx/src/import/metadata.rs @@ -0,0 +1,81 @@ +use std::io::Read; + +use ironcalc_base::types::Metadata; + +use crate::error::XlsxError; + +use super::util::get_value_or_default; + +struct AppData { + application: String, + app_version: String, +} + +struct CoreData { + creator: String, + last_modified_by: String, + created: String, + last_modified: String, +} + +fn load_core( + archive: &mut zip::read::ZipArchive, +) -> Result { + let mut file = archive.by_name("docProps/core.xml")?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + let doc = roxmltree::Document::parse(&text)?; + let core_data = doc + .root() + .first_child() + .ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))?; + // Note the namespace should be "http://purl.org/dc/elements/1.1/" + let creator = get_value_or_default(&core_data, "creator", "Anonymous User"); + // Note namespace is "http://schemas.openxmlformats.org/package/2006/metadata/core-properties" + let last_modified_by = get_value_or_default(&core_data, "lastModifiedBy", "Anonymous User"); + // In these two cases the namespace is "http://purl.org/dc/terms/" + let created = get_value_or_default(&core_data, "created", ""); + let last_modified = get_value_or_default(&core_data, "modified", ""); + + Ok(CoreData { + creator, + last_modified_by, + created, + last_modified, + }) +} + +fn load_app( + archive: &mut zip::read::ZipArchive, +) -> Result { + let mut file = archive.by_name("docProps/app.xml")?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + let doc = roxmltree::Document::parse(&text)?; + let app_data = doc + .root() + .first_child() + .ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))?; + + let application = get_value_or_default(&app_data, "Application", "Unknown application"); + let app_version = get_value_or_default(&app_data, "AppVersion", ""); + Ok(AppData { + application, + app_version, + }) +} + +pub(super) fn load_metadata( + archive: &mut zip::read::ZipArchive, +) -> Result { + let app_data = load_app(archive)?; + let core_data = load_core(archive)?; + Ok(Metadata { + application: app_data.application, + app_version: app_data.app_version, + creator: core_data.creator, + last_modified_by: core_data.last_modified_by, + created: core_data.created, + last_modified: core_data.last_modified, + }) +} diff --git a/xlsx/src/import/mod.rs b/xlsx/src/import/mod.rs new file mode 100644 index 0000000..6403f62 --- /dev/null +++ b/xlsx/src/import/mod.rs @@ -0,0 +1,124 @@ +mod colors; +mod metadata; +mod shared_strings; +mod styles; +mod tables; +mod util; +mod workbook; +mod worksheets; + +use std::{ + collections::HashMap, + fs, + io::{BufReader, Read}, +}; + +use roxmltree::Node; + +use ironcalc_base::{ + model::Model, + types::{Metadata, Workbook, WorkbookSettings}, +}; + +use crate::error::XlsxError; + +use shared_strings::read_shared_strings; + +use metadata::load_metadata; +use styles::load_styles; +use util::get_attribute; +use workbook::load_workbook; +use worksheets::{load_sheets, Relationship}; + +fn load_relationships( + archive: &mut zip::ZipArchive, +) -> Result, XlsxError> { + let mut file = archive.by_name("xl/_rels/workbook.xml.rels")?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + let doc = roxmltree::Document::parse(&text)?; + let nodes: Vec = doc + .descendants() + .filter(|n| n.has_tag_name("Relationship")) + .collect(); + let mut rels = HashMap::new(); + for node in nodes { + rels.insert( + get_attribute(&node, "Id")?.to_string(), + Relationship { + rel_type: get_attribute(&node, "Type")?.to_string(), + target: get_attribute(&node, "Target")?.to_string(), + }, + ); + } + Ok(rels) +} + +fn load_xlsx_from_reader( + name: String, + reader: R, + locale: &str, + tz: &str, +) -> Result { + let mut archive = zip::ZipArchive::new(reader)?; + + let mut shared_strings = read_shared_strings(&mut archive)?; + let workbook = load_workbook(&mut archive)?; + let rels = load_relationships(&mut archive)?; + let mut tables = HashMap::new(); + let worksheets = load_sheets( + &mut archive, + &rels, + &workbook, + &mut tables, + &mut shared_strings, + )?; + let styles = load_styles(&mut archive)?; + let metadata = match load_metadata(&mut archive) { + Ok(metadata) => metadata, + Err(_) => { + // In case there is no metadata, add some + Metadata { + application: "Unknown application".to_string(), + app_version: "".to_string(), + creator: "".to_string(), + last_modified_by: "".to_string(), + created: "".to_string(), + last_modified: "".to_string(), + } + } + }; + Ok(Workbook { + shared_strings, + defined_names: workbook.defined_names, + worksheets, + styles, + name, + settings: WorkbookSettings { + tz: tz.to_string(), + locale: locale.to_string(), + }, + metadata, + tables, + }) +} + +// Public methods + +/// Imports a file from disk into an internal representation +pub fn load_from_excel(file_name: &str, locale: &str, tz: &str) -> Result { + let file_path = std::path::Path::new(file_name); + let file = fs::File::open(file_path)?; + let reader = BufReader::new(file); + let name = file_path + .file_stem() + .ok_or_else(|| XlsxError::IO("Could not extract workbook name".to_string()))? + .to_string_lossy() + .to_string(); + load_xlsx_from_reader(name, reader, locale, tz) +} + +pub fn load_model_from_xlsx(file_name: &str, locale: &str, tz: &str) -> Result { + let workbook = load_from_excel(file_name, locale, tz)?; + Model::from_workbook(workbook).map_err(XlsxError::Workbook) +} diff --git a/xlsx/src/import/shared_strings.rs b/xlsx/src/import/shared_strings.rs new file mode 100644 index 0000000..cd6ccec --- /dev/null +++ b/xlsx/src/import/shared_strings.rs @@ -0,0 +1,80 @@ +use std::io::Read; + +use roxmltree::Node; + +use crate::error::XlsxError; + +/// Reads the list of shared strings in an Excel workbook +/// Note than in IronCalc we lose _internal_ styling of a string +/// See Section 18.4 +pub(crate) fn read_shared_strings( + archive: &mut zip::read::ZipArchive, +) -> Result, XlsxError> { + match archive.by_name("xl/sharedStrings.xml") { + Ok(mut file) => { + let mut text = String::new(); + file.read_to_string(&mut text)?; + read_shared_strings_from_string(&text) + } + Err(_e) => Ok(Vec::new()), + } +} + +fn read_shared_strings_from_string(text: &str) -> Result, XlsxError> { + let doc = roxmltree::Document::parse(text)?; + let mut shared_strings = Vec::new(); + let nodes: Vec = doc.descendants().filter(|n| n.has_tag_name("si")).collect(); + for node in nodes { + let text = node + .descendants() + .filter(|n| n.has_tag_name("t")) + .map(|n| n.text().unwrap_or("").to_string()) + .collect::>() + .join(""); + shared_strings.push(text); + } + Ok(shared_strings) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_shared_strings() { + let xml_string = r#" + + + A string + + + A second String + + + + Hello + + + + + + + + + + + World + + +"#; + let shared_strings = read_shared_strings_from_string(xml_string.trim()).unwrap(); + assert_eq!( + shared_strings, + [ + "A string".to_string(), + "A second String".to_string(), + "Hello World".to_string() + ] + ); + } +} diff --git a/xlsx/src/import/styles.rs b/xlsx/src/import/styles.rs new file mode 100644 index 0000000..efabe54 --- /dev/null +++ b/xlsx/src/import/styles.rs @@ -0,0 +1,386 @@ +use std::{collections::HashMap, io::Read}; + +use ironcalc_base::types::{ + Alignment, Border, BorderItem, BorderStyle, CellStyleXfs, CellStyles, CellXfs, Fill, Font, + FontScheme, HorizontalAlignment, NumFmt, Styles, VerticalAlignment, +}; +use roxmltree::Node; + +use crate::error::XlsxError; + +use super::util::{get_attribute, get_bool, get_bool_false, get_color, get_number}; + +fn get_border(node: Node, name: &str) -> Result, XlsxError> { + let style; + let color; + let border_nodes = node + .children() + .filter(|n| n.has_tag_name(name)) + .collect::>(); + if border_nodes.len() == 1 { + let border = border_nodes[0]; + style = match border.attribute("style") { + Some("thin") => BorderStyle::Thin, + Some("medium") => BorderStyle::Medium, + Some("thick") => BorderStyle::Thick, + Some("double") => BorderStyle::Double, + Some("slantdashdot") => BorderStyle::SlantDashDot, + Some("mediumdashed") => BorderStyle::MediumDashed, + Some("mediumdashdot") => BorderStyle::MediumDashDot, + Some("mediumdashdotdot") => BorderStyle::MediumDashDotDot, + // TODO: Should we fail in this case or set the border to None? + Some(_) => BorderStyle::Thin, + None => { + return Ok(None); + } + }; + + let color_node = border + .children() + .filter(|n| n.has_tag_name("color")) + .collect::>(); + if color_node.len() == 1 { + color = get_color(color_node[0])?; + } else { + color = None; + } + } else { + return Ok(None); + } + Ok(Some(BorderItem { style, color })) +} + +pub(super) fn load_styles( + archive: &mut zip::read::ZipArchive, +) -> Result { + let mut file = archive.by_name("xl/styles.xml")?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + let doc = roxmltree::Document::parse(&text)?; + let style_sheet = doc + .root() + .first_child() + .ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))?; + + let mut num_fmts = Vec::new(); + let num_fmts_nodes = style_sheet + .children() + .filter(|n| n.has_tag_name("numFmts")) + .collect::>(); + if num_fmts_nodes.len() == 1 { + for num_fmt in num_fmts_nodes[0].children() { + let num_fmt_id = get_number(num_fmt, "numFmtId"); + let format_code = num_fmt.attribute("formatCode").unwrap_or("").to_string(); + num_fmts.push(NumFmt { + num_fmt_id, + format_code, + }); + } + } + + let mut fonts = Vec::new(); + let font_nodes = style_sheet + .children() + .filter(|n| n.has_tag_name("fonts")) + .collect::>()[0]; + for font in font_nodes.children() { + let mut sz = 11; + let mut name = "Calibri".to_string(); + // NOTE: In Excel you can have simple underline or double underline + // In IronCalc convert double underline to simple + // This in excel is u with a value of "double" + let mut u = false; + let mut b = false; + let mut i = false; + let mut strike = false; + let mut color = Some("FFFFFF00".to_string()); + let mut family = 2; + let mut scheme = FontScheme::default(); + for feature in font.children() { + match feature.tag_name().name() { + "sz" => { + sz = feature + .attribute("val") + .unwrap_or("11") + .parse::() + .unwrap_or(11); + } + "color" => { + color = get_color(feature)?; + } + "u" => { + u = true; + } + "b" => { + b = true; + } + "i" => { + i = true; + } + "strike" => { + strike = true; + } + "name" => name = feature.attribute("val").unwrap_or("Calibri").to_string(), + // If there is a theme the font scheme and family overrides other properties like the name + "family" => { + family = feature + .attribute("val") + .unwrap_or("2") + .parse::() + .unwrap_or(2); + } + "scheme" => { + scheme = match feature.attribute("val") { + None => FontScheme::default(), + Some("minor") => FontScheme::Minor, + Some("major") => FontScheme::Major, + Some("none") => FontScheme::None, + // TODO: Should we fail? + Some(_) => FontScheme::default(), + } + } + "charset" => {} + _ => { + println!("Unexpected feature {:?}", feature); + } + } + } + fonts.push(Font { + strike, + u, + b, + i, + sz, + color, + name, + family, + scheme, + }); + } + + let mut fills = Vec::new(); + let fill_nodes = style_sheet + .children() + .filter(|n| n.has_tag_name("fills")) + .collect::>()[0]; + for fill in fill_nodes.children() { + let pattern_fill = fill + .children() + .filter(|n| n.has_tag_name("patternFill")) + .collect::>(); + if pattern_fill.len() != 1 { + // safety belt + // Some fills do not have a patternFill, but they have gradientFill + fills.push(Fill { + pattern_type: "solid".to_string(), + fg_color: None, + bg_color: None, + }); + continue; + } + let pattern_fill = pattern_fill[0]; + + let pattern_type = pattern_fill + .attribute("patternType") + .unwrap_or("none") + .to_string(); + let mut fg_color = None; + let mut bg_color = None; + for feature in pattern_fill.children() { + match feature.tag_name().name() { + "fgColor" => { + fg_color = get_color(feature)?; + } + "bgColor" => { + bg_color = get_color(feature)?; + } + _ => { + println!("Unexpected pattern"); + dbg!(feature); + } + } + } + fills.push(Fill { + pattern_type, + fg_color, + bg_color, + }) + } + + let mut borders = Vec::new(); + let border_nodes = style_sheet + .children() + .filter(|n| n.has_tag_name("borders")) + .collect::>()[0]; + for border in border_nodes.children() { + let diagonal_up = get_bool_false(border, "diagonal_up"); + let diagonal_down = get_bool_false(border, "diagonal_down"); + let left = get_border(border, "left")?; + let right = get_border(border, "right")?; + let top = get_border(border, "top")?; + let bottom = get_border(border, "bottom")?; + let diagonal = get_border(border, "diagonal")?; + borders.push(Border { + diagonal_up, + diagonal_down, + left, + right, + top, + bottom, + diagonal, + }); + } + + let mut cell_style_xfs = Vec::new(); + let cell_style_xfs_nodes = style_sheet + .children() + .filter(|n| n.has_tag_name("cellStyleXfs")) + .collect::>()[0]; + for xfs in cell_style_xfs_nodes.children() { + let num_fmt_id = get_number(xfs, "numFmtId"); + let font_id = get_number(xfs, "fontId"); + let fill_id = get_number(xfs, "fillId"); + let border_id = get_number(xfs, "borderId"); + let apply_number_format = get_bool(xfs, "applyNumberFormat"); + let apply_border = get_bool(xfs, "applyBorder"); + let apply_alignment = get_bool(xfs, "applyAlignment"); + let apply_protection = get_bool(xfs, "applyProtection"); + let apply_font = get_bool(xfs, "applyFont"); + let apply_fill = get_bool(xfs, "applyFill"); + + cell_style_xfs.push(CellStyleXfs { + num_fmt_id, + font_id, + fill_id, + border_id, + apply_number_format, + apply_border, + apply_alignment, + apply_protection, + apply_font, + apply_fill, + }); + } + + let mut cell_styles = Vec::new(); + let mut style_names = HashMap::new(); + let cell_style_nodes = style_sheet + .children() + .filter(|n| n.has_tag_name("cellStyles")) + .collect::>()[0]; + for cell_style in cell_style_nodes.children() { + let name = get_attribute(&cell_style, "name")?.to_string(); + let xf_id = get_number(cell_style, "xfId"); + let builtin_id = get_number(cell_style, "builtinId"); + style_names.insert(xf_id, name.clone()); + cell_styles.push(CellStyles { + name, + xf_id, + builtin_id, + }) + } + + let mut cell_xfs = Vec::new(); + let cell_xfs_nodes = style_sheet + .children() + .filter(|n| n.has_tag_name("cellXfs")) + .collect::>()[0]; + for xfs in cell_xfs_nodes.children() { + let xf_id = get_attribute(&xfs, "xfId")?.parse::()?; + let num_fmt_id = get_number(xfs, "numFmtId"); + let font_id = get_number(xfs, "fontId"); + let fill_id = get_number(xfs, "fillId"); + let border_id = get_number(xfs, "borderId"); + let apply_number_format = get_bool_false(xfs, "applyNumberFormat"); + let apply_border = get_bool_false(xfs, "applyBorder"); + let apply_alignment = get_bool_false(xfs, "applyAlignment"); + let apply_protection = get_bool_false(xfs, "applyProtection"); + let apply_font = get_bool_false(xfs, "applyFont"); + let apply_fill = get_bool_false(xfs, "applyFill"); + let quote_prefix = get_bool_false(xfs, "quotePrefix"); + + // TODO: Pivot Tables + // let pivotButton = get_bool(xfs, "pivotButton"); + + let alignment_nodes = xfs + .children() + .filter(|n| n.has_tag_name("alignment")) + .collect::>(); + let alignment = if alignment_nodes.len() == 1 { + let alignment_node = alignment_nodes[0]; + let wrap_text = get_bool_false(alignment_node, "wrapText"); + + let horizontal = match alignment_node.attribute("horizontal") { + Some("center") => HorizontalAlignment::Center, + Some("centerContinuous") => HorizontalAlignment::CenterContinuous, + Some("distributed") => HorizontalAlignment::Distributed, + Some("fill") => HorizontalAlignment::Fill, + Some("general") => HorizontalAlignment::General, + Some("justify") => HorizontalAlignment::Justify, + Some("left") => HorizontalAlignment::Left, + Some("right") => HorizontalAlignment::Right, + // TODO: Should we fail in this case or set the alignment to default? + Some(_) => HorizontalAlignment::default(), + None => HorizontalAlignment::default(), + }; + + let vertical = match alignment_node.attribute("vertical") { + Some("bottom") => VerticalAlignment::Bottom, + Some("center") => VerticalAlignment::Center, + Some("distributed") => VerticalAlignment::Distributed, + Some("justify") => VerticalAlignment::Justify, + Some("top") => VerticalAlignment::Top, + // TODO: Should we fail in this case or set the alignment to default? + Some(_) => VerticalAlignment::default(), + None => VerticalAlignment::default(), + }; + + Some(Alignment { + horizontal, + vertical, + wrap_text, + }) + } else { + None + }; + + cell_xfs.push(CellXfs { + xf_id, + num_fmt_id, + font_id, + fill_id, + border_id, + apply_number_format, + apply_border, + apply_alignment, + apply_protection, + apply_font, + apply_fill, + quote_prefix, + alignment, + }); + } + + // TODO + // let mut dxfs = Vec::new(); + // let mut tableStyles = Vec::new(); + // let mut colors = Vec::new(); + // + // + // + // + // + // + // + // + + Ok(Styles { + num_fmts, + fonts, + fills, + borders, + cell_style_xfs, + cell_xfs, + cell_styles, + }) +} diff --git a/xlsx/src/import/tables.rs b/xlsx/src/import/tables.rs new file mode 100644 index 0000000..c0a019a --- /dev/null +++ b/xlsx/src/import/tables.rs @@ -0,0 +1,215 @@ +use std::io::Read; + +use ironcalc_base::types::{Table, TableColumn, TableStyleInfo}; +use roxmltree::Node; + +use crate::error::XlsxError; + +use super::util::{get_bool, get_bool_false}; + +// +// +// +// +// +// +// +// +// ... +// +// +//
+ +/// Reads a table in an Excel workbook +pub(crate) fn load_table( + archive: &mut zip::read::ZipArchive, + path: &str, + sheet_name: &str, +) -> Result { + let mut file = archive.by_name(path)?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + let document = roxmltree::Document::parse(&text)?; + + // table + let table = document + .root() + .first_child() + .ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))?; + + // Name and display name are normally the same and are unique in a workbook + // They also need to be different from any defined name + let name = table + .attribute("name") + .expect("Missing table name") + .to_string(); + + let display_name = table + .attribute("name") + .expect("Missing table display name") + .to_string(); + + // Range of the table, including the totals if any and headers. + let reference = table + .attribute("ref") + .expect("Missing table ref") + .to_string(); + + // Either 0 or 1, indicates if the table has a formula for totals at the bottom of the table + let totals_row_count = match table.attribute("totalsRowCount") { + Some(s) => s.parse::().expect("Invalid totalsRowCount"), + None => 0, + }; + + // Either 0 or 1, indicates if the table has headers at the top of the table + let header_row_count = match table.attribute("headerRowCount") { + Some(s) => s.parse::().expect("Invalid headerRowCount"), + None => 1, + }; + + // style index of the header row of the table + let header_row_dxf_id = if let Some(index_str) = table.attribute("headerRowDxfId") { + match index_str.parse::() { + Ok(i) => Some(i), + Err(_) => None, + } + } else { + None + }; + + // style index of the header row of the table + let data_dxf_id = if let Some(index_str) = table.attribute("headerRowDxfId") { + match index_str.parse::() { + Ok(i) => Some(i), + Err(_) => None, + } + } else { + None + }; + + // style index of the totals row of the table + let totals_row_dxf_id = if let Some(index_str) = table.attribute("totalsRowDxfId") { + match index_str.parse::() { + Ok(i) => Some(i), + Err(_) => None, + } + } else { + None + }; + + // Missing in Calc: styles can also be defined via a name: + // headerRowCellStyle, dataCellStyle, totalsRowCellStyle + + // Missing in Calc: styles can also be applied to the borders: + // headerRowBorderDxfId, tableBorderDxfId, totalsRowBorderDxfId + + // TODO: Conformant implementations should panic if header_row_dxf_id or data_dxf_id are out of bounds. + + // Note that filters are non dynamic + // The only thing important for us is whether or not it has filters + let auto_filter = table + .descendants() + .filter(|n| n.has_tag_name("autoFilter")) + .collect::>(); + + let has_filters = if let Some(filter) = auto_filter.get(0) { + filter.children().count() > 0 + } else { + false + }; + + // tableColumn + let table_column = table + .descendants() + .filter(|n| n.has_tag_name("tableColumn")) + .collect::>(); + let mut columns = Vec::new(); + for table_column in table_column { + let column_name = table_column.attribute("name").expect("Missing column name"); + let id = table_column.attribute("id").expect("Missing column id"); + let id = id.parse::().expect("Invalid id"); + + // style index of the header row of the table + let header_row_dxf_id = if let Some(index_str) = table_column.attribute("headerRowDxfId") { + match index_str.parse::() { + Ok(i) => Some(i), + Err(_) => None, + } + } else { + None + }; + + // style index of the header row of the table column + let data_dxf_id = if let Some(index_str) = table_column.attribute("headerRowDxfId") { + match index_str.parse::() { + Ok(i) => Some(i), + Err(_) => None, + } + } else { + None + }; + + // style index of the totals row of the table column + let totals_row_dxf_id = if let Some(index_str) = table_column.attribute("totalsRowDxfId") { + match index_str.parse::() { + Ok(i) => Some(i), + Err(_) => None, + } + } else { + None + }; + + // NOTE: Same as before, we should panic if indices to differential formatting records are out of bounds + // Missing in Calc: styles can also be defined via a name: + // headerRowCellStyle, dataCellStyle, totalsRowCellStyle + + columns.push(TableColumn { + id, + name: column_name.to_string(), + totals_row_label: None, + header_row_dxf_id, + data_dxf_id, + totals_row_function: None, + totals_row_dxf_id, + }); + } + + // tableInfo + let table_info = table + .descendants() + .filter(|n| n.has_tag_name("tableInfo")) + .collect::>(); + let style_info = match table_info.get(0) { + Some(node) => { + let name = node.attribute("name").map(|s| s.to_string()); + TableStyleInfo { + name, + show_first_column: get_bool_false(*node, "showFirstColumn"), + show_last_column: get_bool_false(*node, "showLastColumn"), + show_row_stripes: get_bool(*node, "showRowStripes"), + show_column_stripes: get_bool_false(*node, "showColumnStripes"), + } + } + None => TableStyleInfo { + name: None, + show_first_column: false, + show_last_column: false, + show_row_stripes: true, + show_column_stripes: false, + }, + }; + Ok(Table { + name, + display_name, + reference, + totals_row_count, + header_row_count, + header_row_dxf_id, + data_dxf_id, + totals_row_dxf_id, + columns, + style_info, + has_filters, + sheet_name: sheet_name.to_string(), + }) +} diff --git a/xlsx/src/import/util.rs b/xlsx/src/import/util.rs new file mode 100644 index 0000000..f186b3a --- /dev/null +++ b/xlsx/src/import/util.rs @@ -0,0 +1,78 @@ +use colors::{get_indexed_color, get_themed_color}; +use roxmltree::{ExpandedName, Node}; + +use crate::error::XlsxError; + +use super::colors; + +pub(crate) fn get_number(node: Node, s: &str) -> i32 { + node.attribute(s).unwrap_or("0").parse::().unwrap_or(0) +} + +#[inline] +pub(super) fn get_attribute<'a, 'n, 'm, N>( + node: &'a Node, + attr_name: N, +) -> Result<&'a str, XlsxError> +where + N: Into>, +{ + let attr_name = attr_name.into(); + node.attribute(attr_name) + .ok_or_else(|| XlsxError::Xml(format!("Missing \"{:?}\" XML attribute", attr_name))) +} + +pub(super) fn get_value_or_default(node: &Node, tag_name: &str, default: &str) -> String { + let application_nodes = node + .children() + .filter(|n| n.has_tag_name(tag_name)) + .collect::>(); + if application_nodes.len() == 1 { + application_nodes[0].text().unwrap_or(default).to_string() + } else { + default.to_string() + } +} + +pub(super) fn get_color(node: Node) -> Result, XlsxError> { + // 18.3.1.15 color (Data Bar Color) + if node.has_attribute("rgb") { + let mut val = node.attribute("rgb").unwrap().to_string(); + // FIXME the two first values is normally the alpha. + if val.len() == 8 { + val = format!("#{}", &val[2..8]); + } + Ok(Some(val)) + } else if node.has_attribute("indexed") { + let index = node.attribute("indexed").unwrap().parse::()?; + let rgb = get_indexed_color(index); + Ok(Some(rgb)) + // Color::Indexed(val) + } else if node.has_attribute("theme") { + let theme = node.attribute("theme").unwrap().parse::()?; + let tint = match node.attribute("tint") { + Some(t) => t.parse::().unwrap_or(0.0), + None => 0.0, + }; + let rgb = get_themed_color(theme, tint); + Ok(Some(rgb)) + // Color::Theme { theme, tint } + } else if node.has_attribute("auto") { + // TODO: Is this correct? + // A boolean value indicating the color is automatic and system color dependent. + Ok(None) + } else { + println!("Unexpected color node {:?}", node); + Ok(None) + } +} + +pub(super) fn get_bool(node: Node, s: &str) -> bool { + // defaults to true + !matches!(node.attribute(s), Some("0")) +} + +pub(super) fn get_bool_false(node: Node, s: &str) -> bool { + // defaults to false + matches!(node.attribute(s), Some("1")) +} diff --git a/xlsx/src/import/workbook.rs b/xlsx/src/import/workbook.rs new file mode 100644 index 0000000..490ab28 --- /dev/null +++ b/xlsx/src/import/workbook.rs @@ -0,0 +1,79 @@ +use std::io::Read; + +use ironcalc_base::types::{DefinedName, SheetState}; +use roxmltree::Node; + +use crate::error::XlsxError; + +use super::{ + util::get_attribute, + worksheets::{Sheet, WorkbookXML}, +}; + +pub(super) fn load_workbook( + archive: &mut zip::read::ZipArchive, +) -> Result { + let mut file = archive.by_name("xl/workbook.xml")?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + let doc = roxmltree::Document::parse(&text)?; + let mut defined_names = Vec::new(); + let mut sheets = Vec::new(); + // Get the sheets + let sheet_nodes: Vec = doc + .descendants() + .filter(|n| n.has_tag_name("sheet")) + .collect(); + for sheet in sheet_nodes { + let name = get_attribute(&sheet, "name")?.to_string(); + let sheet_id = get_attribute(&sheet, "sheetId")?.to_string(); + let sheet_id = sheet_id.parse::()?; + let id = get_attribute( + &sheet, + ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships", + "id", + ), + )? + .to_string(); + let state = match sheet.attribute("state") { + Some("visible") | None => SheetState::Visible, + Some("hidden") => SheetState::Hidden, + Some("veryHidden") => SheetState::VeryHidden, + Some(state) => return Err(XlsxError::Xml(format!("Unknown sheet state: {}", state))), + }; + sheets.push(Sheet { + name, + sheet_id, + id, + state, + }); + } + // Get the defined names + let name_nodes: Vec = doc + .descendants() + .filter(|n| n.has_tag_name("definedName")) + .collect(); + for node in name_nodes { + let name = get_attribute(&node, "name")?.to_string(); + let formula = node.text().unwrap_or("").to_string(); + // NOTE: In Excel the `localSheetId` is just the index of the worksheet and unrelated to the sheetId + let sheet_id = match node.attribute("localSheetId") { + Some(s) => { + let index = s.parse::()?; + Some(sheets[index].sheet_id) + } + None => None, + }; + defined_names.push(DefinedName { + name, + formula, + sheet_id, + }) + } + // read the relationships file + Ok(WorkbookXML { + worksheets: sheets, + defined_names, + }) +} diff --git a/xlsx/src/import/worksheets.rs b/xlsx/src/import/worksheets.rs new file mode 100644 index 0000000..2a7f3ee --- /dev/null +++ b/xlsx/src/import/worksheets.rs @@ -0,0 +1,925 @@ +use std::{collections::HashMap, io::Read, num::ParseIntError}; + +use ironcalc_base::{ + expressions::{ + parser::{stringify::to_rc_format, Parser}, + token::{get_error_by_english_name, Error}, + types::CellReferenceRC, + utils::column_to_number, + }, + types::{Cell, Col, Comment, DefinedName, Row, SheetData, SheetState, Table, Worksheet}, +}; +use roxmltree::Node; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use crate::error::XlsxError; + +use super::{ + tables::load_table, + util::{get_attribute, get_color, get_number}, +}; + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct Sheet { + pub(crate) name: String, + pub(crate) sheet_id: u32, + pub(crate) id: String, + pub(crate) state: SheetState, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct WorkbookXML { + pub(crate) worksheets: Vec, + pub(crate) defined_names: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct Relationship { + pub(crate) target: String, + pub(crate) rel_type: String, +} + +fn get_column_from_ref(s: &str) -> String { + let cs = s.chars(); + let mut column = Vec::::new(); + for c in cs { + if !c.is_ascii_digit() { + column.push(c); + } + } + column.into_iter().collect() +} + +fn load_dimension(ws: Node) -> String { + // + let application_nodes = ws + .children() + .filter(|n| n.has_tag_name("dimension")) + .collect::>(); + if application_nodes.len() == 1 { + application_nodes[0] + .attribute("ref") + .unwrap_or("A1") + .to_string() + } else { + "A1".to_string() + } +} + +fn load_columns(ws: Node) -> Result, XlsxError> { + // cols + // + // + // + // + // + let mut cols = Vec::new(); + let columns = ws + .children() + .filter(|n| n.has_tag_name("cols")) + .collect::>(); + if columns.len() == 1 { + for col in columns[0].children() { + let min = get_attribute(&col, "min")?; + let min = min.parse::()?; + let max = get_attribute(&col, "max")?; + let max = max.parse::()?; + let width = get_attribute(&col, "width")?; + let width = width.parse::()?; + let custom_width = matches!(col.attribute("customWidth"), Some("1")); + let style = col + .attribute("style") + .map(|s| s.parse::().unwrap_or(0)); + cols.push(Col { + min, + max, + width, + custom_width, + style, + }) + } + } + Ok(cols) +} + +fn load_merge_cells(ws: Node) -> Result, XlsxError> { + // 18.3.1.55 Merge Cells + // + // + // + let mut merge_cells = Vec::new(); + let merge_cells_nodes = ws + .children() + .filter(|n| n.has_tag_name("mergeCells")) + .collect::>(); + if merge_cells_nodes.len() == 1 { + for merge_cell in merge_cells_nodes[0].children() { + let reference = get_attribute(&merge_cell, "ref")?.to_string(); + merge_cells.push(reference); + } + } + Ok(merge_cells) +} + +fn load_sheet_color(ws: Node) -> Result, XlsxError> { + // + // + // + let mut color = None; + let sheet_pr = ws + .children() + .filter(|n| n.has_tag_name("sheetPr")) + .collect::>(); + if sheet_pr.len() == 1 { + let tabs = sheet_pr[0] + .children() + .filter(|n| n.has_tag_name("tabColor")) + .collect::>(); + if tabs.len() == 1 { + color = get_color(tabs[0])?; + } + } + Ok(color) +} + +fn load_comments( + archive: &mut zip::read::ZipArchive, + path: &str, +) -> Result, XlsxError> { + let mut comments = Vec::new(); + let mut file = archive.by_name(path)?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + let doc = roxmltree::Document::parse(&text)?; + let ws = doc + .root() + .first_child() + .ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))?; + let comment_list = ws + .children() + .filter(|n| n.has_tag_name("commentList")) + .collect::>(); + if comment_list.len() == 1 { + for comment in comment_list[0].children() { + let text = comment + .descendants() + .filter(|n| n.has_tag_name("t")) + .map(|n| n.text().unwrap().to_string()) + .collect::>() + .join(""); + let cell_ref = get_attribute(&comment, "ref")?.to_string(); + // TODO: Read author_name from the list of authors + let author_name = "".to_string(); + comments.push(Comment { + text, + author_name, + author_id: None, + cell_ref, + }); + } + } + + Ok(comments) +} + +#[derive(Error, Debug, PartialEq, Eq)] +enum ParseReferenceError { + #[error("RowError: {0}")] + RowError(ParseIntError), + #[error("ColumnError: {0}")] + ColumnError(String), +} + +// This parses Sheet1!AS23 into sheet, column and row +// FIXME: This is buggy. Does not check that is a valid sheet name +// There is a similar named function in ironcalc_base. We probably should fix both at the same time. +// NB: Maybe use regexes for this? +fn parse_reference(s: &str) -> Result { + let bytes = s.as_bytes(); + let mut sheet_name = "".to_string(); + let mut column = "".to_string(); + let mut row = "".to_string(); + let mut state = "sheet"; // "sheet", "col", "row" + for &byte in bytes { + match state { + "sheet" => { + if byte == b'!' { + state = "col" + } else { + sheet_name.push(byte as char); + } + } + "col" => { + if byte.is_ascii_alphabetic() { + column.push(byte as char); + } else { + state = "row"; + row.push(byte as char); + } + } + _ => { + row.push(byte as char); + } + } + } + Ok(CellReferenceRC { + sheet: sheet_name, + row: row.parse::().map_err(ParseReferenceError::RowError)?, + column: column_to_number(&column).map_err(ParseReferenceError::ColumnError)?, + }) +} + +fn from_a1_to_rc( + formula: String, + worksheets: &[String], + context: String, + tables: HashMap, +) -> Result { + let mut parser = Parser::new(worksheets.to_owned(), tables); + let cell_reference = + parse_reference(&context).map_err(|error| XlsxError::Xml(error.to_string()))?; + let t = parser.parse(&formula, &Some(cell_reference)); + Ok(to_rc_format(&t)) +} + +fn get_formula_index(formula: &str, shared_formulas: &[String]) -> Option { + for (index, f) in shared_formulas.iter().enumerate() { + if f == formula { + return Some(index as i32); + } + } + None +} + +// FIXME +#[allow(clippy::too_many_arguments)] +fn get_cell_from_excel( + cell_value: Option<&str>, + value_metadata: Option<&str>, + cell_type: &str, + cell_style: i32, + formula_index: i32, + sheet_name: &str, + cell_ref: &str, + shared_strings: &mut Vec, +) -> Cell { + // Possible cell types: + // 18.18.11 ST_CellType (Cell Type) + // b (Boolean) + // d (Date) + // e (Error) + // inlineStr (Inline String) + // n (Number) + // s (Shared String) + // str (String) + + if formula_index == -1 { + match cell_type { + "b" => Cell::BooleanCell { + v: cell_value == Some("1"), + s: cell_style, + }, + "n" => Cell::NumberCell { + v: cell_value.unwrap_or("0").parse::().unwrap_or(0.0), + s: cell_style, + }, + "e" => { + // For compatibility reasons Excel does not put the value #SPILL! but adds it as a metadata + // Older engines would just import #VALUE! + let mut error_name = cell_value.unwrap_or("#ERROR!"); + if error_name == "#VALUE!" && value_metadata.is_some() { + error_name = match value_metadata { + Some("1") => "#CALC!", + Some("2") => "#SPILL!", + _ => error_name, + } + } + Cell::ErrorCell { + ei: get_error_by_english_name(error_name).unwrap_or(Error::ERROR), + s: cell_style, + } + } + "s" => Cell::SharedString { + si: cell_value.unwrap_or("0").parse::().unwrap_or(0), + s: cell_style, + }, + "str" => { + let s = cell_value.unwrap_or(""); + let si = if let Some(i) = shared_strings.iter().position(|r| r == s) { + i + } else { + shared_strings.push(s.to_string()); + shared_strings.len() - 1 + } as i32; + + Cell::SharedString { si, s: cell_style } + } + "d" => { + // Not implemented + println!("Invalid type (d) in {}!{}", sheet_name, cell_ref); + Cell::ErrorCell { + ei: Error::NIMPL, + s: cell_style, + } + } + "inlineStr" => { + // Not implemented + println!("Invalid type (inlineStr) in {}!{}", sheet_name, cell_ref); + Cell::ErrorCell { + ei: Error::NIMPL, + s: cell_style, + } + } + "empty" => Cell::EmptyCell { s: cell_style }, + _ => { + // error + println!( + "Unexpected type ({}) in {}!{}", + cell_type, sheet_name, cell_ref + ); + Cell::ErrorCell { + ei: Error::ERROR, + s: cell_style, + } + } + } + } else { + match cell_type { + "b" => Cell::CellFormulaBoolean { + f: formula_index, + v: cell_value == Some("1"), + s: cell_style, + }, + "n" => Cell::CellFormulaNumber { + f: formula_index, + v: cell_value.unwrap_or("0").parse::().unwrap_or(0.0), + s: cell_style, + }, + "e" => { + // For compatibility reasons Excel does not put the value #SPILL! but adds it as a metadata + // Older engines would just import #VALUE! + let mut error_name = cell_value.unwrap_or("#ERROR!"); + if error_name == "#VALUE!" && value_metadata.is_some() { + error_name = match value_metadata { + Some("1") => "#CALC!", + Some("2") => "#SPILL!", + _ => error_name, + } + } + Cell::CellFormulaError { + f: formula_index, + ei: get_error_by_english_name(error_name).unwrap_or(Error::ERROR), + s: cell_style, + o: format!("{}!{}", sheet_name, cell_ref), + m: cell_value.unwrap_or("#ERROR!").to_string(), + } + } + "s" => { + // Not implemented + let o = format!("{}!{}", sheet_name, cell_ref); + let m = Error::NIMPL.to_string(); + println!("Invalid type (s) in {}!{}", sheet_name, cell_ref); + Cell::CellFormulaError { + f: formula_index, + ei: Error::NIMPL, + s: cell_style, + o, + m, + } + } + "str" => { + // In Excel and in IronCalc all strings in cells result of a formula are *not* shared strings. + Cell::CellFormulaString { + f: formula_index, + v: cell_value.unwrap_or("").to_string(), + s: cell_style, + } + } + "d" => { + // Not implemented + println!("Invalid type (d) in {}!{}", sheet_name, cell_ref); + let o = format!("{}!{}", sheet_name, cell_ref); + let m = Error::NIMPL.to_string(); + Cell::CellFormulaError { + f: formula_index, + ei: Error::NIMPL, + s: cell_style, + o, + m, + } + } + "inlineStr" => { + // Not implemented + let o = format!("{}!{}", sheet_name, cell_ref); + let m = Error::NIMPL.to_string(); + println!("Invalid type (inlineStr) in {}!{}", sheet_name, cell_ref); + Cell::CellFormulaError { + f: formula_index, + ei: Error::NIMPL, + s: cell_style, + o, + m, + } + } + _ => { + // error + println!( + "Unexpected type ({}) in {}!{}", + cell_type, sheet_name, cell_ref + ); + let o = format!("{}!{}", sheet_name, cell_ref); + let m = Error::ERROR.to_string(); + Cell::CellFormulaError { + f: formula_index, + ei: Error::ERROR, + s: cell_style, + o, + m, + } + } + } + } +} + +fn load_sheet_rels( + archive: &mut zip::read::ZipArchive, + path: &str, + tables: &mut HashMap, + sheet_name: &str, +) -> Result, XlsxError> { + // ...xl/worksheets/sheet6.xml -> xl/worksheets/_rels/sheet6.xml.rels + let mut comments = Vec::new(); + let v: Vec<&str> = path.split("/worksheets/").collect(); + let mut path = v[0].to_string(); + path.push_str("/worksheets/_rels/"); + path.push_str(v[1]); + path.push_str(".rels"); + let file = archive.by_name(&path); + if file.is_err() { + return Ok(comments); + } + let mut text = String::new(); + file.unwrap().read_to_string(&mut text)?; + let doc = roxmltree::Document::parse(&text)?; + + let rels = doc + .root() + .first_child() + .ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))? + .children() + .collect::>(); + for rel in rels { + let t = get_attribute(&rel, "Type")?.to_string(); + if t.ends_with("comments") { + let mut target = get_attribute(&rel, "Target")?.to_string(); + // Target="../comments1.xlsx" + target.replace_range(..2, v[0]); + comments = load_comments(archive, &target)?; + } else if t.ends_with("table") { + let mut target = get_attribute(&rel, "Target")?.to_string(); + + let path = if let Some(p) = target.strip_prefix('/') { + p.to_string() + } else { + // Target="../table1.xlsx" + target.replace_range(..2, v[0]); + target + }; + + let table = load_table(archive, &path, sheet_name)?; + tables.insert(table.name.clone(), table); + } + } + Ok(comments) +} + +fn get_frozen_rows_and_columns(ws: Node) -> (i32, i32) { + // + // + // + // + // + // + + // If we have frozen rows and columns: + + // + // + // + // + // + // + + // 18.18.52 ST_Pane (Pane Types) + // bottomLeft, bottomRight, topLeft, topRight + + // NB: bottomLeft is used when only rows are frozen, etc + // Calc ignores all those. + + let mut frozen_rows = 0; + let mut frozen_columns = 0; + + // In Calc there can only be one sheetView + let sheet_views = ws + .children() + .filter(|n| n.has_tag_name("sheetViews")) + .collect::>(); + + if sheet_views.len() != 1 { + return (0, 0); + } + + let sheet_view = sheet_views[0] + .children() + .filter(|n| n.has_tag_name("sheetView")) + .collect::>(); + + if sheet_view.len() != 1 { + return (0, 0); + } + + let pane = sheet_view[0] + .children() + .filter(|n| n.has_tag_name("pane")) + .collect::>(); + + // 18.18.53 ST_PaneState (Pane State) + // frozen, frozenSplit, split + if pane.len() == 1 && pane[0].attribute("state").unwrap_or("split") == "frozen" { + // TODO: Should we assert that topLeft is consistent? + // let top_left_cell = pane[0].attribute("topLeftCell").unwrap_or("A1").to_string(); + + frozen_columns = get_number(pane[0], "xSplit"); + frozen_rows = get_number(pane[0], "ySplit"); + } + (frozen_rows, frozen_columns) +} + +pub(super) struct SheetSettings { + pub id: u32, + pub name: String, + pub state: SheetState, + pub comments: Vec, +} + +pub(super) fn load_sheet( + archive: &mut zip::read::ZipArchive, + path: &str, + settings: SheetSettings, + worksheets: &[String], + tables: &HashMap, + shared_strings: &mut Vec, +) -> Result { + let sheet_name = &settings.name; + let sheet_id = settings.id; + let state = &settings.state; + + let mut file = archive.by_name(path)?; + let mut text = String::new(); + file.read_to_string(&mut text)?; + let doc = roxmltree::Document::parse(&text)?; + let ws = doc + .root() + .first_child() + .ok_or_else(|| XlsxError::Xml("Corrupt XML structure".to_string()))?; + let mut shared_formulas = Vec::new(); + + let dimension = load_dimension(ws); + + let (frozen_rows, frozen_columns) = get_frozen_rows_and_columns(ws); + + let cols = load_columns(ws)?; + let color = load_sheet_color(ws)?; + + // sheetData + // + // + // 0 + // + // + // C1+1 + // + // + + // holds the row heights + let mut rows = Vec::new(); + let mut sheet_data = SheetData::new(); + let sheet_data_nodes = ws + .children() + .filter(|n| n.has_tag_name("sheetData")) + .collect::>()[0]; + + let default_row_height = 14.5; + + // holds a map from the formula index in Excel to the index in IronCalc + let mut index_map = HashMap::new(); + for row in sheet_data_nodes.children() { + // This is the row number 1-indexed + let row_index = get_attribute(&row, "r")?.parse::()?; + // `spans` is not used in IronCalc at the moment (it's an optimization) + // let spans = row.attribute("spans"); + // This is the height of the row + let has_height_attribute; + let height = match row.attribute("ht") { + Some(s) => { + has_height_attribute = true; + s.parse::().unwrap_or(default_row_height) + } + None => { + has_height_attribute = false; + default_row_height + } + }; + let custom_height = matches!(row.attribute("customHeight"), Some("1")); + // The height of the row is always the visible height of the row + // If custom_height is false that means the height was calculated automatically: + // for example because a cell has many lines or a larger font + + let row_style = match row.attribute("s") { + Some(s) => s.parse::().unwrap_or(0), + None => 0, + }; + let custom_format = matches!(row.attribute("customFormat"), Some("1")); + let hidden = matches!(row.attribute("hidden"), Some("1")); + + if custom_height || custom_format || row_style != 0 || has_height_attribute || hidden { + rows.push(Row { + r: row_index, + height, + s: row_style, + custom_height, + custom_format, + hidden, + }); + } + + // Unused attributes: + // * thickBot, thickTop, ph, collapsed, outlineLevel + + let mut data_row = HashMap::new(); + + // 18.3.1.4 c (Cell) + // Child Elements: + // * v: Cell value + // * is: Rich Text Inline (not used in IronCalc) + // * f: Formula + // Attributes: + // r: reference. A1 style + // s: style index + // t: cell type + // Unused attributes + // cm (cell metadata), ph (Show Phonetic), vm (value metadata) + for cell in row.children() { + let cell_ref = get_attribute(&cell, "r")?; + let column_letter = get_column_from_ref(cell_ref); + let column = column_to_number(column_letter.as_str()).map_err(XlsxError::Xml)?; + + let value_metadata = cell.attribute("vm"); + + // We check the value "v" child. + let vs: Vec = cell.children().filter(|n| n.has_tag_name("v")).collect(); + let cell_value = if vs.len() == 1 { + Some(vs[0].text().unwrap_or("")) + } else { + None + }; + + // type, the default type being "n" for number + // If the cell does not have a value is an empty cell + let cell_type = match cell.attribute("t") { + Some(t) => t, + None => { + if cell_value.is_none() { + "empty" + } else { + "n" + } + } + }; + + // style index, the default style is 0 + let cell_style = match cell.attribute("s") { + Some(s) => s.parse::().unwrap_or(0), + None => 0, + }; + + // Check for formula + // In Excel some formulas are shared and some are not, but in IronCalc all formulas are shared + // A cell with a "non-shared" formula is like: + // + // C2+1 + // 3 + // + // A cell with a shared formula will be either a "mother" cell: + // + // C2+1 + // 3 + // + // Or a "daughter" cell: + // + // + // 4 + // + // In IronCalc two cells have the same formula iff the R1C1 representation is the same + // TODO: This algorithm could end up with "repeated" shared formulas + // We could solve that with a second transversal. + let fs: Vec = cell.children().filter(|n| n.has_tag_name("f")).collect(); + let mut formula_index = -1; + if fs.len() == 1 { + // formula types: + // 18.18.6 ST_CellFormulaType (Formula Type) + // array (Array Formula) Formula is an array formula. + // dataTable (Table Formula) Formula is a data table formula. + // normal (Normal) Formula is a regular cell formula. (Default) + // shared (Shared Formula) Formula is part of a shared formula. + let formula_type = fs[0].attribute("t").unwrap_or("normal"); + match formula_type { + "shared" => { + // We have a shared formula + let si = get_attribute(&fs[0], "si")?; + let si = si.parse::()?; + match fs[0].attribute("ref") { + Some(_) => { + // It's the mother cell. We do not use the ref attribute in IronCalc + let formula = fs[0].text().unwrap_or("").to_string(); + let context = format!("{}!{}", sheet_name, cell_ref); + let formula = + from_a1_to_rc(formula, worksheets, context, tables.clone())?; + match index_map.get(&si) { + Some(index) => { + // The index for that formula already exists meaning we bumped into a daughter cell first + // TODO: Worth assert the content is a placeholder? + formula_index = *index; + shared_formulas.insert(formula_index as usize, formula); + } + None => { + // We haven't met any of the daughter cells + match get_formula_index(&formula, &shared_formulas) { + // The formula is already present, use that index + Some(index) => { + formula_index = index; + } + None => { + shared_formulas.push(formula); + formula_index = shared_formulas.len() as i32 - 1; + } + }; + index_map.insert(si, formula_index); + } + } + } + None => { + // It's a daughter cell + match index_map.get(&si) { + Some(index) => { + formula_index = *index; + } + None => { + // Haven't bumped into the mother cell yet. We insert a placeholder. + // Note that it is perfectly possible that the formula of the mother cell + // is already in the set of array formulas. This will lead to the above mention duplicity. + // This is not a problem + let placeholder = "".to_string(); + shared_formulas.push(placeholder); + formula_index = shared_formulas.len() as i32 - 1; + index_map.insert(si, formula_index); + } + } + } + } + } + "array" => { + return Err(XlsxError::NotImplemented("array formulas".to_string())); + } + "dataTable" => { + return Err(XlsxError::NotImplemented("data table formulas".to_string())); + } + "normal" => { + // Its a cell with a simple formula + let formula = fs[0].text().unwrap_or("").to_string(); + let context = format!("{}!{}", sheet_name, cell_ref); + let formula = from_a1_to_rc(formula, worksheets, context, tables.clone())?; + + match get_formula_index(&formula, &shared_formulas) { + Some(index) => formula_index = index, + None => { + shared_formulas.push(formula); + formula_index = shared_formulas.len() as i32 - 1; + } + } + } + _ => { + return Err(XlsxError::Xml(format!( + "Invalid formula type {:?}.", + formula_type, + ))); + } + } + } + let cell = get_cell_from_excel( + cell_value, + value_metadata, + cell_type, + cell_style, + formula_index, + sheet_name, + cell_ref, + shared_strings, + ); + data_row.insert(column, cell); + } + sheet_data.insert(row_index, data_row); + } + + let merge_cells = load_merge_cells(ws)?; + + // Conditional Formatting + // + // + // + // + // + // + // + // + // + // + // pageSetup + // + + Ok(Worksheet { + dimension, + cols, + rows, + shared_formulas, + sheet_data, + name: sheet_name.to_string(), + sheet_id, + state: state.to_owned(), + color, + merge_cells, + comments: settings.comments, + frozen_rows, + frozen_columns, + }) +} + +pub(super) fn load_sheets( + archive: &mut zip::read::ZipArchive, + rels: &HashMap, + workbook: &WorkbookXML, + tables: &mut HashMap, + shared_strings: &mut Vec, +) -> Result, XlsxError> { + // load comments and tables + let mut comments = HashMap::new(); + for sheet in &workbook.worksheets { + let rel = &rels[&sheet.id]; + if rel.rel_type.ends_with("worksheet") { + let path = &rel.target; + let path = if let Some(p) = path.strip_prefix('/') { + p.to_string() + } else { + format!("xl/{path}") + }; + comments.insert( + &sheet.id, + load_sheet_rels(archive, &path, tables, &sheet.name)?, + ); + } + } + + // load all sheets + let worksheets: &Vec = &workbook.worksheets.iter().map(|s| s.name.clone()).collect(); + let mut sheets = Vec::new(); + for sheet in &workbook.worksheets { + let sheet_name = &sheet.name; + let rel_id = &sheet.id; + let state = &sheet.state; + let rel = &rels[rel_id]; + if rel.rel_type.ends_with("worksheet") { + let path = &rel.target; + let path = if let Some(p) = path.strip_prefix('/') { + p.to_string() + } else { + format!("xl/{path}") + }; + let settings = SheetSettings { + name: sheet_name.to_string(), + id: sheet.sheet_id, + state: state.clone(), + comments: comments.get(rel_id).expect("").to_vec(), + }; + sheets.push(load_sheet( + archive, + &path, + settings, + worksheets, + tables, + shared_strings, + )?); + } + } + Ok(sheets) +} diff --git a/xlsx/src/lib.rs b/xlsx/src/lib.rs new file mode 100644 index 0000000..39789da --- /dev/null +++ b/xlsx/src/lib.rs @@ -0,0 +1,61 @@ +//! This cate reads an xlsx file and transforms it into an internal representation ([`Model`]). +//! An `xlsx` is a zip file containing a set of folders and `xml` files. The IronCalc json structure mimics the relevant parts of the Excel zip. +//! Although the xlsx structure is quite complicated, it's essentials regarding the spreadsheet technology are easier to grasp. +//! +//! The simplest workbook folder structure might look like this: +//! +//! ```text +//! docProps +//! app.xml +//! core.xml +//! +//! _rels +//! .rels +//! +//! xl +//! _rels +//! workbook.xml.rels +//! theme +//! theme1.xml +//! worksheets +//! sheet1.xml +//! calcChain.xml +//! styles.xml +//! workbook.xml +//! sharedStrings.xml +//! +//! [Content_Types].xml +//! ``` +//! +//! Note that more complicated workbooks will have many more files and folders. +//! For instance charts, pivot tables, comments, tables,... +//! +//! The relevant json structure in IronCalc will be: +//! +//! ```json +//! { +//! "name": "Workbook1", +//! "defined_names": [], +//! "shared_strings": [], +//! "worksheets": [], +//! "styles": { +//! "num_fmts": [], +//! "fonts": [], +//! "fills": [], +//! "borders": [], +//! "cell_style_xfs": [], +//! "cell_styles" : [], +//! "cell_xfs": [] +//! } +//! } +//! ``` +//! +//! Note that there is not a 1-1 correspondence but there is a close resemblance. +//! +//! [`Model`]: ../ironcalc/struct.Model.html + +pub mod compare; +pub mod error; +pub mod export; +pub mod import; +pub use ironcalc_base as base; diff --git a/xlsx/tests/basic_text.xlsx b/xlsx/tests/basic_text.xlsx new file mode 100644 index 0000000..b168d73 Binary files /dev/null and b/xlsx/tests/basic_text.xlsx differ diff --git a/xlsx/tests/calc_test_no_export/tables.xlsx b/xlsx/tests/calc_test_no_export/tables.xlsx new file mode 100644 index 0000000..4a5db1a Binary files /dev/null and b/xlsx/tests/calc_test_no_export/tables.xlsx differ diff --git a/xlsx/tests/calc_tests/ABS.xlsx b/xlsx/tests/calc_tests/ABS.xlsx new file mode 100644 index 0000000..525e928 Binary files /dev/null and b/xlsx/tests/calc_tests/ABS.xlsx differ diff --git a/xlsx/tests/calc_tests/ATAN2_POWER.xlsx b/xlsx/tests/calc_tests/ATAN2_POWER.xlsx new file mode 100644 index 0000000..5035a3d Binary files /dev/null and b/xlsx/tests/calc_tests/ATAN2_POWER.xlsx differ diff --git a/xlsx/tests/calc_tests/AVERAGE.xlsx b/xlsx/tests/calc_tests/AVERAGE.xlsx new file mode 100644 index 0000000..ffb0683 Binary files /dev/null and b/xlsx/tests/calc_tests/AVERAGE.xlsx differ diff --git a/xlsx/tests/calc_tests/BESSEL.xlsx b/xlsx/tests/calc_tests/BESSEL.xlsx new file mode 100644 index 0000000..9bf5186 Binary files /dev/null and b/xlsx/tests/calc_tests/BESSEL.xlsx differ diff --git a/xlsx/tests/calc_tests/BITs.xlsx b/xlsx/tests/calc_tests/BITs.xlsx new file mode 100644 index 0000000..ccb46bf Binary files /dev/null and b/xlsx/tests/calc_tests/BITs.xlsx differ diff --git a/xlsx/tests/calc_tests/CHOOSE.xlsx b/xlsx/tests/calc_tests/CHOOSE.xlsx new file mode 100644 index 0000000..b7ee516 Binary files /dev/null and b/xlsx/tests/calc_tests/CHOOSE.xlsx differ diff --git a/xlsx/tests/calc_tests/COMPLEXs.xlsx b/xlsx/tests/calc_tests/COMPLEXs.xlsx new file mode 100644 index 0000000..99a5a1c Binary files /dev/null and b/xlsx/tests/calc_tests/COMPLEXs.xlsx differ diff --git a/xlsx/tests/calc_tests/CONCAT.xlsx b/xlsx/tests/calc_tests/CONCAT.xlsx new file mode 100644 index 0000000..63aa9e0 Binary files /dev/null and b/xlsx/tests/calc_tests/CONCAT.xlsx differ diff --git a/xlsx/tests/calc_tests/CONCATENATE.xlsx b/xlsx/tests/calc_tests/CONCATENATE.xlsx new file mode 100644 index 0000000..9dd8e0b Binary files /dev/null and b/xlsx/tests/calc_tests/CONCATENATE.xlsx differ diff --git a/xlsx/tests/calc_tests/CONVERT.xlsx b/xlsx/tests/calc_tests/CONVERT.xlsx new file mode 100644 index 0000000..f24354e Binary files /dev/null and b/xlsx/tests/calc_tests/CONVERT.xlsx differ diff --git a/xlsx/tests/calc_tests/COUNT.xlsx b/xlsx/tests/calc_tests/COUNT.xlsx new file mode 100644 index 0000000..2cd72da Binary files /dev/null and b/xlsx/tests/calc_tests/COUNT.xlsx differ diff --git a/xlsx/tests/calc_tests/COUNTIF.xlsx b/xlsx/tests/calc_tests/COUNTIF.xlsx new file mode 100644 index 0000000..9b93cbc Binary files /dev/null and b/xlsx/tests/calc_tests/COUNTIF.xlsx differ diff --git a/xlsx/tests/calc_tests/COUNTIFS.xlsx b/xlsx/tests/calc_tests/COUNTIFS.xlsx new file mode 100644 index 0000000..1a9a3a1 Binary files /dev/null and b/xlsx/tests/calc_tests/COUNTIFS.xlsx differ diff --git a/xlsx/tests/calc_tests/CUMPRINC_CUMIPMT.xlsx b/xlsx/tests/calc_tests/CUMPRINC_CUMIPMT.xlsx new file mode 100644 index 0000000..4f145e7 Binary files /dev/null and b/xlsx/tests/calc_tests/CUMPRINC_CUMIPMT.xlsx differ diff --git a/xlsx/tests/calc_tests/DATE_DAY_MONTH_YEAR.xlsx b/xlsx/tests/calc_tests/DATE_DAY_MONTH_YEAR.xlsx new file mode 100644 index 0000000..3189d28 Binary files /dev/null and b/xlsx/tests/calc_tests/DATE_DAY_MONTH_YEAR.xlsx differ diff --git a/xlsx/tests/calc_tests/DB_DDB.xlsx b/xlsx/tests/calc_tests/DB_DDB.xlsx new file mode 100644 index 0000000..bd1629d Binary files /dev/null and b/xlsx/tests/calc_tests/DB_DDB.xlsx differ diff --git a/xlsx/tests/calc_tests/DOLLARs.xlsx b/xlsx/tests/calc_tests/DOLLARs.xlsx new file mode 100644 index 0000000..a7dc78d Binary files /dev/null and b/xlsx/tests/calc_tests/DOLLARs.xlsx differ diff --git a/xlsx/tests/calc_tests/EOMONTH.xlsx b/xlsx/tests/calc_tests/EOMONTH.xlsx new file mode 100644 index 0000000..bf3bc99 Binary files /dev/null and b/xlsx/tests/calc_tests/EOMONTH.xlsx differ diff --git a/xlsx/tests/calc_tests/ERFs.xlsx b/xlsx/tests/calc_tests/ERFs.xlsx new file mode 100644 index 0000000..cc15e27 Binary files /dev/null and b/xlsx/tests/calc_tests/ERFs.xlsx differ diff --git a/xlsx/tests/calc_tests/ERROR.TYPE.xlsx b/xlsx/tests/calc_tests/ERROR.TYPE.xlsx new file mode 100644 index 0000000..f42dc8b Binary files /dev/null and b/xlsx/tests/calc_tests/ERROR.TYPE.xlsx differ diff --git a/xlsx/tests/calc_tests/EXACT.xlsx b/xlsx/tests/calc_tests/EXACT.xlsx new file mode 100644 index 0000000..d106e04 Binary files /dev/null and b/xlsx/tests/calc_tests/EXACT.xlsx differ diff --git a/xlsx/tests/calc_tests/FIND_SEARCH.xlsx b/xlsx/tests/calc_tests/FIND_SEARCH.xlsx new file mode 100644 index 0000000..3a60ad2 Binary files /dev/null and b/xlsx/tests/calc_tests/FIND_SEARCH.xlsx differ diff --git a/xlsx/tests/calc_tests/FV.xlsx b/xlsx/tests/calc_tests/FV.xlsx new file mode 100644 index 0000000..9df63db Binary files /dev/null and b/xlsx/tests/calc_tests/FV.xlsx differ diff --git a/xlsx/tests/calc_tests/GESTEP_DELTA.xlsx b/xlsx/tests/calc_tests/GESTEP_DELTA.xlsx new file mode 100644 index 0000000..51ccbcd Binary files /dev/null and b/xlsx/tests/calc_tests/GESTEP_DELTA.xlsx differ diff --git a/xlsx/tests/calc_tests/HVLOOKUP.xlsx b/xlsx/tests/calc_tests/HVLOOKUP.xlsx new file mode 100644 index 0000000..1444b40 Binary files /dev/null and b/xlsx/tests/calc_tests/HVLOOKUP.xlsx differ diff --git a/xlsx/tests/calc_tests/IFNA.xlsx b/xlsx/tests/calc_tests/IFNA.xlsx new file mode 100644 index 0000000..e508d89 Binary files /dev/null and b/xlsx/tests/calc_tests/IFNA.xlsx differ diff --git a/xlsx/tests/calc_tests/IFS.xlsx b/xlsx/tests/calc_tests/IFS.xlsx new file mode 100644 index 0000000..9897c68 Binary files /dev/null and b/xlsx/tests/calc_tests/IFS.xlsx differ diff --git a/xlsx/tests/calc_tests/IPMT_PPMT.xlsx b/xlsx/tests/calc_tests/IPMT_PPMT.xlsx new file mode 100644 index 0000000..0221c65 Binary files /dev/null and b/xlsx/tests/calc_tests/IPMT_PPMT.xlsx differ diff --git a/xlsx/tests/calc_tests/IRR.xlsx b/xlsx/tests/calc_tests/IRR.xlsx new file mode 100644 index 0000000..3421951 Binary files /dev/null and b/xlsx/tests/calc_tests/IRR.xlsx differ diff --git a/xlsx/tests/calc_tests/ISPMT.xlsx b/xlsx/tests/calc_tests/ISPMT.xlsx new file mode 100644 index 0000000..9b770d9 Binary files /dev/null and b/xlsx/tests/calc_tests/ISPMT.xlsx differ diff --git a/xlsx/tests/calc_tests/ISREF_ISFORMULA_ISODD_ISEVEN.xlsx b/xlsx/tests/calc_tests/ISREF_ISFORMULA_ISODD_ISEVEN.xlsx new file mode 100644 index 0000000..6815c25 Binary files /dev/null and b/xlsx/tests/calc_tests/ISREF_ISFORMULA_ISODD_ISEVEN.xlsx differ diff --git a/xlsx/tests/calc_tests/IS_INFORMATION.xlsx b/xlsx/tests/calc_tests/IS_INFORMATION.xlsx new file mode 100644 index 0000000..90bad49 Binary files /dev/null and b/xlsx/tests/calc_tests/IS_INFORMATION.xlsx differ diff --git a/xlsx/tests/calc_tests/MATCH.xlsx b/xlsx/tests/calc_tests/MATCH.xlsx new file mode 100644 index 0000000..c8341f9 Binary files /dev/null and b/xlsx/tests/calc_tests/MATCH.xlsx differ diff --git a/xlsx/tests/calc_tests/MIN_MAX.xlsx b/xlsx/tests/calc_tests/MIN_MAX.xlsx new file mode 100644 index 0000000..0000bc8 Binary files /dev/null and b/xlsx/tests/calc_tests/MIN_MAX.xlsx differ diff --git a/xlsx/tests/calc_tests/MIRR.xlsx b/xlsx/tests/calc_tests/MIRR.xlsx new file mode 100644 index 0000000..d9bf4d4 Binary files /dev/null and b/xlsx/tests/calc_tests/MIRR.xlsx differ diff --git a/xlsx/tests/calc_tests/NOMINAL_EFFECT.xlsx b/xlsx/tests/calc_tests/NOMINAL_EFFECT.xlsx new file mode 100644 index 0000000..b647b77 Binary files /dev/null and b/xlsx/tests/calc_tests/NOMINAL_EFFECT.xlsx differ diff --git a/xlsx/tests/calc_tests/NPER.xlsx b/xlsx/tests/calc_tests/NPER.xlsx new file mode 100644 index 0000000..f7e46f5 Binary files /dev/null and b/xlsx/tests/calc_tests/NPER.xlsx differ diff --git a/xlsx/tests/calc_tests/NPV.xlsx b/xlsx/tests/calc_tests/NPV.xlsx new file mode 100644 index 0000000..89eea0b Binary files /dev/null and b/xlsx/tests/calc_tests/NPV.xlsx differ diff --git a/xlsx/tests/calc_tests/NUMBER_SYSTEMS.xlsx b/xlsx/tests/calc_tests/NUMBER_SYSTEMS.xlsx new file mode 100644 index 0000000..b4fa9c6 Binary files /dev/null and b/xlsx/tests/calc_tests/NUMBER_SYSTEMS.xlsx differ diff --git a/xlsx/tests/calc_tests/PDURATION.xlsx b/xlsx/tests/calc_tests/PDURATION.xlsx new file mode 100644 index 0000000..9ed08b0 Binary files /dev/null and b/xlsx/tests/calc_tests/PDURATION.xlsx differ diff --git a/xlsx/tests/calc_tests/PMT.xlsx b/xlsx/tests/calc_tests/PMT.xlsx new file mode 100644 index 0000000..9420e15 Binary files /dev/null and b/xlsx/tests/calc_tests/PMT.xlsx differ diff --git a/xlsx/tests/calc_tests/PRODUCT.xlsx b/xlsx/tests/calc_tests/PRODUCT.xlsx new file mode 100644 index 0000000..bba14f8 Binary files /dev/null and b/xlsx/tests/calc_tests/PRODUCT.xlsx differ diff --git a/xlsx/tests/calc_tests/PV.xlsx b/xlsx/tests/calc_tests/PV.xlsx new file mode 100644 index 0000000..26be835 Binary files /dev/null and b/xlsx/tests/calc_tests/PV.xlsx differ diff --git a/xlsx/tests/calc_tests/RATE.xlsx b/xlsx/tests/calc_tests/RATE.xlsx new file mode 100644 index 0000000..fb9bdac Binary files /dev/null and b/xlsx/tests/calc_tests/RATE.xlsx differ diff --git a/xlsx/tests/calc_tests/REPT.xlsx b/xlsx/tests/calc_tests/REPT.xlsx new file mode 100644 index 0000000..2fa4ab1 Binary files /dev/null and b/xlsx/tests/calc_tests/REPT.xlsx differ diff --git a/xlsx/tests/calc_tests/ROUND.xlsx b/xlsx/tests/calc_tests/ROUND.xlsx new file mode 100644 index 0000000..47a445c Binary files /dev/null and b/xlsx/tests/calc_tests/ROUND.xlsx differ diff --git a/xlsx/tests/calc_tests/ROW_COLUM.xlsx b/xlsx/tests/calc_tests/ROW_COLUM.xlsx new file mode 100644 index 0000000..e74331f Binary files /dev/null and b/xlsx/tests/calc_tests/ROW_COLUM.xlsx differ diff --git a/xlsx/tests/calc_tests/RRI.xlsx b/xlsx/tests/calc_tests/RRI.xlsx new file mode 100644 index 0000000..80d83d5 Binary files /dev/null and b/xlsx/tests/calc_tests/RRI.xlsx differ diff --git a/xlsx/tests/calc_tests/SLN_SYD.xlsx b/xlsx/tests/calc_tests/SLN_SYD.xlsx new file mode 100644 index 0000000..18524a3 Binary files /dev/null and b/xlsx/tests/calc_tests/SLN_SYD.xlsx differ diff --git a/xlsx/tests/calc_tests/SQRT_SQRTPI.xlsx b/xlsx/tests/calc_tests/SQRT_SQRTPI.xlsx new file mode 100644 index 0000000..82c8af9 Binary files /dev/null and b/xlsx/tests/calc_tests/SQRT_SQRTPI.xlsx differ diff --git a/xlsx/tests/calc_tests/STRING_HANDLING.xlsx b/xlsx/tests/calc_tests/STRING_HANDLING.xlsx new file mode 100644 index 0000000..588f1d1 Binary files /dev/null and b/xlsx/tests/calc_tests/STRING_HANDLING.xlsx differ diff --git a/xlsx/tests/calc_tests/SUBSTITUTE.xlsx b/xlsx/tests/calc_tests/SUBSTITUTE.xlsx new file mode 100644 index 0000000..611b2f8 Binary files /dev/null and b/xlsx/tests/calc_tests/SUBSTITUTE.xlsx differ diff --git a/xlsx/tests/calc_tests/SUMIFS.xlsx b/xlsx/tests/calc_tests/SUMIFS.xlsx new file mode 100644 index 0000000..d52c3c7 Binary files /dev/null and b/xlsx/tests/calc_tests/SUMIFS.xlsx differ diff --git a/xlsx/tests/calc_tests/SUMIF_AVERAGE_IF.xlsx b/xlsx/tests/calc_tests/SUMIF_AVERAGE_IF.xlsx new file mode 100644 index 0000000..8fe67a2 Binary files /dev/null and b/xlsx/tests/calc_tests/SUMIF_AVERAGE_IF.xlsx differ diff --git a/xlsx/tests/calc_tests/TBILLs.xlsx b/xlsx/tests/calc_tests/TBILLs.xlsx new file mode 100644 index 0000000..114aa11 Binary files /dev/null and b/xlsx/tests/calc_tests/TBILLs.xlsx differ diff --git a/xlsx/tests/calc_tests/TEXT.xlsx b/xlsx/tests/calc_tests/TEXT.xlsx new file mode 100644 index 0000000..4a593bc Binary files /dev/null and b/xlsx/tests/calc_tests/TEXT.xlsx differ diff --git a/xlsx/tests/calc_tests/TEXTBEFORE_TEXTAFTER.xlsx b/xlsx/tests/calc_tests/TEXTBEFORE_TEXTAFTER.xlsx new file mode 100644 index 0000000..b0dc6bb Binary files /dev/null and b/xlsx/tests/calc_tests/TEXTBEFORE_TEXTAFTER.xlsx differ diff --git a/xlsx/tests/calc_tests/TEXTJOIN.xlsx b/xlsx/tests/calc_tests/TEXTJOIN.xlsx new file mode 100644 index 0000000..7beffa7 Binary files /dev/null and b/xlsx/tests/calc_tests/TEXTJOIN.xlsx differ diff --git a/xlsx/tests/calc_tests/TYPE.xlsx b/xlsx/tests/calc_tests/TYPE.xlsx new file mode 100644 index 0000000..3afc98d Binary files /dev/null and b/xlsx/tests/calc_tests/TYPE.xlsx differ diff --git a/xlsx/tests/calc_tests/T_VALUE_VALUETOTEXT.xlsx b/xlsx/tests/calc_tests/T_VALUE_VALUETOTEXT.xlsx new file mode 100644 index 0000000..b1da5a5 Binary files /dev/null and b/xlsx/tests/calc_tests/T_VALUE_VALUETOTEXT.xlsx differ diff --git a/xlsx/tests/calc_tests/XIRR.xlsx b/xlsx/tests/calc_tests/XIRR.xlsx new file mode 100644 index 0000000..ef18f3a Binary files /dev/null and b/xlsx/tests/calc_tests/XIRR.xlsx differ diff --git a/xlsx/tests/calc_tests/XLOOKUP.xlsx b/xlsx/tests/calc_tests/XLOOKUP.xlsx new file mode 100644 index 0000000..0acf680 Binary files /dev/null and b/xlsx/tests/calc_tests/XLOOKUP.xlsx differ diff --git a/xlsx/tests/calc_tests/XNPV.xlsx b/xlsx/tests/calc_tests/XNPV.xlsx new file mode 100644 index 0000000..7b900e5 Binary files /dev/null and b/xlsx/tests/calc_tests/XNPV.xlsx differ diff --git a/xlsx/tests/calc_tests/arithmetic.xlsx b/xlsx/tests/calc_tests/arithmetic.xlsx new file mode 100644 index 0000000..c8005a9 Binary files /dev/null and b/xlsx/tests/calc_tests/arithmetic.xlsx differ diff --git a/xlsx/tests/calc_tests/defined_names.xlsx b/xlsx/tests/calc_tests/defined_names.xlsx new file mode 100644 index 0000000..a92ea9d Binary files /dev/null and b/xlsx/tests/calc_tests/defined_names.xlsx differ diff --git a/xlsx/tests/calc_tests/defined_names_for_unit_test.xlsx b/xlsx/tests/calc_tests/defined_names_for_unit_test.xlsx new file mode 100644 index 0000000..bc8deb5 Binary files /dev/null and b/xlsx/tests/calc_tests/defined_names_for_unit_test.xlsx differ diff --git a/xlsx/tests/calc_tests/issue_341.xlsx b/xlsx/tests/calc_tests/issue_341.xlsx new file mode 100644 index 0000000..f8b85d5 Binary files /dev/null and b/xlsx/tests/calc_tests/issue_341.xlsx differ diff --git a/xlsx/tests/calc_tests/logical.xlsx b/xlsx/tests/calc_tests/logical.xlsx new file mode 100644 index 0000000..c95c0a8 Binary files /dev/null and b/xlsx/tests/calc_tests/logical.xlsx differ diff --git a/xlsx/tests/calc_tests/percentage.xlsx b/xlsx/tests/calc_tests/percentage.xlsx new file mode 100644 index 0000000..c29f87d Binary files /dev/null and b/xlsx/tests/calc_tests/percentage.xlsx differ diff --git a/xlsx/tests/calc_tests/quotes.xlsx b/xlsx/tests/calc_tests/quotes.xlsx new file mode 100644 index 0000000..42bf965 Binary files /dev/null and b/xlsx/tests/calc_tests/quotes.xlsx differ diff --git a/xlsx/tests/calc_tests/range_operator.xlsx b/xlsx/tests/calc_tests/range_operator.xlsx new file mode 100644 index 0000000..32b1f6c Binary files /dev/null and b/xlsx/tests/calc_tests/range_operator.xlsx differ diff --git a/xlsx/tests/calc_tests/simple_functions.xlsx b/xlsx/tests/calc_tests/simple_functions.xlsx new file mode 100644 index 0000000..35491fd Binary files /dev/null and b/xlsx/tests/calc_tests/simple_functions.xlsx differ diff --git a/xlsx/tests/calc_tests/trigonometric_functions.xlsx b/xlsx/tests/calc_tests/trigonometric_functions.xlsx new file mode 100644 index 0000000..65def21 Binary files /dev/null and b/xlsx/tests/calc_tests/trigonometric_functions.xlsx differ diff --git a/xlsx/tests/example.json b/xlsx/tests/example.json new file mode 100644 index 0000000..00b9b34 --- /dev/null +++ b/xlsx/tests/example.json @@ -0,0 +1,2524 @@ +{ + "settings": { + "tz": "UTC", + "locale": "en" + }, + "metadata": { + "application": "Microsoft Excel", + "app_version": "16.0300", + "creator": "nicol", + "last_modified_by": "nicol", + "created": "2020-08-06T21:20:53Z", + "last_modified": "2020-11-20T16:24:35Z" + }, + "tables": { + "Table1": { + "name": "Table1", + "display_name": "Table1", + "sheet_name": "Table", + "reference": "A1:D4", + "totals_row_count": 0, + "header_row_count": 1, + "columns": [ + { "id": 1, "name": "Cars" }, + { "id": 2, "name": "Year" }, + { "id": 3, "name": "Make" }, + { "id": 4, "name": "Other" } + ], + "style_info": { + "show_first_column": false, + "show_last_column": false, + "show_row_stripes": true, + "show_column_stripes": false + } + } + }, + "shared_strings": [ + "A string", + "Long column", + "Short column", + "Red", + "in", + "yellow", + "merged cells", + "Bold text", + "Some colours", + "quantum", + "A def name", + "Hola Tu", + "High Row", + "Tres", + "Noch eine", + "sredtg", + "This was created fourth, in the third position then moved second", + "Units", + "Hola", + "Cars", + "Year", + "Make", + "R5", + "Renault", + "C6", + "Citroen", + "MB", + "Mercedes", + "Other", + "A", + "B", + "Row Labels", + "Grand Total", + "Sum of Year", + "Just a coment", + "42 too", + "Red only row", + "Also read only row", + "Read only cell", + "Original Themes", + "Standard Colors", + "With a tint" + ], + "defined_names": [ + { + "name": "answer", + "formula": "shared!$G$5", + "sheet_id": 9 + }, + { + "name": "answer2", + "formula": "Sheet1!$I$6", + "sheet_id": 1 + }, + { + "name": "local_thing", + "formula": "Second!$B$1:$B$9", + "sheet_id": 3 + }, + { + "name": "numbers", + "formula": "Sheet1!$A$16:$A$18" + }, + { + "name": "quantum", + "formula": "Sheet1!$C$14" + } + ], + "worksheets": [ + { + "dimension": "A1:O33", + "cols": [ + { + "min": 5, + "max": 5, + "width": 38.26953125, + "custom_width": true + }, + { + "min": 6, + "max": 6, + "width": 9.1796875, + "custom_width": false, + "style": 1 + }, + { + "min": 8, + "max": 8, + "width": 4, + "custom_width": true + } + ], + "rows": [ + { + "height": 13, + "r": 7, + "custom_format": false, + "custom_height": true, + "s": 0 + }, + { + "height": 20, + "r": 8, + "custom_format": false, + "custom_height": true, + "s": 0 + }, + { + "height": 29.5, + "r": 9, + "custom_format": false, + "custom_height": true, + "s": 0 + }, + { + "height": 21.0, + "r": 15, + "custom_format": false, + "custom_height": false, + "s": 0 + }, + { + "height": 15.0, + "r": 17, + "custom_format": false, + "custom_height": false, + "s": 0 + }, + { + "height": 15.0, + "r": 20, + "custom_format": false, + "custom_height": false, + "s": 0 + }, + { + "height": 15.0, + "r": 22, + "custom_format": false, + "custom_height": false, + "s": 0 + }, + { + "height": 15.5, + "r": 23, + "custom_format": false, + "custom_height": false, + "s": 0 + }, + { + "height": 15.0, + "r": 24, + "custom_format": false, + "custom_height": false, + "s": 0 + } + ], + "name": "Sheet1", + "sheet_data": { + "1": { + "1": { + "t": "s", + "si": 0, + "s": 0 + }, + "3": { + "t": "n", + "v": 1, + "s": 0 + }, + "4": { + "t": "fn", + "f": 0, + "v": 2, + "s": 0 + }, + "5": { + "t": "s", + "si": 1, + "s": 0 + }, + "6": { + "t": "s", + "si": 3, + "s": 1 + }, + "8": { + "t": "s", + "si": 2, + "s": 0 + } + }, + "2": { + "1": { + "t": "n", + "v": 222, + "s": 0 + }, + "3": { + "t": "n", + "v": 2, + "s": 0 + }, + "4": { + "t": "fn", + "f": 0, + "v": 3, + "s": 0 + }, + "6": { + "t": "s", + "si": 4, + "s": 1 + }, + "12": { + "t": "s", + "si": 7, + "s": 2 + } + }, + "3": { + "2": { + "t": "fn", + "f": 1, + "v": 0, + "s": 0 + }, + "3": { + "t": "n", + "v": 3, + "s": 0 + }, + "4": { + "t": "fn", + "f": 0, + "v": 4, + "s": 0 + }, + "6": { + "t": "s", + "si": 5, + "s": 1 + } + }, + "6": { + "9": { + "t": "s", + "si": 35, + "s": 0 + } + }, + "7": { + "11": { + "t": "s", + "si": 6, + "s": 17 + }, + "12": { + "t": "empty", + "s": 17 + } + }, + "8": { + "11": { + "t": "empty", + "s": 17 + }, + "12": { + "t": "empty", + "s": 17 + }, + "14": { + "t": "empty", + "s": 3 + }, + "15": { + "t": "empty", + "s": 3 + } + }, + "9": { + "5": { + "t": "s", + "si": 12, + "s": 0 + }, + "11": { + "t": "empty", + "s": 17 + }, + "12": { + "t": "empty", + "s": 17 + }, + "14": { + "t": "s", + "si": 8, + "s": 3 + }, + "15": { + "t": "empty", + "s": 3 + } + }, + "10": { + "11": { + "t": "empty", + "s": 17 + }, + "12": { + "t": "empty", + "s": 17 + } + }, + "13": { + "3": { + "t": "s", + "si": 10, + "s": 0 + } + }, + "14": { + "3": { + "t": "s", + "si": 9, + "s": 0 + }, + "4": { + "t": "str", + "f": 2, + "v": "quantum", + "s": 0 + } + }, + "15": { + "11": { + "t": "s", + "si": 11, + "s": 0 + } + }, + "16": { + "1": { + "t": "n", + "v": 2, + "s": 0 + }, + "8": { + "t": "empty", + "s": 12 + } + }, + "17": { + "1": { + "t": "n", + "v": 6, + "s": 0 + }, + "8": { + "t": "empty", + "s": 14 + } + }, + "18": { + "1": { + "t": "n", + "v": 7, + "s": 0 + }, + "3": { + "t": "fn", + "f": 3, + "v": 15, + "s": 0 + }, + "8": { + "t": "n", + "v": 1, + "s": 18 + }, + "9": { + "t": "empty", + "s": 19 + }, + "10": { + "t": "empty", + "s": 20 + } + }, + "19": { + "8": { + "t": "empty", + "s": 21 + }, + "9": { + "t": "empty", + "s": 22 + }, + "10": { + "t": "empty", + "s": 23 + } + }, + "20": { + "8": { + "t": "empty", + "s": 24 + }, + "9": { + "t": "empty", + "s": 25 + }, + "10": { + "t": "empty", + "s": 26 + } + }, + "21": { + "8": { + "t": "empty", + "s": 13 + } + }, + "22": { + "8": { + "t": "empty", + "s": 12 + } + }, + "23": { + "2": { + "t": "s", + "si": 18, + "s": 5 + }, + "3": { + "t": "empty", + "s": 6 + } + }, + "24": {}, + "26": { + "1": { + "t": "e", + "ei": 2, + "s": 0 + }, + "3": { + "t": "str", + "f": 4, + "v": "#N/A", + "s": 0 + }, + "4": { + "t": "fb", + "f": 5, + "v": true, + "s": 0 + } + }, + "27": { + "1": { + "t": "fe", + "f": 6, + "ei": 4, + "s": 0, + "o": "Sheet1!A27", + "m": "#N/A" + }, + "2": { + "t": "fe", + "f": 6, + "ei": 4, + "s": 0, + "o": "Sheet1!B27", + "m": "#N/A" + }, + "3": { + "t": "e", + "ei": 4, + "s": 0 + }, + "4": { + "t": "fb", + "f": 5, + "v": true, + "s": 0 + } + }, + "28": { + "1": { + "t": "fe", + "f": 6, + "ei": 4, + "s": 0, + "o": "Sheet1!A28", + "m": "#N/A" + }, + "2": { + "t": "fe", + "f": 6, + "ei": 4, + "s": 0, + "o": "Sheet1!B28", + "m": "#N/A" + }, + "4": { + "t": "fb", + "f": 5, + "v": true, + "s": 0 + } + }, + "29": { + "1": { + "t": "fe", + "f": 6, + "ei": 4, + "s": 0, + "o": "Sheet1!A29", + "m": "#N/A" + }, + "2": { + "t": "fe", + "f": 6, + "ei": 4, + "s": 0, + "o": "Sheet1!B29", + "m": "#N/A" + }, + "4": { + "t": "fb", + "f": 5, + "v": true, + "s": 0 + } + }, + "32": { + "4": { + "t": "fb", + "f": 7, + "v": false, + "s": 0 + } + }, + "33": { + "4": { + "t": "fb", + "f": 7, + "v": true, + "s": 0 + } + } + }, + "shared_formulas": [ + "R[0]C[-1]+1", + "R[0]C[-1]", + "quantum", + "SUM(numbers)", + "\"#N/A\"", + "ISERROR(R[0]C[-3])", + "NA()", + "ISERROR(R[-6]C[-1])" + ], + "sheet_id": 1, + "state": "visible", + "merge_cells": ["K7:L10", "H18:J20"], + "comments": [ + { + "text": "nicol:\nThis cell has bold! Text. Náguara!", + "author_name": "", + "cell_ref": "L2" + }, + { + "text": "nicol:\nThere is a coment here, you know\n", + "author_name": "", + "cell_ref": "K7" + }, + { + "text": "nicol:\nNew comment", + "author_name": "", + "cell_ref": "E9" + }, + { + "text": "nicol:\nThere is a 15 here.\n", + "author_name": "", + "cell_ref": "C18" + } + ] + }, + { + "dimension": "A1:R19", + "cols": [ + { + "min": 1, + "max": 1, + "width": 17.26953125, + "custom_width": true + }, + { + "min": 4, + "max": 4, + "width": 18.54296875, + "custom_width": true + }, + { + "min": 8, + "max": 8, + "width": 25.6328125, + "custom_width": true + } + ], + "rows": [], + "name": "Second", + "sheet_data": { + "1": { + "1": { + "t": "s", + "si": 13, + "s": 0 + }, + "2": { + "t": "n", + "v": 1, + "s": 0 + }, + "4": { + "t": "n", + "v": 1, + "s": 0 + }, + "5": { + "t": "fn", + "f": 0, + "v": 45, + "s": 0 + }, + "8": { + "t": "s", + "si": 39, + "s": 0 + }, + "9": { + "t": "empty", + "s": 27 + }, + "10": { + "t": "empty", + "s": 28 + }, + "11": { + "t": "empty", + "s": 29 + }, + "12": { + "t": "empty", + "s": 30 + }, + "13": { + "t": "empty", + "s": 31 + }, + "14": { + "t": "empty", + "s": 32 + }, + "15": { + "t": "empty", + "s": 33 + }, + "16": { + "t": "empty", + "s": 34 + }, + "17": { + "t": "empty", + "s": 35 + }, + "18": { + "t": "empty", + "s": 36 + } + }, + "2": { + "1": { + "t": "s", + "si": 14, + "s": 0 + }, + "2": { + "t": "n", + "v": 2, + "s": 0 + }, + "4": { + "t": "n", + "v": -2, + "s": 0 + }, + "8": { + "t": "s", + "si": 41, + "s": 0 + }, + "9": { + "t": "empty", + "s": 37 + }, + "10": { + "t": "empty", + "s": 38 + }, + "11": { + "t": "empty", + "s": 39 + }, + "12": { + "t": "empty", + "s": 40 + }, + "13": { + "t": "empty", + "s": 41 + }, + "14": { + "t": "empty", + "s": 42 + }, + "15": { + "t": "empty", + "s": 43 + }, + "16": { + "t": "empty", + "s": 44 + }, + "17": { + "t": "empty", + "s": 45 + }, + "18": { + "t": "empty", + "s": 46 + } + }, + "3": { + "2": { + "t": "n", + "v": 3, + "s": 0 + }, + "4": { + "t": "n", + "v": 3, + "s": 0 + } + }, + "4": { + "2": { + "t": "n", + "v": 4, + "s": 0 + }, + "4": { + "t": "n", + "v": -4, + "s": 0 + } + }, + "5": { + "2": { + "t": "n", + "v": 5, + "s": 0 + }, + "4": { + "t": "n", + "v": -2, + "s": 0 + } + }, + "6": { + "2": { + "t": "n", + "v": 6, + "s": 0 + }, + "4": { + "t": "n", + "v": -3, + "s": 0 + }, + "7": { + "t": "s", + "si": 15, + "s": 4 + } + }, + "7": { + "2": { + "t": "n", + "v": 7, + "s": 0 + }, + "8": { + "t": "s", + "si": 40, + "s": 0 + }, + "9": { + "t": "empty", + "s": 47 + }, + "10": { + "t": "empty", + "s": 48 + }, + "11": { + "t": "empty", + "s": 49 + }, + "12": { + "t": "empty", + "s": 50 + }, + "13": { + "t": "empty", + "s": 51 + }, + "14": { + "t": "empty", + "s": 52 + }, + "15": { + "t": "empty", + "s": 53 + }, + "16": { + "t": "empty", + "s": 54 + }, + "17": { + "t": "empty", + "s": 55 + }, + "18": { + "t": "empty", + "s": 56 + } + }, + "8": { + "2": { + "t": "n", + "v": 8, + "s": 0 + } + }, + "9": { + "2": { + "t": "n", + "v": 9, + "s": 0 + } + }, + "16": { + "1": { + "t": "s", + "si": 17, + "s": 0 + } + }, + "17": { + "1": { + "t": "n", + "v": 1, + "s": 0 + }, + "2": { + "t": "n", + "v": 7, + "s": 0 + } + }, + "18": { + "1": { + "t": "n", + "v": 2, + "s": 0 + }, + "2": { + "t": "n", + "v": 8, + "s": 0 + } + }, + "19": { + "1": { + "t": "n", + "v": 3, + "s": 0 + }, + "2": { + "t": "n", + "v": 9, + "s": 0 + } + } + }, + "shared_formulas": ["SUM(local_thing)"], + "sheet_id": 3, + "state": "visible", + "merge_cells": [], + "comments": [] + }, + { + "dimension": "A3:B13", + "cols": [ + { + "min": 1, + "max": 1, + "width": 12.81640625, + "custom_width": true + }, + { + "min": 2, + "max": 2, + "width": 10.81640625, + "custom_width": true + } + ], + "rows": [], + "name": "Sheet4", + "sheet_data": { + "3": { + "1": { + "t": "s", + "si": 31, + "s": 7 + }, + "2": { + "t": "s", + "si": 33, + "s": 0 + } + }, + "4": { + "1": { + "t": "s", + "si": 24, + "s": 8 + }, + "2": { + "t": "n", + "v": 1980, + "s": 9 + } + }, + "5": { + "1": { + "t": "s", + "si": 25, + "s": 10 + }, + "2": { + "t": "n", + "v": 1980, + "s": 9 + } + }, + "6": { + "1": { + "t": "s", + "si": 29, + "s": 11 + }, + "2": { + "t": "n", + "v": 1980, + "s": 9 + } + }, + "7": { + "1": { + "t": "s", + "si": 26, + "s": 8 + }, + "2": { + "t": "n", + "v": 1980, + "s": 9 + } + }, + "8": { + "1": { + "t": "s", + "si": 27, + "s": 10 + }, + "2": { + "t": "n", + "v": 1980, + "s": 9 + } + }, + "9": { + "1": { + "t": "s", + "si": 30, + "s": 11 + }, + "2": { + "t": "n", + "v": 1980, + "s": 9 + } + }, + "10": { + "1": { + "t": "s", + "si": 22, + "s": 8 + }, + "2": { + "t": "n", + "v": 1975, + "s": 9 + } + }, + "11": { + "1": { + "t": "s", + "si": 23, + "s": 10 + }, + "2": { + "t": "n", + "v": 1975, + "s": 9 + } + }, + "12": { + "1": { + "t": "s", + "si": 29, + "s": 11 + }, + "2": { + "t": "n", + "v": 1975, + "s": 9 + } + }, + "13": { + "1": { + "t": "s", + "si": 32, + "s": 8 + }, + "2": { + "t": "n", + "v": 5935, + "s": 9 + } + } + }, + "shared_formulas": [], + "sheet_id": 8, + "state": "visible", + "merge_cells": [], + "comments": [] + }, + { + "dimension": "A1:G5", + "cols": [], + "rows": [], + "name": "shared", + "sheet_data": { + "1": { + "1": { + "t": "n", + "v": 1, + "s": 0 + }, + "2": { + "t": "n", + "v": 5, + "s": 0 + }, + "3": { + "t": "fn", + "f": 0, + "v": 0.8414709848078965, + "s": 0 + }, + "4": { + "t": "fn", + "f": 0, + "v": 0.1682941969615793, + "s": 0 + }, + "5": { + "t": "fn", + "f": 0, + "v": 1, + "s": 0 + } + }, + "2": { + "1": { + "t": "n", + "v": 2, + "s": 0 + }, + "2": { + "t": "n", + "v": 6, + "s": 0 + }, + "3": { + "t": "fn", + "f": 0, + "v": 0.42073549240394825, + "s": 0 + }, + "4": { + "t": "fn", + "f": 0, + "v": 0.1402451641346494, + "s": 0 + }, + "5": { + "t": "fn", + "f": 0, + "v": 2, + "s": 0 + } + }, + "3": { + "1": { + "t": "n", + "v": 3, + "s": 0 + }, + "2": { + "t": "n", + "v": 7, + "s": 0 + }, + "3": { + "t": "fn", + "f": 0, + "v": 0.2804903282692988, + "s": 0 + }, + "4": { + "t": "fn", + "f": 0, + "v": 0.12021014068684235, + "s": 0 + }, + "5": { + "t": "fn", + "f": 0, + "v": 3, + "s": 0 + } + }, + "4": { + "1": { + "t": "n", + "v": 4, + "s": 0 + }, + "2": { + "t": "n", + "v": 8, + "s": 0 + }, + "3": { + "t": "fn", + "f": 0, + "v": 0.21036774620197413, + "s": 0 + }, + "4": { + "t": "fn", + "f": 0, + "v": 0.10518387310098706, + "s": 0 + }, + "5": { + "t": "fn", + "f": 0, + "v": 4, + "s": 0 + } + }, + "5": { + "1": { + "t": "n", + "v": 5, + "s": 0 + }, + "2": { + "t": "n", + "v": 9, + "s": 0 + }, + "3": { + "t": "fn", + "f": 0, + "v": 0.1682941969615793, + "s": 0 + }, + "4": { + "t": "fn", + "f": 0, + "v": 0.09349677608976628, + "s": 0 + }, + "5": { + "t": "fn", + "f": 0, + "v": 5, + "s": 0 + }, + "7": { + "t": "n", + "v": 42, + "s": 0 + } + } + }, + "shared_formulas": ["SIN(R1C1)/R[0]C[-2]"], + "sheet_id": 9, + "state": "visible", + "merge_cells": [], + "comments": [] + }, + { + "dimension": "A1:D4", + "cols": [], + "rows": [], + "name": "Table", + "sheet_data": { + "1": { + "1": { + "t": "s", + "si": 19, + "s": 0 + }, + "2": { + "t": "s", + "si": 20, + "s": 0 + }, + "3": { + "t": "s", + "si": 21, + "s": 0 + }, + "4": { + "t": "s", + "si": 28, + "s": 0 + } + }, + "2": { + "1": { + "t": "s", + "si": 22, + "s": 0 + }, + "2": { + "t": "n", + "v": 1975, + "s": 0 + }, + "3": { + "t": "s", + "si": 23, + "s": 0 + }, + "4": { + "t": "s", + "si": 29, + "s": 0 + } + }, + "3": { + "1": { + "t": "s", + "si": 24, + "s": 0 + }, + "2": { + "t": "n", + "v": 1980, + "s": 0 + }, + "3": { + "t": "s", + "si": 25, + "s": 0 + }, + "4": { + "t": "s", + "si": 29, + "s": 0 + } + }, + "4": { + "1": { + "t": "s", + "si": 26, + "s": 0 + }, + "2": { + "t": "n", + "v": 1980, + "s": 0 + }, + "3": { + "t": "s", + "si": 27, + "s": 0 + }, + "4": { + "t": "s", + "si": 30, + "s": 0 + } + } + }, + "shared_formulas": [], + "sheet_id": 7, + "state": "visible", + "color": "#C55911", + "merge_cells": [], + "comments": [] + }, + { + "dimension": "A1:B3", + "cols": [], + "rows": [], + "name": "Sheet2", + "sheet_data": { + "1": { + "1": { + "t": "n", + "v": 1, + "s": 0 + }, + "2": { + "t": "n", + "v": 5, + "s": 0 + } + }, + "2": { + "1": { + "t": "n", + "v": 2, + "s": 0 + }, + "2": { + "t": "n", + "v": 6, + "s": 0 + } + }, + "3": { + "1": { + "t": "n", + "v": 3, + "s": 0 + }, + "2": { + "t": "n", + "v": 7, + "s": 0 + } + } + }, + "shared_formulas": [], + "sheet_id": 2, + "state": "visible", + "merge_cells": [], + "comments": [] + }, + { + "dimension": "A1:B11", + "cols": [ + { + "min": 1, + "max": 1, + "width": 39.81640625, + "custom_width": true + } + ], + "rows": [ + { + "height": 14.5, + "r": 6, + "custom_format": true, + "custom_height": false, + "s": 15 + }, + { + "height": 14.5, + "r": 7, + "custom_format": true, + "custom_height": false, + "s": 16 + } + ], + "name": "Created fourth", + "sheet_data": { + "1": { + "1": { + "t": "s", + "si": 16, + "s": 0 + } + }, + "4": { + "2": { + "t": "s", + "si": 34, + "s": 0 + } + }, + "6": { + "1": { + "t": "s", + "si": 36, + "s": 15 + } + }, + "7": { + "1": { + "t": "s", + "si": 37, + "s": 16 + } + }, + "11": { + "2": { + "t": "s", + "si": 38, + "s": 15 + } + } + }, + "shared_formulas": [], + "sheet_id": 4, + "state": "visible", + "merge_cells": [], + "comments": [ + { + "text": "Someone else", + "author_name": "", + "cell_ref": "B4" + } + ] + }, + { + "dimension": "A1", + "cols": [], + "rows": [], + "name": "Hidden", + "sheet_data": {}, + "shared_formulas": [], + "sheet_id": 5, + "state": "hidden", + "merge_cells": [], + "comments": [] + } + ], + "styles": { + "num_fmts": [], + "fonts": [ + { + "b": false, + "i": false, + "sz": 11, + "color": "#000000", + "name": "Calibri", + "family": 2, + "scheme": "minor" + }, + { + "b": false, + "i": false, + "sz": 11, + "color": "#FF0000", + "name": "Calibri", + "family": 2, + "scheme": "minor" + }, + { + "b": true, + "i": false, + "sz": 11, + "color": "#000000", + "name": "Calibri", + "family": 2, + "scheme": "minor" + }, + { + "b": false, + "i": false, + "sz": 11, + "color": "#FFC000", + "name": "Calibri", + "family": 2, + "scheme": "minor" + }, + { + "b": true, + "i": false, + "sz": 11, + "color": "#FF0000", + "name": "Calibri", + "family": 2, + "scheme": "minor" + }, + { + "b": false, + "i": true, + "sz": 16, + "color": "#FF0000", + "name": "Calibri", + "family": 2, + "scheme": "minor" + }, + { + "b": false, + "i": false, + "sz": 11, + "color": "#9C5700", + "name": "Calibri", + "family": 2, + "scheme": "minor" + }, + { + "b": false, + "i": false, + "sz": 9, + "color": "#000000", + "name": "Tahoma", + "family": 2, + "scheme": "minor" + }, + { + "b": true, + "i": false, + "sz": 9, + "color": "#000000", + "name": "Tahoma", + "family": 2, + "scheme": "minor" + }, + { + "b": true, + "i": false, + "sz": 16, + "color": "#000000", + "name": "Tahoma", + "family": 2, + "scheme": "minor" + }, + { + "b": false, + "i": false, + "sz": 11, + "color": "#FF66CC", + "name": "Calibri", + "family": 2, + "scheme": "minor" + }, + { + "b": false, + "i": false, + "sz": 11, + "color": "#3F3F76", + "name": "Calibri", + "family": 2, + "scheme": "minor" + }, + { + "b": false, + "i": false, + "sz": 9, + "color": "#000000", + "name": "Tahoma", + "family": 2, + "scheme": "minor" + }, + { + "b": false, + "i": false, + "sz": 11, + "color": "#C55911", + "name": "Calibri", + "family": 2, + "scheme": "minor" + } + ], + "fills": [ + { + "pattern_type": "none" + }, + { + "pattern_type": "gray125" + }, + { + "pattern_type": "solid", + "fg_color": "#FFFF00", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#B5C8E8", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#FFEB9C" + }, + { + "pattern_type": "solid", + "fg_color": "#00B0F0", + "bg_color": "#000000" + }, + { + "pattern_type": "lightUp", + "fg_color": "#DFEBF7" + }, + { + "pattern_type": "solid", + "fg_color": "#FFCC99" + }, + { + "pattern_type": "solid", + "fg_color": "#F2F2F2", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#FFFFFF", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#000000", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#E7E6E6", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#44546A", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#4472C4", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#ED7D31", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#A5A5A5", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#FFC000", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#5B9BD5", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#70AD47", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#808080", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#D0CECE", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#D7DDE5", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#D8E2F3", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#FBE4D5", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#EDEDED", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#FFF2CC", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#DFEBF7", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#E3F0DB", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#C00000", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#FF0000", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#FFC000", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#92D050", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#00B050", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#0070C0", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#002060", + "bg_color": "#000000" + }, + { + "pattern_type": "solid", + "fg_color": "#7030A0", + "bg_color": "#000000" + } + ], + "borders": [ + {}, + { + "left": { + "style": "thick", + "color": "#FFF2CC" + } + }, + { + "left": { + "style": "thick", + "color": "#6C56DC" + }, + "top": { + "style": "thick", + "color": "#6C56DC" + }, + "bottom": { + "style": "thick", + "color": "#6C56DC" + }, + "diagonal": { + "style": "thick", + "color": "#F8C9AB" + } + }, + { + "left": { + "style": "thin", + "color": "#7F7F7F" + }, + "right": { + "style": "thin", + "color": "#7F7F7F" + }, + "top": { + "style": "thin", + "color": "#7F7F7F" + }, + "bottom": { + "style": "thin", + "color": "#7F7F7F" + } + }, + { + "left": { + "style": "thin", + "color": "#7F7F7F" + }, + "right": { + "style": "thin", + "color": "#7F7F7F" + }, + "bottom": { + "style": "thin", + "color": "#7F7F7F" + } + }, + { + "left": { + "style": "thin", + "color": "#7F7F7F" + }, + "right": { + "style": "thin", + "color": "#7F7F7F" + }, + "top": { + "style": "thin", + "color": "#7F7F7F" + } + }, + { + "left": { + "style": "medium", + "color": "#000000" + }, + "top": { + "style": "medium", + "color": "#000000" + } + }, + { + "top": { + "style": "medium", + "color": "#000000" + } + }, + { + "right": { + "style": "medium", + "color": "#000000" + }, + "top": { + "style": "medium", + "color": "#000000" + } + }, + { + "left": { + "style": "medium", + "color": "#000000" + } + }, + { + "right": { + "style": "medium", + "color": "#000000" + } + }, + { + "left": { + "style": "medium", + "color": "#000000" + }, + "bottom": { + "style": "medium", + "color": "#000000" + } + }, + { + "bottom": { + "style": "medium", + "color": "#000000" + } + }, + { + "right": { + "style": "medium", + "color": "#000000" + }, + "bottom": { + "style": "medium", + "color": "#000000" + } + } + ], + "cell_style_xfs": [ + { + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 0, + "border_id": 0 + }, + { + "num_fmt_id": 0, + "font_id": 6, + "fill_id": 4, + "border_id": 0, + "apply_number_format": false, + "apply_border": false, + "apply_alignment": false, + "apply_protection": false + }, + { + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 3, + "apply_number_format": false, + "apply_alignment": false, + "apply_protection": false + }, + { + "num_fmt_id": 0, + "font_id": 13, + "fill_id": 8, + "border_id": 0 + } + ], + "cell_xfs": [ + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 0, + "border_id": 0 + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 1, + "fill_id": 2, + "border_id": 0, + "apply_font": true, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 2, + "fill_id": 0, + "border_id": 0, + "apply_font": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 3, + "fill_id": 3, + "border_id": 0, + "apply_font": true, + "apply_fill": true + }, + { + "xf_id": 1, + "num_fmt_id": 0, + "font_id": 6, + "fill_id": 4, + "border_id": 0 + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 10, + "fill_id": 5, + "border_id": 2, + "apply_border": true, + "apply_font": true, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 6, + "border_id": 1, + "apply_border": true, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 0, + "border_id": 0 + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 0, + "border_id": 0, + "alignment": { + "horizontal": "left" + }, + "apply_alignment": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 0, + "border_id": 0, + "apply_number_format": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 0, + "border_id": 0, + "alignment": { + "horizontal": "left" + }, + "apply_alignment": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 0, + "border_id": 0, + "alignment": { + "horizontal": "left" + }, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 3 + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 4, + "apply_border": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 5, + "apply_border": true + }, + { + "xf_id": 3, + "num_fmt_id": 0, + "font_id": 13, + "fill_id": 8, + "border_id": 0, + "read_only": true + }, + { + "xf_id": 3, + "num_fmt_id": 0, + "font_id": 13, + "fill_id": 2, + "border_id": 0, + "read_only": true, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 0, + "border_id": 0, + "alignment": { + "horizontal": "center" + }, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 6, + "alignment": { + "horizontal": "center" + }, + "apply_border": true, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 7, + "alignment": { + "horizontal": "center" + }, + "apply_border": true, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 8, + "alignment": { + "horizontal": "center" + }, + "apply_border": true, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 9, + "alignment": { + "horizontal": "center" + }, + "apply_border": true, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 0, + "alignment": { + "horizontal": "center" + }, + "apply_border": true, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 10, + "alignment": { + "horizontal": "center" + }, + "apply_border": true, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 11, + "alignment": { + "horizontal": "center" + }, + "apply_border": true, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 12, + "alignment": { + "horizontal": "center" + }, + "apply_border": true, + "apply_alignment": true + }, + { + "xf_id": 2, + "num_fmt_id": 0, + "font_id": 11, + "fill_id": 7, + "border_id": 13, + "alignment": { + "horizontal": "center" + }, + "apply_border": true, + "apply_alignment": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 9, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 10, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 11, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 12, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 13, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 14, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 15, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 16, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 17, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 18, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 8, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 19, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 20, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 21, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 22, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 23, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 24, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 25, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 26, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 27, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 28, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 29, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 30, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 2, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 31, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 32, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 5, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 33, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 34, + "border_id": 0, + "apply_fill": true + }, + { + "xf_id": 0, + "num_fmt_id": 0, + "font_id": 0, + "fill_id": 35, + "border_id": 0, + "apply_fill": true + } + ], + "cell_styles": [ + { + "name": "Input", + "xf_id": 2, + "builtin_id": 20 + }, + { + "name": "Neutral", + "xf_id": 1, + "builtin_id": 28 + }, + { + "name": "Normal", + "xf_id": 0, + "builtin_id": 0 + }, + { + "name": "ReadOnly1", + "xf_id": 3, + "builtin_id": 0 + } + ] + }, + "name": "example" +} diff --git a/xlsx/tests/example.xlsx b/xlsx/tests/example.xlsx new file mode 100644 index 0000000..b8a20b1 Binary files /dev/null and b/xlsx/tests/example.xlsx differ diff --git a/xlsx/tests/freeze.xlsx b/xlsx/tests/freeze.xlsx new file mode 100644 index 0000000..625fa3c Binary files /dev/null and b/xlsx/tests/freeze.xlsx differ diff --git a/xlsx/tests/split.xlsx b/xlsx/tests/split.xlsx new file mode 100644 index 0000000..72dc492 Binary files /dev/null and b/xlsx/tests/split.xlsx differ diff --git a/xlsx/tests/test.rs b/xlsx/tests/test.rs new file mode 100644 index 0000000..d9e9420 --- /dev/null +++ b/xlsx/tests/test.rs @@ -0,0 +1,243 @@ +use std::{env, fs, io}; +use uuid::Uuid; + +use ironcalc::compare::{test_file, test_load_and_saving}; +use ironcalc::export::save_to_xlsx; +use ironcalc::import::{load_from_excel, load_model_from_xlsx}; +use ironcalc_base::model::Model; +use ironcalc_base::types::{HorizontalAlignment, VerticalAlignment, Workbook}; + +// This is a functional test. +// We check that the output of example.xlsx is what we expect. +#[test] +fn test_example() { + let model = load_from_excel("tests/example.xlsx", "en", "UTC").unwrap(); + assert_eq!(model.worksheets[0].frozen_rows, 0); + assert_eq!(model.worksheets[0].frozen_columns, 0); + let contents = + fs::read_to_string("tests/example.json").expect("Something went wrong reading the file"); + let model2: Workbook = serde_json::from_str(&contents).unwrap(); + let s = serde_json::to_string(&model).unwrap(); + assert_eq!(model, model2, "{s}"); +} + +#[test] +fn test_save_to_xlsx() { + let mut model = load_model_from_xlsx("tests/example.xlsx", "en", "UTC").unwrap(); + model.evaluate(); + let temp_file_name = "temp_file_example.xlsx"; + // test can safe + save_to_xlsx(&model, temp_file_name).unwrap(); + // test can open + let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); + let metadata = &model.workbook.metadata; + assert_eq!(metadata.application, "IronCalc Sheets"); + // FIXME: This will need to be updated once we fix versioning + assert_eq!(metadata.app_version, "10.0000"); + // TODO: can we show it is the 'same' model? + fs::remove_file(temp_file_name).unwrap(); +} + +#[test] +fn test_freeze() { + // freeze has 3 frozen columns and 2 frozen rows + let model = load_from_excel("tests/freeze.xlsx", "en", "UTC").unwrap(); + assert_eq!(model.worksheets[0].frozen_rows, 2); + assert_eq!(model.worksheets[0].frozen_columns, 3); +} + +#[test] +fn test_split() { + // We test that a workbook with split panes do not produce frozen rows and columns + let model = load_from_excel("tests/split.xlsx", "en", "UTC").unwrap(); + assert_eq!(model.worksheets[0].frozen_rows, 0); + assert_eq!(model.worksheets[0].frozen_columns, 0); +} + +fn test_model_has_correct_styles(model: &Model) { + // A1 is bold + let style_a1 = model.get_style_for_cell(0, 1, 1); + assert!(style_a1.font.b); + assert!(!style_a1.font.i); + assert!(!style_a1.font.u); + + // B1 is Italics + let style_b1 = model.get_style_for_cell(0, 1, 2); + assert!(style_b1.font.i); + assert!(!style_b1.font.b); + assert!(!style_b1.font.u); + + // C1 Underlined + let style_c1 = model.get_style_for_cell(0, 1, 3); + assert!(style_c1.font.u); + assert!(!style_c1.font.b); + assert!(!style_c1.font.i); + + // D1 Bold and Italics + let style_d1 = model.get_style_for_cell(0, 1, 4); + assert!(style_d1.font.b); + assert!(style_d1.font.i); + assert!(!style_d1.font.u); + + // E1 Bold, italics and underlined + let style_e1 = model.get_style_for_cell(0, 1, 5); + assert!(style_e1.font.b); + assert!(style_e1.font.i); + assert!(style_e1.font.u); + assert!(!style_e1.font.strike); + + // F1 strikethrough + let style_f1 = model.get_style_for_cell(0, 1, 6); + assert!(style_f1.font.strike); + + // G1 Double underlined just get simple underlined + let style_g1 = model.get_style_for_cell(0, 1, 7); + assert!(style_g1.font.u); + + let height_row_3 = model.workbook.worksheet(0).unwrap().row_height(3).unwrap(); + assert_eq!(height_row_3, 136.0); + + let height_row_5 = model.workbook.worksheet(0).unwrap().row_height(5).unwrap(); + assert_eq!(height_row_5, 62.0); + + // Second sheet has alignment + // Horizontal + let alignment = model.get_style_for_cell(1, 2, 1).alignment; + assert_eq!(alignment, None); + + let alignment = model.get_style_for_cell(1, 3, 1).alignment.unwrap(); + assert_eq!(alignment.horizontal, HorizontalAlignment::Left); + + let alignment = model.get_style_for_cell(1, 4, 1).alignment.unwrap(); + assert_eq!(alignment.horizontal, HorizontalAlignment::Distributed); + + let alignment = model.get_style_for_cell(1, 5, 1).alignment.unwrap(); + assert_eq!(alignment.horizontal, HorizontalAlignment::Right); + + let alignment = model.get_style_for_cell(1, 6, 1).alignment.unwrap(); + assert_eq!(alignment.horizontal, HorizontalAlignment::Center); + + let alignment = model.get_style_for_cell(1, 7, 1).alignment.unwrap(); + assert_eq!(alignment.horizontal, HorizontalAlignment::Fill); + + let alignment = model.get_style_for_cell(1, 8, 1).alignment.unwrap(); + assert_eq!(alignment.horizontal, HorizontalAlignment::Justify); + + // Vertical + let alignment = model.get_style_for_cell(1, 2, 2).alignment; + assert_eq!(alignment, None); + + let alignment = model.get_style_for_cell(1, 3, 2).alignment; + assert_eq!(alignment, None); + + let alignment = model.get_style_for_cell(1, 4, 2).alignment.unwrap(); + assert_eq!(alignment.vertical, VerticalAlignment::Top); + + let alignment = model.get_style_for_cell(1, 5, 2).alignment.unwrap(); + assert_eq!(alignment.vertical, VerticalAlignment::Center); + + let alignment = model.get_style_for_cell(1, 6, 2).alignment.unwrap(); + assert_eq!(alignment.vertical, VerticalAlignment::Justify); + + let alignment = model.get_style_for_cell(1, 7, 2).alignment.unwrap(); + assert_eq!(alignment.vertical, VerticalAlignment::Distributed); +} + +#[test] +fn test_simple_text() { + let model = load_model_from_xlsx("tests/basic_text.xlsx", "en", "UTC").unwrap(); + + test_model_has_correct_styles(&model); + + let temp_file_name = "temp_file_test_named_styles.xlsx"; + save_to_xlsx(&model, temp_file_name).unwrap(); + + let model = load_model_from_xlsx(temp_file_name, "en", "UTC").unwrap(); + fs::remove_file(temp_file_name).unwrap(); + test_model_has_correct_styles(&model); +} + +#[test] +fn test_defined_names_casing() { + let test_file_path = "tests/calc_tests/defined_names_for_unit_test.xlsx"; + let loaded_workbook = load_from_excel(test_file_path, "en", "UTC").unwrap(); + let mut model = Model::from_json(&serde_json::to_string(&loaded_workbook).unwrap()).unwrap(); + + let (row, column) = (2, 13); // B13 + let test_cases = [ + ("=named1", "11"), + ("=NAMED1", "11"), + ("=NaMeD1", "11"), + ("=named2", "22"), + ("=NAMED2", "22"), + ("=NaMeD2", "22"), + ("=named3", "33"), + ("=NAMED3", "33"), + ("=NaMeD3", "33"), + ]; + for (formula, expected_value) in test_cases { + model.set_user_input(0, row, column, formula.to_string()); + model.evaluate(); + assert_eq!( + model.formatted_cell_value(0, row, column).unwrap(), + expected_value + ); + } +} + +#[test] +fn test_xlsx() { + let mut entries = fs::read_dir("tests/calc_tests/") + .unwrap() + .map(|res| res.map(|e| e.path())) + .collect::, io::Error>>() + .unwrap(); + entries.sort(); + let temp_folder = env::temp_dir(); + let path = format!("{}", Uuid::new_v4()); + let dir = temp_folder.join(path); + fs::create_dir(&dir).unwrap(); + for file_path in entries { + let file_name_str = file_path.file_name().unwrap().to_str().unwrap(); + let file_path_str = file_path.to_str().unwrap(); + println!("Testing file: {}", file_path_str); + if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') { + if let Err(message) = test_file(file_path_str) { + println!("{}", message); + panic!("Model was evaluated inconsistently with XLSX data.") + } + assert!(test_load_and_saving(file_path_str, &dir).is_ok()); + } else { + println!("skipping"); + } + } + fs::remove_dir_all(&dir).unwrap(); +} + +#[test] +fn no_export() { + let mut entries = fs::read_dir("tests/calc_test_no_export/") + .unwrap() + .map(|res| res.map(|e| e.path())) + .collect::, io::Error>>() + .unwrap(); + entries.sort(); + let temp_folder = env::temp_dir(); + let path = format!("{}", Uuid::new_v4()); + let dir = temp_folder.join(path); + fs::create_dir(&dir).unwrap(); + for file_path in entries { + let file_name_str = file_path.file_name().unwrap().to_str().unwrap(); + let file_path_str = file_path.to_str().unwrap(); + println!("Testing file: {}", file_path_str); + if file_name_str.ends_with(".xlsx") && !file_name_str.starts_with('~') { + if let Err(message) = test_file(file_path_str) { + println!("{}", message); + panic!("Model was evaluated inconsistently with XLSX data.") + } + } else { + println!("skipping"); + } + } + fs::remove_dir_all(&dir).unwrap(); +}