sql_cli/data/
unit_converter.rs

1use anyhow::{anyhow, Result};
2use std::collections::HashMap;
3
4/// Unit categories for conversion
5#[derive(Debug, Clone, PartialEq)]
6enum UnitCategory {
7    Length,
8    Mass,
9    Temperature,
10    Volume,
11    Time,
12    Area,
13    Speed,
14    Pressure,
15}
16
17/// Unit definition with conversion factor to SI base unit
18#[derive(Debug, Clone)]
19struct Unit {
20    category: UnitCategory,
21    to_si_factor: f64,   // Multiply by this to get SI unit
22    to_si_offset: f64,   // Add this after multiplication (for temperature)
23    from_si_factor: f64, // Multiply SI by this to get unit
24    from_si_offset: f64, // Add this after multiplication (for temperature)
25}
26
27impl Unit {
28    /// Create a simple unit with just a conversion factor
29    fn simple(category: UnitCategory, to_si_factor: f64) -> Self {
30        Unit {
31            category,
32            to_si_factor,
33            to_si_offset: 0.0,
34            from_si_factor: 1.0 / to_si_factor,
35            from_si_offset: 0.0,
36        }
37    }
38
39    /// Create a temperature unit with custom conversion
40    fn temperature(
41        to_si_factor: f64,
42        to_si_offset: f64,
43        from_si_factor: f64,
44        from_si_offset: f64,
45    ) -> Self {
46        Unit {
47            category: UnitCategory::Temperature,
48            to_si_factor,
49            to_si_offset,
50            from_si_factor,
51            from_si_offset,
52        }
53    }
54}
55
56/// Main unit converter
57pub struct UnitConverter {
58    units: HashMap<String, Unit>,
59}
60
61impl Default for UnitConverter {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl UnitConverter {
68    #[must_use]
69    pub fn new() -> Self {
70        let mut units = HashMap::new();
71
72        // LENGTH - Base unit: meter (m)
73        units.insert("m".to_string(), Unit::simple(UnitCategory::Length, 1.0));
74        units.insert("meter".to_string(), Unit::simple(UnitCategory::Length, 1.0));
75        units.insert(
76            "meters".to_string(),
77            Unit::simple(UnitCategory::Length, 1.0),
78        );
79        units.insert("metre".to_string(), Unit::simple(UnitCategory::Length, 1.0));
80        units.insert(
81            "metres".to_string(),
82            Unit::simple(UnitCategory::Length, 1.0),
83        );
84
85        // Metric length
86        units.insert("km".to_string(), Unit::simple(UnitCategory::Length, 1000.0));
87        units.insert(
88            "kilometer".to_string(),
89            Unit::simple(UnitCategory::Length, 1000.0),
90        );
91        units.insert(
92            "kilometers".to_string(),
93            Unit::simple(UnitCategory::Length, 1000.0),
94        );
95        units.insert("cm".to_string(), Unit::simple(UnitCategory::Length, 0.01));
96        units.insert(
97            "centimeter".to_string(),
98            Unit::simple(UnitCategory::Length, 0.01),
99        );
100        units.insert(
101            "centimeters".to_string(),
102            Unit::simple(UnitCategory::Length, 0.01),
103        );
104        units.insert("mm".to_string(), Unit::simple(UnitCategory::Length, 0.001));
105        units.insert(
106            "millimeter".to_string(),
107            Unit::simple(UnitCategory::Length, 0.001),
108        );
109        units.insert(
110            "millimeters".to_string(),
111            Unit::simple(UnitCategory::Length, 0.001),
112        );
113        units.insert("nm".to_string(), Unit::simple(UnitCategory::Length, 1e-9));
114        units.insert(
115            "nanometer".to_string(),
116            Unit::simple(UnitCategory::Length, 1e-9),
117        );
118        units.insert("um".to_string(), Unit::simple(UnitCategory::Length, 1e-6));
119        units.insert(
120            "micrometer".to_string(),
121            Unit::simple(UnitCategory::Length, 1e-6),
122        );
123
124        // Imperial length
125        units.insert(
126            "mi".to_string(),
127            Unit::simple(UnitCategory::Length, 1609.344),
128        );
129        units.insert(
130            "mile".to_string(),
131            Unit::simple(UnitCategory::Length, 1609.344),
132        );
133        units.insert(
134            "miles".to_string(),
135            Unit::simple(UnitCategory::Length, 1609.344),
136        );
137        units.insert("yd".to_string(), Unit::simple(UnitCategory::Length, 0.9144));
138        units.insert(
139            "yard".to_string(),
140            Unit::simple(UnitCategory::Length, 0.9144),
141        );
142        units.insert(
143            "yards".to_string(),
144            Unit::simple(UnitCategory::Length, 0.9144),
145        );
146        units.insert("ft".to_string(), Unit::simple(UnitCategory::Length, 0.3048));
147        units.insert(
148            "foot".to_string(),
149            Unit::simple(UnitCategory::Length, 0.3048),
150        );
151        units.insert(
152            "feet".to_string(),
153            Unit::simple(UnitCategory::Length, 0.3048),
154        );
155        units.insert("in".to_string(), Unit::simple(UnitCategory::Length, 0.0254));
156        units.insert(
157            "inch".to_string(),
158            Unit::simple(UnitCategory::Length, 0.0254),
159        );
160        units.insert(
161            "inches".to_string(),
162            Unit::simple(UnitCategory::Length, 0.0254),
163        );
164
165        // Nautical
166        units.insert(
167            "nmi".to_string(),
168            Unit::simple(UnitCategory::Length, 1852.0),
169        );
170        units.insert(
171            "nautical_mile".to_string(),
172            Unit::simple(UnitCategory::Length, 1852.0),
173        );
174
175        // MASS - Base unit: kilogram (kg)
176        units.insert("kg".to_string(), Unit::simple(UnitCategory::Mass, 1.0));
177        units.insert(
178            "kilogram".to_string(),
179            Unit::simple(UnitCategory::Mass, 1.0),
180        );
181        units.insert(
182            "kilograms".to_string(),
183            Unit::simple(UnitCategory::Mass, 1.0),
184        );
185
186        // Metric mass
187        units.insert("g".to_string(), Unit::simple(UnitCategory::Mass, 0.001));
188        units.insert("gram".to_string(), Unit::simple(UnitCategory::Mass, 0.001));
189        units.insert("grams".to_string(), Unit::simple(UnitCategory::Mass, 0.001));
190        units.insert("mg".to_string(), Unit::simple(UnitCategory::Mass, 0.000001));
191        units.insert(
192            "milligram".to_string(),
193            Unit::simple(UnitCategory::Mass, 0.000001),
194        );
195        units.insert(
196            "milligrams".to_string(),
197            Unit::simple(UnitCategory::Mass, 0.000001),
198        );
199        units.insert("ug".to_string(), Unit::simple(UnitCategory::Mass, 1e-9));
200        units.insert(
201            "microgram".to_string(),
202            Unit::simple(UnitCategory::Mass, 1e-9),
203        );
204        units.insert("t".to_string(), Unit::simple(UnitCategory::Mass, 1000.0));
205        units.insert(
206            "tonne".to_string(),
207            Unit::simple(UnitCategory::Mass, 1000.0),
208        );
209        units.insert(
210            "metric_ton".to_string(),
211            Unit::simple(UnitCategory::Mass, 1000.0),
212        );
213
214        // Imperial mass
215        units.insert(
216            "lb".to_string(),
217            Unit::simple(UnitCategory::Mass, 0.45359237),
218        );
219        units.insert(
220            "lbs".to_string(),
221            Unit::simple(UnitCategory::Mass, 0.45359237),
222        );
223        units.insert(
224            "pound".to_string(),
225            Unit::simple(UnitCategory::Mass, 0.45359237),
226        );
227        units.insert(
228            "pounds".to_string(),
229            Unit::simple(UnitCategory::Mass, 0.45359237),
230        );
231        units.insert(
232            "oz".to_string(),
233            Unit::simple(UnitCategory::Mass, 0.028349523125),
234        );
235        units.insert(
236            "ounce".to_string(),
237            Unit::simple(UnitCategory::Mass, 0.028349523125),
238        );
239        units.insert(
240            "ounces".to_string(),
241            Unit::simple(UnitCategory::Mass, 0.028349523125),
242        );
243        units.insert(
244            "ton".to_string(),
245            Unit::simple(UnitCategory::Mass, 907.18474),
246        );
247        units.insert(
248            "short_ton".to_string(),
249            Unit::simple(UnitCategory::Mass, 907.18474),
250        );
251        units.insert(
252            "long_ton".to_string(),
253            Unit::simple(UnitCategory::Mass, 1016.0469088),
254        );
255        units.insert(
256            "stone".to_string(),
257            Unit::simple(UnitCategory::Mass, 6.35029318),
258        );
259
260        // TEMPERATURE - Base unit: Kelvin (K)
261        units.insert(
262            "k".to_string(),
263            Unit::simple(UnitCategory::Temperature, 1.0),
264        );
265        units.insert(
266            "kelvin".to_string(),
267            Unit::simple(UnitCategory::Temperature, 1.0),
268        );
269
270        // Celsius: K = C + 273.15
271        units.insert(
272            "c".to_string(),
273            Unit::temperature(1.0, 273.15, 1.0, -273.15),
274        );
275        units.insert(
276            "celsius".to_string(),
277            Unit::temperature(1.0, 273.15, 1.0, -273.15),
278        );
279        units.insert(
280            "centigrade".to_string(),
281            Unit::temperature(1.0, 273.15, 1.0, -273.15),
282        );
283
284        // Fahrenheit: K = (F + 459.67) × 5/9
285        units.insert(
286            "f".to_string(),
287            Unit::temperature(5.0 / 9.0, 459.67 * 5.0 / 9.0, 9.0 / 5.0, -459.67),
288        );
289        units.insert(
290            "fahrenheit".to_string(),
291            Unit::temperature(5.0 / 9.0, 459.67 * 5.0 / 9.0, 9.0 / 5.0, -459.67),
292        );
293
294        // VOLUME - Base unit: liter (L)
295        units.insert("l".to_string(), Unit::simple(UnitCategory::Volume, 1.0));
296        units.insert("liter".to_string(), Unit::simple(UnitCategory::Volume, 1.0));
297        units.insert(
298            "liters".to_string(),
299            Unit::simple(UnitCategory::Volume, 1.0),
300        );
301        units.insert("litre".to_string(), Unit::simple(UnitCategory::Volume, 1.0));
302        units.insert(
303            "litres".to_string(),
304            Unit::simple(UnitCategory::Volume, 1.0),
305        );
306
307        // Metric volume
308        units.insert("ml".to_string(), Unit::simple(UnitCategory::Volume, 0.001));
309        units.insert(
310            "milliliter".to_string(),
311            Unit::simple(UnitCategory::Volume, 0.001),
312        );
313        units.insert(
314            "milliliters".to_string(),
315            Unit::simple(UnitCategory::Volume, 0.001),
316        );
317        units.insert("m3".to_string(), Unit::simple(UnitCategory::Volume, 1000.0));
318        units.insert(
319            "cubic_meter".to_string(),
320            Unit::simple(UnitCategory::Volume, 1000.0),
321        );
322        units.insert("cm3".to_string(), Unit::simple(UnitCategory::Volume, 0.001));
323        units.insert(
324            "cubic_centimeter".to_string(),
325            Unit::simple(UnitCategory::Volume, 0.001),
326        );
327        units.insert("cc".to_string(), Unit::simple(UnitCategory::Volume, 0.001));
328
329        // Imperial volume
330        units.insert(
331            "gal".to_string(),
332            Unit::simple(UnitCategory::Volume, 3.785411784),
333        );
334        units.insert(
335            "gallon".to_string(),
336            Unit::simple(UnitCategory::Volume, 3.785411784),
337        );
338        units.insert(
339            "gallons".to_string(),
340            Unit::simple(UnitCategory::Volume, 3.785411784),
341        );
342        units.insert(
343            "qt".to_string(),
344            Unit::simple(UnitCategory::Volume, 0.946352946),
345        );
346        units.insert(
347            "quart".to_string(),
348            Unit::simple(UnitCategory::Volume, 0.946352946),
349        );
350        units.insert(
351            "quarts".to_string(),
352            Unit::simple(UnitCategory::Volume, 0.946352946),
353        );
354        units.insert(
355            "pt".to_string(),
356            Unit::simple(UnitCategory::Volume, 0.473176473),
357        );
358        units.insert(
359            "pint".to_string(),
360            Unit::simple(UnitCategory::Volume, 0.473176473),
361        );
362        units.insert(
363            "pints".to_string(),
364            Unit::simple(UnitCategory::Volume, 0.473176473),
365        );
366        units.insert(
367            "cup".to_string(),
368            Unit::simple(UnitCategory::Volume, 0.2365882365),
369        );
370        units.insert(
371            "cups".to_string(),
372            Unit::simple(UnitCategory::Volume, 0.2365882365),
373        );
374        units.insert(
375            "fl_oz".to_string(),
376            Unit::simple(UnitCategory::Volume, 0.0295735295625),
377        );
378        units.insert(
379            "fluid_ounce".to_string(),
380            Unit::simple(UnitCategory::Volume, 0.0295735295625),
381        );
382        units.insert(
383            "tbsp".to_string(),
384            Unit::simple(UnitCategory::Volume, 0.01478676478125),
385        );
386        units.insert(
387            "tablespoon".to_string(),
388            Unit::simple(UnitCategory::Volume, 0.01478676478125),
389        );
390        units.insert(
391            "tsp".to_string(),
392            Unit::simple(UnitCategory::Volume, 0.00492892159375),
393        );
394        units.insert(
395            "teaspoon".to_string(),
396            Unit::simple(UnitCategory::Volume, 0.00492892159375),
397        );
398
399        // Imperial UK volume
400        units.insert(
401            "imperial_gal".to_string(),
402            Unit::simple(UnitCategory::Volume, 4.54609),
403        );
404        units.insert(
405            "imperial_gallon".to_string(),
406            Unit::simple(UnitCategory::Volume, 4.54609),
407        );
408        units.insert(
409            "imperial_qt".to_string(),
410            Unit::simple(UnitCategory::Volume, 1.1365225),
411        );
412        units.insert(
413            "imperial_pt".to_string(),
414            Unit::simple(UnitCategory::Volume, 0.56826125),
415        );
416
417        // TIME - Base unit: second (s)
418        units.insert("s".to_string(), Unit::simple(UnitCategory::Time, 1.0));
419        units.insert("sec".to_string(), Unit::simple(UnitCategory::Time, 1.0));
420        units.insert("second".to_string(), Unit::simple(UnitCategory::Time, 1.0));
421        units.insert("seconds".to_string(), Unit::simple(UnitCategory::Time, 1.0));
422
423        units.insert("ms".to_string(), Unit::simple(UnitCategory::Time, 0.001));
424        units.insert(
425            "millisecond".to_string(),
426            Unit::simple(UnitCategory::Time, 0.001),
427        );
428        units.insert(
429            "milliseconds".to_string(),
430            Unit::simple(UnitCategory::Time, 0.001),
431        );
432        units.insert("us".to_string(), Unit::simple(UnitCategory::Time, 0.000001));
433        units.insert(
434            "microsecond".to_string(),
435            Unit::simple(UnitCategory::Time, 0.000001),
436        );
437        units.insert(
438            "microseconds".to_string(),
439            Unit::simple(UnitCategory::Time, 0.000001),
440        );
441        units.insert(
442            "ns".to_string(),
443            Unit::simple(UnitCategory::Time, 0.000000001),
444        );
445        units.insert(
446            "nanosecond".to_string(),
447            Unit::simple(UnitCategory::Time, 0.000000001),
448        );
449        units.insert(
450            "nanoseconds".to_string(),
451            Unit::simple(UnitCategory::Time, 0.000000001),
452        );
453
454        units.insert("min".to_string(), Unit::simple(UnitCategory::Time, 60.0));
455        units.insert("minute".to_string(), Unit::simple(UnitCategory::Time, 60.0));
456        units.insert(
457            "minutes".to_string(),
458            Unit::simple(UnitCategory::Time, 60.0),
459        );
460        units.insert("hr".to_string(), Unit::simple(UnitCategory::Time, 3600.0));
461        units.insert("hour".to_string(), Unit::simple(UnitCategory::Time, 3600.0));
462        units.insert(
463            "hours".to_string(),
464            Unit::simple(UnitCategory::Time, 3600.0),
465        );
466        units.insert("day".to_string(), Unit::simple(UnitCategory::Time, 86400.0));
467        units.insert(
468            "days".to_string(),
469            Unit::simple(UnitCategory::Time, 86400.0),
470        );
471        units.insert(
472            "week".to_string(),
473            Unit::simple(UnitCategory::Time, 604800.0),
474        );
475        units.insert(
476            "weeks".to_string(),
477            Unit::simple(UnitCategory::Time, 604800.0),
478        );
479        units.insert(
480            "month".to_string(),
481            Unit::simple(UnitCategory::Time, 2629746.0),
482        ); // Average month
483        units.insert(
484            "months".to_string(),
485            Unit::simple(UnitCategory::Time, 2629746.0),
486        );
487        units.insert(
488            "year".to_string(),
489            Unit::simple(UnitCategory::Time, 31557600.0),
490        ); // 365.25 days
491        units.insert(
492            "years".to_string(),
493            Unit::simple(UnitCategory::Time, 31557600.0),
494        );
495
496        // AREA - Base unit: square meter (m²)
497        units.insert("m2".to_string(), Unit::simple(UnitCategory::Area, 1.0));
498        units.insert("sqm".to_string(), Unit::simple(UnitCategory::Area, 1.0));
499        units.insert(
500            "square_meter".to_string(),
501            Unit::simple(UnitCategory::Area, 1.0),
502        );
503        units.insert(
504            "square_meters".to_string(),
505            Unit::simple(UnitCategory::Area, 1.0),
506        );
507
508        units.insert(
509            "km2".to_string(),
510            Unit::simple(UnitCategory::Area, 1000000.0),
511        );
512        units.insert(
513            "square_kilometer".to_string(),
514            Unit::simple(UnitCategory::Area, 1000000.0),
515        );
516        units.insert("cm2".to_string(), Unit::simple(UnitCategory::Area, 0.0001));
517        units.insert(
518            "square_centimeter".to_string(),
519            Unit::simple(UnitCategory::Area, 0.0001),
520        );
521
522        units.insert(
523            "sq_ft".to_string(),
524            Unit::simple(UnitCategory::Area, 0.09290304),
525        );
526        units.insert(
527            "square_foot".to_string(),
528            Unit::simple(UnitCategory::Area, 0.09290304),
529        );
530        units.insert(
531            "square_feet".to_string(),
532            Unit::simple(UnitCategory::Area, 0.09290304),
533        );
534        units.insert(
535            "sq_in".to_string(),
536            Unit::simple(UnitCategory::Area, 0.00064516),
537        );
538        units.insert(
539            "square_inch".to_string(),
540            Unit::simple(UnitCategory::Area, 0.00064516),
541        );
542        units.insert(
543            "sq_mi".to_string(),
544            Unit::simple(UnitCategory::Area, 2589988.110336),
545        );
546        units.insert(
547            "square_mile".to_string(),
548            Unit::simple(UnitCategory::Area, 2589988.110336),
549        );
550        units.insert(
551            "acre".to_string(),
552            Unit::simple(UnitCategory::Area, 4046.8564224),
553        );
554        units.insert(
555            "acres".to_string(),
556            Unit::simple(UnitCategory::Area, 4046.8564224),
557        );
558        units.insert(
559            "hectare".to_string(),
560            Unit::simple(UnitCategory::Area, 10000.0),
561        );
562        units.insert(
563            "hectares".to_string(),
564            Unit::simple(UnitCategory::Area, 10000.0),
565        );
566
567        // SPEED - Base unit: meters per second (m/s)
568        units.insert("mps".to_string(), Unit::simple(UnitCategory::Speed, 1.0));
569        units.insert("m/s".to_string(), Unit::simple(UnitCategory::Speed, 1.0));
570        units.insert(
571            "meters_per_second".to_string(),
572            Unit::simple(UnitCategory::Speed, 1.0),
573        );
574
575        units.insert(
576            "kph".to_string(),
577            Unit::simple(UnitCategory::Speed, 0.277777778),
578        );
579        units.insert(
580            "kmh".to_string(),
581            Unit::simple(UnitCategory::Speed, 0.277777778),
582        );
583        units.insert(
584            "km/h".to_string(),
585            Unit::simple(UnitCategory::Speed, 0.277777778),
586        );
587        units.insert(
588            "kilometers_per_hour".to_string(),
589            Unit::simple(UnitCategory::Speed, 0.277777778),
590        );
591
592        units.insert(
593            "mph".to_string(),
594            Unit::simple(UnitCategory::Speed, 0.44704),
595        );
596        units.insert(
597            "mi/h".to_string(),
598            Unit::simple(UnitCategory::Speed, 0.44704),
599        );
600        units.insert(
601            "miles_per_hour".to_string(),
602            Unit::simple(UnitCategory::Speed, 0.44704),
603        );
604
605        units.insert(
606            "knot".to_string(),
607            Unit::simple(UnitCategory::Speed, 0.514444444),
608        );
609        units.insert(
610            "knots".to_string(),
611            Unit::simple(UnitCategory::Speed, 0.514444444),
612        );
613        units.insert(
614            "kt".to_string(),
615            Unit::simple(UnitCategory::Speed, 0.514444444),
616        );
617
618        units.insert("fps".to_string(), Unit::simple(UnitCategory::Speed, 0.3048));
619        units.insert(
620            "ft/s".to_string(),
621            Unit::simple(UnitCategory::Speed, 0.3048),
622        );
623        units.insert(
624            "feet_per_second".to_string(),
625            Unit::simple(UnitCategory::Speed, 0.3048),
626        );
627
628        // Speed of light (for physics calculations)
629        // Note: "c" conflicts with Celsius, so we only use "light_speed"
630        units.insert(
631            "light_speed".to_string(),
632            Unit::simple(UnitCategory::Speed, 299792458.0),
633        );
634
635        // PRESSURE - Base unit: Pascal (Pa)
636        units.insert("pa".to_string(), Unit::simple(UnitCategory::Pressure, 1.0));
637        units.insert(
638            "pascal".to_string(),
639            Unit::simple(UnitCategory::Pressure, 1.0),
640        );
641        units.insert(
642            "pascals".to_string(),
643            Unit::simple(UnitCategory::Pressure, 1.0),
644        );
645
646        units.insert(
647            "kpa".to_string(),
648            Unit::simple(UnitCategory::Pressure, 1000.0),
649        );
650        units.insert(
651            "kilopascal".to_string(),
652            Unit::simple(UnitCategory::Pressure, 1000.0),
653        );
654        units.insert(
655            "mpa".to_string(),
656            Unit::simple(UnitCategory::Pressure, 1000000.0),
657        );
658        units.insert(
659            "megapascal".to_string(),
660            Unit::simple(UnitCategory::Pressure, 1000000.0),
661        );
662        units.insert(
663            "gpa".to_string(),
664            Unit::simple(UnitCategory::Pressure, 1000000000.0),
665        );
666        units.insert(
667            "gigapascal".to_string(),
668            Unit::simple(UnitCategory::Pressure, 1000000000.0),
669        );
670
671        units.insert(
672            "bar".to_string(),
673            Unit::simple(UnitCategory::Pressure, 100000.0),
674        );
675        units.insert(
676            "bars".to_string(),
677            Unit::simple(UnitCategory::Pressure, 100000.0),
678        );
679        units.insert(
680            "mbar".to_string(),
681            Unit::simple(UnitCategory::Pressure, 100.0),
682        );
683        units.insert(
684            "millibar".to_string(),
685            Unit::simple(UnitCategory::Pressure, 100.0),
686        );
687
688        units.insert(
689            "atm".to_string(),
690            Unit::simple(UnitCategory::Pressure, 101325.0),
691        );
692        units.insert(
693            "atmosphere".to_string(),
694            Unit::simple(UnitCategory::Pressure, 101325.0),
695        );
696        units.insert(
697            "atmospheres".to_string(),
698            Unit::simple(UnitCategory::Pressure, 101325.0),
699        );
700
701        units.insert(
702            "psi".to_string(),
703            Unit::simple(UnitCategory::Pressure, 6894.757293168),
704        );
705        units.insert(
706            "pounds_per_square_inch".to_string(),
707            Unit::simple(UnitCategory::Pressure, 6894.757293168),
708        );
709
710        units.insert(
711            "torr".to_string(),
712            Unit::simple(UnitCategory::Pressure, 133.322368421),
713        );
714        units.insert(
715            "mmhg".to_string(),
716            Unit::simple(UnitCategory::Pressure, 133.322368421),
717        );
718        units.insert(
719            "mm_hg".to_string(),
720            Unit::simple(UnitCategory::Pressure, 133.322368421),
721        );
722
723        UnitConverter { units }
724    }
725
726    /// Convert a value from one unit to another
727    pub fn convert(&self, value: f64, from_unit: &str, to_unit: &str) -> Result<f64> {
728        // Normalize unit names to lowercase for lookup
729        let from_key = from_unit.to_lowercase();
730        let to_key = to_unit.to_lowercase();
731
732        // Get unit definitions
733        let from = self
734            .units
735            .get(&from_key)
736            .ok_or_else(|| anyhow!("Unknown unit: {}", from_unit))?;
737        let to = self
738            .units
739            .get(&to_key)
740            .ok_or_else(|| anyhow!("Unknown unit: {}", to_unit))?;
741
742        // Check that units are in the same category
743        if from.category != to.category {
744            return Err(anyhow!(
745                "Cannot convert between different unit types: {} ({:?}) to {} ({:?})",
746                from_unit,
747                from.category,
748                to_unit,
749                to.category
750            ));
751        }
752
753        // Convert to SI base unit
754        let si_value = value * from.to_si_factor + from.to_si_offset;
755
756        // Convert from SI base unit to target unit
757        let result = si_value * to.from_si_factor + to.from_si_offset;
758
759        Ok(result)
760    }
761
762    /// Get list of supported units for a category
763    #[must_use]
764    pub fn get_units_for_category(&self, category: &str) -> Vec<String> {
765        let cat = match category.to_lowercase().as_str() {
766            "length" => Some(UnitCategory::Length),
767            "mass" | "weight" => Some(UnitCategory::Mass),
768            "temperature" | "temp" => Some(UnitCategory::Temperature),
769            "volume" => Some(UnitCategory::Volume),
770            "time" => Some(UnitCategory::Time),
771            "area" => Some(UnitCategory::Area),
772            "speed" | "velocity" => Some(UnitCategory::Speed),
773            "pressure" => Some(UnitCategory::Pressure),
774            _ => None,
775        };
776
777        if let Some(category) = cat {
778            self.units
779                .iter()
780                .filter(|(_, unit)| unit.category == category)
781                .map(|(name, _)| name.clone())
782                .collect()
783        } else {
784            Vec::new()
785        }
786    }
787}
788
789// Singleton instance
790lazy_static::lazy_static! {
791    static ref CONVERTER: UnitConverter = UnitConverter::new();
792}
793
794/// Public API for unit conversion
795pub fn convert_units(value: f64, from_unit: &str, to_unit: &str) -> Result<f64> {
796    CONVERTER.convert(value, from_unit, to_unit)
797}
798
799/// Get list of units for a category
800#[must_use]
801pub fn list_units(category: &str) -> Vec<String> {
802    CONVERTER.get_units_for_category(category)
803}
804
805#[cfg(test)]
806mod tests {
807    use super::*;
808
809    #[test]
810    fn test_length_conversions() {
811        let converter = UnitConverter::new();
812
813        // Kilometers to miles
814        let result = converter.convert(100.0, "km", "miles").unwrap();
815        assert!((result - 62.137).abs() < 0.01);
816
817        // Feet to meters
818        let result = converter.convert(100.0, "ft", "m").unwrap();
819        assert!((result - 30.48).abs() < 0.01);
820
821        // Inches to centimeters
822        let result = converter.convert(12.0, "in", "cm").unwrap();
823        assert!((result - 30.48).abs() < 0.01);
824    }
825
826    #[test]
827    fn test_mass_conversions() {
828        let converter = UnitConverter::new();
829
830        // Pounds to kilograms
831        let result = converter.convert(100.0, "lb", "kg").unwrap();
832        assert!((result - 45.359).abs() < 0.01);
833
834        // Ounces to grams
835        let result = converter.convert(16.0, "oz", "g").unwrap();
836        assert!((result - 453.592).abs() < 0.01);
837    }
838
839    #[test]
840    fn test_temperature_conversions() {
841        let converter = UnitConverter::new();
842
843        // Fahrenheit to Celsius
844        let result = converter.convert(32.0, "F", "C").unwrap();
845        assert!((result - 0.0).abs() < 0.01);
846
847        let result = converter.convert(212.0, "F", "C").unwrap();
848        assert!((result - 100.0).abs() < 0.01);
849
850        // Celsius to Kelvin
851        let result = converter.convert(0.0, "C", "K").unwrap();
852        assert!((result - 273.15).abs() < 0.01);
853    }
854
855    #[test]
856    fn test_volume_conversions() {
857        let converter = UnitConverter::new();
858
859        // Gallons to liters
860        let result = converter.convert(1.0, "gal", "L").unwrap();
861        assert!((result - 3.785).abs() < 0.01);
862
863        // Cups to milliliters
864        let result = converter.convert(1.0, "cup", "ml").unwrap();
865        assert!((result - 236.588).abs() < 0.01);
866    }
867
868    #[test]
869    fn test_invalid_conversion() {
870        let converter = UnitConverter::new();
871
872        // Cannot convert between different categories
873        let result = converter.convert(100.0, "km", "kg");
874        assert!(result.is_err());
875
876        // Unknown unit
877        let result = converter.convert(100.0, "xyz", "m");
878        assert!(result.is_err());
879    }
880
881    #[test]
882    fn test_case_insensitive() {
883        let converter = UnitConverter::new();
884
885        // Should handle various cases
886        let result1 = converter.convert(100.0, "KM", "MILES").unwrap();
887        let result2 = converter.convert(100.0, "km", "miles").unwrap();
888        let result3 = converter.convert(100.0, "Km", "Miles").unwrap();
889
890        assert!((result1 - result2).abs() < 0.001);
891        assert!((result2 - result3).abs() < 0.001);
892    }
893}