1use core::fmt;
5
6use crate::dim::Dim;
7use crate::error::{Error, Result};
8use crate::expr::{Expr, is_valid_symbol};
9use crate::kind::{Exactness, Kind, Shape};
10use crate::prefix::PrefixMode;
11use crate::ratio::Ratio;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct UnitDef {
19 pub symbol: &'static str,
21 pub aliases: &'static [&'static str],
23 pub dim: [i8; 7],
25 pub kind: Kind,
27 pub shape: Shape,
29 pub factor: (i128, i128),
31 pub offset: (i128, i128),
33 pub exactness: Exactness,
35 pub prefixes: PrefixMode,
37 pub namespace: &'static str,
39 pub source: &'static str,
41 pub note: &'static str,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub struct AmbiguousDef {
48 pub symbol: &'static str,
50 pub candidates: &'static [&'static str],
52 pub planned: &'static [&'static str],
54 pub note: &'static str,
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct Unit {
63 symbol: String,
64 aliases: Vec<String>,
65 dim: Dim,
66 kind: Kind,
67 factor: Ratio,
68 offset: Ratio,
69 exactness: Exactness,
70 prefixes: PrefixMode,
71 namespace: String,
72 source: String,
73 note: String,
74}
75
76impl Unit {
77 pub fn linear(symbol: &str, dim: Dim, factor: Ratio) -> Result<Self> {
92 Self::new(symbol, dim, Kind::Plain, factor, Ratio::ZERO)
93 }
94
95 pub fn new(symbol: &str, dim: Dim, kind: Kind, factor: Ratio, offset: Ratio) -> Result<Self> {
102 let bad = |message: &str| Error::InvalidDefinition {
103 symbol: symbol.to_string(),
104 message: message.to_string(),
105 };
106 if !is_valid_symbol(symbol) {
107 return Err(bad(
108 "symbol must be non-empty and contain only letters, '_' or the degree sign",
109 ));
110 }
111 if factor.signum() <= 0 {
112 return Err(bad("factor must be positive"));
113 }
114 if !offset.is_zero() && kind != Kind::AbsoluteTemperature {
115 return Err(bad("only absolute temperature units may have an offset"));
116 }
117 Ok(Self {
118 symbol: symbol.to_string(),
119 aliases: Vec::new(),
120 dim,
121 kind,
122 factor,
123 offset,
124 exactness: Exactness::Defined,
125 prefixes: PrefixMode::None,
126 namespace: "std".to_string(),
127 source: String::new(),
128 note: String::new(),
129 })
130 }
131
132 pub fn from_def(def: &UnitDef) -> Result<Self> {
139 let factor = Ratio::new(def.factor.0, def.factor.1)?;
140 let offset = Ratio::new(def.offset.0, def.offset.1)?;
141 let mut unit = Self::new(def.symbol, Dim::new(def.dim), def.kind, factor, offset)?;
142 let bad = |message: &str| Error::InvalidDefinition {
143 symbol: def.symbol.to_string(),
144 message: message.to_string(),
145 };
146 match (def.shape, offset.is_zero()) {
147 (Shape::Linear, false) => return Err(bad("linear shape requires a zero offset")),
148 (Shape::Affine, true) => return Err(bad("affine shape requires a non-zero offset")),
149 _ => {}
150 }
151 if !offset.is_zero() && def.prefixes != PrefixMode::None {
152 return Err(bad("affine units cannot take prefixes"));
153 }
154 for alias in def.aliases {
155 if !is_valid_symbol(alias) {
156 return Err(bad(
157 "alias contains characters that cannot appear in a symbol",
158 ));
159 }
160 unit.aliases.push((*alias).to_string());
161 }
162 unit.exactness = def.exactness;
163 unit.prefixes = def.prefixes;
164 unit.namespace = def.namespace.to_string();
165 unit.source = def.source.to_string();
166 unit.note = def.note.to_string();
167 Ok(unit)
168 }
169
170 pub fn with_aliases(mut self, aliases: &[&str]) -> Result<Self> {
176 for a in aliases {
177 if !is_valid_symbol(a) {
178 return Err(Error::InvalidDefinition {
179 symbol: self.symbol,
180 message: format!("alias '{a}' is not a well-formed symbol"),
181 });
182 }
183 self.aliases.push((*a).to_string());
184 }
185 Ok(self)
186 }
187
188 #[must_use]
190 pub const fn with_prefixes(mut self, prefixes: PrefixMode) -> Self {
191 if self.offset.is_zero() {
192 self.prefixes = prefixes;
193 }
194 self
195 }
196
197 #[must_use]
199 pub fn with_provenance(mut self, exactness: Exactness, source: &str, note: &str) -> Self {
200 self.exactness = exactness;
201 self.source = source.to_string();
202 self.note = note.to_string();
203 self
204 }
205
206 #[must_use]
208 pub fn with_namespace(mut self, namespace: &str) -> Self {
209 self.namespace = namespace.to_string();
210 self
211 }
212
213 #[must_use]
215 pub fn symbol(&self) -> &str {
216 &self.symbol
217 }
218
219 #[must_use]
221 pub fn aliases(&self) -> &[String] {
222 &self.aliases
223 }
224
225 #[must_use]
227 pub const fn dim(&self) -> Dim {
228 self.dim
229 }
230
231 #[must_use]
233 pub const fn kind(&self) -> Kind {
234 self.kind
235 }
236
237 #[must_use]
239 pub const fn shape(&self) -> Shape {
240 if self.offset.is_zero() {
241 Shape::Linear
242 } else {
243 Shape::Affine
244 }
245 }
246
247 #[must_use]
249 pub const fn factor(&self) -> Ratio {
250 self.factor
251 }
252
253 #[must_use]
255 pub const fn offset(&self) -> Ratio {
256 self.offset
257 }
258
259 #[must_use]
261 pub const fn exactness(&self) -> Exactness {
262 self.exactness
263 }
264
265 #[must_use]
267 pub const fn prefixes(&self) -> PrefixMode {
268 self.prefixes
269 }
270
271 #[must_use]
273 pub fn namespace(&self) -> &str {
274 &self.namespace
275 }
276
277 #[must_use]
279 pub fn source(&self) -> &str {
280 &self.source
281 }
282
283 #[must_use]
285 pub fn note(&self) -> &str {
286 &self.note
287 }
288}
289
290#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct UnitExpr {
297 expr: Expr,
298 dim: Dim,
299 kind: Kind,
300 factor: Ratio,
301 offset: Ratio,
302 exactness: Exactness,
303 sources: Vec<String>,
304 difference: Option<String>,
307}
308
309impl UnitExpr {
310 #[must_use]
312 pub const fn unity() -> Self {
313 Self {
314 expr: Expr::One,
315 dim: Dim::NONE,
316 kind: Kind::Plain,
317 factor: Ratio::ONE,
318 offset: Ratio::ZERO,
319 exactness: Exactness::Defined,
320 sources: Vec::new(),
321 difference: None,
322 }
323 }
324
325 pub(crate) const fn from_parts(
326 expr: Expr,
327 dim: Dim,
328 kind: Kind,
329 factor: Ratio,
330 offset: Ratio,
331 exactness: Exactness,
332 sources: Vec<String>,
333 ) -> Self {
334 Self {
335 expr,
336 dim,
337 kind,
338 factor,
339 offset,
340 exactness,
341 sources,
342 difference: None,
343 }
344 }
345
346 pub(crate) fn with_difference(mut self, difference: Option<String>) -> Self {
347 self.difference = difference;
348 self
349 }
350
351 #[must_use]
355 pub fn difference_symbol(&self) -> Option<&str> {
356 self.difference.as_deref()
357 }
358
359 #[must_use]
361 pub fn is_unity(&self) -> bool {
362 self.dim.is_dimensionless()
363 && self.kind == Kind::Plain
364 && self.factor == Ratio::ONE
365 && self.offset.is_zero()
366 }
367
368 #[must_use]
370 pub const fn expr(&self) -> &Expr {
371 &self.expr
372 }
373
374 #[must_use]
376 pub const fn dim(&self) -> Dim {
377 self.dim
378 }
379
380 #[must_use]
382 pub const fn kind(&self) -> Kind {
383 self.kind
384 }
385
386 #[must_use]
388 pub const fn factor(&self) -> Ratio {
389 self.factor
390 }
391
392 #[must_use]
394 pub const fn offset(&self) -> Ratio {
395 self.offset
396 }
397
398 #[must_use]
400 pub const fn shape(&self) -> Shape {
401 if self.offset.is_zero() {
402 Shape::Linear
403 } else {
404 Shape::Affine
405 }
406 }
407
408 #[must_use]
410 pub const fn exactness(&self) -> Exactness {
411 self.exactness
412 }
413
414 #[must_use]
416 pub fn sources(&self) -> &[String] {
417 &self.sources
418 }
419
420 fn merged_sources(&self, other: &Self) -> Vec<String> {
421 let mut out = self.sources.clone();
422 for s in &other.sources {
423 if !out.contains(s) {
424 out.push(s.clone());
425 }
426 }
427 out
428 }
429
430 fn refuse_affine(&self, other: &Self) -> Result<()> {
431 for u in [self, other] {
432 if !u.offset.is_zero() {
433 let suggestions: Vec<String> = u.difference.iter().cloned().collect();
434 let advice = suggestions.first().map_or_else(
435 || {
436 String::from(
437 "use a difference unit (delta_...) for a temperature difference",
438 )
439 },
440 |d| format!("use the difference unit {d} for a temperature difference"),
441 );
442 return Err(Error::Refused {
443 reason: format!(
444 "'{}' has an offset (temperature scale) and cannot be part of a compound unit; {advice}",
445 u.expr
446 ),
447 suggestions,
448 });
449 }
450 }
451 Ok(())
452 }
453
454 pub fn mul(&self, rhs: &Self) -> Result<Self> {
461 if self.is_unity() {
462 return Ok(rhs.clone());
463 }
464 if rhs.is_unity() {
465 return Ok(self.clone());
466 }
467 self.refuse_affine(rhs)?;
468 Ok(Self {
469 expr: Expr::Mul(Box::new(self.expr.clone()), Box::new(rhs.expr.clone())),
470 dim: self.dim.checked_add(rhs.dim)?,
471 kind: Kind::Plain,
472 factor: self.factor.checked_mul(rhs.factor)?,
473 offset: Ratio::ZERO,
474 exactness: self.exactness.combine(rhs.exactness),
475 sources: self.merged_sources(rhs),
476 difference: None,
477 })
478 }
479
480 pub fn div(&self, rhs: &Self) -> Result<Self> {
486 if rhs.is_unity() {
487 return Ok(self.clone());
488 }
489 self.refuse_affine(rhs)?;
490 Ok(Self {
491 expr: Expr::Div(Box::new(self.expr.clone()), Box::new(rhs.expr.clone())),
492 dim: self.dim.checked_sub(rhs.dim)?,
493 kind: Kind::Plain,
494 factor: self.factor.checked_div(rhs.factor)?,
495 offset: Ratio::ZERO,
496 exactness: self.exactness.combine(rhs.exactness),
497 sources: self.merged_sources(rhs),
498 difference: None,
499 })
500 }
501
502 pub fn pow(&self, exp: i32) -> Result<Self> {
510 if exp == 1 {
511 return Ok(self.clone());
512 }
513 self.refuse_affine(&Self::unity())?;
514 Ok(Self {
515 expr: Expr::Pow(Box::new(self.expr.clone()), exp),
516 dim: self.dim.checked_scale(exp)?,
517 kind: Kind::Plain,
518 factor: self.factor.checked_pow(exp)?,
519 offset: Ratio::ZERO,
520 exactness: self.exactness,
521 sources: self.sources.clone(),
522 difference: None,
523 })
524 }
525}
526
527impl fmt::Display for UnitExpr {
528 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
529 self.expr.fmt(f)
530 }
531}
532
533#[cfg(test)]
534mod tests {
535 use super::*;
536
537 fn m() -> UnitExpr {
538 UnitExpr::from_parts(
539 Expr::Unit("m".into()),
540 Dim::M,
541 Kind::Plain,
542 Ratio::ONE,
543 Ratio::ZERO,
544 Exactness::Defined,
545 vec!["SI".into()],
546 )
547 }
548
549 #[test]
550 fn unity_is_identity_for_mul_and_div() {
551 let u = UnitExpr::unity();
552 assert_eq!(u.mul(&m()).unwrap(), m());
553 assert_eq!(m().mul(&u).unwrap(), m());
554 assert_eq!(m().div(&u).unwrap(), m());
555 assert!(m().pow(0).unwrap().is_unity() || m().pow(0).unwrap().dim().is_dimensionless());
556 }
557
558 #[test]
559 fn products_compose_exactly() {
560 let sq = m().mul(&m()).unwrap();
561 assert_eq!(sq.dim(), Dim::new([0, 2, 0, 0, 0, 0, 0]));
562 assert_eq!(sq.to_string(), "m.m");
563 let inv = UnitExpr::unity().div(&m()).unwrap();
564 assert_eq!(inv.dim(), Dim::new([0, -1, 0, 0, 0, 0, 0]));
565 assert_eq!(inv.sources(), ["SI"]);
566 }
567
568 #[test]
569 fn rejects_bad_definitions() {
570 assert!(Unit::linear("", Dim::M, Ratio::ONE).is_err());
571 assert!(Unit::linear("m2", Dim::M, Ratio::ONE).is_err());
572 assert!(Unit::linear("m", Dim::M, Ratio::ZERO).is_err());
573 assert!(Unit::linear("m", Dim::M, Ratio::from_int(-1)).is_err());
574 assert!(Unit::new("x", Dim::M, Kind::Plain, Ratio::ONE, Ratio::ONE).is_err());
575 assert!(
576 Unit::linear("m", Dim::M, Ratio::ONE)
577 .unwrap()
578 .with_aliases(&["a b"])
579 .is_err()
580 );
581 }
582
583 #[test]
584 fn from_def_checks_shape() {
585 let mut d = UnitDef {
586 symbol: "x",
587 aliases: &[],
588 dim: [0, 1, 0, 0, 0, 0, 0],
589 kind: Kind::Plain,
590 shape: Shape::Linear,
591 factor: (2, 4),
592 offset: (0, 1),
593 exactness: Exactness::Defined,
594 prefixes: PrefixMode::Si,
595 namespace: "std",
596 source: "test",
597 note: "",
598 };
599 assert_eq!(
600 Unit::from_def(&d).unwrap().factor(),
601 Ratio::new(1, 2).unwrap()
602 );
603 d.shape = Shape::Affine;
604 assert!(Unit::from_def(&d).is_err());
605 d.shape = Shape::Linear;
606 d.offset = (1, 1);
607 assert!(Unit::from_def(&d).is_err());
608 d.factor = (1, 0);
609 assert!(Unit::from_def(&d).is_err());
610 }
611}