1use std::fmt;
7
8#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32#[repr(i32)]
33pub enum ErrorCode {
34 Normal = 0,
36 Parse = 1,
38 BadSum = 5,
40 NotANumber = 6,
42 NotRoot = 7,
44 UnknownUnit = 8,
46 NotInDomain = 10,
48 BadFuncArg = 11,
50 IrrationalExponent = 12,
52 NotAFunc = 13,
54 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct UnitsError(ErrorCode);
81
82impl UnitsError {
83 pub fn code(&self) -> ErrorCode {
85 self.0
86 }
87
88 pub fn is_not_dimensionless(&self) -> bool {
90 self.0 == ErrorCode::NotANumber
91 }
92
93 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
107pub type Result<T> = std::result::Result<T, UnitsError>;
109
110pub struct Unit {
113 raw: engine::RawUnit,
114}
115
116#[cfg(feature = "vendored")]
119unsafe impl Send for Unit {}
120#[cfg(feature = "vendored")]
121unsafe impl Sync for Unit {}
122
123impl Unit {
124 pub fn new() -> Self {
126 Self {
127 raw: engine::unit_new(),
128 }
129 }
130
131 pub fn parse(input: &str) -> Result<Self> {
133 ensure_definitions();
134 engine::unit_parse(input).map(|raw| Self { raw })
135 }
136
137 pub fn factor(&self) -> f64 {
139 engine::unit_factor(&self.raw)
140 }
141
142 pub fn base_units(&self) -> String {
144 engine::unit_base_units(&self.raw)
145 }
146
147 pub fn multiply(&mut self, rhs: Unit) -> Result<()> {
149 engine::unit_multiply(&mut self.raw, &rhs.raw)
150 }
151
152 pub fn divide(&mut self, rhs: Unit) -> Result<()> {
154 engine::unit_divide(&mut self.raw, &rhs.raw)
155 }
156
157 pub fn add(&mut self, rhs: Unit) -> Result<()> {
159 engine::unit_add(&mut self.raw, &rhs.raw)
160 }
161
162 pub fn invert(&mut self) {
164 engine::unit_invert(&mut self.raw);
165 }
166
167 pub fn pow(&mut self, power: i32) -> Result<()> {
169 engine::unit_pow(&mut self.raw, power)
170 }
171
172 pub fn root(&mut self, n: i32) -> Result<()> {
174 engine::unit_root(&mut self.raw, n)
175 }
176
177 pub fn to_number(&self) -> Result<f64> {
179 engine::unit_to_number(&self.raw)
180 }
181
182 pub fn convert_to(mut self, to: Unit) -> Result<f64> {
185 self.divide(to)?;
186 self.to_number()
187 }
188
189 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
215pub fn parse(input: &str) -> Result<Unit> {
217 Unit::parse(input)
218}
219
220pub 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
234pub 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
253pub 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#[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#[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 #[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 #[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 #[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 #[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}