Add DateTime type and operations

This commit introduces the `DateTime` type to the language, enabling
users to work with dates and times. It includes:

- A new `DateTime` variant in the `Value` and `StaticType` enums.
- A `date` function for parsing date strings into `DateTime` values.
- Overloads for `+` and `-` operators to support `DateTime` arithmetic.
- Comparison operators (`>`, `<`) for `DateTime` values.
- Corresponding unit tests for `DateTime` operations.
- Updates to the `gemini.md` documentation to reflect the new
  functionality.
  Add DateTime type and operations

Introduce a new `DateTime` type to the language, allowing for date and
time manipulation. This includes:

*   Parsing dates and datetimes from strings.
*   Performing arithmetic operations (addition and subtraction) between
    `DateTime` and `Int` (representing milliseconds) and between two
    `DateTime` values (resulting in an `Int` duration).
*   Enabling comparison operations (`>`, `<`) between `DateTime` values.
This commit is contained in:
Michael Schimmel
2026-02-19 13:27:44 +01:00
parent 49db73800a
commit a5957f524b
4 changed files with 54 additions and 2 deletions
+2 -1
View File
@@ -40,4 +40,5 @@ Wichtig: zentraler Einstiegspunkt ist Delphi/Myc.Ast.Environment.pasund die dort
## Testing ## Testing
* Das Projekt enthält eine Testsuite für Skripte. Logik in src/utils/tester.rs. Diese Testes werden automatisch in "cargo test" eingebunden. * Das Projekt enthält eine Testsuite für Skripte. Logik in src/utils/tester.rs. Diese Testes werden automatisch in "cargo test" eingebunden.
* Nach Fertigstellung einer Aufgabe: Warnungen sind zu eliminieren. Tests müssen durchlaufen. Clippy auch. * Nach Fertigstellung einer Aufgabe: Warnungen sind zu eliminieren. Tests müssen durchlaufen.
* Wenn alles funktioniert, muss auch clippy ohne Beanstandungen durchlaufen.
+12
View File
@@ -291,4 +291,16 @@ mod tests {
assert_eq!(check_source("(+ \"a\" \"b\")").ty, StaticType::Text); assert_eq!(check_source("(+ \"a\" \"b\")").ty, StaticType::Text);
assert_eq!(check_source("(/ 1 2)").ty, StaticType::Float); assert_eq!(check_source("(/ 1 2)").ty, StaticType::Float);
} }
#[test]
fn test_datetime_inference() {
// date("2023-01-01") -> DateTime
assert_eq!(check_source("(date \"2023-01-01\")").ty, StaticType::DateTime);
// DateTime + Int -> DateTime
assert_eq!(check_source("(+ (date \"2023-01-01\") 86400000)").ty, StaticType::DateTime);
// DateTime - DateTime -> Int (Duration)
assert_eq!(check_source("(- (date \"2023-01-02\") (date \"2023-01-01\"))").ty, StaticType::Int);
// DateTime comparison -> Bool
assert_eq!(check_source("(> (date \"2023-01-02\") (date \"2023-01-01\"))").ty, StaticType::Bool);
}
} }
+29 -1
View File
@@ -89,11 +89,31 @@ impl Environment {
fn register_stdlib(&self) { fn register_stdlib(&self) {
use crate::ast::types::Signature; use crate::ast::types::Signature;
use chrono::{NaiveDate, NaiveDateTime};
self.register_native("date", StaticType::Function(Box::new(Signature {
params: vec![StaticType::Text],
ret: StaticType::DateTime
})), |args| {
if let Value::Text(s) = &args[0] {
// Try parse YYYY-MM-DD
if let Ok(dt) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
let ts = dt.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp_millis();
return Value::DateTime(ts);
}
// Try parse YYYY-MM-DD HH:MM:SS
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S") {
return Value::DateTime(dt.and_utc().timestamp_millis());
}
}
Value::Void
});
let plus_ty = StaticType::FunctionOverloads(vec![ let plus_ty = StaticType::FunctionOverloads(vec![
Signature { params: vec![StaticType::Int, StaticType::Int], ret: StaticType::Int }, Signature { params: vec![StaticType::Int, StaticType::Int], ret: StaticType::Int },
Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Float }, Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Float },
Signature { params: vec![StaticType::Text, StaticType::Text], ret: StaticType::Text }, Signature { params: vec![StaticType::Text, StaticType::Text], ret: StaticType::Text },
Signature { params: vec![StaticType::DateTime, StaticType::Int], ret: StaticType::DateTime },
]); ]);
self.register_native("+", plus_ty, |args| { self.register_native("+", plus_ty, |args| {
if args.len() == 2 { if args.len() == 2 {
@@ -106,7 +126,8 @@ impl Environment {
let mut res = a.to_string(); let mut res = a.to_string();
res.push_str(b); res.push_str(b);
Value::Text(Rc::from(res)) Value::Text(Rc::from(res))
} },
(Value::DateTime(ts), Value::Int(ms)) => Value::DateTime(ts + ms),
_ => Value::Void, _ => Value::Void,
} }
} else { } else {
@@ -124,6 +145,8 @@ impl Environment {
Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Float }, Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Float },
Signature { params: vec![StaticType::Int], ret: StaticType::Int }, Signature { params: vec![StaticType::Int], ret: StaticType::Int },
Signature { params: vec![StaticType::Float], ret: StaticType::Float }, Signature { params: vec![StaticType::Float], ret: StaticType::Float },
Signature { params: vec![StaticType::DateTime, StaticType::DateTime], ret: StaticType::Int },
Signature { params: vec![StaticType::DateTime, StaticType::Int], ret: StaticType::DateTime },
]); ]);
self.register_native("-", minus_ty, |args| { self.register_native("-", minus_ty, |args| {
if args.is_empty() { return Value::Void; } if args.is_empty() { return Value::Void; }
@@ -140,6 +163,8 @@ impl Environment {
(Value::Float(a), Value::Float(b)) => Value::Float(a - b), (Value::Float(a), Value::Float(b)) => Value::Float(a - b),
(Value::Int(a), Value::Float(b)) => Value::Float(*a as f64 - b), (Value::Int(a), Value::Float(b)) => Value::Float(*a as f64 - b),
(Value::Float(a), Value::Int(b)) => Value::Float(a - *b as f64), (Value::Float(a), Value::Int(b)) => Value::Float(a - *b as f64),
(Value::DateTime(a), Value::DateTime(b)) => Value::Int(a - b),
(Value::DateTime(a), Value::Int(b)) => Value::DateTime(a - b),
_ => Value::Void, _ => Value::Void,
}; };
} }
@@ -192,6 +217,7 @@ impl Environment {
let cmp_ty = StaticType::FunctionOverloads(vec![ let cmp_ty = StaticType::FunctionOverloads(vec![
Signature { params: vec![StaticType::Int, StaticType::Int], ret: StaticType::Bool }, Signature { params: vec![StaticType::Int, StaticType::Int], ret: StaticType::Bool },
Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Bool }, Signature { params: vec![StaticType::Float, StaticType::Float], ret: StaticType::Bool },
Signature { params: vec![StaticType::DateTime, StaticType::DateTime], ret: StaticType::Bool },
]); ]);
self.register_native(">", cmp_ty.clone(), |args| { self.register_native(">", cmp_ty.clone(), |args| {
if args.len() != 2 { return Value::Void; } if args.len() != 2 { return Value::Void; }
@@ -200,6 +226,7 @@ impl Environment {
(Value::Int(a), Value::Float(b)) => Value::Bool((*a as f64) > *b), (Value::Int(a), Value::Float(b)) => Value::Bool((*a as f64) > *b),
(Value::Float(a), Value::Int(b)) => Value::Bool(*a > (*b as f64)), (Value::Float(a), Value::Int(b)) => Value::Bool(*a > (*b as f64)),
(Value::Float(a), Value::Float(b)) => Value::Bool(a > b), (Value::Float(a), Value::Float(b)) => Value::Bool(a > b),
(Value::DateTime(a), Value::DateTime(b)) => Value::Bool(a > b),
_ => Value::Bool(false), _ => Value::Bool(false),
} }
}); });
@@ -211,6 +238,7 @@ impl Environment {
(Value::Int(a), Value::Float(b)) => Value::Bool((*a as f64) < *b), (Value::Int(a), Value::Float(b)) => Value::Bool((*a as f64) < *b),
(Value::Float(a), Value::Int(b)) => Value::Bool(*a < (*b as f64)), (Value::Float(a), Value::Int(b)) => Value::Bool(*a < (*b as f64)),
(Value::Float(a), Value::Float(b)) => Value::Bool(a < b), (Value::Float(a), Value::Float(b)) => Value::Bool(a < b),
(Value::DateTime(a), Value::DateTime(b)) => Value::Bool(a < b),
_ => Value::Bool(false), _ => Value::Bool(false),
} }
}); });
+11
View File
@@ -5,6 +5,7 @@ use std::fmt;
use std::sync::OnceLock; use std::sync::OnceLock;
use std::sync::Mutex; // Still needed for global keyword registry use std::sync::Mutex; // Still needed for global keyword registry
use std::any::Any; use std::any::Any;
use chrono::{TimeZone, Utc};
/// Simple source location /// Simple source location
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@@ -61,6 +62,7 @@ pub enum Value {
Bool(bool), Bool(bool),
Int(i64), Int(i64),
Float(f64), Float(f64),
DateTime(i64),
Text(Rc<str>), Text(Rc<str>),
Keyword(Keyword), Keyword(Keyword),
List(Rc<Vec<Value>>), List(Rc<Vec<Value>>),
@@ -84,6 +86,7 @@ pub enum StaticType {
Bool, Bool,
Int, Int,
Float, Float,
DateTime,
Text, Text,
Keyword, Keyword,
List(Box<StaticType>), List(Box<StaticType>),
@@ -101,6 +104,7 @@ impl fmt::Display for StaticType {
StaticType::Bool => write!(f, "bool"), StaticType::Bool => write!(f, "bool"),
StaticType::Int => write!(f, "int"), StaticType::Int => write!(f, "int"),
StaticType::Float => write!(f, "float"), StaticType::Float => write!(f, "float"),
StaticType::DateTime => write!(f, "datetime"),
StaticType::Text => write!(f, "text"), StaticType::Text => write!(f, "text"),
StaticType::Keyword => write!(f, "keyword"), StaticType::Keyword => write!(f, "keyword"),
StaticType::List(inner) => write!(f, "[{}]", inner), StaticType::List(inner) => write!(f, "[{}]", inner),
@@ -182,6 +186,7 @@ impl Value {
Value::Bool(_) => StaticType::Bool, Value::Bool(_) => StaticType::Bool,
Value::Int(_) => StaticType::Int, Value::Int(_) => StaticType::Int,
Value::Float(_) => StaticType::Float, Value::Float(_) => StaticType::Float,
Value::DateTime(_) => StaticType::DateTime,
Value::Text(_) => StaticType::Text, Value::Text(_) => StaticType::Text,
Value::Keyword(_) => StaticType::Keyword, Value::Keyword(_) => StaticType::Keyword,
Value::List(_) => StaticType::List(Box::new(StaticType::Any)), Value::List(_) => StaticType::List(Box::new(StaticType::Any)),
@@ -201,6 +206,12 @@ impl fmt::Display for Value {
Value::Bool(b) => write!(f, "{}", b), Value::Bool(b) => write!(f, "{}", b),
Value::Int(i) => write!(f, "{}", i), Value::Int(i) => write!(f, "{}", i),
Value::Float(fl) => write!(f, "{}", fl), Value::Float(fl) => write!(f, "{}", fl),
Value::DateTime(ts) => {
match Utc.timestamp_millis_opt(*ts) {
chrono::LocalResult::Single(dt) => write!(f, "#{}#", dt.format("%Y-%m-%d %H:%M:%S")),
_ => write!(f, "#timestamp({})#", ts),
}
},
Value::Text(t) => write!(f, "\"{}\"", t), Value::Text(t) => write!(f, "\"{}\"", t),
Value::Keyword(k) => write!(f, ":{}", k.name()), Value::Keyword(k) => write!(f, ":{}", k.name()),
Value::List(l) => { Value::List(l) => {