diff --git a/Cargo.lock b/Cargo.lock index 85b36790..8ffdf5a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anes" version = "0.1.6" @@ -185,6 +200,42 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", +] + +[[package]] +name = "chrono-tz" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "ciborium" version = "0.2.1" @@ -300,6 +351,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -572,6 +629,29 @@ dependencies = [ "itoa", ] +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "idna" version = "0.5.0" @@ -779,6 +859,8 @@ name = "numbat" version = "1.9.0" dependencies = [ "approx", + "chrono", + "chrono-tz", "codespan-reporting", "criterion", "glob", @@ -807,6 +889,7 @@ version = "1.9.0" dependencies = [ "anyhow", "assert_cmd", + "chrono-tz", "clap", "colored", "dirs", @@ -845,12 +928,59 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + [[package]] name = "percent-encoding" version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "plotters" version = "0.3.5" @@ -955,6 +1085,21 @@ dependencies = [ "nibble_vec", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "rayon" version = "1.8.1" @@ -1238,6 +1383,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "smallvec" version = "1.13.1" @@ -1580,6 +1731,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/examples/datetime_tests.nbt b/examples/datetime_tests.nbt new file mode 100644 index 00000000..7bcb0879 --- /dev/null +++ b/examples/datetime_tests.nbt @@ -0,0 +1,15 @@ +let epoch = parse_datetime("1970-01-01T00:00:00Z") +assert_eq(to_unixtime(epoch), 0) + +assert_eq(to_unixtime(epoch + 1000 milliseconds + 2 seconds), 3) + +let x = parse_datetime("Wed, 20 Jul 2022 21:52:05 +0200") +assert_eq(to_unixtime(x), 1658346725) + +assert_eq(to_unixtime(from_unixtime(1658346725)), 1658346725) + +# 2020 was a leap year +let y = parse_datetime("2020-02-28T20:00:00Z") +assert(format_datetime("%Y/%m/%d", y + 12 hours) == "2020/02/29") +let z = parse_datetime("2021-02-28T20:00:00Z") +assert(format_datetime("%Y/%m/%d", z + 12 hours) == "2021/03/01") diff --git a/numbat-cli/Cargo.toml b/numbat-cli/Cargo.toml index 9e47a337..cd57e756 100644 --- a/numbat-cli/Cargo.toml +++ b/numbat-cli/Cargo.toml @@ -22,6 +22,7 @@ itertools = "0.12" toml = { version = "0.8.8", features = ["parse"] } serde = { version = "1.0.195", features = ["derive"] } terminal_size = "0.3.0" +chrono-tz = "0.8.5" [dependencies.clap] version = "4" diff --git a/numbat-cli/src/completer.rs b/numbat-cli/src/completer.rs index 764186d9..8ca833ea 100644 --- a/numbat-cli/src/completer.rs +++ b/numbat-cli/src/completer.rs @@ -9,6 +9,7 @@ use rustyline::{ pub struct NumbatCompleter { pub context: Arc>, pub modules: Vec, + pub all_timezones: Vec<&'static str>, } impl Completer for NumbatCompleter { @@ -73,6 +74,45 @@ impl Completer for NumbatCompleter { )); } + // does it look like we're tab-completing a timezone (via the conversion operator)? + let complete_tz = line + .find("->") + .or_else(|| line.find("→")) + .or_else(|| line.find("➞")) + .or_else(|| line.find(" to ")) + .and_then(|convert_pos| { + if let Some(quote_pos) = line.rfind('"') { + if quote_pos > convert_pos && pos > quote_pos { + return Some(quote_pos + 1); + } + } + None + }); + if let Some(pos_word) = complete_tz { + let word_part = &line[pos_word..]; + let matches = self + .all_timezones + .iter() + .filter(|tz| tz.starts_with(word_part)) + .collect::>(); + let append_closing_quote = matches.len() <= 1; + + return Ok(( + pos_word, + matches + .into_iter() + .map(|tz| Pair { + display: tz.to_string(), + replacement: if append_closing_quote { + format!("{tz}\"") + } else { + tz.to_string() + }, + }) + .collect(), + )); + } + let (pos_word, word_part) = extract_word(line, pos, None, |c| { // TODO: we could use is_identifier_char here potentially match c { diff --git a/numbat-cli/src/main.rs b/numbat-cli/src/main.rs index 059d8830..95c061c0 100644 --- a/numbat-cli/src/main.rs +++ b/numbat-cli/src/main.rs @@ -263,6 +263,11 @@ impl Cli { completer: NumbatCompleter { context: self.context.clone(), modules: self.context.lock().unwrap().list_modules().collect(), + all_timezones: { + let mut all_tz: Vec<_> = chrono_tz::TZ_VARIANTS.map(|v| v.name()).into(); + all_tz.push("local"); + all_tz + }, }, highlighter: NumbatHighlighter { context: self.context.clone(), diff --git a/numbat-wasm/Cargo.lock b/numbat-wasm/Cargo.lock index 76358f98..088b08d2 100644 --- a/numbat-wasm/Cargo.lock +++ b/numbat-wasm/Cargo.lock @@ -2,6 +2,30 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "arrayvec" version = "0.7.4" @@ -41,12 +65,57 @@ version = "3.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "libc", +] + [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets 0.52.0", +] + +[[package]] +name = "chrono-tz" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91d7b79e99bfaa0d47da0687c43aa3b7381938a62ad3a6498599039321f660b7" +dependencies = [ + "chrono", + "chrono-tz-build", + "phf", +] + +[[package]] +name = "chrono-tz-build" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "433e39f13c9a060046954e0592a8d0a4bcb1040125cbf91cb8ee58964cfb350f" +dependencies = [ + "parse-zoneinfo", + "phf", + "phf_codegen", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -67,6 +136,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "cpufeatures" version = "0.2.12" @@ -162,6 +237,29 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "iana-time-zone" +version = "0.1.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6a67363e2aa4443928ce15e57ebae94fd8949958fd1223c4cfc0cd473ad7539" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "itertools" version = "0.12.0" @@ -271,6 +369,8 @@ dependencies = [ name = "numbat" version = "1.9.0" dependencies = [ + "chrono", + "chrono-tz", "codespan-reporting", "heck", "itertools", @@ -321,6 +421,53 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "parse-zoneinfo" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41" +dependencies = [ + "regex", +] + +[[package]] +name = "phf" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_codegen" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_shared" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b" +dependencies = [ + "siphasher", +] + [[package]] name = "pretty_dtoa" version = "0.3.0" @@ -357,6 +504,21 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + [[package]] name = "redox_syscall" version = "0.4.1" @@ -377,6 +539,35 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "rust-embed" version = "8.2.0" @@ -453,6 +644,12 @@ dependencies = [ "dirs", ] +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + [[package]] name = "strsim" version = "0.11.0" @@ -683,13 +880,22 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets", + "windows-targets 0.48.5", ] [[package]] @@ -698,13 +904,28 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", ] [[package]] @@ -713,38 +934,80 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" diff --git a/numbat/Cargo.toml b/numbat/Cargo.toml index 6170bdc3..677b9bb7 100644 --- a/numbat/Cargo.toml +++ b/numbat/Cargo.toml @@ -29,6 +29,8 @@ libc = "0.2.152" rust-embed = { version = "8.2.0", features = ["interpolate-folder-path"] } num-format = "0.4.4" walkdir = "2" +chrono = "0.4.31" +chrono-tz = "0.8.5" [features] default = ["fetch-exchangerates"] diff --git a/numbat/modules/datetime/functions.nbt b/numbat/modules/datetime/functions.nbt new file mode 100644 index 00000000..61b26255 --- /dev/null +++ b/numbat/modules/datetime/functions.nbt @@ -0,0 +1,7 @@ +use units::si + +fn now() -> DateTime +fn parse_datetime(input: String) -> DateTime +fn format_datetime(format: String, input: DateTime) -> String +fn to_unixtime(input: DateTime) -> Scalar +fn from_unixtime(input: Scalar) -> DateTime diff --git a/numbat/modules/prelude.nbt b/numbat/modules/prelude.nbt index a6e83951..b2e83cd1 100644 --- a/numbat/modules/prelude.nbt +++ b/numbat/modules/prelude.nbt @@ -28,3 +28,5 @@ use units::placeholder use physics::constants use physics::temperature_conversion + +use datetime::functions diff --git a/numbat/src/ast.rs b/numbat/src/ast.rs index 5787b7a9..b45bad11 100644 --- a/numbat/src/ast.rs +++ b/numbat/src/ast.rs @@ -215,6 +215,7 @@ pub enum TypeAnnotation { DimensionExpression(DimensionExpression), Bool(Span), String(Span), + DateTime(Span), } impl TypeAnnotation { @@ -223,6 +224,7 @@ impl TypeAnnotation { TypeAnnotation::DimensionExpression(d) => d.full_span(), TypeAnnotation::Bool(span) => *span, TypeAnnotation::String(span) => *span, + TypeAnnotation::DateTime(span) => *span, } } } @@ -233,6 +235,7 @@ impl PrettyPrint for TypeAnnotation { TypeAnnotation::DimensionExpression(d) => d.pretty_print(), TypeAnnotation::Bool(_) => m::type_identifier("Bool"), TypeAnnotation::String(_) => m::type_identifier("String"), + TypeAnnotation::DateTime(_) => m::type_identifier("DateTime"), } } } @@ -364,6 +367,7 @@ impl ReplaceSpans for TypeAnnotation { } TypeAnnotation::Bool(_) => TypeAnnotation::Bool(Span::dummy()), TypeAnnotation::String(_) => TypeAnnotation::String(Span::dummy()), + TypeAnnotation::DateTime(_) => TypeAnnotation::DateTime(Span::dummy()), } } } diff --git a/numbat/src/bytecode_interpreter.rs b/numbat/src/bytecode_interpreter.rs index c230293f..a5d51271 100644 --- a/numbat/src/bytecode_interpreter.rs +++ b/numbat/src/bytecode_interpreter.rs @@ -46,7 +46,7 @@ impl BytecodeInterpreter { self.vm.add_op1(Op::LoadConstant, index); } Expression::Identifier(_span, identifier, _type) => { - // Searching in reverse order ensures that we find the innermost identifer of that name first (shadowing) + // Searching in reverse order ensures that we find the innermost identifier of that name first (shadowing) let current_depth = self.locals.len() - 1; @@ -113,6 +113,32 @@ impl BytecodeInterpreter { }; self.vm.add_op(op); } + Expression::BinaryOperatorForDate(_span, operator, lhs, rhs, type_) => { + self.compile_expression(lhs)?; + self.compile_expression(rhs)?; + + // if the result is a duration: + let op = if type_.is_dtype() { + // the VM will need to return a value with the units of Seconds. so look up that unit here, and push it + // onto the stack, so the VM can easily reference it. + // TODO: We do not want to hard-code 'second' here. Instead, we might + // introduce a decorator to register the 'second' unit in the prelude for + // this specific purpose. We also need to handle errors in case no such unit + // was registered. + let second_idx = self.unit_name_to_constant_index.get("second"); + self.vm.add_op1(Op::LoadConstant, *second_idx.unwrap()); + Op::DiffDateTime + } else { + match operator { + BinaryOperator::Add => Op::AddDateTime, + BinaryOperator::Sub => Op::SubDateTime, + BinaryOperator::ConvertTo => Op::ConvertDateTime, + _ => unreachable!("{operator:?} is not valid with a DateTime"), // should be unreachable, because the typechecker will error first + } + }; + + self.vm.add_op(op); + } Expression::FunctionCall(_span, _full_span, name, args, _type) => { // Put all arguments on top of the stack for arg in args { @@ -186,7 +212,7 @@ impl BytecodeInterpreter { | Expression::Boolean(..) | Expression::String(..) | Expression::Condition(..) => {} - Expression::BinaryOperator(..) => { + Expression::BinaryOperator(..) | Expression::BinaryOperatorForDate(..) => { self.vm.add_op(Op::FullSimplify); } } diff --git a/numbat/src/diagnostic.rs b/numbat/src/diagnostic.rs index 65be37c5..fce1c776 100644 --- a/numbat/src/diagnostic.rs +++ b/numbat/src/diagnostic.rs @@ -3,6 +3,7 @@ use codespan_reporting::diagnostic::LabelStyle; use crate::{ interpreter::RuntimeError, parser::ParseError, + pretty_print::PrettyPrint, resolver::ResolverError, typechecker::{IncompatibleDimensionsError, TypeCheckError}, NameResolutionError, @@ -351,6 +352,33 @@ impl ErrorDiagnostic for TypeCheckError { | TypeCheckError::ExpectedBool(span) => d.with_labels(vec![span .diagnostic_label(LabelStyle::Primary) .with_message(inner_error)]), + TypeCheckError::MissingDimension(span, dim) => d + .with_labels(vec![span + .diagnostic_label(LabelStyle::Primary) + .with_message(format!("Missing dimension '{dim}'"))]) + .with_notes(vec![format!( + "This operation requires the '{dim}' dimension to be defined" + )]), + TypeCheckError::IncompatibleTypesInOperator( + span, + op, + lhs_type, + lhs_span, + rhs_type, + rhs_span, + ) => d.with_labels(vec![ + span.diagnostic_label(LabelStyle::Primary) + .with_message(format!( + "Operator {} can not be applied to these types", + op.pretty_print() + )), + lhs_span + .diagnostic_label(LabelStyle::Secondary) + .with_message(lhs_type.to_string()), + rhs_span + .diagnostic_label(LabelStyle::Secondary) + .with_message(rhs_type.to_string()), + ]), }; vec![d] } diff --git a/numbat/src/ffi.rs b/numbat/src/ffi.rs index fe685ab0..19b0ed6a 100644 --- a/numbat/src/ffi.rs +++ b/numbat/src/ffi.rs @@ -2,6 +2,8 @@ use std::collections::HashMap; use std::sync::OnceLock; +use chrono::Offset; + use crate::currency::ExchangeRatesCache; use crate::interpreter::RuntimeError; use crate::pretty_print::PrettyPrint; @@ -331,6 +333,49 @@ pub(crate) fn functions() -> &'static HashMap { callable: Callable::Function(Box::new(chr)), }, ); + m.insert( + "now".to_string(), + ForeignFunction { + name: "now".into(), + arity: 0..=0, + callable: Callable::Function(Box::new(now)), + }, + ); + m.insert( + "parse_datetime".to_string(), + ForeignFunction { + name: "parse_datetime".into(), + arity: 1..=1, + callable: Callable::Function(Box::new(parse_datetime)), + }, + ); + + m.insert( + "format_datetime".to_string(), + ForeignFunction { + name: "format_datetime".into(), + arity: 2..=2, + callable: Callable::Function(Box::new(format_datetime)), + }, + ); + + m.insert( + "to_unixtime".to_string(), + ForeignFunction { + name: "to_unixtime".into(), + arity: 1..=1, + callable: Callable::Function(Box::new(to_unixtime)), + }, + ); + + m.insert( + "from_unixtime".to_string(), + ForeignFunction { + name: "from_unixtime".into(), + arity: 1..=1, + callable: Callable::Function(Box::new(from_unixtime)), + }, + ); m }) @@ -740,3 +785,59 @@ fn chr(args: &[Value]) -> Result { Ok(Value::String(output.to_string())) } + +fn now(args: &[Value]) -> Result { + assert!(args.is_empty()); + let now = chrono::Utc::now(); + + let offset = now.with_timezone(&chrono::Local).offset().fix(); + + Ok(Value::DateTime(now, offset)) +} + +fn parse_datetime(args: &[Value]) -> Result { + assert!(args.len() == 1); + + let input = args[0].unsafe_as_string(); + + // Try to parse as rfc3339 and if that fails then as rfc2822 + let output = chrono::DateTime::parse_from_rfc3339(input) + .or_else(|_| chrono::DateTime::parse_from_rfc2822(input)) + .map_err(RuntimeError::DateParsingError)?; + + let offset = output.offset(); + + Ok(Value::DateTime(output.into(), *offset)) +} + +fn format_datetime(args: &[Value]) -> Result { + assert!(args.len() == 2); + + let format = args[0].unsafe_as_string(); + let dt = args[1].unsafe_as_datetime(); + + let output = dt.format(format).to_string(); + + Ok(Value::String(output)) +} + +fn to_unixtime(args: &[Value]) -> Result { + assert!(args.len() == 1); + + let input = args[0].unsafe_as_datetime(); + + let output = input.timestamp(); + + Ok(Value::Quantity(Quantity::from_scalar(output as f64))) +} + +fn from_unixtime(args: &[Value]) -> Result { + assert!(args.len() == 1); + + let timestamp = args[0].unsafe_as_quantity().unsafe_value().to_f64() as i64; + + let dt = chrono::DateTime::from_timestamp(timestamp, 0).unwrap(); + let offset = dt.offset().fix(); + + Ok(Value::DateTime(dt, offset)) +} diff --git a/numbat/src/interpreter.rs b/numbat/src/interpreter.rs index 5481ecc2..b06d786c 100644 --- a/numbat/src/interpreter.rs +++ b/numbat/src/interpreter.rs @@ -37,6 +37,10 @@ pub enum RuntimeError { CouldNotLoadExchangeRates, #[error("User error: {0}")] UserError(String), + #[error("Could not parse date: {0}")] + DateParsingError(chrono::ParseError), + #[error("Unknown timezone: {0}")] + UnknownTimezone(String), } #[derive(Debug, PartialEq, Eq)] diff --git a/numbat/src/keywords.rs b/numbat/src/keywords.rs index 4c95c24b..016e4149 100644 --- a/numbat/src/keywords.rs +++ b/numbat/src/keywords.rs @@ -21,6 +21,7 @@ pub const KEYWORDS: &[&str] = &[ "true", "false", "String", + "DateTime", // decorators "metric_prefixes", "binary_prefixes", diff --git a/numbat/src/parser.rs b/numbat/src/parser.rs index d565f334..af983c4f 100644 --- a/numbat/src/parser.rs +++ b/numbat/src/parser.rs @@ -1230,6 +1230,8 @@ impl<'a> Parser<'a> { Ok(TypeAnnotation::Bool(token.span)) } else if let Some(token) = self.match_exact(TokenKind::String) { Ok(TypeAnnotation::String(token.span)) + } else if let Some(token) = self.match_exact(TokenKind::DateTime) { + Ok(TypeAnnotation::DateTime(token.span)) } else { Ok(TypeAnnotation::DimensionExpression( self.dimension_expression()?, diff --git a/numbat/src/quantity.rs b/numbat/src/quantity.rs index 8b798393..d4bbf7db 100644 --- a/numbat/src/quantity.rs +++ b/numbat/src/quantity.rs @@ -53,7 +53,7 @@ impl Quantity { self.value.to_f64() == 0.0 } - fn to_base_unit_representation(&self) -> Quantity { + pub fn to_base_unit_representation(&self) -> Quantity { let (unit, factor) = self.unit.to_base_unit_representation(); Quantity::new(self.value * factor, unit) } diff --git a/numbat/src/tokenizer.rs b/numbat/src/tokenizer.rs index 9b823f70..d3700707 100644 --- a/numbat/src/tokenizer.rs +++ b/numbat/src/tokenizer.rs @@ -95,6 +95,7 @@ pub enum TokenKind { Else, String, + DateTime, Long, Short, @@ -353,6 +354,7 @@ impl Tokenizer { m.insert("then", TokenKind::Then); m.insert("else", TokenKind::Else); m.insert("String", TokenKind::String); + m.insert("DateTime", TokenKind::DateTime); // Keep this list in sync with keywords::KEYWORDS! m }); diff --git a/numbat/src/typechecker.rs b/numbat/src/typechecker.rs index feb9f3ac..7cfe70f8 100644 --- a/numbat/src/typechecker.rs +++ b/numbat/src/typechecker.rs @@ -19,7 +19,7 @@ use crate::{dimension::DimensionRegistry, typed_ast::DType}; use crate::{ffi::ArityRange, typed_ast::Expression}; use crate::{name_resolution::LAST_RESULT_IDENTIFIERS, pretty_print::PrettyPrint}; -use ast::DimensionExpression; +use ast::{BinaryOperator, DimensionExpression}; use itertools::Itertools; use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, FromPrimitive, Zero}; use thiserror::Error; @@ -280,11 +280,17 @@ pub enum TypeCheckError { #[error("Incompatible types in comparison operator")] IncompatibleTypesInComparison(Span, Type, Span, Type, Span), + #[error("Incompatible types in operator")] + IncompatibleTypesInOperator(Span, BinaryOperator, Type, Span, Type, Span), + #[error("Incompatible types in function call")] IncompatibleTypesInFunctionCall(Span, Type, Span, Type), #[error("This name is already used by {0}")] NameAlreadyUsedBy(&'static str, Span, Option), + + #[error("Missing a definition for dimension {1}")] + MissingDimension(Span, String), } type Result = std::result::Result; @@ -395,6 +401,10 @@ fn evaluate_const_expr(expr: &typed_ast::Expression) -> Result { e @ typed_ast::Expression::Condition(..) => Err( TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "Conditional"), ), + e @ Expression::BinaryOperatorForDate(..) => Err( + // TODO i think maybe this could be const evaluated? + TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "BinaryOperatorForDate"), + ), } } @@ -487,134 +497,201 @@ impl TypeChecker { let lhs_checked = self.check_expression(lhs)?; let rhs_checked = self.check_expression(rhs)?; - let get_type_and_assert_equality = || { - let lhs_type = dtype(&lhs_checked)?; - let rhs_type = dtype(&rhs_checked)?; - if lhs_type != rhs_type { - let full_span = ast::Expression::BinaryOperator { - op: *op, - lhs: lhs.clone(), - rhs: rhs.clone(), - span_op: *span_op, - } - .full_span(); - Err(TypeCheckError::IncompatibleDimensions( - IncompatibleDimensionsError { - span_operation: span_op.unwrap_or(full_span), - operation: match op { - typed_ast::BinaryOperator::Add => "addition".into(), - typed_ast::BinaryOperator::Sub => "subtraction".into(), - typed_ast::BinaryOperator::Mul => "multiplication".into(), - typed_ast::BinaryOperator::Div => "division".into(), - typed_ast::BinaryOperator::Power => "exponentiation".into(), - typed_ast::BinaryOperator::ConvertTo => { - "unit conversion".into() - } - typed_ast::BinaryOperator::LessThan - | typed_ast::BinaryOperator::GreaterThan - | typed_ast::BinaryOperator::LessOrEqual - | typed_ast::BinaryOperator::GreaterOrEqual - | typed_ast::BinaryOperator::Equal - | typed_ast::BinaryOperator::NotEqual => "comparison".into(), - typed_ast::BinaryOperator::LogicalAnd => "and".into(), - typed_ast::BinaryOperator::LogicalOr => "or".into(), - }, - span_expected: lhs.full_span(), - expected_name: " left hand side", - expected_dimensions: self - .registry - .get_derived_entry_names_for(&lhs_type), - expected_type: lhs_type, - span_actual: rhs.full_span(), - actual_name: "right hand side", - actual_name_for_fix: "expression on the right hand side", - actual_dimensions: self - .registry - .get_derived_entry_names_for(&rhs_type), - actual_type: rhs_type, - }, - )) + // DateTime types need special handling here, since they're not scalars with dimensions, yet some select binary operators can be applied to them + // TODO how to better handle all the operations we want to support with date + if lhs_checked.get_type() == Type::DateTime { + let rhs_is_time = dtype(&rhs_checked) + .ok() + .map(|t| t.is_time_dimension()) + .unwrap_or(false); + let rhs_is_datetime = rhs_checked.get_type() == Type::DateTime; + + if *op == BinaryOperator::ConvertTo && rhs_checked.get_type() == Type::String { + // Supports timezone conversion + typed_ast::Expression::BinaryOperatorForDate( + *span_op, + *op, + Box::new(lhs_checked), + Box::new(rhs_checked), + Type::DateTime, + ) + } else if *op == BinaryOperator::Sub && rhs_is_datetime { + let time = self + .registry + .get_base_representation_for_name("Time") + .map_err(|_| { + TypeCheckError::MissingDimension(ast.full_span(), "Time".into()) + })?; + + // TODO make sure the "second" unit exists + + typed_ast::Expression::BinaryOperatorForDate( + *span_op, + *op, + Box::new(lhs_checked), + Box::new(rhs_checked), + Type::Dimension(time), + ) + } else if (*op == BinaryOperator::Add || *op == BinaryOperator::Sub) + && rhs_is_time + { + typed_ast::Expression::BinaryOperatorForDate( + *span_op, + *op, + Box::new(lhs_checked), + Box::new(rhs_checked), + Type::DateTime, + ) } else { - Ok(Type::Dimension(lhs_type)) + return Err(TypeCheckError::IncompatibleTypesInOperator( + span_op.unwrap_or_else(|| { + ast::Expression::BinaryOperator { + op: *op, + lhs: lhs.clone(), + rhs: rhs.clone(), + span_op: *span_op, + } + .full_span() + }), + *op, + lhs_checked.get_type(), + lhs.full_span(), + rhs_checked.get_type(), + rhs.full_span(), + )); } - }; + } else { + let get_type_and_assert_equality = || { + let lhs_type = dtype(&lhs_checked)?; + let rhs_type = dtype(&rhs_checked)?; + if lhs_type != rhs_type { + let full_span = ast::Expression::BinaryOperator { + op: *op, + lhs: lhs.clone(), + rhs: rhs.clone(), + span_op: *span_op, + } + .full_span(); + Err(TypeCheckError::IncompatibleDimensions( + IncompatibleDimensionsError { + span_operation: span_op.unwrap_or(full_span), + operation: match op { + typed_ast::BinaryOperator::Add => "addition".into(), + typed_ast::BinaryOperator::Sub => "subtraction".into(), + typed_ast::BinaryOperator::Mul => "multiplication".into(), + typed_ast::BinaryOperator::Div => "division".into(), + typed_ast::BinaryOperator::Power => "exponentiation".into(), + typed_ast::BinaryOperator::ConvertTo => { + "unit conversion".into() + } + typed_ast::BinaryOperator::LessThan + | typed_ast::BinaryOperator::GreaterThan + | typed_ast::BinaryOperator::LessOrEqual + | typed_ast::BinaryOperator::GreaterOrEqual + | typed_ast::BinaryOperator::Equal + | typed_ast::BinaryOperator::NotEqual => { + "comparison".into() + } + typed_ast::BinaryOperator::LogicalAnd => "and".into(), + typed_ast::BinaryOperator::LogicalOr => "or".into(), + }, + span_expected: lhs.full_span(), + expected_name: " left hand side", + expected_dimensions: self + .registry + .get_derived_entry_names_for(&lhs_type), + expected_type: lhs_type, + span_actual: rhs.full_span(), + actual_name: "right hand side", + actual_name_for_fix: "expression on the right hand side", + actual_dimensions: self + .registry + .get_derived_entry_names_for(&rhs_type), + actual_type: rhs_type, + }, + )) + } else { + Ok(Type::Dimension(lhs_type)) + } + }; - let type_ = match op { - typed_ast::BinaryOperator::Add => get_type_and_assert_equality()?, - typed_ast::BinaryOperator::Sub => get_type_and_assert_equality()?, - typed_ast::BinaryOperator::Mul => { - Type::Dimension(dtype(&lhs_checked)? * dtype(&rhs_checked)?) - } - typed_ast::BinaryOperator::Div => { - Type::Dimension(dtype(&lhs_checked)? / dtype(&rhs_checked)?) - } - typed_ast::BinaryOperator::Power => { - let exponent_type = dtype(&rhs_checked)?; - if !exponent_type.is_scalar() { - return Err(TypeCheckError::NonScalarExponent( - rhs.full_span(), - exponent_type, - )); + let type_ = match op { + typed_ast::BinaryOperator::Add => get_type_and_assert_equality()?, + typed_ast::BinaryOperator::Sub => get_type_and_assert_equality()?, + typed_ast::BinaryOperator::Mul => { + Type::Dimension(dtype(&lhs_checked)? * dtype(&rhs_checked)?) + } + typed_ast::BinaryOperator::Div => { + Type::Dimension(dtype(&lhs_checked)? / dtype(&rhs_checked)?) } + typed_ast::BinaryOperator::Power => { + let exponent_type = dtype(&rhs_checked)?; + if !exponent_type.is_scalar() { + return Err(TypeCheckError::NonScalarExponent( + rhs.full_span(), + exponent_type, + )); + } - let base_type = dtype(&lhs_checked)?; - if base_type.is_scalar() { - // Skip evaluating the exponent if the lhs is a scalar. This allows - // for arbitrary (decimal) exponents, if the base is a scalar. + let base_type = dtype(&lhs_checked)?; + if base_type.is_scalar() { + // Skip evaluating the exponent if the lhs is a scalar. This allows + // for arbitrary (decimal) exponents, if the base is a scalar. - Type::Dimension(base_type) - } else { - let exponent = evaluate_const_expr(&rhs_checked)?; - Type::Dimension(base_type.power(exponent)) + Type::Dimension(base_type) + } else { + let exponent = evaluate_const_expr(&rhs_checked)?; + Type::Dimension(base_type.power(exponent)) + } } - } - typed_ast::BinaryOperator::ConvertTo => get_type_and_assert_equality()?, - typed_ast::BinaryOperator::LessThan - | typed_ast::BinaryOperator::GreaterThan - | typed_ast::BinaryOperator::LessOrEqual - | typed_ast::BinaryOperator::GreaterOrEqual => { - let _ = get_type_and_assert_equality()?; - Type::Boolean - } - typed_ast::BinaryOperator::Equal | typed_ast::BinaryOperator::NotEqual => { - let lhs_type = lhs_checked.get_type(); - let rhs_type = rhs_checked.get_type(); - if lhs_type.is_dtype() || rhs_type.is_dtype() { + typed_ast::BinaryOperator::ConvertTo => get_type_and_assert_equality()?, + typed_ast::BinaryOperator::LessThan + | typed_ast::BinaryOperator::GreaterThan + | typed_ast::BinaryOperator::LessOrEqual + | typed_ast::BinaryOperator::GreaterOrEqual => { let _ = get_type_and_assert_equality()?; - } else if lhs_type != rhs_type { - return Err(TypeCheckError::IncompatibleTypesInComparison( - span_op.unwrap(), - lhs_type, - lhs.full_span(), - rhs_type, - rhs.full_span(), - )); + Type::Boolean } + typed_ast::BinaryOperator::Equal | typed_ast::BinaryOperator::NotEqual => { + let lhs_type = lhs_checked.get_type(); + let rhs_type = rhs_checked.get_type(); + if lhs_type.is_dtype() || rhs_type.is_dtype() { + let _ = get_type_and_assert_equality()?; + } else if lhs_type != rhs_type { + return Err(TypeCheckError::IncompatibleTypesInComparison( + span_op.unwrap(), + lhs_type, + lhs.full_span(), + rhs_type, + rhs.full_span(), + )); + } - Type::Boolean - } - typed_ast::BinaryOperator::LogicalAnd - | typed_ast::BinaryOperator::LogicalOr => { - let lhs_type = lhs_checked.get_type(); - let rhs_type = rhs_checked.get_type(); - - if lhs_type != Type::Boolean { - return Err(TypeCheckError::ExpectedBool(lhs.full_span())); - } else if rhs_type != Type::Boolean { - return Err(TypeCheckError::ExpectedBool(rhs.full_span())); + Type::Boolean } + typed_ast::BinaryOperator::LogicalAnd + | typed_ast::BinaryOperator::LogicalOr => { + let lhs_type = lhs_checked.get_type(); + let rhs_type = rhs_checked.get_type(); + + if lhs_type != Type::Boolean { + return Err(TypeCheckError::ExpectedBool(lhs.full_span())); + } else if rhs_type != Type::Boolean { + return Err(TypeCheckError::ExpectedBool(rhs.full_span())); + } - Type::Boolean - } - }; + Type::Boolean + } + }; - typed_ast::Expression::BinaryOperator( - *span_op, - *op, - Box::new(lhs_checked), - Box::new(rhs_checked), - type_, - ) + typed_ast::Expression::BinaryOperator( + *span_op, + *op, + Box::new(lhs_checked), + Box::new(rhs_checked), + type_, + ) + } } ast::Expression::FunctionCall(span, full_span, function_name, args) => { let FunctionSignature { @@ -1401,6 +1478,7 @@ impl TypeChecker { .map_err(TypeCheckError::RegistryError), TypeAnnotation::Bool(_) => Ok(Type::Boolean), TypeAnnotation::String(_) => Ok(Type::String), + TypeAnnotation::DateTime(_) => Ok(Type::DateTime), } } } diff --git a/numbat/src/typed_ast.rs b/numbat/src/typed_ast.rs index e29cfa70..087161c6 100644 --- a/numbat/src/typed_ast.rs +++ b/numbat/src/typed_ast.rs @@ -1,15 +1,15 @@ use itertools::Itertools; -use crate::arithmetic::Exponent; +use crate::arithmetic::{Exponent, Rational}; use crate::ast::ProcedureKind; pub use crate::ast::{BinaryOperator, DimensionExpression, UnaryOperator}; use crate::dimension::DimensionRegistry; -use crate::markup as m; use crate::{ decorator::Decorator, markup::Markup, number::Number, prefix::Prefix, prefix_parser::AcceptsPrefix, pretty_print::PrettyPrint, registry::BaseRepresentation, span::Span, }; +use crate::{markup as m, BaseRepresentationFactor}; /// Dimension type pub type DType = BaseRepresentation; @@ -40,6 +40,16 @@ impl DType { } } } + /// Is the current dimension type the Time dimension? + /// + /// This is special helper that's useful when dealing with DateTimes + pub fn is_time_dimension(&self) -> bool { + *self + == BaseRepresentation::from_factor(BaseRepresentationFactor( + "Time".into(), + Rational::from_integer(1), + )) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -47,6 +57,7 @@ pub enum Type { Dimension(DType), Boolean, String, + DateTime, } impl std::fmt::Display for Type { @@ -55,6 +66,7 @@ impl std::fmt::Display for Type { Type::Dimension(d) => d.fmt(f), Type::Boolean => write!(f, "Bool"), Type::String => write!(f, "String"), + Type::DateTime => write!(f, "DateTime"), } } } @@ -65,6 +77,7 @@ impl PrettyPrint for Type { Type::Dimension(d) => d.pretty_print(), Type::Boolean => m::keyword("Bool"), Type::String => m::keyword("String"), + Type::DateTime => m::keyword("DateTime"), } } } @@ -122,6 +135,16 @@ pub enum Expression { Box, Type, ), + /// A special binary operator that has a DateTime as one (or both) of the operands + BinaryOperatorForDate( + Option, + BinaryOperator, + /// LHS must evaluate to a DateTime + Box, + /// RHS can evaluate to a DateTime, a quantity of type Time, or a String (for timezone conversions) + Box, + Type, + ), FunctionCall(Span, Span, String, Vec, Type), Boolean(Span, bool), Condition(Span, Box, Box, Box), @@ -142,6 +165,13 @@ impl Expression { } span } + Expression::BinaryOperatorForDate(span_op, _op, lhs, rhs, ..) => { + let mut span = lhs.full_span().extend(&rhs.full_span()); + if let Some(span_op) = span_op { + span = span.extend(span_op); + } + span + } Expression::FunctionCall(_identifier_span, full_span, _, _, _) => *full_span, Expression::Boolean(span, _) => *span, Expression::Condition(span_if, _, _, then_expr) => { @@ -195,6 +225,7 @@ impl Expression { Expression::UnitIdentifier(_, _, _, _, _type) => _type.clone(), Expression::UnaryOperator(_, _, _, type_) => type_.clone(), Expression::BinaryOperator(_, _, _, _, type_) => type_.clone(), + Expression::BinaryOperatorForDate(_, _, _, _, type_, ..) => type_.clone(), Expression::FunctionCall(_, _, _, _, type_) => type_.clone(), Expression::Boolean(_, _) => Type::Boolean, Expression::Condition(_, _, then, _) => then.get_type(), @@ -403,6 +434,7 @@ fn with_parens(expr: &Expression) -> Markup { | Expression::String(..) => expr.pretty_print(), Expression::UnaryOperator { .. } | Expression::BinaryOperator { .. } + | Expression::BinaryOperatorForDate { .. } | Expression::Condition(..) => m::operator("(") + expr.pretty_print() + m::operator(")"), } } @@ -542,6 +574,7 @@ impl PrettyPrint for Expression { m::operator("!") + with_parens(expr) } BinaryOperator(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs), + BinaryOperatorForDate(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs), FunctionCall(_, _, name, args, _type) => { m::identifier(name) + m::operator("(") diff --git a/numbat/src/value.rs b/numbat/src/value.rs index 12b3aa25..c92389a0 100644 --- a/numbat/src/value.rs +++ b/numbat/src/value.rs @@ -5,6 +5,8 @@ pub enum Value { Quantity(Quantity), Boolean(bool), String(String), + /// A DateTime with an associated offset used when pretty printing + DateTime(chrono::DateTime, chrono::FixedOffset), } impl Value { @@ -31,6 +33,14 @@ impl Value { panic!("Expected value to be a string"); } } + + pub fn unsafe_as_datetime(&self) -> &chrono::DateTime { + if let Value::DateTime(dt, _) = self { + dt + } else { + panic!("Expected value to be a string"); + } + } } impl std::fmt::Display for Value { @@ -39,6 +49,7 @@ impl std::fmt::Display for Value { Value::Quantity(q) => write!(f, "{}", q), Value::Boolean(b) => write!(f, "{}", b), Value::String(s) => write!(f, "\"{}\"", s), + Value::DateTime(dt, _) => write!(f, "{:?}", dt), } } } @@ -49,6 +60,11 @@ impl PrettyPrint for Value { Value::Quantity(q) => q.pretty_print(), Value::Boolean(b) => b.pretty_print(), Value::String(s) => s.pretty_print(), + Value::DateTime(dt, offset) => { + let l: chrono::DateTime = + chrono::DateTime::from_naive_utc_and_offset(dt.naive_utc(), *offset); + crate::markup::string(format!("{}", l.to_rfc2822())) + } } } } diff --git a/numbat/src/vm.rs b/numbat/src/vm.rs index 00785997..b56d4446 100644 --- a/numbat/src/vm.rs +++ b/numbat/src/vm.rs @@ -1,10 +1,13 @@ use std::{cmp::Ordering, fmt::Display}; +use chrono::Offset; + use crate::{ ffi::{self, ArityRange, Callable, ForeignFunction}, interpreter::{InterpreterResult, PrintFunction, Result, RuntimeError}, markup::Markup, math, + number::Number, prefix::Prefix, quantity::{Quantity, QuantityError}, unit::Unit, @@ -69,6 +72,15 @@ pub enum Op { LogicalOr, LogicalNeg, + /// Similar to Add, but has DateTime on the LHS and a quantity on the RHS + AddDateTime, + /// Similar to Sub, but has DateTime on the LHS and a quantity on the RHS + SubDateTime, + /// Computes the difference between two DateTimes + DiffDateTime, + /// Converts a DateTime value to another timezone + ConvertDateTime, + /// Move IP forward by the given offset argument if the popped-of value on /// top of the stack is false. JumpIfFalse, @@ -110,7 +122,11 @@ impl Op { Op::Negate | Op::Factorial | Op::Add + | Op::AddDateTime | Op::Subtract + | Op::SubDateTime + | Op::DiffDateTime + | Op::ConvertDateTime | Op::Multiply | Op::Divide | Op::Power @@ -141,7 +157,11 @@ impl Op { Op::Negate => "Negate", Op::Factorial => "Factorial", Op::Add => "Add", + Op::AddDateTime => "AddDateTime", Op::Subtract => "Subtract", + Op::SubDateTime => "SubDateTime", + Op::DiffDateTime => "DiffDateTime", + Op::ConvertDateTime => "ConvertDateTime", Op::Multiply => "Multiply", Op::Divide => "Divide", Op::Power => "Power", @@ -168,7 +188,7 @@ impl Op { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum Constant { Scalar(f64), Unit(Unit), @@ -506,6 +526,20 @@ impl Vm { self.pop().unsafe_as_bool() } + fn pop_datetime(&mut self) -> chrono::DateTime { + match self.pop() { + Value::DateTime(q, _) => q, + _ => panic!("Expected datetime to be on the top of the stack"), + } + } + + fn pop_string(&mut self) -> String { + match self.pop() { + Value::String(s) => s, + _ => panic!("Expected string to be on the top of the stack"), + } + } + fn pop(&mut self) -> Value { self.stack.pop().expect("stack should not be empty") } @@ -615,6 +649,59 @@ impl Vm { }; self.push_quantity(result.map_err(RuntimeError::QuantityError)?); } + op @ (Op::AddDateTime | Op::SubDateTime) => { + let rhs = self.pop_quantity(); + let lhs = self.pop_datetime(); + + // for time, the base unit is in seconds + let base = rhs.to_base_unit_representation(); + let seconds_f = base.unsafe_value().to_f64(); + + let duration = chrono::Duration::seconds(seconds_f.trunc() as i64) + + chrono::Duration::nanoseconds( + (seconds_f.fract() * 1_000_000_000f64).round() as i64, + ); + + self.push(Value::DateTime( + match op { + Op::AddDateTime => lhs + duration, + Op::SubDateTime => lhs - duration, + _ => unreachable!(), + }, + chrono::Local::now().offset().fix(), + )); + } + Op::DiffDateTime => { + let unit = self.pop_quantity(); + let rhs = self.pop_datetime(); + let lhs = self.pop_datetime(); + + let duration = lhs - rhs; + let duration = duration.subsec_nanos() as f64 / 1_000_000_000f64 + + duration.num_seconds() as f64; + + let ret = Value::Quantity(Quantity::new( + Number::from_f64(duration), + unit.unit().clone(), + )); + + self.push(ret); + } + Op::ConvertDateTime => { + let rhs = self.pop_string(); + let lhs = self.pop_datetime(); + + let offset = if rhs == "local" { + chrono::Local::now().offset().fix() + } else { + let tz: chrono_tz::Tz = rhs + .parse() + .map_err(|_| RuntimeError::UnknownTimezone(rhs))?; + lhs.with_timezone(&tz).offset().fix() + }; + + self.push(Value::DateTime(lhs, offset)); + } op @ (Op::LessThan | Op::GreaterThan | Op::LessOrEqual | Op::GreatorOrEqual) => { let rhs = self.pop_quantity(); let lhs = self.pop_quantity(); @@ -743,6 +830,14 @@ impl Vm { Value::Quantity(q) => q.to_string(), Value::Boolean(b) => b.to_string(), Value::String(s) => s, + Value::DateTime(dt, offset) => { + let l: chrono::DateTime = + chrono::DateTime::from_naive_utc_and_offset( + dt.naive_utc(), + offset, + ); + l.to_rfc2822() + } }; joined = part + &joined; // reverse order }