Skip to main content

cooklang/convert/
mod.rs

1//! Support for **configurable** unit conversion
2//!
3//! This includes:
4//! - A layered configuration system
5//! - Conversions between systems
6//! - Conversions to the best fit possible
7
8use std::{collections::HashMap, ops::RangeInclusive, sync::Arc};
9
10use enum_map::EnumMap;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14use crate::{
15    quantity::{Number, Quantity, Value},
16    Recipe,
17};
18
19pub use builder::{ConverterBuilder, ConverterBuilderError};
20pub use units_file::UnitsFile;
21
22mod builder;
23pub mod units_file;
24
25/// Main struct to perform conversions
26///
27/// This holds information about all the known units and how to convert them.
28///
29/// To create one use [`Converter::builder`].
30///
31/// [`Converter::default`] changes with the feature `bundled_units`:
32/// - When enabled, [`Converter::bundled`].
33/// - When disabled, [`Converter::empty`].
34#[derive(Debug, Clone)]
35pub struct Converter {
36    all_units: Vec<Arc<Unit>>,
37    unit_index: UnitIndex,
38    quantity_index: UnitQuantityIndex,
39    best: EnumMap<PhysicalQuantity, BestConversionsStore>,
40    fractions: Fractions,
41    default_system: System,
42}
43
44impl Converter {
45    /// Start to create a new [Converter]
46    pub fn builder() -> ConverterBuilder {
47        ConverterBuilder::new()
48    }
49
50    /// Empty converter
51    ///
52    /// This is the default when the `bundled_units` feature is disabled.
53    ///
54    /// The main use case for this is to ignore the units, because an empty
55    /// converter will fail to convert everything. Also, if the `ADVANCED_UNITS`
56    /// extension is enabled, every timer unit will throw an error, because they
57    /// have to be known time units.
58    pub fn empty() -> Self {
59        Self {
60            all_units: Default::default(),
61            unit_index: Default::default(),
62            quantity_index: Default::default(),
63            best: Default::default(),
64            default_system: Default::default(),
65            fractions: Default::default(),
66        }
67    }
68
69    /// Converter with the bundled units
70    ///
71    /// The converter will have the bundled units that doens't need any external
72    /// file. These are the basic unit for most of the recipes you will need
73    /// (in English).
74    ///
75    /// This is only available when the `bundled_units` feature is enabled.
76    ///
77    /// This is the default when the `bundled_units` feature is enabled.
78    #[cfg(feature = "bundled_units")]
79    pub fn bundled() -> Self {
80        ConverterBuilder::new()
81            .with_units_file(UnitsFile::bundled())
82            .unwrap()
83            .finish()
84            .unwrap()
85    }
86
87    /// Get the default unit [System]
88    pub fn default_system(&self) -> System {
89        self.default_system
90    }
91
92    /// Get the total number of known units.
93    ///
94    /// This is **not** all the known unit names, just **different units**.
95    pub fn unit_count(&self) -> usize {
96        self.all_units.len()
97    }
98
99    /// Get an iterator of all the known units.
100    pub fn all_units(&self) -> impl Iterator<Item = &Unit> {
101        self.all_units.iter().map(|u| u.as_ref())
102    }
103
104    /// Check if a unit is one of the possible conversions in it's units system.
105    ///
106    /// When a unit is a *best unit*, the converter can choose it when trying
107    /// to get the best match for a value.
108    ///
109    /// # Panics
110    /// If the unit is not known.
111    pub fn is_best_unit(&self, unit: &Unit) -> bool {
112        let unit_id = self
113            .unit_index
114            .get_unit_id(unit.symbol())
115            .expect("unit not found");
116        let Some(system) = unit.system else {
117            return false;
118        };
119        let conversions = self.best[unit.physical_quantity].conversions(system);
120        conversions.0.iter().any(|&(_, id)| id == unit_id)
121    }
122
123    /// Get the (marked) best units for a quantity and a system.
124    ///
125    /// If system is None, returns for all the systems.
126    pub fn best_units(&self, quantity: PhysicalQuantity, system: Option<System>) -> Vec<Arc<Unit>> {
127        match &self.best[quantity] {
128            BestConversionsStore::Unified(u) => u.all_units(self).cloned().collect(),
129            BestConversionsStore::BySystem { metric, imperial } => match system {
130                Some(System::Metric) => metric.all_units(self).cloned().collect(),
131                Some(System::Imperial) => imperial.all_units(self).cloned().collect(),
132                None => metric
133                    .all_units(self)
134                    .chain(imperial.all_units(self))
135                    .cloned()
136                    .collect(),
137            },
138        }
139    }
140
141    /// Find a unit by any of it's names, symbols or aliases
142    pub fn find_unit(&self, unit: &str) -> Option<Arc<Unit>> {
143        let uid = self.unit_index.get_unit_id(unit).ok()?;
144        Some(self.all_units[uid].clone())
145    }
146
147    /// Gets the fractions configuration for the given unit
148    ///
149    /// # Panics
150    /// If the unit is not known.
151    #[tracing::instrument(level = "trace", skip_all, fields(unit = %unit), ret)]
152    pub(crate) fn fractions_config(&self, unit: &Unit) -> FractionsConfig {
153        let unit_id = self
154            .unit_index
155            .get_unit_id(unit.symbol())
156            .expect("unit not found");
157        self.fractions
158            .config(unit.system, unit.physical_quantity, unit_id)
159    }
160
161    /// Determines if the unit should be tried to be converted into a fraction
162    ///
163    /// # Panics
164    /// If the unit is not known.
165    pub(crate) fn should_fit_fraction(&self, unit: &Unit) -> bool {
166        self.fractions_config(unit).enabled
167    }
168}
169
170#[cfg(not(feature = "bundled_units"))]
171impl Default for Converter {
172    fn default() -> Self {
173        Self::empty()
174    }
175}
176
177#[cfg(feature = "bundled_units")]
178impl Default for Converter {
179    fn default() -> Self {
180        Self::bundled()
181    }
182}
183
184impl PartialEq for Converter {
185    fn eq(&self, other: &Self) -> bool {
186        self.all_units == other.all_units
187            && self.unit_index == other.unit_index
188            && self.quantity_index == other.quantity_index
189            && self.best == other.best
190            && self.default_system == other.default_system
191        // temperature_regex ignored, it should be the same if the rest is the
192        // the same
193    }
194}
195
196#[derive(Debug, Clone, Default)]
197struct Fractions {
198    all: Option<FractionsConfig>,
199    metric: Option<FractionsConfig>,
200    imperial: Option<FractionsConfig>,
201    quantity: HashMap<PhysicalQuantity, FractionsConfig>,
202    unit: HashMap<usize, FractionsConfig>,
203}
204
205impl Fractions {
206    fn config(
207        &self,
208        system: Option<System>,
209        quantity: PhysicalQuantity,
210        unit_id: usize,
211    ) -> FractionsConfig {
212        self.unit
213            .get(&unit_id)
214            .or_else(|| self.quantity.get(&quantity))
215            .or_else(|| {
216                system.and_then(|s| match s {
217                    System::Metric => self.metric.as_ref(),
218                    System::Imperial => self.imperial.as_ref(),
219                })
220            })
221            .or(self.all.as_ref())
222            .copied()
223            .unwrap_or_default()
224    }
225}
226
227#[derive(Debug, Clone, Copy)]
228pub(crate) struct FractionsConfig {
229    pub enabled: bool,
230    pub accuracy: f32,
231    pub max_denominator: u8,
232    pub max_whole: u32,
233}
234
235impl Default for FractionsConfig {
236    fn default() -> Self {
237        Self {
238            enabled: false,
239            accuracy: 0.05,
240            max_denominator: 4,
241            max_whole: u32::MAX,
242        }
243    }
244}
245
246#[derive(Debug, Default, Clone, PartialEq)]
247pub(crate) struct UnitIndex(HashMap<Arc<str>, usize>);
248
249impl UnitIndex {
250    fn get_unit_id(&self, key: &str) -> Result<usize, UnknownUnit> {
251        self.0
252            .get(key)
253            .copied()
254            .ok_or_else(|| UnknownUnit(key.to_string()))
255    }
256}
257
258pub(crate) type UnitQuantityIndex = EnumMap<PhysicalQuantity, Vec<usize>>;
259
260/// A unit
261///
262/// Conversion will be `val * [Self::ratio] + [Self::difference]`
263///
264/// It implements [Display](std::fmt::Display). It will use [`Self::symbol`] or,
265/// if alternate (`#`) is given, it will try the first name.
266#[derive(Debug, Clone, Serialize)]
267pub struct Unit {
268    /// All the names that may be used to format the unit
269    pub names: Vec<Arc<str>>,
270    /// All the symbols (abbreviations), like `ml` for `millilitres`
271    pub symbols: Vec<Arc<str>>,
272    /// Custom aliases to parse the unit from a different string
273    pub aliases: Vec<Arc<str>>,
274    /// Conversion ratio
275    pub ratio: f64,
276    /// Difference offset to the conversion ratio
277    pub difference: f64,
278    /// The [`PhysicalQuantity`] this unit belongs to
279    pub physical_quantity: PhysicalQuantity,
280    /// The unit [System] this unit belongs to, if any
281    pub system: Option<System>,
282}
283
284impl Unit {
285    fn all_keys(&self) -> impl Iterator<Item = &Arc<str>> {
286        self.names.iter().chain(&self.symbols).chain(&self.aliases)
287    }
288
289    /// Get the symbol that represent this unit. The process is:
290    /// - First symbol (if any)
291    /// - Or first name (if any)
292    /// - Or first alias (if any)
293    /// - **panics**
294    pub fn symbol(&self) -> &str {
295        self.symbols
296            .first()
297            .or_else(|| self.names.first())
298            .or_else(|| self.aliases.first())
299            .expect("symbol, name or alias in unit")
300    }
301}
302
303impl PartialEq for Unit {
304    fn eq(&self, other: &Self) -> bool {
305        self.names == other.names
306            && self.symbols == other.symbols
307            && self.aliases == other.aliases
308            && self.ratio == other.ratio
309            && self.difference == other.difference
310            && self.physical_quantity == other.physical_quantity
311            && self.system == other.system
312        // expand_si and expanded_units ignored
313    }
314}
315
316impl std::fmt::Display for Unit {
317    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318        if f.alternate() && !self.names.is_empty() {
319            write!(f, "{}", self.names[0])
320        } else {
321            write!(f, "{}", self.symbol())
322        }
323    }
324}
325
326#[derive(Debug, Clone, PartialEq)]
327enum BestConversionsStore {
328    Unified(BestConversions),
329    BySystem {
330        metric: BestConversions,
331        imperial: BestConversions,
332    },
333}
334
335impl BestConversionsStore {
336    pub(crate) fn conversions(&self, system: System) -> &BestConversions {
337        match self {
338            BestConversionsStore::Unified(u) => u,
339            BestConversionsStore::BySystem { metric, imperial } => match system {
340                System::Metric => metric,
341                System::Imperial => imperial,
342            },
343        }
344    }
345}
346
347impl Default for BestConversionsStore {
348    fn default() -> Self {
349        Self::Unified(Default::default())
350    }
351}
352
353#[derive(Debug, Clone, Default, PartialEq)]
354struct BestConversions(Vec<(f64, usize)>);
355
356impl BestConversions {
357    fn base(&self) -> Option<usize> {
358        self.0.first().map(|c| c.1)
359    }
360
361    fn best_unit(
362        &self,
363        converter: &Converter,
364        value: &ConvertValue,
365        unit: &Unit,
366    ) -> Option<Arc<Unit>> {
367        let value = match value {
368            ConvertValue::Number(n) => n.abs(),
369            ConvertValue::Range(r) => r.start().abs(),
370        };
371        let base_unit_id = self.base()?;
372        let base_unit = &converter.all_units[base_unit_id];
373        let norm = converter.convert_f64(value, unit, base_unit);
374
375        let best_id = self
376            .0
377            .iter()
378            .rev()
379            .find(|(th, _)| norm >= (th - 0.001))
380            .or_else(|| self.0.first())
381            .map(|&(_, id)| id)?;
382        Some(Arc::clone(&converter.all_units[best_id]))
383    }
384
385    fn all_units<'c>(&'c self, converter: &'c Converter) -> impl Iterator<Item = &'c Arc<Unit>> {
386        self.0.iter().map(|(_, uid)| &converter.all_units[*uid])
387    }
388}
389
390#[derive(
391    Debug,
392    Clone,
393    Copy,
394    PartialEq,
395    Eq,
396    Deserialize,
397    Serialize,
398    PartialOrd,
399    Ord,
400    Hash,
401    strum::Display,
402    strum::EnumString,
403    enum_map::Enum,
404)]
405#[serde(rename_all = "camelCase")]
406#[strum(serialize_all = "camelCase")]
407pub enum PhysicalQuantity {
408    Volume,
409    Mass,
410    Length,
411    Temperature,
412    Time,
413}
414
415impl Recipe {
416    /// Convert a [`ScaledRecipe`] to another [`System`] in place.
417    ///
418    /// When an error occurs, it is stored and the quantity stays the same.
419    ///
420    /// Returns all the errors while converting. These usually are missing units,
421    /// unknown units or text values.
422    pub fn convert(&mut self, to: System, converter: &Converter) -> Vec<ConvertError> {
423        let mut errors = Vec::new();
424
425        let to = ConvertTo::from(to);
426
427        let mut conv = |q: &mut Quantity| {
428            if let Err(e) = q.convert(to, converter) {
429                errors.push(e)
430            }
431        };
432
433        for igr in &mut self.ingredients {
434            if let Some(q) = &mut igr.quantity {
435                conv(q);
436            }
437        }
438
439        // cookware can't have units
440
441        for timer in &mut self.timers {
442            if let Some(q) = &mut timer.quantity {
443                conv(q);
444            }
445        }
446
447        for q in &mut self.inline_quantities {
448            conv(q);
449        }
450
451        errors
452    }
453}
454
455impl Quantity {
456    pub fn convert<'a>(
457        &mut self,
458        to: impl Into<ConvertTo<'a>>,
459        converter: &Converter,
460    ) -> Result<(), ConvertError> {
461        self.convert_impl(to.into(), converter)
462    }
463
464    #[tracing::instrument(level = "trace", name = "convert", skip_all)]
465    fn convert_impl(&mut self, to: ConvertTo, converter: &Converter) -> Result<(), ConvertError> {
466        if self.unit().is_none() {
467            return Err(ConvertError::NoUnit(self.clone()));
468        }
469
470        let unit_info = self.unit_info(converter);
471        let original_system;
472        let unit = match unit_info {
473            Some(ref u) => {
474                original_system = u.system;
475                ConvertUnit::Unit(u)
476            }
477            None => {
478                return Err(ConvertError::UnknownUnit(UnknownUnit(
479                    self.unit().unwrap().to_string(),
480                )))
481            }
482        };
483        let value = ConvertValue::try_from(self.value())?;
484
485        let (new_value, new_unit) = converter.convert(value, unit, to)?;
486        *self = Quantity::new(new_value.into(), Some(new_unit.symbol().to_string()));
487        match to {
488            ConvertTo::Unit(_) => {
489                self.try_fraction(converter);
490            }
491            ConvertTo::Best(target_system) => {
492                self.fit_fraction(&new_unit, Some(target_system), converter)?;
493            }
494            ConvertTo::SameSystem => {
495                self.fit_fraction(&new_unit, original_system, converter)?;
496            }
497        }
498        Ok(())
499    }
500
501    /// Converts the unit to the best possible match in the same unit system.
502    ///
503    /// For example, `1000 ml` would be converted to `1 l`.
504    #[tracing::instrument(level = "trace", skip_all)]
505    pub fn fit(&mut self, converter: &Converter) -> Result<(), ConvertError> {
506        // only known units can be fitted
507        let Some(unit) = self.unit_info(converter) else {
508            return Ok(());
509        };
510
511        // If configured, try fitting as a fraction
512        if converter.should_fit_fraction(&unit)
513            && self.fit_fraction(&unit, unit.system, converter)?
514        {
515            return Ok(());
516        }
517
518        // convert to the best in the same system
519        self.convert(ConvertTo::SameSystem, converter)?;
520
521        Ok(())
522    }
523
524    /// Fits the quantity as an approximation.
525    ///
526    /// - Finds all the conversions where an approximation is possible
527    /// - Get's the best one
528    /// - Convert the value(s)
529    ///
530    /// Returns Ok(true) only if the value could be approximated.
531    fn fit_fraction(
532        &mut self,
533        unit: &Arc<Unit>,
534        target_system: Option<System>,
535        converter: &Converter,
536    ) -> Result<bool, ConvertError> {
537        let approx = |val: f64, cfg: FractionsConfig| {
538            Number::new_approx(val, cfg.accuracy, cfg.max_denominator, cfg.max_whole)
539        };
540
541        let Some(system) = target_system else {
542            return Ok(self.try_fraction(converter)); // no system, just keep the same unit
543        };
544
545        let value = match self.value() {
546            Value::Number(n) => n.value(),
547            Value::Range { start, .. } => start.value(),
548            Value::Text(ref t) => return Err(ConvertError::TextValue(t.clone())),
549        };
550
551        let possible_conversions = converter.best[unit.physical_quantity]
552            .conversions(system)
553            .0
554            .iter()
555            .filter_map(|&(_, new_unit_id)| {
556                let new_unit = &converter.all_units[new_unit_id];
557                let cfg = converter.fractions.config(
558                    new_unit.system,
559                    new_unit.physical_quantity,
560                    new_unit_id,
561                );
562                if !cfg.enabled {
563                    return None;
564                }
565                let new_value = converter.convert_f64(value, unit, new_unit);
566                let new_value = approx(new_value, cfg)?;
567                Some((new_value, new_unit))
568            });
569
570        let selected = possible_conversions.min_by(|(a, _), (b, _)| {
571            let key = |v| match v {
572                Number::Fraction {
573                    den, err, whole, ..
574                } => (den, whole as f64, err.abs()),
575                Number::Regular(whole) => (1, whole, 0.0),
576            };
577            let a = key(*a);
578            let b = key(*b);
579            a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Less)
580        });
581
582        let Some((new_value, new_unit)) = selected else {
583            return Ok(false);
584        };
585
586        let new_value = match self.value() {
587            Value::Number(_) => Value::Number(new_value),
588            Value::Range { end, .. } => {
589                let end = converter.convert_f64(end.value(), unit, new_unit);
590                let end_frac = approx(end, converter.fractions_config(new_unit))
591                    .unwrap_or(Number::Regular(end));
592                Value::Range {
593                    start: new_value,
594                    end: end_frac,
595                }
596            }
597            Value::Text(_) => unreachable!(),
598        };
599        *self = Quantity::new(new_value, Some(new_unit.symbol().to_string()));
600        Ok(true)
601    }
602
603    /// Tries to convert the value to a fraction, keeping the same unit
604    ///
605    /// It respects the converter configuration for the unit.
606    #[tracing::instrument(level = "trace", skip_all)]
607    pub fn try_fraction(&mut self, converter: &Converter) -> bool {
608        // only known units can be fitted
609        let Some(unit) = self.unit_info(converter) else {
610            return false;
611        };
612
613        let cfg = converter.fractions_config(&unit);
614        if !cfg.enabled {
615            return false;
616        }
617
618        match self.value_mut() {
619            Value::Number(n) => n.try_approx(cfg.accuracy, cfg.max_denominator, cfg.max_whole),
620            Value::Range { start, end } => {
621                start.try_approx(cfg.accuracy, cfg.max_denominator, cfg.max_whole)
622                    || end.try_approx(cfg.accuracy, cfg.max_denominator, cfg.max_whole)
623            }
624            Value::Text(_) => false,
625        }
626    }
627}
628
629impl Converter {
630    /// Perform a conversion
631    pub fn convert(
632        &self,
633        value: ConvertValue,
634        unit: ConvertUnit,
635        to: ConvertTo,
636    ) -> Result<(ConvertValue, Arc<Unit>), ConvertError> {
637        let unit = self.get_unit(&unit)?;
638
639        let (value, unit) = match to {
640            ConvertTo::Unit(target_unit) => {
641                let to = self.get_unit(&target_unit)?;
642                let val = self.convert_to_unit(value, unit, to.as_ref())?;
643                (val, Arc::clone(to))
644            }
645            ConvertTo::Best(system) => self.convert_to_best(value, unit, system)?,
646            ConvertTo::SameSystem => {
647                self.convert_to_best(value, unit, unit.system.unwrap_or(self.default_system))?
648            }
649        };
650        Ok((value, unit))
651    }
652
653    fn convert_to_unit(
654        &self,
655        value: ConvertValue,
656        unit: &Unit,
657        target_unit: &Unit,
658    ) -> Result<ConvertValue, ConvertError> {
659        if unit.physical_quantity != target_unit.physical_quantity {
660            return Err(ConvertError::MixedQuantities {
661                from: unit.physical_quantity,
662                to: target_unit.physical_quantity,
663            });
664        }
665        Ok(self.convert_value(value, unit, target_unit))
666    }
667
668    fn convert_to_best(
669        &self,
670        value: ConvertValue,
671        unit: &Unit,
672        system: System,
673    ) -> Result<(ConvertValue, Arc<Unit>), ConvertError> {
674        let conversions = self.best[unit.physical_quantity].conversions(system);
675
676        let best_unit = conversions.best_unit(self, &value, unit).ok_or({
677            ConvertError::BestUnitNotFound {
678                physical_quantity: unit.physical_quantity,
679                system: unit.system,
680            }
681        })?;
682        let converted = self.convert_value(value, unit, best_unit.as_ref());
683
684        Ok((converted, best_unit))
685    }
686
687    fn convert_value(&self, value: ConvertValue, from: &Unit, to: &Unit) -> ConvertValue {
688        match value {
689            ConvertValue::Number(n) => ConvertValue::Number(self.convert_f64(n, from, to)),
690            ConvertValue::Range(r) => {
691                let s = self.convert_f64(*r.start(), from, to);
692                let e = self.convert_f64(*r.end(), from, to);
693                ConvertValue::Range(s..=e)
694            }
695        }
696    }
697
698    fn convert_f64(&self, value: f64, from: &Unit, to: &Unit) -> f64 {
699        if std::ptr::eq(from, to) {
700            return value;
701        }
702        convert_f64(value, from, to)
703    }
704
705    pub(crate) fn get_unit<'a>(
706        &'a self,
707        unit: &'a ConvertUnit,
708    ) -> Result<&'a Arc<Unit>, UnknownUnit> {
709        let unit = match unit {
710            ConvertUnit::Unit(u) => u,
711            ConvertUnit::Key(key) => {
712                let id = self.unit_index.get_unit_id(key)?;
713                &self.all_units[id]
714            }
715        };
716        Ok(unit)
717    }
718}
719
720pub(crate) fn convert_f64(value: f64, from: &Unit, to: &Unit) -> f64 {
721    assert_eq!(from.physical_quantity, to.physical_quantity);
722
723    let norm = (value + from.difference) * from.ratio;
724    (norm / to.ratio) - to.difference
725}
726
727/// Error when try to convert an unknown unit
728#[derive(Debug, Error)]
729#[error("Unknown unit: '{0}'")]
730pub struct UnknownUnit(pub String);
731
732/// Input value for [`Converter::convert`]
733#[derive(PartialEq, Clone, Debug)]
734pub enum ConvertValue {
735    Number(f64),
736    /// It will convert the range as if start and end were 2 calls to convert as
737    /// a number
738    Range(RangeInclusive<f64>),
739}
740
741/// Input unit for [`Converter::convert`]
742#[derive(Debug, Clone, Copy)]
743pub enum ConvertUnit<'a> {
744    /// A unit directly
745    ///
746    /// This is a small optimization when you already know the unit instance,
747    /// but [`ConvertUnit::Key`] will produce the same result with a fast
748    /// lookup.
749    Unit(&'a Arc<Unit>),
750    /// Any name, symbol or alias to a unit
751    Key(&'a str),
752}
753
754/// Input target for [`Converter::convert`]
755#[derive(Debug, Clone, Copy)]
756pub enum ConvertTo<'a> {
757    SameSystem,
758    Best(System),
759    Unit(ConvertUnit<'a>),
760}
761
762#[derive(
763    Debug,
764    Clone,
765    Copy,
766    PartialEq,
767    Eq,
768    Deserialize,
769    Serialize,
770    Default,
771    PartialOrd,
772    Ord,
773    strum::Display,
774    strum::EnumString,
775    enum_map::Enum,
776)]
777#[serde(rename_all = "camelCase")]
778#[strum(serialize_all = "camelCase")]
779pub enum System {
780    #[default]
781    Metric,
782    Imperial,
783}
784
785impl<'a> From<&'a str> for ConvertUnit<'a> {
786    fn from(value: &'a str) -> Self {
787        Self::Key(value)
788    }
789}
790
791impl<'a> From<&'a Arc<Unit>> for ConvertUnit<'a> {
792    fn from(value: &'a Arc<Unit>) -> Self {
793        Self::Unit(value)
794    }
795}
796
797impl<'a> From<&'a str> for ConvertTo<'a> {
798    fn from(value: &'a str) -> Self {
799        Self::Unit(ConvertUnit::Key(value))
800    }
801}
802
803impl From<System> for ConvertTo<'_> {
804    fn from(value: System) -> Self {
805        Self::Best(value)
806    }
807}
808
809impl<'a> From<&'a Arc<Unit>> for ConvertTo<'a> {
810    fn from(value: &'a Arc<Unit>) -> Self {
811        Self::Unit(value.into())
812    }
813}
814
815impl From<ConvertValue> for Value {
816    fn from(value: ConvertValue) -> Self {
817        match value {
818            ConvertValue::Number(n) => Self::Number(n.into()),
819            ConvertValue::Range(r) => Self::Range {
820                start: (*r.start()).into(),
821                end: (*r.end()).into(),
822            },
823        }
824    }
825}
826
827impl TryFrom<&Value> for ConvertValue {
828    type Error = ConvertError;
829    fn try_from(value: &Value) -> Result<Self, Self::Error> {
830        let value = match value {
831            Value::Number(n) => ConvertValue::Number(n.value()),
832            Value::Range { start, end } => ConvertValue::Range(start.value()..=end.value()),
833            Value::Text(t) => return Err(ConvertError::TextValue(t.clone())),
834        };
835        Ok(value)
836    }
837}
838
839impl From<f64> for ConvertValue {
840    fn from(value: f64) -> Self {
841        Self::Number(value)
842    }
843}
844
845impl From<RangeInclusive<f64>> for ConvertValue {
846    fn from(value: RangeInclusive<f64>) -> Self {
847        Self::Range(value)
848    }
849}
850
851impl PartialOrd<Self> for ConvertValue {
852    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
853        fn extract(v: &ConvertValue) -> f64 {
854            match v {
855                ConvertValue::Number(n) => *n,
856                ConvertValue::Range(r) => *r.start(),
857            }
858        }
859        let this = extract(self);
860        let other = extract(other);
861        this.partial_cmp(&other)
862    }
863}
864
865/// Errors from converting
866#[derive(Debug, Error)]
867pub enum ConvertError {
868    #[error("Tried to convert a value with no unit")]
869    NoUnit(Quantity),
870
871    #[error("Tried to convert a text value: {0}")]
872    TextValue(String),
873
874    #[error("Mixed physical quantities: {from} {to}")]
875    MixedQuantities {
876        from: PhysicalQuantity,
877        to: PhysicalQuantity,
878    },
879
880    #[error("Could not find best unit for a {physical_quantity} unit. System: {system:?}")]
881    BestUnitNotFound {
882        physical_quantity: PhysicalQuantity,
883        system: Option<System>,
884    },
885
886    #[error(transparent)]
887    UnknownUnit(#[from] UnknownUnit),
888}