Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Embed timezone conversion into conversion-function framework #339

Merged
merged 1 commit into from
Feb 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion book/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion book/src/conversion-functions.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand Down
9 changes: 6 additions & 3 deletions book/src/date-and-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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. |

<div class="warning">

Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions book/src/list-functions.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,9 @@ fn sphere_volume<L>(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
Expand Down
21 changes: 8 additions & 13 deletions numbat-cli/src/completer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions numbat-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
3 changes: 3 additions & 0 deletions numbat/modules/datetime/functions.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion numbat/src/bytecode_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
};
Expand Down
40 changes: 39 additions & 1 deletion numbat/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -375,6 +375,24 @@ pub(crate) fn functions() -> &'static HashMap<String, ForeignFunction> {
},
);

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 {
Expand Down Expand Up @@ -848,6 +866,26 @@ fn format_datetime(args: &[Value]) -> Result<Value> {
Ok(Value::String(output))
}

fn get_local_timezone(args: &[Value]) -> Result<Value> {
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<Value> {
assert!(args.len() == 1);

let tz = args[0].unsafe_as_string();

Ok(Value::FunctionReference(FunctionReference::TzConversion(
tz.into(),
)))
}

fn unixtime(args: &[Value]) -> Result<Value> {
assert!(args.len() == 1);

Expand Down
12 changes: 1 addition & 11 deletions numbat/src/typechecker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -760,24 +760,14 @@ 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()
.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 {
if *op == BinaryOperator::Sub && rhs_is_datetime {
let time = self
.registry
.get_base_representation_for_name("Time")
Expand Down
2 changes: 1 addition & 1 deletion numbat/src/typed_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ pub enum Expression {
BinaryOperator,
/// LHS must evaluate to a DateTime
Box<Expression>,
/// 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<Expression>,
Type,
),
Expand Down
9 changes: 7 additions & 2 deletions numbat/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<builtin function: {}>", name),
FunctionReference::Normal(name) => write!(f, "<function: {}>", name),
FunctionReference::Foreign(name) => write!(f, "<builtin function: {name}>"),
FunctionReference::Normal(name) => write!(f, "<function: {name}>"),
FunctionReference::TzConversion(tz) => {
write!(f, "<builtin timezone conversion function: {tz}>")
}
}
}
}
Expand Down
40 changes: 13 additions & 27 deletions numbat/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -130,7 +128,6 @@ impl Op {
| Op::Subtract
| Op::SubFromDateTime
| Op::DiffDateTime
| Op::ConvertDateTime
| Op::Multiply
| Op::Divide
| Op::Power
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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 => {
Expand Down
Loading