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 let from_unit = Unit::parse(from).or_else(|e| {
232 if e.code() != ErrorCode::Parse && e.code() != ErrorCode::UnknownUnit {
233 return Err(e);
234 }
235 from.rsplit_once(' ')
236 .ok_or(e)
237 .and_then(|(arg, func)| Unit::parse(&format!("{func}({arg})")))
238 })?;
239 from_unit.convert_to(Unit::parse(to)?)
240}
241
242pub fn conformable(expr: &str) -> Result<Vec<String>> {
244 let target = Unit::parse(expr)?;
245 let defs = list_definitions();
246 let names = defs
247 .iter()
248 .filter(|d| d.kind == DefinitionKind::Unit)
249 .filter_map(|d| {
250 let parsed = Unit::parse(&d.name).ok()?;
251 if parsed.is_conformable(&target) {
252 Some(d.name.clone())
253 } else {
254 None
255 }
256 })
257 .collect();
258 Ok(names)
259}
260
261pub fn list_definitions() -> Vec<Definition> {
264 ensure_definitions();
265 DEFINITIONS
266 .read()
267 .unwrap_or_else(|e| e.into_inner())
268 .clone()
269}
270
271#[cfg(feature = "currency-update")]
273pub fn reload_currency(content: &str) {
274 ensure_definitions();
275 let new_defs = load_definitions(content, c"currency.units");
276 let mut defs = DEFINITIONS.write().unwrap_or_else(|e| e.into_inner());
277 defs.retain(|d| d.kind != DefinitionKind::Unit || !new_defs.iter().any(|n| n.name == d.name));
278 defs.extend(new_defs);
279 defs.sort_by(|a, b| a.name.cmp(&b.name));
280}
281
282#[cfg(test)]
285impl fmt::Debug for Unit {
286 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287 write!(f, "Unit {{ factor: {} }}", self.factor())
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use rstest::rstest;
294
295 use super::*;
296 use crate::definitions::{ensure_definitions, replace_operators};
297
298 #[test]
299 fn units_error_display() {
300 let err = UnitsError(ErrorCode::BadNum);
301
302 let s = format!("{err}");
303
304 assert!(s.contains("20"));
305 }
306
307 #[rstest]
308 #[case::eq_same_code(ErrorCode::Parse, ErrorCode::Parse, true)]
309 #[case::ne_different_code(ErrorCode::Parse, ErrorCode::BadSum, false)]
310 fn units_error_eq_semantics(#[case] a: ErrorCode, #[case] b: ErrorCode, #[case] equal: bool) {
311 let ea = UnitsError(a);
312 let eb = UnitsError(b);
313
314 assert_eq!(ea == eb, equal);
315 }
316
317 #[test]
318 fn units_error_copy_semantics() {
319 let original = UnitsError(ErrorCode::BadSum);
320
321 let copy = original;
322
323 assert_eq!(copy, original);
324 }
325
326 #[rstest]
327 #[case::notanumber(ErrorCode::NotANumber, true)]
328 #[case::e_parse(ErrorCode::Parse, false)]
329 #[case::e_badsum(ErrorCode::BadSum, false)]
330 fn is_not_dimensionless(#[case] code: ErrorCode, #[case] expected: bool) {
331 let err = UnitsError(code);
332
333 assert_eq!(err.is_not_dimensionless(), expected);
334 }
335
336 #[rstest]
337 #[case::e_parse(ErrorCode::Parse, true)]
338 #[case::e_unknownunit(ErrorCode::UnknownUnit, true)]
339 #[case::e_notanumber(ErrorCode::NotANumber, false)]
340 fn is_invalid_unit(#[case] code: ErrorCode, #[case] expected: bool) {
341 let err = UnitsError(code);
342
343 assert_eq!(err.is_invalid_unit(), expected);
344 }
345
346 #[rstest]
347 #[case::via_new(Unit::new())]
348 #[case::via_default(Unit::default())]
349 fn initial_factor_is_one(#[case] unit: Unit) {
350 assert_eq!(unit.factor(), 1.0);
351 }
352
353 #[test]
354 fn error_on_unary_plus() {
355 let result = Unit::parse("+5");
356
357 assert!(result.is_err());
358 }
359
360 #[rstest]
361 #[case::hex_literal("0xff", 255.0)]
362 #[case::negative_exponent_dimensionless("2^-1", 0.5)]
363 fn parse_numeric(#[case] input: &str, #[case] expected: f64) {
364 let result = Unit::parse(input);
365
366 assert!(result.is_ok());
367 assert!((result.unwrap().factor() - expected).abs() < 1e-6);
368 }
369
370 #[test]
371 fn parse_hex_without_valid_digits_is_zero() {
372 let result = Unit::parse("0xGG");
373
374 assert!(result.is_ok());
375 assert_eq!(result.unwrap().factor(), 0.0);
376 }
377
378 #[rstest]
381 #[case::error_on_trailing_paren("2 )")]
382 fn parse_error_code_is_e_parse(#[case] input: &str) {
383 let result = Unit::parse(input);
384
385 let err = result.unwrap_err();
386 assert_eq!(err.code(), ErrorCode::Parse);
387 }
388
389 #[test]
390 fn parse_m_inverse_has_inverse_dimension() {
391 let unit = Unit::parse("m^-1").unwrap();
392
393 let base = unit.base_units();
394
395 assert_eq!(unit.factor(), 1.0);
396 assert!(
397 base.contains("/ m"),
398 "expected '/ m' in base_units, got: {base}"
399 );
400 }
401
402 #[test]
403 fn clone_preserves_factor() {
404 let original = Unit::parse("5").unwrap();
405
406 let cloned = original.clone();
407
408 assert_eq!(cloned.factor(), original.factor());
409 }
410
411 #[test]
412 fn to_number_returns_factor() {
413 let unit = Unit::parse("42").unwrap();
414
415 let result = unit.to_number();
416
417 assert!(result.is_ok());
418 assert_eq!(result.unwrap(), 42.0);
419 }
420
421 #[test]
422 fn all_definitions_have_non_empty_names() {
423 let defs = list_definitions();
424
425 for d in defs.iter() {
426 assert!(!d.name.is_empty(), "found a definition with an empty name");
427 }
428 }
429
430 #[rstest]
431 #[case::kilo_prefix("kilo-")]
432 #[case::hms("hms")]
433 fn list_definitions_contains_known_entry(#[case] name: &str) {
434 let defs = list_definitions();
435
436 assert!(
437 defs.iter().any(|d| d.name == name),
438 "expected '{name}' in definitions"
439 );
440 }
441
442 #[rstest]
443 #[case::tempc("tempC", DefinitionKind::Function)]
444 #[case::gasmark("gasmark", DefinitionKind::Table)]
445 fn list_definitions_contains_known_kind_entry(
446 #[case] canonical: &str,
447 #[case] kind: DefinitionKind,
448 ) {
449 let defs = list_definitions();
450
451 assert!(
452 defs.iter()
453 .any(|d| d.canonical_name() == canonical && d.kind == kind),
454 "expected '{canonical}' with kind {kind:?} in definitions"
455 );
456 }
457
458 #[test]
459 fn definition_kind_name_invariant() {
460 let defs = list_definitions();
461
462 for d in defs.iter() {
463 match d.kind {
464 DefinitionKind::Prefix => {
465 assert!(
466 d.name.ends_with('-'),
467 "prefix '{}' must end with '-'",
468 d.name
469 );
470 }
471 DefinitionKind::Table => {
472 assert!(d.name.contains('['), "table '{}' must contain '['", d.name);
473 }
474 DefinitionKind::Function => {
475 assert!(
476 d.name.contains('('),
477 "function '{}' must contain '('",
478 d.name
479 );
480 }
481 _ => {}
482 }
483 }
484 }
485
486 #[rstest]
487 #[case::simple_meter("m", "m")]
488 #[case::compound("kg m/s^2", " / ")]
489 fn base_units_contains_expected(#[case] expr: &str, #[case] contains: &str) {
490 let unit = Unit::parse(expr).unwrap();
491
492 let base = unit.base_units();
493
494 assert!(
495 base.contains(contains),
496 "base_units('{expr}') = '{base}', expected it to contain '{contains}'"
497 );
498 }
499
500 #[test]
501 fn base_units_dimensionless_is_empty() {
502 let unit = Unit::parse("42").unwrap();
503
504 let base = unit.base_units();
505
506 assert_eq!(base, "");
507 }
508
509 #[test]
510 fn conformable_does_not_contain_wrong_domain() {
511 let result = conformable("km").unwrap();
512
513 assert!(!result.contains(&"kg".to_owned()));
514 assert!(!result.contains(&"s".to_owned()));
515 }
516
517 #[rstest]
518 #[case::en_dash('\u{2013}', "-")]
519 #[case::minus_sign('\u{2212}', "-")]
520 #[case::times_sign('\u{00D7}', "*")]
521 #[case::middle_dot('\u{00B7}', "*")]
522 #[case::division_sign('\u{00F7}', "/")]
523 #[case::fraction_slash('\u{2044}', "|")]
524 #[case::nbsp('\u{00A0}', " ")]
525 #[case::zero_width_space('\u{200B}', "")]
526 fn replace_operators_cases(#[case] input: char, #[case] expected: &str) {
527 let result = replace_operators(&input.to_string());
528
529 assert_eq!(result, expected);
530 }
531
532 #[test]
533 fn root_even_negative_is_error() {
534 let mut unit = Unit::parse("-4").unwrap();
535
536 let result = unit.root(2);
537
538 assert!(result.is_err(), "even root of negative should fail");
539 }
540
541 #[test]
542 fn root_odd_negative_is_error() {
543 let mut unit = Unit::parse("-8").unwrap();
544
545 let result = unit.root(3);
546
547 assert!(
548 result.is_err(),
549 "odd root of negative should fail (matching C behavior)"
550 );
551 }
552
553 #[rstest]
554 #[case::gasmark1("gasmark(1)", "degR", 734.67, 0.1)]
555 #[case::gasmark5("gasmark(5)", "degR", 834.67, 0.1)]
556 #[case::gasmark10("gasmark(10)", "degR", 959.67, 0.1)]
557 fn table_parse_factor(
558 #[case] input: &str,
559 #[case] to: &str,
560 #[case] expected_factor_in_degr: f64,
561 #[case] tol: f64,
562 ) {
563 let result = convert(input, to);
564
565 assert!(
566 result.is_ok(),
567 "convert({input:?}, {to:?}) failed: {:?}",
568 result.err()
569 );
570 let factor = result.unwrap();
571 assert!(
572 (factor - expected_factor_in_degr).abs() < tol,
573 "convert({input:?}, {to:?}) = {factor}, expected {expected_factor_in_degr}\u{00b1}{tol}"
574 );
575 }
576
577 #[rstest]
578 #[case::tempc_inverse("~tempC(0 K)", -273.15, 0.01)]
579 fn inverse_function_parses(#[case] input: &str, #[case] expected: f64, #[case] tol: f64) {
580 let unit = Unit::parse(input);
581
582 assert!(unit.is_ok(), "parse({input:?}) failed: {:?}", unit.err());
583 let factor = unit.unwrap().factor();
584 assert!(
585 (factor - expected).abs() < tol,
586 "parse({input:?}).factor() = {factor}, expected {expected}\u{00b1}{tol}"
587 );
588 }
589
590 #[rstest]
591 #[case::tempc_0("tempC(0)", 273.15, 0.01)]
592 #[case::tempc_100("tempC(100)", 373.15, 0.01)]
593 #[case::tempf_32("tempF(32)", 273.15, 0.01)]
594 #[case::tempf_212("tempF(212)", 373.15, 0.01)]
595 fn forward_function_to_kelvin(
596 #[case] input: &str,
597 #[case] expected_kelvin: f64,
598 #[case] tol: f64,
599 ) {
600 let result = convert(input, "K");
601
602 assert!(
603 result.is_ok(),
604 "convert({input:?}, \"K\") failed: {:?}",
605 result.err()
606 );
607 let factor = result.unwrap();
608 assert!(
609 (factor - expected_kelvin).abs() < tol,
610 "convert({input:?}, \"K\") = {factor}, expected {expected_kelvin}\u{00b1}{tol}"
611 );
612 }
613
614 #[rstest]
615 #[case::kilogram("kilogram", "gram", 1000.0, 1e-9)]
616 #[case::milligram("milligram", "gram", 0.001, 1e-9)]
617 #[case::megabyte("megabyte", "byte", 1e6, 1e-3)]
618 #[case::microsecond("microsecond", "second", 1e-6, 1e-15)]
619 fn prefix_resolution(
620 #[case] from: &str,
621 #[case] to: &str,
622 #[case] expected: f64,
623 #[case] tol: f64,
624 ) {
625 let result = convert(from, to);
626
627 assert!(
628 result.is_ok(),
629 "convert({from:?}, {to:?}) failed: {:?}",
630 result.err()
631 );
632 let factor = result.unwrap();
633 assert!(
634 (factor - expected).abs() < tol,
635 "convert({from:?}, {to:?}) = {factor}, expected {expected}\u{00b1}{tol}"
636 );
637 }
638
639 #[rstest]
640 #[case::reciprocal("1|2", 0.5)]
641 #[case::fraction_5_9("5|9", 5.0 / 9.0)]
642 #[case::numdiv_3_8("3|8", 0.375)]
643 fn pipe_factor(#[case] input: &str, #[case] expected: f64) {
644 let unit = Unit::parse(input).unwrap();
645
646 assert!(
647 (unit.factor() - expected).abs() < 1e-12,
648 "{input} factor = {}, expected {expected}",
649 unit.factor()
650 );
651 }
652
653 #[test]
654 fn list_definitions_returns_independent_copy() {
655 let mut first = list_definitions();
656 let original_len = first.len();
657 first.clear();
658
659 let second = list_definitions();
660
661 assert_eq!(second.len(), original_len);
662 }
663
664 #[test]
669 fn rational_exponent_one_third() {
670 ensure_definitions();
671
672 let result = Unit::parse("m^(1|3)");
673
674 assert!(
675 result.is_err(),
676 "m^(1|3) should fail (integer-only dimension exponents)"
677 );
678 }
679
680 #[rstest]
681 #[case::fractional_exponent("8^(1|3)", 2.0)]
682 #[case::asin("asin(1)", std::f64::consts::FRAC_PI_2)]
683 #[case::cuberoot("cuberoot(27)", 3.0)]
684 #[case::round("round(3.7)", 4.0)]
685 fn builtin_factor(#[case] input: &str, #[case] expected: f64) {
686 ensure_definitions();
687
688 let v = Unit::parse(input).unwrap();
689
690 assert!(
691 (v.factor() - expected).abs() < 1e-9,
692 "{input} factor = {}, expected {expected}",
693 v.factor()
694 );
695 }
696
697 #[test]
700 fn pipe_numdiv_with_unit() {
701 ensure_definitions();
702
703 let v = Unit::parse("3|8 m").unwrap();
704
705 assert!(
706 (v.factor() - 0.375).abs() < 1e-12,
707 "3|8 m factor = {}, expected 0.375",
708 v.factor()
709 );
710 assert!(
711 v.base_units().contains('m'),
712 "3|8 m base_units = '{}', expected it to contain 'm'",
713 v.base_units()
714 );
715 }
716
717 #[test]
719 fn per_keyword_as_division() {
720 ensure_definitions();
721
722 let result = convert("per meter", "1/m");
723
724 assert!(
725 result.is_ok(),
726 "convert(\"per meter\", \"1/m\") failed: {:?}",
727 result.err()
728 );
729 assert!(
730 (result.unwrap() - 1.0).abs() < 1e-9,
731 "convert(\"per meter\", \"1/m\") != 1.0"
732 );
733 }
734
735 #[test]
736 fn add_incompatible_dimensions_returns_badsum() {
737 let result = convert("m + kg", "m");
738
739 assert!(result.is_err());
740 assert_eq!(result.unwrap_err().code(), ErrorCode::BadSum);
741 }
742
743 #[cfg(feature = "native")]
744 #[test]
745 fn non_divisible_root_returns_not_root() {
746 ensure_definitions();
747
748 let result = Unit::parse("m^(1|3)");
749
750 assert!(result.is_err());
751 assert_eq!(result.unwrap_err().code(), ErrorCode::NotRoot);
752 }
753
754 #[cfg(feature = "native")]
755 #[test]
756 fn table_dimension_mismatch_returns_bad_func_arg() {
757 ensure_definitions();
758
759 let result = convert("5 m", "gasmark");
760
761 assert!(result.is_err());
762 assert_eq!(result.unwrap_err().code(), ErrorCode::BadFuncArg);
763 }
764
765 #[rstest]
766 #[case::error_on_ln_of_zero("ln(0)")]
767 #[case::error_on_ln_of_negative("ln(-1)")]
768 #[case::error_on_log_of_zero("log(0)")]
769 #[case::error_on_log_of_negative("log(-1)")]
770 #[case::error_on_log2_of_zero("log2(0)")]
771 fn error_on_builtin_domain(#[case] expr: &str) {
772 let result = convert(expr, "1");
773
774 assert!(result.is_err(), "convert({expr:?}, \"1\") should fail");
775 }
776
777 #[rstest]
778 #[case::error_on_to_number_dimensional("m", ErrorCode::NotANumber)]
779 #[case::error_on_to_number_compound("kg m/s^2", ErrorCode::NotANumber)]
780 fn error_on_to_number(#[case] input: &str, #[case] expected_code: ErrorCode) {
781 let unit = Unit::parse(input).unwrap();
782
783 let result = unit.to_number();
784
785 assert!(result.is_err());
786 assert_eq!(result.unwrap_err().code(), expected_code);
787 }
788
789 #[test]
790 fn error_on_add_incompatible_dimensions() {
791 let mut a = Unit::parse("m").unwrap();
792 let b = Unit::parse("kg").unwrap();
793
794 let result = a.add(b);
795
796 assert!(result.is_err());
797 assert_eq!(result.unwrap_err().code(), ErrorCode::BadSum);
798 }
799
800 #[test]
801 fn error_on_convert_to_incompatible_units() {
802 let m = Unit::parse("m").unwrap();
803 let kg = Unit::parse("kg").unwrap();
804
805 let result = m.convert_to(kg);
806
807 assert!(result.is_err());
808 }
809
810 #[rstest]
811 #[case::division_by_zero("1/0", f64::INFINITY)]
812 #[case::deeply_nested("(((((((((( 1 ))))))))))", 1.0)]
813 fn parse_does_not_panic(#[case] input: &str, #[case] expected: f64) {
814 let result = Unit::parse(input);
815
816 assert!(result.is_ok(), "parse({input:?}) should not panic or fail");
817 assert_eq!(result.unwrap().factor(), expected);
818 }
819}