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