fend_core/num/unit/
named_unit.rs

1use std::cmp::Ordering;
2use std::{borrow::Cow, collections::HashMap, fmt, io};
3
4use super::base_unit::BaseUnit;
5use crate::Interrupt;
6use crate::num::complex::Complex;
7use crate::result::FResult;
8use crate::serialize::{Deserialize, Serialize};
9
10/// A named unit, like kilogram, megabyte or percent.
11#[derive(Clone)]
12pub(crate) struct NamedUnit {
13	prefix: Cow<'static, str>,
14	pub(super) singular_name: Cow<'static, str>,
15	plural_name: Cow<'static, str>,
16	alias: bool,
17	pub(super) base_units: HashMap<BaseUnit, Complex>,
18	pub(super) scale: Complex,
19}
20
21pub(crate) fn compare_hashmaps<I: Interrupt>(
22	a: &HashMap<BaseUnit, Complex>,
23	b: &HashMap<BaseUnit, Complex>,
24	int: &I,
25) -> FResult<bool> {
26	if a.len() != b.len() {
27		return Ok(false);
28	}
29	for (k, v) in a {
30		match b.get(k) {
31			None => return Ok(false),
32			Some(o) => {
33				if v.compare(o, int)? != Some(Ordering::Equal) {
34					return Ok(false);
35				}
36			}
37		}
38	}
39	Ok(true)
40}
41
42impl NamedUnit {
43	pub(crate) fn new(
44		prefix: Cow<'static, str>,
45		singular_name: Cow<'static, str>,
46		plural_name: Cow<'static, str>,
47		alias: bool,
48		base_units: HashMap<BaseUnit, Complex>,
49		scale: impl Into<Complex>,
50	) -> Self {
51		Self {
52			prefix,
53			singular_name,
54			plural_name,
55			alias,
56			base_units,
57			scale: scale.into(),
58		}
59	}
60
61	pub(crate) fn serialize(&self, write: &mut impl io::Write) -> FResult<()> {
62		self.prefix.as_ref().serialize(write)?;
63		self.singular_name.as_ref().serialize(write)?;
64		self.plural_name.as_ref().serialize(write)?;
65		self.alias.serialize(write)?;
66
67		self.base_units.len().serialize(write)?;
68		for (a, b) in &self.base_units {
69			a.serialize(write)?;
70			b.serialize(write)?;
71		}
72
73		self.scale.serialize(write)?;
74		Ok(())
75	}
76
77	pub(crate) fn deserialize(read: &mut impl io::Read) -> FResult<Self> {
78		let prefix = String::deserialize(read)?;
79		let singular_name = String::deserialize(read)?;
80		let plural_name = String::deserialize(read)?;
81		let alias = bool::deserialize(read)?;
82
83		let len = usize::deserialize(read)?;
84		let mut hashmap = HashMap::with_capacity(len);
85		for _ in 0..len {
86			let k = BaseUnit::deserialize(read)?;
87			let v = Complex::deserialize(read)?;
88			hashmap.insert(k, v);
89		}
90		Ok(Self {
91			prefix: Cow::Owned(prefix),
92			singular_name: Cow::Owned(singular_name),
93			plural_name: Cow::Owned(plural_name),
94			alias,
95			base_units: hashmap,
96			scale: Complex::deserialize(read)?,
97		})
98	}
99
100	pub(crate) fn compare<I: Interrupt>(&self, other: &Self, int: &I) -> FResult<bool> {
101		Ok(self.prefix == other.prefix
102			&& self.singular_name == other.singular_name
103			&& self.plural_name == other.plural_name
104			&& self.alias == other.alias
105			&& self.scale.compare(&other.scale, int)? == Some(Ordering::Equal)
106			&& compare_hashmaps(&self.base_units, &other.base_units, int)?)
107	}
108
109	pub(crate) fn new_from_base(base_unit: BaseUnit) -> Self {
110		Self {
111			prefix: "".into(),
112			singular_name: base_unit.name().to_string().into(),
113			plural_name: base_unit.name().to_string().into(),
114			alias: false,
115			base_units: {
116				let mut base_units = HashMap::new();
117				base_units.insert(base_unit, 1.into());
118				base_units
119			},
120			scale: 1.into(),
121		}
122	}
123
124	pub(crate) fn prefix_and_name(&self, plural: bool) -> (&str, &str) {
125		(
126			self.prefix.as_ref(),
127			if plural {
128				self.plural_name.as_ref()
129			} else {
130				self.singular_name.as_ref()
131			},
132		)
133	}
134
135	pub(crate) fn has_no_base_units(&self) -> bool {
136		self.base_units.is_empty()
137	}
138
139	pub(crate) fn is_alias(&self) -> bool {
140		self.alias && self.has_no_base_units()
141	}
142
143	/// Returns whether or not this unit should be printed with a
144	/// space (between the number and the unit). This should be true for most
145	/// units like kg or m, but not for % or °
146	pub(crate) fn print_with_space(&self) -> bool {
147		// Alphabetic names like kg or m should have a space,
148		// while non-alphabetic names like % or ' shouldn't.
149		// Empty names shouldn't really exist, but they might as well have a space.
150
151		// degree symbol
152		if self.singular_name == "\u{b0}" {
153			return false;
154		}
155
156		// if it starts with a quote and is more than one character long, print it with a space
157		if (self.singular_name.starts_with('\'') || self.singular_name.starts_with('\"'))
158			&& self.singular_name.len() > 1
159		{
160			return true;
161		}
162
163		self.singular_name
164			.chars()
165			.next()
166			.is_none_or(|first_char| char::is_alphabetic(first_char) || first_char == '\u{b0}')
167	}
168}
169
170impl fmt::Debug for NamedUnit {
171	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172		if self.prefix.is_empty() {
173			write!(f, "{}", self.singular_name)?;
174		} else {
175			write!(f, "{}-{}", self.prefix, self.singular_name)?;
176		}
177		write!(f, " (")?;
178		if self.plural_name != self.singular_name {
179			if self.prefix.is_empty() {
180				write!(f, "{}, ", self.plural_name)?;
181			} else {
182				write!(f, "{}-{}, ", self.prefix, self.plural_name)?;
183			}
184		}
185		write!(f, "= {:?}", self.scale)?;
186		let mut it = self.base_units.iter().collect::<Vec<_>>();
187		it.sort_by_key(|(k, _v)| k.name());
188		for (base_unit, exponent) in &it {
189			write!(f, " {base_unit:?}")?;
190			if !exponent.is_definitely_one() {
191				write!(f, "^{exponent:?}")?;
192			}
193		}
194		write!(f, ")")?;
195		Ok(())
196	}
197}