Skip to main content

jpx_core/extensions/
duration.rs

1//! Duration parsing and formatting functions.
2
3use std::collections::HashSet;
4
5use serde_json::{Number, Value};
6
7use crate::functions::{Function, number_value};
8use crate::interpreter::SearchResult;
9use crate::registry::register_if_enabled;
10use crate::{Context, Runtime, arg, defn};
11
12defn!(ParseDurationFn, vec![arg!(string)], None);
13
14impl Function for ParseDurationFn {
15    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
16        self.signature.validate(args, ctx)?;
17
18        let s = args[0]
19            .as_str()
20            .ok_or_else(|| crate::functions::custom_error(ctx, "Expected string"))?;
21
22        match parse_duration_str(s) {
23            Some(secs) => Ok(number_value(secs as f64)),
24            None => Ok(Value::Null),
25        }
26    }
27}
28
29defn!(FormatDurationFn, vec![arg!(number)], None);
30
31impl Function for FormatDurationFn {
32    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
33        self.signature.validate(args, ctx)?;
34
35        let num = args[0]
36            .as_f64()
37            .ok_or_else(|| crate::functions::custom_error(ctx, "Expected number"))?;
38
39        let total_secs = num as u64;
40        let formatted = format_duration_secs(total_secs);
41
42        Ok(Value::String(formatted))
43    }
44}
45
46defn!(DurationHoursFn, vec![arg!(number)], None);
47
48impl Function for DurationHoursFn {
49    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
50        self.signature.validate(args, ctx)?;
51
52        let num = args[0]
53            .as_f64()
54            .ok_or_else(|| crate::functions::custom_error(ctx, "Expected number"))?;
55
56        let total_secs = num as u64;
57        let hours = (total_secs / 3600) % 24;
58
59        Ok(Value::Number(Number::from(hours)))
60    }
61}
62
63defn!(DurationMinutesFn, vec![arg!(number)], None);
64
65impl Function for DurationMinutesFn {
66    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
67        self.signature.validate(args, ctx)?;
68
69        let num = args[0]
70            .as_f64()
71            .ok_or_else(|| crate::functions::custom_error(ctx, "Expected number"))?;
72
73        let total_secs = num as u64;
74        let minutes = (total_secs / 60) % 60;
75
76        Ok(Value::Number(Number::from(minutes)))
77    }
78}
79
80defn!(DurationSecondsFn, vec![arg!(number)], None);
81
82impl Function for DurationSecondsFn {
83    fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
84        self.signature.validate(args, ctx)?;
85
86        let num = args[0]
87            .as_f64()
88            .ok_or_else(|| crate::functions::custom_error(ctx, "Expected number"))?;
89
90        let total_secs = num as u64;
91        let seconds = total_secs % 60;
92
93        Ok(Value::Number(Number::from(seconds)))
94    }
95}
96
97/// Parse a duration string into total seconds.
98fn parse_duration_str(s: &str) -> Option<u64> {
99    let s = s.trim().to_lowercase();
100    if s.is_empty() {
101        return None;
102    }
103
104    let mut total_secs: u64 = 0;
105    let mut current_num = String::new();
106
107    let chars: Vec<char> = s.chars().collect();
108    let mut i = 0;
109
110    while i < chars.len() {
111        let c = chars[i];
112
113        if c.is_ascii_digit() {
114            current_num.push(c);
115            i += 1;
116        } else if c.is_ascii_alphabetic() {
117            let num: u64 = if current_num.is_empty() {
118                return None;
119            } else {
120                current_num.parse().ok()?
121            };
122            current_num.clear();
123
124            let mut unit = String::new();
125            while i < chars.len() && chars[i].is_ascii_alphabetic() {
126                unit.push(chars[i]);
127                i += 1;
128            }
129
130            let multiplier = match unit.as_str() {
131                "w" | "week" | "weeks" => 7 * 24 * 3600,
132                "d" | "day" | "days" => 24 * 3600,
133                "h" | "hr" | "hrs" | "hour" | "hours" => 3600,
134                "m" | "min" | "mins" | "minute" | "minutes" => 60,
135                "s" | "sec" | "secs" | "second" | "seconds" => 1,
136                _ => return None,
137            };
138
139            total_secs += num * multiplier;
140        } else if c.is_whitespace() {
141            i += 1;
142        } else {
143            return None;
144        }
145    }
146
147    if !current_num.is_empty() {
148        let num: u64 = current_num.parse().ok()?;
149        total_secs += num;
150    }
151
152    Some(total_secs)
153}
154
155/// Format seconds as a human-readable duration string.
156fn format_duration_secs(total_secs: u64) -> String {
157    if total_secs == 0 {
158        return "0s".to_string();
159    }
160
161    let weeks = total_secs / (7 * 24 * 3600);
162    let days = (total_secs / (24 * 3600)) % 7;
163    let hours = (total_secs / 3600) % 24;
164    let minutes = (total_secs / 60) % 60;
165    let seconds = total_secs % 60;
166
167    let mut result = String::new();
168
169    if weeks > 0 {
170        result.push_str(&format!("{}w", weeks));
171    }
172    if days > 0 {
173        result.push_str(&format!("{}d", days));
174    }
175    if hours > 0 {
176        result.push_str(&format!("{}h", hours));
177    }
178    if minutes > 0 {
179        result.push_str(&format!("{}m", minutes));
180    }
181    if seconds > 0 {
182        result.push_str(&format!("{}s", seconds));
183    }
184
185    result
186}
187
188/// Register duration functions that are in the enabled set.
189pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
190    register_if_enabled(
191        runtime,
192        "parse_duration",
193        enabled,
194        Box::new(ParseDurationFn::new()),
195    );
196    register_if_enabled(
197        runtime,
198        "format_duration",
199        enabled,
200        Box::new(FormatDurationFn::new()),
201    );
202    register_if_enabled(
203        runtime,
204        "duration_hours",
205        enabled,
206        Box::new(DurationHoursFn::new()),
207    );
208    register_if_enabled(
209        runtime,
210        "duration_minutes",
211        enabled,
212        Box::new(DurationMinutesFn::new()),
213    );
214    register_if_enabled(
215        runtime,
216        "duration_seconds",
217        enabled,
218        Box::new(DurationSecondsFn::new()),
219    );
220}