Skip to main content

gnu_units/
lib.rs

1//! Safe, high-level Rust interface to GNU Units.
2//!
3//! By default this crate uses a native pure-Rust unit engine.
4//! Build with `--no-default-features --features vendored` to use the vendored C library instead.
5
6use std::fmt;
7
8/// The vendored C-bindings crate. Only available when `features = ["vendored"]`.
9#[cfg(feature = "vendored")]
10pub use gnu_units_sys;
11
12#[cfg(feature = "currency-update")]
13pub mod currency_update;
14
15pub(crate) mod definitions;
16mod engine;
17mod units;
18
19use self::definitions::{DEFINITIONS, ensure_definitions};
20pub use self::definitions::{Definition, DefinitionKind};
21
22#[cfg(feature = "currency-update")]
23use self::definitions::load_definitions;
24
25#[cfg(feature = "currency-update")]
26pub use self::currency_update::{
27    CurrencySource, CurrencyUpdateOptions, UpdateError, fetch_currency_updates,
28};
29
30/// Numeric error codes matching the GNU units C library values.
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[repr(i32)]
33pub enum ErrorCode {
34    /// Successful conversion (no error).
35    Normal = 0,
36    /// Expression could not be parsed.
37    Parse = 1,
38    /// Incompatible dimensions in addition or subtraction.
39    BadSum = 5,
40    /// Result is not a dimensionless number.
41    NotANumber = 6,
42    /// Root of a non-integer dimension.
43    NotRoot = 7,
44    /// Unit name not found in the database.
45    UnknownUnit = 8,
46    /// Function argument outside its valid domain.
47    NotInDomain = 10,
48    /// Function argument has wrong dimensions.
49    BadFuncArg = 11,
50    /// Dimensional unit raised to an irrational exponent.
51    IrrationalExponent = 12,
52    /// Expression is not a function or table (used for fallback logic).
53    NotAFunc = 13,
54    /// Invalid numeric literal.
55    BadNum = 20,
56}
57
58impl ErrorCode {
59    #[cfg(feature = "vendored")]
60    pub(crate) fn from_raw(code: i32) -> Self {
61        match code {
62            0 => Self::Normal,
63            1 => Self::Parse,
64            5 => Self::BadSum,
65            6 => Self::NotANumber,
66            7 => Self::NotRoot,
67            8 => Self::UnknownUnit,
68            10 => Self::NotInDomain,
69            11 => Self::BadFuncArg,
70            12 => Self::IrrationalExponent,
71            13 => Self::NotAFunc,
72            20 => Self::BadNum,
73            _ => Self::Parse,
74        }
75    }
76}
77
78/// Wraps a raw error code returned by the units engine.
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct UnitsError(ErrorCode);
81
82impl UnitsError {
83    /// Returns the error code as defined in [`ErrorCode`].
84    pub fn code(&self) -> ErrorCode {
85        self.0
86    }
87
88    /// Returns `true` when the error indicates a dimensionless reduction failed.
89    pub fn is_not_dimensionless(&self) -> bool {
90        self.0 == ErrorCode::NotANumber
91    }
92
93    /// Returns `true` when the expression was invalid (unknown/unparseable).
94    pub fn is_invalid_unit(&self) -> bool {
95        self.0 == ErrorCode::UnknownUnit || self.0 == ErrorCode::Parse
96    }
97}
98
99impl fmt::Display for UnitsError {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        write!(f, "GNU units error code {}", self.0 as i32)
102    }
103}
104
105impl std::error::Error for UnitsError {}
106
107/// Convenience alias for [`std::result::Result<T, UnitsError>`].
108pub type Result<T> = std::result::Result<T, UnitsError>;
109
110/// Represents a dimensional quantity: a numeric factor paired with zero or
111/// more base dimensions.
112pub struct Unit {
113    raw: engine::RawUnit,
114}
115
116// SAFETY: For the vendored backend, all FFI calls are serialised through a
117// global mutex. The raw unittype is not accessed concurrently.
118#[cfg(feature = "vendored")]
119unsafe impl Send for Unit {}
120#[cfg(feature = "vendored")]
121unsafe impl Sync for Unit {}
122
123impl Unit {
124    /// Creates a freshly initialised unit with factor `1.0` and no dimensions.
125    pub fn new() -> Self {
126        Self {
127            raw: engine::unit_new(),
128        }
129    }
130
131    /// Parses a GNU units expression string and returns the resulting [`Unit`].
132    pub fn parse(input: &str) -> Result<Self> {
133        ensure_definitions();
134        engine::unit_parse(input).map(|raw| Self { raw })
135    }
136
137    /// Returns the numeric factor of the unit.
138    pub fn factor(&self) -> f64 {
139        engine::unit_factor(&self.raw)
140    }
141
142    /// Returns the base dimensions as a human-readable string.
143    pub fn base_units(&self) -> String {
144        engine::unit_base_units(&self.raw)
145    }
146
147    /// Multiplies `self` by `rhs` in place.
148    pub fn multiply(&mut self, rhs: Unit) -> Result<()> {
149        engine::unit_multiply(&mut self.raw, &rhs.raw)
150    }
151
152    /// Divides `self` by `rhs` in place.
153    pub fn divide(&mut self, rhs: Unit) -> Result<()> {
154        engine::unit_divide(&mut self.raw, &rhs.raw)
155    }
156
157    /// Adds `rhs` to `self` in place.
158    pub fn add(&mut self, rhs: Unit) -> Result<()> {
159        engine::unit_add(&mut self.raw, &rhs.raw)
160    }
161
162    /// Inverts `self` in place (reciprocal).
163    pub fn invert(&mut self) {
164        engine::unit_invert(&mut self.raw);
165    }
166
167    /// Raises `self` to a non-negative integer `power` in place.
168    pub fn pow(&mut self, power: i32) -> Result<()> {
169        engine::unit_pow(&mut self.raw, power)
170    }
171
172    /// Takes the `n`-th root of `self` in place.
173    pub fn root(&mut self, n: i32) -> Result<()> {
174        engine::unit_root(&mut self.raw, n)
175    }
176
177    /// Converts a dimensionless unit to its numeric value.
178    pub fn to_number(&self) -> Result<f64> {
179        engine::unit_to_number(&self.raw)
180    }
181
182    /// Converts `self` into the unit expressed by `to`, returning the numeric
183    /// conversion factor.
184    pub fn convert_to(mut self, to: Unit) -> Result<f64> {
185        self.divide(to)?;
186        self.to_number()
187    }
188
189    /// Returns `true` when `self` and `other` have the same base dimensions.
190    pub fn is_conformable(&self, other: &Unit) -> bool {
191        engine::unit_is_conformable(&self.raw, &other.raw)
192    }
193}
194
195impl Default for Unit {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl Clone for Unit {
202    fn clone(&self) -> Self {
203        Self {
204            raw: engine::unit_clone(&self.raw),
205        }
206    }
207}
208
209impl Drop for Unit {
210    fn drop(&mut self) {
211        engine::unit_drop(&mut self.raw);
212    }
213}
214
215/// Convenience wrapper around [`Unit::parse`].
216pub fn parse(input: &str) -> Result<Unit> {
217    Unit::parse(input)
218}
219
220/// Parses `from` and `to` as GNU units expressions and returns the numeric
221/// conversion factor.
222///
223/// First attempts a function or table conversion; if neither applies
224/// ([`ErrorCode::NotAFunc`]), falls back to standard unit division.
225pub fn convert(from: &str, to: &str) -> Result<f64> {
226    ensure_definitions();
227    match engine::convert_func(from, to) {
228        Err(e) if e.code() == ErrorCode::NotAFunc => {}
229        result => return result,
230    }
231    Unit::parse(from)?.convert_to(Unit::parse(to)?)
232}
233
234/// Returns all unit definitions conformable with `expr` in alphabetical order.
235pub fn conformable(expr: &str) -> Result<Vec<String>> {
236    let target = Unit::parse(expr)?;
237    let defs = list_definitions();
238    let names = defs
239        .iter()
240        .filter(|d| d.kind == DefinitionKind::Unit)
241        .filter_map(|d| {
242            let parsed = Unit::parse(&d.name).ok()?;
243            if parsed.is_conformable(&target) {
244                Some(d.name.clone())
245            } else {
246                None
247            }
248        })
249        .collect();
250    Ok(names)
251}
252
253/// Returns all unit definitions from the embedded GNU units database, sorted
254/// alphabetically.
255pub fn list_definitions() -> Vec<Definition> {
256    ensure_definitions();
257    DEFINITIONS
258        .read()
259        .unwrap_or_else(|e| e.into_inner())
260        .clone()
261}
262
263/// Reloads currency unit definitions from a GNU units currency file string.
264#[cfg(feature = "currency-update")]
265pub fn reload_currency(content: &str) {
266    ensure_definitions();
267    let new_defs = load_definitions(content, c"currency.units");
268    let mut defs = DEFINITIONS.write().unwrap_or_else(|e| e.into_inner());
269    defs.retain(|d| d.kind != DefinitionKind::Unit || !new_defs.iter().any(|n| n.name == d.name));
270    defs.extend(new_defs);
271    defs.sort_by(|a, b| a.name.cmp(&b.name));
272}
273
274// Required so that `Result<Unit, UnitsError>::unwrap_err()` compiles in tests
275// (unwrap_err needs T: Debug to format the unexpected Ok value).
276#[cfg(test)]
277impl fmt::Debug for Unit {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        write!(f, "Unit {{ factor: {} }}", self.factor())
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use rstest::rstest;
286
287    use super::*;
288    use crate::definitions::{ensure_definitions, replace_operators};
289
290    #[test]
291    fn units_error_display() {
292        let err = UnitsError(ErrorCode::BadNum);
293
294        let s = format!("{err}");
295
296        assert!(s.contains("20"));
297    }
298
299    #[rstest]
300    #[case::eq_same_code(ErrorCode::Parse, ErrorCode::Parse, true)]
301    #[case::ne_different_code(ErrorCode::Parse, ErrorCode::BadSum, false)]
302    fn units_error_eq_semantics(#[case] a: ErrorCode, #[case] b: ErrorCode, #[case] equal: bool) {
303        let ea = UnitsError(a);
304        let eb = UnitsError(b);
305
306        assert_eq!(ea == eb, equal);
307    }
308
309    #[test]
310    fn units_error_copy_semantics() {
311        let original = UnitsError(ErrorCode::BadSum);
312
313        let copy = original;
314
315        assert_eq!(copy, original);
316    }
317
318    #[rstest]
319    #[case::notanumber(ErrorCode::NotANumber, true)]
320    #[case::e_parse(ErrorCode::Parse, false)]
321    #[case::e_badsum(ErrorCode::BadSum, false)]
322    fn is_not_dimensionless(#[case] code: ErrorCode, #[case] expected: bool) {
323        let err = UnitsError(code);
324
325        assert_eq!(err.is_not_dimensionless(), expected);
326    }
327
328    #[rstest]
329    #[case::e_parse(ErrorCode::Parse, true)]
330    #[case::e_unknownunit(ErrorCode::UnknownUnit, true)]
331    #[case::e_notanumber(ErrorCode::NotANumber, false)]
332    fn is_invalid_unit(#[case] code: ErrorCode, #[case] expected: bool) {
333        let err = UnitsError(code);
334
335        assert_eq!(err.is_invalid_unit(), expected);
336    }
337
338    #[rstest]
339    #[case::via_new(Unit::new())]
340    #[case::via_default(Unit::default())]
341    fn initial_factor_is_one(#[case] unit: Unit) {
342        assert_eq!(unit.factor(), 1.0);
343    }
344
345    #[test]
346    fn error_on_unary_plus() {
347        let result = Unit::parse("+5");
348
349        assert!(result.is_err());
350    }
351
352    #[rstest]
353    #[case::hex_literal("0xff", 255.0)]
354    #[case::negative_exponent_dimensionless("2^-1", 0.5)]
355    fn parse_numeric(#[case] input: &str, #[case] expected: f64) {
356        let result = Unit::parse(input);
357
358        assert!(result.is_ok());
359        assert!((result.unwrap().factor() - expected).abs() < 1e-6);
360    }
361
362    #[test]
363    fn parse_hex_without_valid_digits_is_zero() {
364        let result = Unit::parse("0xGG");
365
366        assert!(result.is_ok());
367        assert_eq!(result.unwrap().factor(), 0.0);
368    }
369
370    /// Malformed numeric literals and trailing input must produce `E_PARSE`,
371    /// not `E_UNKNOWNUNIT` — ensures the parser gate is hit before unit lookup.
372    #[rstest]
373    #[case::error_on_trailing_paren("2 )")]
374    fn parse_error_code_is_e_parse(#[case] input: &str) {
375        let result = Unit::parse(input);
376
377        let err = result.unwrap_err();
378        assert_eq!(err.code(), ErrorCode::Parse);
379    }
380
381    #[test]
382    fn parse_m_inverse_has_inverse_dimension() {
383        let unit = Unit::parse("m^-1").unwrap();
384
385        let base = unit.base_units();
386
387        assert_eq!(unit.factor(), 1.0);
388        assert!(
389            base.contains("/ m"),
390            "expected '/ m' in base_units, got: {base}"
391        );
392    }
393
394    #[test]
395    fn clone_preserves_factor() {
396        let original = Unit::parse("5").unwrap();
397
398        let cloned = original.clone();
399
400        assert_eq!(cloned.factor(), original.factor());
401    }
402
403    #[test]
404    fn to_number_returns_factor() {
405        let unit = Unit::parse("42").unwrap();
406
407        let result = unit.to_number();
408
409        assert!(result.is_ok());
410        assert_eq!(result.unwrap(), 42.0);
411    }
412
413    #[test]
414    fn all_definitions_have_non_empty_names() {
415        let defs = list_definitions();
416
417        for d in defs.iter() {
418            assert!(!d.name.is_empty(), "found a definition with an empty name");
419        }
420    }
421
422    #[rstest]
423    #[case::kilo_prefix("kilo-")]
424    #[case::hms("hms")]
425    fn list_definitions_contains_known_entry(#[case] name: &str) {
426        let defs = list_definitions();
427
428        assert!(
429            defs.iter().any(|d| d.name == name),
430            "expected '{name}' in definitions"
431        );
432    }
433
434    #[rstest]
435    #[case::tempc("tempC", DefinitionKind::Function)]
436    #[case::gasmark("gasmark", DefinitionKind::Table)]
437    fn list_definitions_contains_known_kind_entry(
438        #[case] canonical: &str,
439        #[case] kind: DefinitionKind,
440    ) {
441        let defs = list_definitions();
442
443        assert!(
444            defs.iter()
445                .any(|d| d.canonical_name() == canonical && d.kind == kind),
446            "expected '{canonical}' with kind {kind:?} in definitions"
447        );
448    }
449
450    #[test]
451    fn definition_kind_name_invariant() {
452        let defs = list_definitions();
453
454        for d in defs.iter() {
455            match d.kind {
456                DefinitionKind::Prefix => {
457                    assert!(
458                        d.name.ends_with('-'),
459                        "prefix '{}' must end with '-'",
460                        d.name
461                    );
462                }
463                DefinitionKind::Table => {
464                    assert!(d.name.contains('['), "table '{}' must contain '['", d.name);
465                }
466                DefinitionKind::Function => {
467                    assert!(
468                        d.name.contains('('),
469                        "function '{}' must contain '('",
470                        d.name
471                    );
472                }
473                _ => {}
474            }
475        }
476    }
477
478    #[rstest]
479    #[case::simple_meter("m", "m")]
480    #[case::compound("kg m/s^2", " / ")]
481    fn base_units_contains_expected(#[case] expr: &str, #[case] contains: &str) {
482        let unit = Unit::parse(expr).unwrap();
483
484        let base = unit.base_units();
485
486        assert!(
487            base.contains(contains),
488            "base_units('{expr}') = '{base}', expected it to contain '{contains}'"
489        );
490    }
491
492    #[test]
493    fn base_units_dimensionless_is_empty() {
494        let unit = Unit::parse("42").unwrap();
495
496        let base = unit.base_units();
497
498        assert_eq!(base, "");
499    }
500
501    #[test]
502    fn conformable_does_not_contain_wrong_domain() {
503        let result = conformable("km").unwrap();
504
505        assert!(!result.contains(&"kg".to_owned()));
506        assert!(!result.contains(&"s".to_owned()));
507    }
508
509    #[rstest]
510    #[case::en_dash('\u{2013}', "-")]
511    #[case::minus_sign('\u{2212}', "-")]
512    #[case::times_sign('\u{00D7}', "*")]
513    #[case::middle_dot('\u{00B7}', "*")]
514    #[case::division_sign('\u{00F7}', "/")]
515    #[case::fraction_slash('\u{2044}', "|")]
516    #[case::nbsp('\u{00A0}', " ")]
517    #[case::zero_width_space('\u{200B}', "")]
518    fn replace_operators_cases(#[case] input: char, #[case] expected: &str) {
519        let result = replace_operators(&input.to_string());
520
521        assert_eq!(result, expected);
522    }
523
524    #[test]
525    fn root_even_negative_is_error() {
526        let mut unit = Unit::parse("-4").unwrap();
527
528        let result = unit.root(2);
529
530        assert!(result.is_err(), "even root of negative should fail");
531    }
532
533    #[test]
534    fn root_odd_negative_is_error() {
535        let mut unit = Unit::parse("-8").unwrap();
536
537        let result = unit.root(3);
538
539        assert!(
540            result.is_err(),
541            "odd root of negative should fail (matching C behavior)"
542        );
543    }
544
545    #[rstest]
546    #[case::gasmark1("gasmark(1)", "degR", 734.67, 0.1)]
547    #[case::gasmark5("gasmark(5)", "degR", 834.67, 0.1)]
548    #[case::gasmark10("gasmark(10)", "degR", 959.67, 0.1)]
549    fn table_parse_factor(
550        #[case] input: &str,
551        #[case] to: &str,
552        #[case] expected_factor_in_degr: f64,
553        #[case] tol: f64,
554    ) {
555        let result = convert(input, to);
556
557        assert!(
558            result.is_ok(),
559            "convert({input:?}, {to:?}) failed: {:?}",
560            result.err()
561        );
562        let factor = result.unwrap();
563        assert!(
564            (factor - expected_factor_in_degr).abs() < tol,
565            "convert({input:?}, {to:?}) = {factor}, expected {expected_factor_in_degr}\u{00b1}{tol}"
566        );
567    }
568
569    #[rstest]
570    #[case::tempc_inverse("~tempC(0 K)", -273.15, 0.01)]
571    fn inverse_function_parses(#[case] input: &str, #[case] expected: f64, #[case] tol: f64) {
572        let unit = Unit::parse(input);
573
574        assert!(unit.is_ok(), "parse({input:?}) failed: {:?}", unit.err());
575        let factor = unit.unwrap().factor();
576        assert!(
577            (factor - expected).abs() < tol,
578            "parse({input:?}).factor() = {factor}, expected {expected}\u{00b1}{tol}"
579        );
580    }
581
582    #[rstest]
583    #[case::tempc_0("tempC(0)", 273.15, 0.01)]
584    #[case::tempc_100("tempC(100)", 373.15, 0.01)]
585    #[case::tempf_32("tempF(32)", 273.15, 0.01)]
586    #[case::tempf_212("tempF(212)", 373.15, 0.01)]
587    fn forward_function_to_kelvin(
588        #[case] input: &str,
589        #[case] expected_kelvin: f64,
590        #[case] tol: f64,
591    ) {
592        let result = convert(input, "K");
593
594        assert!(
595            result.is_ok(),
596            "convert({input:?}, \"K\") failed: {:?}",
597            result.err()
598        );
599        let factor = result.unwrap();
600        assert!(
601            (factor - expected_kelvin).abs() < tol,
602            "convert({input:?}, \"K\") = {factor}, expected {expected_kelvin}\u{00b1}{tol}"
603        );
604    }
605
606    #[rstest]
607    #[case::kilogram("kilogram", "gram", 1000.0, 1e-9)]
608    #[case::milligram("milligram", "gram", 0.001, 1e-9)]
609    #[case::megabyte("megabyte", "byte", 1e6, 1e-3)]
610    #[case::microsecond("microsecond", "second", 1e-6, 1e-15)]
611    fn prefix_resolution(
612        #[case] from: &str,
613        #[case] to: &str,
614        #[case] expected: f64,
615        #[case] tol: f64,
616    ) {
617        let result = convert(from, to);
618
619        assert!(
620            result.is_ok(),
621            "convert({from:?}, {to:?}) failed: {:?}",
622            result.err()
623        );
624        let factor = result.unwrap();
625        assert!(
626            (factor - expected).abs() < tol,
627            "convert({from:?}, {to:?}) = {factor}, expected {expected}\u{00b1}{tol}"
628        );
629    }
630
631    #[rstest]
632    #[case::reciprocal("1|2", 0.5)]
633    #[case::fraction_5_9("5|9", 5.0 / 9.0)]
634    #[case::numdiv_3_8("3|8", 0.375)]
635    fn pipe_factor(#[case] input: &str, #[case] expected: f64) {
636        let unit = Unit::parse(input).unwrap();
637
638        assert!(
639            (unit.factor() - expected).abs() < 1e-12,
640            "{input} factor = {}, expected {expected}",
641            unit.factor()
642        );
643    }
644
645    #[test]
646    fn list_definitions_returns_independent_copy() {
647        let mut first = list_definitions();
648        let original_len = first.len();
649        first.clear();
650
651        let second = list_definitions();
652
653        assert_eq!(second.len(), original_len);
654    }
655
656    /// `m^(1|3)` must fail: the native engine uses integer dimension exponents,
657    /// so root-3 of `m^1` is not representable (1 % 3 != 0).
658    /// GAP: to support fractional-power dimensional units, the engine would need
659    /// rational exponents in `UnitValue::dimensions`.
660    #[test]
661    fn rational_exponent_one_third() {
662        ensure_definitions();
663
664        let result = Unit::parse("m^(1|3)");
665
666        assert!(
667            result.is_err(),
668            "m^(1|3) should fail (integer-only dimension exponents)"
669        );
670    }
671
672    #[rstest]
673    #[case::fractional_exponent("8^(1|3)", 2.0)]
674    #[case::asin("asin(1)", std::f64::consts::FRAC_PI_2)]
675    #[case::cuberoot("cuberoot(27)", 3.0)]
676    #[case::round("round(3.7)", 4.0)]
677    fn builtin_factor(#[case] input: &str, #[case] expected: f64) {
678        ensure_definitions();
679
680        let v = Unit::parse(input).unwrap();
681
682        assert!(
683            (v.factor() - expected).abs() < 1e-9,
684            "{input} factor = {}, expected {expected}",
685            v.factor()
686        );
687    }
688
689    /// `3|8 m`: the `|` consumes only its two adjacent `power` operands (both
690    /// dimensionless), then `m` is juxtaposed onto the 0.375 result.
691    #[test]
692    fn pipe_numdiv_with_unit() {
693        ensure_definitions();
694
695        let v = Unit::parse("3|8 m").unwrap();
696
697        assert!(
698            (v.factor() - 0.375).abs() < 1e-12,
699            "3|8 m factor = {}, expected 0.375",
700            v.factor()
701        );
702        assert!(
703            v.base_units().contains('m'),
704            "3|8 m base_units = '{}', expected it to contain 'm'",
705            v.base_units()
706        );
707    }
708
709    /// `per meter` is equivalent to `1/m`; converting between them must give 1.
710    #[test]
711    fn per_keyword_as_division() {
712        ensure_definitions();
713
714        let result = convert("per meter", "1/m");
715
716        assert!(
717            result.is_ok(),
718            "convert(\"per meter\", \"1/m\") failed: {:?}",
719            result.err()
720        );
721        assert!(
722            (result.unwrap() - 1.0).abs() < 1e-9,
723            "convert(\"per meter\", \"1/m\") != 1.0"
724        );
725    }
726
727    #[test]
728    fn add_incompatible_dimensions_returns_badsum() {
729        let result = convert("m + kg", "m");
730
731        assert!(result.is_err());
732        assert_eq!(result.unwrap_err().code(), ErrorCode::BadSum);
733    }
734
735    #[cfg(feature = "native")]
736    #[test]
737    fn non_divisible_root_returns_not_root() {
738        ensure_definitions();
739
740        let result = Unit::parse("m^(1|3)");
741
742        assert!(result.is_err());
743        assert_eq!(result.unwrap_err().code(), ErrorCode::NotRoot);
744    }
745
746    #[cfg(feature = "native")]
747    #[test]
748    fn table_dimension_mismatch_returns_bad_func_arg() {
749        ensure_definitions();
750
751        let result = convert("5 m", "gasmark");
752
753        assert!(result.is_err());
754        assert_eq!(result.unwrap_err().code(), ErrorCode::BadFuncArg);
755    }
756
757    #[rstest]
758    #[case::error_on_ln_of_zero("ln(0)")]
759    #[case::error_on_ln_of_negative("ln(-1)")]
760    #[case::error_on_log_of_zero("log(0)")]
761    #[case::error_on_log_of_negative("log(-1)")]
762    #[case::error_on_log2_of_zero("log2(0)")]
763    fn error_on_builtin_domain(#[case] expr: &str) {
764        let result = convert(expr, "1");
765
766        assert!(result.is_err(), "convert({expr:?}, \"1\") should fail");
767    }
768
769    #[rstest]
770    #[case::error_on_to_number_dimensional("m", ErrorCode::NotANumber)]
771    #[case::error_on_to_number_compound("kg m/s^2", ErrorCode::NotANumber)]
772    fn error_on_to_number(#[case] input: &str, #[case] expected_code: ErrorCode) {
773        let unit = Unit::parse(input).unwrap();
774
775        let result = unit.to_number();
776
777        assert!(result.is_err());
778        assert_eq!(result.unwrap_err().code(), expected_code);
779    }
780
781    #[test]
782    fn error_on_add_incompatible_dimensions() {
783        let mut a = Unit::parse("m").unwrap();
784        let b = Unit::parse("kg").unwrap();
785
786        let result = a.add(b);
787
788        assert!(result.is_err());
789        assert_eq!(result.unwrap_err().code(), ErrorCode::BadSum);
790    }
791
792    #[test]
793    fn error_on_convert_to_incompatible_units() {
794        let m = Unit::parse("m").unwrap();
795        let kg = Unit::parse("kg").unwrap();
796
797        let result = m.convert_to(kg);
798
799        assert!(result.is_err());
800    }
801
802    #[rstest]
803    #[case::division_by_zero("1/0", f64::INFINITY)]
804    #[case::deeply_nested("(((((((((( 1 ))))))))))", 1.0)]
805    fn parse_does_not_panic(#[case] input: &str, #[case] expected: f64) {
806        let result = Unit::parse(input);
807
808        assert!(result.is_ok(), "parse({input:?}) should not panic or fail");
809        assert_eq!(result.unwrap().factor(), expected);
810    }
811}