Skip to content
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
119 changes: 119 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"
clap = { version = "4", features = ["derive"] }
fastrand = "2"
regex = "1"
chrono = { version = "0.4", default-features = false, features = ["clock"] }

[dev-dependencies]
wiremock = "0.6"
Expand Down
16 changes: 16 additions & 0 deletions examples/datetime.ilo
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
-- Datetime builtins: dtfmt (epoch -> R t t), dtparse (text -> R n t).
-- UTC only. Format strings follow strftime conventions.

-- Format a unix epoch as a date string. dtfmt returns a Result because the
-- epoch may be non-finite or out of range for chrono; the enclosing fn
-- returns R so the caller can pattern-match the result.
fmtday e:n>R t t;dtfmt e "%Y-%m-%d"

-- Parse a date string back to an epoch. dtparse returns a Result, so the
-- enclosing fn must also return R for `!` to short-circuit on failure.
parseday s:t>R n t;v=dtparse! s "%Y-%m-%d";~v

-- run: fmtday 0
-- out: ~1970-01-01
-- run: fmtday 1735689600
-- out: ~2025-01-01
8 changes: 8 additions & 0 deletions src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ pub enum Builtin {
Rnd,
Rndn,
Now,
Dtfmt,
Dtparse,

// I/O
Rd,
Expand Down Expand Up @@ -202,6 +204,8 @@ impl Builtin {
"rnd" => Some(Builtin::Rnd),
"rndn" => Some(Builtin::Rndn),
"now" => Some(Builtin::Now),
"dtfmt" => Some(Builtin::Dtfmt),
"dtparse" => Some(Builtin::Dtparse),
"rd" => Some(Builtin::Rd),
"rdl" => Some(Builtin::Rdl),
"rdb" => Some(Builtin::Rdb),
Expand Down Expand Up @@ -310,6 +314,8 @@ impl Builtin {
Builtin::Rnd => "rnd",
Builtin::Rndn => "rndn",
Builtin::Now => "now",
Builtin::Dtfmt => "dtfmt",
Builtin::Dtparse => "dtparse",
Builtin::Rd => "rd",
Builtin::Rdl => "rdl",
Builtin::Rdb => "rdb",
Expand Down Expand Up @@ -460,6 +466,8 @@ mod tests {
"inv",
"det",
"rdjl",
"dtfmt",
"dtparse",
];
for name in &all {
let b = Builtin::from_name(name).unwrap_or_else(|| panic!("missing builtin: {name}"));
Expand Down
50 changes: 50 additions & 0 deletions src/interpreter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,56 @@ fn call_function(env: &mut Env, name: &str, args: Vec<Value>) -> Result<Value> {
)),
};
}
if builtin == Some(Builtin::Dtfmt) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Number(epoch), Value::Text(fmt_str)) => {
if !epoch.is_finite() {
return Ok(Value::Err(Box::new(Value::Text(format!(
"dtfmt: epoch is not finite ({epoch})"
)))));
}
if *epoch < i64::MIN as f64 || *epoch > i64::MAX as f64 {
return Ok(Value::Err(Box::new(Value::Text(format!(
"dtfmt: epoch out of range ({epoch})"
)))));
}
let secs = *epoch as i64;
match chrono::DateTime::<chrono::Utc>::from_timestamp(secs, 0) {
Some(dt) => {
let formatted = dt.format(fmt_str.as_str()).to_string();
Ok(Value::Ok(Box::new(Value::Text(formatted))))
}
None => Ok(Value::Err(Box::new(Value::Text(format!(
"dtfmt: timestamp out of range ({secs})"
))))),
}
}
_ => Err(RuntimeError::new(
"ILO-R009",
"dtfmt requires a number (epoch) and text (format)".to_string(),
)),
};
}
if builtin == Some(Builtin::Dtparse) && args.len() == 2 {
return match (&args[0], &args[1]) {
(Value::Text(text), Value::Text(fmt_str)) => {
let parsed = chrono::NaiveDateTime::parse_from_str(text, fmt_str)
.map(|ndt| ndt.and_utc().timestamp() as f64)
.or_else(|_| {
chrono::NaiveDate::parse_from_str(text, fmt_str)
.map(|nd| nd.and_hms_opt(0, 0, 0).unwrap().and_utc().timestamp() as f64)
});
match parsed {
Ok(n) => Ok(Value::Ok(Box::new(Value::Number(n)))),
Err(e) => Ok(Value::Err(Box::new(Value::Text(format!("dtparse: {e}"))))),
}
}
_ => Err(RuntimeError::new(
"ILO-R009",
"dtparse requires two text args".to_string(),
)),
};
}
if builtin == Some(Builtin::Rnd) {
if args.is_empty() {
return Ok(Value::Number(fastrand::f64()));
Expand Down
56 changes: 56 additions & 0 deletions src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ const BUILTINS: &[(&str, &[&str], &str)] = &[
("rnd", &[], "n"),
("rndn", &["n", "n"], "n"),
("now", &[], "n"),
("dtfmt", &["n", "t"], "R t t"),
("dtparse", &["t", "t"], "R n t"),
("env", &["t"], "R t t"),
("jpth", &["t", "t"], "R t t"),
("jdmp", &["any"], "t"),
Expand Down Expand Up @@ -1452,6 +1454,60 @@ fn builtin_check_args(
errors,
)
}
"dtfmt" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Number)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'dtfmt' first arg must be n (unix epoch), got {arg}"),
hint: None,
span,
is_warning: false,
});
}
if let Some(arg) = arg_types.get(1)
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'dtfmt' second arg must be t (format), got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Result(Box::new(Ty::Text), Box::new(Ty::Text)), errors)
}
"dtparse" => {
if let Some(arg) = arg_types.first()
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'dtparse' first arg must be t, got {arg}"),
hint: None,
span,
is_warning: false,
});
}
if let Some(arg) = arg_types.get(1)
&& !compatible(arg, &Ty::Text)
{
errors.push(VerifyError {
code: "ILO-T013",
function: func_ctx.to_string(),
message: format!("'dtparse' second arg must be t (format), got {arg}"),
hint: None,
span,
is_warning: false,
});
}
(Ty::Result(Box::new(Ty::Number), Box::new(Ty::Text)), errors)
}
"map" => {
// map fn:F a b xs:L a → L b
// map fn:F a c b ctx:c xs:L a → L b (closure-bind variant)
Expand Down
Loading
Loading