1use crate::error::PncError;
2use crate::value::Value;
3use num_bigint::BigInt;
4use num_rational::BigRational;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Dimension {
11 Temperature,
12 Length,
13 Mass,
14 Time,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub enum UnitId {
21 Celsius,
23 Fahrenheit,
24 Kelvin,
25 Millimeter,
27 Centimeter,
28 Meter,
29 Kilometer,
30 Mile,
31 Foot,
32 Inch,
33 Yard,
34 Milligram,
36 Gram,
37 Kilogram,
38 Pound,
39 Ounce,
40 Millisecond,
42 Second,
43 Minute,
44 Hour,
45 Day,
46 Week,
47 Unknown(String),
49}
50
51impl fmt::Display for UnitId {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "{}", self.symbol())
54 }
55}
56
57impl UnitId {
58 pub fn symbol(&self) -> &str {
59 match self {
60 UnitId::Celsius => "c",
61 UnitId::Fahrenheit => "f",
62 UnitId::Kelvin => "k",
63 UnitId::Millimeter => "mm",
64 UnitId::Centimeter => "cm",
65 UnitId::Meter => "m",
66 UnitId::Kilometer => "km",
67 UnitId::Mile => "mi",
68 UnitId::Foot => "ft",
69 UnitId::Inch => "in",
70 UnitId::Yard => "yd",
71 UnitId::Milligram => "mg",
72 UnitId::Gram => "g",
73 UnitId::Kilogram => "kg",
74 UnitId::Pound => "lb",
75 UnitId::Ounce => "oz",
76 UnitId::Millisecond => "ms",
77 UnitId::Second => "s",
78 UnitId::Minute => "min",
79 UnitId::Hour => "h",
80 UnitId::Day => "d",
81 UnitId::Week => "wk",
82 UnitId::Unknown(s) => s,
83 }
84 }
85
86 pub fn from_symbol(s: &str) -> Option<UnitId> {
87 match s {
88 "c" => Some(UnitId::Celsius),
89 "f" => Some(UnitId::Fahrenheit),
90 "k" => Some(UnitId::Kelvin),
91 "mm" => Some(UnitId::Millimeter),
92 "cm" => Some(UnitId::Centimeter),
93 "m" => Some(UnitId::Meter),
94 "km" => Some(UnitId::Kilometer),
95 "mi" => Some(UnitId::Mile),
96 "ft" => Some(UnitId::Foot),
97 "in" => Some(UnitId::Inch),
98 "yd" => Some(UnitId::Yard),
99 "mg" => Some(UnitId::Milligram),
100 "g" => Some(UnitId::Gram),
101 "kg" => Some(UnitId::Kilogram),
102 "lb" => Some(UnitId::Pound),
103 "oz" => Some(UnitId::Ounce),
104 "ms" => Some(UnitId::Millisecond),
105 "s" => Some(UnitId::Second),
106 "min" => Some(UnitId::Minute),
107 "h" => Some(UnitId::Hour),
108 "d" => Some(UnitId::Day),
109 "wk" => Some(UnitId::Week),
110 _ => None,
111 }
112 }
113
114 pub fn dimension(&self) -> Option<Dimension> {
115 match self {
116 UnitId::Celsius | UnitId::Fahrenheit | UnitId::Kelvin => Some(Dimension::Temperature),
117 UnitId::Millimeter
118 | UnitId::Centimeter
119 | UnitId::Meter
120 | UnitId::Kilometer
121 | UnitId::Mile
122 | UnitId::Foot
123 | UnitId::Inch
124 | UnitId::Yard => Some(Dimension::Length),
125 UnitId::Milligram | UnitId::Gram | UnitId::Kilogram | UnitId::Pound | UnitId::Ounce => {
126 Some(Dimension::Mass)
127 }
128 UnitId::Millisecond
129 | UnitId::Second
130 | UnitId::Minute
131 | UnitId::Hour
132 | UnitId::Day
133 | UnitId::Week => Some(Dimension::Time),
134 UnitId::Unknown(_) => None,
135 }
136 }
137
138 pub(crate) fn factor(&self) -> BigRational {
139 match self {
140 UnitId::Celsius => BigRational::from_integer(BigInt::from(1)),
141 UnitId::Fahrenheit => BigRational::new(BigInt::from(5), BigInt::from(9)),
142 UnitId::Kelvin => BigRational::from_integer(BigInt::from(1)),
143 UnitId::Millimeter => BigRational::new(BigInt::from(1), BigInt::from(1000)),
144 UnitId::Centimeter => BigRational::new(BigInt::from(1), BigInt::from(100)),
145 UnitId::Meter => BigRational::from_integer(BigInt::from(1)),
146 UnitId::Kilometer => BigRational::from_integer(BigInt::from(1000)),
147 UnitId::Mile => BigRational::new(BigInt::from(1609344), BigInt::from(1000)),
148 UnitId::Foot => BigRational::new(BigInt::from(3048), BigInt::from(10000)),
149 UnitId::Inch => BigRational::new(BigInt::from(254), BigInt::from(10000)),
150 UnitId::Yard => BigRational::new(BigInt::from(9144), BigInt::from(10000)),
151 UnitId::Milligram => BigRational::new(BigInt::from(1), BigInt::from(1000000)),
152 UnitId::Gram => BigRational::new(BigInt::from(1), BigInt::from(1000)),
153 UnitId::Kilogram => BigRational::from_integer(BigInt::from(1)),
154 UnitId::Pound => BigRational::new(BigInt::from(45359237), BigInt::from(100000000)),
155 UnitId::Ounce => BigRational::new(BigInt::from(45359237), BigInt::from(1600000000)),
156 UnitId::Millisecond => BigRational::new(BigInt::from(1), BigInt::from(1000)),
157 UnitId::Second => BigRational::from_integer(BigInt::from(1)),
158 UnitId::Minute => BigRational::from_integer(BigInt::from(60)),
159 UnitId::Hour => BigRational::from_integer(BigInt::from(3600)),
160 UnitId::Day => BigRational::from_integer(BigInt::from(86400)),
161 UnitId::Week => BigRational::from_integer(BigInt::from(604800)),
162 UnitId::Unknown(_) => BigRational::from_integer(BigInt::from(1)),
163 }
164 }
165
166 pub(crate) fn offset(&self) -> BigRational {
167 match self {
168 UnitId::Celsius => BigRational::new(BigInt::from(27315), BigInt::from(100)),
169 UnitId::Fahrenheit => BigRational::new(BigInt::from(45967), BigInt::from(180)),
170 _ => BigRational::from_integer(BigInt::from(0)),
171 }
172 }
173
174 pub fn units_in_dimension(d: Dimension) -> &'static [UnitId] {
175 match d {
176 Dimension::Temperature => &[UnitId::Celsius, UnitId::Fahrenheit, UnitId::Kelvin],
177 Dimension::Length => &[
178 UnitId::Millimeter,
179 UnitId::Centimeter,
180 UnitId::Meter,
181 UnitId::Kilometer,
182 UnitId::Mile,
183 UnitId::Foot,
184 UnitId::Inch,
185 UnitId::Yard,
186 ],
187 Dimension::Mass => &[
188 UnitId::Milligram,
189 UnitId::Gram,
190 UnitId::Kilogram,
191 UnitId::Pound,
192 UnitId::Ounce,
193 ],
194 Dimension::Time => &[
195 UnitId::Millisecond,
196 UnitId::Second,
197 UnitId::Minute,
198 UnitId::Hour,
199 UnitId::Day,
200 UnitId::Week,
201 ],
202 }
203 }
204}
205
206pub fn convert(value: &Value, target: UnitId) -> Result<Value, PncError> {
212 match value {
214 Value::Int(n) => {
215 return Ok(Value::Unit {
216 number: Box::new(Value::Int(n.clone())),
217 unit: target,
218 });
219 }
220 Value::Rational(r) => {
221 return Ok(Value::Unit {
222 number: Box::new(Value::Rational(r.clone())),
223 unit: target,
224 });
225 }
226 _ => {}
227 }
228
229 if let Value::Unit {
231 number,
232 unit: source,
233 } = value
234 {
235 if *source == target {
237 return Ok(value.clone());
238 }
239 if source.dimension().is_none() || target.dimension().is_none() {
241 return Err(PncError::InvalidUnitConversion);
242 }
243 if source.dimension() != target.dimension() {
245 return Err(PncError::InvalidUnitConversion);
246 }
247
248 let num = match number.as_ref() {
250 Value::Int(n) => BigRational::from_integer(n.clone()),
251 Value::Rational(r) => r.clone(),
252 _ => return Err(PncError::InvalidUnitConversion),
253 };
254 let base = num * source.factor() + source.offset();
255 let result = (base - target.offset()) / target.factor();
256
257 crate::ops::check_precision(&Value::Rational(result.clone()))
259 .map_err(|_| PncError::PrecisionBudgetExceeded)?;
260
261 let normalized = if result.denom() == &BigInt::from(1) {
263 Value::Int(result.numer().clone())
264 } else {
265 Value::Rational(result)
266 };
267
268 return Ok(Value::Unit {
269 number: Box::new(normalized),
270 unit: target,
271 });
272 }
273
274 Err(PncError::InvalidUnitConversion)
275}