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 unreachable!("Calendar units (month/year) should be handled by date arithmetic")
161 }
162 }
163}
164
165fn try_parse_temperature_unit(s: &str) -> Option<TemperatureUnit> {
166 match s {
167 "celsius" => Some(TemperatureUnit::Celsius),
168 "fahrenheit" => Some(TemperatureUnit::Fahrenheit),
169 "kelvin" => Some(TemperatureUnit::Kelvin),
170 _ => None,
171 }
172}
173
174fn try_parse_power_unit(s: &str) -> Option<PowerUnit> {
176 match s {
177 "megawatt" | "megawatts" => Some(PowerUnit::Megawatt),
178 "kilowatt" | "kilowatts" => Some(PowerUnit::Kilowatt),
179 "watt" | "watts" => Some(PowerUnit::Watt),
180 "milliwatt" | "milliwatts" => Some(PowerUnit::Milliwatt),
181 "horsepower" => Some(PowerUnit::Horsepower),
182 _ => None,
183 }
184}
185
186fn try_parse_force_unit(s: &str) -> Option<ForceUnit> {
188 match s {
189 "newton" | "newtons" => Some(ForceUnit::Newton),
190 "kilonewton" | "kilonewtons" => Some(ForceUnit::Kilonewton),
191 "lbf" | "poundforce" => Some(ForceUnit::Lbf),
192 _ => None,
193 }
194}
195
196fn try_parse_pressure_unit(s: &str) -> Option<PressureUnit> {
198 match s {
199 "megapascal" | "megapascals" => Some(PressureUnit::Megapascal),
200 "kilopascal" | "kilopascals" => Some(PressureUnit::Kilopascal),
201 "pascal" | "pascals" => Some(PressureUnit::Pascal),
202 "atmosphere" | "atmospheres" => Some(PressureUnit::Atmosphere),
203 "bar" => Some(PressureUnit::Bar),
204 "psi" => Some(PressureUnit::Psi),
205 "torr" => Some(PressureUnit::Torr),
206 "mmhg" => Some(PressureUnit::Mmhg),
207 _ => None,
208 }
209}
210
211fn try_parse_energy_unit(s: &str) -> Option<EnergyUnit> {
213 match s {
214 "megajoule" | "megajoules" => Some(EnergyUnit::Megajoule),
215 "kilojoule" | "kilojoules" => Some(EnergyUnit::Kilojoule),
216 "joule" | "joules" => Some(EnergyUnit::Joule),
217 "kilowatthour" | "kilowatthours" => Some(EnergyUnit::Kilowatthour),
218 "watthour" | "watthours" => Some(EnergyUnit::Watthour),
219 "kilocalorie" | "kilocalories" => Some(EnergyUnit::Kilocalorie),
220 "calorie" | "calories" => Some(EnergyUnit::Calorie),
221 "btu" => Some(EnergyUnit::Btu),
222 _ => None,
223 }
224}
225
226fn try_parse_frequency_unit(s: &str) -> Option<FrequencyUnit> {
228 match s {
229 "hertz" => Some(FrequencyUnit::Hertz),
230 "kilohertz" => Some(FrequencyUnit::Kilohertz),
231 "megahertz" => Some(FrequencyUnit::Megahertz),
232 "gigahertz" => Some(FrequencyUnit::Gigahertz),
233 _ => None,
234 }
235}
236
237fn try_parse_data_size_unit(s: &str) -> Option<DataUnit> {
239 match s {
240 "petabyte" | "petabytes" => Some(DataUnit::Petabyte),
241 "terabyte" | "terabytes" => Some(DataUnit::Terabyte),
242 "gigabyte" | "gigabytes" => Some(DataUnit::Gigabyte),
243 "megabyte" | "megabytes" => Some(DataUnit::Megabyte),
244 "kilobyte" | "kilobytes" => Some(DataUnit::Kilobyte),
245 "byte" | "bytes" => Some(DataUnit::Byte),
246 "tebibyte" | "tebibytes" => Some(DataUnit::Tebibyte),
247 "gibibyte" | "gibibytes" => Some(DataUnit::Gibibyte),
248 "mebibyte" | "mebibytes" => Some(DataUnit::Mebibyte),
249 "kibibyte" | "kibibytes" => Some(DataUnit::Kibibyte),
250 _ => None,
251 }
252}
253
254fn try_parse_money_unit(s: &str) -> Option<MoneyUnit> {
256 match s {
257 "eur" => Some(MoneyUnit::Eur),
258 "usd" => Some(MoneyUnit::Usd),
259 "gbp" => Some(MoneyUnit::Gbp),
260 "jpy" => Some(MoneyUnit::Jpy),
261 "cny" => Some(MoneyUnit::Cny),
262 "chf" => Some(MoneyUnit::Chf),
263 "cad" => Some(MoneyUnit::Cad),
264 "aud" => Some(MoneyUnit::Aud),
265 "inr" => Some(MoneyUnit::Inr),
266 _ => None,
267 }
268}
269
270pub fn resolve_conversion_target(unit_str: &str) -> Result<ConversionTarget, LemmaError> {
272 let unit_lower = unit_str.to_lowercase();
273
274 if unit_lower == "percentage" || unit_lower == "percent" {
276 return Ok(ConversionTarget::Percentage);
277 }
278
279 if let Some(unit) = try_parse_mass_unit(&unit_lower) {
281 return Ok(ConversionTarget::Mass(unit));
282 }
283
284 if let Some(unit) = try_parse_length_unit(&unit_lower) {
285 return Ok(ConversionTarget::Length(unit));
286 }
287
288 if let Some(unit) = try_parse_volume_unit(&unit_lower) {
289 return Ok(ConversionTarget::Volume(unit));
290 }
291
292 if let Some(unit) = try_parse_duration_unit(&unit_lower) {
293 return Ok(ConversionTarget::Duration(unit));
294 }
295
296 if let Some(unit) = try_parse_temperature_unit(&unit_lower) {
297 return Ok(ConversionTarget::Temperature(unit));
298 }
299
300 if let Some(unit) = try_parse_power_unit(&unit_lower) {
301 return Ok(ConversionTarget::Power(unit));
302 }
303
304 if let Some(unit) = try_parse_force_unit(&unit_lower) {
305 return Ok(ConversionTarget::Force(unit));
306 }
307
308 if let Some(unit) = try_parse_pressure_unit(&unit_lower) {
309 return Ok(ConversionTarget::Pressure(unit));
310 }
311
312 if let Some(unit) = try_parse_energy_unit(&unit_lower) {
313 return Ok(ConversionTarget::Energy(unit));
314 }
315
316 if let Some(unit) = try_parse_frequency_unit(&unit_lower) {
317 return Ok(ConversionTarget::Frequency(unit));
318 }
319
320 if let Some(unit) = try_parse_data_size_unit(&unit_lower) {
321 return Ok(ConversionTarget::Data(unit));
322 }
323
324 if let Some(unit) = try_parse_money_unit(&unit_lower) {
325 return Ok(ConversionTarget::Money(unit));
326 }
327
328 let suggestion = find_closest_unit(&unit_lower);
330 Err(LemmaError::Engine(format!(
331 "Unknown conversion target unit: '{}'. {}",
332 unit_str, suggestion
333 )))
334}
335
336fn find_closest_unit(s: &str) -> String {
338 let suggestions: Vec<(&str, &str)> = vec![
340 ("kilometer", "kilometers"),
341 ("kilometre", "kilometers"),
342 ("metre", "meters"),
343 ("kilogramme", "kilograms"),
344 ("gramme", "grams"),
345 ("litre", "liters"),
346 ("sec", "seconds"),
347 ("min", "minutes"),
348 ("hr", "hours"),
349 ];
350
351 for (typo, correct) in &suggestions {
352 if s == *typo || s.starts_with(typo) {
353 return format!("Did you mean '{}'?", correct);
354 }
355 }
356
357 if s.len() <= 3 {
359 return "Try using the full unit name (e.g., 'kilometers' instead of 'km')".to_string();
360 }
361
362 "Check the unit name spelling".to_string()
363}