diff --git a/bindings/python/src/variable.rs b/bindings/python/src/variable.rs index ffae4536..a12e649e 100644 --- a/bindings/python/src/variable.rs +++ b/bindings/python/src/variable.rs @@ -2,7 +2,7 @@ use anyhow::Context; use pyo3::prelude::{PyAnyMethods, PyBytesMethods, PyDictMethods, PyListMethods, PyStringMethods}; use pyo3::types::{PyBytes, PyDict, PyList, PyString}; use pyo3::{Bound, FromPyObject, IntoPyObject, IntoPyObjectExt, PyAny, PyErr, PyResult, Python}; -use pythonize::depythonize; +use pythonize::{depythonize, pythonize}; use rust_decimal::prelude::ToPrimitive; use zen_expression::Variable; @@ -40,11 +40,12 @@ pub fn variable_to_object<'py>(py: Python<'py>, val: &Variable) -> PyResult Ok(pythonize(py, &d.to_value())?), } } diff --git a/core/engine/src/handler/function/module/http.rs b/core/engine/src/handler/function/module/http.rs index b611cdce..7ff49dd8 100644 --- a/core/engine/src/handler/function/module/http.rs +++ b/core/engine/src/handler/function/module/http.rs @@ -91,12 +91,13 @@ impl<'js> FromJs<'js> for HttpConfig { let value = JsValue::from_js(ctx, value)?; let str_value = match value.0 { - Variable::Null => None, Variable::Bool(b) => Some(b.to_string()), Variable::Number(n) => Some(n.to_string()), Variable::String(s) => Some(s.to_string()), + Variable::Null => None, Variable::Array(_) => None, Variable::Object(_) => None, + Variable::Dynamic(_) => None, }; let key_value = key.to_string()?; @@ -121,12 +122,13 @@ impl<'js> FromJs<'js> for HttpConfig { let value = JsValue::from_js(ctx, value)?; let str_value = match value.0 { - Variable::Null => None, Variable::Bool(b) => Some(b.to_string()), Variable::Number(n) => Some(n.to_string()), Variable::String(s) => Some(s.to_string()), + Variable::Null => None, Variable::Array(_) => None, Variable::Object(_) => None, + Variable::Dynamic(_) => None, }; let key = key.to_string()?; diff --git a/core/engine/src/handler/function/serde.rs b/core/engine/src/handler/function/serde.rs index 1799167a..97970ba7 100644 --- a/core/engine/src/handler/function/serde.rs +++ b/core/engine/src/handler/function/serde.rs @@ -125,6 +125,7 @@ impl<'js> IntoJs<'js> for JsValue { qmap.into_value() } + Variable::Dynamic(d) => d.to_string().into_js(ctx)?, }; Ok(res) diff --git a/core/expression/src/vm/date/mod.rs b/core/expression/src/vm/date/mod.rs index b0ab5b23..b331d23f 100644 --- a/core/expression/src/vm/date/mod.rs +++ b/core/expression/src/vm/date/mod.rs @@ -359,8 +359,8 @@ mod helper { }; if let Some(unit_secs) = unit.as_secs() { - let secs = (a.timestamp_millis() - b.timestamp_millis()) / 1_000; - return secs.checked_mul(unit_secs.to_i64()?); + let secs = a.timestamp() - b.timestamp(); + return secs.checked_div(unit_secs.to_i64()?); } let year_diff = a.year().to_i64()?.checked_sub(b.year().to_i64()?)?; diff --git a/core/expression/tests/data/date.csv b/core/expression/tests/data/date.csv new file mode 100644 index 00000000..ed993098 --- /dev/null +++ b/core/expression/tests/data/date.csv @@ -0,0 +1,115 @@ +expression (string);input (json 5);output (json 5) + +# Date function basics +d('2023-10-15');;'2023-10-15T00:00:00Z' +d('2023-10-15 14:30');;'2023-10-15T14:30:00Z' +d('2023-10-15 14:30:45');;'2023-10-15T14:30:45Z' +d('2023-10-15', 'Europe/Berlin');;'2023-10-15T00:00:00+02:00' +d('2023-10-15 14:30', 'Europe/Berlin');;'2023-10-15T14:30:00+02:00' +d('2023-10-15 14:30:45', 'Europe/Berlin');;'2023-10-15T14:30:45+02:00' +d('Europe/Berlin').isValid() and d('Europe/Berlin').isToday();;true + +# Date manipulation +d('2023-10-15').add('1d');;'2023-10-16T00:00:00Z' +d('2023-10-15').add('1d 5h');;'2023-10-16T05:00:00Z' +d('2023-10-15').add(1, 'd');;'2023-10-16T00:00:00Z' +d('2023-10-15').sub('2d');;'2023-10-13T00:00:00Z' +d('2023-10-15').sub(1, 'M');;'2023-09-15T00:00:00Z' + +# Date comparisons +d('2023-10-15').isBefore(d('2023-10-16'));;true +d('2023-10-15').isBefore(d('2023-10-15'));;false +d('2023-10-15').isAfter(d('2023-10-14'));;true +d('2023-10-15').isAfter(d('2023-10-15'));;false +d('2023-10-15').isSame(d('2023-10-15'));;true +d('2023-10-15').isSame(d('2023-10-16'));;false +d('2023-10-15').isSameOrBefore(d('2023-10-15'));;true +d('2023-10-15').isSameOrBefore(d('2023-10-16'));;true +d('2023-10-15').isSameOrBefore(d('2023-10-14'));;false +d('2023-10-15').isSameOrAfter(d('2023-10-15'));;true +d('2023-10-15').isSameOrAfter(d('2023-10-14'));;true +d('2023-10-15').isSameOrAfter(d('2023-10-16'));;false + +# Date getters +d('2023-10-15').year();;2023 +d('2023-10-15').month();;10 +d('2023-10-15').day();;15 +d('2023-10-15').weekday();;0 +d('2023-10-15').hour();;0 +d('2023-10-15').minute();;0 +d('2023-10-15').second();;0 +d('2023-10-15').dayOfYear();;288 +d('2023-01-01').dayOfYear();;1 +d('2023-12-31').dayOfYear();;365 +d('2024-12-31').dayOfYear();;366 +d('2023-10-15').quarter();;4 +d('2023-01-15').quarter();;1 +d('2023-04-15').quarter();;2 +d('2023-07-15').quarter();;3 +d('2023-10-15').timestamp();;1697328000000 +d('2023-10-15', 'Europe/Berlin').offsetName();;'Europe/Berlin' +d('2023-10-15', 'America/Los_Angeles').offsetName();;'America/Los_Angeles' +d('2023-10-15').isLeapYear();;false +d('2024-10-15').isLeapYear();;true +d('2000-10-15').isLeapYear();;true +d('1900-10-15').isLeapYear();;false + +# Date setters +d('2023-10-15').set(2024, 'year');;'2024-10-15T00:00:00Z' +d('2023-10-15').set(5, 'month');;'2023-05-15T00:00:00Z' +d('2023-10-15').set(20, 'day');;'2023-10-20T00:00:00Z' +d('2023-10-15T10:30:00Z').set(15, 'hour');;'2023-10-15T15:30:00Z' +d('2023-10-15T10:30:00Z').set(45, 'minute');;'2023-10-15T10:45:00Z' +d('2023-10-15T10:30:00Z').set(30, 'second');;'2023-10-15T10:30:30Z' + +# Special date checks +d().isSame(d(), 'day');;true +d('2023-10-15').startOf('day');;'2023-10-15T00:00:00Z' +d('2023-10-15T10:30:45Z').startOf('hour');;'2023-10-15T10:00:00Z' +d('2023-10-15').endOf('day');;'2023-10-15T23:59:59Z' +d('2023-10-15T10:30:45Z').endOf('hour');;'2023-10-15T10:59:59Z' +d('2023-10-15').startOf('month');;'2023-10-01T00:00:00Z' +d('2023-10-15').endOf('month');;'2023-10-31T23:59:59Z' +d('2023-10-15').startOf('year');;'2023-01-01T00:00:00Z' +d('2023-10-15').endOf('year');;'2023-12-31T23:59:59Z' +d('2023-10-15').startOf('week');;'2023-10-09T00:00:00Z' +d('2023-10-15').endOf('week');;'2023-10-15T23:59:59Z' +d('2023-10-15').startOf('quarter');;'2023-10-01T00:00:00Z' +d('2023-10-15').endOf('quarter');;'2023-12-31T23:59:59Z' +d('2023-03-15').startOf('quarter');;'2023-01-01T00:00:00Z' +d('2023-03-15').endOf('quarter');;'2023-03-31T23:59:59Z' +d('2023-06-15').startOf('quarter');;'2023-04-01T00:00:00Z' +d('2023-06-15').endOf('quarter');;'2023-06-30T23:59:59Z' +d('2023-09-15').startOf('quarter');;'2023-07-01T00:00:00Z' +d('2023-09-15').endOf('quarter');;'2023-09-30T23:59:59Z' + +# Timezone operations +d('2023-10-15T00:00:00Z').tz('America/New_York');;'2023-10-14T20:00:00-04:00' +d('2023-10-15T00:00:00Z').tz('Europe/London');;'2023-10-15T01:00:00+01:00' +d('2023-10-15T00:00:00+00:00').tz('UTC');;'2023-10-15T00:00:00Z' +d('2023-10-15T12:00:00+02:00', 'Etc/GMT-2').tz('UTC');;'2023-10-15T10:00:00Z' + +# Relative date methods +d().sub(1, 'd').isYesterday();;true +d().add(1, 'd').isTomorrow();;true +d('2023-10-15').isToday();;false + +# Difference calculations +d('2023-10-15').diff(d('2023-10-10'), 'day');;5 +d('2023-10-15T10:00:00Z').diff(d('2023-10-15T08:30:00Z'), 'hour');;1 +d('2023-10-15').diff(d('2023-09-15'), 'month');;1 +d('2023-12-31').diff(d('2023-01-01'), 'year');;0 +d('2023-12-31').diff(d('2022-01-01'), 'year');;1 + +# Formatting +d('2023-10-15').format('%Y-%m-%d');;'2023-10-15' +d('2023-10-15').format('%Y/%m/%d');;'2023/10/15' +d('2023-10-15T14:30:45Z').format('%A, %B %d %Y, %H:%M:%S');;'Sunday, October 15 2023, 14:30:45' +d('2023-10-15T14:30:45Z').format('%a %b %d %H:%M');;'Sun Oct 15 14:30' +d('2023-10-15T14:30:45Z').format('Day %j of year %Y');;'Day 288 of year 2023' +d('2023-10-15T14:30:45.123Z').format('%H:%M:%S.%f');;'14:30:45.123000000' + +# Date validations +d('foo').isValid();;false +d('2023-13-01').isValid();;false +d('2023-02-30').isValid();;false \ No newline at end of file diff --git a/core/expression/tests/isolate.rs b/core/expression/tests/isolate.rs index 5299ae7c..82a029e8 100644 --- a/core/expression/tests/isolate.rs +++ b/core/expression/tests/isolate.rs @@ -1,6 +1,3 @@ -use std::ops::Index; - -use anyhow::Context; use serde_json::{json, Value}; use zen_expression::variable::Variable; @@ -754,86 +751,106 @@ fn isolate_unary_tests() { } } -#[test] -fn isolate_test_decimals() { - let mut isolate = Isolate::new(); - let result = isolate.run_standard("9223372036854775807").unwrap(); +#[cfg(test)] +mod test { + use anyhow::Context; + use serde_json::Value; + use std::env; + use std::ops::Index; + use zen_expression::Isolate; - assert_eq!(result.to_value(), Value::from(9223372036854775807i64)); -} + fn test_csv_standard(csv_data: &str) { + let mut r = csv::ReaderBuilder::new() + .delimiter(b';') + .from_reader(csv_data.as_bytes()); -#[test] -fn test_standard_csv() { - let csv_data = include_str!("data/standard.csv"); - let mut r = csv::ReaderBuilder::new() - .delimiter(b';') - .from_reader(csv_data.as_bytes()); + while let Some(maybe_row) = r.records().next() { + let Ok(row) = maybe_row else { + continue; + }; - while let Some(maybe_row) = r.records().next() { - let Ok(row) = maybe_row else { - continue; - }; + let (expression, input_str, output_str) = (row.index(0), row.index(1), row.index(2)); + if expression.starts_with("#") { + continue; + } - let (expression, input_str, output_str) = (row.index(0), row.index(1), row.index(2)); - if expression.starts_with("#") { - continue; + let output: Value = serde_json5::from_str(output_str).unwrap(); + + let mut isolate = Isolate::new(); + if !input_str.is_empty() { + let input: Value = serde_json5::from_str(input_str).unwrap(); + isolate.set_environment(input.into()); + } + + let maybe_result = isolate + .run_standard(expression) + .context(format!("Expression: {expression}")); + assert!(maybe_result.is_ok(), "{}", maybe_result.unwrap_err()); + + let result = maybe_result.unwrap().to_value(); + assert_eq!( + result, output, + "Expression {expression}. Expected: {output}, got: {result}" + ); } + } - let output: Value = serde_json5::from_str(output_str).unwrap(); - + #[test] + fn isolate_test_decimals() { let mut isolate = Isolate::new(); - if !input_str.is_empty() { - let input: Value = serde_json5::from_str(input_str).unwrap(); - isolate.set_environment(input.into()); + let result = isolate.run_standard("9223372036854775807").unwrap(); + + assert_eq!(result.to_value(), Value::from(9223372036854775807i64)); + } + + #[test] + fn test_standard_csv() { + let csv_data = include_str!("data/standard.csv"); + test_csv_standard(csv_data); + } + + #[test] + fn test_dates_csv() { + env::set_var("TZ", "UTC"); + + let csv_data = include_str!("data/date.csv"); + test_csv_standard(csv_data); + } + + #[test] + fn test_unary_csv() { + let csv_data = include_str!("data/unary.csv"); + let mut r = csv::ReaderBuilder::new() + .delimiter(b';') + .from_reader(csv_data.as_bytes()); + + while let Some(maybe_row) = r.records().next() { + let Ok(row) = maybe_row else { + continue; + }; + + let (expression, input_str, output_str) = (row.index(0), row.index(1), row.index(2)); + if expression.starts_with("#") { + continue; + } + + let output: Value = serde_json5::from_str(output_str).unwrap(); + + let mut isolate = Isolate::new(); + if !input_str.is_empty() { + let input: Value = serde_json5::from_str(input_str).unwrap(); + isolate.set_environment(input.into()); + } + + let result = isolate + .run_unary(expression) + .context(format!("Expression: {expression}")) + .unwrap(); + + assert_eq!( + result, output, + "Expression {expression}. Expected: {output}, got: {result}" + ); } - - let maybe_result = isolate - .run_standard(expression) - .context(format!("Expression: {expression}")); - assert!(maybe_result.is_ok(), "{}", maybe_result.unwrap_err()); - - let result = maybe_result.unwrap(); - let var_output = Variable::from(output); - assert_eq!( - result, var_output, - "Expression {expression}. Expected: {var_output}, got: {result}" - ); - } -} - -#[test] -fn test_unary_csv() { - let csv_data = include_str!("data/unary.csv"); - let mut r = csv::ReaderBuilder::new() - .delimiter(b';') - .from_reader(csv_data.as_bytes()); - - while let Some(maybe_row) = r.records().next() { - let Ok(row) = maybe_row else { - continue; - }; - - let (expression, input_str, output_str) = (row.index(0), row.index(1), row.index(2)); - if expression.starts_with("#") { - continue; - } - - let output: Value = serde_json5::from_str(output_str).unwrap(); - - let mut isolate = Isolate::new(); - if !input_str.is_empty() { - let input: Value = serde_json5::from_str(input_str).unwrap(); - isolate.set_environment(input.into()); - } - - let result = isolate - .run_unary(expression) - .context(format!("Expression: {expression}")) - .unwrap(); - - assert_eq!( - result, output, - "Expression {expression}. Expected: {output}, got: {result}" - ); } }