Skip to main content

dimvexa_core/
unit.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Unit definitions (static data and owned form) and resolved unit expressions.
3
4use 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/// A unit as it appears in a compiled data table (plain data, `const`-constructible).
14///
15/// Fractions are `(numerator, denominator)` pairs; the registry validates and reduces them
16/// when loading.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub struct UnitDef {
19    /// Primary symbol, for example `psi`.
20    pub symbol: &'static str,
21    /// Alternative symbols, for example `["\u{b0}C"]` for `degC`.
22    pub aliases: &'static [&'static str],
23    /// Dimension exponents of `[kg, m, s, A, K, mol, cd]`.
24    pub dim: [i8; 7],
25    /// Quantity kind.
26    pub kind: Kind,
27    /// Conversion shape to the coherent SI unit.
28    pub shape: Shape,
29    /// Factor `a` in `si = a * x + b`, as `(numerator, denominator)`.
30    pub factor: (i128, i128),
31    /// Offset `b` in `si = a * x + b`, as `(numerator, denominator)`.
32    pub offset: (i128, i128),
33    /// Defined or measured.
34    pub exactness: Exactness,
35    /// Accepted prefixes.
36    pub prefixes: PrefixMode,
37    /// Namespace, `si` or `std` for the core table.
38    pub namespace: &'static str,
39    /// Citation of the primary definition.
40    pub source: &'static str,
41    /// Free-text note (derivation, caveats).
42    pub note: &'static str,
43    /// The reference state the unit is defined at (ADR-013), for example
44    /// `273.15 K, 101 325 Pa`. Empty for a unit without one; required for a reference kind.
45    pub conditions: &'static str,
46}
47
48/// A bare symbol that is deliberately rejected, with the explicit symbols to use instead.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub struct AmbiguousDef {
51    /// The ambiguous bare symbol, for example `ton`.
52    pub symbol: &'static str,
53    /// Explicit unit symbols present in the table.
54    pub candidates: &'static [&'static str],
55    /// Explicit symbols that are planned but not yet in the table.
56    pub planned: &'static [&'static str],
57    /// Why the symbol is ambiguous.
58    pub note: &'static str,
59}
60
61/// An owned unit definition held by a [`crate::Registry`].
62///
63/// `si_value = factor * x + offset`, where `x` is the value in this unit.
64#[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    /// A linear unit: `si = factor * x`, defined exactly, no prefixes, namespace `std`.
82    ///
83    /// # Errors
84    ///
85    /// [`Error::InvalidDefinition`] for a malformed symbol or a non-positive factor.
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use dimvexa_core::{Dim, Ratio, Unit};
91    /// let inch = Unit::linear("in", Dim::M, Ratio::new(127, 5000)?)?;
92    /// assert_eq!(inch.symbol(), "in");
93    /// # Ok::<(), dimvexa_core::Error>(())
94    /// ```
95    pub fn linear(symbol: &str, dim: Dim, factor: Ratio) -> Result<Self> {
96        Self::new(symbol, dim, Kind::Plain, factor, Ratio::ZERO)
97    }
98
99    /// A general unit with an offset (affine when `offset` is non-zero).
100    ///
101    /// # Errors
102    ///
103    /// [`Error::InvalidDefinition`] for a malformed symbol, a non-positive factor, or an
104    /// offset on a unit that is not a temperature.
105    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    /// Validates and converts a static table row.
138    ///
139    /// # Errors
140    ///
141    /// [`Error::InvalidDefinition`] when the row is inconsistent (shape/offset mismatch,
142    /// bad fraction, bad symbol or alias), or [`Error::DivisionByZero`] for a zero denominator.
143    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    /// Adds aliases (alternative symbols).
180    ///
181    /// # Errors
182    ///
183    /// [`Error::InvalidDefinition`] if an alias is not a well-formed symbol.
184    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    /// Sets the accepted prefixes (ignored for units with an offset, which never take any).
198    #[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    /// Sets exactness, source and note.
207    #[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    /// Sets the reference state text (ADR-013), for a unit that is defined from a reference unit.
216    #[must_use]
217    pub fn with_conditions(mut self, conditions: &str) -> Self {
218        self.conditions = conditions.to_string();
219        self
220    }
221
222    /// Sets the namespace.
223    #[must_use]
224    pub fn with_namespace(mut self, namespace: &str) -> Self {
225        self.namespace = namespace.to_string();
226        self
227    }
228
229    /// Primary symbol.
230    #[must_use]
231    pub fn symbol(&self) -> &str {
232        &self.symbol
233    }
234
235    /// Alternative symbols.
236    #[must_use]
237    pub fn aliases(&self) -> &[String] {
238        &self.aliases
239    }
240
241    /// Dimension.
242    #[must_use]
243    pub const fn dim(&self) -> Dim {
244        self.dim
245    }
246
247    /// Quantity kind.
248    #[must_use]
249    pub const fn kind(&self) -> Kind {
250        self.kind
251    }
252
253    /// Conversion shape.
254    #[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    /// Factor to the coherent SI unit.
264    #[must_use]
265    pub const fn factor(&self) -> Ratio {
266        self.factor
267    }
268
269    /// Offset to the coherent SI unit.
270    #[must_use]
271    pub const fn offset(&self) -> Ratio {
272        self.offset
273    }
274
275    /// Defined or measured.
276    #[must_use]
277    pub const fn exactness(&self) -> Exactness {
278        self.exactness
279    }
280
281    /// Accepted prefixes.
282    #[must_use]
283    pub const fn prefixes(&self) -> PrefixMode {
284        self.prefixes
285    }
286
287    /// Namespace.
288    #[must_use]
289    pub fn namespace(&self) -> &str {
290        &self.namespace
291    }
292
293    /// Citation of the definition.
294    #[must_use]
295    pub fn source(&self) -> &str {
296        &self.source
297    }
298
299    /// Note.
300    #[must_use]
301    pub fn note(&self) -> &str {
302        &self.note
303    }
304
305    /// The reference state the unit is defined at (`273.15 K, 101 325 Pa` for `Nm3`), or an
306    /// empty string for a unit without one (ADR-013).
307    #[must_use]
308    pub fn conditions(&self) -> &str {
309        &self.conditions
310    }
311}
312
313/// A resolved unit expression such as `km/h` or `kW.h`: dimension, kind and the exact
314/// relation to the coherent SI unit, composed once.
315///
316/// `si_value = factor * x + offset`. Only a single (possibly prefixed) unit can have a
317/// non-zero offset; compound expressions are linear.
318#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct UnitExpr {
320    expr: Expr,
321    dim: Dim,
322    kind: Kind,
323    /// Signed power of the reference state: 1 for `Nm3`, -1 for `1/Nm3`, 0 without one.
324    ref_exp: i32,
325    /// The reference state text of the unit that carries the reference (ADR-013), empty without.
326    conditions: String,
327    factor: Ratio,
328    offset: Ratio,
329    exactness: Exactness,
330    sources: Vec<String>,
331    /// Symbol of the registered temperature-difference unit of the same scale (`delta_K`
332    /// for `K`), only set for a plain absolute temperature unit.
333    difference: Option<String>,
334}
335
336impl UnitExpr {
337    /// The unit `1` (dimensionless, factor one).
338    #[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    /// The reference state of the unit that gives this expression its reference kind
385    /// (`273.15 K (0 degC), 101 325 Pa` for `Nm3/h`), or an empty string without one.
386    #[must_use]
387    pub fn conditions(&self) -> &str {
388        &self.conditions
389    }
390
391    /// The conditions of whichever operand carries the reference, if the result has one.
392    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    /// Symbol of the registered temperature-difference unit that goes with this absolute
408    /// temperature unit (`delta_K` for `K`, `delta_degC` for `degC`). `None` for every other
409    /// unit expression, including prefixed temperatures such as `mK`.
410    #[must_use]
411    pub fn difference_symbol(&self) -> Option<&str> {
412        self.difference.as_deref()
413    }
414
415    /// True for the plain scalar `1`.
416    #[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    /// The syntax tree this expression was resolved from.
425    #[must_use]
426    pub const fn expr(&self) -> &Expr {
427        &self.expr
428    }
429
430    /// Dimension.
431    #[must_use]
432    pub const fn dim(&self) -> Dim {
433        self.dim
434    }
435
436    /// Kind.
437    #[must_use]
438    pub const fn kind(&self) -> Kind {
439        self.kind
440    }
441
442    /// The signed power of the reference state (ADR-013): 1 for `Nm3` or `Nm3/h`, -1 for `h/Nm3`,
443    /// 0 for a unit without a reference. Two units convert only when kind and power agree.
444    #[must_use]
445    pub const fn reference_power(&self) -> i32 {
446        self.ref_exp
447    }
448
449    /// Exact factor to the coherent SI unit.
450    #[must_use]
451    pub const fn factor(&self) -> Ratio {
452        self.factor
453    }
454
455    /// Exact offset to the coherent SI unit.
456    #[must_use]
457    pub const fn offset(&self) -> Ratio {
458        self.offset
459    }
460
461    /// Conversion shape relative to SI.
462    #[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    /// Defined, or measured if any component is measured.
472    #[must_use]
473    pub const fn exactness(&self) -> Exactness {
474        self.exactness
475    }
476
477    /// Distinct source citations of all components, in order of first use.
478    #[must_use]
479    pub fn sources(&self) -> &[String] {
480        &self.sources
481    }
482
483    /// Kind and reference power of a product (`sign` 1) or quotient (`sign` -1). A reference
484    /// survives (`Nm3/h` is still a normal volume per time) and its power is tracked, so
485    /// `Nm3/m3` and `m3/Nm3` differ; the same reference cancels; two different references in
486    /// one expression are refused.
487    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    /// Product of two unit expressions. Multiplying by the plain scalar `1` returns the
549    /// other operand unchanged.
550    ///
551    /// # Errors
552    ///
553    /// [`Error::Refused`] if an operand has an offset, [`Error::Overflow`] on overflow.
554    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    /// Quotient of two unit expressions. Dividing by the plain scalar `1` returns `self`.
579    ///
580    /// # Errors
581    ///
582    /// [`Error::Refused`] if an operand has an offset, [`Error::Overflow`] on overflow.
583    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    /// Integer power. The power 1 returns `self` unchanged (so `degC^1` stays valid), the
605    /// power 0 returns the scalar `1`.
606    ///
607    /// # Errors
608    ///
609    /// [`Error::Refused`] for an offset unit with a power other than 1,
610    /// [`Error::Overflow`] on overflow.
611    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        // A power of 0 cancels the reference: `(scf)^0` is the plain number 1, not a
620        // dimensionless standard volume that would let an actual volume pass as `scf`.
621        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}