Skip to main content

lemma/computation/
units.rs

1//! Unit conversion system
2//!
3//! Handles conversions between duration units and scale units.
4
5use crate::evaluation::OperationResult;
6use crate::semantic::{DurationUnit, LiteralValue, TypeSpecification, Unit, Value};
7use crate::ConversionTarget;
8use rust_decimal::Decimal;
9
10/// Convert a value to a target unit (for `in` operator).
11///
12pub fn convert_unit(value: &LiteralValue, target: &ConversionTarget) -> OperationResult {
13    match &value.value {
14        Value::Duration(v, from) => match target {
15            ConversionTarget::Duration(to) => {
16                let val = convert_duration(*v, from, to);
17                OperationResult::Value(LiteralValue::duration_with_type(
18                    val,
19                    to.clone(),
20                    value.lemma_type.clone(),
21                ))
22            }
23            ConversionTarget::Percentage | ConversionTarget::ScaleUnit(_) => unreachable!(
24                "BUG: invalid conversion target {:?} for duration; this should be rejected during planning",
25                target
26            ),
27        },
28
29        Value::Number(n) => match target {
30            ConversionTarget::Duration(u) => {
31                OperationResult::Value(LiteralValue::duration(*n, u.clone()))
32            }
33            ConversionTarget::Percentage => {
34                // Convert number to ratio with percent unit (e.g., 0.5 -> 50%)
35                use crate::semantic::standard_ratio;
36                OperationResult::Value(LiteralValue::ratio_with_type(
37                    *n,
38                    Some("percent".to_string()),
39                    standard_ratio().clone(),
40                ))
41            }
42            ConversionTarget::ScaleUnit(_) => unreachable!(
43                "BUG: converting number to scale unit should be rejected during planning"
44            ),
45        },
46
47        Value::Ratio(r, unit_opt) => match target {
48            ConversionTarget::Percentage => OperationResult::Value(LiteralValue::ratio_with_type(
49                *r,
50                unit_opt.clone().or(Some("percent".to_string())),
51                value.lemma_type.clone(),
52            )),
53            ConversionTarget::Duration(_) | ConversionTarget::ScaleUnit(_) => unreachable!(
54                "BUG: invalid conversion target {:?} for ratio; this should be rejected during planning",
55                target
56            ),
57        },
58
59        Value::Scale(v, from_unit) => match target {
60            ConversionTarget::ScaleUnit(to_unit) => {
61                let from_unit = match from_unit {
62                    Some(u) => u,
63                    None => {
64                        unreachable!(
65                            "BUG: cannot convert scale value without a unit; unit must be provided by parsing/input validation"
66                        );
67                    }
68                };
69
70                let from_factor = scale_unit_factor(&value.lemma_type, from_unit);
71                let to_factor = scale_unit_factor(&value.lemma_type, to_unit);
72
73                let converted = (*v) * (to_factor / from_factor);
74
75                OperationResult::Value(LiteralValue::scale_with_type(
76                    converted,
77                    Some(to_unit.clone()),
78                    value.lemma_type.clone(),
79                ))
80            }
81            ConversionTarget::Duration(_) | ConversionTarget::Percentage => unreachable!(
82                "BUG: invalid conversion target {:?} for scale; this should be rejected during planning",
83                target
84            ),
85        },
86
87        _ => unreachable!(
88            "BUG: unsupported unit conversion during evaluation: {} -> {}",
89            value,
90            target
91        ),
92    }
93}
94
95fn scale_unit_factor(lemma_type: &crate::semantic::LemmaType, unit_name: &str) -> Decimal {
96    let units = match &lemma_type.specifications {
97        TypeSpecification::Scale { units, .. } => units,
98        _ => unreachable!(
99            "BUG: scale_unit_factor called with non-scale type {}",
100            lemma_type.name()
101        ),
102    };
103
104    match units
105        .iter()
106        .find(|u| u.name.eq_ignore_ascii_case(unit_name))
107    {
108        Some(Unit { value, .. }) => *value,
109        None => {
110            let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
111            unreachable!(
112                "BUG: unknown unit '{}' for scale type {}. Valid units: {}",
113                unit_name,
114                lemma_type.name(),
115                valid.join(", ")
116            );
117        }
118    }
119}
120
121/// Convert a duration value between units
122fn convert_duration(value: Decimal, from: &DurationUnit, to: &DurationUnit) -> Decimal {
123    if from == to {
124        return value;
125    }
126
127    let seconds = duration_to_seconds(value, from);
128    seconds_to_duration(seconds, to)
129}
130
131/// Convert a duration value to seconds (base unit)
132pub fn duration_to_seconds(value: Decimal, unit: &DurationUnit) -> Decimal {
133    match unit {
134        DurationUnit::Microsecond => value / Decimal::from(1_000_000),
135        DurationUnit::Millisecond => value / Decimal::from(1_000),
136        DurationUnit::Second => value,
137        DurationUnit::Minute => value * Decimal::from(60),
138        DurationUnit::Hour => value * Decimal::from(3_600),
139        DurationUnit::Day => value * Decimal::from(86_400),
140        DurationUnit::Week => value * Decimal::from(604_800),
141        DurationUnit::Month => value * Decimal::from(2_592_000), // 30 days
142        DurationUnit::Year => value * Decimal::from(31_536_000), // 365 days
143    }
144}
145
146/// Convert seconds to a duration value in the target unit
147pub fn seconds_to_duration(seconds: Decimal, unit: &DurationUnit) -> Decimal {
148    match unit {
149        DurationUnit::Microsecond => seconds * Decimal::from(1_000_000),
150        DurationUnit::Millisecond => seconds * Decimal::from(1_000),
151        DurationUnit::Second => seconds,
152        DurationUnit::Minute => seconds / Decimal::from(60),
153        DurationUnit::Hour => seconds / Decimal::from(3_600),
154        DurationUnit::Day => seconds / Decimal::from(86_400),
155        DurationUnit::Week => seconds / Decimal::from(604_800),
156        DurationUnit::Month => seconds / Decimal::from(2_592_000), // 30 days
157        DurationUnit::Year => seconds / Decimal::from(31_536_000), // 365 days
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn duration_conversion() {
167        let result = convert_duration(Decimal::from(2), &DurationUnit::Hour, &DurationUnit::Minute);
168        assert_eq!(result, Decimal::from(120));
169    }
170
171    #[test]
172    fn duration_seconds_roundtrip() {
173        let original = Decimal::from(5);
174        let seconds = duration_to_seconds(original, &DurationUnit::Day);
175        let back = seconds_to_duration(seconds, &DurationUnit::Day);
176        assert_eq!(original, back);
177    }
178}