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}