UPDATE: Dump of initial files
This commit is contained in:
1
xlsx/.gitignore
vendored
Normal file
1
xlsx/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/*
|
||||
716
xlsx/Cargo.lock
generated
Normal file
716
xlsx/Cargo.lock
generated
Normal file
@@ -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",
|
||||
]
|
||||
31
xlsx/Cargo.toml
Normal file
31
xlsx/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "ironcalc"
|
||||
version = "0.1.0"
|
||||
authors = ["Nicolás Hatcher <nicolas@theuniverse.today>"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
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"
|
||||
17
xlsx/README.md
Normal file
17
xlsx/README.md
Normal file
@@ -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
|
||||
64
xlsx/documentation/README.md
Normal file
64
xlsx/documentation/README.md
Normal file
@@ -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)
|
||||
67
xlsx/documentation/sharedStrings.md
Normal file
67
xlsx/documentation/sharedStrings.md
Normal file
@@ -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
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<sst xmlns="https://schemas.openxmlformats.org/spreadsheetml/2006/main" count="6" uniqueCount="2">
|
||||
|
||||
<si>
|
||||
<t>Cell A1</t>
|
||||
</si>
|
||||
<si>
|
||||
<r>
|
||||
<rPr>
|
||||
<sz val="11"/>
|
||||
<color rgb="FFFF0000"/>
|
||||
<rFont val="Calibri"/>
|
||||
<family val="2"/>
|
||||
<scheme val="minor"/>
|
||||
</rPr>
|
||||
<t>Cell</t>
|
||||
</r>
|
||||
<r>
|
||||
<rPr>
|
||||
<sz val="11"/>
|
||||
<color theme="1"/>
|
||||
<rFont val="Calibri"/>
|
||||
<family val="2"/>
|
||||
<scheme val="minor"/>
|
||||
</rPr>
|
||||
<t xml:space="preserve"> </t>
|
||||
</r>
|
||||
<r>
|
||||
<rPr>
|
||||
<b/>
|
||||
<sz val="11"/>
|
||||
<color theme="1"/>
|
||||
<rFont val="Calibri"/>
|
||||
<family val="2"/>
|
||||
<scheme val="minor"/>
|
||||
</rPr>
|
||||
<t>A2</t>
|
||||
</r>
|
||||
</si>
|
||||
</sst>
|
||||
```
|
||||
|
||||
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.
|
||||
68
xlsx/documentation/workbook.md
Normal file
68
xlsx/documentation/workbook.md
Normal file
@@ -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
|
||||
<sheets>
|
||||
<sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
||||
<sheet name="Chart1" sheetId="6" r:id="rId2"/>
|
||||
<sheet name="Second" sheetId="3" r:id="rId3"/>
|
||||
<sheet name="Sheet4" sheetId="8" r:id="rId4"/>
|
||||
<sheet name="shared" sheetId="9" r:id="rId5"/>
|
||||
<sheet name="Table" sheetId="7" r:id="rId6"/>
|
||||
<sheet name="Sheet2" sheetId="2" r:id="rId7"/>
|
||||
<sheet name="Created fourth" sheetId="4" r:id="rId8"/>
|
||||
<sheet name="Hidden" sheetId="5" state="hidden" r:id="rId9"/>
|
||||
</sheets>
|
||||
```
|
||||
|
||||
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
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId8" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet7.xml"/>
|
||||
<Relationship Id="rId13" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" Target="sharedStrings.xml"/>
|
||||
<Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet2.xml"/>
|
||||
<Relationship Id="rId7" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet6.xml"/>
|
||||
<Relationship Id="rId12" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/>
|
||||
<Relationship Id="rId2" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" Target="chartsheets/sheet1.xml"/>
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet1.xml"/>
|
||||
<Relationship Id="rId6" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet5.xml"/>
|
||||
<Relationship Id="rId11" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" Target="theme/theme1.xml"/>
|
||||
<Relationship Id="rId5" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet4.xml"/>
|
||||
<Relationship Id="rId10" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCacheDefinition" Target="pivotCache/pivotCacheDefinition1.xml"/>
|
||||
<Relationship Id="rId4" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet3.xml"/>
|
||||
<Relationship Id="rId9" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet" Target="worksheets/sheet8.xml"/>
|
||||
<Relationship Id="rId14" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" Target="calcChain.xml"/>
|
||||
</Relationships>
|
||||
```
|
||||
|
||||
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
|
||||
<definedNames>
|
||||
<definedName name="answer" localSheetId="4">shared!$G$5</definedName>
|
||||
<definedName name="answer2" localSheetId="0">Sheet1!$I$6</definedName>
|
||||
<definedName name="local_thing" localSheetId="2">Second!$B$1:$B$9</definedName>
|
||||
<definedName name="numbers">Sheet1!$A$16:$A$18</definedName>
|
||||
<definedName name="quantum">Sheet1!$C$14</definedName>
|
||||
</definedNames>
|
||||
```
|
||||
|
||||
So `answer2` is scoped to `Sheet1` and `answer` is scoped to `shared`.
|
||||
61
xlsx/documentation/worksheets.md
Normal file
61
xlsx/documentation/worksheets.md
Normal file
@@ -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
|
||||
<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:xr="http://schemas.microsoft.com/office/spreadsheetml/2014/revision" xmlns:xr2="http://schemas.microsoft.com/office/spreadsheetml/2015/revision2" xmlns:xr3="http://schemas.microsoft.com/office/spreadsheetml/2016/revision3" mc:Ignorable="x14ac xr xr2 xr3" xr:uid="{65AA7E95-0880-433A-9B1F-8563DB0FF1B5}">
|
||||
<dimension ref="A1:O33"/>
|
||||
<sheetViews>
|
||||
<sheetView workbookViewId="0">
|
||||
<selection activeCell="I6" sqref="I6"/>
|
||||
</sheetView>
|
||||
</sheetViews>
|
||||
<sheetFormatPr defaultRowHeight="14.5" x14ac:dyDescent="0.35"/>
|
||||
<cols>
|
||||
<col min="5" max="5" width="38.26953125" customWidth="1"/>
|
||||
<col min="6" max="6" width="9.1796875" style="1"/>
|
||||
<col min="8" max="8" width="4" customWidth="1"/>
|
||||
</cols>
|
||||
<sheetData>
|
||||
...
|
||||
</sheetData>
|
||||
<mergeCells count="2">
|
||||
<mergeCell ref="K7:L10"/>
|
||||
<mergeCell ref="H18:J20"/>
|
||||
</mergeCells>
|
||||
<pageMargins left="0.7" right="0.7" top="0.75" bottom="0.75" header="0.3" footer="0.3"/>
|
||||
<pageSetup orientation="portrait" r:id="rId1"/>
|
||||
<legacyDrawing r:id="rId2"/>
|
||||
</worksheet>
|
||||
```
|
||||
|
||||
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
|
||||
<sheetData>
|
||||
<row r="1" spans="1:2" x14ac:dyDescent="0.35">
|
||||
<c r="A1" t="s">
|
||||
<v>0</v>
|
||||
</c>
|
||||
<c r="C1">
|
||||
<v>1</v>
|
||||
</c>
|
||||
</row>
|
||||
<row r="2" spans="1:2" x14ac:dyDescent="0.35">
|
||||
<c r="A2">
|
||||
<v>222</v>
|
||||
</c>
|
||||
<c r="C2">
|
||||
<v>2</v>
|
||||
</c>
|
||||
</row>
|
||||
</sheetData>
|
||||
```
|
||||
|
||||
In IronCalc the `spans` (an Excel optimization) is not used. The `dyDescent` property is also ignore in `IronCalc`,
|
||||
34
xlsx/src/bin/test.rs
Normal file
34
xlsx/src/bin/test.rs
Normal file
@@ -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: {} <file.xlsx>", 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();
|
||||
}
|
||||
206
xlsx/src/compare.rs
Normal file
206
xlsx/src/compare.rs
Normal file
@@ -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<T> = std::result::Result<T, CompareError>;
|
||||
|
||||
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<Vec<Diff>> {
|
||||
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)
|
||||
}
|
||||
89
xlsx/src/error.rs
Normal file
89
xlsx/src/error.rs
Normal file
@@ -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<String>),
|
||||
#[error("Comparison Error: {0}")]
|
||||
Comparison(String),
|
||||
#[error("Not Implemented Error: {0}")]
|
||||
NotImplemented(String),
|
||||
}
|
||||
|
||||
impl From<io::Error> for XlsxError {
|
||||
fn from(error: io::Error) -> Self {
|
||||
XlsxError::IO(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ZipError> for XlsxError {
|
||||
fn from(error: ZipError) -> Self {
|
||||
XlsxError::Zip(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseIntError> for XlsxError {
|
||||
fn from(error: ParseIntError) -> Self {
|
||||
XlsxError::Xml(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseFloatError> for XlsxError {
|
||||
fn from(error: ParseFloatError) -> Self {
|
||||
XlsxError::Xml(error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<roxmltree::Error> 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}"
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
6
xlsx/src/export/_rels.rs
Normal file
6
xlsx/src/export/_rels.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
use ironcalc_base::types::Workbook;
|
||||
|
||||
pub(crate) fn get_dot_rels(_: &Workbook) -> String {
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="xl/workbook.xml"/></Relationships>"#.to_owned()
|
||||
}
|
||||
69
xlsx/src/export/doc_props.rs
Normal file
69
xlsx/src/export/doc_props.rs
Normal file
@@ -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!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
|
||||
<Properties xmlns=\"http://schemas.openxmlformats.org/officeDocument/2006/extended-properties\" \
|
||||
xmlns:vt=\"http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes\">\
|
||||
<Application>{}</Application>\
|
||||
<AppVersion>{}</AppVersion>\
|
||||
</Properties>",
|
||||
APPLICATION, APP_VERSION
|
||||
)
|
||||
}
|
||||
|
||||
// Core File Properties part
|
||||
pub(crate) fn get_core_xml(workbook: &Workbook, milliseconds: i64) -> Result<String, XlsxError> {
|
||||
// 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!(
|
||||
"<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>
|
||||
<cp:coreProperties \
|
||||
xmlns:cp=\"http://schemas.openxmlformats.org/package/2006/metadata/core-properties\" \
|
||||
xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:dcterms=\"http://purl.org/dc/terms/\" \
|
||||
xmlns:dcmitype=\"http://purl.org/dc/dcmitype/\" \
|
||||
xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"> \
|
||||
<dc:title></dc:title><dc:subject></dc:subject>\
|
||||
<dc:creator>{}</dc:creator>\
|
||||
<cp:keywords></cp:keywords>\
|
||||
<dc:description></dc:description>\
|
||||
<cp:lastModifiedBy>{}</cp:lastModifiedBy>\
|
||||
<cp:revision></cp:revision>\
|
||||
<dcterms:created xsi:type=\"dcterms:W3CDTF\">{}</dcterms:created>\
|
||||
<dcterms:modified xsi:type=\"dcterms:W3CDTF\">{}</dcterms:modified>\
|
||||
<cp:category></cp:category>\
|
||||
<cp:contentStatus></cp:contentStatus>\
|
||||
</cp:coreProperties>",
|
||||
creator, last_modified_by, created, last_modified
|
||||
))
|
||||
}
|
||||
99
xlsx/src/export/escape.rs
Normal file
99
xlsx/src/export/escape.rs
Normal file
@@ -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<I: IntoIterator<Item = (usize, Value)>>(&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<str> {
|
||||
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
|
||||
138
xlsx/src/export/mod.rs
Normal file
138
xlsx/src/export/mod.rs
Normal file
@@ -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#"<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">"#.to_string(),
|
||||
r#"<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>"#.to_string(),
|
||||
r#"<Default Extension="xml" ContentType="application/xml"/>"#.to_string(),
|
||||
r#"<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>"#.to_string(),
|
||||
];
|
||||
for worksheet in 0..workbook.worksheets.len() {
|
||||
let sheet = format!(
|
||||
r#"<Override PartName="/xl/worksheets/sheet{}.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>"#,
|
||||
worksheet + 1
|
||||
);
|
||||
content.push(sheet);
|
||||
}
|
||||
// we skip the theme and calcChain
|
||||
// r#"<Override PartName="/xl/theme/theme1.xml" ContentType="application/vnd.openxmlformats-officedocument.theme+xml"/>"#,
|
||||
// r#"<Override PartName="/xl/calcChain.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml"/>"#,
|
||||
content.extend([
|
||||
r#"<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>"#.to_string(),
|
||||
r#"<Override PartName="/xl/sharedStrings.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml"/>"#.to_string(),
|
||||
r#"<Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/>"#.to_string(),
|
||||
r#"<Override PartName="/docProps/app.xml" ContentType="application/vnd.openxmlformats-officedocument.extended-properties+xml"/>"#.to_string(),
|
||||
r#"</Types>"#.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<W: Write + Seek>(model: &Model, writer: W) -> Result<W, XlsxError> {
|
||||
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();
|
||||
}
|
||||
16
xlsx/src/export/shared_strings.rs
Normal file
16
xlsx/src/export/shared_strings.rs
Normal file
@@ -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<String> = 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!("<si><t>{}</t></si>", escape_xml(shared_string)));
|
||||
}
|
||||
format!("{}\n\
|
||||
<sst xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" count=\"{count}\" uniqueCount=\"{unique_count}\">\
|
||||
{}\
|
||||
</sst>", XML_DECLARATION, shared_strings.join(""))
|
||||
}
|
||||
282
xlsx/src/export/styles.rs
Normal file
282
xlsx/src/export/styles.rs
Normal file
@@ -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<String> = vec![];
|
||||
for font in fonts {
|
||||
let size = format!("<sz val=\"{}\"/>", font.sz);
|
||||
let color = if let Some(some_color) = &font.color {
|
||||
format!("<color rgb=\"FF{}\"/>", some_color.trim_start_matches('#'))
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let name = format!("<name val=\"{}\"/>", escape_xml(&font.name));
|
||||
let bold = if font.b { "<b/>" } else { "" };
|
||||
let italic = if font.i { "<i/>" } else { "" };
|
||||
let underline = if font.u { "<u/>" } else { "" };
|
||||
let strike = if font.strike { "<strike/>" } else { "" };
|
||||
let family = format!("<family val=\"{}\"/>", font.family);
|
||||
let scheme = format!("<scheme val=\"{}\"/>", font.scheme);
|
||||
fonts_str.push(format!(
|
||||
"<font>\
|
||||
{size}\
|
||||
{color}\
|
||||
{name}\
|
||||
{bold}\
|
||||
{italic}\
|
||||
{underline}\
|
||||
{strike}\
|
||||
{family}\
|
||||
{scheme}\
|
||||
</font>"
|
||||
));
|
||||
}
|
||||
let font_count = fonts.len();
|
||||
format!(
|
||||
"<fonts count=\"{font_count}\">{}</fonts>",
|
||||
fonts_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
fn get_color_xml(color: &Option<String>, 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<String> = 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!(
|
||||
"<fill><patternFill patternType=\"{pattern_type}\">{fg_color}{bg_color}</patternFill></fill>"
|
||||
));
|
||||
}
|
||||
let fill_count = fills.len();
|
||||
format!(
|
||||
"<fills count=\"{fill_count}\">{}</fills>",
|
||||
fills_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
fn get_border_xml(border: &Option<BorderItem>, name: &str) -> String {
|
||||
if let Some(border_item) = border {
|
||||
let color = get_color_xml(&border_item.color, "color");
|
||||
return format!("<{name} style=\"{}\">{color}</{name}>", border_item.style);
|
||||
}
|
||||
format!("<{name}/>")
|
||||
}
|
||||
|
||||
fn get_borders_xml(styles: &Styles) -> String {
|
||||
let borders = &styles.borders;
|
||||
let mut borders_str: Vec<String> = 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>{border_left}{border_right}{border_top}{border_bottom}{border_diagonal}</border>"
|
||||
));
|
||||
}
|
||||
format!(
|
||||
"<borders count=\"{border_count}\">{}</borders>",
|
||||
borders_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
// <numFmts count="1">
|
||||
// <numFmt numFmtId="164" formatCode="##,#00;[Blue]\-\-#,##0"/>
|
||||
// </numFmts>
|
||||
fn get_cell_number_formats_xml(styles: &Styles) -> String {
|
||||
let num_fmts = &styles.num_fmts;
|
||||
let mut num_fmts_str: Vec<String> = 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!(
|
||||
"<numFmt numFmtId=\"{num_fmt_id}\" formatCode=\"{format_code}\"/>"
|
||||
));
|
||||
}
|
||||
if num_fmt_count == 0 {
|
||||
return "".to_string();
|
||||
}
|
||||
format!(
|
||||
"<numFmts count=\"{num_fmt_count}\">{}</numFmts>",
|
||||
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!("<alignment{wrap_text}{horizontal}{vertical}/>")
|
||||
}
|
||||
|
||||
fn get_cell_style_xfs_xml(styles: &Styles) -> String {
|
||||
let cell_style_xfs = &styles.cell_style_xfs;
|
||||
let mut cell_style_str: Vec<String> = 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!(
|
||||
"<xf \
|
||||
borderId=\"{border_id}\" \
|
||||
fillId=\"{fill_id}\" \
|
||||
fontId=\"{font_id}\" \
|
||||
numFmtId=\"{num_fmt_id}\"\
|
||||
{apply_alignment_str}\
|
||||
{apply_font_str}\
|
||||
{apply_fill_str}/>"
|
||||
));
|
||||
}
|
||||
let style_count = cell_style_xfs.len();
|
||||
format!(
|
||||
"<cellStyleXfs count=\"{style_count}\">{}</cellStyleXfs>",
|
||||
cell_style_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
fn get_cell_xfs_xml(styles: &Styles) -> String {
|
||||
let cell_xfs = &styles.cell_xfs;
|
||||
let mut cell_xfs_str: Vec<String> = 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!("<xf {properties}>{alignment}</xf>"));
|
||||
} else {
|
||||
cell_xfs_str.push(format!("<xf {properties}/>"));
|
||||
}
|
||||
}
|
||||
let style_count = cell_xfs.len();
|
||||
format!(
|
||||
"<cellXfs count=\"{style_count}\">{}</cellXfs>",
|
||||
cell_xfs_str.join("")
|
||||
)
|
||||
}
|
||||
|
||||
// <cellStyle xfId="0" name="Normal" builtinId="0"/>
|
||||
fn get_cell_styles_xml(styles: &Styles) -> String {
|
||||
let cell_styles = &styles.cell_styles;
|
||||
let mut cell_styles_str: Vec<String> = 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!(
|
||||
"<cellStyle xfId=\"{xf_id}\" name=\"{name}\" builtinId=\"{builtin_id}\"/>"
|
||||
));
|
||||
}
|
||||
let style_count = cell_styles.len();
|
||||
format!(
|
||||
"<cellStyles count=\"{style_count}\">{}</cellStyles>",
|
||||
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}
|
||||
<styleSheet xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\">\
|
||||
{number_formats}\
|
||||
{fonts}\
|
||||
{fills}\
|
||||
{borders}\
|
||||
{cell_style_xfs}\
|
||||
{cell_xfs}\
|
||||
{cell_styles}\
|
||||
<dxfs count=\"0\"/>\
|
||||
</styleSheet>"
|
||||
)
|
||||
}
|
||||
2
xlsx/src/export/test/mod.rs
Normal file
2
xlsx/src/export/test/mod.rs
Normal file
@@ -0,0 +1,2 @@
|
||||
mod test_escape;
|
||||
mod test_export;
|
||||
25
xlsx/src/export/test/test_escape.rs
Normal file
25
xlsx/src/export/test/test_escape.rs
Normal file
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
// '&' => "&"
|
||||
// '<' "<")
|
||||
// '>' => ">"
|
||||
// '"' => """
|
||||
// '\'' => "'"
|
||||
134
xlsx/src/export/test/test_export.rs
Normal file
134
xlsx/src/export/test/test_export.rs
Normal file
@@ -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();
|
||||
}
|
||||
91
xlsx/src/export/workbook.rs
Normal file
91
xlsx/src/export/workbook.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
//! <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
||||
|
||||
//! 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:
|
||||
//!
|
||||
//! <workbook>
|
||||
//! <sheets>
|
||||
//! <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
||||
//! </sheets>
|
||||
//! </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
|
||||
// <sheet name="Sheet1" sheetId="1" r:id="rId1"/>
|
||||
let mut sheets_str: Vec<String> = vec![];
|
||||
let mut sheet_id_to_sheet_index: HashMap<u32, u32> = 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 name=\"{name}\" sheetId=\"{sheet_id}\" r:id=\"rId{}\"{state_str}/>",
|
||||
sheet_index + 1
|
||||
));
|
||||
sheet_id_to_sheet_index.insert(sheet_id, sheet_index as u32);
|
||||
}
|
||||
|
||||
// defined names
|
||||
// <definedName localSheetId="4" name="answer">shared!$G$5</definedName>
|
||||
// <definedName name="numbers">Sheet1!$A$16:$A$18</definedName>
|
||||
let mut defined_names_str: Vec<String> = 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!(
|
||||
"<definedName name=\"{name}\"{local_sheet_id}>{formula}</definedName>"
|
||||
))
|
||||
}
|
||||
|
||||
let sheets = sheets_str.join("");
|
||||
let defined_names = defined_names_str.join("");
|
||||
format!("{XML_DECLARATION}\n\
|
||||
<workbook xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
|
||||
<sheets>\
|
||||
{sheets}\
|
||||
</sheets>\
|
||||
<definedNames>\
|
||||
{defined_names}\
|
||||
</definedNames>\
|
||||
<calcPr/>\
|
||||
</workbook>")
|
||||
}
|
||||
25
xlsx/src/export/workbook_xml_rels.rs
Normal file
25
xlsx/src/export/workbook_xml_rels.rs
Normal file
@@ -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<String> = vec![];
|
||||
let worksheet_count = workbook.worksheets.len() + 1;
|
||||
for id in 1..worksheet_count {
|
||||
relationships_str.push(format!(
|
||||
"<Relationship Id=\"rId{id}\" Type=\"{XML_WORKSHEET}\" Target=\"worksheets/sheet{id}.xml\"/>"
|
||||
));
|
||||
}
|
||||
let mut id = worksheet_count;
|
||||
relationships_str.push(
|
||||
format!("<Relationship Id=\"rId{id}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles\" Target=\"styles.xml\"/>")
|
||||
);
|
||||
id += 1;
|
||||
relationships_str.push(
|
||||
format!("<Relationship Id=\"rId{id}\" Type=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings\" Target=\"sharedStrings.xml\"/>")
|
||||
);
|
||||
format!(
|
||||
"{XML_DECLARATION}\n<Relationships xmlns=\"http://schemas.openxmlformats.org/package/2006/relationships\">{}</Relationships>",
|
||||
relationships_str.join("")
|
||||
)
|
||||
}
|
||||
267
xlsx/src/export/worksheets.rs
Normal file
267
xlsx/src/export/worksheets.rs
Normal file
@@ -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:
|
||||
//! <c r="F4" t="str">
|
||||
//! <f t="shared" ref="F4:F8" si="42">A4+C4</f>
|
||||
//! <v>123</v>
|
||||
//! </c>
|
||||
//! Cells in the range F4:F8 will then link to that formula like so:
|
||||
//! <c r="F6">
|
||||
//! <f t="shared" si="42"/>
|
||||
//! <v>1</v>
|
||||
//! </c>
|
||||
//! 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<String> = vec![];
|
||||
let mut cols_str: Vec<String> = vec![];
|
||||
|
||||
for col in &worksheet.cols {
|
||||
// <col min="4" max="4" width="12" customWidth="1"/>
|
||||
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!(
|
||||
"<col min=\"{min}\" max=\"{max}\" width=\"{width}\" customWidth=\"{custom_width}\"{column_style}/>"
|
||||
));
|
||||
}
|
||||
|
||||
// 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<String> = 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!("<c r=\"{cell_name}\"{style}/>"));
|
||||
}
|
||||
Cell::BooleanCell { v, s } => {
|
||||
// <c r="A8" t="b" s="1">
|
||||
// <v>1</v>
|
||||
// </c>
|
||||
let b = i32::from(*v);
|
||||
let style = get_cell_style_attribute(*s);
|
||||
row_data_str.push(format!(
|
||||
"<c r=\"{cell_name}\" t=\"b\"{style}><v>{b}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::NumberCell { v, s } => {
|
||||
// Normally the type number is left out. Example:
|
||||
// <c r="C6" s="1">
|
||||
// <v>3</v>
|
||||
// </c>
|
||||
let style = get_cell_style_attribute(*s);
|
||||
row_data_str.push(format!("<c r=\"{cell_name}\"{style}><v>{v}</v></c>"));
|
||||
}
|
||||
Cell::ErrorCell { ei, s } => {
|
||||
let style = get_cell_style_attribute(*s);
|
||||
row_data_str.push(format!(
|
||||
"<c r=\"{cell_name}\" t=\"e\"{style}><v>{ei}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::SharedString { si, s } => {
|
||||
// Example:
|
||||
// <c r="A1" s="1" t="s">
|
||||
// <v>5</v>
|
||||
// </c>
|
||||
// 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!(
|
||||
"<c r=\"{cell_name}\" t=\"s\"{style}><v>{si}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::CellFormula { f: _, s: _ } => {
|
||||
panic!("Model needs to be evaluated before saving!");
|
||||
}
|
||||
Cell::CellFormulaBoolean { f, v, s } => {
|
||||
// <c r="A4" t="b" s="3">
|
||||
// <f>ISTEXT(A5)</f>
|
||||
// <v>1</v>
|
||||
// </c>
|
||||
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!(
|
||||
"<c r=\"{cell_name}\" t=\"b\"{style}><f>{formula}</f><v>{b}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::CellFormulaNumber { f, v, s } => {
|
||||
// Note again type is skipped
|
||||
// <c r="C4" s="3">
|
||||
// <f>A5+C3</f>
|
||||
// <v>123</v>
|
||||
// </c>
|
||||
|
||||
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!(
|
||||
"<c r=\"{cell_name}\"{style}><f>{formula}</f><v>{v}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::CellFormulaString { f, v, s } => {
|
||||
// <c r="C6" t="str" s="5">
|
||||
// <f>CONCATENATE(A1, A2)</f>
|
||||
// <v>Hello world!</v>
|
||||
// </c>
|
||||
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!(
|
||||
"<c r=\"{cell_name}\" t=\"str\"{style}><f>{formula}</f><v>{v}</v></c>"
|
||||
));
|
||||
}
|
||||
Cell::CellFormulaError {
|
||||
f,
|
||||
ei,
|
||||
s,
|
||||
o: _,
|
||||
m: _,
|
||||
} => {
|
||||
// <c r="C6" t="e" s="4">
|
||||
// <f>A1/A3<f/>
|
||||
// <v>#DIV/0!</v>
|
||||
// </c>
|
||||
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!(
|
||||
"<c r=\"{cell_name}\" t=\"e\"{style}><f>{formula}</f><v>{ei}</v></c>"
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
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 r=\"{row_index}\"{row_style_str}>{}</row>",
|
||||
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>{cols}</cols>")
|
||||
};
|
||||
|
||||
format!(
|
||||
"{XML_DECLARATION}
|
||||
<worksheet \
|
||||
xmlns=\"http://schemas.openxmlformats.org/spreadsheetml/2006/main\" \
|
||||
xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\">\
|
||||
<dimension ref=\"{dimension}\"/>\
|
||||
<sheetViews>\
|
||||
<sheetView workbookViewId=\"0\">\
|
||||
<selection activeCell=\"A1\" sqref=\"A1\"/>\
|
||||
</sheetView>\
|
||||
</sheetViews>\
|
||||
{cols}\
|
||||
<sheetData>\
|
||||
{sheet_data}\
|
||||
</sheetData>\
|
||||
</worksheet>"
|
||||
)
|
||||
}
|
||||
5
xlsx/src/export/xml_constants.rs
Normal file
5
xlsx/src/export/xml_constants.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub(crate) const XML_DECLARATION: &str =
|
||||
r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>"#;
|
||||
|
||||
pub(crate) const XML_WORKSHEET: &str =
|
||||
r#"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"#;
|
||||
257
xlsx/src/import/colors.rs
Normal file
257
xlsx/src/import/colors.rs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
81
xlsx/src/import/metadata.rs
Normal file
81
xlsx/src/import/metadata.rs
Normal file
@@ -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<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
) -> Result<CoreData, XlsxError> {
|
||||
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<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
) -> Result<AppData, XlsxError> {
|
||||
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<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
) -> Result<Metadata, XlsxError> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
124
xlsx/src/import/mod.rs
Normal file
124
xlsx/src/import/mod.rs
Normal file
@@ -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<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::ZipArchive<R>,
|
||||
) -> Result<HashMap<String, Relationship>, 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<Node> = 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<R: Read + std::io::Seek>(
|
||||
name: String,
|
||||
reader: R,
|
||||
locale: &str,
|
||||
tz: &str,
|
||||
) -> Result<Workbook, XlsxError> {
|
||||
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<Workbook, XlsxError> {
|
||||
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<Model, XlsxError> {
|
||||
let workbook = load_from_excel(file_name, locale, tz)?;
|
||||
Model::from_workbook(workbook).map_err(XlsxError::Workbook)
|
||||
}
|
||||
80
xlsx/src/import/shared_strings.rs
Normal file
80
xlsx/src/import/shared_strings.rs
Normal file
@@ -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<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
) -> Result<Vec<String>, 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<Vec<String>, XlsxError> {
|
||||
let doc = roxmltree::Document::parse(text)?;
|
||||
let mut shared_strings = Vec::new();
|
||||
let nodes: Vec<Node> = 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::<Vec<String>>()
|
||||
.join("");
|
||||
shared_strings.push(text);
|
||||
}
|
||||
Ok(shared_strings)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_shared_strings() {
|
||||
let xml_string = r#"
|
||||
<sst xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" count="3" uniqueCount="3">
|
||||
<si>
|
||||
<t>A string</t>
|
||||
</si>
|
||||
<si>
|
||||
<t>A second String</t>
|
||||
</si>
|
||||
<si>
|
||||
<r>
|
||||
<t>Hello</t>
|
||||
</r>
|
||||
<r>
|
||||
<rPr>
|
||||
<b/>
|
||||
<sz val="11"/>
|
||||
<color rgb="FFFF0000"/>
|
||||
<rFont val="Calibri"/>
|
||||
<family val="2"/>
|
||||
<scheme val="minor"/>
|
||||
</rPr>
|
||||
<t xml:space="preserve"> World</t>
|
||||
</r>
|
||||
</si>
|
||||
</sst>"#;
|
||||
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()
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
386
xlsx/src/import/styles.rs
Normal file
386
xlsx/src/import/styles.rs
Normal file
@@ -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<Option<BorderItem>, XlsxError> {
|
||||
let style;
|
||||
let color;
|
||||
let border_nodes = node
|
||||
.children()
|
||||
.filter(|n| n.has_tag_name(name))
|
||||
.collect::<Vec<Node>>();
|
||||
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::<Vec<Node>>();
|
||||
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<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
) -> Result<Styles, XlsxError> {
|
||||
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::<Vec<Node>>();
|
||||
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::<Vec<Node>>()[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::<i32>()
|
||||
.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::<i32>()
|
||||
.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::<Vec<Node>>()[0];
|
||||
for fill in fill_nodes.children() {
|
||||
let pattern_fill = fill
|
||||
.children()
|
||||
.filter(|n| n.has_tag_name("patternFill"))
|
||||
.collect::<Vec<Node>>();
|
||||
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::<Vec<Node>>()[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::<Vec<Node>>()[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::<Vec<Node>>()[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::<Vec<Node>>()[0];
|
||||
for xfs in cell_xfs_nodes.children() {
|
||||
let xf_id = get_attribute(&xfs, "xfId")?.parse::<i32>()?;
|
||||
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::<Vec<Node>>();
|
||||
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();
|
||||
// <colors>
|
||||
// <mruColors>
|
||||
// <color rgb="FFB1BB4D"/>
|
||||
// <color rgb="FFFF99CC"/>
|
||||
// <color rgb="FF6C56DC"/>
|
||||
// <color rgb="FFFF66CC"/>
|
||||
// </mruColors>
|
||||
// </colors>
|
||||
|
||||
Ok(Styles {
|
||||
num_fmts,
|
||||
fonts,
|
||||
fills,
|
||||
borders,
|
||||
cell_style_xfs,
|
||||
cell_xfs,
|
||||
cell_styles,
|
||||
})
|
||||
}
|
||||
215
xlsx/src/import/tables.rs
Normal file
215
xlsx/src/import/tables.rs
Normal file
@@ -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};
|
||||
|
||||
// <table name="Table" displayName="Table" totalsRowCount ref="A1:D6">
|
||||
// <autoFilter ref="A1:D6">
|
||||
// <filterColumn colId="0">
|
||||
// <customFilters><customFilter operator="greaterThan" val=20></customFilter></customFilters>
|
||||
// </filterColumn>
|
||||
// </autoFilter>
|
||||
// <tableColumns count="5">
|
||||
// <tableColumn name="Monday" totalsRowFunction="sum" />
|
||||
// ...
|
||||
// </tableColumns>
|
||||
// <tableStyleInfo name="TableStyle5"/>
|
||||
// </table>
|
||||
|
||||
/// Reads a table in an Excel workbook
|
||||
pub(crate) fn load_table<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
path: &str,
|
||||
sheet_name: &str,
|
||||
) -> Result<Table, XlsxError> {
|
||||
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::<u32>().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::<u32>().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::<u32>() {
|
||||
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::<u32>() {
|
||||
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::<u32>() {
|
||||
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::<Vec<Node>>();
|
||||
|
||||
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::<Vec<Node>>();
|
||||
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::<u32>().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::<u32>() {
|
||||
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::<u32>() {
|
||||
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::<u32>() {
|
||||
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::<Vec<Node>>();
|
||||
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(),
|
||||
})
|
||||
}
|
||||
78
xlsx/src/import/util.rs
Normal file
78
xlsx/src/import/util.rs
Normal file
@@ -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::<i32>().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<ExpandedName<'n, 'm>>,
|
||||
{
|
||||
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::<Vec<Node>>();
|
||||
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<Option<String>, 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::<i32>()?;
|
||||
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::<i32>()?;
|
||||
let tint = match node.attribute("tint") {
|
||||
Some(t) => t.parse::<f64>().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"))
|
||||
}
|
||||
79
xlsx/src/import/workbook.rs
Normal file
79
xlsx/src/import/workbook.rs
Normal file
@@ -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<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
) -> Result<WorkbookXML, XlsxError> {
|
||||
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<Node> = 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::<u32>()?;
|
||||
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<Node> = 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::<usize>()?;
|
||||
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,
|
||||
})
|
||||
}
|
||||
925
xlsx/src/import/worksheets.rs
Normal file
925
xlsx/src/import/worksheets.rs
Normal file
@@ -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<Sheet>,
|
||||
pub(crate) defined_names: Vec<DefinedName>,
|
||||
}
|
||||
|
||||
#[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::<char>::new();
|
||||
for c in cs {
|
||||
if !c.is_ascii_digit() {
|
||||
column.push(c);
|
||||
}
|
||||
}
|
||||
column.into_iter().collect()
|
||||
}
|
||||
|
||||
fn load_dimension(ws: Node) -> String {
|
||||
// <dimension ref="A1:O18"/>
|
||||
let application_nodes = ws
|
||||
.children()
|
||||
.filter(|n| n.has_tag_name("dimension"))
|
||||
.collect::<Vec<Node>>();
|
||||
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<Vec<Col>, XlsxError> {
|
||||
// cols
|
||||
// <cols>
|
||||
// <col min="5" max="5" width="38.26953125" customWidth="1"/>
|
||||
// <col min="6" max="6" width="9.1796875" style="1"/>
|
||||
// <col min="8" max="8" width="4" customWidth="1"/>
|
||||
// </cols>
|
||||
let mut cols = Vec::new();
|
||||
let columns = ws
|
||||
.children()
|
||||
.filter(|n| n.has_tag_name("cols"))
|
||||
.collect::<Vec<Node>>();
|
||||
if columns.len() == 1 {
|
||||
for col in columns[0].children() {
|
||||
let min = get_attribute(&col, "min")?;
|
||||
let min = min.parse::<i32>()?;
|
||||
let max = get_attribute(&col, "max")?;
|
||||
let max = max.parse::<i32>()?;
|
||||
let width = get_attribute(&col, "width")?;
|
||||
let width = width.parse::<f64>()?;
|
||||
let custom_width = matches!(col.attribute("customWidth"), Some("1"));
|
||||
let style = col
|
||||
.attribute("style")
|
||||
.map(|s| s.parse::<i32>().unwrap_or(0));
|
||||
cols.push(Col {
|
||||
min,
|
||||
max,
|
||||
width,
|
||||
custom_width,
|
||||
style,
|
||||
})
|
||||
}
|
||||
}
|
||||
Ok(cols)
|
||||
}
|
||||
|
||||
fn load_merge_cells(ws: Node) -> Result<Vec<String>, XlsxError> {
|
||||
// 18.3.1.55 Merge Cells
|
||||
// <mergeCells count="1">
|
||||
// <mergeCell ref="K7:L10"/>
|
||||
// </mergeCells>
|
||||
let mut merge_cells = Vec::new();
|
||||
let merge_cells_nodes = ws
|
||||
.children()
|
||||
.filter(|n| n.has_tag_name("mergeCells"))
|
||||
.collect::<Vec<Node>>();
|
||||
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<Option<String>, XlsxError> {
|
||||
// <sheetPr>
|
||||
// <tabColor theme="5" tint="-0.249977111117893"/>
|
||||
// </sheetPr>
|
||||
let mut color = None;
|
||||
let sheet_pr = ws
|
||||
.children()
|
||||
.filter(|n| n.has_tag_name("sheetPr"))
|
||||
.collect::<Vec<Node>>();
|
||||
if sheet_pr.len() == 1 {
|
||||
let tabs = sheet_pr[0]
|
||||
.children()
|
||||
.filter(|n| n.has_tag_name("tabColor"))
|
||||
.collect::<Vec<Node>>();
|
||||
if tabs.len() == 1 {
|
||||
color = get_color(tabs[0])?;
|
||||
}
|
||||
}
|
||||
Ok(color)
|
||||
}
|
||||
|
||||
fn load_comments<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
path: &str,
|
||||
) -> Result<Vec<Comment>, 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::<Vec<Node>>();
|
||||
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::<Vec<String>>()
|
||||
.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<CellReferenceRC, ParseReferenceError> {
|
||||
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::<i32>().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<String, Table>,
|
||||
) -> Result<String, XlsxError> {
|
||||
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<i32> {
|
||||
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<String>,
|
||||
) -> 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::<f64>().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::<i32>().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::<f64>().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<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
path: &str,
|
||||
tables: &mut HashMap<String, Table>,
|
||||
sheet_name: &str,
|
||||
) -> Result<Vec<Comment>, 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::<Vec<Node>>();
|
||||
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) {
|
||||
// <sheetViews>
|
||||
// <sheetView workbookViewId="0">
|
||||
// <selection activeCell="E10" sqref="E10"/>
|
||||
// </sheetView>
|
||||
// </sheetViews>
|
||||
// <sheetFormatPr defaultRowHeight="14.5" x14ac:dyDescent="0.35"/>
|
||||
|
||||
// If we have frozen rows and columns:
|
||||
|
||||
// <sheetView tabSelected="1" workbookViewId="0">
|
||||
// <pane xSplit="3" ySplit="2" topLeftCell="D3" activePane="bottomRight" state="frozen"/>
|
||||
// <selection pane="topRight" activeCell="D1" sqref="D1"/>
|
||||
// <selection pane="bottomLeft" activeCell="A3" sqref="A3"/>
|
||||
// <selection pane="bottomRight" activeCell="K16" sqref="K16"/>
|
||||
// </sheetView>
|
||||
|
||||
// 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::<Vec<Node>>();
|
||||
|
||||
if sheet_views.len() != 1 {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
let sheet_view = sheet_views[0]
|
||||
.children()
|
||||
.filter(|n| n.has_tag_name("sheetView"))
|
||||
.collect::<Vec<Node>>();
|
||||
|
||||
if sheet_view.len() != 1 {
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
let pane = sheet_view[0]
|
||||
.children()
|
||||
.filter(|n| n.has_tag_name("pane"))
|
||||
.collect::<Vec<Node>>();
|
||||
|
||||
// 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<Comment>,
|
||||
}
|
||||
|
||||
pub(super) fn load_sheet<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
path: &str,
|
||||
settings: SheetSettings,
|
||||
worksheets: &[String],
|
||||
tables: &HashMap<String, Table>,
|
||||
shared_strings: &mut Vec<String>,
|
||||
) -> Result<Worksheet, XlsxError> {
|
||||
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
|
||||
// <row r="1" spans="1:15" x14ac:dyDescent="0.35">
|
||||
// <c r="A1" t="s">
|
||||
// <v>0</v>
|
||||
// </c>
|
||||
// <c r="D1">
|
||||
// <f>C1+1</f>
|
||||
// </c>
|
||||
// </row>
|
||||
|
||||
// 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::<Vec<Node>>()[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::<i32>()?;
|
||||
// `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::<f64>().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::<i32>().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<Node> = 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::<i32>().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:
|
||||
// <c r="E3">
|
||||
// <f>C2+1</f>
|
||||
// <v>3</v>
|
||||
// </c>
|
||||
// A cell with a shared formula will be either a "mother" cell:
|
||||
// <c r="D2">
|
||||
// <f t="shared" ref="D2:D3" si="0">C2+1</f>
|
||||
// <v>3</v>
|
||||
// </c>
|
||||
// Or a "daughter" cell:
|
||||
// <c r="D3">
|
||||
// <f t="shared" si="0"/>
|
||||
// <v>4</v>
|
||||
// </c>
|
||||
// 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<Node> = 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::<i32>()?;
|
||||
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
|
||||
// <conditionalFormatting sqref="B1:B9">
|
||||
// <cfRule type="colorScale" priority="1">
|
||||
// <colorScale>
|
||||
// <cfvo type="min"/>
|
||||
// <cfvo type="max"/>
|
||||
// <color rgb="FFF8696B"/>
|
||||
// <color rgb="FFFCFCFF"/>
|
||||
// </colorScale>
|
||||
// </cfRule>
|
||||
// </conditionalFormatting>
|
||||
// pageSetup
|
||||
// <pageSetup orientation="portrait" r:id="rId1"/>
|
||||
|
||||
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<R: Read + std::io::Seek>(
|
||||
archive: &mut zip::read::ZipArchive<R>,
|
||||
rels: &HashMap<String, Relationship>,
|
||||
workbook: &WorkbookXML,
|
||||
tables: &mut HashMap<String, Table>,
|
||||
shared_strings: &mut Vec<String>,
|
||||
) -> Result<Vec<Worksheet>, 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<String> = &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)
|
||||
}
|
||||
61
xlsx/src/lib.rs
Normal file
61
xlsx/src/lib.rs
Normal file
@@ -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;
|
||||
BIN
xlsx/tests/basic_text.xlsx
Normal file
BIN
xlsx/tests/basic_text.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_test_no_export/tables.xlsx
Normal file
BIN
xlsx/tests/calc_test_no_export/tables.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/ABS.xlsx
Normal file
BIN
xlsx/tests/calc_tests/ABS.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/ATAN2_POWER.xlsx
Normal file
BIN
xlsx/tests/calc_tests/ATAN2_POWER.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/AVERAGE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/AVERAGE.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/BESSEL.xlsx
Normal file
BIN
xlsx/tests/calc_tests/BESSEL.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/BITs.xlsx
Normal file
BIN
xlsx/tests/calc_tests/BITs.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/CHOOSE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/CHOOSE.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/COMPLEXs.xlsx
Normal file
BIN
xlsx/tests/calc_tests/COMPLEXs.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/CONCAT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/CONCAT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/CONCATENATE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/CONCATENATE.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/CONVERT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/CONVERT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/COUNT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/COUNT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/COUNTIF.xlsx
Normal file
BIN
xlsx/tests/calc_tests/COUNTIF.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/COUNTIFS.xlsx
Normal file
BIN
xlsx/tests/calc_tests/COUNTIFS.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/CUMPRINC_CUMIPMT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/CUMPRINC_CUMIPMT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/DATE_DAY_MONTH_YEAR.xlsx
Normal file
BIN
xlsx/tests/calc_tests/DATE_DAY_MONTH_YEAR.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/DB_DDB.xlsx
Normal file
BIN
xlsx/tests/calc_tests/DB_DDB.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/DOLLARs.xlsx
Normal file
BIN
xlsx/tests/calc_tests/DOLLARs.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/EOMONTH.xlsx
Normal file
BIN
xlsx/tests/calc_tests/EOMONTH.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/ERFs.xlsx
Normal file
BIN
xlsx/tests/calc_tests/ERFs.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/ERROR.TYPE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/ERROR.TYPE.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/EXACT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/EXACT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/FIND_SEARCH.xlsx
Normal file
BIN
xlsx/tests/calc_tests/FIND_SEARCH.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/FV.xlsx
Normal file
BIN
xlsx/tests/calc_tests/FV.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/GESTEP_DELTA.xlsx
Normal file
BIN
xlsx/tests/calc_tests/GESTEP_DELTA.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/HVLOOKUP.xlsx
Normal file
BIN
xlsx/tests/calc_tests/HVLOOKUP.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/IFNA.xlsx
Normal file
BIN
xlsx/tests/calc_tests/IFNA.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/IFS.xlsx
Normal file
BIN
xlsx/tests/calc_tests/IFS.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/IPMT_PPMT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/IPMT_PPMT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/IRR.xlsx
Normal file
BIN
xlsx/tests/calc_tests/IRR.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/ISPMT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/ISPMT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/ISREF_ISFORMULA_ISODD_ISEVEN.xlsx
Normal file
BIN
xlsx/tests/calc_tests/ISREF_ISFORMULA_ISODD_ISEVEN.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/IS_INFORMATION.xlsx
Normal file
BIN
xlsx/tests/calc_tests/IS_INFORMATION.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/MATCH.xlsx
Normal file
BIN
xlsx/tests/calc_tests/MATCH.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/MIN_MAX.xlsx
Normal file
BIN
xlsx/tests/calc_tests/MIN_MAX.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/MIRR.xlsx
Normal file
BIN
xlsx/tests/calc_tests/MIRR.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/NOMINAL_EFFECT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/NOMINAL_EFFECT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/NPER.xlsx
Normal file
BIN
xlsx/tests/calc_tests/NPER.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/NPV.xlsx
Normal file
BIN
xlsx/tests/calc_tests/NPV.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/NUMBER_SYSTEMS.xlsx
Normal file
BIN
xlsx/tests/calc_tests/NUMBER_SYSTEMS.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/PDURATION.xlsx
Normal file
BIN
xlsx/tests/calc_tests/PDURATION.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/PMT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/PMT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/PRODUCT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/PRODUCT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/PV.xlsx
Normal file
BIN
xlsx/tests/calc_tests/PV.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/RATE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/RATE.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/REPT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/REPT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/ROUND.xlsx
Normal file
BIN
xlsx/tests/calc_tests/ROUND.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/ROW_COLUM.xlsx
Normal file
BIN
xlsx/tests/calc_tests/ROW_COLUM.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/RRI.xlsx
Normal file
BIN
xlsx/tests/calc_tests/RRI.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SLN_SYD.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SLN_SYD.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SQRT_SQRTPI.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SQRT_SQRTPI.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/STRING_HANDLING.xlsx
Normal file
BIN
xlsx/tests/calc_tests/STRING_HANDLING.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SUBSTITUTE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SUBSTITUTE.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SUMIFS.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SUMIFS.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/SUMIF_AVERAGE_IF.xlsx
Normal file
BIN
xlsx/tests/calc_tests/SUMIF_AVERAGE_IF.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/TBILLs.xlsx
Normal file
BIN
xlsx/tests/calc_tests/TBILLs.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/TEXT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/TEXT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/TEXTBEFORE_TEXTAFTER.xlsx
Normal file
BIN
xlsx/tests/calc_tests/TEXTBEFORE_TEXTAFTER.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/TEXTJOIN.xlsx
Normal file
BIN
xlsx/tests/calc_tests/TEXTJOIN.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/TYPE.xlsx
Normal file
BIN
xlsx/tests/calc_tests/TYPE.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/T_VALUE_VALUETOTEXT.xlsx
Normal file
BIN
xlsx/tests/calc_tests/T_VALUE_VALUETOTEXT.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/XIRR.xlsx
Normal file
BIN
xlsx/tests/calc_tests/XIRR.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/XLOOKUP.xlsx
Normal file
BIN
xlsx/tests/calc_tests/XLOOKUP.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/XNPV.xlsx
Normal file
BIN
xlsx/tests/calc_tests/XNPV.xlsx
Normal file
Binary file not shown.
BIN
xlsx/tests/calc_tests/arithmetic.xlsx
Normal file
BIN
xlsx/tests/calc_tests/arithmetic.xlsx
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user