1use core::fmt;
5
6use crate::dim::Dim;
7use crate::error::{Error, Result};
8use crate::expr::{Expr, is_valid_symbol, is_valid_symbol_for};
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 pub conditions: &'static str,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct AmbiguousDef {
51 pub symbol: &'static str,
53 pub candidates: &'static [&'static str],
55 pub planned: &'static [&'static str],
57 pub note: &'static str,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct Unit {
66 symbol: String,
67 aliases: Vec<String>,
68 dim: Dim,
69 kind: Kind,
70 factor: Ratio,
71 offset: Ratio,
72 exactness: Exactness,
73 prefixes: PrefixMode,
74 namespace: String,
75 source: String,
76 note: String,
77 conditions: String,
78}
79
80impl Unit {
81 pub fn linear(symbol: &str, dim: Dim, factor: Ratio) -> Result<Self> {
96 Self::new(symbol, dim, Kind::Plain, factor, Ratio::ZERO)
97 }
98
99 pub fn new(symbol: &str, dim: Dim, kind: Kind, factor: Ratio, offset: Ratio) -> Result<Self> {
106 let bad = |message: &str| Error::InvalidDefinition {
107 symbol: symbol.to_string(),
108 message: message.to_string(),
109 };
110 if !is_valid_symbol_for(symbol, kind) {
111 return Err(bad(
112 "symbol must be non-empty and contain only letters, '_' or the degree sign",
113 ));
114 }
115 if factor.signum() <= 0 {
116 return Err(bad("factor must be positive"));
117 }
118 if !offset.is_zero() && kind != Kind::AbsoluteTemperature {
119 return Err(bad("only absolute temperature units may have an offset"));
120 }
121 Ok(Self {
122 symbol: symbol.to_string(),
123 aliases: Vec::new(),
124 dim,
125 kind,
126 factor,
127 offset,
128 exactness: Exactness::Defined,
129 prefixes: PrefixMode::None,
130 namespace: "std".to_string(),
131 source: String::new(),
132 note: String::new(),
133 conditions: String::new(),
134 })
135 }
136
137 pub fn from_def(def: &UnitDef) -> Result<Self> {
144 let factor = Ratio::new(def.factor.0, def.factor.1)?;
145 let offset = Ratio::new(def.offset.0, def.offset.1)?;
146 let mut unit = Self::new(def.symbol, Dim::new(def.dim), def.kind, factor, offset)?;
147 let bad = |message: &str| Error::InvalidDefinition {
148 symbol: def.symbol.to_string(),
149 message: message.to_string(),
150 };
151 match (def.shape, offset.is_zero()) {
152 (Shape::Linear, false) => return Err(bad("linear shape requires a zero offset")),
153 (Shape::Affine, true) => return Err(bad("affine shape requires a non-zero offset")),
154 _ => {}
155 }
156 if !offset.is_zero() && def.prefixes != PrefixMode::None {
157 return Err(bad("affine units cannot take prefixes"));
158 }
159 for alias in def.aliases {
160 if !is_valid_symbol(alias) {
161 return Err(bad(
162 "alias contains characters that cannot appear in a symbol",
163 ));
164 }
165 unit.aliases.push((*alias).to_string());
166 }
167 if def.kind.is_reference() && def.conditions.trim().is_empty() {
168 return Err(bad("a unit with a reference state needs its conditions"));
169 }
170 unit.exactness = def.exactness;
171 unit.prefixes = def.prefixes;
172 unit.namespace = def.namespace.to_string();
173 unit.source = def.source.to_string();
174 unit.note = def.note.to_string();
175 unit.conditions = def.conditions.to_string();
176 Ok(unit)
177 }
178
179 pub fn with_aliases(mut self, aliases: &[&str]) -> Result<Self> {
185 for a in aliases {
186 if !is_valid_symbol(a) {
187 return Err(Error::InvalidDefinition {
188 symbol: self.symbol,
189 message: format!("alias '{a}' is not a well-formed symbol"),
190 });
191 }
192 self.aliases.push((*a).to_string());
193 }
194 Ok(self)
195 }
196
197 #[must_use]
199 pub const fn with_prefixes(mut self, prefixes: PrefixMode) -> Self {
200 if self.offset.is_zero() {
201 self.prefixes = prefixes;
202 }
203 self
204 }
205
206 #[must_use]
208 pub fn with_provenance(mut self, exactness: Exactness, source: &str, note: &str) -> Self {
209 self.exactness = exactness;
210 self.source = source.to_string();
211 self.note = note.to_string();
212 self
213 }
214
215 #[must_use]
217 pub fn with_conditions(mut self, conditions: &str) -> Self {
218 self.conditions = conditions.to_string();
219 self
220 }
221
222 #[must_use]
224 pub fn with_namespace(mut self, namespace: &str) -> Self {
225 self.namespace = namespace.to_string();
226 self
227 }
228
229 #[must_use]
231 pub fn symbol(&self) -> &str {
232 &self.symbol
233 }
234
235 #[must_use]
237 pub fn aliases(&self) -> &[String] {
238 &self.aliases
239 }
240
241 #[must_use]
243 pub const fn dim(&self) -> Dim {
244 self.dim
245 }
246
247 #[must_use]
249 pub const fn kind(&self) -> Kind {
250 self.kind
251 }
252
253 #[must_use]
255 pub const fn shape(&self) -> Shape {
256 if self.offset.is_zero() {
257 Shape::Linear
258 } else {
259 Shape::Affine
260 }
261 }
262
263 #[must_use]
265 pub const fn factor(&self) -> Ratio {
266 self.factor
267 }
268
269 #[must_use]
271 pub const fn offset(&self) -> Ratio {
272 self.offset
273 }
274
275 #[must_use]
277 pub const fn exactness(&self) -> Exactness {
278 self.exactness
279 }
280
281 #[must_use]
283 pub const fn prefixes(&self) -> PrefixMode {
284 self.prefixes
285 }
286
287 #[must_use]
289 pub fn namespace(&self) -> &str {
290 &self.namespace
291 }
292
293 #[must_use]
295 pub fn source(&self) -> &str {
296 &self.source
297 }
298
299 #[must_use]
301 pub fn note(&self) -> &str {
302 &self.note
303 }
304
305 #[must_use]
308 pub fn conditions(&self) -> &str {
309 &self.conditions
310 }
311}
312
313#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct UnitExpr {
320 expr: Expr,
321 dim: Dim,
322 kind: Kind,
323 ref_exp: i32,
325 conditions: String,
327 factor: Ratio,
328 offset: Ratio,
329 exactness: Exactness,
330 sources: Vec<String>,
331 difference: Option<String>,
334}
335
336impl UnitExpr {
337 #[must_use]
339 pub const fn unity() -> Self {
340 Self {
341 expr: Expr::One,
342 dim: Dim::NONE,
343 kind: Kind::Plain,
344 ref_exp: 0,
345 conditions: String::new(),
346 factor: Ratio::ONE,
347 offset: Ratio::ZERO,
348 exactness: Exactness::Defined,
349 sources: Vec::new(),
350 difference: None,
351 }
352 }
353
354 pub(crate) const fn from_parts(
355 expr: Expr,
356 dim: Dim,
357 kind: Kind,
358 factor: Ratio,
359 offset: Ratio,
360 exactness: Exactness,
361 sources: Vec<String>,
362 ) -> Self {
363 Self {
364 expr,
365 dim,
366 kind,
367 ref_exp: if kind.is_reference() { 1 } else { 0 },
368 conditions: String::new(),
369 factor,
370 offset,
371 exactness,
372 sources,
373 difference: None,
374 }
375 }
376
377 pub(crate) fn with_conditions(mut self, conditions: &str) -> Self {
378 if self.kind.is_reference() {
379 self.conditions = conditions.to_string();
380 }
381 self
382 }
383
384 #[must_use]
387 pub fn conditions(&self) -> &str {
388 &self.conditions
389 }
390
391 fn composed_conditions(&self, other: &Self, kind: Kind) -> String {
393 if !kind.is_reference() {
394 String::new()
395 } else if self.kind.is_reference() {
396 self.conditions.clone()
397 } else {
398 other.conditions.clone()
399 }
400 }
401
402 pub(crate) fn with_difference(mut self, difference: Option<String>) -> Self {
403 self.difference = difference;
404 self
405 }
406
407 #[must_use]
411 pub fn difference_symbol(&self) -> Option<&str> {
412 self.difference.as_deref()
413 }
414
415 #[must_use]
417 pub fn is_unity(&self) -> bool {
418 self.dim.is_dimensionless()
419 && self.kind == Kind::Plain
420 && self.factor == Ratio::ONE
421 && self.offset.is_zero()
422 }
423
424 #[must_use]
426 pub const fn expr(&self) -> &Expr {
427 &self.expr
428 }
429
430 #[must_use]
432 pub const fn dim(&self) -> Dim {
433 self.dim
434 }
435
436 #[must_use]
438 pub const fn kind(&self) -> Kind {
439 self.kind
440 }
441
442 #[must_use]
445 pub const fn reference_power(&self) -> i32 {
446 self.ref_exp
447 }
448
449 #[must_use]
451 pub const fn factor(&self) -> Ratio {
452 self.factor
453 }
454
455 #[must_use]
457 pub const fn offset(&self) -> Ratio {
458 self.offset
459 }
460
461 #[must_use]
463 pub const fn shape(&self) -> Shape {
464 if self.offset.is_zero() {
465 Shape::Linear
466 } else {
467 Shape::Affine
468 }
469 }
470
471 #[must_use]
473 pub const fn exactness(&self) -> Exactness {
474 self.exactness
475 }
476
477 #[must_use]
479 pub fn sources(&self) -> &[String] {
480 &self.sources
481 }
482
483 fn composed_reference(&self, other: &Self, sign: i32) -> Result<(Kind, i32)> {
488 let overflow = || Error::Overflow {
489 operation: "reference power",
490 };
491 let other_exp = other.ref_exp.checked_mul(sign).ok_or_else(overflow)?;
492 match (self.kind.is_reference(), other.kind.is_reference()) {
493 (true, true) if self.kind != other.kind => Err(Error::Refused {
494 reason: format!(
495 "'{}' ({}) and '{}' ({}) each carry a different reference state and cannot be combined",
496 self.expr, self.kind, other.expr, other.kind
497 ),
498 suggestions: Vec::new(),
499 }),
500 (true, true) => {
501 let exp = self.ref_exp.checked_add(other_exp).ok_or_else(overflow)?;
502 Ok(if exp == 0 {
503 (Kind::Plain, 0)
504 } else {
505 (self.kind, exp)
506 })
507 }
508 (true, false) => Ok((self.kind, self.ref_exp)),
509 (false, true) => Ok((other.kind, other_exp)),
510 (false, false) => Ok((Kind::Plain, 0)),
511 }
512 }
513
514 fn merged_sources(&self, other: &Self) -> Vec<String> {
515 let mut out = self.sources.clone();
516 for s in &other.sources {
517 if !out.contains(s) {
518 out.push(s.clone());
519 }
520 }
521 out
522 }
523
524 fn refuse_affine(&self, other: &Self) -> Result<()> {
525 for u in [self, other] {
526 if !u.offset.is_zero() {
527 let suggestions: Vec<String> = u.difference.iter().cloned().collect();
528 let advice = suggestions.first().map_or_else(
529 || {
530 String::from(
531 "use a difference unit (delta_...) for a temperature difference",
532 )
533 },
534 |d| format!("use the difference unit {d} for a temperature difference"),
535 );
536 return Err(Error::Refused {
537 reason: format!(
538 "'{}' has an offset (temperature scale) and cannot be part of a compound unit; {advice}",
539 u.expr
540 ),
541 suggestions,
542 });
543 }
544 }
545 Ok(())
546 }
547
548 pub fn mul(&self, rhs: &Self) -> Result<Self> {
555 if self.is_unity() {
556 return Ok(rhs.clone());
557 }
558 if rhs.is_unity() {
559 return Ok(self.clone());
560 }
561 self.refuse_affine(rhs)?;
562 let (kind, ref_exp) = self.composed_reference(rhs, 1)?;
563 let conditions = self.composed_conditions(rhs, kind);
564 Ok(Self {
565 expr: Expr::Mul(Box::new(self.expr.clone()), Box::new(rhs.expr.clone())),
566 dim: self.dim.checked_add(rhs.dim)?,
567 kind,
568 ref_exp,
569 conditions,
570 factor: self.factor.checked_mul(rhs.factor)?,
571 offset: Ratio::ZERO,
572 exactness: self.exactness.combine(rhs.exactness),
573 sources: self.merged_sources(rhs),
574 difference: None,
575 })
576 }
577
578 pub fn div(&self, rhs: &Self) -> Result<Self> {
584 if rhs.is_unity() {
585 return Ok(self.clone());
586 }
587 self.refuse_affine(rhs)?;
588 let (kind, ref_exp) = self.composed_reference(rhs, -1)?;
589 let conditions = self.composed_conditions(rhs, kind);
590 Ok(Self {
591 expr: Expr::Div(Box::new(self.expr.clone()), Box::new(rhs.expr.clone())),
592 dim: self.dim.checked_sub(rhs.dim)?,
593 kind,
594 ref_exp,
595 conditions,
596 factor: self.factor.checked_div(rhs.factor)?,
597 offset: Ratio::ZERO,
598 exactness: self.exactness.combine(rhs.exactness),
599 sources: self.merged_sources(rhs),
600 difference: None,
601 })
602 }
603
604 pub fn pow(&self, exp: i32) -> Result<Self> {
612 if exp == 1 {
613 return Ok(self.clone());
614 }
615 self.refuse_affine(&Self::unity())?;
616 let ref_exp = self.ref_exp.checked_mul(exp).ok_or(Error::Overflow {
617 operation: "reference power",
618 })?;
619 let keeps_reference = self.kind.is_reference() && ref_exp != 0;
622 Ok(Self {
623 expr: Expr::Pow(Box::new(self.expr.clone()), exp),
624 dim: self.dim.checked_scale(exp)?,
625 kind: if keeps_reference {
626 self.kind
627 } else {
628 Kind::Plain
629 },
630 ref_exp,
631 conditions: if keeps_reference {
632 self.conditions.clone()
633 } else {
634 String::new()
635 },
636 factor: self.factor.checked_pow(exp)?,
637 offset: Ratio::ZERO,
638 exactness: self.exactness,
639 sources: self.sources.clone(),
640 difference: None,
641 })
642 }
643}
644
645impl fmt::Display for UnitExpr {
646 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
647 self.expr.fmt(f)
648 }
649}
650
651#[cfg(test)]
652mod tests {
653 use super::*;
654
655 fn m() -> UnitExpr {
656 UnitExpr::from_parts(
657 Expr::Unit("m".into()),
658 Dim::M,
659 Kind::Plain,
660 Ratio::ONE,
661 Ratio::ZERO,
662 Exactness::Defined,
663 vec!["SI".into()],
664 )
665 }
666
667 #[test]
668 fn unity_is_identity_for_mul_and_div() {
669 let u = UnitExpr::unity();
670 assert_eq!(u.mul(&m()).unwrap(), m());
671 assert_eq!(m().mul(&u).unwrap(), m());
672 assert_eq!(m().div(&u).unwrap(), m());
673 assert!(m().pow(0).unwrap().is_unity() || m().pow(0).unwrap().dim().is_dimensionless());
674 }
675
676 #[test]
677 fn products_compose_exactly() {
678 let sq = m().mul(&m()).unwrap();
679 assert_eq!(sq.dim(), Dim::new([0, 2, 0, 0, 0, 0, 0]));
680 assert_eq!(sq.to_string(), "m.m");
681 let inv = UnitExpr::unity().div(&m()).unwrap();
682 assert_eq!(inv.dim(), Dim::new([0, -1, 0, 0, 0, 0, 0]));
683 assert_eq!(inv.sources(), ["SI"]);
684 }
685
686 #[test]
687 fn rejects_bad_definitions() {
688 assert!(Unit::linear("", Dim::M, Ratio::ONE).is_err());
689 assert!(Unit::linear("m2", Dim::M, Ratio::ONE).is_err());
690 assert!(Unit::linear("m", Dim::M, Ratio::ZERO).is_err());
691 assert!(Unit::linear("m", Dim::M, Ratio::from_int(-1)).is_err());
692 assert!(Unit::new("x", Dim::M, Kind::Plain, Ratio::ONE, Ratio::ONE).is_err());
693 assert!(
694 Unit::linear("m", Dim::M, Ratio::ONE)
695 .unwrap()
696 .with_aliases(&["a b"])
697 .is_err()
698 );
699 }
700
701 #[test]
702 fn from_def_checks_shape() {
703 let mut d = UnitDef {
704 symbol: "x",
705 aliases: &[],
706 dim: [0, 1, 0, 0, 0, 0, 0],
707 kind: Kind::Plain,
708 shape: Shape::Linear,
709 factor: (2, 4),
710 offset: (0, 1),
711 exactness: Exactness::Defined,
712 prefixes: PrefixMode::Si,
713 namespace: "std",
714 source: "test",
715 note: "",
716 conditions: "",
717 };
718 assert_eq!(
719 Unit::from_def(&d).unwrap().factor(),
720 Ratio::new(1, 2).unwrap()
721 );
722 d.shape = Shape::Affine;
723 assert!(Unit::from_def(&d).is_err());
724 d.shape = Shape::Linear;
725 d.offset = (1, 1);
726 assert!(Unit::from_def(&d).is_err());
727 d.factor = (1, 0);
728 assert!(Unit::from_def(&d).is_err());
729 }
730}