1use crate::error::{Error, Result};
2use crate::unit::{DimensionVector, Unit, UnitKind};
3use once_cell::sync::Lazy;
4use rust_decimal::Decimal;
5use std::collections::HashMap;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct Quantity {
9 pub value: Decimal,
10 pub unit: String,
11}
12
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct NormalizedQuantity {
15 pub value: Decimal,
16 pub unit: String,
17}
18
19pub fn normalize(value: Decimal, unit: &str) -> Result<NormalizedQuantity> {
20 let u = Unit::parse(unit)?;
21 match &u.kind {
22 UnitKind::NonLinear => Err(Error::NonLinear(unit.into())),
23 UnitKind::Affine { .. } => normalize_to("K", &u, value),
24 UnitKind::Multiplicative { .. } => normalize_to_best(&u, value),
25 }
26}
27
28fn normalize_to(target_unit: &str, from: &Unit, value: Decimal) -> Result<NormalizedQuantity> {
29 let from_value = crate::unit::decimal_to_rational(value)?;
30 let base = from.to_base(&from_value)?;
31 let to = Unit::parse(target_unit)?;
32 let out = to.from_base(&base)?;
33 let out_decimal = crate::unit::rational_to_decimal(out)?;
34 Ok(NormalizedQuantity {
35 value: out_decimal,
36 unit: target_unit.into(),
37 })
38}
39
40fn normalize_to_best(from: &Unit, value: Decimal) -> Result<NormalizedQuantity> {
41 let from_value = crate::unit::decimal_to_rational(value)?;
42 let base = from.to_base(&from_value)?;
43
44 if let Some(target) = best_named_unit_for_dimension(from.dimensions) {
45 let to = Unit::parse(&target)?;
46 let out = to.from_base(&base)?;
47 let out_decimal = crate::unit::rational_to_decimal(out)?;
48 return Ok(NormalizedQuantity {
49 value: out_decimal,
50 unit: target,
51 });
52 }
53
54 let out_decimal = crate::unit::rational_to_decimal(base)?;
55 Ok(NormalizedQuantity {
56 value: out_decimal,
57 unit: render_base_expr(from.dimensions),
58 })
59}
60
61fn best_named_unit_for_dimension(dim: DimensionVector) -> Option<String> {
62 static CANON: Lazy<HashMap<DimensionVector, String>> = Lazy::new(build_canon_map);
63 CANON.get(&dim).cloned()
64}
65
66fn build_canon_map() -> HashMap<DimensionVector, String> {
67 let mut map: HashMap<DimensionVector, (u32, String)> = HashMap::new();
68 let db = crate::db();
69
70 for (code, def) in &db.units {
71 if def.is_special || def.is_arbitrary || code.starts_with('[') {
73 continue;
74 }
75 let Ok(u) = Unit::parse(code) else { continue };
76 let UnitKind::Multiplicative { factor: _ } = u.kind else { continue };
77 if u.dimensions == DimensionVector::ZERO {
78 continue;
79 }
80
81 let rank = rank(def, code);
82 let key = u.dimensions;
83 match map.get(&key) {
84 Some((cur_rank, _)) if *cur_rank <= rank => {}
85 _ => {
86 map.insert(key, (rank, code.clone()));
87 }
88 }
89 }
90
91 map.into_iter().map(|(k, (_, v))| (k, v)).collect()
92}
93
94fn rank(def: &crate::db::UnitDef, code: &str) -> u32 {
95 let mut score = 0u32;
97 if def.class.as_deref() == Some("si") {
98 score += 0;
99 } else {
100 score += 1000;
101 }
102 if def.is_metric {
103 score += 0;
104 } else {
105 score += 100;
106 }
107 score += code.len() as u32;
108 score
109}
110
111fn render_base_expr(dim: DimensionVector) -> String {
112 let mut out = String::new();
113 let parts = [
114 ("g", dim.0[1]),
115 ("mol", dim.0[7]),
116 ("m", dim.0[0]),
117 ("s", dim.0[2]),
118 ("K", dim.0[4]),
119 ("C", dim.0[5]),
120 ("rad", dim.0[3]),
121 ("cd", dim.0[6]),
122 ];
123 for (sym, exp) in parts {
124 if exp == 0 {
125 continue;
126 }
127 if !out.is_empty() {
128 out.push('.');
129 }
130 out.push_str(sym);
131 if exp != 1 {
132 out.push_str(&exp.to_string());
133 }
134 }
135 if out.is_empty() {
136 out.push('1');
137 }
138 out
139}