tlq_ucum/
quantity.rs

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        // Don't canonicalize bracketed / special units.
72        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    // Lower is better.
96    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}