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