1use 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
97fn 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
155fn 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
188pub 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}
221
222#[cfg(test)]
223mod tests {
224 use crate::Runtime;
225 use serde_json::json;
226
227 use super::{format_duration_secs, parse_duration_str};
228
229 fn setup_runtime() -> Runtime {
230 Runtime::builder()
231 .with_standard()
232 .with_all_extensions()
233 .build()
234 }
235
236 #[test]
237 fn test_parse_duration() {
238 assert_eq!(parse_duration_str("1h"), Some(3600));
239 assert_eq!(parse_duration_str("30m"), Some(1800));
240 assert_eq!(parse_duration_str("45s"), Some(45));
241 assert_eq!(parse_duration_str("1h30m"), Some(5400));
242 assert_eq!(parse_duration_str("2h30m45s"), Some(9045));
243 assert_eq!(parse_duration_str("1d"), Some(86400));
244 assert_eq!(parse_duration_str("1w"), Some(604800));
245 assert_eq!(parse_duration_str("1w2d3h4m5s"), Some(788645));
246 assert_eq!(parse_duration_str("1 hour 30 minutes"), Some(5400));
247 assert_eq!(parse_duration_str(""), None);
248 assert_eq!(parse_duration_str("invalid"), None);
249 }
250
251 #[test]
252 fn test_format_duration() {
253 assert_eq!(format_duration_secs(0), "0s");
254 assert_eq!(format_duration_secs(45), "45s");
255 assert_eq!(format_duration_secs(60), "1m");
256 assert_eq!(format_duration_secs(3600), "1h");
257 assert_eq!(format_duration_secs(5400), "1h30m");
258 assert_eq!(format_duration_secs(86400), "1d");
259 assert_eq!(format_duration_secs(90061), "1d1h1m1s");
260 assert_eq!(format_duration_secs(788645), "1w2d3h4m5s");
261 }
262
263 #[test]
264 fn test_roundtrip() {
265 let values = [0, 45, 60, 3600, 5400, 86400, 90061, 788645];
266 for &v in &values {
267 let formatted = format_duration_secs(v);
268 let parsed = parse_duration_str(&formatted).unwrap_or(0);
269 assert_eq!(
270 parsed, v,
271 "Roundtrip failed for {}: {} -> {}",
272 v, formatted, parsed
273 );
274 }
275 }
276
277 #[test]
278 fn test_parse_duration_via_runtime() {
279 let runtime = setup_runtime();
280 let data = json!("1h30m");
281 let expr = runtime.compile("parse_duration(@)").unwrap();
282 let result = expr.search(&data).unwrap();
283 assert_eq!(result.as_f64().unwrap(), 5400.0);
284 }
285
286 #[test]
287 fn test_format_duration_via_runtime() {
288 let runtime = setup_runtime();
289 let expr = runtime.compile("format_duration(`5400`)").unwrap();
290 let result = expr.search(&json!(null)).unwrap();
291 assert_eq!(result.as_str().unwrap(), "1h30m");
292 }
293
294 #[test]
295 fn test_duration_hours_via_runtime() {
296 let runtime = setup_runtime();
297 let expr = runtime.compile("duration_hours(`90061`)").unwrap();
299 let result = expr.search(&json!(null)).unwrap();
300 assert_eq!(result.as_i64().unwrap(), 1);
301 }
302
303 #[test]
304 fn test_duration_minutes_via_runtime() {
305 let runtime = setup_runtime();
306 let expr = runtime.compile("duration_minutes(`90061`)").unwrap();
308 let result = expr.search(&json!(null)).unwrap();
309 assert_eq!(result.as_i64().unwrap(), 1);
310 }
311
312 #[test]
313 fn test_duration_seconds_via_runtime() {
314 let runtime = setup_runtime();
315 let expr = runtime.compile("duration_seconds(`90061`)").unwrap();
317 let result = expr.search(&json!(null)).unwrap();
318 assert_eq!(result.as_i64().unwrap(), 1);
319 }
320}