jpx_core/extensions/
duration.rs1use 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}