Skip to content

Commit

Permalink
Add tentative impl for timezone conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
eminence committed Jan 7, 2024
1 parent 479a6c4 commit 3f236f8
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 25 deletions.
91 changes: 91 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions numbat/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ rust-embed = { version = "8.0.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"]
Expand Down
1 change: 1 addition & 0 deletions numbat/src/bytecode_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ impl BytecodeInterpreter {
match operator {
BinaryOperator::Add => Op::AddDateTime,
BinaryOperator::Sub => Op::SubDateTime,
BinaryOperator::ConvertTo => Op::ConvertDateTime,
_ => panic!("{operator:?} is not valid with a DateTime"), // should be unreachable, because the typechecker will error first
}
};
Expand Down
13 changes: 10 additions & 3 deletions numbat/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -788,7 +790,9 @@ fn now(args: &[Value]) -> Result<Value> {
assert!(args.len() == 0);
let now = chrono::Utc::now();

Ok(Value::DateTime(now))
let offset = now.with_timezone(&chrono::Local).offset().fix();

Ok(Value::DateTime(now, offset))
}

fn parse_datetime(args: &[Value]) -> Result<Value> {
Expand All @@ -801,7 +805,9 @@ fn parse_datetime(args: &[Value]) -> Result<Value> {
.or_else(|_| chrono::DateTime::parse_from_rfc2822(input))
.map_err(|e| RuntimeError::DateParsingError(e))?;

Ok(Value::DateTime(output.into()))
let offset = output.offset();

Ok(Value::DateTime(output.into(), *offset))
}

fn format_datetime(args: &[Value]) -> Result<Value> {
Expand Down Expand Up @@ -832,6 +838,7 @@ fn from_unixtime(args: &[Value]) -> Result<Value> {
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))
Ok(Value::DateTime(dt, offset))
}
9 changes: 0 additions & 9 deletions numbat/src/pretty_print.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,3 @@ impl PrettyPrint for String {
crate::markup::string(self)
}
}

impl<T> PrettyPrint for chrono::DateTime<T>
where
T: chrono::TimeZone,
{
fn pretty_print(&self) -> Markup {
crate::markup::string(format!("{self:?}"))
}
}
14 changes: 13 additions & 1 deletion numbat/src/typechecker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,19 @@ impl TypeChecker {
.unwrap_or(false);
let rhs_is_datetime = rhs_checked.get_type() == Type::DateTime;

if *op == BinaryOperator::Sub && rhs_is_datetime {
if *op == BinaryOperator::ConvertTo
&& matches!(rhs_checked, typed_ast::Expression::String(..))
{
// Supports timezone conversion
typed_ast::Expression::BinaryOperatorForDate(
*span_op,
*op,
Box::new(lhs_checked),
Box::new(rhs_checked),
Type::DateTime,
false,
)
} else if *op == BinaryOperator::Sub && rhs_is_datetime {
// TODO error handling
let time = self
.registry
Expand Down
3 changes: 2 additions & 1 deletion numbat/src/typed_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ pub enum Expression {
BinaryOperatorForDate(
Option<Span>,
BinaryOperator,
/// Must be a DateTime
/// LHS must be a DateTime
Box<Expression>,
/// RHS may be a DateTime, a Time unit, or a String
Box<Expression>,
Type,
/// If true, then this is a date subtration operation
Expand Down
13 changes: 9 additions & 4 deletions numbat/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ pub enum Value {
Quantity(Quantity),
Boolean(bool),
String(String),
DateTime(chrono::DateTime<chrono::Utc>),
/// A DateTime with an associated offset used when pretty printing
DateTime(chrono::DateTime<chrono::Utc>, chrono::FixedOffset),
}

impl Value {
Expand Down Expand Up @@ -34,7 +35,7 @@ impl Value {
}

pub fn unsafe_as_datetime(&self) -> &chrono::DateTime<chrono::Utc> {
if let Value::DateTime(dt) = self {
if let Value::DateTime(dt, _) = self {
dt
} else {
panic!("Expected value to be a string");
Expand All @@ -48,7 +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),
Value::DateTime(dt, _) => write!(f, "{:?}", dt),
}
}
}
Expand All @@ -59,7 +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) => dt.pretty_print(),
Value::DateTime(dt, offset) => {
let l: chrono::DateTime<chrono::FixedOffset> =
chrono::DateTime::from_naive_utc_and_offset(dt.naive_utc(), *offset);
crate::markup::string(format!("{}", l.to_rfc2822()))
}
}
}
}
44 changes: 37 additions & 7 deletions numbat/src/vm.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::{cmp::Ordering, fmt::Display};

use chrono::Offset;

use crate::{
ffi::{self, ArityRange, Callable, ForeignFunction},
interpreter::{InterpreterResult, PrintFunction, Result, RuntimeError},
Expand Down Expand Up @@ -76,6 +78,8 @@ pub enum Op {
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.
Expand Down Expand Up @@ -122,6 +126,7 @@ impl Op {
| Op::Subtract
| Op::SubDateTime
| Op::DiffDateTime
| Op::ConvertDateTime
| Op::Multiply
| Op::Divide
| Op::Power
Expand Down Expand Up @@ -156,6 +161,7 @@ impl Op {
Op::Subtract => "Subtract",
Op::SubDateTime => "SubDateTime",
Op::DiffDateTime => "DiffDateTime",
Op::ConvertDateTime => "ConvertDateTime",
Op::Multiply => "Multiply",
Op::Divide => "Divide",
Op::Power => "Power",
Expand Down Expand Up @@ -198,7 +204,7 @@ impl Constant {
Constant::Unit(u) => Value::Quantity(Quantity::from_unit(u.clone())),
Constant::Boolean(b) => Value::Boolean(*b),
Constant::String(s) => Value::String(s.clone()),
Constant::DateTime(d) => Value::DateTime(*d),
Constant::DateTime(d) => Value::DateTime(*d, d.offset().fix()),
}
}
}
Expand Down Expand Up @@ -525,11 +531,18 @@ impl Vm {

fn pop_datetime(&mut self) -> chrono::DateTime<chrono::Utc> {
match self.pop() {
Value::DateTime(q) => q,
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")
}
Expand Down Expand Up @@ -653,11 +666,14 @@ impl Vm {
(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!(),
}));
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();
Expand All @@ -675,6 +691,20 @@ impl Vm {

self.push(ret);
}
Op::ConvertDateTime => {
let rhs = self.pop_string();
let lhs = self.pop_datetime();

// TODO how to handle errors, and is this `chrono_tz` crate the best choice?
let offset = if rhs == "local" {
chrono::Local::now().offset().fix()
} else {
let tz: chrono_tz::Tz = rhs.parse().unwrap_or(chrono_tz::UTC);
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

0 comments on commit 3f236f8

Please sign in to comment.