1use crate::error::LemmaError;
4use crate::semantic::*;
5use rust_decimal::Decimal;
6
7pub fn resolve_unit(value: Decimal, unit_str: &str) -> Result<LiteralValue, LemmaError> {
9 let unit_lower = unit_str.to_lowercase();
10
11 if let Some(unit) = try_parse_mass_unit(&unit_lower) {
13 return Ok(LiteralValue::Unit(NumericUnit::Mass(value, unit)));
14 }
15
16 if let Some(unit) = try_parse_length_unit(&unit_lower) {
17 return Ok(LiteralValue::Unit(NumericUnit::Length(value, unit)));
18 }
19
20 if let Some(unit) = try_parse_volume_unit(&unit_lower) {
21 return Ok(LiteralValue::Unit(NumericUnit::Volume(value, unit)));
22 }
23
24 if let Some(unit) = try_parse_duration_unit(&unit_lower) {
25 return Ok(LiteralValue::Unit(NumericUnit::Duration(value, unit)));
26 }
27
28 if let Some(unit) = try_parse_temperature_unit(&unit_lower) {
29 return Ok(LiteralValue::Unit(NumericUnit::Temperature(value, unit)));
30 }
31
32 if let Some(unit) = try_parse_power_unit(&unit_lower) {
33 return Ok(LiteralValue::Unit(NumericUnit::Power(value, unit)));
34 }
35
36 if let Some(unit) = try_parse_force_unit(&unit_lower) {
37 return Ok(LiteralValue::Unit(NumericUnit::Force(value, unit)));
38 }
39
40 if let Some(unit) = try_parse_pressure_unit(&unit_lower) {
41 return Ok(LiteralValue::Unit(NumericUnit::Pressure(value, unit)));
42 }
43
44 if let Some(unit) = try_parse_energy_unit(&unit_lower) {
45 return Ok(LiteralValue::Unit(NumericUnit::Energy(value, unit)));
46 }
47
48 if let Some(unit) = try_parse_frequency_unit(&unit_lower) {
49 return Ok(LiteralValue::Unit(NumericUnit::Frequency(value, unit)));
50 }
51
52 if let Some(unit) = try_parse_data_size_unit(&unit_lower) {
53 return Ok(LiteralValue::Unit(NumericUnit::Data(value, unit)));
54 }
55
56 if let Some(currency) = try_parse_money_unit(&unit_lower) {
57 return Ok(LiteralValue::Unit(NumericUnit::Money(value, currency)));
58 }
59
60 let suggestion = find_closest_unit(&unit_lower);
62 Err(LemmaError::Engine(format!(
63 "Unknown unit: '{}'. {}",
64 unit_str, suggestion
65 )))
66}
67
68fn try_parse_mass_unit(s: &str) -> Option<MassUnit> {
70 match s {
71 "kilogram" | "kilograms" => Some(MassUnit::Kilogram),
72 "gram" | "grams" => Some(MassUnit::Gram),
73 "milligram" | "milligrams" => Some(MassUnit::Milligram),
74 "ton" | "tons" | "tonne" | "tonnes" => Some(MassUnit::Ton),
75 "pound" | "pounds" => Some(MassUnit::Pound),
76 "ounce" | "ounces" => Some(MassUnit::Ounce),
77 _ => None,
78 }
79}
80
81fn try_parse_length_unit(s: &str) -> Option<LengthUnit> {
83 match s {
84 "kilometer" | "kilometers" | "kilometre" | "kilometres" => Some(LengthUnit::Kilometer),
85 "mile" | "miles" => Some(LengthUnit::Mile),
86 "nautical_mile" | "nautical_miles" | "nauticalmile" | "nauticalmiles" => {
87 Some(LengthUnit::NauticalMile)
88 }
89 "meter" | "meters" | "metre" | "metres" => Some(LengthUnit::Meter),
90 "decimeter" | "decimeters" | "decimetre" | "decimetres" => Some(LengthUnit::Decimeter),
91 "centimeter" | "centimeters" | "centimetre" | "centimetres" => Some(LengthUnit::Centimeter),
92 "millimeter" | "millimeters" | "millimetre" | "millimetres" => Some(LengthUnit::Millimeter),
93 "yard" | "yards" => Some(LengthUnit::Yard),
94 "foot" | "feet" => Some(LengthUnit::Foot),
95 "inch" | "inches" => Some(LengthUnit::Inch),
96 _ => None,
97 }
98}
99
100fn try_parse_volume_unit(s: &str) -> Option<VolumeUnit> {
102 match s {
103 "cubic_meter" | "cubic_meters" | "cubic_metre" | "cubic_metres" | "cubicmeter"
104 | "cubicmeters" | "cubicmetre" | "cubicmetres" => Some(VolumeUnit::CubicMeter),
105 "cubic_centimeter" | "cubic_centimeters" | "cubic_centimetre" | "cubic_centimetres"
106 | "cubiccentimeter" | "cubiccentimeters" => Some(VolumeUnit::CubicCentimeter),
107 "liter" | "liters" | "litre" | "litres" => Some(VolumeUnit::Liter),
108 "deciliter" | "deciliters" | "decilitre" | "decilitres" => Some(VolumeUnit::Deciliter),
109 "centiliter" | "centiliters" | "centilitre" | "centilitres" => Some(VolumeUnit::Centiliter),
110 "milliliter" | "milliliters" | "millilitre" | "millilitres" => Some(VolumeUnit::Milliliter),
111 "gallon" | "gallons" => Some(VolumeUnit::Gallon),
112 "quart" | "quarts" => Some(VolumeUnit::Quart),
113 "pint" | "pints" => Some(VolumeUnit::Pint),
114 "fluid_ounce" | "fluid_ounces" | "fluidounce" | "fluidounces" => {
115 Some(VolumeUnit::FluidOunce)
116 }
117 _ => None,
118 }
119}
120
121fn try_parse_duration_unit(s: &str) -> Option<DurationUnit> {
123 match s {
124 "year" | "years" => Some(DurationUnit::Year),
125 "month" | "months" => Some(DurationUnit::Month),
126 "week" | "weeks" => Some(DurationUnit::Week),
127 "day" | "days" => Some(DurationUnit::Day),
128 "hour" | "hours" => Some(DurationUnit::Hour),
129 "minute" | "minutes" => Some(DurationUnit::Minute),
130 "second" | "seconds" => Some(DurationUnit::Second),
131 "millisecond" | "milliseconds" => Some(DurationUnit::Millisecond),
132 "microsecond" | "microseconds" => Some(DurationUnit::Microsecond),
133 _ => None,
134 }
135}
136
137const SECONDS_PER_MINUTE: i32 = 60;
139const SECONDS_PER_HOUR: i32 = 3600; const SECONDS_PER_DAY: i32 = 86400; const SECONDS_PER_WEEK: i32 = 604800; const MILLISECONDS_PER_SECOND: i32 = 1000;
143const MICROSECONDS_PER_SECOND: i32 = 1000000;
144
145pub(crate) fn duration_to_seconds(value: Decimal, unit: &DurationUnit) -> Decimal {
151 match unit {
152 DurationUnit::Microsecond => value / Decimal::from(MICROSECONDS_PER_SECOND),
153 DurationUnit::Millisecond => value / Decimal::from(MILLISECONDS_PER_SECOND),
154 DurationUnit::Second => value,
155 DurationUnit::Minute => value * Decimal::from(SECONDS_PER_MINUTE),
156 DurationUnit::Hour => value * Decimal::from(SECONDS_PER_HOUR),
157 DurationUnit::Day => value * Decimal::from(SECONDS_PER_DAY),
158 DurationUnit::Week => value * Decimal::from(SECONDS_PER_WEEK),
159 DurationUnit::Month | DurationUnit::Year => {
160 Decimal::ZERO
164 }
165 }
166}
167
168fn try_parse_temperature_unit(s: &str) -> Option<TemperatureUnit> {
169 match s {
170 "celsius" => Some(TemperatureUnit::Celsius),
171 "fahrenheit" => Some(TemperatureUnit::Fahrenheit),
172 "kelvin" => Some(TemperatureUnit::Kelvin),
173 _ => None,
174 }
175}
176
177fn try_parse_power_unit(s: &str) -> Option<PowerUnit> {
179 match s {
180 "megawatt" | "megawatts" => Some(PowerUnit::Megawatt),
181 "kilowatt" | "kilowatts" => Some(PowerUnit::Kilowatt),
182 "watt" | "watts" => Some(PowerUnit::Watt),
183 "milliwatt" | "milliwatts" => Some(PowerUnit::Milliwatt),
184 "horsepower" => Some(PowerUnit::Horsepower),
185 _ => None,
186 }
187}
188
189fn try_parse_force_unit(s: &str) -> Option<ForceUnit> {
191 match s {
192 "newton" | "newtons" => Some(ForceUnit::Newton),
193 "kilonewton" | "kilonewtons" => Some(ForceUnit::Kilonewton),
194 "lbf" | "poundforce" => Some(ForceUnit::Lbf),
195 _ => None,
196 }
197}
198
199fn try_parse_pressure_unit(s: &str) -> Option<PressureUnit> {
201 match s {
202 "megapascal" | "megapascals" => Some(PressureUnit::Megapascal),
203 "kilopascal" | "kilopascals" => Some(PressureUnit::Kilopascal),
204 "pascal" | "pascals" => Some(PressureUnit::Pascal),
205 "atmosphere" | "atmospheres" => Some(PressureUnit::Atmosphere),
206 "bar" => Some(PressureUnit::Bar),
207 "psi" => Some(PressureUnit::Psi),
208 "torr" => Some(PressureUnit::Torr),
209 "mmhg" => Some(PressureUnit::Mmhg),
210 _ => None,
211 }
212}
213
214fn try_parse_energy_unit(s: &str) -> Option<EnergyUnit> {
216 match s {
217 "megajoule" | "megajoules" => Some(EnergyUnit::Megajoule),
218 "kilojoule" | "kilojoules" => Some(EnergyUnit::Kilojoule),
219 "joule" | "joules" => Some(EnergyUnit::Joule),
220 "kilowatthour" | "kilowatthours" => Some(EnergyUnit::Kilowatthour),
221 "watthour" | "watthours" => Some(EnergyUnit::Watthour),
222 "kilocalorie" | "kilocalories" => Some(EnergyUnit::Kilocalorie),
223 "calorie" | "calories" => Some(EnergyUnit::Calorie),
224 "btu" => Some(EnergyUnit::Btu),
225 _ => None,
226 }
227}
228
229fn try_parse_frequency_unit(s: &str) -> Option<FrequencyUnit> {
231 match s {
232 "hertz" => Some(FrequencyUnit::Hertz),
233 "kilohertz" => Some(FrequencyUnit::Kilohertz),
234 "megahertz" => Some(FrequencyUnit::Megahertz),
235 "gigahertz" => Some(FrequencyUnit::Gigahertz),
236 _ => None,
237 }
238}
239
240fn try_parse_data_size_unit(s: &str) -> Option<DataUnit> {
242 match s {
243 "petabyte" | "petabytes" => Some(DataUnit::Petabyte),
244 "terabyte" | "terabytes" => Some(DataUnit::Terabyte),
245 "gigabyte" | "gigabytes" => Some(DataUnit::Gigabyte),
246 "megabyte" | "megabytes" => Some(DataUnit::Megabyte),
247 "kilobyte" | "kilobytes" => Some(DataUnit::Kilobyte),
248 "byte" | "bytes" => Some(DataUnit::Byte),
249 "tebibyte" | "tebibytes" => Some(DataUnit::Tebibyte),
250 "gibibyte" | "gibibytes" => Some(DataUnit::Gibibyte),
251 "mebibyte" | "mebibytes" => Some(DataUnit::Mebibyte),
252 "kibibyte" | "kibibytes" => Some(DataUnit::Kibibyte),
253 _ => None,
254 }
255}
256
257fn try_parse_money_unit(s: &str) -> Option<MoneyUnit> {
259 match s {
260 "eur" => Some(MoneyUnit::Eur),
261 "usd" => Some(MoneyUnit::Usd),
262 "gbp" => Some(MoneyUnit::Gbp),
263 "jpy" => Some(MoneyUnit::Jpy),
264 "cny" => Some(MoneyUnit::Cny),
265 "chf" => Some(MoneyUnit::Chf),
266 "cad" => Some(MoneyUnit::Cad),
267 "aud" => Some(MoneyUnit::Aud),
268 "inr" => Some(MoneyUnit::Inr),
269 _ => None,
270 }
271}
272
273pub fn resolve_conversion_target(unit_str: &str) -> Result<ConversionTarget, LemmaError> {
275 let unit_lower = unit_str.to_lowercase();
276
277 if unit_lower == "percentage" || unit_lower == "percent" {
279 return Ok(ConversionTarget::Percentage);
280 }
281
282 if let Some(unit) = try_parse_mass_unit(&unit_lower) {
284 return Ok(ConversionTarget::Mass(unit));
285 }
286
287 if let Some(unit) = try_parse_length_unit(&unit_lower) {
288 return Ok(ConversionTarget::Length(unit));
289 }
290
291 if let Some(unit) = try_parse_volume_unit(&unit_lower) {
292 return Ok(ConversionTarget::Volume(unit));
293 }
294
295 if let Some(unit) = try_parse_duration_unit(&unit_lower) {
296 return Ok(ConversionTarget::Duration(unit));
297 }
298
299 if let Some(unit) = try_parse_temperature_unit(&unit_lower) {
300 return Ok(ConversionTarget::Temperature(unit));
301 }
302
303 if let Some(unit) = try_parse_power_unit(&unit_lower) {
304 return Ok(ConversionTarget::Power(unit));
305 }
306
307 if let Some(unit) = try_parse_force_unit(&unit_lower) {
308 return Ok(ConversionTarget::Force(unit));
309 }
310
311 if let Some(unit) = try_parse_pressure_unit(&unit_lower) {
312 return Ok(ConversionTarget::Pressure(unit));
313 }
314
315 if let Some(unit) = try_parse_energy_unit(&unit_lower) {
316 return Ok(ConversionTarget::Energy(unit));
317 }
318
319 if let Some(unit) = try_parse_frequency_unit(&unit_lower) {
320 return Ok(ConversionTarget::Frequency(unit));
321 }
322
323 if let Some(unit) = try_parse_data_size_unit(&unit_lower) {
324 return Ok(ConversionTarget::Data(unit));
325 }
326
327 if let Some(unit) = try_parse_money_unit(&unit_lower) {
328 return Ok(ConversionTarget::Money(unit));
329 }
330
331 let suggestion = find_closest_unit(&unit_lower);
333 Err(LemmaError::Engine(format!(
334 "Unknown conversion target unit: '{}'. {}",
335 unit_str, suggestion
336 )))
337}
338
339fn find_closest_unit(s: &str) -> String {
341 let suggestions: Vec<(&str, &str)> = vec![
343 ("kilometer", "kilometers"),
344 ("kilometre", "kilometers"),
345 ("metre", "meters"),
346 ("kilogramme", "kilograms"),
347 ("gramme", "grams"),
348 ("litre", "liters"),
349 ("sec", "seconds"),
350 ("min", "minutes"),
351 ("hr", "hours"),
352 ];
353
354 for (typo, correct) in &suggestions {
355 if s == *typo || s.starts_with(typo) {
356 return format!("Did you mean '{}'?", correct);
357 }
358 }
359
360 if s.len() <= 3 {
362 return "Try using the full unit name (e.g., 'kilometers' instead of 'km')".to_string();
363 }
364
365 "Check the unit name spelling".to_string()
366}