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 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 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 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 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 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 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 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 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
445pub 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 Unit::new(
455 symbol.clone(),
456 symbol.clone(),
457 Dimension::Count,
458 Decimal::from(1),
459 symbol.clone(),
460 )
461 })
462}