1#![forbid(unsafe_code)]
28#![doc(html_root_url = "https://docs.rs/convert-units/0.1.0")]
29#![allow(
30 clippy::unreadable_literal,
31 clippy::must_use_candidate,
32 clippy::float_cmp
34)]
35
36use core::fmt;
37
38mod data;
39use data::{Unit, ANCHOR_RATIOS, UNITS};
40
41#[cfg(doctest)]
43#[doc = include_str!("../README.md")]
44struct ReadmeDoctests;
45
46#[derive(Debug, Clone, PartialEq, Eq)]
48pub enum Error {
49 UnsupportedUnit(String),
51 IncompatibleMeasures {
53 from: &'static str,
55 to: &'static str,
57 },
58}
59
60impl fmt::Display for Error {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 match self {
63 Error::UnsupportedUnit(u) => write!(f, "unsupported unit: {u}"),
64 Error::IncompatibleMeasures { from, to } => {
65 write!(f, "cannot convert incompatible measures of {to} and {from}")
66 }
67 }
68 }
69}
70
71impl std::error::Error for Error {}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct UnitInfo {
76 pub abbr: &'static str,
78 pub measure: &'static str,
80 pub system: &'static str,
82 pub singular: &'static str,
84 pub plural: &'static str,
86}
87
88#[derive(Debug, Clone, Copy, PartialEq)]
90pub struct Best {
91 pub val: f64,
93 pub unit: &'static str,
95 pub singular: &'static str,
97 pub plural: &'static str,
99}
100
101fn find_unit(abbr: &str) -> Option<&'static Unit> {
102 UNITS.iter().find(|u| u.abbr == abbr)
103}
104
105fn anchor_ratio(measure: &str, system: &str) -> f64 {
106 ANCHOR_RATIOS
107 .iter()
108 .find(|(m, s, _)| *m == measure && *s == system)
109 .map_or(1.0, |(_, _, r)| *r)
110}
111
112fn temperature_transform(from_system: &str, value: f64) -> f64 {
114 if from_system == "metric" {
115 value / (5.0 / 9.0) + 32.0
117 } else {
118 (value - 32.0) * (5.0 / 9.0)
120 }
121}
122
123#[must_use]
125pub fn convert(value: f64) -> Convert {
126 Convert { value }
127}
128
129#[derive(Debug, Clone, Copy)]
131pub struct Convert {
132 value: f64,
133}
134
135impl Convert {
136 pub fn from(self, unit: &str) -> Result<Conversion, Error> {
141 let origin = find_unit(unit).ok_or_else(|| Error::UnsupportedUnit(unit.to_string()))?;
142 Ok(Conversion {
143 value: self.value,
144 origin,
145 })
146 }
147}
148
149#[derive(Debug, Clone, Copy)]
151pub struct Conversion {
152 value: f64,
153 origin: &'static Unit,
154}
155
156impl Conversion {
157 pub fn to(&self, unit: &str) -> Result<f64, Error> {
163 let dest = find_unit(unit).ok_or_else(|| Error::UnsupportedUnit(unit.to_string()))?;
164
165 if self.origin.abbr == dest.abbr {
166 return Ok(self.value);
167 }
168 if self.origin.measure != dest.measure {
169 return Err(Error::IncompatibleMeasures {
170 from: self.origin.measure,
171 to: dest.measure,
172 });
173 }
174
175 let mut result = self.value * self.origin.to_anchor;
177 result -= self.origin.anchor_shift;
178
179 if self.origin.system != dest.system {
181 if self.origin.measure == "temperature" {
182 result = temperature_transform(self.origin.system, result);
183 } else {
184 result *= anchor_ratio(self.origin.measure, self.origin.system);
185 }
186 }
187
188 result += dest.anchor_shift;
189 Ok(result / dest.to_anchor)
190 }
191
192 #[must_use]
194 pub fn to_best(&self) -> Option<Best> {
195 self.to_best_with(&[], 1.0)
196 }
197
198 #[must_use]
201 pub fn to_best_with(&self, exclude: &[&str], cut_off: f64) -> Option<Best> {
202 let mut best: Option<Best> = None;
203 for possibility in self.possibilities() {
204 let Some(unit) = find_unit(possibility) else {
205 continue;
206 };
207 let included = !exclude.contains(&possibility);
208 if included && unit.system == self.origin.system {
209 let Ok(result) = self.to(possibility) else {
210 continue;
211 };
212 let replace = match best {
213 None => true,
214 Some(b) => result >= cut_off && result < b.val,
215 };
216 if replace {
217 best = Some(Best {
218 val: result,
219 unit: possibility,
220 singular: unit.singular,
221 plural: unit.plural,
222 });
223 }
224 }
225 }
226 best
227 }
228
229 #[must_use]
231 pub fn possibilities(&self) -> Vec<&'static str> {
232 UNITS
233 .iter()
234 .filter(|u| u.measure == self.origin.measure)
235 .map(|u| u.abbr)
236 .collect()
237 }
238
239 #[must_use]
241 pub fn describe_origin(&self) -> UnitInfo {
242 unit_info(self.origin)
243 }
244}
245
246fn unit_info(u: &'static Unit) -> UnitInfo {
247 UnitInfo {
248 abbr: u.abbr,
249 measure: u.measure,
250 system: u.system,
251 singular: u.singular,
252 plural: u.plural,
253 }
254}
255
256#[must_use]
258pub fn measures() -> Vec<&'static str> {
259 let mut out: Vec<&'static str> = Vec::new();
260 for u in UNITS {
261 if !out.contains(&u.measure) {
262 out.push(u.measure);
263 }
264 }
265 out
266}
267
268#[must_use]
270pub fn describe(unit: &str) -> Option<UnitInfo> {
271 find_unit(unit).map(unit_info)
272}
273
274#[must_use]
276pub fn list(measure: Option<&str>) -> Vec<UnitInfo> {
277 UNITS
278 .iter()
279 .filter(|u| measure.map_or(true, |m| u.measure == m))
280 .map(unit_info)
281 .collect()
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287
288 fn c(v: f64, from: &str, to: &str) -> f64 {
289 convert(v).from(from).unwrap().to(to).unwrap()
290 }
291
292 #[test]
293 fn basic_conversions() {
294 assert_eq!(c(1.0, "kg", "lb"), 2.2046244201837775);
295 assert_eq!(c(100.0, "cm", "m"), 1.0);
296 assert_eq!(c(1.0, "l", "ml"), 1000.0);
297 assert_eq!(c(1.0, "mi", "km"), 1.6093439485009937);
298 assert_eq!(c(1.0, "h", "min"), 60.0);
299 assert_eq!(c(5.0, "m", "m"), 5.0); }
301
302 #[test]
303 fn temperature() {
304 assert_eq!(c(0.0, "C", "F"), 32.0);
305 assert_eq!(c(100.0, "C", "F"), 212.0);
306 assert_eq!(c(32.0, "F", "C"), 0.0);
307 assert_eq!(c(0.0, "C", "K"), 273.15);
308 assert_eq!(c(0.0, "K", "C"), -273.15);
309 }
310
311 #[test]
312 fn digital() {
313 assert_eq!(c(1.0, "B", "b"), 8.0);
314 assert_eq!(c(1.0, "KB", "B"), 1024.0);
315 }
316
317 #[test]
318 fn errors() {
319 assert!(matches!(
320 convert(1.0).from("nope"),
321 Err(Error::UnsupportedUnit(_))
322 ));
323 assert!(matches!(
324 convert(1.0).from("kg").unwrap().to("m"),
325 Err(Error::IncompatibleMeasures { .. })
326 ));
327 }
328
329 #[test]
330 fn best_and_meta() {
331 let best = convert(12000.0).from("mm").unwrap().to_best().unwrap();
332 assert_eq!(best.unit, "m");
333 assert_eq!(best.val, 12.0);
334
335 assert_eq!(convert(1.0).from("kg").unwrap().possibilities().len(), 8);
336 assert_eq!(measures().len(), 23);
337 assert_eq!(describe("kg").unwrap().singular, "Kilogram");
338 assert_eq!(list(Some("length")).len(), 9);
339 assert_eq!(list(None).len(), 185);
340 }
341}