Skip to main content

gnu_units/
lib.rs

1//! Safe, high-level Rust interface to GNU Units.
2//!
3//! Provides unit parsing, conversion, and definition listing backed by the
4//! vendored C library exposed through [`gnu_units_sys`].
5
6use std::ffi::CStr;
7use std::fmt;
8use std::mem::MaybeUninit;
9use std::os::raw::c_int;
10
11pub use gnu_units_sys;
12
13#[cfg(feature = "currency-update")]
14pub mod currency_update;
15
16mod definitions;
17mod ffi;
18mod units;
19
20#[cfg(feature = "currency-update")]
21use definitions::load_definitions;
22
23use definitions::{DEFINITIONS, ensure_definitions};
24
25pub use definitions::{Definition, DefinitionKind};
26
27#[cfg(feature = "currency-update")]
28pub use currency_update::{
29    CurrencySource, CurrencyUpdateOptions, UpdateError, fetch_currency_updates,
30};
31
32/// `UnitsError` wraps a raw error code returned by the GNU units C library.
33///
34/// Every fallible operation in this crate returns [`Result<T>`], which resolves
35/// to `Err(UnitsError)` when the underlying C function signals failure. Inspect
36/// [`code`](UnitsError::code) against the `E_*` constants re-exported from
37/// [`gnu_units_sys`] to identify the specific error kind.
38///
39/// # Examples
40///
41/// ```no_run
42/// use gnu_units::Unit;
43///
44/// # fn main() -> gnu_units::Result<()> {
45/// match Unit::parse(")") {
46///     Ok(_) => {}
47///     Err(e) => println!("parse failed: {e}"),
48/// }
49/// # Ok(())
50/// # }
51/// ```
52#[derive(Debug, Copy, Clone, Eq, PartialEq)]
53pub struct UnitsError {
54    /// Raw error code from the GNU units C library.
55    ///
56    /// Compare against the `E_*` constants exported by `gnu_units_sys`
57    /// (e.g. `gnu_units_sys::E_PARSE`, `gnu_units_sys::E_BADSUM`) to identify
58    /// the failure mode.
59    pub code: c_int,
60}
61
62impl UnitsError {
63    fn from_code(code: c_int) -> Option<Self> {
64        if code == gnu_units_sys::E_NORMAL as c_int {
65            return None;
66        }
67
68        Some(Self { code })
69    }
70
71    /// Returns `true` when the error indicates that a unit is not
72    /// dimensionless (it still carries base dimensions after reduction).
73    ///
74    /// This error typically arises in two scenarios:
75    /// - A failed [`Unit::convert_to`] where the source and target have
76    ///   incompatible dimensions (conformability mismatch).
77    /// - A [`Unit::to_number`] call on a unit that still has dimensions.
78    pub fn is_not_dimensionless(&self) -> bool {
79        self.code == gnu_units_sys::E_NOTANUMBER as c_int
80    }
81
82    /// Returns `true` when the error indicates that the input could not
83    /// be resolved to a valid unit, either because parsing failed
84    /// (`E_PARSE`) or because the unit name is not in the database
85    /// (`E_UNKNOWNUNIT`).
86    pub fn is_invalid_unit(&self) -> bool {
87        self.code == gnu_units_sys::E_UNKNOWNUNIT as c_int
88            || self.code == gnu_units_sys::E_PARSE as c_int
89    }
90}
91
92impl fmt::Display for UnitsError {
93    /// Formats the error as `"GNU units error code <N>"`.
94    ///
95    /// `<N>` is the raw integer value of [`UnitsError::code`]; compare it
96    /// against the `E_*` constants in `gnu_units_sys` to identify the specific
97    /// failure mode.
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "GNU units error code {}", self.code)
100    }
101}
102
103impl std::error::Error for UnitsError {}
104
105/// Convenience alias for [`std::result::Result<T, UnitsError>`].
106///
107/// All fallible functions in this crate return `Result<T>` rather than
108/// spelling out the full error type. Use `Result<()>` for operations that
109/// only signal success or failure, and `Result<f64>` (or another concrete
110/// type) when a value is also produced.
111///
112/// # Examples
113///
114/// ```no_run
115/// use gnu_units::{parse, Result};
116///
117/// # fn main() -> gnu_units::Result<()> {
118/// let unit = parse("km")?;
119/// println!("factor: {}", unit.factor());
120/// # Ok(())
121/// # }
122/// ```
123pub type Result<T> = std::result::Result<T, UnitsError>;
124
125/// `Unit` wraps a GNU units `unittype` for safe use from Rust.
126///
127/// A `Unit` represents a dimensional quantity: a numeric factor paired with
128/// zero or more base dimensions (length, time, mass, …). Instances are
129/// constructed via [`Unit::new`] (dimensionless, factor 1) or [`Unit::parse`]
130/// (from a GNU units expression string). All arithmetic operations mutate
131/// `self` in place and return [`Result<()>`].
132///
133/// `Unit` owns the memory allocated by the C library; it is freed
134/// automatically when the value is dropped.
135///
136/// # Examples
137///
138/// ```no_run
139/// use gnu_units::Unit;
140///
141/// # fn main() -> gnu_units::Result<()> {
142/// let mut area = Unit::parse("3 m")?;
143/// area.multiply(Unit::parse("4 m")?)?;
144/// println!("factor: {}", area.factor()); // 12.0
145/// # Ok(())
146/// # }
147/// ```
148pub struct Unit {
149    pub(crate) raw: gnu_units_sys::unittype,
150}
151
152// SAFETY: All FFI calls that read or mutate `unittype` fields are serialized
153// through `GNU_UNITS_MUTEX` in `ffi.rs`. The raw pointers inside `unittype` point to
154// C heap allocations that are only accessed under the same lock.
155unsafe impl Send for Unit {}
156unsafe impl Sync for Unit {}
157
158impl Unit {
159    /// Creates a freshly initialized unit with factor `1.0` and no dimensions.
160    ///
161    /// The underlying C function `initializeunit` zeroes all fields of the
162    /// `unittype` struct, producing the multiplicative identity, equivalent
163    /// to the dimensionless number `1`.
164    ///
165    /// Prefer [`Unit::parse`] when you want a unit with a specific value or
166    /// dimensions.
167    ///
168    /// # Examples
169    ///
170    /// ```no_run
171    /// use gnu_units::Unit;
172    ///
173    /// let unit = Unit::new();
174    /// assert_eq!(unit.factor(), 1.0);
175    /// ```
176    pub fn new() -> Self {
177        let mut raw = MaybeUninit::<gnu_units_sys::unittype>::zeroed();
178        // SAFETY: zeroed() ensures all pointer slots are null (valid for
179        // *mut c_char). initializeunit then sets factor=1.0 and the first
180        // terminator slots. The function only writes to the passed struct
181        // without accessing any global state, no lock is needed.
182        unsafe {
183            gnu_units_sys::initializeunit(raw.as_mut_ptr());
184            Self {
185                raw: raw.assume_init(),
186            }
187        }
188    }
189
190    /// Parses a GNU units expression string and returns the resulting [`Unit`].
191    ///
192    /// `input` is passed to the underlying C function `parseunit`. The string
193    /// is first converted to a null-terminated C string; a null byte anywhere
194    /// in `input` causes an immediate `E_PARSE` error without reaching the C
195    /// layer.
196    ///
197    /// # Errors
198    ///
199    /// Returns `Err(UnitsError)` with `code == E_PARSE` when:
200    ///
201    /// - `input` contains a null byte (`\0`).
202    /// - `input` is not a valid GNU units expression (e.g. unbalanced
203    ///   parentheses, unknown unit name).
204    ///
205    /// # Examples
206    ///
207    /// ```no_run
208    /// use gnu_units::Unit;
209    ///
210    /// # fn main() -> gnu_units::Result<()> {
211    /// let km = Unit::parse("km")?;
212    /// println!("factor: {}", km.factor());
213    ///
214    /// assert!(Unit::parse(")").is_err());
215    /// # Ok(())
216    /// # }
217    /// ```
218    pub fn parse(input: &str) -> Result<Self> {
219        ensure_definitions();
220        ffi::parseunit(input)
221    }
222
223    /// Multiplies `self` by `rhs` in place, consuming `rhs`.
224    ///
225    /// Delegates to the C function `multunit`. Ownership of `rhs` is
226    /// transferred to the C layer, which merges its dimension arrays into
227    /// `self`. `rhs` is dropped after the call; the underlying C allocation
228    /// is freed safely because `multunit` leaves the `rhs` struct in a
229    /// defined (empty) state.
230    ///
231    /// # Errors
232    ///
233    /// Returns `Err(UnitsError)` if the multiplication cannot be represented
234    /// (e.g. a dimensional overflow reported by the C library).
235    ///
236    /// # Examples
237    ///
238    /// ```no_run
239    /// use gnu_units::Unit;
240    ///
241    /// # fn main() -> gnu_units::Result<()> {
242    /// let mut lhs = Unit::parse("3")?;
243    /// lhs.multiply(Unit::parse("4")?)?;
244    /// assert_eq!(lhs.factor(), 12.0);
245    /// # Ok(())
246    /// # }
247    /// ```
248    pub fn multiply(&mut self, mut rhs: Unit) -> Result<()> {
249        ffi::multunit(self, &mut rhs)
250    }
251
252    /// Divides `self` by `rhs` in place, consuming `rhs`.
253    ///
254    /// Delegates to the C function `divunit`. Ownership of `rhs` is
255    /// transferred to the C layer, which inverts `rhs` and merges its
256    /// dimension arrays into `self`. `rhs` is dropped after the call; the
257    /// underlying C allocation is freed safely because `divunit` leaves the
258    /// `rhs` struct in a defined (empty) state.
259    ///
260    /// # Errors
261    ///
262    /// Returns `Err(UnitsError)` if the division cannot be represented (e.g.
263    /// a dimensional inconsistency reported by the C library).
264    ///
265    /// # Examples
266    ///
267    /// ```no_run
268    /// use gnu_units::Unit;
269    ///
270    /// # fn main() -> gnu_units::Result<()> {
271    /// let mut lhs = Unit::parse("10")?;
272    /// lhs.divide(Unit::parse("2")?)?;
273    /// assert_eq!(lhs.factor(), 5.0);
274    /// # Ok(())
275    /// # }
276    /// ```
277    pub fn divide(&mut self, mut rhs: Unit) -> Result<()> {
278        ffi::divunit(self, &mut rhs)
279    }
280
281    /// Adds `rhs` to `self` in place, consuming `rhs`.
282    ///
283    /// Delegates to the C function `addunit`. Both units must be
284    /// dimensionally compatible (same base dimensions). Ownership of `rhs`
285    /// is transferred to the C layer; `rhs` is dropped after the call.
286    ///
287    /// # Errors
288    ///
289    /// Returns `Err(UnitsError)` when the two units have incompatible
290    /// dimensions (e.g. adding a length to a mass).
291    ///
292    /// # Examples
293    ///
294    /// ```no_run
295    /// use gnu_units::Unit;
296    ///
297    /// # fn main() -> gnu_units::Result<()> {
298    /// let mut lhs = Unit::parse("3")?;
299    /// lhs.add(Unit::parse("7")?)?;
300    /// assert_eq!(lhs.factor(), 10.0);
301    /// # Ok(())
302    /// # }
303    /// ```
304    pub fn add(&mut self, mut rhs: Unit) -> Result<()> {
305        ffi::addunit(self, &mut rhs)
306    }
307
308    /// Swaps the numerator and denominator of `self` in place.
309    ///
310    /// Delegates to the C function `invertunit`, which negates the exponent
311    /// of every base dimension and takes the reciprocal of the numeric
312    /// factor. The operation is always well-defined and cannot fail.
313    ///
314    /// # Examples
315    ///
316    /// ```no_run
317    /// use gnu_units::Unit;
318    ///
319    /// # fn main() -> gnu_units::Result<()> {
320    /// let mut unit = Unit::parse("5")?;
321    /// unit.invert();
322    /// assert_eq!(unit.factor(), 0.2);
323    /// # Ok(())
324    /// # }
325    /// ```
326    pub fn invert(&mut self) {
327        // SAFETY: self is a valid initialized unit. invertunit only swaps
328        // the numerator and denominator arrays in place and reciprocates
329        // the factor, no global state is accessed.
330        unsafe {
331            gnu_units_sys::invertunit(self.as_mut_ptr());
332        }
333    }
334
335    /// Raises `self` to a non-negative integer `power` in place.
336    ///
337    /// Delegates to the C function `expunit`, which multiplies the exponent
338    /// of every base dimension by `power` and raises the numeric factor to
339    /// `power`.
340    ///
341    /// # Errors
342    ///
343    /// Returns `Err(UnitsError)` when:
344    ///
345    /// - `power` is negative (negative exponents are not supported).
346    /// - The resulting dimensions cannot be represented (e.g. an exponent
347    ///   overflow reported by the C library).
348    ///
349    /// # Examples
350    ///
351    /// ```no_run
352    /// use gnu_units::Unit;
353    ///
354    /// # fn main() -> gnu_units::Result<()> {
355    /// let mut unit = Unit::parse("3")?;
356    /// unit.pow(2)?;
357    /// assert_eq!(unit.factor(), 9.0);
358    /// # Ok(())
359    /// # }
360    /// ```
361    pub fn pow(&mut self, power: c_int) -> Result<()> {
362        if power < 0 {
363            return Err(UnitsError {
364                code: gnu_units_sys::E_BADNUM as c_int,
365            });
366        }
367        ffi::expunit(self, power)
368    }
369
370    /// Takes the `n`th root of `self` in place.
371    ///
372    /// Delegates to the C function `rootunit`. The root must be exact: every
373    /// base-dimension exponent in `self` must be divisible by `n`. If it is
374    /// not, the C library returns an error rather than producing a fractional
375    /// exponent.
376    ///
377    /// # Errors
378    ///
379    /// Returns `Err(UnitsError)` when `n` is not positive (greater than zero),
380    /// when the root is not exact (i.e. a dimension exponent is not divisible
381    /// by `n`), or when the C library signals another failure.
382    ///
383    /// # Examples
384    ///
385    /// ```no_run
386    /// use gnu_units::Unit;
387    ///
388    /// # fn main() -> gnu_units::Result<()> {
389    /// let mut unit = Unit::parse("9")?;
390    /// unit.root(2)?;
391    /// assert_eq!(unit.factor(), 3.0);
392    /// # Ok(())
393    /// # }
394    /// ```
395    pub fn root(&mut self, n: c_int) -> Result<()> {
396        if n <= 0 {
397            return Err(UnitsError {
398                code: gnu_units_sys::E_NOTROOT as c_int,
399            });
400        }
401        ffi::rootunit(self, n)
402    }
403
404    /// Converts a dimensionless unit to its numeric value.
405    ///
406    /// Internally clones `self` and calls the C function `unit2num` on the
407    /// clone, so the original unit is not mutated. The returned `f64` is
408    /// the numeric factor of the dimensionless quantity.
409    ///
410    /// # Errors
411    ///
412    /// Returns `Err(UnitsError)` when `self` carries non-zero base
413    /// dimensions (e.g. metres, kilograms). Use [`Unit::factor`] to read
414    /// the numeric factor unconditionally regardless of dimensions.
415    ///
416    /// # Examples
417    ///
418    /// ```no_run
419    /// use gnu_units::Unit;
420    ///
421    /// # fn main() -> gnu_units::Result<()> {
422    /// let unit = Unit::parse("42")?;
423    /// assert_eq!(unit.to_number()?, 42.0);
424    /// # Ok(())
425    /// # }
426    /// ```
427    pub fn to_number(&self) -> Result<f64> {
428        let mut tmp = self.clone();
429        ffi::unit2num(&mut tmp)?;
430        Ok(tmp.raw.factor)
431    }
432
433    /// Returns the numeric factor of the unit.
434    ///
435    /// The factor is the `double factor` field of the underlying `unittype`
436    /// struct. For a dimensionless unit it is the plain numeric value; for
437    /// a dimensional unit it is the SI conversion factor (e.g. `1000.0`
438    /// for `km` when the base unit is metres).
439    ///
440    /// This accessor is always infallible. For a strict dimensionless
441    /// check, use [`Unit::to_number`] instead.
442    ///
443    /// # Examples
444    ///
445    /// ```no_run
446    /// use gnu_units::Unit;
447    ///
448    /// # fn main() -> gnu_units::Result<()> {
449    /// let unit = Unit::parse("5")?;
450    /// assert_eq!(unit.factor(), 5.0);
451    /// # Ok(())
452    /// # }
453    /// ```
454    pub fn factor(&self) -> f64 {
455        self.raw.factor
456    }
457
458    /// Converts `self` into the unit expressed by `to`, returning the numeric
459    /// conversion factor.
460    ///
461    /// Both `self` and `to` are consumed by this call. Internally, `to` is
462    /// divided out of `self` using [`Unit::divide`], and the dimensionless
463    /// result is extracted with [`Unit::to_number`].
464    ///
465    /// # Errors
466    ///
467    /// Returns `Err(UnitsError)` when:
468    ///
469    /// - `self` and `to` have incompatible dimensions (e.g. converting
470    ///   kilometres to kilograms), in which case [`Unit::to_number`] reports a
471    ///   dimensional mismatch.
472    /// - The division itself fails (e.g. a dimensional overflow reported by the
473    ///   C library).
474    ///
475    /// # Examples
476    ///
477    /// ```no_run
478    /// use gnu_units::Unit;
479    ///
480    /// # fn main() -> gnu_units::Result<()> {
481    /// let factor = Unit::parse("5 km")?.convert_to(Unit::parse("miles")?)?;
482    /// println!("{factor}"); // ≈ 3.1069
483    /// # Ok(())
484    /// # }
485    /// ```
486    pub fn convert_to(mut self, to: Unit) -> Result<f64> {
487        self.divide(to)?;
488        self.to_number()
489    }
490
491    /// Returns the base dimensions of the unit as a human-readable string.
492    ///
493    /// Each non-empty, non-sentinel slot in the numerator and denominator
494    /// arrays of the underlying `unittype` is read and the corresponding C
495    /// string is collected. The terms are primitive base units as resolved by
496    /// the parser, but they are **not** sorted or canceled, redundant terms
497    /// (e.g. from `"m/s * s"`) may appear in both numerator and denominator.
498    ///
499    /// Result formats:
500    ///
501    /// - `"m"`, numerator only
502    /// - `"kg m / s s"`, numerator and denominator
503    /// - `"1 / s"`, denominator only (numerator is empty)
504    /// - `""`, dimensionless (both arrays are empty)
505    ///
506    pub fn base_units(&self) -> String {
507        // SAFETY: The pointer fields inside raw were set by the C library
508        // during parse and are immutable for the lifetime of self. NULLUNIT
509        // is a process-global constant (`""`) assigned once at file scope,
510        // reading its address is safe without the lock. CStr::from_ptr is
511        // safe because each non-null, non-NULLUNIT pointer references a
512        // valid NUL-terminated C string owned by this Unit.
513        let null_sentinel = unsafe { gnu_units_sys::NULLUNIT };
514
515        let mut numerators: Vec<String> = Vec::new();
516        let mut denominators: Vec<String> = Vec::new();
517
518        for &ptr in self.raw.numerator.iter() {
519            if ptr.is_null() {
520                // NULL is the array terminator; everything beyond is uninitialised.
521                break;
522            }
523            if ptr == null_sentinel {
524                // NULLUNIT marks a cancelled (dimensionally eliminated) entry; skip it.
525                continue;
526            }
527            // SAFETY: ptr is non-null and not NULLUNIT, so it points to a
528            // valid NUL-terminated C string managed by the C library.
529            let s = unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() };
530            numerators.push(s);
531        }
532
533        for &ptr in self.raw.denominator.iter() {
534            if ptr.is_null() {
535                break;
536            }
537            if ptr == null_sentinel {
538                continue;
539            }
540            let s = unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() };
541            denominators.push(s);
542        }
543
544        if numerators.is_empty() && denominators.is_empty() {
545            return String::new();
546        }
547
548        if denominators.is_empty() {
549            return numerators.join(" ");
550        }
551
552        let num_part = if numerators.is_empty() {
553            "1".to_string()
554        } else {
555            numerators.join(" ")
556        };
557
558        format!("{num_part} / {}", denominators.join(" "))
559    }
560
561    /// Returns `true` when `self` and `other` have the same base dimensions.
562    ///
563    /// Two units are conformable when they describe the same physical quantity
564    /// (e.g. `km` and `miles` are both lengths). The check divides a clone of
565    /// `self` by a clone of `other` and attempts to reduce the result to a
566    /// dimensionless number: success means the units are conformable.
567    ///
568    /// Neither `self` nor `other` is consumed or mutated.
569    pub fn is_conformable(&self, other: &Unit) -> bool {
570        let mut ratio = self.clone();
571        let mut other_clone = other.clone();
572        if ffi::divunit(&mut ratio, &mut other_clone).is_err() {
573            return false;
574        }
575        ffi::unit2num(&mut ratio).is_ok()
576    }
577
578    // SAFETY: Caller must ensure no aliasing pointers to self.raw exist
579    // for the duration of the FFI call.
580    pub(crate) unsafe fn as_mut_ptr(&mut self) -> *mut gnu_units_sys::unittype {
581        &mut self.raw
582    }
583}
584
585impl Default for Unit {
586    /// Returns a freshly initialized unit with factor `1.0` and no dimensions.
587    ///
588    /// Equivalent to [`Unit::new`].
589    fn default() -> Self {
590        Self::new()
591    }
592}
593
594impl Clone for Unit {
595    /// Returns a deep copy of `self`.
596    ///
597    /// Delegates to the C function `unitcopy`, which duplicates all heap
598    /// allocations owned by the `unittype` struct. The cloned `Unit` is
599    /// fully independent: dropping either value does not affect the other.
600    fn clone(&self) -> Self {
601        ffi::unitcopy(self)
602    }
603}
604
605impl Drop for Unit {
606    /// Releases the C heap memory owned by this unit.
607    ///
608    /// Delegates to the C function `freeunit`, which frees the dimension
609    /// arrays allocated by `parseunit` or `unitcopy`. After `drop`, all
610    /// pointer fields inside the `unittype` struct are invalid; the struct
611    /// itself is on the Rust stack and is reclaimed by Rust after this
612    /// function returns.
613    fn drop(&mut self) {
614        ffi::freeunit(self)
615    }
616}
617
618/// Convenience free function, equivalent to [`Unit::parse`].
619///
620/// Parses a GNU units expression string and returns the resulting [`Unit`].
621/// This function exists so callers can write `gnu_units::parse("km")` without
622/// importing [`Unit`] explicitly.
623///
624/// # Errors
625///
626/// Returns `Err(UnitsError)` for the same reasons as [`Unit::parse`]: a null
627/// byte in `input`, or an expression the GNU units parser cannot recognise.
628///
629/// # Examples
630///
631/// ```no_run
632/// # fn main() -> gnu_units::Result<()> {
633/// let unit = gnu_units::parse("km")?;
634/// println!("factor: {}", unit.factor());
635/// # Ok(())
636/// # }
637/// ```
638pub fn parse(input: &str) -> Result<Unit> {
639    Unit::parse(input)
640}
641
642/// Reloads currency unit definitions from a GNU units currency file string.
643///
644/// Parses `content` as a GNU units definitions file and registers every
645/// definition found into the C library's global hash tables, overwriting any
646/// existing entry with the same name. Also updates the in-memory definitions
647/// list returned by [`list_definitions`].
648///
649/// Call this after writing an updated currency file via
650/// [`update_currency_file`] to make the new rates effective for all subsequent
651/// [`parse`], [`convert`], and [`list_definitions`] calls.
652#[cfg(feature = "currency-update")]
653pub fn reload_currency(content: &str) {
654    ensure_definitions();
655    let new_defs = load_definitions(content, c"currency.units");
656    let mut defs = DEFINITIONS.write().unwrap_or_else(|e| e.into_inner());
657    // Remove old entries from the same file, then merge new ones
658    defs.retain(|d| d.kind != DefinitionKind::Unit || !new_defs.iter().any(|n| n.name == d.name));
659    defs.extend(new_defs);
660    defs.sort_by(|a, b| a.name.cmp(&b.name));
661}
662
663/// Parses `from` and `to` as GNU units expressions and returns the numeric
664/// conversion factor from `from` to `to`.
665///
666/// This is a convenience wrapper around [`Unit::parse`] and
667/// [`Unit::convert_to`]. Callers can write `gnu_units::convert("km", "miles")`
668/// without importing [`Unit`] explicitly.
669///
670/// # Errors
671///
672/// Returns `Err(UnitsError)` when:
673///
674/// - Either `from` or `to` cannot be parsed (see [`Unit::parse`] for the
675///   exact conditions).
676/// - The two units have incompatible dimensions (e.g. kilometres and
677///   kilograms).
678///
679/// # Examples
680///
681/// ```no_run
682/// # fn main() -> gnu_units::Result<()> {
683/// let factor = gnu_units::convert("km", "miles")?;
684/// println!("{factor}"); // ≈ 0.62137
685/// # Ok(())
686/// # }
687/// ```
688pub fn convert(from: &str, to: &str) -> Result<f64> {
689    ensure_definitions();
690    if let Some(unit) = ffi::convert_func(from, to) {
691        return Ok(unit.factor());
692    }
693    Unit::parse(from)?.convert_to(Unit::parse(to)?)
694}
695
696/// Finds all unit definitions that are conformable with `expr`.
697///
698/// Parses `expr` into a [`Unit`], then iterates over every
699/// [`DefinitionKind::Unit`] entry returned by [`list_definitions`]. Any entry
700/// whose name parses successfully and whose dimensions match those of `expr`
701/// is included in the result. The returned names are in alphabetical order.
702///
703/// # Errors
704///
705/// Returns `Err(UnitsError)` if `expr` itself cannot be parsed.
706///
707/// # Examples
708///
709/// ```no_run
710/// # fn main() -> gnu_units::Result<()> {
711/// let lengths = gnu_units::conformable("m")?;
712/// assert!(lengths.contains(&"km".to_string()));
713/// # Ok(())
714/// # }
715/// ```
716pub fn conformable(expr: &str) -> Result<Vec<String>> {
717    let target = Unit::parse(expr)?;
718    let names = list_definitions()
719        .iter()
720        .filter(|d| d.kind == DefinitionKind::Unit)
721        .filter_map(|d| {
722            let parsed = Unit::parse(&d.name).ok()?;
723            if parsed.is_conformable(&target) {
724                Some(d.name.clone())
725            } else {
726                None
727            }
728        })
729        .collect();
730    Ok(names)
731}
732
733/// Returns all unit definitions from the embedded GNU units database.
734///
735/// Each entry contains the unit name, its definition string, and what
736/// kind of definition it is (unit, prefix, function, table, or alias).
737/// The list is sorted alphabetically by name.
738pub fn list_definitions() -> std::sync::RwLockReadGuard<'static, Vec<Definition>> {
739    ensure_definitions();
740    DEFINITIONS.read().unwrap_or_else(|e| e.into_inner())
741}
742
743#[cfg(test)]
744mod tests {
745    use super::*;
746    use crate::definitions::replace_operators;
747    use rstest::rstest;
748    use std::os::raw::c_int;
749
750    #[test]
751    fn units_error_display() {
752        let err = UnitsError { code: 5 };
753
754        let formatted = format!("{err}");
755
756        assert_eq!(formatted, "GNU units error code 5");
757    }
758
759    #[test]
760    fn units_error_eq_same_code() {
761        let a = UnitsError { code: 3 };
762        let b = UnitsError { code: 3 };
763
764        assert_eq!(a, b);
765    }
766
767    #[test]
768    fn units_error_ne_different_code() {
769        let a = UnitsError { code: 1 };
770        let b = UnitsError { code: 2 };
771
772        assert_ne!(a, b);
773    }
774
775    #[test]
776    fn units_error_copy_semantics() {
777        let original = UnitsError { code: 7 };
778        let copied: UnitsError = original;
779
780        assert_eq!(copied, original);
781    }
782
783    #[rstest]
784    #[case::new(Unit::new())]
785    #[case::default(Unit::default())]
786    fn initial_factor_is_one(#[case] unit: Unit) {
787        assert_eq!(unit.factor(), 1.0);
788    }
789
790    #[rstest]
791    #[case::integer("5", 5.0)]
792    #[case::float("3.15", 3.15)]
793    #[case::large("1e10", 1e10)]
794    fn parse_numeric(#[case] input: &str, #[case] expected: f64) {
795        let unit = Unit::parse(input).unwrap();
796
797        assert_eq!(unit.factor(), expected);
798    }
799
800    #[rstest]
801    #[case::null_byte("foo\0bar", gnu_units_sys::E_PARSE as c_int)]
802    #[case::unparsable(")", gnu_units_sys::E_PARSE as c_int)]
803    fn parse_error(#[case] input: &str, #[case] expected_code: c_int) {
804        let result = Unit::parse(input);
805
806        assert_eq!(
807            result.err().unwrap(),
808            UnitsError {
809                code: expected_code
810            }
811        );
812    }
813
814    #[test]
815    fn clone_preserves_factor() {
816        let unit = Unit::parse("7").unwrap();
817
818        let cloned = unit.clone();
819
820        assert_eq!(cloned.factor(), 7.0);
821    }
822
823    #[test]
824    fn multiply_five_by_three() {
825        let mut lhs = Unit::parse("5").unwrap();
826        let rhs = Unit::parse("3").unwrap();
827
828        lhs.multiply(rhs).unwrap();
829
830        assert_eq!(lhs.factor(), 15.0);
831    }
832
833    #[test]
834    fn divide_ten_by_two() {
835        let mut lhs = Unit::parse("10").unwrap();
836        let rhs = Unit::parse("2").unwrap();
837
838        lhs.divide(rhs).unwrap();
839
840        assert_eq!(lhs.factor(), 5.0);
841    }
842
843    #[test]
844    fn add_three_and_seven() {
845        let mut lhs = Unit::parse("3").unwrap();
846        let rhs = Unit::parse("7").unwrap();
847
848        lhs.add(rhs).unwrap();
849
850        assert_eq!(lhs.factor(), 10.0);
851    }
852
853    #[test]
854    fn invert_five_is_point_two() {
855        let mut unit = Unit::parse("5").unwrap();
856
857        unit.invert();
858
859        assert_eq!(unit.factor(), 0.2);
860    }
861
862    #[test]
863    fn pow_three_squared_is_nine() {
864        let mut unit = Unit::parse("3").unwrap();
865
866        unit.pow(2).unwrap();
867
868        assert_eq!(unit.factor(), 9.0);
869    }
870
871    #[test]
872    fn root_sqrt_nine_is_three() {
873        let mut unit = Unit::parse("9").unwrap();
874
875        unit.root(2).unwrap();
876
877        assert_eq!(unit.factor(), 3.0);
878    }
879
880    #[rstest]
881    #[case::negative(-1, gnu_units_sys::E_BADNUM as c_int)]
882    #[case::min_int(c_int::MIN, gnu_units_sys::E_BADNUM as c_int)]
883    fn pow_error(#[case] power: c_int, #[case] expected_code: c_int) {
884        let mut unit = Unit::parse("3").unwrap();
885
886        let result = unit.pow(power);
887
888        assert_eq!(
889            result,
890            Err(UnitsError {
891                code: expected_code
892            })
893        );
894    }
895
896    #[rstest]
897    #[case::zero(0, gnu_units_sys::E_NOTROOT as c_int)]
898    #[case::negative(-1, gnu_units_sys::E_NOTROOT as c_int)]
899    fn root_error(#[case] n: c_int, #[case] expected_code: c_int) {
900        let mut unit = Unit::parse("9").unwrap();
901
902        let result = unit.root(n);
903
904        assert_eq!(
905            result,
906            Err(UnitsError {
907                code: expected_code
908            })
909        );
910    }
911
912    #[test]
913    fn to_number_returns_factor() {
914        let unit = Unit::parse("42").unwrap();
915
916        let result = unit.to_number().unwrap();
917
918        assert_eq!(result, 42.0);
919    }
920
921    #[test]
922    fn free_parse_delegates_to_unit_parse() {
923        let unit = parse("5").unwrap();
924
925        assert_eq!(unit.factor(), 5.0);
926    }
927
928    #[rstest]
929    #[case::dimensionless_to_itself("5", "1", 5.0, 1e-10)]
930    #[case::km_to_m("km", "m", 1000.0, 1e-10)]
931    #[case::identity_m_to_m("m", "m", 1.0, 1e-10)]
932    #[case::numeric_prefix("5 km", "miles", 3.10686, 1e-4)]
933    fn convert_to_compatible_units(
934        #[case] from: &str,
935        #[case] to: &str,
936        #[case] expected: f64,
937        #[case] tolerance: f64,
938    ) {
939        let from_unit = Unit::parse(from).unwrap();
940        let to_unit = Unit::parse(to).unwrap();
941
942        let result = from_unit.convert_to(to_unit).unwrap();
943
944        assert!(
945            (result - expected).abs() < tolerance,
946            "convert_to({from:?}, {to:?}) = {result}, expected {expected} ±{tolerance}"
947        );
948    }
949
950    #[test]
951    fn error_on_convert_to_incompatible_dimensions() {
952        let from_unit = Unit::parse("km").unwrap();
953        let to_unit = Unit::parse("kg").unwrap();
954
955        let result = from_unit.convert_to(to_unit);
956
957        assert_eq!(
958            result,
959            Err(UnitsError {
960                code: gnu_units_sys::E_NOTANUMBER as c_int
961            })
962        );
963    }
964
965    #[rstest]
966    #[case::invalid_from(")", "m", gnu_units_sys::E_PARSE as c_int)]
967    #[case::invalid_to("m", ")", gnu_units_sys::E_PARSE as c_int)]
968    #[case::incompatible_dimensions("km", "kg", gnu_units_sys::E_NOTANUMBER as c_int)]
969    fn convert_error(#[case] from: &str, #[case] to: &str, #[case] expected_code: c_int) {
970        let result = convert(from, to);
971
972        assert_eq!(
973            result,
974            Err(UnitsError {
975                code: expected_code
976            })
977        );
978    }
979
980    #[rstest]
981    #[case::figure_dash("\u{2012}x", "-x")]
982    #[case::en_dash("\u{2013}y", "-y")]
983    #[case::minus_sign("\u{2212}z", "-z")]
984    #[case::times("\u{00D7}a", "*a")]
985    #[case::nary_times("\u{2A09}b", "*b")]
986    #[case::middle_dot("\u{00B7}c", "*c")]
987    #[case::dot_operator("\u{22C5}d", "*d")]
988    #[case::division_sign("\u{00F7}e", "/e")]
989    #[case::division_slash("\u{2215}f", "/f")]
990    #[case::fraction_slash("\u{2044}g", "|g")]
991    #[case::no_break_space("a\u{00A0}b", "a b")]
992    #[case::ogham_space("a\u{1680}b", "a b")]
993    #[case::en_quad("a\u{2000}b", "a b")]
994    #[case::thin_space("a\u{2009}b", "a b")]
995    #[case::hair_space("a\u{200A}b", "a b")]
996    #[case::narrow_no_break_space("a\u{202F}b", "a b")]
997    #[case::medium_math_space("a\u{205F}b", "a b")]
998    #[case::ideographic_space("a\u{3000}b", "a b")]
999    #[case::zero_width_space("a\u{200B}b", "ab")]
1000    #[case::zero_width_non_joiner("a\u{200C}b", "ab")]
1001    #[case::plain_ascii("hello", "hello")]
1002    #[case::empty_string("", "")]
1003    #[case::multiple_replacements("\u{2212}3\u{00D7}4", "-3*4")]
1004    #[case::preserves_ascii_operators("3*4 + 5/2 - 1", "3*4 + 5/2 - 1")]
1005    #[case::mixed_unicode_and_ascii("3\u{00D7}4 + 5\u{00F7}2", "3*4 + 5/2")]
1006    fn replace_operators_cases(#[case] input: &str, #[case] expected: &str) {
1007        let result = replace_operators(input);
1008
1009        assert_eq!(result, expected);
1010    }
1011
1012    #[rstest]
1013    #[case::prefix_kilo("kilogram", "gram", 1000.0, 1e-10)]
1014    #[case::temperature_diff("degF", "degC", 0.555_555_555_6, 1e-8)]
1015    #[case::element_mercury("mercury", "1", 200.59, 0.01)]
1016    #[case::utf8_micro("\u{00B5}m", "m", 1e-6, 1e-16)]
1017    #[case::knot_to_mps("knot", "m/s", 0.514_444, 1e-4)]
1018    #[case::inches_to_cm("inch", "cm", 2.54, 1e-10)]
1019    #[case::hour_to_seconds("hour", "s", 3600.0, 1e-10)]
1020    #[case::line_continuation("spherevolume(1 m)", "m^3", 4.18879, 1e-4)]
1021    fn definitions_convert(
1022        #[case] from: &str,
1023        #[case] to: &str,
1024        #[case] expected: f64,
1025        #[case] tolerance: f64,
1026    ) {
1027        let result = convert(from, to).unwrap();
1028
1029        assert!(
1030            (result - expected).abs() < tolerance,
1031            "convert({from:?}, {to:?}) = {result}, expected {expected} ±{tolerance}"
1032        );
1033    }
1034
1035    #[rstest]
1036    #[case::table_gasmark("gasmark1", 1.0, 1e-10)]
1037    #[case::function_tempf("tempF(32)", 273.15, 0.01)]
1038    fn definitions_parse_factor(
1039        #[case] input: &str,
1040        #[case] expected: f64,
1041        #[case] tolerance: f64,
1042    ) {
1043        let unit = Unit::parse(input).unwrap();
1044
1045        assert!(
1046            (unit.factor() - expected).abs() < tolerance,
1047            "parse({input:?}).factor() = {}, expected {expected} ±{tolerance}",
1048            unit.factor()
1049        );
1050    }
1051
1052    #[cfg(feature = "currency-update")]
1053    #[rstest]
1054    #[case::currency_usd("USD")]
1055    #[case::cpi_now("UScpi_now")]
1056    fn definitions_parse_currency(#[case] input: &str) {
1057        let unit = Unit::parse(input).unwrap();
1058
1059        assert!(
1060            unit.factor() > 0.0,
1061            "parse({input:?}).factor() should be > 0"
1062        );
1063    }
1064
1065    #[test]
1066    fn list_definitions_is_not_empty() {
1067        let defs = list_definitions();
1068
1069        assert!(
1070            defs.len() > 1000,
1071            "expected >1000 definitions, got {}",
1072            defs.len()
1073        );
1074    }
1075
1076    #[test]
1077    fn list_definitions_is_sorted_alphabetically() {
1078        let defs = list_definitions();
1079        let names: Vec<&str> = defs.iter().map(|d| d.name.as_str()).collect();
1080
1081        let mut sorted = names.clone();
1082        sorted.sort();
1083
1084        assert_eq!(names, sorted);
1085    }
1086
1087    #[test]
1088    fn all_definitions_have_non_empty_names() {
1089        let defs = list_definitions();
1090
1091        for def in defs.iter() {
1092            assert!(!def.name.is_empty(), "found empty name entry");
1093        }
1094    }
1095
1096    #[rstest]
1097    #[case::unit_m("m", "!", DefinitionKind::Unit)]
1098    #[case::unit_meter("meter", "m", DefinitionKind::Unit)]
1099    #[case::prefix_kilo("kilo-", "1e3", DefinitionKind::Prefix)]
1100    #[case::alias_hms("hms", "hr;min;sec", DefinitionKind::Alias)]
1101    fn list_definitions_contains_known_entry(
1102        #[case] name: &str,
1103        #[case] expected_def: &str,
1104        #[case] expected_kind: DefinitionKind,
1105    ) {
1106        let defs = list_definitions();
1107        let found = defs.iter().find(|d| d.name == name);
1108
1109        assert!(found.is_some(), "entry '{name}' not found in definitions");
1110        let entry = found.unwrap();
1111        assert_eq!(entry.definition, expected_def);
1112        assert_eq!(entry.kind, expected_kind);
1113    }
1114
1115    #[rstest]
1116    #[case::function_tempc("tempC(x)", DefinitionKind::Function)]
1117    #[case::table_gasmark("gasmark[degR]", DefinitionKind::Table)]
1118    fn list_definitions_contains_known_kind_entry(
1119        #[case] name: &str,
1120        #[case] expected_kind: DefinitionKind,
1121    ) {
1122        let defs = list_definitions();
1123        let found = defs.iter().find(|d| d.name == name);
1124
1125        assert!(found.is_some(), "entry '{name}' not found in definitions");
1126        assert_eq!(found.unwrap().kind, expected_kind);
1127    }
1128
1129    #[rstest]
1130    #[case::prefix_ends_with_dash(DefinitionKind::Prefix, '-')]
1131    #[case::table_contains_bracket(DefinitionKind::Table, '[')]
1132    #[case::function_contains_paren(DefinitionKind::Function, '(')]
1133    fn definition_kind_name_invariant(#[case] kind: DefinitionKind, #[case] expected_char: char) {
1134        let defs = list_definitions();
1135
1136        for def in defs.iter().filter(|d| d.kind == kind) {
1137            assert!(
1138                def.name.contains(expected_char),
1139                "{kind:?} entry '{}' does not contain '{expected_char}'",
1140                def.name
1141            );
1142        }
1143    }
1144
1145    #[rstest]
1146    #[case::e_notanumber_true(gnu_units_sys::E_NOTANUMBER as c_int, true)]
1147    #[case::e_parse_false(gnu_units_sys::E_PARSE as c_int, false)]
1148    #[case::e_unknownunit_false(gnu_units_sys::E_UNKNOWNUNIT as c_int, false)]
1149    #[case::arbitrary_code_false(42, false)]
1150    fn is_not_dimensionless(#[case] code: c_int, #[case] expected: bool) {
1151        let err = UnitsError { code };
1152
1153        let result = err.is_not_dimensionless();
1154
1155        assert_eq!(result, expected);
1156    }
1157
1158    #[rstest]
1159    #[case::e_unknownunit_true(gnu_units_sys::E_UNKNOWNUNIT as c_int, true)]
1160    #[case::e_parse_true(gnu_units_sys::E_PARSE as c_int, true)]
1161    #[case::e_notanumber_false(gnu_units_sys::E_NOTANUMBER as c_int, false)]
1162    #[case::arbitrary_code_false(42, false)]
1163    fn is_invalid_unit(#[case] code: c_int, #[case] expected: bool) {
1164        let err = UnitsError { code };
1165
1166        let result = err.is_invalid_unit();
1167
1168        assert_eq!(result, expected);
1169    }
1170
1171    #[rstest]
1172    #[case::meter_contains_m("m", "m")]
1173    #[case::compound_contains_slash("kg m/s^2", " / ")]
1174    #[case::inverse_contains_one_slash("1/s", "1 / ")]
1175    fn base_units_contains_expected(#[case] input: &str, #[case] expected_substr: &str) {
1176        let unit = Unit::parse(input).unwrap();
1177
1178        let result = unit.base_units();
1179
1180        assert!(
1181            result.contains(expected_substr),
1182            "expected '{expected_substr}' in {:?}",
1183            result
1184        );
1185    }
1186
1187    #[rstest]
1188    #[case::unit_new_is_empty("")]
1189    #[case::dimensionless_number_is_empty("")]
1190    fn base_units_dimensionless_is_empty(#[case] expected: &str) {
1191        let unit = Unit::new();
1192
1193        let result = unit.base_units();
1194
1195        assert_eq!(result, expected);
1196    }
1197
1198    #[test]
1199    fn base_units_pure_number_is_empty() {
1200        let unit = Unit::parse("5").unwrap();
1201
1202        let result = unit.base_units();
1203
1204        assert_eq!(result, "");
1205    }
1206
1207    #[rstest]
1208    #[case::km_and_miles("km", "miles", true)]
1209    #[case::m_and_kg("m", "kg", false)]
1210    #[case::velocity_conformable("m/s", "knot", true)]
1211    fn is_conformable(#[case] a: &str, #[case] b: &str, #[case] expected: bool) {
1212        let unit_a = Unit::parse(a).unwrap();
1213        let unit_b = Unit::parse(b).unwrap();
1214
1215        let result = unit_a.is_conformable(&unit_b);
1216
1217        assert_eq!(result, expected);
1218    }
1219
1220    #[test]
1221    fn is_conformable_with_itself() {
1222        let unit = Unit::parse("m").unwrap();
1223
1224        let result = unit.is_conformable(&unit.clone());
1225
1226        assert!(result);
1227    }
1228
1229    #[rstest]
1230    #[case::m_contains_meter("m", "meter")]
1231    #[case::m_contains_mile("m", "mile")]
1232    #[case::m_contains_ft("m", "ft")]
1233    #[case::m_contains_inch("m", "inch")]
1234    #[case::kg_contains_lb("kg", "lb")]
1235    #[case::kg_contains_g("kg", "g")]
1236    fn conformable_contains_expected_unit(#[case] expr: &str, #[case] expected_unit: &str) {
1237        let result = conformable(expr).unwrap();
1238
1239        assert!(
1240            result.contains(&expected_unit.to_string()),
1241            "{} missing from {:?}",
1242            expected_unit,
1243            result
1244        );
1245    }
1246
1247    #[rstest]
1248    #[case::m_not_kg("m", "kg")]
1249    #[case::m_not_second("m", "second")]
1250    fn conformable_does_not_contain_wrong_domain(
1251        #[case] expr: &str,
1252        #[case] unexpected_unit: &str,
1253    ) {
1254        let result = conformable(expr).unwrap();
1255
1256        assert!(
1257            !result.contains(&unexpected_unit.to_string()),
1258            "{} should not appear in {:?}",
1259            unexpected_unit,
1260            result
1261        );
1262    }
1263
1264    #[test]
1265    fn error_on_conformable_invalid_expression() {
1266        let result = conformable(")");
1267
1268        assert!(result.is_err(), "expected Err for invalid expression");
1269    }
1270
1271    #[rstest]
1272    #[case::zero_celsius("273.15 K", "tempC", 0.0, 1e-6)]
1273    #[case::boiling_point_celsius("373.15 K", "tempC", 100.0, 1e-6)]
1274    #[case::freezing_point_fahrenheit("273.15 K", "tempF", 32.0, 1e-4)]
1275    #[case::body_temp_fahrenheit("310.15 K", "tempF", 98.6, 0.1)]
1276    #[case::absolute_zero_kelvin("0 K", "tempK", 0.0, 1e-10)]
1277    #[case::fallback_non_function("km", "m", 1000.0, 1e-10)]
1278    fn convert_via_function(
1279        #[case] from: &str,
1280        #[case] to: &str,
1281        #[case] expected: f64,
1282        #[case] tolerance: f64,
1283    ) {
1284        let result = convert(from, to).unwrap();
1285
1286        assert!(
1287            (result - expected).abs() < tolerance,
1288            "convert({from:?}, {to:?}) = {result}, expected {expected} ±{tolerance}"
1289        );
1290    }
1291}