lemma/computation/
units.rs1use crate::evaluation::OperationResult;
6use crate::semantic::{DurationUnit, LiteralValue, TypeSpecification, Unit, Value};
7use crate::ConversionTarget;
8use rust_decimal::Decimal;
9
10pub 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 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
121fn 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
131pub 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), DurationUnit::Year => value * Decimal::from(31_536_000), }
144}
145
146pub 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), DurationUnit::Year => seconds / Decimal::from(31_536_000), }
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}