diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 9ca433b7..cf4ef127 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -28,7 +28,7 @@ - [Operations and precedence](./operations.md) - [Constants](./constant-definitions.md) - [Unit conversions](./unit-conversions.md) - - [Other conversions](./conversion-functions.md) + - [Conversions functions](./conversion-functions.md) - [Function definitions](./function-definitions.md) - [Conditionals](./conditionals.md) - [Date and time](./date-and-time.md) diff --git a/book/src/conversion-functions.md b/book/src/conversion-functions.md index afa8ec5e..88275b6c 100644 --- a/book/src/conversion-functions.md +++ b/book/src/conversion-functions.md @@ -1,4 +1,4 @@ -# Other conversions +# Conversion functions The conversion operator `->` (or `to`) can not just be used for [unit conversions](./unit-conversions.md), but also for other types of conversions. The way this is set up in Numbat is that you can call `x -> f` for any function `f` that takes a single argument of the same type as `x`. diff --git a/book/src/date-and-time.md b/book/src/date-and-time.md index 8ca0365a..777a3549 100644 --- a/book/src/date-and-time.md +++ b/book/src/date-and-time.md @@ -16,10 +16,10 @@ now() - 1 million seconds parse_datetime("2024-11-01 12:30:00") - now() -> days # What time is it in Nepal right now? -now() -> "Asia/Kathmandu" # use tab completion to find time zone names +now() -> tz("Asia/Kathmandu") # use tab completion to find time zone names # What is the local time when it is 2024-11-01 12:30:00 in Australia? -parse_datetime("2024-11-01 12:30:00 Australia/Sydney") -> "local" +parse_datetime("2024-11-01 12:30:00 Australia/Sydney") -> local # What is the current UNIX timestamp? now() -> unixtime @@ -40,7 +40,7 @@ The following operations are supported for `DateTime` objects: | `DateTime` | `-` | `DateTime` | Duration between the two dates as a `Time`. In `seconds`, by default. Use normal conversion for other time units. | | `DateTime` | `+` | `Time` | New `DateTime` by adding the duration to the date | | `DateTime` | `-` | `Time` | New `DateTime` by subtracting the duration from the date | -| `DateTime` | `->` | `String` | Converts the datetime to the specified time zone. Note that you can use tab-completion for time zone names. | +| `DateTime` | `->` | `tz("…")` | Converts the datetime to the specified time zone. Note that you can use tab-completion for time zone names. |
@@ -60,6 +60,9 @@ The following functions are available for date and time handling: - `now() -> DateTime`: Returns the current date and time. - `parse_datetime(input: String) -> DateTime`: Parses a string into a `DateTime` object. - `format_datetime(format: String, dt: DateTime) -> String`: Formats a `DateTime` object as a string. See [this page](https://docs.rs/chrono/latest/chrono/format/strftime/index.html#specifiers) for possible format specifiers. +- `tz(tz: String) -> Fn[(DateTime) -> DateTime]`: Returns a timezone conversion function, typically used with the conversion operator (`datetime -> tz("Europe/Berlin")`) +- `local(dt: DateTime) -> DateTime`: Timezone conversion function targeting the users local timezone (`datetime -> local`) +- `get_local_timezone() -> String`: Returns the users local timezone - `unixtime(dt: DateTime) -> Scalar`: Converts a `DateTime` to a UNIX timestamp. - `from_unixtime(ut: Scalar) -> DateTime`: Converts a UNIX timestamp to a `DateTime` object. - `human(duration: Time) -> String`: Converts a `Time` to a human-readable string in days, hours, minutes and seconds diff --git a/book/src/list-functions.md b/book/src/list-functions.md index b6703365..ce80f024 100644 --- a/book/src/list-functions.md +++ b/book/src/list-functions.md @@ -105,6 +105,9 @@ fn sphere_volume(radius: L) -> L^3 fn now() -> DateTime fn parse_datetime(input: String) -> DateTime fn format_datetime(format: String, dt: DateTime) -> String +fn tz(tz: String) -> Fn[(DateTime) -> DateTime] +fn local(dt: DateTime) -> DateTime +fn get_local_timezone() -> String fn from_unixtime(t: Scalar) -> DateTime fn unixtime(dt: DateTime) -> Scalar fn human(t: Time) -> String diff --git a/numbat-cli/src/completer.rs b/numbat-cli/src/completer.rs index 59b73857..2d9b8fd8 100644 --- a/numbat-cli/src/completer.rs +++ b/numbat-cli/src/completer.rs @@ -74,20 +74,15 @@ 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); - } + // does it look like we're tab-completing a timezone? + let complete_tz = line.find("tz(").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 - }); + } + None + }); if let Some(pos_word) = complete_tz { let word_part = &line[pos_word..]; let matches = self diff --git a/numbat-cli/src/main.rs b/numbat-cli/src/main.rs index 156376d7..a68b8306 100644 --- a/numbat-cli/src/main.rs +++ b/numbat-cli/src/main.rs @@ -262,11 +262,7 @@ 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 - }, + all_timezones: chrono_tz::TZ_VARIANTS.map(|v| v.name()).into(), }, highlighter: NumbatHighlighter { context: self.context.clone(), diff --git a/numbat/modules/datetime/functions.nbt b/numbat/modules/datetime/functions.nbt index 6908f9a4..87352653 100644 --- a/numbat/modules/datetime/functions.nbt +++ b/numbat/modules/datetime/functions.nbt @@ -3,5 +3,8 @@ use units::si fn now() -> DateTime fn parse_datetime(input: String) -> DateTime fn format_datetime(format: String, input: DateTime) -> String +fn get_local_timezone() -> String +fn tz(tz: String) -> Fn[(DateTime) -> DateTime] +let local: Fn[(DateTime) -> DateTime] = tz(get_local_timezone()) fn unixtime(input: DateTime) -> Scalar fn from_unixtime(input: Scalar) -> DateTime diff --git a/numbat/src/bytecode_interpreter.rs b/numbat/src/bytecode_interpreter.rs index 608cf6e5..ce301ee1 100644 --- a/numbat/src/bytecode_interpreter.rs +++ b/numbat/src/bytecode_interpreter.rs @@ -144,7 +144,6 @@ impl BytecodeInterpreter { match operator { BinaryOperator::Add => Op::AddToDateTime, BinaryOperator::Sub => Op::SubFromDateTime, - BinaryOperator::ConvertTo => Op::ConvertDateTime, _ => unreachable!("{operator:?} is not valid with a DateTime"), // should be unreachable, because the typechecker will error first } }; diff --git a/numbat/src/ffi.rs b/numbat/src/ffi.rs index 13b0e75b..cf511020 100644 --- a/numbat/src/ffi.rs +++ b/numbat/src/ffi.rs @@ -7,7 +7,7 @@ use chrono::Offset; use crate::currency::ExchangeRatesCache; use crate::interpreter::RuntimeError; use crate::pretty_print::PrettyPrint; -use crate::value::Value; +use crate::value::{FunctionReference, Value}; use crate::vm::ExecutionContext; use crate::{ast::ProcedureKind, quantity::Quantity}; @@ -375,6 +375,24 @@ pub(crate) fn functions() -> &'static HashMap { }, ); + m.insert( + "get_local_timezone".to_string(), + ForeignFunction { + name: "get_local_timezone".into(), + arity: 0..=0, + callable: Callable::Function(Box::new(get_local_timezone)), + }, + ); + + m.insert( + "tz".to_string(), + ForeignFunction { + name: "tz".into(), + arity: 1..=1, + callable: Callable::Function(Box::new(tz)), + }, + ); + m.insert( "unixtime".to_string(), ForeignFunction { @@ -848,6 +866,26 @@ fn format_datetime(args: &[Value]) -> Result { Ok(Value::String(output)) } +fn get_local_timezone(args: &[Value]) -> Result { + assert!(args.len() == 0); + + let local_tz = crate::datetime::get_local_timezone() + .unwrap_or(chrono_tz::Tz::UTC) + .to_string(); + + Ok(Value::String(local_tz)) +} + +fn tz(args: &[Value]) -> Result { + assert!(args.len() == 1); + + let tz = args[0].unsafe_as_string(); + + Ok(Value::FunctionReference(FunctionReference::TzConversion( + tz.into(), + ))) +} + fn unixtime(args: &[Value]) -> Result { assert!(args.len() == 1); diff --git a/numbat/src/typechecker.rs b/numbat/src/typechecker.rs index 38c3842c..9960c003 100644 --- a/numbat/src/typechecker.rs +++ b/numbat/src/typechecker.rs @@ -760,7 +760,6 @@ impl TypeChecker { } else if lhs_checked.get_type() == Type::DateTime { // 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 let rhs_is_time = dtype(&rhs_checked) .ok() @@ -768,16 +767,7 @@ impl TypeChecker { .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 { + if *op == BinaryOperator::Sub && rhs_is_datetime { let time = self .registry .get_base_representation_for_name("Time") diff --git a/numbat/src/typed_ast.rs b/numbat/src/typed_ast.rs index 556026c6..411613d3 100644 --- a/numbat/src/typed_ast.rs +++ b/numbat/src/typed_ast.rs @@ -168,7 +168,7 @@ pub enum Expression { 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) + /// RHS can evaluate to a DateTime or a quantity of type Time Box, Type, ), diff --git a/numbat/src/value.rs b/numbat/src/value.rs index 8caaafdd..4c7da42e 100644 --- a/numbat/src/value.rs +++ b/numbat/src/value.rs @@ -4,13 +4,18 @@ use crate::{pretty_print::PrettyPrint, quantity::Quantity}; pub enum FunctionReference { Foreign(String), Normal(String), + // TODO: We can get rid of this variant once we implement closures: + TzConversion(String), } impl std::fmt::Display for FunctionReference { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - FunctionReference::Foreign(name) => write!(f, "", name), - FunctionReference::Normal(name) => write!(f, "", name), + FunctionReference::Foreign(name) => write!(f, ""), + FunctionReference::Normal(name) => write!(f, ""), + FunctionReference::TzConversion(tz) => { + write!(f, "") + } } } } diff --git a/numbat/src/vm.rs b/numbat/src/vm.rs index afe70c69..071af8cd 100644 --- a/numbat/src/vm.rs +++ b/numbat/src/vm.rs @@ -78,8 +78,6 @@ pub enum Op { SubFromDateTime, /// 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. @@ -130,7 +128,6 @@ impl Op { | Op::Subtract | Op::SubFromDateTime | Op::DiffDateTime - | Op::ConvertDateTime | Op::Multiply | Op::Divide | Op::Power @@ -165,7 +162,6 @@ impl Op { Op::Subtract => "Subtract", Op::SubFromDateTime => "SubDateTime", Op::DiffDateTime => "DiffDateTime", - Op::ConvertDateTime => "ConvertDateTime", Op::Multiply => "Multiply", Op::Divide => "Divide", Op::Power => "Power", @@ -544,14 +540,6 @@ impl Vm { } } - #[track_caller] - fn pop_string(&mut self) -> String { - match self.pop() { - Value::String(s) => s, - _ => panic!("Expected string to be on the top of the stack"), - } - } - #[track_caller] fn pop(&mut self) -> Value { self.stack.pop().expect("stack should not be empty") @@ -705,21 +693,6 @@ impl Vm { self.push(ret); } - Op::ConvertDateTime => { - let rhs = self.pop_string(); - let lhs = self.pop_datetime(); - - let offset = if rhs == "local" { - crate::datetime::local_offset_for_datetime(&lhs) - } 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(); @@ -870,6 +843,19 @@ impl Vm { Callable::Procedure(..) => unreachable!("Foreign procedures can not be targeted by a function reference"), } } + FunctionReference::TzConversion(tz_name) => { + // TODO: implement this using a closure, once we have that in the language + + let dt = self.pop_datetime(); + + let tz: chrono_tz::Tz = tz_name + .parse() + .map_err(|_| RuntimeError::UnknownTimezone(tz_name.into()))?; + + let offset = dt.with_timezone(&tz).offset().fix(); + + self.push(Value::DateTime(dt, offset)); + } } } Op::PrintString => {