diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6985cf1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..622dfeb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1874 @@ +# 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 = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[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 = "anyhow" +version = "1.0.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb2f989d18dd141ab8ae82f64d1a8cdd37e0840f73a406896cf5e99502fab61" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "assert_cmd" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa3d466004a8b4cb1bc34044240a2fd29d17607e2e3bd613eb44fd48e8100da3" +dependencies = [ + "bstr 1.1.0", + "doc-comment", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "assert_fs" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d94b2a3f3786ff2996a98afbd6b4e5b7e890d685ccf67577f508ee2342c71cc9" +dependencies = [ + "doc-comment", + "globwalk", + "predicates", + "predicates-core", + "predicates-tree", + "tempfile", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", +] + +[[package]] +name = "bstr" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45ea9b00a7b3f2988e9a65ad3917e62123c38dba709b666506207be96d1790b" +dependencies = [ + "memchr", + "once_cell", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "bytes" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" + +[[package]] +name = "bytesize" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" + +[[package]] +name = "cargo" +version = "0.66.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbff5076ff17b84f81946ca2d5536e60bfa2344cd365efb57c19fd808a17640" +dependencies = [ + "anyhow", + "atty", + "bytesize", + "cargo-platform", + "cargo-util", + "clap 3.2.23", + "crates-io", + "curl", + "curl-sys", + "env_logger 0.9.3", + "filetime", + "flate2", + "fwdansi", + "git2", + "git2-curl", + "glob", + "hex 0.4.3", + "home", + "humantime", + "ignore", + "im-rc", + "indexmap", + "itertools", + "jobserver", + "lazy_static", + "lazycell", + "libc", + "libgit2-sys", + "log", + "memchr", + "opener", + "os_info", + "pathdiff", + "percent-encoding", + "rustc-workspace-hack", + "rustfix", + "semver", + "serde", + "serde_ignored", + "serde_json", + "shell-escape", + "strip-ansi-escapes", + "tar", + "tempfile", + "termcolor", + "toml_edit", + "unicode-width", + "unicode-xid", + "url", + "walkdir", + "winapi", +] + +[[package]] +name = "cargo-instruments" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbd75a6a7b43a70eade9c42d2fb41f2bba3c556d06ed8a8f426c146bac2cffc" +dependencies = [ + "anyhow", + "cargo", + "chrono", + "semver", + "structopt", + "termcolor", +] + +[[package]] +name = "cargo-platform" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-util" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75f6bfca7b85d6e8c6a42405e9b4ecadd2e63f75f94aabfb524378b57a557a4" +dependencies = [ + "anyhow", + "core-foundation", + "crypto-hash", + "filetime", + "hex 0.4.3", + "jobserver", + "libc", + "log", + "miow", + "same-file", + "shell-escape", + "tempfile", + "walkdir", + "winapi", +] + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cell" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "assert_fs", + "cargo-instruments", + "clap 4.0.32", + "env_logger 0.10.0", + "log", + "predicates", +] + +[[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.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "bitflags", + "textwrap 0.11.0", + "unicode-width", +] + +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "atty", + "bitflags", + "clap_lex 0.2.4", + "indexmap", + "strsim", + "termcolor", + "textwrap 0.16.0", +] + +[[package]] +name = "clap" +version = "4.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7db700bc935f9e43e88d00b0850dae18a63773cfbec6d8e070fccf7fef89a39" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex 0.3.0", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4198f73e42b4936b35b5bb248d81d2b595ecb170da0bac7655c54eedfa8da8" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "commoncrypto" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d056a8586ba25a1e4d61cb090900e495952c7886786fc55f909ab2f819b69007" +dependencies = [ + "commoncrypto-sys", +] + +[[package]] +name = "commoncrypto-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fed34f46747aa73dfaa578069fd8279d2818ade2b55f38f22a9401c7f4083e2" +dependencies = [ + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crates-io" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4a87459133b2e708195eaab34be55039bc30e0d120658bd40794bb00b6328d" +dependencies = [ + "anyhow", + "curl", + "percent-encoding", + "serde", + "serde_json", + "url", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-hash" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a77162240fd97248d19a564a565eb563a3f592b386e4136fb300909e67dddca" +dependencies = [ + "commoncrypto", + "hex 0.3.2", + "openssl", + "winapi", +] + +[[package]] +name = "curl" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509bd11746c7ac09ebd19f0b17782eae80aadee26237658a6b4808afb5c11a22" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "winapi", +] + +[[package]] +name = "curl-sys" +version = "0.4.59+curl-7.86.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cfce34829f448b08f55b7db6d0009e23e2e86a34e8c2b366269bf5799b4a407" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "winapi", +] + +[[package]] +name = "cxx" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d1075c37807dcf850c379432f0df05ba52cc30f279c5cfc43cc221ce7f8579" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5044281f61b27bc598f2f6647d480aed48d2bf52d6eb0b627d84c0361b17aa70" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61b50bc93ba22c27b0d31128d2d130a0a6b3d267ae27ef7e4fae2167dfe8781c" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e61fda7e62115119469c7b3591fd913ecca96fb766cfd3f2e2502ab7bc87a5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e884668cd0c7480504233e951174ddc3b382f7c2666e3b7310b5c4e7b0c37f9" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "libz-sys", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fwdansi" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c1f5787fe85505d1f7777268db5103d80a7a374d2316a7ce262e57baf8f208" +dependencies = [ + "memchr", + "termcolor", +] + +[[package]] +name = "git2" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2994bee4a3a6a51eb90c218523be382fd7ea09b16380b9312e9dbe955ff7c7d1" +dependencies = [ + "bitflags", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + +[[package]] +name = "git2-curl" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed817a00721e2f8037ba722e60358d4956dae9cca10315fc982f967907d3b0cd" +dependencies = [ + "curl", + "git2", + "log", + "url", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "globset" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +dependencies = [ + "aho-corasick", + "bstr 0.2.17", + "fnv", + "log", + "regex", +] + +[[package]] +name = "globwalk" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc" +dependencies = [ + "bitflags", + "ignore", + "walkdir", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747309b4b440c06d57b0b25f2aee03ee9b5e5397d288c60e21fc709bb98a7408" +dependencies = [ + "winapi", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "ignore" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +dependencies = [ + "crossbeam-utils", + "globset", + "lazy_static", + "log", + "memchr", + "regex", + "same-file", + "thread_local", + "walkdir", + "winapi-util", +] + +[[package]] +name = "im-rc" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1955a75fa080c677d3972822ec4bad316169ab1cfc6c257a942c2265dbe5fe" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46112a93252b123d31a119a8d1a1ac19deac4fac6e0e8b0df58f0d4e5870e63c" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dfb6c8100ccc63462345b67d1bbc3679177c75ee4bf59bf29c8b1d110b8189" +dependencies = [ + "hermit-abi 0.2.6", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "jobserver" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "kstring" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3066350882a1cd6d950d055997f379ac37fd39f81cd4d8ed186032eb3c5747" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "libgit2-sys" +version = "0.14.1+1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a07fb2692bc3593bda59de45a502bb3071659f2c515e28c71e728306b038e17" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + +[[package]] +name = "libnghttp2-sys" +version = "0.1.7+1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "libssh2-sys" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b094a36eb4b8b8c8a7b4b8ae43b2944502be3e59cd87687595cf6b0a71b3f4ca" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "opener" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea3ebcd72a54701f56345f16785a6d3ac2df7e986d273eb4395c0b01db17952" +dependencies = [ + "bstr 0.2.17", + "winapi", +] + +[[package]] +name = "openssl" +version = "0.10.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b102428fd03bc5edf97f62620f7298614c45cedf287c271e7ed450bbaf83f2e1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23bbbf7854cd45b83958ebe919f0e8e516793727652e27fda10a8384cfc790b7" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "os_info" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4750134fb6a5d49afc80777394ad5d95b04bc12068c6abb92fae8f43817270f" +dependencies = [ + "log", + "serde", + "winapi", +] + +[[package]] +name = "os_str_bytes" +version = "6.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "predicates" +version = "2.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54fc5dc63ed3bbf19494623db4f3af16842c0d975818e469022d09e53f0aa05" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f883590242d3c6fc5bf50299011695fa6590c2c70eac95ee1bdb9a733ad1a2" + +[[package]] +name = "predicates-tree" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ff541861505aabf6ea722d2131ee980b8276e10a1297b94e896dd8b621850d" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a8eca9f9c4ffde41714334dee777596264c7825420f521abc92b5b5deb63a5" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi", +] + +[[package]] +name = "rustc-workspace-hack" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc71d2faa173b74b232dedc235e3ee1696581bb132fc116fa3626d6151a1a8fb" + +[[package]] +name = "rustfix" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd2853d9e26988467753bd9912c3a126f642d05d229a4b53f5752ee36c56481" +dependencies = [ + "anyhow", + "log", + "serde", + "serde_json", +] + +[[package]] +name = "rustix" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3807b5d10909833d3e9acd1eb5fb988f79376ff10fce42937de71a449c4c588" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "semver" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fed41fc1a24994d044e6db6935e69511a1153b52c15eb42493b26fa87feba0" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "255abe9a125a985c05190d687b320c12f9b1f0b99445e608c21ba0782c719ad8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_ignored" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94eb4a4087ba8bdf14a9208ac44fddbf55c01a6195f7edfc511ddaff6cae45a6" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_json" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c235533714907a8c2464236f5c4b2a17262ef1bd71f38f35ea592c8da6883" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shell-escape" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "socket2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strip-ansi-escapes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "011cbb39cf7c1f62871aea3cc46e5817b0937b49e9447370c93cacbe93a766d8" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap 2.34.0", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck 0.3.3", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059e91184749cb66be6dc994f67f182b6d897cb3df74a5bf66b5e709295fd8" + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml_edit" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5376256e44f2443f8896ac012507c19a012df0fe8758b55246ae51a2279db51f" +dependencies = [ + "combine", + "indexmap", + "itertools", + "kstring", + "serde", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "vte" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cbce692ab4ca2f1f3047fcf732430249c0e971bfdd2b234cf2c47ad93af5983" +dependencies = [ + "arrayvec", + "utf8parse", + "vte_generate_state_changes", +] + +[[package]] +name = "vte_generate_state_changes" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d257817081c7dffcdbab24b9e62d2def62e2ff7d00b1c20062551e6cccc145ff" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasm-bindgen" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +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.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6d1b7dd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "cell" +description = "A simple spreadsheet calculator with a custom grammar" +version = "0.1.0" +edition = "2021" +authors = ["Rafael Beckel "] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +clap = { version = "4.0", features = ["derive"] } +env_logger = "0.10" +log = "0.4" + +[dev-dependencies] +assert_cmd = "2.0.7" +assert_fs = "1.0.10" +predicates = "2.1.4" +cargo-instruments = "0.4.8" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d048a3f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM rust:latest as builder +WORKDIR /usr/src/cell +COPY . . +RUN cargo install --path . + +FROM debian:buster-slim +COPY --from=builder /usr/local/cargo/bin/cell /usr/local/bin/cell +COPY --from=builder /usr/src/cell/transactions.csv /usr/local/transactions.csv +CMD ["cell", "/usr/local/transactions.csv"] diff --git a/README.md b/README.md index 6cdcb80..e30f7db 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ language is very similar to excel formulas, it supports basic arithmetic express as well as function calls that provide additional features like comparisons, string concatenations and other useful utility functions. -**Operations** +#### Operations - `^^` Copies the formula from the cell above in the same column, with some special evaluation rules - `(A..Z)n` references a cell by a combination of a column-letter+row-number. Ex: A2 B3 diff --git a/dist/macos-arm64/cell b/dist/macos-arm64/cell new file mode 100755 index 0000000..cdfcd59 Binary files /dev/null and b/dist/macos-arm64/cell differ diff --git a/dist/macos-arm64/transactions.csv b/dist/macos-arm64/transactions.csv new file mode 100644 index 0000000..03b9a04 --- /dev/null +++ b/dist/macos-arm64/transactions.csv @@ -0,0 +1,10 @@ +!date|!transaction_id|!tokens|!token_prices|!total_cost +2022-02-20|=concat("t_", text(incFrom(1)))|btc,eth,dai|38341.88,2643.77,1.0003|=sum(split(D2, ",")) +2022-02-21|=^^|bch,eth,dai|304.38,2621.15,1.0001|=E^+sum(split(D3, ",")) +2022-02-22|=^^|sol,eth,dai|85,2604.17,0.9997|=^^ +!fee|!cost_threshold||| +0.09|10000||| +!adjusted_cost|||| +=E^v+(E^v*A6)|||| +!cost_too_high|||| +=text(gte(@adjusted_cost<1>, @cost_threshold<1>))|||| diff --git a/dist/macos-arm64/transactions_false.csv b/dist/macos-arm64/transactions_false.csv new file mode 100644 index 0000000..4e0a9e4 --- /dev/null +++ b/dist/macos-arm64/transactions_false.csv @@ -0,0 +1,10 @@ +!date|!transaction_id|!tokens|!token_prices|!total_cost +2022-02-20|=concat("t_", text(incFrom(1)))|btc,eth,dai|38341.88,2643.77,1.0003|=sum(split(D2, ",")) +2022-02-21|=^^|bch,eth,dai|304.38,2621.15,1.0001|=E^+sum(split(D3, ",")) +2022-02-22|=^^|sol,eth,dai|85,2604.17,0.9997|=^^ +!fee|!cost_threshold||| +0.09|51000||| +!adjusted_cost|||| +=E^v+(E^v*A6)|||| +!cost_too_high|||| +=text(gte(@adjusted_cost<1>, @cost_threshold<1>))|||| diff --git a/dist/macos-arm64/transactions_invalid.csv b/dist/macos-arm64/transactions_invalid.csv new file mode 100644 index 0000000..4027da6 --- /dev/null +++ b/dist/macos-arm64/transactions_invalid.csv @@ -0,0 +1,10 @@ +!date|!transaction_id|!tokens|!token_prices|!total_cost +2022-02-20|=concat("t_", text(incFrom(1)))|btc,eth,dai|38341.88,2643.77,1.0003|=sum(split(D2, ",")) +2022-02-21|=^^|bch,eth,dai|304.38,2621.15,1.0001|=E^+sum(split(D3, ",")) +2022-02-22|=^^|sol,eth,dai|85,2604.17,0.9997|=^^ +!fee|!cost_threshold||| +0.09|10000||| +!adjusted_cost||| +=E^v+(E^v*A6)|||| +!cost_too_high|||| +=text(gte(@adjusted_cost<1>, @cost_threshold<1>))|||| diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f4551ad --- /dev/null +++ b/src/main.rs @@ -0,0 +1,37 @@ +use std::time::Instant; +use std::{io::stdout, path::PathBuf}; + +use anyhow::Result; +use clap::Parser; +use log::{info, trace}; + +mod spreadsheets; +use spreadsheets::table::Table; + +/// Parses an Excel-like CSV file and prints the result to stdout +#[derive(Debug, Parser)] +struct Cli { + /// The path to the CSV file to read + path: PathBuf, +} + +fn main() -> Result<()> { + env_logger::init(); + trace!("starting up"); + let start = Instant::now(); + let args = Cli::parse(); + + let table = Table::from_file(&args.path)?; + let table = table.borrow(); + + table.print(&mut stdout())?; + + print_elapsed_time(start); + Ok(()) +} + +fn print_elapsed_time(start: Instant) { + let elapsed = start.elapsed(); + let miliseconds = elapsed.as_micros() as f32 / 1000.0; + info!("executed in {}ms", format!("{:.2}", miliseconds)); +} diff --git a/src/spreadsheets.rs b/src/spreadsheets.rs new file mode 100644 index 0000000..62546d4 --- /dev/null +++ b/src/spreadsheets.rs @@ -0,0 +1,6 @@ +mod calculator; +mod cell; +mod expression; +mod lexer; +mod parser; +pub mod table; diff --git a/src/spreadsheets/calculator.rs b/src/spreadsheets/calculator.rs new file mode 100644 index 0000000..4a0b3dd --- /dev/null +++ b/src/spreadsheets/calculator.rs @@ -0,0 +1,196 @@ +use crate::spreadsheets::cell::Cell; +use crate::spreadsheets::lexer::Lexer; +use crate::spreadsheets::parser::Parser; + +use anyhow::Result; + +pub struct Calculator; + +impl Calculator { + pub fn calculate(cell: &Cell) -> Result { + let tokens = Lexer::tokenize(&cell.value); + let expression = Parser::parse(&tokens)?; + let result = expression.evaluate(cell)?; + + Ok(result.to_string()) + } +} + +#[cfg(test)] +mod tests { + use std::cell::RefCell; + use std::rc::Rc; + + use super::*; + use crate::spreadsheets::cell::Cell; + use crate::spreadsheets::table::Table; + + fn mock_table() -> Rc> { + // A B C D + let contents = "=incFrom(1) | 3.0 | !total | !total_plus_1 | text,to,split \n\ + =^^ | =A1+B^ | =A1+B^v | 1.0 | 1,2,3,4 \n\ + =sum(A1,A2) | =sum(A1:B2) | =sum(A3,B3) | =@total<2> + 1.0 | 1.0,2.1,3.2,4 \n"; + + Table::from_string(contents).unwrap() + } + + #[test] + fn test_copy_and_increment_formula() { + let cell = Cell::new(&mock_table(), 2, 1, "=^^"); + let result = Calculator::calculate(&cell).unwrap(); + + // ^^ = incFrom(1) = 2 + assert_eq!(result, String::from("2")); + } + + #[test] + fn test_copy_and_increment_cell_references() { + let cell = Cell::new(&mock_table(), 4, 1, "=^^"); + let result = Calculator::calculate(&cell).unwrap(); + + // ^^ = A3 = sum(A1, A2) => increments to sum(A2, A3) + // A1 = 1, A2 = 2, A3 = 3 + // sum(A2, A3) = 5 + assert_eq!(result, String::from("5")); + } + + #[test] + fn test_copy_and_increment_cell_range() { + let cell = Cell::new(&mock_table(), 4, 2, "=^^"); + let result = Calculator::calculate(&cell).unwrap(); + + // ^^ = sum(A1:B2) = sum(A2:B3) + // sum(A1:A2) = (1 + 2 + 3 + 4) = 10 + // sum(A2:A3) = (2 + 3 + 4 + 10) = 19 + assert_eq!(result, String::from("19")); + } + + #[test] + fn test_calculate_sum_with_range() { + let cell = Cell::new(&mock_table(), 2, 4, "=sum(A1:B2) + 1"); + let result = Calculator::calculate(&cell).unwrap(); + + // A1:B2 = (1 + 2 + 3 + 4) = 10 + // 10 + 1 = 11 + assert_eq!(result, String::from("11")); + } + + #[test] + fn test_calculate_sum_with_parameters_and_above_formula() { + let cell = Cell::new(&mock_table(), 3, 4, "=sum(A1,A2)-D^"); + let result = Calculator::calculate(&cell).unwrap(); + + // A1 = 1, A2 = 2, total 3, + // D^ = 1 + // 3 - 1 = 2 + assert_eq!(result, String::from("2")); + } + + #[test] + fn test_calculate_sum_with_label_reference() { + let cell = Cell::new(&mock_table(), 4, 1, "=sum(A1, A2)+@total_plus_1<1>"); + let result = Calculator::calculate(&cell).unwrap(); + + // A1 = 1, A2 = 2, total 3, + // @total_plus_1<1> = 1 + // 3 + 1 = 4 + assert_eq!(result, String::from("4")); + } + + #[test] + fn test_calculate_gte_with_two_label_references() { + let cell = Cell::new( + &mock_table(), + 4, + 1, + "=text(gte(@total<2>, @total_plus_1<2>))", + ); + let result = Calculator::calculate(&cell).unwrap(); + + // @total<2> = 13, @total_plus_1<2> = 14 + // 13 >= 14 = false + // text(false) = "false" (string) + assert_eq!(result, String::from("false")); + } + + #[test] + fn test_calculate_sum_with_copy_last_result() { + let cell = Cell::new(&mock_table(), 4, 1, "=sum( A1,B2)+A^v"); + let result = Calculator::calculate(&cell).unwrap(); + + // A1 = 1, B2 = 4, total 5, + // A^v = sum(A1,A2) = 1 + 2 = 3 + // 3 + 5 = 8 + assert_eq!(result, String::from("8")); + } + + #[test] + fn test_calculate_copy_last_result_twice() { + let cell = Cell::new(&mock_table(), 1, 1, "=D^v+(D^v*A2)"); + let result = Calculator::calculate(&cell).unwrap(); + + // D^v = sum(A3,B3) + 1 + // A3 = sum(A1,A2) = 1 + 2 = 3 + // B3 = sum(A1:B2) = 1 + 2 + 3 + 4 = 10 + // D^v = 3 + 10 + 1 = 14 + // 14 + (14 * 2) = 42 + assert_eq!(result, String::from("42")); + } + + #[test] + fn test_calculate_sum_divided_by_copy_above_result() { + let cell = Cell::new(&mock_table(), 4, 1, "=sum(A1,A2)/B^"); + let result = Calculator::calculate(&cell).unwrap(); + + // A1 = 1, A2 = 2, total 3, + // B^ = 10 (the mock cell is in A4, so B^ is relative to row 4, results in B3) + // B3 = sum(A1:B2) = 1 + 2 + 3 + 4 = 10 + // 3 / 10 = 0.3 + assert_eq!(result, String::from("0.30")); + } + + #[test] + fn test_calculate_concat_with_inc() { + let cell = Cell::new(&mock_table(), 4, 1, "=concat(\"t_\", text(incFrom(1)))"); + let result = Calculator::calculate(&cell).unwrap(); + + // incfrom does not increment + assert_eq!(result, String::from("t_1")); + } + + #[test] + fn test_calculate_sum_with_text_returns_error() { + let cell = Cell::new(&mock_table(), 3, 1, "=D^+sum(split(E1, \",\"))"); + let result = Calculator::calculate(&cell).unwrap(); + + // E1 = split(text,to,split) = "text" "to" "split" + // sum("text", "to", "split") = 0 + // D^ = 1 + // 1 + 0 = 1 + assert_eq!(result, String::from("1")); + } + + #[test] + fn test_calculate_sum_with_split_integers() { + let cell = Cell::new(&mock_table(), 3, 1, "=D^+sum(split(E2, \",\"))"); + let result = Calculator::calculate(&cell).unwrap(); + + // E2 = split(1,2,3,4) = 1 2 3 4 + // sum(1,2,3,4) = 10 + // D^ = 1 + // 1 + 10 = 11 + assert_eq!(result, String::from("11")); + } + + #[test] + fn test_calculate_sum_with_split_floats() { + let cell = Cell::new(&mock_table(), 3, 1, "=D^+sum(split(E3, \",\"))"); + let result = Calculator::calculate(&cell).unwrap(); + + // E3 = split(1.0,2.1,3.2,4) = 1.0 2.1 3.2 4 + // sum(1.0,2.1,3.2,4) = 10.3 + // D^ = 1 + // 1 + 10.3 = 11.3 + assert_eq!(result, String::from("11.30")); + } +} diff --git a/src/spreadsheets/cell.rs b/src/spreadsheets/cell.rs new file mode 100644 index 0000000..7e024d4 --- /dev/null +++ b/src/spreadsheets/cell.rs @@ -0,0 +1,183 @@ +use std::cell::RefCell; +use std::rc::Rc; + +use crate::spreadsheets::calculator::Calculator; +use crate::spreadsheets::table::Table; + +#[derive(Debug, Clone)] +pub struct Cell { + pub table: Rc>, + pub row: usize, + pub column: usize, + pub hash: String, + pub value: String, + result: String, +} + +const LABEL_PREFIX: char = '!'; +const FORMULA_PREFIX: char = '='; + +impl Cell { + pub fn new(table: &Rc>, row: usize, column: usize, value: &str) -> Self { + let column_name = Self::column_name(column); + let hash = format!("{}{}", column_name, row); + let value = value.to_string(); + + Cell { + table: Rc::clone(&table), + row, + column, + hash, + value, + result: String::new(), + } + } + + pub fn label(&self) -> Option { + if self.is_label() { + return Some(self.value[1..].to_string()); + } + + None + } + + pub fn result(&self) -> String { + if self.is_formula() { + if self.result.is_empty() { + return Calculator::calculate(&self) + .expect(format!("invalid formula: {}", self.value).as_str()); + } else { + return self.result.clone(); + } + } + + self.value.clone() + } + + pub fn formula(&self) -> Option { + if self.is_formula() { + return Some(self.value[1..].to_string()); + } + + None + } + + pub fn column_name(column: usize) -> String { + let mut column_name = String::new(); + let mut column = column; + + while column > 0 { + let remainder = (column - 1) % 26; + let character = char::from_u32('A' as u32 + remainder as u32) + .expect("invalid character in column name"); + + column_name.insert(0, character); + column = (column - remainder) / 26; + } + + return format!("{}", column_name); + } + + pub fn column_number(column: &str) -> usize { + let mut column_number = 0; + let mut multiplier = 1; + + for c in column.chars().rev() { + column_number += (c as usize - 'A' as usize + 1) * multiplier; + multiplier *= 26; + } + + return column_number; + } + + fn is_label(&self) -> bool { + self.value.starts_with(LABEL_PREFIX) + } + + fn is_formula(&self) -> bool { + self.value.starts_with(FORMULA_PREFIX) + } +} + +pub fn get_column_name(column: usize) -> String { + Cell::column_name(column) +} + +pub fn get_column_number(column: &str) -> usize { + Cell::column_number(column) +} + +#[cfg(test)] + +mod tests { + use super::*; + #[test] + fn test_column_name_assignment() { + assert_eq!(Cell::column_name(1), "A"); + assert_eq!(Cell::column_name(2), "B"); + assert_eq!(Cell::column_name(3), "C"); + + assert_eq!(Cell::column_name(24), "X"); + assert_eq!(Cell::column_name(25), "Y"); + assert_eq!(Cell::column_name(26), "Z"); + + assert_eq!(Cell::column_name(27), "AA"); + assert_eq!(Cell::column_name(28), "AB"); + assert_eq!(Cell::column_name(29), "AC"); + + assert_eq!(Cell::column_name(50), "AX"); + assert_eq!(Cell::column_name(51), "AY"); + assert_eq!(Cell::column_name(52), "AZ"); + + assert_eq!(Cell::column_name(53), "BA"); + assert_eq!(Cell::column_name(54), "BB"); + assert_eq!(Cell::column_name(55), "BC"); + + assert_eq!(Cell::column_name(676), "YZ"); + assert_eq!(Cell::column_name(677), "ZA"); + assert_eq!(Cell::column_name(678), "ZB"); + + assert_eq!(Cell::column_name(702), "ZZ"); + assert_eq!(Cell::column_name(703), "AAA"); + assert_eq!(Cell::column_name(704), "AAB"); + + assert_eq!(Cell::column_name(18278), "ZZZ"); + assert_eq!(Cell::column_name(18279), "AAAA"); + assert_eq!(Cell::column_name(18280), "AAAB"); + } + + #[test] + fn test_column_number_retrieval() { + assert_eq!(Cell::column_number("A"), 1); + assert_eq!(Cell::column_number("B"), 2); + assert_eq!(Cell::column_number("C"), 3); + + assert_eq!(Cell::column_number("X"), 24); + assert_eq!(Cell::column_number("Y"), 25); + assert_eq!(Cell::column_number("Z"), 26); + + assert_eq!(Cell::column_number("AA"), 27); + assert_eq!(Cell::column_number("AB"), 28); + assert_eq!(Cell::column_number("AC"), 29); + + assert_eq!(Cell::column_number("AX"), 50); + assert_eq!(Cell::column_number("AY"), 51); + assert_eq!(Cell::column_number("AZ"), 52); + + assert_eq!(Cell::column_number("BA"), 53); + assert_eq!(Cell::column_number("BB"), 54); + assert_eq!(Cell::column_number("BC"), 55); + + assert_eq!(Cell::column_number("YZ"), 676); + assert_eq!(Cell::column_number("ZA"), 677); + assert_eq!(Cell::column_number("ZB"), 678); + + assert_eq!(Cell::column_number("ZZ"), 702); + assert_eq!(Cell::column_number("AAA"), 703); + assert_eq!(Cell::column_number("AAB"), 704); + + assert_eq!(Cell::column_number("ZZZ"), 18278); + assert_eq!(Cell::column_number("AAAA"), 18279); + assert_eq!(Cell::column_number("AAAB"), 18280); + } +} diff --git a/src/spreadsheets/expression.rs b/src/spreadsheets/expression.rs new file mode 100644 index 0000000..19d7aa3 --- /dev/null +++ b/src/spreadsheets/expression.rs @@ -0,0 +1,439 @@ +use crate::spreadsheets::cell::{get_column_name, Cell}; +use crate::spreadsheets::lexer::Lexer; +use crate::spreadsheets::parser::Parser; +use crate::spreadsheets::table::CellProvider; + +use anyhow::{anyhow, Result}; + +trait Functions { + fn sum(&self, cell: &Cell, args: &[Expression]) -> Result; + fn gte(&self, cell: &Cell, args: &[Expression]) -> Result; + fn lte(&self, cell: &Cell, args: &[Expression]) -> Result; + fn text(&self, cell: &Cell, args: &[Expression]) -> Result; + fn split(&self, cell: &Cell, args: &[Expression]) -> Result; + fn concat(&self, cell: &Cell, args: &[Expression]) -> Result; + fn incfrom(&self, cell: &Cell, args: &[Expression]) -> Result; + fn copy_last_result(&self, cell: &Cell, args: &[Expression]) -> Result; + fn copy_above_result(&self, cell: &Cell, args: &[Expression]) -> Result; + fn copy_and_increments_formula(&self, cell: &Cell, args: &[Expression]) -> Result; +} + +#[derive(Debug, Clone, PartialEq)] +pub struct CellReference { + pub hash: String, + pub column_name: String, + pub column: usize, + pub row: usize, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LabelReference { + pub label: String, + pub n_rows: usize, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ColumnReference { + pub name: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum Expression { + Number(f64), + String(String), + CellReference(CellReference), + LabelReference(LabelReference), + ColumnReference(ColumnReference), + Sum { args: Vec }, + Difference { args: Vec }, + Product { args: Vec }, + Quotient { args: Vec }, + Function { name: String, args: Vec }, + Collection { expressions: Vec }, +} + +// provides .to_string() for Expression +impl std::fmt::Display for Expression { + fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Expression::Number(number) => { + if number.fract() == 0.0 { + fmt.write_str(&number.to_string()) + } else { + fmt.write_str(&format!("{:.2}", number)) + } + } + Expression::String(string) => match string.parse::() { + Ok(number) => { + if number.fract() == 0.0 { + fmt.write_str(&number.to_string()) + } else { + fmt.write_str(&format!("{:.2}", number)) + } + } + Err(_) => fmt.write_str(string), + }, + _ => fmt.write_str("!ERROR!"), + } + } +} + +impl Functions for Expression { + fn split(&self, cell: &Cell, args: &[Expression]) -> Result { + if args.len() != 2 { + return Err(anyhow!("split must have exactly 2 arguments")); + } + + let string = args[0].evaluate(&cell)?; + let delimiter = args[1].evaluate(&cell)?; + + let string = string.to_string(); + let delimiter = delimiter.to_string(); + + let parts = string.split(&delimiter).collect::>(); + + let mut expressions = Vec::new(); + for part in parts { + let tokens = Lexer::tokenize(&part); + let expression = Parser::parse(&tokens)?; + + expressions.push(expression); + } + + Ok(Expression::Collection { expressions }) + } + + fn sum(&self, cell: &Cell, args: &[Expression]) -> Result { + let mut sum = 0.0; + + for arg in args { + let mut result; + + if arg.is_collection() { + result = self.sum(cell, &arg.expressions())?; + } else { + result = arg.evaluate(&cell)?; + } + + if result.is_collection() { + result = self.sum(cell, &result.expressions())?; + } + + sum += result.to_number()?; + } + + Ok(Expression::Number(sum)) + } + + fn gte(&self, cell: &Cell, args: &[Expression]) -> Result { + if args.len() != 2 { + return Err(anyhow!("gte must have exactly 2 arguments")); + } + + let left = args[0].evaluate(&cell)?; + let right = args[1].evaluate(&cell)?; + + let left = left.to_number()?; + let right = right.to_number()?; + + Ok(Expression::String((left >= right).to_string())) + } + + fn lte(&self, cell: &Cell, args: &[Expression]) -> Result { + if args.len() != 2 { + return Err(anyhow!("lte must have exactly 2 arguments")); + } + + let left = args[0].evaluate(&cell)?; + let right = args[1].evaluate(&cell)?; + + let left = left.to_number()?; + let right = right.to_number()?; + + Ok(Expression::String((left <= right).to_string())) + } + + // This function does nothing, because our output is always a string. + // However, it's here for the sake of completeness. It could be used + // as a formatter, for example to render the table as HTML. + fn text(&self, cell: &Cell, args: &[Expression]) -> Result { + if args.len() != 1 { + return Err(anyhow!("text must have exactly 1 argument")); + } + + let arg = args[0].evaluate(&cell)?; + + Ok(Expression::String(format!("{}", arg.to_string()))) + } + + fn concat(&self, cell: &Cell, args: &[Expression]) -> Result { + let mut result = String::new(); + + for arg in args { + let arg = arg.evaluate(&cell)?; + result.push_str(&arg.to_string()); + } + + Ok(Expression::String(result)) + } + + // this function does not increment, it is just a marker for copy_and_increments_formula + fn incfrom(&self, cell: &Cell, args: &[Expression]) -> Result { + if args.len() != 1 { + return Err(anyhow!("incfrom must have exactly 1 argument")); + } + + let arg = args[0].evaluate(&cell)?; + let value = arg.to_number()?; + + Ok(Expression::Number(value)) + } + + fn copy_above_result(&self, cell: &Cell, args: &[Expression]) -> Result { + if args.len() != 1 { + return Err(anyhow!("copy_above_result must have exactly 1 argument")); + } + + if cell.row == 1 { + return Err(anyhow!("copy_above_result cannot be used in the first row")); + } + + match &args[0] { + Expression::ColumnReference(column) => { + let hash = format!("{}{}", column.name, cell.row - 1); + let table = cell.table.borrow(); + let cell = table + .cell(&hash) + .expect(format!("cell not found: {}", hash).as_str()); + + Ok(Expression::String(cell.result())) + } + _ => { + return Err(anyhow!( + "copy_above_result must have a column reference as its first argument" + )) + } + } + } + + fn copy_last_result(&self, cell: &Cell, args: &[Expression]) -> Result { + if args.len() != 1 { + return Err(anyhow!("copy_last_result must have exactly 1 argument")); + } + + match &args[0] { + Expression::ColumnReference(column) => { + let mut row = cell.table.borrow().num_rows; + + while row > 0 { + let hash = format!("{}{}", column.name, row); + let table = cell.table.borrow(); + let cell = table + .cell(&hash) + .expect(format!("cell not found: {}", hash).as_str()); + + if !cell.value.is_empty() { + return Ok(Expression::String(cell.result())); + } + + row -= 1; + } + + Ok(Expression::String(String::new())) + } + _ => { + return Err(anyhow!( + "copy_last_result must have a column reference as its first argument" + )) + } + } + } + + // copies the formula from the cell above, increments row numbers and numbers marked with incfrom + fn copy_and_increments_formula(&self, cell: &Cell, args: &[Expression]) -> Result { + if args.len() != 0 { + return Err(anyhow!( + "copy_and_increments_formula must have no arguments" + )); + } + + if cell.row == 1 { + return Err(anyhow!( + "copy_and_increments_formula cannot be used in the first row" + )); + } + + let mut formula = String::new(); + let mut increment_amount = 1; + let mut row = cell.row - 1; + while row > 0 { + let hash = format!("{}{}", get_column_name(cell.column), row); + let table = cell.table.borrow(); + let cell_above = table + .cell(&hash) + .expect(format!("cell above not found: {}", hash).as_str()); + + if cell_above.formula().is_none() { + return Err(anyhow!( + "copy_and_increments_formula can only refer to cells with a formula" + )); + } + + if cell_above.value != cell.value { + formula = cell_above.value.clone(); + break; + } + + row -= 1; + increment_amount += 1; + } + + if formula == "" { + return Err(anyhow!( + "copy_and_increments_formula could not find a cell with a different formula" + )); + } + + let new_cell = Cell::new(&cell.table, cell.row, cell.column, &formula); + let tokens = Lexer::tokenize_and_increment(&new_cell.value, increment_amount); + let expression = Parser::parse(&tokens)?; + + expression.evaluate(&new_cell) + } +} + +impl Expression { + pub fn evaluate(&self, cell: &Cell) -> Result { + match self { + // Literals + Expression::Number(number) => Ok(Expression::String(number.to_string())), + Expression::String(string) => Ok(Expression::String(string.clone())), + + // References + Expression::CellReference(cell_ref) => { + let table = cell.table.borrow(); + let cell = table + .cell(&cell_ref.hash) + .expect(format!("cell not found: {}", cell_ref.hash).as_str()); + + Ok(Expression::String(cell.result())) + } + Expression::LabelReference(label_ref) => { + let table = cell.table.borrow(); + let cell = table + .cell(&label_ref.label) + .expect(format!("label not found: {}", label_ref.label).as_str()); + + let mut target_row = cell.row + label_ref.n_rows; + + if target_row > table.num_rows { + target_row = table.num_rows; + } + + let hash = format!("{}{}", get_column_name(cell.column), target_row); + let cell = table + .cell(&hash) + .expect(format!("target cell not found: {}", hash).as_str()); + + Ok(Expression::String(cell.result())) + } + + // Operators + Expression::Sum { args } => { + let mut result = 0.0; + + for arg in args { + let value = arg.evaluate(cell)?; + let value = value.to_number()?; + result += value; + } + + Ok(Expression::String(result.to_string())) + } + Expression::Difference { args } => { + let result = args[0].evaluate(cell)?; + let mut result = result.to_number()?; + + for arg in &args[1..] { + let value = arg.evaluate(cell)?; + let value = value.to_number()?; + result -= value; + } + + Ok(Expression::String(result.to_string())) + } + Expression::Product { args } => { + let mut result = 1.0; + + for arg in args { + let value = arg.evaluate(cell)?; + let value = value.to_number()?; + result *= value; + } + + Ok(Expression::String(result.to_string())) + } + Expression::Quotient { args } => { + let result = args[0].evaluate(cell)?; + let mut result = result.to_number()?; + + for arg in &args[1..] { + let value = arg.evaluate(cell)?; + let value = value.to_number()?; + + if value == 0.0 { + return Err(anyhow!( + "it's all fun and games until someone divides by zero" + )); + } + + result /= value; + } + + Ok(Expression::String(result.to_string())) + } + + // Functions + Expression::Function { name, args } => match name.as_str() { + "sum" => self.sum(cell, args), + "gte" => self.gte(cell, args), + "lte" => self.lte(cell, args), + "text" => self.text(cell, args), + "split" => self.split(cell, args), + "concat" => self.concat(cell, args), + "incfrom" => self.incfrom(cell, args), + "copy_last_result" => self.copy_last_result(cell, args), + "copy_above_result" => self.copy_above_result(cell, args), + "copy_and_increments_formula" => self.copy_and_increments_formula(cell, args), + _ => return Err(anyhow!("unknown function")), + }, + + // Collections + _ => Err(anyhow!("unexpected expression")), + } + } + + fn is_collection(&self) -> bool { + match self { + Expression::Collection { expressions: _ } => true, + _ => false, + } + } + + fn expressions(&self) -> Vec { + match self { + Expression::Collection { expressions } => expressions.clone(), + _ => vec![self.clone()], + } + } + + fn to_number(&self) -> Result { + match self { + Expression::Number(number) => Ok(*number), + Expression::String(string) => Ok(string.parse::().unwrap_or(0.0)), + _ => Err(anyhow!( + "cannot convert to number: expected number or numeric string" + )), + } + } +} diff --git a/src/spreadsheets/lexer.rs b/src/spreadsheets/lexer.rs new file mode 100644 index 0000000..ff72ff4 --- /dev/null +++ b/src/spreadsheets/lexer.rs @@ -0,0 +1,656 @@ +use std::{iter::Peekable, str::Chars}; + +use crate::spreadsheets::cell::get_column_number; +use crate::spreadsheets::expression::{CellReference, ColumnReference, LabelReference}; + +#[derive(Debug, Clone, PartialEq)] +pub enum Token { + Plus, + Minus, + Multiply, + Divide, + OpenParenthesis, + CloseParenthesis, + Comma, + Number(f64), + String(String), + Formula(String), + + // (A..Z)n references a cell by position + CellReference(CellReference), // ex: A1, B2, etc. + + // (A..Z)n:[A..Z]n references a range of cells + CellRange { + start: CellReference, + end: CellReference, + }, // ex: A1:B2 + + // @label references a cell that is n rows below a labelled cell + LabelReference(LabelReference), // ex: @label<1> + + // (A..Z)^ copies the evaluated result of the cell above in the same column + CopyAboveResult(ColumnReference), // ex: A^ (without v) + + // (A..Z)^v copies the evaluated result of the last non-empty cell in the column + CopyLastResult(ColumnReference), // ex: A^v or B^v (with v) + + // ^^ Copies the formula from the cell above in the same column + CopyAndIncrementsFormula, // ^^ +} + +pub struct Lexer { + pub increment: usize, +} + +impl Lexer { + pub fn tokenize(content: &str) -> Vec { + Self::_tokenize(content, 0) + } + + pub fn tokenize_and_increment(content: &str, amount: usize) -> Vec { + Self::_tokenize(content, amount) + } + + fn _tokenize(content: &str, increment: usize) -> Vec { + if !content.starts_with('=') { + return vec![Token::String(content.trim().to_string())]; + } + + let mut tokens = Vec::new(); + let mut chars = content.chars().peekable(); + while let Some(c) = chars.next() { + match c { + '+' => tokens.push(Token::Plus), + '-' => tokens.push(Token::Minus), + '*' => tokens.push(Token::Multiply), + '/' => tokens.push(Token::Divide), + '(' => tokens.push(Token::OpenParenthesis), + ')' => tokens.push(Token::CloseParenthesis), + ',' => tokens.push(Token::Comma), + '@' => Self::tokenize_label_reference(&mut chars, &mut tokens), + '^' => Self::tokenize_copy_and_increment(&mut chars, &mut tokens), + '"' | '\'' => Self::tokenize_string(&mut chars, &mut tokens, c), + 'A'..='Z' | 'a'..='z' => { + Self::tokenize_reference_or_formula(&mut chars, &mut tokens, c, increment) + } + '0'..='9' => Self::tokenize_number(&mut chars, &mut tokens, c), + '!' => panic!("Label identifier is not allowed in formulas"), + _ => (), + } + } + + tokens + } + + fn tokenize_copy_and_increment(chars: &mut Peekable, tokens: &mut Vec) { + if let Some(&c) = chars.peek() { + match c { + '^' => { + chars.next(); + tokens.push(Token::CopyAndIncrementsFormula); + } + _ => panic!("Invalid copy and increment simbol, expected ^^."), + } + } + } + + fn tokenize_string(chars: &mut Peekable, tokens: &mut Vec, quote: char) { + let mut text = String::new(); + + while let Some(c) = chars.next() { + if c == quote { + break; + } + text.push(c); + } + + tokens.push(Token::String(text)); + } + + fn tokenize_label_reference(chars: &mut Peekable, tokens: &mut Vec) { + let mut label = String::new(); + let mut n_rows = String::new(); + let mut is_label = true; + + while let Some(c) = chars.next() { + match c { + 'a'..='z' | 'A'..='Z' | '_' => { + label.push(c); + } + '0'..='9' => { + if is_label { + label.push(c); + } else { + n_rows.push(c); + } + } + '<' => is_label = false, + '>' => break, + _ => panic!("invalid label reference token"), + } + } + + if label == "" || n_rows == "" { + panic!("empty label reference or number of rows to shift"); + } + + tokens.push(Token::LabelReference(LabelReference { + label: label.to_lowercase(), + n_rows: n_rows + .parse() + .expect("cannot parse number of rows to shift"), + })); + } + + fn tokenize_reference_or_formula( + chars: &mut Peekable, + tokens: &mut Vec, + ch: char, + increment: usize, + ) { + let mut text = String::new(); + let mut column = String::new(); + let mut is_cell_reference = false; + + // We always start with (A..Z) + text.push(uppercase_char(ch)); + + while let Some(&c) = chars.peek() { + match c { + // (A..Z) after a text can be either formula or reference + 'A'..='Z' | 'a'..='z' => { + text.push(uppercase_char(c)); + chars.next(); + if is_cell_reference { + panic!("References must end with a number: {}", text); + } + } + + // The presence of a number after a text indicates a reference + '0'..='9' => { + is_cell_reference = true; + column = text.clone(); + text.push(c); + chars.next(); + } + + // The presence of '(' after a text indicates a formula + '(' => { + if is_cell_reference { + panic!("Formulas cannot contain a number: {}", text); + } + + match text.as_str() { + "INCFROM" => { + tokens.push(Token::Formula(text.to_lowercase())); + + if increment > 0 { + chars.next(); + tokens.push(Token::OpenParenthesis); + + let mut number = String::new(); + + while let Some(&c) = chars.peek() { + match c { + ')' => { + break; + } + '0'..='9' | '.' => { + number.push(c); + chars.next(); + } + _ => panic!("Invalid formula: {}", text), + } + } + + let number = number + .parse::() + .expect("cannot parse number in INCFROM lexical analysis"); + + tokens.push(Token::Number(number + increment as f64)); + } + } + "SUM" | "SPLIT" | "GTE" | "LTE" | "TEXT" | "CONCAT" => { + tokens.push(Token::Formula(text.to_lowercase())) + } + _ => panic!("Unknown formula: {}", text), + }; + + break; + } + + // The presence of the ^ symbol after a text indicates a column reference + '^' => { + if is_cell_reference { + panic!( + "The column copy symbol ^ expects a column without a row number: {}", + text + ); + } + + chars.next(); + if let Some(&c) = chars.peek() { + match c { + // The presence of v after ^ inverts the direction of the column reference + 'v' | 'V' => { + chars.next(); + tokens.push(Token::CopyLastResult(ColumnReference { name: text })); + } + + _ => { + tokens.push(Token::CopyAboveResult(ColumnReference { name: text })) + } + } + } else { + tokens.push(Token::CopyAboveResult(ColumnReference { name: text })); + } + + break; + } + + _ => { + if is_cell_reference { + let mut row = text[column.len()..] + .parse() + .expect("cannot parse row number in tokenize_reference_or_formula"); + + if increment > 0 { + row += 1; + text = format!("{}{}", column, row); + } + + let start_cell = CellReference { + hash: text, + column_name: column.clone(), + column: get_column_number(&column), + row, + }; + Self::tokenize_cell_or_range(chars, tokens, start_cell, increment); + break; + } + } + } + } + } + + fn tokenize_cell_or_range( + chars: &mut Peekable, + tokens: &mut Vec, + start_cell: CellReference, + increment: usize, + ) { + if let Some(&c) = chars.peek() { + match c { + ':' => { + chars.next(); + + if let Some(&c) = chars.peek() { + match c { + 'A'..='Z' | 'a'..='z' => { + tokens.push(Token::CellRange { + start: start_cell, + end: Self::tokenize_cell(chars, increment), + }); + } + _ => panic!("Invalid range"), + } + } else { + panic!("Invalid range"); + } + } + _ => { + tokens.push(Token::CellReference(start_cell)); + } + } + } else { + tokens.push(Token::CellReference(start_cell)); + } + } + + fn tokenize_cell(chars: &mut Peekable, increment: usize) -> CellReference { + let mut text = String::new(); + let mut column = String::new(); + let mut is_cell_reference = false; + + while let Some(&c) = chars.peek() { + match c { + 'A'..='Z' | 'a'..='z' => { + text.push(uppercase_char(c)); + chars.next(); + if is_cell_reference { + panic!("References must end with a number: {}", text); + } + } + + '0'..='9' => { + is_cell_reference = true; + column = text.clone(); + text.push(c); + chars.next(); + } + _ => break, + } + } + + if is_cell_reference { + let mut row = text[column.len()..] + .parse() + .expect("cannot parse row number in tokenize_cell"); + + if increment > 0 { + row += increment; + text = format!("{}{}", column, row); + } + + CellReference { + hash: text, + column_name: column.clone(), + column: get_column_number(&column), + row, + } + } else { + panic!("Invalid cell reference: {}", text); + } + } + + fn tokenize_number(chars: &mut Peekable, tokens: &mut Vec, c: char) { + let mut number = String::new(); + number.push(c); + + while let Some(&c) = chars.peek() { + match c { + '0'..='9' | '.' => { + number.push(c); + chars.next(); + } + _ => break, + } + } + + tokens.push(Token::Number( + number.parse::().expect("cannot parse number"), + )); + } +} + +fn uppercase_char(c: char) -> char { + c.to_uppercase() + .collect::>() + .first() + .expect("cannot uppercase char") + .to_owned() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_tokenize() { + let content = String::from("=sum(A1:A2) + 1"); + let tokens = Lexer::tokenize(&content); + + assert_eq!( + tokens, + vec![ + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::CellRange { + start: CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1 + }, + end: CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2 + } + }, + Token::CloseParenthesis, + Token::Plus, + Token::Number(1.0), + ] + ); + } + + #[test] + fn test_tokenize_copy_and_increment_formula() { + let content = String::from("=sum(A1,A2)-^^"); + let tokens = Lexer::tokenize(&content); + + assert_eq!( + tokens, + vec![ + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1 + }), + Token::Comma, + Token::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2 + }), + Token::CloseParenthesis, + Token::Minus, + Token::CopyAndIncrementsFormula, + ] + ); + } + + #[test] + fn test_tokenize_label_reference_with_number() { + let content = String::from("=sum(A1, A2)+@label_1<2>"); + let tokens = Lexer::tokenize(&content); + + assert_eq!( + tokens, + vec![ + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1 + }), + Token::Comma, + Token::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2 + }), + Token::CloseParenthesis, + Token::Plus, + Token::LabelReference(LabelReference { + label: String::from("label_1"), + n_rows: 2, + }), + ] + ); + } + + #[test] + fn test_tokenize_multiple_label_references() { + let content = String::from("=text(gte(@adjusted_cost<1>, @cost_threshold<1>))"); + let tokens = Lexer::tokenize(&content); + + assert_eq!( + tokens, + vec![ + Token::Formula(String::from("text")), + Token::OpenParenthesis, + Token::Formula(String::from("gte")), + Token::OpenParenthesis, + Token::LabelReference(LabelReference { + label: String::from("adjusted_cost"), + n_rows: 1, + }), + Token::Comma, + Token::LabelReference(LabelReference { + label: String::from("cost_threshold"), + n_rows: 1, + }), + Token::CloseParenthesis, + Token::CloseParenthesis, + ] + ); + } + + #[test] + fn test_tokenize_copy_last_result() { + let content = String::from("=sum( A1,AB2)+A^v"); + let tokens = Lexer::tokenize(&content); + + assert_eq!( + tokens, + vec![ + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1 + }), + Token::Comma, + Token::CellReference(CellReference { + hash: String::from("AB2"), + column_name: String::from("AB"), + column: 28, + row: 2 + }), + Token::CloseParenthesis, + Token::Plus, + Token::CopyLastResult(ColumnReference { + name: String::from("A") + }), + ] + ); + } + + #[test] + fn test_multiple_copy_last_result() { + let content = String::from("=E^v+(E^v*A9)"); + let tokens = Lexer::tokenize(&content); + + assert_eq!( + tokens, + vec![ + Token::CopyLastResult(ColumnReference { + name: String::from("E") + }), + Token::Plus, + Token::OpenParenthesis, + Token::CopyLastResult(ColumnReference { + name: String::from("E") + }), + Token::Multiply, + Token::CellReference(CellReference { + hash: String::from("A9"), + column_name: String::from("A"), + column: 1, + row: 9 + }), + Token::CloseParenthesis, + ] + ); + } + + #[test] + fn test_tokenize_copy_above_result() { + let content = String::from("=sum(A1,A2)/B^"); + let tokens = Lexer::tokenize(&content); + + assert_eq!( + tokens, + vec![ + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1 + }), + Token::Comma, + Token::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2 + }), + Token::CloseParenthesis, + Token::Divide, + Token::CopyAboveResult(ColumnReference { + name: String::from("B") + }), + ] + ); + } + + #[test] + fn test_tokenize_concat_formula_with_text() { + let content = String::from("=concat(\"t_\", text(incFrom(1)))"); + let tokens = Lexer::tokenize(&content); + + assert_eq!( + tokens, + vec![ + Token::Formula(String::from("concat")), + Token::OpenParenthesis, + Token::String(String::from("t_")), + Token::Comma, + Token::Formula(String::from("text")), + Token::OpenParenthesis, + Token::Formula(String::from("incfrom")), + Token::OpenParenthesis, + Token::Number(1.0), + Token::CloseParenthesis, + Token::CloseParenthesis, + Token::CloseParenthesis, + ] + ); + } + + #[test] + fn test_tokenize_copy_above_and_nested_formulas() { + let content = String::from("=E^+sum(split(D3, \",\"))"); + let tokens = Lexer::tokenize(&content); + + assert_eq!( + tokens, + vec![ + Token::CopyAboveResult(ColumnReference { + name: String::from("E") + }), + Token::Plus, + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::Formula(String::from("split")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("D3"), + column_name: String::from("D"), + column: 4, + row: 3 + }), + Token::Comma, + Token::String(String::from(",")), + Token::CloseParenthesis, + Token::CloseParenthesis, + ] + ); + } + + // @todo custom errors instead of panics, so we can test them +} diff --git a/src/spreadsheets/parser.rs b/src/spreadsheets/parser.rs new file mode 100644 index 0000000..5bf13cc --- /dev/null +++ b/src/spreadsheets/parser.rs @@ -0,0 +1,689 @@ +use crate::spreadsheets::cell::get_column_name; +use crate::spreadsheets::expression::{CellReference, Expression}; +use crate::spreadsheets::lexer::Token; + +use anyhow::anyhow; +use anyhow::Result; + +pub struct Parser { + tokens: Vec, + index: usize, +} + +impl Parser { + pub fn parse(input: &[Token]) -> Result { + let mut parser = Parser { + tokens: input.to_vec(), + index: 0, + }; + + let expression = parser.parse_expression()?; + + Ok(expression) + } + + fn parse_expression(&mut self) -> Result { + let mut expression = self.parse_term()?; + + while self.index < self.tokens.len() { + match self.tokens[self.index] { + Token::Plus => { + self.index += 1; + let right = self.parse_term()?; + expression = Expression::Sum { + args: vec![expression, right], + }; + } + Token::Minus => { + self.index += 1; + let right = self.parse_term()?; + expression = Expression::Difference { + args: vec![expression, right], + }; + } + _ => break, + } + } + + Ok(expression) + } + + fn parse_term(&mut self) -> Result { + let mut expression = self.parse_factor()?; + + while self.index < self.tokens.len() { + match self.tokens[self.index] { + Token::Multiply => { + self.index += 1; + let right = self.parse_factor()?; + expression = Expression::Product { + args: vec![expression, right], + }; + } + Token::Divide => { + self.index += 1; + let right = self.parse_factor()?; + expression = Expression::Quotient { + args: vec![expression, right], + }; + } + _ => break, + } + } + + Ok(expression) + } + + fn parse_factor(&mut self) -> Result { + let expression = match self.tokens[self.index].to_owned() { + Token::Number(value) => { + self.index += 1; + Expression::Number(value) + } + Token::String(value) => { + self.index += 1; + Expression::String(value) + } + Token::CellReference(cell) => { + self.index += 1; + Expression::CellReference(cell) + } + Token::CellRange { start, end } => { + self.index += 1; + + let mut cells = Vec::new(); + + let start_row = start.row; + let start_column = start.column; + let end_row = end.row; + let end_column = end.column; + + for row in start_row..=end_row { + for column in start_column..=end_column { + let hash = format!("{}{}", get_column_name(column), row); + + let cell = Expression::CellReference(CellReference { + hash, + column_name: get_column_name(column), + column, + row, + }); + + cells.push(cell); + } + } + + Expression::Collection { expressions: cells } + } + Token::LabelReference(label) => { + self.index += 1; + Expression::LabelReference(label) + } + Token::Formula(name) => { + self.index += 1; + self.parse_function(name)? + } + Token::CopyAboveResult(column) => { + self.index += 1; + let name = String::from("copy_above_result"); + let args = vec![Expression::ColumnReference(column)]; + Expression::Function { name, args } + } + Token::CopyLastResult(column) => { + self.index += 1; + let name = String::from("copy_last_result"); + let args = vec![Expression::ColumnReference(column)]; + Expression::Function { name, args } + } + Token::CopyAndIncrementsFormula => { + self.index += 1; + let name = String::from("copy_and_increments_formula"); + let args = vec![]; + Expression::Function { name, args } + } + Token::OpenParenthesis => { + self.index += 1; + let expression = self.parse_expression()?; + + match self.tokens[self.index] { + Token::CloseParenthesis => self.index += 1, + _ => { + return Err(anyhow!( + "Unexpected token in parse_factor. Expected ')'. Got: {:?}", + self.tokens[self.index] + )) + } + } + + expression + } + _ => return Err(anyhow!("Unexpected factor: {:?}", self.tokens[self.index])), + }; + + Ok(expression) + } + + fn parse_function(&mut self, name: String) -> Result { + match self.tokens[self.index] { + Token::OpenParenthesis => self.index += 1, + _ => return Err(anyhow!("Unexpected token in parse_function. Expected '('")), + } + + let mut args = vec![]; + + while self.index < self.tokens.len() { + match self.tokens[self.index] { + Token::Comma => { + self.index += 1; + } + Token::CloseParenthesis => { + self.index += 1; + break; + } + _ => { + let expression = self.parse_expression()?; + args.push(expression); + } + } + } + + Ok(Expression::Function { name, args }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::spreadsheets::expression::{CellReference, ColumnReference, LabelReference}; + + #[test] + fn test_sum_range() { + let input = vec![ + Token::Formula(String::from("sum")), // Expression::Function + Token::OpenParenthesis, + Token::CellRange { + start: CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }, + end: CellReference { + hash: String::from("B2"), + column_name: String::from("B"), + column: 2, + row: 2, + }, + }, + Token::CloseParenthesis, + Token::Plus, // Note: Expression::Sum refers to this token + Token::Number(1.0), + ]; + + let expression = Parser::parse(&input).unwrap(); + + assert_eq!( + expression, + Expression::Sum { + args: vec![ + Expression::Function { + name: String::from("sum"), + args: vec![Expression::Collection { + expressions: vec![ + Expression::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }), + Expression::CellReference(CellReference { + hash: String::from("B1"), + column_name: String::from("B"), + column: 2, + row: 1, + }), + Expression::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2, + }), + Expression::CellReference(CellReference { + hash: String::from("B2"), + column_name: String::from("B"), + column: 2, + row: 2, + }) + ], + }], + }, + Expression::Number(1.0), + ], + } + ); + } + + #[test] + fn test_subtract_values_with_copy_and_increment_formula() { + let input = vec![ + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }), + Token::Comma, + Token::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2, + }), + Token::CloseParenthesis, + Token::Minus, + Token::CopyAndIncrementsFormula, + ]; + + let expression = Parser::parse(&input).unwrap(); + + assert_eq!( + expression, + Expression::Difference { + args: vec![ + Expression::Function { + name: String::from("sum"), + args: vec![ + Expression::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }), + Expression::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2, + }), + ], + }, + Expression::Function { + name: String::from("copy_and_increments_formula"), + args: vec![], + }, + ], + } + ); + } + + #[test] + fn test_label_reference() { + let input = vec![ + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }), + Token::Comma, + Token::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2, + }), + Token::CloseParenthesis, + Token::Plus, + Token::LabelReference(LabelReference { + label: String::from("label"), + n_rows: 2, + }), + ]; + + let expression = Parser::parse(&input).unwrap(); + + assert_eq!( + expression, + Expression::Sum { + args: vec![ + Expression::Function { + name: String::from("sum"), + args: vec![ + Expression::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }), + Expression::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2, + }), + ], + }, + Expression::LabelReference(LabelReference { + label: String::from("label"), + n_rows: 2, + }), + ], + } + ); + } + + #[test] + fn test_multiple_label_references() { + let input = vec![ + Token::Formula(String::from("text")), + Token::OpenParenthesis, + Token::Formula(String::from("gte")), + Token::OpenParenthesis, + Token::LabelReference(LabelReference { + label: String::from("adjusted_cost"), + n_rows: 1, + }), + Token::Comma, + Token::LabelReference(LabelReference { + label: String::from("cost_threshold"), + n_rows: 1, + }), + Token::CloseParenthesis, + Token::CloseParenthesis, + ]; + + let expression = Parser::parse(&input).unwrap(); + + assert_eq!( + expression, + Expression::Function { + name: String::from("text"), + args: vec![Expression::Function { + name: String::from("gte"), + args: vec![ + Expression::LabelReference(LabelReference { + label: String::from("adjusted_cost"), + n_rows: 1, + }), + Expression::LabelReference(LabelReference { + label: String::from("cost_threshold"), + n_rows: 1, + }), + ], + }], + } + ); + } + + #[test] + fn test_copy_last_result() { + let input = vec![ + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }), + Token::Comma, + Token::CellReference(CellReference { + hash: String::from("AB2"), + column_name: String::from("AB"), + column: 28, + row: 2, + }), + Token::CloseParenthesis, + Token::Plus, + Token::CopyLastResult(ColumnReference { + name: String::from("A"), + }), + ]; + + let expression = Parser::parse(&input).unwrap(); + + assert_eq!( + expression, + Expression::Sum { + args: vec![ + Expression::Function { + name: String::from("sum"), + args: vec![ + Expression::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }), + Expression::CellReference(CellReference { + hash: String::from("AB2"), + column_name: String::from("AB"), + column: 28, + row: 2, + }), + ], + }, + Expression::Function { + name: String::from("copy_last_result"), + args: vec![Expression::ColumnReference(ColumnReference { + name: String::from("A"), + })] + }, + ], + } + ); + } + + #[test] + fn test_multiple_copy_last_result() { + let input = vec![ + Token::CopyLastResult(ColumnReference { + name: String::from("E"), + }), + Token::Plus, + Token::OpenParenthesis, + Token::CopyLastResult(ColumnReference { + name: String::from("E"), + }), + Token::Multiply, + Token::CellReference(CellReference { + hash: String::from("A9"), + column_name: String::from("A"), + column: 1, + row: 9, + }), + Token::CloseParenthesis, + ]; + + let expression = Parser::parse(&input).unwrap(); + + assert_eq!( + expression, + Expression::Sum { + args: vec![ + Expression::Function { + name: String::from("copy_last_result"), + args: vec![Expression::ColumnReference(ColumnReference { + name: String::from("E"), + })], + }, + Expression::Product { + args: vec![ + Expression::Function { + name: String::from("copy_last_result"), + args: vec![Expression::ColumnReference(ColumnReference { + name: String::from("E"), + })], + }, + Expression::CellReference(CellReference { + hash: String::from("A9"), + column_name: String::from("A"), + column: 1, + row: 9, + }), + ], + } + ], + } + ); + } + + #[test] + fn test_copy_above_result_result() { + let input = vec![ + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }), + Token::Comma, + Token::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2, + }), + Token::CloseParenthesis, + Token::Divide, + Token::CopyAboveResult(ColumnReference { + name: String::from("B"), + }), + ]; + + let expression = Parser::parse(&input).unwrap(); + + assert_eq!( + expression, + Expression::Quotient { + args: vec![ + Expression::Function { + name: String::from("sum"), + args: vec![ + Expression::CellReference(CellReference { + hash: String::from("A1"), + column_name: String::from("A"), + column: 1, + row: 1, + }), + Expression::CellReference(CellReference { + hash: String::from("A2"), + column_name: String::from("A"), + column: 1, + row: 2, + }), + ], + }, + Expression::Function { + name: String::from("copy_above_result"), + args: vec![Expression::ColumnReference(ColumnReference { + name: String::from("B"), + })], + }, + ], + } + ); + } + + #[test] + fn test_tokenize_concat_formula_with_text() { + let input = vec![ + Token::Formula(String::from("concat")), + Token::OpenParenthesis, + Token::String(String::from("t_")), + Token::Comma, + Token::Formula(String::from("text")), + Token::OpenParenthesis, + Token::Formula(String::from("incfrom")), + Token::OpenParenthesis, + Token::Number(1.0), + Token::CloseParenthesis, + Token::CloseParenthesis, + Token::CloseParenthesis, + ]; + + let expression = Parser::parse(&input).unwrap(); + + assert_eq!( + expression, + Expression::Function { + name: String::from("concat"), + args: vec![ + Expression::String(String::from("t_")), + Expression::Function { + name: String::from("text"), + args: vec![Expression::Function { + name: String::from("incfrom"), + args: vec![Expression::Number(1.0)], + }], + }, + ], + } + ); + } + + #[test] + fn test_tokenize_copy_above_result_and_nested_formulas() { + let input = vec![ + Token::CopyAboveResult(ColumnReference { + name: String::from("E"), + }), + Token::Plus, + Token::Formula(String::from("sum")), + Token::OpenParenthesis, + Token::Formula(String::from("split")), + Token::OpenParenthesis, + Token::CellReference(CellReference { + hash: String::from("D3"), + column_name: String::from("D"), + column: 4, + row: 3, + }), + Token::Comma, + Token::String(String::from(",")), + Token::CloseParenthesis, + Token::CloseParenthesis, + ]; + + let expression = Parser::parse(&input).unwrap(); + + assert_eq!( + expression, + Expression::Sum { + args: vec![ + Expression::Function { + name: String::from("copy_above_result"), + args: vec![Expression::ColumnReference(ColumnReference { + name: String::from("E"), + })], + }, + Expression::Function { + name: String::from("sum"), + args: vec![Expression::Function { + name: String::from("split"), + args: vec![ + Expression::CellReference(CellReference { + hash: String::from("D3"), + column_name: String::from("D"), + column: 4, + row: 3, + }), + Expression::String(String::from(",")), + ], + }], + }, + ], + } + ); + } + + // @todo custom errors instead of panics, so we can test them + // or use #[should_panic(expected = "assertion failed")] + // for testing panics +} diff --git a/src/spreadsheets/table.rs b/src/spreadsheets/table.rs new file mode 100644 index 0000000..f6b6077 --- /dev/null +++ b/src/spreadsheets/table.rs @@ -0,0 +1,314 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::fs::File; +use std::io::{BufRead, BufReader, Read, Write}; +use std::path::PathBuf; +use std::rc::Rc; + +use anyhow::{ensure, Context, Result}; + +use crate::spreadsheets::cell::Cell; + +const DELIMITER: char = '|'; + +pub trait CellProvider: std::fmt::Debug { + fn cell(&self, hash: &str) -> Option<&Cell>; +} + +struct CellResult { + result: String, + column: usize, +} + +#[derive(Debug, Clone)] +pub struct Table { + cells: Vec, + cells_map: HashMap, + pub num_columns: usize, + pub num_rows: usize, +} + +impl Default for Table { + fn default() -> Table { + Table { + cells: Vec::new(), + cells_map: HashMap::new(), + num_columns: 0, + num_rows: 0, + } + } +} + +impl CellProvider for Table { + fn cell(&self, hash: &str) -> Option<&Cell> { + self.cells_map.get(hash).map(|index| &self.cells[*index]) + } +} + +impl Table { + pub fn new() -> Rc> { + Rc::new(RefCell::new(Table { + ..Default::default() + })) + } + + pub fn from_file(path: &PathBuf) -> Result>> { + let table = Table::new(); + let reader = Self::get_file_reader(&path)?; + + Table::fill(&table, reader)?; + + Ok(table) + } + + #[cfg(test)] + pub fn from_string(content: &str) -> Result>> { + let table = Table::new(); + let reader = BufReader::new(content.as_bytes()); + + Table::fill(&table, reader)?; + + Ok(table) + } + + pub fn print(&self, writer: &mut impl Write) -> Result<()> { + writeln!(writer)?; + + let mut width_of = vec![0; self.num_columns + 1]; + + let results: Vec = self + .cells + .iter() + .map(|cell| { + let result = cell.result(); + let column = cell.column; + width_of[column] = width_of[column].max(result.len()); + + CellResult { result, column } + }) + .collect(); + + for result in results { + let CellResult { result, column } = result; + + if column == self.num_columns { + writeln!(writer, "{}", result)?; + } else { + let column_width = width_of[column]; + let spaces = " ".repeat(column_width - result.len()); + write!(writer, "{}{} {} ", result, spaces, DELIMITER)?; + } + } + + writeln!(writer)?; + Ok(()) + } + + fn get_file_reader(path: &PathBuf) -> Result> { + let file = File::open(path).context(format!("file not found: {}", path.display()))?; + + Ok(BufReader::new(file)) + } + + fn fill(rc: &Rc>, reader: BufReader) -> Result<()> { + let mut table = rc.try_borrow_mut()?; + let mut row = 1; + + for line in BufRead::lines(reader) { + let line = line?; + let row_cells_map = line.split(DELIMITER).collect::>(); + + if table.cells_map.is_empty() { + table.num_columns = row_cells_map.len(); + } + + Self::validate_column_count(row, table.num_columns, row_cells_map.len())?; + + let mut column = 1; + for content in row_cells_map { + let cell = Cell::new(rc, row, column, content.trim()); + table.add_cell(cell); + column += 1; + } + row += 1; + } + + table.num_rows = row - 1; + + Ok(()) + } + + fn add_cell(&mut self, cell: Cell) { + let index = self.cells.len(); + + self.cells_map.insert(cell.hash.clone(), index); + if let Some(label) = cell.label().clone() { + self.cells_map.insert(label, index); + } + + self.cells.push(cell); + } + + fn validate_column_count(line: usize, expected: usize, found: usize) -> Result<()> { + ensure!( + expected == found, + format!( + "invalid column count on line {}. Expected {} but found {}", + line, expected, found + ) + ); + + Ok(()) + } +} + +#[cfg(test)] + +mod tests { + use crate::spreadsheets::table::Table; + + #[test] + fn outputs_aligned_columns() { + let file_contents = "this | is | an | example \n\ + csv | file | with | the \n\ + correct | number | of | columns \n"; + + let table = Table::from_string(file_contents).unwrap(); + let table = table.borrow(); + + let mut result = Vec::new(); + table.print(&mut result).unwrap(); + + assert_eq!( + std::str::from_utf8(&result).unwrap(), + "\n\ + this | is | an | example\n\ + csv | file | with | the\n\ + correct | number | of | columns\n\ + \n" + ); + } + + #[test] + fn outputs_aligned_results() { + let file_contents = "=incfrom(999) | results | will | align \n\ + =^^ | 1 | =100+100 | \n\ + 1 | =incfrom(0) + 1.0 | 1 | \n"; + + let table = Table::from_string(file_contents).unwrap(); + let table = table.borrow(); + + let mut result = Vec::new(); + table.print(&mut result).unwrap(); + + assert_eq!( + std::str::from_utf8(&result).unwrap(), + "\n\ + 999 | results | will | align\n\ + 1000 | 1 | 200 | \n\ + 1 | 1 | 1 | \n\ + \n" + ); + } + + #[test] + fn fails_with_too_many_columns() { + let file_contents = "this | is | an | example \n\ + csv | file | with | too | many \n\ + columns \n"; + + let result = Table::from_string(file_contents); + + match result { + Ok(_) => panic!("Expected error"), + Err(err) => assert_eq!( + err.to_string(), + "invalid column count on line 2. Expected 4 but found 5" + ), + } + } + + #[test] + fn fails_with_not_enough_columns() { + let file_contents = "this | is | an | example \n\ + csv | file | with \n\ + not | enough | columns \n"; + + let result = Table::from_string(file_contents); + + match result { + Ok(_) => panic!("Expected error"), + Err(err) => assert_eq!( + err.to_string(), + "invalid column count on line 2. Expected 4 but found 3" + ), + } + } + + #[test] + fn the_full_challenge_true() { + let file_contents = "!date|!transaction_id|!tokens|!token_prices|!total_cost + 2022-02-20|=concat(\"t_\", text(incFrom(1)))|btc,eth,dai|38341.88,2643.77,1.0003|=sum(split(D2, \",\")) + 2022-02-21|=^^|bch,eth,dai|304.38,2621.15,1.0001|=E^+sum(split(D3, \",\")) + 2022-02-22|=^^|sol,eth,dai|85,2604.17,0.9997|=^^ + !fee|!cost_threshold||| + 0.09|10000||| + !adjusted_cost|||| + =E^v+(E^v*A6)|||| + !cost_too_high|||| + =text(gte(@adjusted_cost<1>, @cost_threshold<1>))||||"; + + let table = Table::from_string(file_contents).unwrap(); + let table = table.borrow(); + + let mut result = Vec::new(); + table.print(&mut result).unwrap(); + + assert_eq!( + std::str::from_utf8(&result).unwrap(), "\n\ + !date | !transaction_id | !tokens | !token_prices | !total_cost\n\ + 2022-02-20 | t_1 | btc,eth,dai | 38341.88,2643.77,1.0003 | 40986.65\n\ + 2022-02-21 | t_2 | bch,eth,dai | 304.38,2621.15,1.0001 | 43913.18\n\ + 2022-02-22 | t_3 | sol,eth,dai | 85,2604.17,0.9997 | 46603.35\n\ + !fee | !cost_threshold | | | \n\ + 0.09 | 10000 | | | \n\ + !adjusted_cost | | | | \n\ + 50797.65 | | | | \n\ + !cost_too_high | | | | \n\ + true | | | | \n\ + \n"); + } + + #[test] + fn the_full_challenge_false() { + let file_contents = "!date|!transaction_id|!tokens|!token_prices|!total_cost + 2022-02-20|=concat(\"t_\", text(incFrom(1)))|btc,eth,dai|38341.88,2643.77,1.0003|=sum(split(D2, \",\")) + 2022-02-21|=^^|bch,eth,dai|304.38,2621.15,1.0001|=E^+sum(split(D3, \",\")) + 2022-02-22|=^^|sol,eth,dai|85,2604.17,0.9997|=^^ + !fee|!cost_threshold||| + 0.09|51000||| + !adjusted_cost|||| + =E^v+(E^v*A6)|||| + !cost_too_high|||| + =text(gte(@adjusted_cost<1>, @cost_threshold<1>))||||"; + + let table = Table::from_string(file_contents).unwrap(); + let table = table.borrow(); + + let mut result = Vec::new(); + table.print(&mut result).unwrap(); + + assert_eq!(std::str::from_utf8(&result).unwrap(), "\n\ + !date | !transaction_id | !tokens | !token_prices | !total_cost\n\ + 2022-02-20 | t_1 | btc,eth,dai | 38341.88,2643.77,1.0003 | 40986.65\n\ + 2022-02-21 | t_2 | bch,eth,dai | 304.38,2621.15,1.0001 | 43913.18\n\ + 2022-02-22 | t_3 | sol,eth,dai | 85,2604.17,0.9997 | 46603.35\n\ + !fee | !cost_threshold | | | \n\ + 0.09 | 51000 | | | \n\ + !adjusted_cost | | | | \n\ + 50797.65 | | | | \n\ + !cost_too_high | | | | \n\ + false | | | | \n\ + \n"); + } +} diff --git a/tests/e2e.rs b/tests/e2e.rs new file mode 100644 index 0000000..eb32869 --- /dev/null +++ b/tests/e2e.rs @@ -0,0 +1,127 @@ +use assert_cmd::prelude::*; // Add methods on commands +use assert_fs::prelude::*; +use predicates::prelude::*; // Used for writing assertions +use std::process::Command; // Run programs + +#[test] +fn file_doesnt_exist() -> Result<(), Box> { + let mut cmd = Command::cargo_bin("cell")?; + + cmd.arg("test/file/doesnt/exist"); + cmd.assert().failure().stderr(predicate::str::contains( + "file not found: test/file/doesnt/exist", + )); + + Ok(()) +} + +#[test] +fn prints_file_contents_to_stdout() -> Result<(), Box> { + let file = assert_fs::NamedTempFile::new("sample.txt")?; + file.write_str( + "a | sample | table \n\ + with | two | lines", + )?; + + let mut cmd = Command::cargo_bin("cell")?; + cmd.arg(file.path()); + cmd.assert().success().stdout(predicate::str::contains( + "\n\ + a | sample | table\n\ + with | two | lines\n\n", + )); + + Ok(()) +} + +#[test] +fn lets_test_some_formulas() -> Result<(), Box> { + let file = assert_fs::NamedTempFile::new("sample.txt")?; + file.write_str( + "\ + a | =sum(1,2,incfrom(3)) | table \n\ + with | =^^ | formulas\ + ", + )?; + + let mut cmd = Command::cargo_bin("cell")?; + cmd.arg(file.path()); + cmd.assert().success().stdout(predicate::str::contains( + "a | 6 | table\n\ + with | 7 | formulas\n\n", + )); + + Ok(()) +} + +#[test] +fn the_full_challenge_e2e_true() -> Result<(), Box> { + let file = assert_fs::NamedTempFile::new("sample.txt")?; + file.write_str( + "!date|!transaction_id|!tokens|!token_prices|!total_cost + 2022-02-20|=concat(\"t_\", text(incFrom(1)))|btc,eth,dai|38341.88,2643.77,1.0003|=sum(split(D2, \",\")) + 2022-02-21|=^^|bch,eth,dai|304.38,2621.15,1.0001|=E^+sum(split(D3, \",\")) + 2022-02-22|=^^|sol,eth,dai|85,2604.17,0.9997|=^^ + !fee|!cost_threshold||| + 0.09|10000||| + !adjusted_cost|||| + =E^v+(E^v*A6)|||| + !cost_too_high|||| + =text(gte(@adjusted_cost<1>, @cost_threshold<1>))||||", + )?; + + let mut cmd = Command::cargo_bin("cell")?; + cmd.arg(file.path()); + cmd.assert().success().stdout(predicate::str::contains( + "\n\ + !date | !transaction_id | !tokens | !token_prices | !total_cost\n\ + 2022-02-20 | t_1 | btc,eth,dai | 38341.88,2643.77,1.0003 | 40986.65\n\ + 2022-02-21 | t_2 | bch,eth,dai | 304.38,2621.15,1.0001 | 43913.18\n\ + 2022-02-22 | t_3 | sol,eth,dai | 85,2604.17,0.9997 | 46603.35\n\ + !fee | !cost_threshold | | | \n\ + 0.09 | 10000 | | | \n\ + !adjusted_cost | | | | \n\ + 50797.65 | | | | \n\ + !cost_too_high | | | | \n\ + true | | | | \n\ + \n", + )); + + Ok(()) +} + +#[test] +fn the_full_challenge_e2e_false() -> Result<(), Box> { + let file = assert_fs::NamedTempFile::new("sample.txt")?; + file.write_str( + "!date|!transaction_id|!tokens|!token_prices|!total_cost + 2022-02-20|=concat(\"t_\", text(incFrom(1)))|btc,eth,dai|38341.88,2643.77,1.0003|=sum(split(D2, \",\")) + 2022-02-21|=^^|bch,eth,dai|304.38,2621.15,1.0001|=E^+sum(split(D3, \",\")) + 2022-02-22|=^^|sol,eth,dai|85,2604.17,0.9997|=^^ + !fee|!cost_threshold||| + 0.09|51000||| + !adjusted_cost|||| + =E^v+(E^v*A6)|||| + !cost_too_high|||| + =text(gte(@adjusted_cost<1>, @cost_threshold<1>))||||", + )?; + + let mut cmd = Command::cargo_bin("cell")?; + cmd.arg(file.path()); + cmd.assert().success().stdout(predicate::str::contains( + "\n\ + !date | !transaction_id | !tokens | !token_prices | !total_cost\n\ + 2022-02-20 | t_1 | btc,eth,dai | 38341.88,2643.77,1.0003 | 40986.65\n\ + 2022-02-21 | t_2 | bch,eth,dai | 304.38,2621.15,1.0001 | 43913.18\n\ + 2022-02-22 | t_3 | sol,eth,dai | 85,2604.17,0.9997 | 46603.35\n\ + !fee | !cost_threshold | | | \n\ + 0.09 | 51000 | | | \n\ + !adjusted_cost | | | | \n\ + 50797.65 | | | | \n\ + !cost_too_high | | | | \n\ + false | | | | \n\ + \n", + )); + + Ok(()) +} diff --git a/transactions-sample.csv b/transactions-sample.csv index dc5f24a..ac74b3a 100644 --- a/transactions-sample.csv +++ b/transactions-sample.csv @@ -1,7 +1,7 @@ A B C D E 01 !date |!transaction_id |!tokens |!token_prices |!total_cost -02 2022-02-20 |=concat("t_", text(incFrom(1))) |btc,eth,dai |38341.88,2643.77,1.0003|=sum(spread(split(D2, ","))) -03 2022-02-21 |=^^ |bch,eth,dai |304.38,2621.15,1.0001 |=E^+sum(spread(split(D3, ","))) +02 2022-02-20 |=concat("t_", text(incFrom(1))) |btc,eth,dai |38341.88,2643.77,1.0003|=sum(split(D2, ",")) +03 2022-02-21 |=^^ |bch,eth,dai |304.38,2621.15,1.0001 |=E^+sum(split(D3, ",")) 04 2022-02-22 |=^^ |sol,eth,dai |85,2604.17,0.9997 |=^^ 05 06 @@ -15,4 +15,4 @@ 14 =E^v+(E^v*A9) | | | | 15 16 !cost_too_high| | | | -17 =text(bte(@adjusted_cost<1>, @cost_threshold<1>) | | | +17 =text(gte(@adjusted_cost<1>, @cost_threshold<1>)) | | | diff --git a/transactions.csv b/transactions.csv index ea15819..03b9a04 100644 --- a/transactions.csv +++ b/transactions.csv @@ -1,10 +1,10 @@ !date|!transaction_id|!tokens|!token_prices|!total_cost -2022-02-20|=concat("t_", text(incFrom(1)))|btc,eth,dai|38341.88,2643.77,1.0003|=sum(spread(split(D2, ","))) -2022-02-21|=^^|bch,eth,dai|304.38,2621.15,1.0001|=E^+sum(spread(split(D3, ","))) +2022-02-20|=concat("t_", text(incFrom(1)))|btc,eth,dai|38341.88,2643.77,1.0003|=sum(split(D2, ",")) +2022-02-21|=^^|bch,eth,dai|304.38,2621.15,1.0001|=E^+sum(split(D3, ",")) 2022-02-22|=^^|sol,eth,dai|85,2604.17,0.9997|=^^ !fee|!cost_threshold||| 0.09|10000||| !adjusted_cost|||| =E^v+(E^v*A6)|||| !cost_too_high|||| -=text(bte(@adjusted_cost<1>, @cost_threshold<1>)|||| +=text(gte(@adjusted_cost<1>, @cost_threshold<1>))||||