1
0
Эх сурвалжийг харах

Add new datetime functions and related tests

Andrew Chin 1 жил өмнө
parent
commit
d2bbf0f632

+ 15 - 0
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")

+ 7 - 1
numbat/modules/core/datetime.nbt

@@ -1 +1,7 @@
-fn now() -> DateTime
+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

+ 80 - 0
numbat/src/ffi.rs

@@ -339,6 +339,41 @@ pub(crate) fn functions() -> &'static HashMap<String, ForeignFunction> {
                 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
     })
@@ -755,3 +790,48 @@ fn now(args: &[Value]) -> Result<Value> {
 
     Ok(Value::DateTime(now))
 }
+
+fn parse_datetime(args: &[Value]) -> Result<Value> {
+    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(|e| RuntimeError::DateParsingError(e))?;
+
+    Ok(Value::DateTime(output.into()))
+}
+
+fn format_datetime(args: &[Value]) -> Result<Value> {
+    assert!(args.len() == 2);
+
+    // TODO should we support taking the format/datetime args in either order?
+    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<Value> {
+    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<Value> {
+    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();
+
+    Ok(Value::DateTime(dt))
+}

+ 2 - 0
numbat/src/interpreter.rs

@@ -37,6 +37,8 @@ pub enum RuntimeError {
     CouldNotLoadExchangeRates,
     #[error("User error: {0}")]
     UserError(String),
+    #[error("Could not parse date: {0}")]
+    DateParsingError(chrono::ParseError),
 }
 
 #[derive(Debug, PartialEq, Eq)]

+ 8 - 0
numbat/src/value.rs

@@ -32,6 +32,14 @@ impl Value {
             panic!("Expected value to be a string");
         }
     }
+
+    pub fn unsafe_as_datetime(&self) -> &chrono::DateTime<chrono::Utc> {
+        if let Value::DateTime(dt) = self {
+            dt
+        } else {
+            panic!("Expected value to be a string");
+        }
+    }
 }
 
 impl std::fmt::Display for Value {