1use std::collections::HashSet;
4
5use serde_json::Value;
6
7use crate::functions::{Function, custom_error, number_value};
8use crate::interpreter::SearchResult;
9use crate::registry::register_if_enabled;
10use crate::{Context, Runtime, arg, defn};
11
12pub fn register_filtered(runtime: &mut Runtime, enabled: &HashSet<&str>) {
14 register_if_enabled(
15 runtime,
16 "convert_temperature",
17 enabled,
18 Box::new(ConvertTemperatureFn::new()),
19 );
20 register_if_enabled(
21 runtime,
22 "convert_length",
23 enabled,
24 Box::new(ConvertLengthFn::new()),
25 );
26 register_if_enabled(
27 runtime,
28 "convert_mass",
29 enabled,
30 Box::new(ConvertMassFn::new()),
31 );
32 register_if_enabled(
33 runtime,
34 "convert_volume",
35 enabled,
36 Box::new(ConvertVolumeFn::new()),
37 );
38}
39
40defn!(
45 ConvertTemperatureFn,
46 vec![arg!(number), arg!(string), arg!(string)],
47 None
48);
49
50fn normalize_temp_unit(unit: &str) -> Option<&'static str> {
51 match unit.to_lowercase().as_str() {
52 "c" | "celsius" => Some("C"),
53 "f" | "fahrenheit" => Some("F"),
54 "k" | "kelvin" => Some("K"),
55 _ => None,
56 }
57}
58
59fn to_celsius(value: f64, from: &str) -> f64 {
60 match from {
61 "C" => value,
62 "F" => (value - 32.0) * 5.0 / 9.0,
63 "K" => value - 273.15,
64 _ => unreachable!(),
65 }
66}
67
68fn from_celsius(value: f64, to: &str) -> f64 {
69 match to {
70 "C" => value,
71 "F" => value * 9.0 / 5.0 + 32.0,
72 "K" => value + 273.15,
73 _ => unreachable!(),
74 }
75}
76
77impl Function for ConvertTemperatureFn {
78 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
79 self.signature.validate(args, ctx)?;
80
81 let value = args[0].as_f64().unwrap();
82 let from_str = args[1].as_str().unwrap();
83 let to_str = args[2].as_str().unwrap();
84
85 let from = normalize_temp_unit(from_str)
86 .ok_or_else(|| custom_error(ctx, &format!("unknown temperature unit: {}", from_str)))?;
87 let to = normalize_temp_unit(to_str)
88 .ok_or_else(|| custom_error(ctx, &format!("unknown temperature unit: {}", to_str)))?;
89
90 let celsius = to_celsius(value, from);
91 let result = from_celsius(celsius, to);
92
93 Ok(number_value(result))
94 }
95}
96
97defn!(
102 ConvertLengthFn,
103 vec![arg!(number), arg!(string), arg!(string)],
104 None
105);
106
107fn length_to_meters(unit: &str) -> Option<f64> {
109 match unit.to_lowercase().as_str() {
110 "m" | "meters" => Some(1.0),
111 "km" | "kilometers" => Some(1000.0),
112 "cm" | "centimeters" => Some(0.01),
113 "mm" | "millimeters" => Some(0.001),
114 "mi" | "miles" => Some(1609.344),
115 "ft" | "feet" => Some(0.3048),
116 "in" | "inches" => Some(0.0254),
117 "yd" | "yards" => Some(0.9144),
118 "nmi" | "nautical_miles" => Some(1852.0),
119 _ => None,
120 }
121}
122
123impl Function for ConvertLengthFn {
124 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
125 self.signature.validate(args, ctx)?;
126
127 let value = args[0].as_f64().unwrap();
128 let from_str = args[1].as_str().unwrap();
129 let to_str = args[2].as_str().unwrap();
130
131 let from_factor = length_to_meters(from_str)
132 .ok_or_else(|| custom_error(ctx, &format!("unknown length unit: {}", from_str)))?;
133 let to_factor = length_to_meters(to_str)
134 .ok_or_else(|| custom_error(ctx, &format!("unknown length unit: {}", to_str)))?;
135
136 let meters = value * from_factor;
137 let result = meters / to_factor;
138
139 Ok(number_value(result))
140 }
141}
142
143defn!(
148 ConvertMassFn,
149 vec![arg!(number), arg!(string), arg!(string)],
150 None
151);
152
153fn mass_to_kg(unit: &str) -> Option<f64> {
155 match unit.to_lowercase().as_str() {
156 "kg" | "kilograms" => Some(1.0),
157 "g" | "grams" => Some(0.001),
158 "mg" | "milligrams" => Some(0.000001),
159 "lbs" | "pounds" => Some(0.45359237),
160 "oz" | "ounces" => Some(0.028349523125),
161 "t" | "tonnes" => Some(1000.0),
162 "st" | "stones" => Some(6.35029318),
163 _ => None,
164 }
165}
166
167impl Function for ConvertMassFn {
168 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
169 self.signature.validate(args, ctx)?;
170
171 let value = args[0].as_f64().unwrap();
172 let from_str = args[1].as_str().unwrap();
173 let to_str = args[2].as_str().unwrap();
174
175 let from_factor = mass_to_kg(from_str)
176 .ok_or_else(|| custom_error(ctx, &format!("unknown mass unit: {}", from_str)))?;
177 let to_factor = mass_to_kg(to_str)
178 .ok_or_else(|| custom_error(ctx, &format!("unknown mass unit: {}", to_str)))?;
179
180 let kg = value * from_factor;
181 let result = kg / to_factor;
182
183 Ok(number_value(result))
184 }
185}
186
187defn!(
192 ConvertVolumeFn,
193 vec![arg!(number), arg!(string), arg!(string)],
194 None
195);
196
197fn volume_to_liters(unit: &str) -> Option<f64> {
199 match unit.to_lowercase().as_str() {
200 "l" | "liters" => Some(1.0),
201 "ml" | "milliliters" => Some(0.001),
202 "gal" | "gallons" => Some(3.785411784),
203 "qt" | "quarts" => Some(0.946352946),
204 "pt" | "pints" => Some(0.473176473),
205 "cup" | "cups" => Some(0.2365882365),
206 "floz" | "fluid_ounces" => Some(0.0295735295625),
207 "tbsp" | "tablespoons" => Some(0.0147867647813),
208 "tsp" | "teaspoons" => Some(0.00492892159375),
209 _ => None,
210 }
211}
212
213impl Function for ConvertVolumeFn {
214 fn evaluate(&self, args: &[Value], ctx: &mut Context<'_>) -> SearchResult {
215 self.signature.validate(args, ctx)?;
216
217 let value = args[0].as_f64().unwrap();
218 let from_str = args[1].as_str().unwrap();
219 let to_str = args[2].as_str().unwrap();
220
221 let from_factor = volume_to_liters(from_str)
222 .ok_or_else(|| custom_error(ctx, &format!("unknown volume unit: {}", from_str)))?;
223 let to_factor = volume_to_liters(to_str)
224 .ok_or_else(|| custom_error(ctx, &format!("unknown volume unit: {}", to_str)))?;
225
226 let liters = value * from_factor;
227 let result = liters / to_factor;
228
229 Ok(number_value(result))
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use crate::Runtime;
236 use serde_json::json;
237
238 fn setup_runtime() -> Runtime {
239 Runtime::builder()
240 .with_standard()
241 .with_all_extensions()
242 .build()
243 }
244
245 #[test]
250 fn test_celsius_to_fahrenheit() {
251 let runtime = setup_runtime();
252 let expr = runtime
253 .compile("convert_temperature(`100`, 'C', 'F')")
254 .unwrap();
255 let result = expr.search(&json!(null)).unwrap();
256 assert!((result.as_f64().unwrap() - 212.0).abs() < 1e-9);
257 }
258
259 #[test]
260 fn test_fahrenheit_to_celsius() {
261 let runtime = setup_runtime();
262 let expr = runtime
263 .compile("convert_temperature(`32`, 'F', 'C')")
264 .unwrap();
265 let result = expr.search(&json!(null)).unwrap();
266 assert!((result.as_f64().unwrap() - 0.0).abs() < 1e-9);
267 }
268
269 #[test]
270 fn test_celsius_to_kelvin() {
271 let runtime = setup_runtime();
272 let expr = runtime
273 .compile("convert_temperature(`0`, 'C', 'K')")
274 .unwrap();
275 let result = expr.search(&json!(null)).unwrap();
276 assert!((result.as_f64().unwrap() - 273.15).abs() < 1e-9);
277 }
278
279 #[test]
280 fn test_kelvin_to_celsius() {
281 let runtime = setup_runtime();
282 let expr = runtime
283 .compile("convert_temperature(`273.15`, 'K', 'C')")
284 .unwrap();
285 let result = expr.search(&json!(null)).unwrap();
286 assert!((result.as_f64().unwrap() - 0.0).abs() < 1e-9);
287 }
288
289 #[test]
290 fn test_fahrenheit_to_kelvin() {
291 let runtime = setup_runtime();
292 let expr = runtime
293 .compile("convert_temperature(`212`, 'fahrenheit', 'kelvin')")
294 .unwrap();
295 let result = expr.search(&json!(null)).unwrap();
296 assert!((result.as_f64().unwrap() - 373.15).abs() < 1e-9);
297 }
298
299 #[test]
300 fn test_temperature_same_unit() {
301 let runtime = setup_runtime();
302 let expr = runtime
303 .compile("convert_temperature(`42`, 'C', 'celsius')")
304 .unwrap();
305 let result = expr.search(&json!(null)).unwrap();
306 assert!((result.as_f64().unwrap() - 42.0).abs() < 1e-9);
307 }
308
309 #[test]
310 fn test_temperature_invalid_unit() {
311 let runtime = setup_runtime();
312 let expr = runtime
313 .compile("convert_temperature(`100`, 'C', 'invalid')")
314 .unwrap();
315 let result = expr.search(&json!(null));
316 assert!(result.is_err());
317 }
318
319 #[test]
324 fn test_km_to_miles() {
325 let runtime = setup_runtime();
326 let expr = runtime.compile("convert_length(`1`, 'km', 'mi')").unwrap();
327 let result = expr.search(&json!(null)).unwrap();
328 assert!((result.as_f64().unwrap() - 0.621371192237334).abs() < 1e-6);
329 }
330
331 #[test]
332 fn test_miles_to_km() {
333 let runtime = setup_runtime();
334 let expr = runtime.compile("convert_length(`1`, 'mi', 'km')").unwrap();
335 let result = expr.search(&json!(null)).unwrap();
336 assert!((result.as_f64().unwrap() - 1.609344).abs() < 1e-6);
337 }
338
339 #[test]
340 fn test_feet_to_meters() {
341 let runtime = setup_runtime();
342 let expr = runtime.compile("convert_length(`1`, 'ft', 'm')").unwrap();
343 let result = expr.search(&json!(null)).unwrap();
344 assert!((result.as_f64().unwrap() - 0.3048).abs() < 1e-6);
345 }
346
347 #[test]
348 fn test_inches_to_cm() {
349 let runtime = setup_runtime();
350 let expr = runtime.compile("convert_length(`1`, 'in', 'cm')").unwrap();
351 let result = expr.search(&json!(null)).unwrap();
352 assert!((result.as_f64().unwrap() - 2.54).abs() < 1e-6);
353 }
354
355 #[test]
356 fn test_yards_to_meters() {
357 let runtime = setup_runtime();
358 let expr = runtime.compile("convert_length(`1`, 'yd', 'm')").unwrap();
359 let result = expr.search(&json!(null)).unwrap();
360 assert!((result.as_f64().unwrap() - 0.9144).abs() < 1e-6);
361 }
362
363 #[test]
364 fn test_nautical_miles_to_km() {
365 let runtime = setup_runtime();
366 let expr = runtime.compile("convert_length(`1`, 'nmi', 'km')").unwrap();
367 let result = expr.search(&json!(null)).unwrap();
368 assert!((result.as_f64().unwrap() - 1.852).abs() < 1e-6);
369 }
370
371 #[test]
372 fn test_length_same_unit() {
373 let runtime = setup_runtime();
374 let expr = runtime
375 .compile("convert_length(`42`, 'meters', 'm')")
376 .unwrap();
377 let result = expr.search(&json!(null)).unwrap();
378 assert!((result.as_f64().unwrap() - 42.0).abs() < 1e-9);
379 }
380
381 #[test]
382 fn test_length_invalid_unit() {
383 let runtime = setup_runtime();
384 let expr = runtime
385 .compile("convert_length(`1`, 'm', 'furlongs')")
386 .unwrap();
387 let result = expr.search(&json!(null));
388 assert!(result.is_err());
389 }
390
391 #[test]
396 fn test_kg_to_lbs() {
397 let runtime = setup_runtime();
398 let expr = runtime.compile("convert_mass(`1`, 'kg', 'lbs')").unwrap();
399 let result = expr.search(&json!(null)).unwrap();
400 assert!((result.as_f64().unwrap() - 2.20462262185).abs() < 1e-4);
401 }
402
403 #[test]
404 fn test_lbs_to_kg() {
405 let runtime = setup_runtime();
406 let expr = runtime.compile("convert_mass(`1`, 'lbs', 'kg')").unwrap();
407 let result = expr.search(&json!(null)).unwrap();
408 assert!((result.as_f64().unwrap() - 0.45359237).abs() < 1e-6);
409 }
410
411 #[test]
412 fn test_grams_to_ounces() {
413 let runtime = setup_runtime();
414 let expr = runtime.compile("convert_mass(`1000`, 'g', 'oz')").unwrap();
415 let result = expr.search(&json!(null)).unwrap();
416 assert!((result.as_f64().unwrap() - 35.27396195).abs() < 1e-3);
418 }
419
420 #[test]
421 fn test_tonnes_to_kg() {
422 let runtime = setup_runtime();
423 let expr = runtime.compile("convert_mass(`1`, 't', 'kg')").unwrap();
424 let result = expr.search(&json!(null)).unwrap();
425 assert!((result.as_f64().unwrap() - 1000.0).abs() < 1e-9);
426 }
427
428 #[test]
429 fn test_stones_to_lbs() {
430 let runtime = setup_runtime();
431 let expr = runtime.compile("convert_mass(`1`, 'st', 'lbs')").unwrap();
432 let result = expr.search(&json!(null)).unwrap();
433 assert!((result.as_f64().unwrap() - 14.0).abs() < 0.01);
434 }
435
436 #[test]
437 fn test_mass_same_unit() {
438 let runtime = setup_runtime();
439 let expr = runtime
440 .compile("convert_mass(`42`, 'kilograms', 'kg')")
441 .unwrap();
442 let result = expr.search(&json!(null)).unwrap();
443 assert!((result.as_f64().unwrap() - 42.0).abs() < 1e-9);
444 }
445
446 #[test]
447 fn test_mass_invalid_unit() {
448 let runtime = setup_runtime();
449 let expr = runtime
450 .compile("convert_mass(`1`, 'kg', 'bushels')")
451 .unwrap();
452 let result = expr.search(&json!(null));
453 assert!(result.is_err());
454 }
455
456 #[test]
461 fn test_gallons_to_liters() {
462 let runtime = setup_runtime();
463 let expr = runtime.compile("convert_volume(`1`, 'gal', 'l')").unwrap();
464 let result = expr.search(&json!(null)).unwrap();
465 assert!((result.as_f64().unwrap() - 3.785411784).abs() < 1e-6);
466 }
467
468 #[test]
469 fn test_liters_to_ml() {
470 let runtime = setup_runtime();
471 let expr = runtime.compile("convert_volume(`1`, 'l', 'ml')").unwrap();
472 let result = expr.search(&json!(null)).unwrap();
473 assert!((result.as_f64().unwrap() - 1000.0).abs() < 1e-6);
474 }
475
476 #[test]
477 fn test_cups_to_ml() {
478 let runtime = setup_runtime();
479 let expr = runtime.compile("convert_volume(`1`, 'cup', 'ml')").unwrap();
480 let result = expr.search(&json!(null)).unwrap();
481 assert!((result.as_f64().unwrap() - 236.5882365).abs() < 0.01);
482 }
483
484 #[test]
485 fn test_tbsp_to_tsp() {
486 let runtime = setup_runtime();
487 let expr = runtime
488 .compile("convert_volume(`1`, 'tbsp', 'tsp')")
489 .unwrap();
490 let result = expr.search(&json!(null)).unwrap();
491 assert!((result.as_f64().unwrap() - 3.0).abs() < 0.01);
492 }
493
494 #[test]
495 fn test_quarts_to_pints() {
496 let runtime = setup_runtime();
497 let expr = runtime.compile("convert_volume(`1`, 'qt', 'pt')").unwrap();
498 let result = expr.search(&json!(null)).unwrap();
499 assert!((result.as_f64().unwrap() - 2.0).abs() < 0.01);
500 }
501
502 #[test]
503 fn test_floz_to_ml() {
504 let runtime = setup_runtime();
505 let expr = runtime
506 .compile("convert_volume(`1`, 'floz', 'ml')")
507 .unwrap();
508 let result = expr.search(&json!(null)).unwrap();
509 assert!((result.as_f64().unwrap() - 29.5735295625).abs() < 0.01);
510 }
511
512 #[test]
513 fn test_volume_same_unit() {
514 let runtime = setup_runtime();
515 let expr = runtime
516 .compile("convert_volume(`42`, 'liters', 'l')")
517 .unwrap();
518 let result = expr.search(&json!(null)).unwrap();
519 assert!((result.as_f64().unwrap() - 42.0).abs() < 1e-9);
520 }
521
522 #[test]
523 fn test_volume_invalid_unit() {
524 let runtime = setup_runtime();
525 let expr = runtime
526 .compile("convert_volume(`1`, 'l', 'barrels')")
527 .unwrap();
528 let result = expr.search(&json!(null));
529 assert!(result.is_err());
530 }
531
532 #[test]
537 fn test_convert_from_json_data() {
538 let runtime = setup_runtime();
539 let data = json!({"temp": 100, "from": "C", "to": "F"});
540 let expr = runtime
541 .compile("convert_temperature(temp, from, to)")
542 .unwrap();
543 let result = expr.search(&data).unwrap();
544 assert!((result.as_f64().unwrap() - 212.0).abs() < 1e-9);
545 }
546}