use std::collections::HashSet;
use crate::common::{
ArgumentType, Context, ErrorReason, Function, JmespathError, Rcvar, Variable, rcvar,
};
use crate::define_function;
use crate::register_if_enabled;
define_function!(ParseDurationFn, vec![ArgumentType::String], None);
impl Function for ParseDurationFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let s = args[0].as_string().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected string".to_owned()),
)
})?;
match parse_duration_str(s) {
Some(secs) => Ok(rcvar(Variable::Number(
serde_json::Number::from_f64(secs as f64).unwrap(),
))),
None => Ok(rcvar(Variable::Null)),
}
}
}
define_function!(FormatDurationFn, vec![ArgumentType::Number], None);
impl Function for FormatDurationFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let num = args[0].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number".to_owned()),
)
})?;
let total_secs = num as u64;
let formatted = format_duration_secs(total_secs);
Ok(rcvar(Variable::String(formatted)))
}
}
define_function!(DurationHoursFn, vec![ArgumentType::Number], None);
impl Function for DurationHoursFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let num = args[0].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number".to_owned()),
)
})?;
let total_secs = num as u64;
let hours = (total_secs / 3600) % 24;
Ok(rcvar(Variable::Number(serde_json::Number::from(hours))))
}
}
define_function!(DurationMinutesFn, vec![ArgumentType::Number], None);
impl Function for DurationMinutesFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let num = args[0].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number".to_owned()),
)
})?;
let total_secs = num as u64;
let minutes = (total_secs / 60) % 60;
Ok(rcvar(Variable::Number(serde_json::Number::from(minutes))))
}
}
define_function!(DurationSecondsFn, vec![ArgumentType::Number], None);
impl Function for DurationSecondsFn {
fn evaluate(&self, args: &[Rcvar], ctx: &mut Context<'_>) -> Result<Rcvar, JmespathError> {
self.signature.validate(args, ctx)?;
let num = args[0].as_number().ok_or_else(|| {
JmespathError::new(
ctx.expression,
0,
ErrorReason::Parse("Expected number".to_owned()),
)
})?;
let total_secs = num as u64;
let seconds = total_secs % 60;
Ok(rcvar(Variable::Number(serde_json::Number::from(seconds))))
}
}
fn parse_duration_str(s: &str) -> Option<u64> {
let s = s.trim().to_lowercase();
if s.is_empty() {
return None;
}
let mut total_secs: u64 = 0;
let mut current_num = String::new();
let chars: Vec<char> = s.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c.is_ascii_digit() {
current_num.push(c);
i += 1;
} else if c.is_ascii_alphabetic() {
let num: u64 = if current_num.is_empty() {
return None;
} else {
current_num.parse().ok()?
};
current_num.clear();
let mut unit = String::new();
while i < chars.len() && chars[i].is_ascii_alphabetic() {
unit.push(chars[i]);
i += 1;
}
let multiplier = match unit.as_str() {
"w" | "week" | "weeks" => 7 * 24 * 3600,
"d" | "day" | "days" => 24 * 3600,
"h" | "hr" | "hrs" | "hour" | "hours" => 3600,
"m" | "min" | "mins" | "minute" | "minutes" => 60,
"s" | "sec" | "secs" | "second" | "seconds" => 1,
_ => return None,
};
total_secs += num * multiplier;
} else if c.is_whitespace() {
i += 1;
} else {
return None;
}
}
if !current_num.is_empty() {
let num: u64 = current_num.parse().ok()?;
total_secs += num;
}
Some(total_secs)
}
fn format_duration_secs(total_secs: u64) -> String {
if total_secs == 0 {
return "0s".to_string();
}
let weeks = total_secs / (7 * 24 * 3600);
let days = (total_secs / (24 * 3600)) % 7;
let hours = (total_secs / 3600) % 24;
let minutes = (total_secs / 60) % 60;
let seconds = total_secs % 60;
let mut result = String::new();
if weeks > 0 {
result.push_str(&format!("{}w", weeks));
}
if days > 0 {
result.push_str(&format!("{}d", days));
}
if hours > 0 {
result.push_str(&format!("{}h", hours));
}
if minutes > 0 {
result.push_str(&format!("{}m", minutes));
}
if seconds > 0 {
result.push_str(&format!("{}s", seconds));
}
result
}
pub fn register(runtime: &mut crate::Runtime) {
runtime.register_function("parse_duration", Box::new(ParseDurationFn::new()));
runtime.register_function("format_duration", Box::new(FormatDurationFn::new()));
runtime.register_function("duration_hours", Box::new(DurationHoursFn::new()));
runtime.register_function("duration_minutes", Box::new(DurationMinutesFn::new()));
runtime.register_function("duration_seconds", Box::new(DurationSecondsFn::new()));
}
pub fn register_filtered(runtime: &mut crate::Runtime, enabled: &HashSet<&str>) {
register_if_enabled!(
runtime,
enabled,
"parse_duration",
Box::new(ParseDurationFn::new())
);
register_if_enabled!(
runtime,
enabled,
"format_duration",
Box::new(FormatDurationFn::new())
);
register_if_enabled!(
runtime,
enabled,
"duration_hours",
Box::new(DurationHoursFn::new())
);
register_if_enabled!(
runtime,
enabled,
"duration_minutes",
Box::new(DurationMinutesFn::new())
);
register_if_enabled!(
runtime,
enabled,
"duration_seconds",
Box::new(DurationSecondsFn::new())
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_duration() {
assert_eq!(parse_duration_str("1h"), Some(3600));
assert_eq!(parse_duration_str("30m"), Some(1800));
assert_eq!(parse_duration_str("45s"), Some(45));
assert_eq!(parse_duration_str("1h30m"), Some(5400));
assert_eq!(parse_duration_str("2h30m45s"), Some(9045));
assert_eq!(parse_duration_str("1d"), Some(86400));
assert_eq!(parse_duration_str("1w"), Some(604800));
assert_eq!(parse_duration_str("1w2d3h4m5s"), Some(788645));
assert_eq!(parse_duration_str("1 hour 30 minutes"), Some(5400));
assert_eq!(parse_duration_str(""), None);
assert_eq!(parse_duration_str("invalid"), None);
}
#[test]
fn test_format_duration() {
assert_eq!(format_duration_secs(0), "0s");
assert_eq!(format_duration_secs(45), "45s");
assert_eq!(format_duration_secs(60), "1m");
assert_eq!(format_duration_secs(3600), "1h");
assert_eq!(format_duration_secs(5400), "1h30m");
assert_eq!(format_duration_secs(86400), "1d");
assert_eq!(format_duration_secs(90061), "1d1h1m1s");
assert_eq!(format_duration_secs(788645), "1w2d3h4m5s");
}
#[test]
fn test_roundtrip() {
let values = [0, 45, 60, 3600, 5400, 86400, 90061, 788645];
for &v in &values {
let formatted = format_duration_secs(v);
let parsed = parse_duration_str(&formatted).unwrap_or(0);
assert_eq!(
parsed, v,
"Roundtrip failed for {}: {} -> {}",
v, formatted, parsed
);
}
}
}