sea_core/units/
mod.rs

1use rust_decimal::prelude::FromPrimitive;
2use rust_decimal::Decimal;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::sync::{OnceLock, RwLock};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum Dimension {
9    Mass,
10    Length,
11    Volume,
12    Currency,
13    Time,
14    Temperature,
15    Count,
16    Custom(String),
17}
18
19impl Dimension {
20    /// Parse a dimension name in a case-insensitive way and map to builtin dimension
21    pub fn parse(name: &str) -> Self {
22        match name.to_ascii_lowercase().as_str() {
23            "mass" => Dimension::Mass,
24            "length" => Dimension::Length,
25            "volume" => Dimension::Volume,
26            "currency" => Dimension::Currency,
27            "time" => Dimension::Time,
28            "temperature" => Dimension::Temperature,
29            "count" => Dimension::Count,
30            other => Dimension::Custom(other.to_string()),
31        }
32    }
33}
34
35impl std::str::FromStr for Dimension {
36    type Err = ();
37
38    fn from_str(s: &str) -> Result<Self, Self::Err> {
39        Ok(Dimension::parse(s))
40    }
41}
42
43impl std::fmt::Display for Dimension {
44    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45        match self {
46            Dimension::Mass => write!(f, "Mass"),
47            Dimension::Length => write!(f, "Length"),
48            Dimension::Volume => write!(f, "Volume"),
49            Dimension::Currency => write!(f, "Currency"),
50            Dimension::Time => write!(f, "Time"),
51            Dimension::Temperature => write!(f, "Temperature"),
52            Dimension::Count => write!(f, "Count"),
53            Dimension::Custom(s) => write!(f, "{}", s),
54        }
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
59pub struct Unit {
60    symbol: String,
61    name: String,
62    dimension: Dimension,
63    base_factor: Decimal,
64    base_unit: String,
65}
66
67impl std::fmt::Display for Unit {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        write!(f, "{}", self.symbol)
70    }
71}
72
73impl Unit {
74    pub fn new(
75        symbol: impl Into<String>,
76        name: impl Into<String>,
77        dimension: Dimension,
78        base_factor: Decimal,
79        base_unit: impl Into<String>,
80    ) -> Self {
81        let symbol = symbol.into();
82        Self {
83            symbol,
84            name: name.into(),
85            dimension,
86            base_factor,
87            base_unit: base_unit.into(),
88        }
89    }
90
91    pub fn new_base(
92        symbol: impl Into<String>,
93        name: impl Into<String>,
94        dimension: Dimension,
95    ) -> Result<Self, UnitError> {
96        let symbol = symbol.into();
97        let base_unit = symbol.clone();
98        Ok(Self {
99            symbol,
100            name: name.into(),
101            dimension,
102            base_factor: Decimal::ONE,
103            base_unit,
104        })
105    }
106
107    pub fn with_base(mut self, base_unit: impl Into<String>) -> Self {
108        self.base_unit = base_unit.into();
109        self
110    }
111
112    pub fn symbol(&self) -> &str {
113        &self.symbol
114    }
115    pub fn name(&self) -> &str {
116        &self.name
117    }
118    pub fn dimension(&self) -> &Dimension {
119        &self.dimension
120    }
121    pub fn base_factor(&self) -> Decimal {
122        self.base_factor
123    }
124    pub fn base_unit(&self) -> &str {
125        &self.base_unit
126    }
127}
128
129pub trait UnitConversion {
130    fn convert_to_base(&self, value: Decimal) -> Decimal;
131    fn convert_from_base(&self, value: Decimal) -> Decimal;
132}
133
134impl UnitConversion for Unit {
135    fn convert_to_base(&self, value: Decimal) -> Decimal {
136        value * self.base_factor
137    }
138
139    fn convert_from_base(&self, value: Decimal) -> Decimal {
140        value / self.base_factor
141    }
142}
143
144#[derive(Debug, Clone, PartialEq)]
145pub enum UnitError {
146    UnitNotFound(String),
147    IncompatibleDimensions { from: Dimension, to: Dimension },
148    ConversionNotDefined { from: String, to: String },
149    ZeroBaseFactor,
150    DuplicateUnit(String),
151}
152
153impl std::fmt::Display for UnitError {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        match self {
156            UnitError::UnitNotFound(symbol) => write!(f, "Unit not found: {}", symbol),
157            UnitError::IncompatibleDimensions { from, to } => {
158                write!(f, "Cannot convert between {:?} and {:?}", from, to)
159            }
160            UnitError::ConversionNotDefined { from, to } => {
161                write!(f, "Conversion not defined from {} to {}", from, to)
162            }
163            UnitError::ZeroBaseFactor => {
164                write!(f, "Unit base_factor cannot be zero")
165            }
166            UnitError::DuplicateUnit(symbol) => {
167                write!(f, "Unit already registered: {}", symbol)
168            }
169        }
170    }
171}
172
173impl std::error::Error for UnitError {}
174
175#[derive(Debug, Clone)]
176pub struct UnitRegistry {
177    units: HashMap<String, Unit>,
178    base_units: HashMap<Dimension, String>,
179}
180
181impl Default for UnitRegistry {
182    fn default() -> Self {
183        let mut registry = Self {
184            units: HashMap::new(),
185            base_units: HashMap::new(),
186        };
187
188        // Mass units
189        registry.register_base(Dimension::Mass, "kg");
190        registry.register_builtin(Unit::new(
191            "kg",
192            "kilogram",
193            Dimension::Mass,
194            Decimal::from(1),
195            "kg",
196        ));
197        registry.register_builtin(Unit::new(
198            "g",
199            "gram",
200            Dimension::Mass,
201            Decimal::new(1, 3),
202            "kg",
203        ));
204        registry.register_builtin(Unit::new(
205            "lb",
206            "pound",
207            Dimension::Mass,
208            Decimal::new(45359237, 8),
209            "kg",
210        ));
211
212        // Length units
213        registry.register_base(Dimension::Length, "m");
214        registry.register_builtin(Unit::new(
215            "m",
216            "meter",
217            Dimension::Length,
218            Decimal::from(1),
219            "m",
220        ));
221        registry.register_builtin(Unit::new(
222            "cm",
223            "centimeter",
224            Dimension::Length,
225            Decimal::new(1, 2),
226            "m",
227        ));
228        registry.register_builtin(Unit::new(
229            "in",
230            "inch",
231            Dimension::Length,
232            Decimal::new(254, 4),
233            "m",
234        ));
235
236        // Volume units
237        registry.register_base(Dimension::Volume, "L");
238        registry.register_builtin(Unit::new(
239            "L",
240            "liter",
241            Dimension::Volume,
242            Decimal::from(1),
243            "L",
244        ));
245        registry.register_builtin(Unit::new(
246            "mL",
247            "milliliter",
248            Dimension::Volume,
249            Decimal::new(1, 3),
250            "L",
251        ));
252
253        // Currency units (no conversion without exchange rates)
254        registry.register_base(Dimension::Currency, "USD");
255        registry.register_builtin(Unit::new(
256            "USD",
257            "US Dollar",
258            Dimension::Currency,
259            Decimal::from(1),
260            "USD",
261        ));
262        registry.register_builtin(Unit::new(
263            "EUR",
264            "Euro",
265            Dimension::Currency,
266            Decimal::from(1),
267            "EUR",
268        ));
269        registry.register_builtin(Unit::new(
270            "GBP",
271            "British Pound",
272            Dimension::Currency,
273            Decimal::from(1),
274            "GBP",
275        ));
276
277        // Time units
278        registry.register_base(Dimension::Time, "s");
279        registry.register_builtin(Unit::new(
280            "s",
281            "second",
282            Dimension::Time,
283            Decimal::from(1),
284            "s",
285        ));
286        registry.register_builtin(Unit::new(
287            "min",
288            "minute",
289            Dimension::Time,
290            Decimal::from(60),
291            "s",
292        ));
293        registry.register_builtin(Unit::new(
294            "h",
295            "hour",
296            Dimension::Time,
297            Decimal::from(3600),
298            "s",
299        ));
300        registry.register_builtin(Unit::new(
301            "ms",
302            "millisecond",
303            Dimension::Time,
304            Decimal::new(1, 3),
305            "s",
306        ));
307        registry.register_builtin(Unit::new(
308            "us",
309            "microsecond",
310            Dimension::Time,
311            Decimal::new(1, 6),
312            "s",
313        ));
314        registry.register_builtin(Unit::new(
315            "ns",
316            "nanosecond",
317            Dimension::Time,
318            Decimal::new(1, 9),
319            "s",
320        ));
321
322        // Count (dimensionless)
323        registry.register_base(Dimension::Count, "units");
324        registry.register_builtin(Unit::new(
325            "units",
326            "units",
327            Dimension::Count,
328            Decimal::from(1),
329            "units",
330        ));
331        registry.register_builtin(Unit::new(
332            "items",
333            "items",
334            Dimension::Count,
335            Decimal::from(1),
336            "items",
337        ));
338
339        registry
340    }
341}
342
343impl UnitRegistry {
344    pub fn new() -> Self {
345        Self {
346            units: HashMap::new(),
347            base_units: HashMap::new(),
348        }
349    }
350
351    pub fn register(&mut self, unit: Unit) -> Result<(), UnitError> {
352        if self.units.contains_key(&unit.symbol) {
353            return Err(UnitError::DuplicateUnit(unit.symbol.clone()));
354        }
355        self.units.insert(unit.symbol.clone(), unit);
356        Ok(())
357    }
358
359    fn register_builtin(&mut self, unit: Unit) {
360        let _ = self.register(unit);
361    }
362
363    pub fn register_dimension(&mut self, dimension: Dimension) {
364        self.base_units.entry(dimension).or_default();
365    }
366
367    pub fn register_base(&mut self, dimension: Dimension, base_unit: impl Into<String>) {
368        self.base_units.insert(dimension, base_unit.into());
369    }
370
371    pub fn get_unit(&self, symbol: &str) -> Result<&Unit, UnitError> {
372        self.units
373            .get(symbol)
374            .ok_or_else(|| UnitError::UnitNotFound(symbol.to_string()))
375    }
376
377    pub fn units(&self) -> &HashMap<String, Unit> {
378        &self.units
379    }
380
381    pub fn base_units(&self) -> &HashMap<Dimension, String> {
382        &self.base_units
383    }
384
385    pub fn convert(&self, value: Decimal, from: &Unit, to: &Unit) -> Result<Decimal, UnitError> {
386        if from.dimension != to.dimension {
387            return Err(UnitError::IncompatibleDimensions {
388                from: from.dimension.clone(),
389                to: to.dimension.clone(),
390            });
391        }
392
393        if matches!(from.dimension, Dimension::Currency) && from.symbol != to.symbol {
394            return Err(UnitError::ConversionNotDefined {
395                from: from.symbol.clone(),
396                to: to.symbol.clone(),
397            });
398        }
399
400        let in_base = from.convert_to_base(value);
401        let in_target = to.convert_from_base(in_base);
402
403        Ok(in_target)
404    }
405
406    pub fn global() -> &'static RwLock<UnitRegistry> {
407        static GLOBAL_REGISTRY: OnceLock<RwLock<UnitRegistry>> = OnceLock::new();
408        GLOBAL_REGISTRY.get_or_init(|| RwLock::new(UnitRegistry::default()))
409    }
410
411    /// Register units defined in a JSON string of the form:
412    /// [{ "symbol": "X", "name": "Name", "dimension": "Currency", "base_factor": 1.0, "base_unit": "USD" }]
413    pub fn register_from_json(&mut self, json: &str) -> Result<(), UnitError> {
414        #[derive(Deserialize)]
415        struct UnitConfig {
416            symbol: String,
417            name: String,
418            dimension: String,
419            base_factor: f64,
420            base_unit: String,
421        }
422
423        let parsed: Vec<UnitConfig> =
424            serde_json::from_str(json).map_err(|e| UnitError::ConversionNotDefined {
425                from: "json".to_string(),
426                to: e.to_string(),
427            })?;
428        for cfg in parsed {
429            let dim = Dimension::parse(&cfg.dimension);
430            let factor = Decimal::from_f64(cfg.base_factor).ok_or(UnitError::ZeroBaseFactor)?;
431            if factor == Decimal::ZERO {
432                return Err(UnitError::ZeroBaseFactor);
433            }
434            let unit = Unit::new(cfg.symbol, cfg.name, dim, factor, cfg.base_unit);
435            self.register(unit)?;
436        }
437        Ok(())
438    }
439}
440
441pub fn get_default_registry() -> &'static RwLock<UnitRegistry> {
442    UnitRegistry::global()
443}
444
445/// Helper function to get a Unit from a string symbol, using the default registry
446/// Returns a Count-based unit if the symbol is not found
447pub fn unit_from_string(symbol: impl Into<String>) -> Unit {
448    let symbol = symbol.into();
449    let registry = get_default_registry();
450    let registry = registry.read().unwrap_or_else(|e| e.into_inner());
451
452    registry.get_unit(&symbol).cloned().unwrap_or_else(|_| {
453        // Default to Count dimension for unknown units
454        Unit::new(
455            symbol.clone(),
456            symbol.clone(),
457            Dimension::Count,
458            Decimal::from(1),
459            symbol.clone(),
460        )
461    })
462}