use std::collections::HashSet;
use std::rc::Rc;
use chrono::{DateTime, Datelike, NaiveDateTime, TimeDelta, TimeZone, Utc, Weekday};
use chrono_tz::Tz;
use crate::common::{Function, custom_error};
use crate::register_if_enabled;
use crate::{ArgumentType, Context, JmespathError, Rcvar, Runtime, Variable, define_function};
pub fn register(runtime: &mut Runtime) {
runtime.register_function("now", Box::new(NowFn::new()));
runtime.register_function("now_millis", Box::new(NowMillisFn::new()));
runtime.register_function("parse_date", Box::new(ParseDateFn::new()));
runtime.register_function("format_date", Box::new(FormatDateFn::new()));
runtime.register_function("date_add", Box::new(DateAddFn::new()));
runtime.register_function("date_diff", Box::new(DateDiffFn::new()));
runtime.register_function("timezone_convert", Box::new(TimezoneConvertFn::new()));
runtime.register_function("is_weekend", Box::new(IsWeekendFn::new()));
runtime.register_function("is_weekday", Box::new(IsWeekdayFn::new()));
runtime.register_function(
"business_days_between",
Box::new(BusinessDaysBetweenFn::new()),
);
runtime.register_function("relative_time", Box::new(RelativeTimeFn::new()));
runtime.register_function("quarter", Box::new(QuarterFn::new()));
runtime.register_function("is_after", Box::new(IsAfterFn::new()));
runtime.register_function("is_before", Box::new(IsBeforeFn::new()));
runtime.register_function("is_between", Box::new(IsBetweenFn::new()));
runtime.register_function("time_ago", Box::new(TimeAgoFn::new()));
runtime.register_function("from_epoch", Box::new(FromEpochFn::new()));
runtime.register_function("from_epoch_ms", Box::new(FromEpochMsFn::new()));
runtime.register_function("to_epoch", Box::new(ToEpochFn::new()));
runtime.register_function("to_epoch_ms", Box::new(ToEpochMsFn::new()));
runtime.register_function("duration_since", Box::new(DurationSinceFn::new()));
runtime.register_function("start_of_day", Box::new(StartOfDayFn::new()));
runtime.register_function("end_of_day", Box::new(EndOfDayFn::new()));
runtime.register_function("start_of_week", Box::new(StartOfWeekFn::new()));
runtime.register_function("start_of_month", Box::new(StartOfMonthFn::new()));
runtime.register_function("start_of_year", Box::new(StartOfYearFn::new()));
runtime.register_function("is_same_day", Box::new(IsSameDayFn::new()));
runtime.register_function("epoch_ms", Box::new(NowMillisFn::new()));
runtime.register_function("parse_datetime", Box::new(ParseDatetimeFn::new()));
runtime.register_function("parse_natural_date", Box::new(ParseNaturalDateFn::new()));
}
pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
register_if_enabled!(runtime, enabled, "now", Box::new(NowFn::new()));
register_if_enabled!(runtime, enabled, "now_millis", Box::new(NowMillisFn::new()));
register_if_enabled!(runtime, enabled, "parse_date", Box::new(ParseDateFn::new()));
register_if_enabled!(
runtime,
enabled,
"format_date",
Box::new(FormatDateFn::new())
);
register_if_enabled!(runtime, enabled, "date_add", Box::new(DateAddFn::new()));
register_if_enabled!(runtime, enabled, "date_diff", Box::new(DateDiffFn::new()));
register_if_enabled!(
runtime,
enabled,
"timezone_convert",
Box::new(TimezoneConvertFn::new())
);
register_if_enabled!(runtime, enabled, "is_weekend", Box::new(IsWeekendFn::new()));
register_if_enabled!(runtime, enabled, "is_weekday", Box::new(IsWeekdayFn::new()));
register_if_enabled!(
runtime,
enabled,
"business_days_between",
Box::new(BusinessDaysBetweenFn::new())
);
register_if_enabled!(
runtime,
enabled,
"relative_time",
Box::new(RelativeTimeFn::new())
);
register_if_enabled!(runtime, enabled, "quarter", Box::new(QuarterFn::new()));
register_if_enabled!(runtime, enabled, "is_after", Box::new(IsAfterFn::new()));
register_if_enabled!(runtime, enabled, "is_before", Box::new(IsBeforeFn::new()));
register_if_enabled!(runtime, enabled, "is_between", Box::new(IsBetweenFn::new()));
register_if_enabled!(runtime, enabled, "time_ago", Box::new(TimeAgoFn::new()));
register_if_enabled!(runtime, enabled, "from_epoch", Box::new(FromEpochFn::new()));
register_if_enabled!(
runtime,
enabled,
"from_epoch_ms",
Box::new(FromEpochMsFn::new())
);
register_if_enabled!(runtime, enabled, "to_epoch", Box::new(ToEpochFn::new()));
register_if_enabled!(
runtime,
enabled,
"to_epoch_ms",
Box::new(ToEpochMsFn::new())
);
register_if_enabled!(
runtime,
enabled,
"duration_since",
Box::new(DurationSinceFn::new())
);
register_if_enabled!(
runtime,
enabled,
"start_of_day",
Box::new(StartOfDayFn::new())
);
register_if_enabled!(runtime, enabled, "end_of_day", Box::new(EndOfDayFn::new()));
register_if_enabled!(
runtime,
enabled,
"start_of_week",
Box::new(StartOfWeekFn::new())
);
register_if_enabled!(
runtime,
enabled,
"start_of_month",
Box::new(StartOfMonthFn::new())
);
register_if_enabled!(
runtime,
enabled,
"start_of_year",
Box::new(StartOfYearFn::new())
);
register_if_enabled!(
runtime,
enabled,
"is_same_day",
Box::new(IsSameDayFn::new())
);
register_if_enabled!(runtime, enabled, "epoch_ms", Box::new(NowMillisFn::new()));
register_if_enabled!(
runtime,
enabled,
"parse_datetime",
Box::new(ParseDatetimeFn::new())
);
register_if_enabled!(
runtime,
enabled,
"parse_natural_date",
Box::new(ParseNaturalDateFn::new())
);
}
define_function!(NowFn, vec![], None);
impl Function for NowFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = Utc::now().timestamp();
Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(ts as f64).unwrap(),
)))
}
}
define_function!(NowMillisFn, vec![], None);
impl Function for NowMillisFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = Utc::now().timestamp_millis();
Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(ts as f64).unwrap(),
)))
}
}
define_function!(
ParseDateFn,
vec![ArgumentType::String],
Some(ArgumentType::String)
);
impl Function for ParseDateFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().unwrap();
if args.len() > 1 {
let format = args[1].as_string().unwrap();
match NaiveDateTime::parse_from_str(s, format) {
Ok(dt) => Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(dt.and_utc().timestamp() as f64).unwrap(),
))),
Err(_) => Ok(Rc::new(Variable::Null)),
}
} else {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(dt.timestamp() as f64).unwrap(),
)));
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
return Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(dt.and_utc().timestamp() as f64).unwrap(),
)));
}
if let Ok(dt) =
NaiveDateTime::parse_from_str(&format!("{}T00:00:00", s), "%Y-%m-%dT%H:%M:%S")
{
return Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(dt.and_utc().timestamp() as f64).unwrap(),
)));
}
Ok(Rc::new(Variable::Null))
}
}
}
define_function!(
FormatDateFn,
vec![ArgumentType::Number, ArgumentType::String],
None
);
impl Function for FormatDateFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = args[0].as_number().unwrap();
let format = args[1].as_string().unwrap();
let dt = Utc.timestamp_opt(ts as i64, 0);
match dt {
chrono::LocalResult::Single(dt) => {
Ok(Rc::new(Variable::String(dt.format(format).to_string())))
}
_ => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(
DateAddFn,
vec![
ArgumentType::Number,
ArgumentType::Number,
ArgumentType::String
],
None
);
impl Function for DateAddFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = args[0].as_number().unwrap();
let amount = args[1].as_number().unwrap();
let unit = args[2].as_string().unwrap();
let duration = match unit.to_lowercase().as_str() {
"seconds" | "second" | "s" => TimeDelta::seconds(amount as i64),
"minutes" | "minute" | "m" => TimeDelta::minutes(amount as i64),
"hours" | "hour" | "h" => TimeDelta::hours(amount as i64),
"days" | "day" | "d" => TimeDelta::days(amount as i64),
"weeks" | "week" | "w" => TimeDelta::weeks(amount as i64),
_ => return Err(custom_error(ctx, &format!("invalid time unit: {}", unit))),
};
let dt = Utc.timestamp_opt(ts as i64, 0);
match dt {
chrono::LocalResult::Single(dt) => {
let new_dt = dt + duration;
Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(new_dt.timestamp() as f64).unwrap(),
)))
}
_ => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(
DateDiffFn,
vec![
ArgumentType::Number,
ArgumentType::Number,
ArgumentType::String
],
None
);
impl Function for DateDiffFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts1 = args[0].as_number().unwrap();
let ts2 = args[1].as_number().unwrap();
let unit = args[2].as_string().unwrap();
let diff_seconds = (ts1 - ts2) as i64;
let result = match unit.to_lowercase().as_str() {
"seconds" | "second" | "s" => diff_seconds as f64,
"minutes" | "minute" | "m" => diff_seconds as f64 / 60.0,
"hours" | "hour" | "h" => diff_seconds as f64 / 3600.0,
"days" | "day" | "d" => diff_seconds as f64 / 86400.0,
"weeks" | "week" | "w" => diff_seconds as f64 / 604800.0,
_ => return Err(custom_error(ctx, &format!("invalid time unit: {}", unit))),
};
Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(result).unwrap(),
)))
}
}
define_function!(
TimezoneConvertFn,
vec![
ArgumentType::String,
ArgumentType::String,
ArgumentType::String
],
None
);
impl Function for TimezoneConvertFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let timestamp_str = args[0].as_string().unwrap();
let from_tz_str = args[1].as_string().unwrap();
let to_tz_str = args[2].as_string().unwrap();
let from_tz: Tz = from_tz_str
.parse()
.map_err(|_| custom_error(ctx, &format!("invalid timezone: {}", from_tz_str)))?;
let to_tz: Tz = to_tz_str
.parse()
.map_err(|_| custom_error(ctx, &format!("invalid timezone: {}", to_tz_str)))?;
let naive_dt =
if let Ok(dt) = NaiveDateTime::parse_from_str(timestamp_str, "%Y-%m-%dT%H:%M:%S") {
dt
} else if let Ok(dt) = NaiveDateTime::parse_from_str(
&format!("{}T00:00:00", timestamp_str),
"%Y-%m-%dT%H:%M:%S",
) {
dt
} else {
return Err(custom_error(
ctx,
&format!("invalid timestamp format: {}", timestamp_str),
));
};
let from_dt = from_tz
.from_local_datetime(&naive_dt)
.single()
.ok_or_else(|| custom_error(ctx, "ambiguous or invalid local time"))?;
let to_dt = from_dt.with_timezone(&to_tz);
Ok(Rc::new(Variable::String(
to_dt.format("%Y-%m-%dT%H:%M:%S").to_string(),
)))
}
}
define_function!(IsWeekendFn, vec![ArgumentType::Number], None);
impl Function for IsWeekendFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = args[0].as_number().unwrap();
let dt = Utc.timestamp_opt(ts as i64, 0);
match dt {
chrono::LocalResult::Single(dt) => {
let weekday = dt.weekday();
let is_weekend = weekday == Weekday::Sat || weekday == Weekday::Sun;
Ok(Rc::new(Variable::Bool(is_weekend)))
}
_ => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(IsWeekdayFn, vec![ArgumentType::Number], None);
impl Function for IsWeekdayFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = args[0].as_number().unwrap();
let dt = Utc.timestamp_opt(ts as i64, 0);
match dt {
chrono::LocalResult::Single(dt) => {
let weekday = dt.weekday();
let is_weekday = weekday != Weekday::Sat && weekday != Weekday::Sun;
Ok(Rc::new(Variable::Bool(is_weekday)))
}
_ => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(
BusinessDaysBetweenFn,
vec![ArgumentType::Number, ArgumentType::Number],
None
);
impl Function for BusinessDaysBetweenFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts1 = args[0].as_number().unwrap() as i64;
let ts2 = args[1].as_number().unwrap() as i64;
let dt1 = match Utc.timestamp_opt(ts1, 0) {
chrono::LocalResult::Single(dt) => dt,
_ => return Ok(Rc::new(Variable::Null)),
};
let dt2 = match Utc.timestamp_opt(ts2, 0) {
chrono::LocalResult::Single(dt) => dt,
_ => return Ok(Rc::new(Variable::Null)),
};
let (start, end) = if dt1 <= dt2 {
(dt1.date_naive(), dt2.date_naive())
} else {
(dt2.date_naive(), dt1.date_naive())
};
let mut count = 0i64;
let mut current = start;
while current < end {
let weekday = current.weekday();
if weekday != Weekday::Sat && weekday != Weekday::Sun {
count += 1;
}
current = current.succ_opt().unwrap_or(current);
}
let result = if ts1 > ts2 { -count } else { count };
Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(result as f64).unwrap(),
)))
}
}
define_function!(RelativeTimeFn, vec![ArgumentType::Number], None);
impl Function for RelativeTimeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = args[0].as_number().unwrap() as i64;
let now = Utc::now().timestamp();
let diff = ts - now;
let (abs_diff, is_future) = if diff >= 0 {
(diff, true)
} else {
(-diff, false)
};
let (value, unit_singular, unit_plural) = if abs_diff < 60 {
(abs_diff, "second", "seconds")
} else if abs_diff < 3600 {
(abs_diff / 60, "minute", "minutes")
} else if abs_diff < 86400 {
(abs_diff / 3600, "hour", "hours")
} else if abs_diff < 2592000 {
(abs_diff / 86400, "day", "days")
} else if abs_diff < 31536000 {
(abs_diff / 2592000, "month", "months")
} else {
(abs_diff / 31536000, "year", "years")
};
let unit = if value == 1 {
unit_singular
} else {
unit_plural
};
let result = if is_future {
format!("in {} {}", value, unit)
} else {
format!("{} {} ago", value, unit)
};
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(QuarterFn, vec![ArgumentType::Number], None);
impl Function for QuarterFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = args[0].as_number().unwrap();
let dt = Utc.timestamp_opt(ts as i64, 0);
match dt {
chrono::LocalResult::Single(dt) => {
let month = dt.month();
let quarter = ((month - 1) / 3) + 1;
Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(quarter as f64).unwrap(),
)))
}
_ => Ok(Rc::new(Variable::Null)),
}
}
}
fn parse_date_value(value: &Variable) -> Option<i64> {
match value {
Variable::Number(n) => n.as_f64().map(|f| f as i64),
Variable::String(s) => {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(dt.timestamp());
}
if let Ok(dt) = NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
return Some(dt.and_utc().timestamp());
}
if let Ok(dt) =
NaiveDateTime::parse_from_str(&format!("{}T00:00:00", s), "%Y-%m-%dT%H:%M:%S")
{
return Some(dt.and_utc().timestamp());
}
None
}
_ => None,
}
}
define_function!(IsAfterFn, vec![ArgumentType::Any, ArgumentType::Any], None);
impl Function for IsAfterFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts1 = parse_date_value(&args[0]);
let ts2 = parse_date_value(&args[1]);
match (ts1, ts2) {
(Some(t1), Some(t2)) => Ok(Rc::new(Variable::Bool(t1 > t2))),
_ => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(IsBeforeFn, vec![ArgumentType::Any, ArgumentType::Any], None);
impl Function for IsBeforeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts1 = parse_date_value(&args[0]);
let ts2 = parse_date_value(&args[1]);
match (ts1, ts2) {
(Some(t1), Some(t2)) => Ok(Rc::new(Variable::Bool(t1 < t2))),
_ => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(
IsBetweenFn,
vec![ArgumentType::Any, ArgumentType::Any, ArgumentType::Any],
None
);
impl Function for IsBetweenFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = parse_date_value(&args[0]);
let start = parse_date_value(&args[1]);
let end = parse_date_value(&args[2]);
match (ts, start, end) {
(Some(t), Some(s), Some(e)) => Ok(Rc::new(Variable::Bool(t >= s && t <= e))),
_ => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(TimeAgoFn, vec![ArgumentType::Any], None);
impl Function for TimeAgoFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = match parse_date_value(&args[0]) {
Some(t) => t,
None => return Ok(Rc::new(Variable::Null)),
};
let now = Utc::now().timestamp();
let diff = now - ts;
let abs_diff = diff.abs();
let (value, unit_singular, unit_plural) = if abs_diff < 60 {
(abs_diff, "second", "seconds")
} else if abs_diff < 3600 {
(abs_diff / 60, "minute", "minutes")
} else if abs_diff < 86400 {
(abs_diff / 3600, "hour", "hours")
} else if abs_diff < 2592000 {
(abs_diff / 86400, "day", "days")
} else if abs_diff < 31536000 {
(abs_diff / 2592000, "month", "months")
} else {
(abs_diff / 31536000, "year", "years")
};
let unit = if value == 1 {
unit_singular
} else {
unit_plural
};
let result = if diff < 0 {
format!("in {} {}", value, unit)
} else {
format!("{} {} ago", value, unit)
};
Ok(Rc::new(Variable::String(result)))
}
}
define_function!(FromEpochFn, vec![ArgumentType::Number], None);
impl Function for FromEpochFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let epoch = args[0].as_number().unwrap() as i64;
match DateTime::from_timestamp(epoch, 0) {
Some(dt) => Ok(Rc::new(Variable::String(
dt.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
))),
None => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(FromEpochMsFn, vec![ArgumentType::Number], None);
impl Function for FromEpochMsFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let epoch_ms = args[0].as_number().unwrap() as i64;
let seconds = epoch_ms / 1000;
let nanos = ((epoch_ms % 1000) * 1_000_000) as u32;
match DateTime::from_timestamp(seconds, nanos) {
Some(dt) => Ok(Rc::new(Variable::String(
dt.format("%Y-%m-%dT%H:%M:%S%.3fZ").to_string(),
))),
None => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(ToEpochFn, vec![ArgumentType::Any], None);
impl Function for ToEpochFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
match parse_date_value(&args[0]) {
Some(ts) => Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(ts as f64).unwrap(),
))),
None => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(ToEpochMsFn, vec![ArgumentType::Any], None);
impl Function for ToEpochMsFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
match parse_date_value(&args[0]) {
Some(ts) => {
let ts_ms = ts * 1000;
Ok(Rc::new(Variable::Number(
serde_json::Number::from_f64(ts_ms as f64).unwrap(),
)))
}
None => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(DurationSinceFn, vec![ArgumentType::Any], None);
impl Function for DurationSinceFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = match parse_date_value(&args[0]) {
Some(t) => t,
None => return Ok(Rc::new(Variable::Null)),
};
let now = Utc::now().timestamp();
let diff = now - ts;
let is_future = diff < 0;
let abs_diff = diff.abs();
let days = abs_diff / 86400;
let hours = (abs_diff % 86400) / 3600;
let minutes = (abs_diff % 3600) / 60;
let seconds = abs_diff % 60;
let human = if days > 0 {
if days == 1 {
"1 day".to_string()
} else {
format!("{} days", days)
}
} else if hours > 0 {
if hours == 1 {
"1 hour".to_string()
} else {
format!("{} hours", hours)
}
} else if minutes > 0 {
if minutes == 1 {
"1 minute".to_string()
} else {
format!("{} minutes", minutes)
}
} else if seconds == 1 {
"1 second".to_string()
} else {
format!("{} seconds", seconds)
};
let human_with_direction = if is_future {
format!("in {}", human)
} else {
format!("{} ago", human)
};
let mut map = serde_json::Map::new();
map.insert(
"seconds".to_string(),
serde_json::Value::Number(serde_json::Number::from(abs_diff)),
);
map.insert(
"minutes".to_string(),
serde_json::Value::Number(serde_json::Number::from(abs_diff / 60)),
);
map.insert(
"hours".to_string(),
serde_json::Value::Number(serde_json::Number::from(abs_diff / 3600)),
);
map.insert(
"days".to_string(),
serde_json::Value::Number(serde_json::Number::from(abs_diff / 86400)),
);
map.insert("is_future".to_string(), serde_json::Value::Bool(is_future));
map.insert(
"human".to_string(),
serde_json::Value::String(human_with_direction),
);
Ok(Rc::new(
Variable::from_json(&serde_json::to_string(&map).unwrap()).unwrap(),
))
}
}
define_function!(StartOfDayFn, vec![ArgumentType::Any], None);
impl Function for StartOfDayFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = match parse_date_value(&args[0]) {
Some(t) => t,
None => return Ok(Rc::new(Variable::Null)),
};
let dt = DateTime::from_timestamp(ts, 0).unwrap();
let start = dt.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc();
Ok(Rc::new(Variable::String(
start.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
)))
}
}
define_function!(EndOfDayFn, vec![ArgumentType::Any], None);
impl Function for EndOfDayFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = match parse_date_value(&args[0]) {
Some(t) => t,
None => return Ok(Rc::new(Variable::Null)),
};
let dt = DateTime::from_timestamp(ts, 0).unwrap();
let end = dt.date_naive().and_hms_opt(23, 59, 59).unwrap().and_utc();
Ok(Rc::new(Variable::String(
end.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
)))
}
}
define_function!(StartOfWeekFn, vec![ArgumentType::Any], None);
impl Function for StartOfWeekFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = match parse_date_value(&args[0]) {
Some(t) => t,
None => return Ok(Rc::new(Variable::Null)),
};
let dt = DateTime::from_timestamp(ts, 0).unwrap();
let days_since_monday = dt.weekday().num_days_from_monday();
let monday = dt.date_naive() - chrono::Duration::days(days_since_monday as i64);
let start = monday.and_hms_opt(0, 0, 0).unwrap().and_utc();
Ok(Rc::new(Variable::String(
start.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
)))
}
}
define_function!(StartOfMonthFn, vec![ArgumentType::Any], None);
impl Function for StartOfMonthFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = match parse_date_value(&args[0]) {
Some(t) => t,
None => return Ok(Rc::new(Variable::Null)),
};
let dt = DateTime::from_timestamp(ts, 0).unwrap();
let start = dt
.date_naive()
.with_day(1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
.and_utc();
Ok(Rc::new(Variable::String(
start.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
)))
}
}
define_function!(StartOfYearFn, vec![ArgumentType::Any], None);
impl Function for StartOfYearFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts = match parse_date_value(&args[0]) {
Some(t) => t,
None => return Ok(Rc::new(Variable::Null)),
};
let dt = DateTime::from_timestamp(ts, 0).unwrap();
let start = chrono::NaiveDate::from_ymd_opt(dt.year(), 1, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap()
.and_utc();
Ok(Rc::new(Variable::String(
start.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
)))
}
}
define_function!(
IsSameDayFn,
vec![ArgumentType::Any, ArgumentType::Any],
None
);
impl Function for IsSameDayFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let ts1 = match parse_date_value(&args[0]) {
Some(t) => t,
None => return Ok(Rc::new(Variable::Null)),
};
let ts2 = match parse_date_value(&args[1]) {
Some(t) => t,
None => return Ok(Rc::new(Variable::Null)),
};
let dt1 = match DateTime::from_timestamp(ts1, 0) {
Some(dt) => dt,
None => return Ok(Rc::new(Variable::Null)),
};
let dt2 = match DateTime::from_timestamp(ts2, 0) {
Some(dt) => dt,
None => return Ok(Rc::new(Variable::Null)),
};
let same_day = dt1.date_naive() == dt2.date_naive();
Ok(Rc::new(Variable::Bool(same_day)))
}
}
define_function!(ParseDatetimeFn, vec![ArgumentType::String], None);
impl Function for ParseDatetimeFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let input = args[0].as_string().unwrap();
match dateparser::parse_with_timezone(input, &Utc) {
Ok(dt) => {
let iso = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
Ok(Rc::new(Variable::String(iso)))
}
Err(_) => Ok(Rc::new(Variable::Null)),
}
}
}
define_function!(ParseNaturalDateFn, vec![ArgumentType::String], None);
impl Function for ParseNaturalDateFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let input = args[0].as_string().unwrap();
match chrono_english::parse_date_string(input, Utc::now(), chrono_english::Dialect::Us) {
Ok(dt) => {
let iso = dt.format("%Y-%m-%dT%H:%M:%SZ").to_string();
Ok(Rc::new(Variable::String(iso)))
}
Err(_) => Ok(Rc::new(Variable::Null)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn setup() -> Runtime {
let mut runtime = Runtime::new();
runtime.register_builtin_functions();
register(&mut runtime);
runtime
}
#[test]
fn test_now() {
let runtime = setup();
let expr = runtime.compile("now()").unwrap();
let result = expr.search(&Variable::Null).unwrap();
let ts = result.as_number().unwrap();
assert!(ts > 1577836800.0);
}
#[test]
fn test_now_millis() {
let runtime = setup();
let expr = runtime.compile("now_millis()").unwrap();
let result = expr.search(&Variable::Null).unwrap();
let ts = result.as_number().unwrap();
assert!(ts > 1577836800000.0);
}
#[test]
fn test_format_date() {
let runtime = setup();
let expr = runtime
.compile("format_date(`1720000000`, '%Y-%m-%d')")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_string().unwrap(), "2024-07-03");
}
#[test]
fn test_format_date_with_time() {
let runtime = setup();
let expr = runtime
.compile("format_date(`0`, '%Y-%m-%dT%H:%M:%S')")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_string().unwrap(), "1970-01-01T00:00:00");
}
#[test]
fn test_parse_date_iso() {
let runtime = setup();
let data = Variable::String("1970-01-01T00:00:00Z".to_string());
let expr = runtime.compile("parse_date(@)").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap(), 0.0);
}
#[test]
fn test_parse_date_date_only() {
let runtime = setup();
let data = Variable::String("2024-07-03".to_string());
let expr = runtime.compile("parse_date(@)").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap(), 1719964800.0);
}
#[test]
fn test_parse_date_with_format() {
let runtime = setup();
let data = Variable::String("03/07/2024 00:00:00".to_string());
let expr = runtime
.compile("parse_date(@, '%d/%m/%Y %H:%M:%S')")
.unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap(), 1719964800.0);
}
#[test]
fn test_parse_date_invalid() {
let runtime = setup();
let data = Variable::String("not a date".to_string());
let expr = runtime.compile("parse_date(@)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.is_null());
}
#[test]
fn test_date_add_days() {
let runtime = setup();
let expr = runtime
.compile("date_add(`1720000000`, `7`, 'days')")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 1720604800.0);
}
#[test]
fn test_date_add_hours() {
let runtime = setup();
let expr = runtime
.compile("date_add(`1720000000`, `24`, 'hours')")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 1720086400.0);
}
#[test]
fn test_date_add_negative() {
let runtime = setup();
let expr = runtime
.compile("date_add(`1720000000`, `-1`, 'day')")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 1719913600.0);
}
#[test]
fn test_date_diff_days() {
let runtime = setup();
let expr = runtime
.compile("date_diff(`1720604800`, `1720000000`, 'days')")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 7.0);
}
#[test]
fn test_date_diff_hours() {
let runtime = setup();
let expr = runtime
.compile("date_diff(`1720086400`, `1720000000`, 'hours')")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 24.0);
}
#[test]
fn test_date_diff_negative() {
let runtime = setup();
let expr = runtime
.compile("date_diff(`1720000000`, `1720604800`, 'days')")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), -7.0);
}
#[test]
fn test_date_add_invalid_unit() {
let runtime = setup();
let expr = runtime
.compile("date_add(`1720000000`, `1`, 'invalid')")
.unwrap();
let result = expr.search(&Variable::Null);
assert!(result.is_err());
}
#[test]
fn test_timezone_convert_ny_to_london() {
let runtime = setup();
let data = Variable::String("2024-01-15T10:00:00".to_string());
let expr = runtime
.compile("timezone_convert(@, 'America/New_York', 'Europe/London')")
.unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "2024-01-15T15:00:00");
}
#[test]
fn test_timezone_convert_tokyo_to_la() {
let runtime = setup();
let data = Variable::String("2024-07-15T09:00:00".to_string());
let expr = runtime
.compile("timezone_convert(@, 'Asia/Tokyo', 'America/Los_Angeles')")
.unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "2024-07-14T17:00:00");
}
#[test]
fn test_timezone_convert_invalid_tz() {
let runtime = setup();
let data = Variable::String("2024-01-15T10:00:00".to_string());
let expr = runtime
.compile("timezone_convert(@, 'Invalid/Zone', 'Europe/London')")
.unwrap();
let result = expr.search(&data);
assert!(result.is_err());
}
#[test]
fn test_is_weekend_saturday() {
let runtime = setup();
let expr = runtime.compile("is_weekend(`1705104000`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_weekend_sunday() {
let runtime = setup();
let expr = runtime.compile("is_weekend(`1705190400`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_weekend_monday() {
let runtime = setup();
let expr = runtime.compile("is_weekend(`1705276800`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_is_weekday_monday() {
let runtime = setup();
let expr = runtime.compile("is_weekday(`1705276800`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_weekday_saturday() {
let runtime = setup();
let expr = runtime.compile("is_weekday(`1705104000`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_business_days_between() {
let runtime = setup();
let expr = runtime
.compile("business_days_between(`1704067200`, `1705276800`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 10.0);
}
#[test]
fn test_business_days_between_reversed() {
let runtime = setup();
let expr = runtime
.compile("business_days_between(`1705276800`, `1704067200`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), -10.0);
}
#[test]
fn test_business_days_between_same_day() {
let runtime = setup();
let expr = runtime
.compile("business_days_between(`1705276800`, `1705276800`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 0.0);
}
#[test]
fn test_quarter_q1() {
let runtime = setup();
let expr = runtime.compile("quarter(`1705276800`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 1.0);
}
#[test]
fn test_quarter_q2() {
let runtime = setup();
let expr = runtime.compile("quarter(`1713139200`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 2.0);
}
#[test]
fn test_quarter_q3() {
let runtime = setup();
let expr = runtime.compile("quarter(`1721001600`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 3.0);
}
#[test]
fn test_quarter_q4() {
let runtime = setup();
let expr = runtime.compile("quarter(`1728950400`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap(), 4.0);
}
#[test]
fn test_relative_time_past() {
let runtime = setup();
let one_year_ago = Utc::now().timestamp() - 31536000;
let expr_str = format!("relative_time(`{}`)", one_year_ago);
let expr = runtime.compile(&expr_str).unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_string().unwrap().contains("ago"));
}
#[test]
fn test_relative_time_future() {
let runtime = setup();
let one_day_future = Utc::now().timestamp() + 86400;
let expr_str = format!("relative_time(`{}`)", one_day_future);
let expr = runtime.compile(&expr_str).unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_string().unwrap().starts_with("in "));
}
#[test]
fn test_is_after_with_timestamps() {
let runtime = setup();
let expr = runtime
.compile("is_after(`1720000000`, `1710000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_after_with_timestamps_false() {
let runtime = setup();
let expr = runtime
.compile("is_after(`1710000000`, `1720000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_is_after_with_date_strings() {
let runtime = setup();
let data = Variable::from_json(r#"{"d1": "2024-07-15", "d2": "2024-01-01"}"#).unwrap();
let expr = runtime.compile("is_after(d1, d2)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_after_with_iso_strings() {
let runtime = setup();
let data =
Variable::from_json(r#"{"d1": "2024-07-15T10:30:00Z", "d2": "2024-07-15T08:00:00Z"}"#)
.unwrap();
let expr = runtime.compile("is_after(d1, d2)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_after_mixed_types() {
let runtime = setup();
let data = Variable::from_json(r#"{"d": "2024-01-01"}"#).unwrap();
let expr = runtime.compile("is_after(`1720000000`, d)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_after_equal_dates() {
let runtime = setup();
let expr = runtime
.compile("is_after(`1720000000`, `1720000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_is_after_invalid_date() {
let runtime = setup();
let data = Variable::from_json(r#"{"d": "not-a-date"}"#).unwrap();
let expr = runtime.compile("is_after(d, `1720000000`)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.is_null());
}
#[test]
fn test_is_before_with_timestamps() {
let runtime = setup();
let expr = runtime
.compile("is_before(`1710000000`, `1720000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_before_with_timestamps_false() {
let runtime = setup();
let expr = runtime
.compile("is_before(`1720000000`, `1710000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_is_before_with_date_strings() {
let runtime = setup();
let data = Variable::from_json(r#"{"d1": "2024-01-01", "d2": "2024-07-15"}"#).unwrap();
let expr = runtime.compile("is_before(d1, d2)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_before_equal_dates() {
let runtime = setup();
let expr = runtime
.compile("is_before(`1720000000`, `1720000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_is_between_with_timestamps_true() {
let runtime = setup();
let expr = runtime
.compile("is_between(`1715000000`, `1710000000`, `1720000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_between_with_timestamps_false() {
let runtime = setup();
let expr = runtime
.compile("is_between(`1700000000`, `1710000000`, `1720000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_is_between_with_date_strings() {
let runtime = setup();
let data = Variable::from_json(
r#"{"d": "2024-06-15", "start": "2024-01-01", "end": "2024-12-31"}"#,
)
.unwrap();
let expr = runtime.compile("is_between(d, start, end)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_between_inclusive_start() {
let runtime = setup();
let expr = runtime
.compile("is_between(`1710000000`, `1710000000`, `1720000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_between_inclusive_end() {
let runtime = setup();
let expr = runtime
.compile("is_between(`1720000000`, `1710000000`, `1720000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_between_outside_range() {
let runtime = setup();
let expr = runtime
.compile("is_between(`1730000000`, `1710000000`, `1720000000`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_time_ago_with_timestamp() {
let runtime = setup();
let one_hour_ago = Utc::now().timestamp() - 3600;
let expr_str = format!("time_ago(`{}`)", one_hour_ago);
let expr = runtime.compile(&expr_str).unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_string().unwrap(), "1 hour ago");
}
#[test]
fn test_time_ago_with_date_string() {
let runtime = setup();
let data = Variable::String("2020-01-01".to_string());
let expr = runtime.compile("time_ago(@)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.as_string().unwrap().contains("years ago"));
}
#[test]
fn test_time_ago_plural() {
let runtime = setup();
let two_days_ago = Utc::now().timestamp() - 172800;
let expr_str = format!("time_ago(`{}`)", two_days_ago);
let expr = runtime.compile(&expr_str).unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_string().unwrap(), "2 days ago");
}
#[test]
fn test_time_ago_singular() {
let runtime = setup();
let one_day_ago = Utc::now().timestamp() - 86400;
let expr_str = format!("time_ago(`{}`)", one_day_ago);
let expr = runtime.compile(&expr_str).unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_string().unwrap(), "1 day ago");
}
#[test]
fn test_time_ago_future() {
let runtime = setup();
let one_day_future = Utc::now().timestamp() + 86400;
let expr_str = format!("time_ago(`{}`)", one_day_future);
let expr = runtime.compile(&expr_str).unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_string().unwrap().starts_with("in "));
}
#[test]
fn test_time_ago_invalid_date() {
let runtime = setup();
let data = Variable::String("not-a-date".to_string());
let expr = runtime.compile("time_ago(@)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.is_null());
}
#[test]
fn test_from_epoch() {
let runtime = setup();
let expr = runtime.compile("from_epoch(`1702425600`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_string().unwrap(), "2023-12-13T00:00:00Z");
}
#[test]
fn test_from_epoch_ms() {
let runtime = setup();
let expr = runtime.compile("from_epoch_ms(`1702425600500`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_string().unwrap(), "2023-12-13T00:00:00.500Z");
}
#[test]
fn test_to_epoch() {
let runtime = setup();
let data = Variable::String("2023-12-13T00:00:00Z".to_string());
let expr = runtime.compile("to_epoch(@)").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 1702425600);
}
#[test]
fn test_to_epoch_ms() {
let runtime = setup();
let data = Variable::String("2023-12-13T00:00:00Z".to_string());
let expr = runtime.compile("to_epoch_ms(@)").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 1702425600000);
}
#[test]
fn test_to_epoch_from_number() {
let runtime = setup();
let expr = runtime.compile("to_epoch(`1702425600`)").unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert_eq!(result.as_number().unwrap() as i64, 1702425600);
}
#[test]
fn test_duration_since() {
let runtime = setup();
let two_days_ago = Utc::now().timestamp() - 172800;
let expr_str = format!("duration_since(`{}`)", two_days_ago);
let expr = runtime.compile(&expr_str).unwrap();
let result = expr.search(&Variable::Null).unwrap();
let obj = result.as_object().unwrap();
assert_eq!(obj.get("days").unwrap().as_number().unwrap() as i64, 2);
assert!(!obj.get("is_future").unwrap().as_boolean().unwrap());
assert!(
obj.get("human")
.unwrap()
.as_string()
.unwrap()
.contains("2 days ago")
);
}
#[test]
fn test_start_of_day() {
let runtime = setup();
let data = Variable::String("2023-12-13T15:30:45Z".to_string());
let expr = runtime.compile("start_of_day(@)").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "2023-12-13T00:00:00Z");
}
#[test]
fn test_end_of_day() {
let runtime = setup();
let data = Variable::String("2023-12-13T15:30:45Z".to_string());
let expr = runtime.compile("end_of_day(@)").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "2023-12-13T23:59:59Z");
}
#[test]
fn test_start_of_week() {
let runtime = setup();
let data = Variable::String("2023-12-13T15:30:45Z".to_string());
let expr = runtime.compile("start_of_week(@)").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "2023-12-11T00:00:00Z");
}
#[test]
fn test_start_of_month() {
let runtime = setup();
let data = Variable::String("2023-12-13T15:30:45Z".to_string());
let expr = runtime.compile("start_of_month(@)").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "2023-12-01T00:00:00Z");
}
#[test]
fn test_start_of_year() {
let runtime = setup();
let data = Variable::String("2023-12-13T15:30:45Z".to_string());
let expr = runtime.compile("start_of_year(@)").unwrap();
let result = expr.search(&data).unwrap();
assert_eq!(result.as_string().unwrap(), "2023-01-01T00:00:00Z");
}
#[test]
fn test_is_same_day_true() {
let runtime = setup();
let expr = runtime
.compile("is_same_day(`\"2023-12-13T10:00:00Z\"`, `\"2023-12-13T23:00:00Z\"`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(result.as_boolean().unwrap());
}
#[test]
fn test_is_same_day_false() {
let runtime = setup();
let expr = runtime
.compile("is_same_day(`\"2023-12-13T10:00:00Z\"`, `\"2023-12-14T10:00:00Z\"`)")
.unwrap();
let result = expr.search(&Variable::Null).unwrap();
assert!(!result.as_boolean().unwrap());
}
#[test]
fn test_epoch_ms_alias() {
let runtime = setup();
let expr = runtime.compile("epoch_ms()").unwrap();
let result = expr.search(&Variable::Null).unwrap();
let ts = result.as_number().unwrap() as i64;
assert!(ts > 1700000000000);
}
#[test]
fn test_parse_datetime_iso_date() {
let runtime = setup();
let data = Variable::String("2024-01-15".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
}
#[test]
fn test_parse_datetime_iso_datetime() {
let runtime = setup();
let data = Variable::String("2024-01-15T10:30:00Z".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert_eq!(s, "2024-01-15T10:30:00Z");
}
#[test]
fn test_parse_datetime_iso_with_offset() {
let runtime = setup();
let data = Variable::String("2024-01-15T10:30:00+05:00".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
}
#[test]
fn test_parse_datetime_human_month_day_year() {
let runtime = setup();
let data = Variable::String("Jan 15, 2024".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
}
#[test]
fn test_parse_datetime_human_full_month() {
let runtime = setup();
let data = Variable::String("January 15, 2024".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
}
#[test]
fn test_parse_datetime_us_format() {
let runtime = setup();
let data = Variable::String("01/15/2024".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.contains("2024"), "Expected year 2024 in {}", s);
}
#[test]
fn test_parse_datetime_rfc2822() {
let runtime = setup();
let data = Variable::String("Mon, 15 Jan 2024 10:30:00 +0000".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.contains("2024-01-15"), "Expected 2024-01-15 in {}", s);
}
#[test]
fn test_parse_datetime_unix_timestamp() {
let runtime = setup();
let data = Variable::String("1705363200".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.contains("2024-01-16"), "Expected 2024-01-16 in {}", s);
}
#[test]
fn test_parse_datetime_invalid() {
let runtime = setup();
let data = Variable::String("not a date at all xyz123".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.is_null(), "Invalid date should return null");
}
#[test]
fn test_parse_datetime_empty() {
let runtime = setup();
let data = Variable::String("".to_string());
let expr = runtime.compile("parse_datetime(@)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.is_null(), "Empty string should return null");
}
#[test]
fn test_parse_natural_date_yesterday() {
let runtime = setup();
let data = Variable::String("yesterday".to_string());
let expr = runtime.compile("parse_natural_date(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
assert!(
s.contains('T'),
"Expected ISO format with T separator in {}",
s
);
}
#[test]
fn test_parse_natural_date_tomorrow() {
let runtime = setup();
let data = Variable::String("tomorrow".to_string());
let expr = runtime.compile("parse_natural_date(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
}
#[test]
fn test_parse_natural_date_days_ago() {
let runtime = setup();
let data = Variable::String("3 days ago".to_string());
let expr = runtime.compile("parse_natural_date(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
}
#[test]
fn test_parse_natural_date_weeks_ago() {
let runtime = setup();
let data = Variable::String("2 weeks ago".to_string());
let expr = runtime.compile("parse_natural_date(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
}
#[test]
fn test_parse_natural_date_next_weekday() {
let runtime = setup();
let data = Variable::String("next friday".to_string());
let expr = runtime.compile("parse_natural_date(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
}
#[test]
fn test_parse_natural_date_last_weekday() {
let runtime = setup();
let data = Variable::String("last monday".to_string());
let expr = runtime.compile("parse_natural_date(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
}
#[test]
fn test_parse_natural_date_hours() {
let runtime = setup();
let data = Variable::String("5 hours ago".to_string());
let expr = runtime.compile("parse_natural_date(@)").unwrap();
let result = expr.search(&data).unwrap();
let s = result.as_string().unwrap();
assert!(s.ends_with('Z'), "Expected UTC timezone marker in {}", s);
}
#[test]
fn test_parse_natural_date_invalid() {
let runtime = setup();
let data = Variable::String("not a natural date expression".to_string());
let expr = runtime.compile("parse_natural_date(@)").unwrap();
let result = expr.search(&data).unwrap();
assert!(result.is_null(), "Invalid expression should return null");
}
}