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 => {