1use 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#[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 pub fn builder() -> ConverterBuilder {
47 ConverterBuilder::new()
48 }
49
50 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 #[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 pub fn default_system(&self) -> System {
89 self.default_system
90 }
91
92 pub fn unit_count(&self) -> usize {
96 self.all_units.len()
97 }
98
99 pub fn all_units(&self) -> impl Iterator<Item = &Unit> {
101 self.all_units.iter().map(|u| u.as_ref())
102 }
103
104 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 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 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 #[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 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 }
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#[derive(Debug, Clone, Serialize)]
267pub struct Unit {
268 pub names: Vec<Arc<str>>,
270 pub symbols: Vec<Arc<str>>,
272 pub aliases: Vec<Arc<str>>,
274 pub ratio: f64,
276 pub difference: f64,
278 pub physical_quantity: PhysicalQuantity,
280 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 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 }
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 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 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 #[tracing::instrument(level = "trace", skip_all)]
505 pub fn fit(&mut self, converter: &Converter) -> Result<(), ConvertError> {
506 let Some(unit) = self.unit_info(converter) else {
508 return Ok(());
509 };
510
511 if converter.should_fit_fraction(&unit)
513 && self.fit_fraction(&unit, unit.system, converter)?
514 {
515 return Ok(());
516 }
517
518 self.convert(ConvertTo::SameSystem, converter)?;
520
521 Ok(())
522 }
523
524 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)); };
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 #[tracing::instrument(level = "trace", skip_all)]
607 pub fn try_fraction(&mut self, converter: &Converter) -> bool {
608 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 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#[derive(Debug, Error)]
729#[error("Unknown unit: '{0}'")]
730pub struct UnknownUnit(pub String);
731
732#[derive(PartialEq, Clone, Debug)]
734pub enum ConvertValue {
735 Number(f64),
736 Range(RangeInclusive<f64>),
739}
740
741#[derive(Debug, Clone, Copy)]
743pub enum ConvertUnit<'a> {
744 Unit(&'a Arc<Unit>),
750 Key(&'a str),
752}
753
754#[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#[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}