ucon 0.3.4__py3-none-any.whl → 0.3.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tests/ucon/__init__.py +3 -0
- tests/ucon/test_algebra.py +3 -1
- tests/ucon/test_core.py +202 -48
- tests/ucon/test_quantity.py +26 -19
- tests/ucon/test_units.py +3 -1
- ucon/__init__.py +7 -1
- ucon/algebra.py +4 -0
- ucon/core.py +167 -155
- ucon/quantity.py +24 -77
- ucon/units.py +4 -0
- {ucon-0.3.4.dist-info → ucon-0.3.5.dist-info}/METADATA +45 -34
- ucon-0.3.5.dist-info/RECORD +16 -0
- {ucon-0.3.4.dist-info → ucon-0.3.5.dist-info}/WHEEL +1 -1
- ucon-0.3.5.dist-info/licenses/LICENSE +202 -0
- ucon-0.3.5.dist-info/licenses/NOTICE +28 -0
- ucon-0.3.4.dist-info/RECORD +0 -15
- ucon-0.3.4.dist-info/licenses/LICENSE +0 -21
- {ucon-0.3.4.dist-info → ucon-0.3.5.dist-info}/top_level.txt +0 -0
tests/ucon/__init__.py
CHANGED
tests/ucon/test_algebra.py
CHANGED
tests/ucon/test_core.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# © 2025 The Radiativity Company
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
# See the LICENSE file for details.
|
|
4
|
+
|
|
1
5
|
import math
|
|
2
6
|
import unittest
|
|
3
7
|
|
|
@@ -9,7 +13,7 @@ from ucon import Dimension
|
|
|
9
13
|
from ucon import Unit
|
|
10
14
|
from ucon import units
|
|
11
15
|
from ucon.algebra import Vector
|
|
12
|
-
from ucon.core import
|
|
16
|
+
from ucon.core import UnitFactor, UnitProduct, ScaleDescriptor
|
|
13
17
|
|
|
14
18
|
|
|
15
19
|
class TestDimension(unittest.TestCase):
|
|
@@ -328,10 +332,11 @@ class TestScaleMultiplicationAdditional(unittest.TestCase):
|
|
|
328
332
|
self.assertIsInstance(result, Scale)
|
|
329
333
|
self.assertEqual(result.value.base, 10)
|
|
330
334
|
|
|
335
|
+
@unittest.skip("TODO: revamp: Unit.scale is deprecated.")
|
|
331
336
|
def test_scale_multiplication_with_unit(self):
|
|
332
|
-
meter =
|
|
337
|
+
meter = UnitFactor('m', name='meter', dimension=Dimension.length)
|
|
333
338
|
kilometer = Scale.kilo * meter
|
|
334
|
-
self.assertIsInstance(kilometer,
|
|
339
|
+
self.assertIsInstance(kilometer, UnitFactor)
|
|
335
340
|
self.assertEqual(kilometer.scale, Scale.kilo)
|
|
336
341
|
self.assertEqual(kilometer.dimension, Dimension.length)
|
|
337
342
|
self.assertIn('meter', kilometer.name)
|
|
@@ -467,70 +472,63 @@ class TestUnit(unittest.TestCase):
|
|
|
467
472
|
self.assertFalse(Unit("m", dimension=Dimension.length) == "meter")
|
|
468
473
|
|
|
469
474
|
|
|
470
|
-
class
|
|
475
|
+
class TestUnitProduct(unittest.TestCase):
|
|
476
|
+
|
|
477
|
+
mf = UnitFactor(unit=units.meter, scale=Scale.one)
|
|
478
|
+
sf = UnitFactor(unit=units.second, scale=Scale.one)
|
|
479
|
+
nf = UnitFactor(unit=units.none, scale=Scale.one)
|
|
480
|
+
velocity = UnitProduct({mf: 1, sf: -1})
|
|
481
|
+
acceleration = UnitProduct({mf: 1, sf: -2})
|
|
482
|
+
|
|
471
483
|
def test_composite_unit_collapses_to_unit(self):
|
|
472
|
-
|
|
473
|
-
cu = CompositeUnit({u: 1})
|
|
484
|
+
cu = UnitProduct({self.mf: 1})
|
|
474
485
|
# should anneal to Unit
|
|
475
|
-
self.assertIsInstance(cu,
|
|
476
|
-
self.assertEqual(cu.shorthand,
|
|
486
|
+
self.assertIsInstance(cu, UnitProduct)
|
|
487
|
+
self.assertEqual(cu.shorthand, self.mf.shorthand)
|
|
477
488
|
|
|
478
489
|
def test_merge_of_identical_units(self):
|
|
479
|
-
m = Unit("m", name="meter", dimension=Dimension.length)
|
|
480
490
|
# Inner composite that already has m^1
|
|
481
|
-
inner =
|
|
491
|
+
inner = UnitProduct({self.mf: 1, self.sf: -1})
|
|
482
492
|
# Outer composite sees both `m:1` and `inner:1`
|
|
483
|
-
|
|
493
|
+
up = UnitProduct({self.mf: 1, inner: 1})
|
|
484
494
|
# merge_unit should accumulate the exponents → m^(1 + 1) = m^2
|
|
485
|
-
self.assertIn(
|
|
486
|
-
self.assertEqual(
|
|
495
|
+
self.assertIn(self.mf, up.factors)
|
|
496
|
+
self.assertEqual(up.factors[self.mf], 2)
|
|
487
497
|
|
|
488
498
|
def test_merge_of_nested_composite_units(self):
|
|
489
|
-
m = Unit("m", dimension=Dimension.length)
|
|
490
|
-
s = Unit("s", dimension=Dimension.time)
|
|
491
|
-
velocity = CompositeUnit({m: 1, s: -1})
|
|
492
|
-
accel = CompositeUnit({velocity: 1, s: -1})
|
|
493
499
|
# expect m*s^-2
|
|
494
|
-
self.assertEqual(
|
|
495
|
-
self.assertEqual(
|
|
500
|
+
self.assertEqual(self.acceleration.factors[self.mf], 1)
|
|
501
|
+
self.assertEqual(self.acceleration.factors[self.sf], -2)
|
|
496
502
|
|
|
497
503
|
def test_drop_dimensionless_component(self):
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
self.
|
|
508
|
-
self.assertEqual(
|
|
504
|
+
up = UnitProduct({self.mf: 2, self.nf: 1})
|
|
505
|
+
self.assertIn(self.mf, up.factors)
|
|
506
|
+
self.assertNotIn(self.nf, up.factors)
|
|
507
|
+
|
|
508
|
+
def test_unitproduct_can_behave_like_single_unit(self):
|
|
509
|
+
"""
|
|
510
|
+
A UnitProduct with only one factor should seem like that factor.
|
|
511
|
+
"""
|
|
512
|
+
up = UnitProduct({self.mf: 1})
|
|
513
|
+
self.assertEqual(up.shorthand, self.mf.shorthand)
|
|
514
|
+
self.assertEqual(up.dimension, self.mf.dimension)
|
|
509
515
|
|
|
510
516
|
def test_composite_mul_with_scale(self):
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
cu = CompositeUnit({m: 1, s: -1})
|
|
514
|
-
result = Scale.kilo * cu
|
|
517
|
+
up = UnitProduct({self.mf: 1, self.sf: -1})
|
|
518
|
+
result = Scale.kilo * up
|
|
515
519
|
# equivalent to scale multiplication on RMUL path
|
|
516
520
|
self.assertIsNotNone(result)
|
|
517
521
|
self.assertIsNotNone(result.shorthand, "km/s")
|
|
518
522
|
|
|
519
523
|
def test_composite_div_dimensionless(self):
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
out = cu / none
|
|
524
|
-
self.assertEqual(out.components[m], 2)
|
|
524
|
+
up = UnitProduct({self.mf: 2})
|
|
525
|
+
out = up / UnitProduct({})
|
|
526
|
+
self.assertEqual(out.factors[self.mf], 2)
|
|
525
527
|
|
|
526
528
|
def test_truediv_composite_by_composite(self):
|
|
527
|
-
|
|
528
|
-
s = Unit("s", dimension=Dimension.time)
|
|
529
|
-
velocity = CompositeUnit({m: 1, s: -1})
|
|
530
|
-
accel = CompositeUnit({m: 1, s: -2})
|
|
531
|
-
jerk = accel / velocity
|
|
529
|
+
jerk = self.acceleration / self.velocity
|
|
532
530
|
# jerk = m^1 s^-2 / m^1 s^-1 = s^-1
|
|
533
|
-
self.assertEqual(list(jerk.
|
|
531
|
+
self.assertEqual(list(jerk.factors.values()), [-1])
|
|
534
532
|
|
|
535
533
|
|
|
536
534
|
class TestUnitEdgeCases(unittest.TestCase):
|
|
@@ -574,7 +572,7 @@ class TestUnitEdgeCases(unittest.TestCase):
|
|
|
574
572
|
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
575
573
|
s = Unit('s', name='second', dimension=Dimension.time)
|
|
576
574
|
v = m / s
|
|
577
|
-
self.assertIsInstance(v,
|
|
575
|
+
self.assertIsInstance(v, UnitProduct)
|
|
578
576
|
self.assertEqual(v.dimension, Dimension.velocity)
|
|
579
577
|
self.assertIn('/', repr(v))
|
|
580
578
|
|
|
@@ -602,8 +600,8 @@ class TestUnitEdgeCases(unittest.TestCase):
|
|
|
602
600
|
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
603
601
|
c = Unit('C', name='coulomb', dimension=Dimension.charge)
|
|
604
602
|
# The result of combination gives CompositeUnit
|
|
605
|
-
self.assertIsInstance(m / c,
|
|
606
|
-
self.assertIsInstance(m * c,
|
|
603
|
+
self.assertIsInstance(m / c, UnitProduct)
|
|
604
|
+
self.assertIsInstance(m * c, UnitProduct)
|
|
607
605
|
|
|
608
606
|
# --- equality, hashing, immutability ----------------------------------
|
|
609
607
|
|
|
@@ -672,3 +670,159 @@ class TestScaleEdgeCases(unittest.TestCase):
|
|
|
672
670
|
all_map = Scale.all()
|
|
673
671
|
by_val = Scale.by_value()
|
|
674
672
|
self.assertTrue(all((val in by_val.values()) for _, val in all_map.items()))
|
|
673
|
+
|
|
674
|
+
def test_descriptor_property(self):
|
|
675
|
+
self.assertIsInstance(Scale.kilo.descriptor, ScaleDescriptor)
|
|
676
|
+
self.assertEqual(Scale.kilo.descriptor, Scale.kilo.value)
|
|
677
|
+
|
|
678
|
+
def test_alias_property(self):
|
|
679
|
+
self.assertEqual(Scale.kilo.alias, "kilo")
|
|
680
|
+
self.assertEqual(Scale.one.alias, "")
|
|
681
|
+
|
|
682
|
+
def test_scale_descriptor_parts(self):
|
|
683
|
+
self.assertEqual(Scale.kilo.value.parts(), (10, 3))
|
|
684
|
+
self.assertEqual(Scale.kibi.value.parts(), (2, 10))
|
|
685
|
+
|
|
686
|
+
def test_scale_hash_used_in_sets(self):
|
|
687
|
+
s = {Scale.kilo, Scale.milli}
|
|
688
|
+
self.assertIn(Scale.kilo, s)
|
|
689
|
+
self.assertNotIn(Scale.one, s)
|
|
690
|
+
|
|
691
|
+
def test_scale_mul_nonmatching_falls_to_nearest(self):
|
|
692
|
+
# kilo * kibi → no exact match, falls through to Scale.nearest
|
|
693
|
+
result = Scale.kilo * Scale.kibi
|
|
694
|
+
self.assertIsInstance(result, Scale)
|
|
695
|
+
|
|
696
|
+
def test_scale_pow(self):
|
|
697
|
+
result = Scale.kilo ** 2
|
|
698
|
+
self.assertEqual(result, Scale.mega)
|
|
699
|
+
|
|
700
|
+
def test_scale_pow_binary(self):
|
|
701
|
+
result = Scale.kibi ** 2
|
|
702
|
+
self.assertEqual(result, Scale.mebi)
|
|
703
|
+
|
|
704
|
+
def test_scale_pow_nonmatching_falls_to_nearest(self):
|
|
705
|
+
result = Scale.kilo ** 0.5
|
|
706
|
+
self.assertIsInstance(result, Scale)
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
class TestUnitAlgebra(unittest.TestCase):
|
|
710
|
+
|
|
711
|
+
def test_unit_mul_unitproduct(self):
|
|
712
|
+
m = units.meter
|
|
713
|
+
velocity = UnitProduct({m: 1, units.second: -1})
|
|
714
|
+
result = m * velocity
|
|
715
|
+
self.assertIsInstance(result, UnitProduct)
|
|
716
|
+
# m * (m/s) = m²/s
|
|
717
|
+
self.assertEqual(result.dimension, Dimension.area / Dimension.time)
|
|
718
|
+
|
|
719
|
+
def test_unit_mul_non_unit_returns_not_implemented(self):
|
|
720
|
+
result = units.meter.__mul__("not a unit")
|
|
721
|
+
self.assertIs(result, NotImplemented)
|
|
722
|
+
|
|
723
|
+
def test_unit_truediv_non_unit_returns_not_implemented(self):
|
|
724
|
+
result = units.meter.__truediv__("not a unit")
|
|
725
|
+
self.assertIs(result, NotImplemented)
|
|
726
|
+
|
|
727
|
+
def test_unit_pow(self):
|
|
728
|
+
m = units.meter
|
|
729
|
+
result = m ** 2
|
|
730
|
+
self.assertIsInstance(result, UnitProduct)
|
|
731
|
+
self.assertEqual(result.dimension, Dimension.area)
|
|
732
|
+
|
|
733
|
+
def test_unit_pow_3(self):
|
|
734
|
+
m = units.meter
|
|
735
|
+
result = m ** 3
|
|
736
|
+
self.assertEqual(result.dimension, Dimension.volume)
|
|
737
|
+
|
|
738
|
+
|
|
739
|
+
class TestUnitFactorCoverage(unittest.TestCase):
|
|
740
|
+
|
|
741
|
+
def test_shorthand_name_fallback(self):
|
|
742
|
+
# UnitFactor where unit has no aliases but has a name
|
|
743
|
+
u = Unit(name='gram', dimension=Dimension.mass)
|
|
744
|
+
fu = UnitFactor(unit=u, scale=Scale.milli)
|
|
745
|
+
self.assertEqual(fu.shorthand, 'mgram')
|
|
746
|
+
|
|
747
|
+
def test_repr(self):
|
|
748
|
+
fu = UnitFactor(unit=units.meter, scale=Scale.kilo)
|
|
749
|
+
self.assertIn('UnitFactor', repr(fu))
|
|
750
|
+
self.assertIn('kilo', repr(fu))
|
|
751
|
+
|
|
752
|
+
def test_eq_non_unit_returns_not_implemented(self):
|
|
753
|
+
fu = UnitFactor(unit=units.meter, scale=Scale.one)
|
|
754
|
+
self.assertIs(fu.__eq__("string"), NotImplemented)
|
|
755
|
+
|
|
756
|
+
|
|
757
|
+
class TestUnitProductAlgebra(unittest.TestCase):
|
|
758
|
+
|
|
759
|
+
def test_mul_unitproduct_by_unitproduct(self):
|
|
760
|
+
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
761
|
+
time_sq = UnitProduct({units.second: 2})
|
|
762
|
+
result = velocity * time_sq
|
|
763
|
+
self.assertIsInstance(result, UnitProduct)
|
|
764
|
+
# (m/s) * s² = m·s
|
|
765
|
+
self.assertEqual(result.dimension, Dimension.length * Dimension.time)
|
|
766
|
+
|
|
767
|
+
def test_mul_unitproduct_by_scale_returns_not_implemented(self):
|
|
768
|
+
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
769
|
+
result = velocity.__mul__(Scale.kilo)
|
|
770
|
+
self.assertIs(result, NotImplemented)
|
|
771
|
+
|
|
772
|
+
def test_mul_unitproduct_by_non_unit_returns_not_implemented(self):
|
|
773
|
+
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
774
|
+
result = velocity.__mul__("string")
|
|
775
|
+
self.assertIs(result, NotImplemented)
|
|
776
|
+
|
|
777
|
+
def test_truediv_unitproduct_by_unitproduct(self):
|
|
778
|
+
acceleration = UnitProduct({units.meter: 1, units.second: -2})
|
|
779
|
+
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
780
|
+
result = acceleration / velocity
|
|
781
|
+
self.assertIsInstance(result, UnitProduct)
|
|
782
|
+
# (m/s²) / (m/s) = 1/s
|
|
783
|
+
self.assertEqual(result.dimension, Dimension.frequency)
|
|
784
|
+
|
|
785
|
+
def test_rmul_unit_times_unitproduct(self):
|
|
786
|
+
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
787
|
+
result = units.meter * velocity
|
|
788
|
+
self.assertIsInstance(result, UnitProduct)
|
|
789
|
+
|
|
790
|
+
def test_rmul_scale_on_empty_unitproduct(self):
|
|
791
|
+
empty = UnitProduct({})
|
|
792
|
+
result = Scale.kilo * empty
|
|
793
|
+
self.assertIs(result, empty)
|
|
794
|
+
|
|
795
|
+
def test_rmul_scale_applies_to_sink_unit(self):
|
|
796
|
+
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
797
|
+
result = Scale.kilo * velocity
|
|
798
|
+
self.assertIsInstance(result, UnitProduct)
|
|
799
|
+
self.assertIn('km', result.shorthand)
|
|
800
|
+
|
|
801
|
+
def test_rmul_scale_combines_with_existing_scale(self):
|
|
802
|
+
km_per_s = Scale.kilo * UnitProduct({units.meter: 1, units.second: -1})
|
|
803
|
+
# Apply another scale on top → should combine scales
|
|
804
|
+
result = Scale.milli * km_per_s
|
|
805
|
+
self.assertIsInstance(result, UnitProduct)
|
|
806
|
+
|
|
807
|
+
def test_rmul_non_unit_returns_not_implemented(self):
|
|
808
|
+
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
809
|
+
result = velocity.__rmul__("string")
|
|
810
|
+
self.assertIs(result, NotImplemented)
|
|
811
|
+
|
|
812
|
+
def test_append_dimensionless_skipped(self):
|
|
813
|
+
# UnitProduct with only dimensionless factor → empty shorthand
|
|
814
|
+
up = UnitProduct({})
|
|
815
|
+
self.assertEqual(up.shorthand, "")
|
|
816
|
+
|
|
817
|
+
def test_shorthand_with_negative_non_unit_exponent(self):
|
|
818
|
+
# e.g. m/s² should show superscript on denominator
|
|
819
|
+
accel = UnitProduct({units.meter: 1, units.second: -2})
|
|
820
|
+
sh = accel.shorthand
|
|
821
|
+
self.assertIn('m', sh)
|
|
822
|
+
self.assertIn('s', sh)
|
|
823
|
+
|
|
824
|
+
def test_shorthand_numerator_exponent(self):
|
|
825
|
+
area = UnitProduct({units.meter: 2})
|
|
826
|
+
self.assertIn('m', area.shorthand)
|
|
827
|
+
# Should contain superscript 2
|
|
828
|
+
self.assertIn('²', area.shorthand)
|
tests/ucon/test_quantity.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
# © 2025 The Radiativity Company
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
# See the LICENSE file for details.
|
|
2
4
|
|
|
3
5
|
import unittest
|
|
4
6
|
|
|
5
7
|
from ucon import units
|
|
6
|
-
from ucon.core import
|
|
8
|
+
from ucon.core import UnitProduct, UnitFactor, Dimension, Scale, Unit
|
|
7
9
|
from ucon.quantity import Number, Ratio
|
|
8
10
|
|
|
9
11
|
|
|
@@ -19,8 +21,8 @@ class TestNumber(unittest.TestCase):
|
|
|
19
21
|
|
|
20
22
|
@unittest.skip("Requires ConversionGraph implementation")
|
|
21
23
|
def test_simplify(self):
|
|
22
|
-
decagram =
|
|
23
|
-
kibigram =
|
|
24
|
+
decagram = UnitFactor(dimension=Dimension.mass, name='gram', scale=Scale.deca)
|
|
25
|
+
kibigram = UnitFactor(dimension=Dimension.mass, name='gram', scale=Scale.kibi)
|
|
24
26
|
|
|
25
27
|
ten_decagrams = Number(unit=decagram, quantity=10)
|
|
26
28
|
point_one_decagrams = Number(unit=decagram, quantity=0.1)
|
|
@@ -32,9 +34,9 @@ class TestNumber(unittest.TestCase):
|
|
|
32
34
|
|
|
33
35
|
@unittest.skip("Requires ConversionGraph implementation")
|
|
34
36
|
def test_to(self):
|
|
35
|
-
kg =
|
|
36
|
-
mg =
|
|
37
|
-
kibigram =
|
|
37
|
+
kg = UnitFactor(dimension=Dimension.mass, name='gram', scale=Scale.kilo)
|
|
38
|
+
mg = UnitFactor(dimension=Dimension.mass, name='gram', scale=Scale.milli)
|
|
39
|
+
kibigram = UnitFactor(dimension=Dimension.mass, name='gram', scale=Scale.kibi)
|
|
38
40
|
|
|
39
41
|
thousandth_of_a_kilogram = Number(unit=kg, quantity=0.001)
|
|
40
42
|
thousand_milligrams = Number(unit=mg, quantity=1000)
|
|
@@ -44,6 +46,7 @@ class TestNumber(unittest.TestCase):
|
|
|
44
46
|
self.assertEqual(thousand_milligrams, self.number.to(Scale.milli))
|
|
45
47
|
self.assertEqual(kibigram_fraction, self.number.to(Scale.kibi))
|
|
46
48
|
|
|
49
|
+
@unittest.skip("TODO: revamp: Unit.scale is deprecated.")
|
|
47
50
|
def test___repr__(self):
|
|
48
51
|
self.assertIn(str(self.number.quantity), str(self.number))
|
|
49
52
|
self.assertIn(str(self.number.unit.scale.value.evaluated), str(self.number))
|
|
@@ -81,16 +84,17 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
81
84
|
two_mL = Number(unit=mL, quantity=2)
|
|
82
85
|
|
|
83
86
|
result = density.evaluate() * two_mL
|
|
84
|
-
self.assertIsInstance(result.unit,
|
|
85
|
-
self.assertDictEqual(result.unit.
|
|
87
|
+
self.assertIsInstance(result.unit, UnitProduct)
|
|
88
|
+
self.assertDictEqual(result.unit.factors, {units.gram: 1})
|
|
86
89
|
self.assertAlmostEqual(result.quantity, 6.238, places=12)
|
|
87
90
|
|
|
88
91
|
mg = Scale.milli * units.gram
|
|
92
|
+
mg_factor = UnitFactor(unit=units.gram, scale=Scale.milli)
|
|
89
93
|
mg_density = Ratio(Number(unit=mg, quantity=3119), Number(unit=mL, quantity=1))
|
|
90
94
|
|
|
91
95
|
mg_result = mg_density.evaluate() * two_mL
|
|
92
|
-
self.assertIsInstance(mg_result.unit,
|
|
93
|
-
self.assertDictEqual(mg_result.unit.
|
|
96
|
+
self.assertIsInstance(mg_result.unit, UnitProduct)
|
|
97
|
+
self.assertDictEqual(mg_result.unit.factors, {mg_factor: 1})
|
|
94
98
|
self.assertAlmostEqual(mg_result.quantity, 6238, places=12)
|
|
95
99
|
|
|
96
100
|
def test_number_mul_asymmetric_density_volume(self):
|
|
@@ -125,7 +129,7 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
125
129
|
result = Number(unit=km, quantity=1) * Number(unit=m, quantity=1)
|
|
126
130
|
|
|
127
131
|
# Should remain composite rather than collapsing to base m^2
|
|
128
|
-
assert isinstance(result.unit,
|
|
132
|
+
assert isinstance(result.unit, UnitProduct)
|
|
129
133
|
assert "km" in result.unit.shorthand
|
|
130
134
|
assert "m" in result.unit.shorthand
|
|
131
135
|
|
|
@@ -150,6 +154,7 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
150
154
|
assert evaluated.unit == units.gram
|
|
151
155
|
assert abs(evaluated.quantity - 6.238) < 1e-12
|
|
152
156
|
|
|
157
|
+
@unittest.skip("TODO: revamp: Unit.scale is deprecated.")
|
|
153
158
|
def test_default_number_is_dimensionless_one(self):
|
|
154
159
|
n = Number()
|
|
155
160
|
self.assertEqual(n.unit, units.none)
|
|
@@ -160,15 +165,16 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
160
165
|
|
|
161
166
|
@unittest.skip("Requires ConversionGraph implementation")
|
|
162
167
|
def test_to_new_scale_changes_value(self):
|
|
163
|
-
thousand =
|
|
168
|
+
thousand = UnitFactor(dimension=Dimension.none, name='', scale=Scale.kilo)
|
|
164
169
|
n = Number(quantity=1000, unit=thousand)
|
|
165
170
|
converted = n.to(Scale.one)
|
|
166
171
|
self.assertNotEqual(n.value, converted.value)
|
|
167
172
|
self.assertAlmostEqual(converted.value, 1000)
|
|
168
173
|
|
|
174
|
+
@unittest.skip("TODO: revamp: Unit.scale is deprecated.")
|
|
169
175
|
@unittest.skip("Requires ConversionGraph implementation")
|
|
170
176
|
def test_simplify_uses_value_as_quantity(self):
|
|
171
|
-
thousand =
|
|
177
|
+
thousand = UnitFactor(dimension=Dimension.none, name='', scale=Scale.kilo)
|
|
172
178
|
n = Number(quantity=2, unit=thousand)
|
|
173
179
|
simplified = n.simplify()
|
|
174
180
|
self.assertEqual(simplified.quantity, n.value)
|
|
@@ -182,16 +188,17 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
182
188
|
self.assertEqual(result.quantity, 6)
|
|
183
189
|
self.assertEqual(result.unit.dimension, Dimension.energy * Dimension.time)
|
|
184
190
|
|
|
191
|
+
@unittest.skip("TODO: revamp: Unit.scale is deprecated.")
|
|
185
192
|
@unittest.skip("Requires ConversionGraph implementation")
|
|
186
193
|
def test_division_combines_units_scales_and_quantities(self):
|
|
187
|
-
km =
|
|
194
|
+
km = UnitFactor('m', name='meter', dimension=Dimension.length, scale=Scale.kilo)
|
|
188
195
|
n1 = Number(unit=km, quantity=1000)
|
|
189
196
|
n2 = Number(unit=units.second, quantity=2)
|
|
190
197
|
|
|
191
198
|
result = n1 / n2 # should yield <500 km/s>
|
|
192
199
|
|
|
193
200
|
cu = result.unit
|
|
194
|
-
self.assertIsInstance(cu,
|
|
201
|
+
self.assertIsInstance(cu, UnitProduct)
|
|
195
202
|
|
|
196
203
|
# --- quantity check ---
|
|
197
204
|
self.assertAlmostEqual(result.quantity, 500)
|
|
@@ -225,7 +232,7 @@ class TestNumberEdgeCases(unittest.TestCase):
|
|
|
225
232
|
self.assertTrue(r == Number())
|
|
226
233
|
|
|
227
234
|
def test_repr_includes_scale_and_unit(self):
|
|
228
|
-
kV = Unit('V', name='volt', dimension=Dimension.voltage
|
|
235
|
+
kV = Scale.kilo * Unit('V', name='volt', dimension=Dimension.voltage)
|
|
229
236
|
n = Number(unit=kV, quantity=5)
|
|
230
237
|
rep = repr(n)
|
|
231
238
|
self.assertIn("kV", rep)
|
|
@@ -262,7 +269,7 @@ class TestRatio(unittest.TestCase):
|
|
|
262
269
|
self.assertEqual(self.one_half * self.three_halves, self.three_fourths)
|
|
263
270
|
|
|
264
271
|
def test___mul__(self):
|
|
265
|
-
mL = Unit('L', name='liter', dimension=Dimension.volume
|
|
272
|
+
mL = Scale.milli * Unit('L', name='liter', dimension=Dimension.volume)
|
|
266
273
|
n1 = Number(unit=units.gram, quantity=3.119)
|
|
267
274
|
n2 = Number(unit=mL)
|
|
268
275
|
bromine_density = Ratio(n1, n2)
|
|
@@ -282,7 +289,7 @@ class TestRatio(unittest.TestCase):
|
|
|
282
289
|
|
|
283
290
|
# How many Wh from 20 kJ?
|
|
284
291
|
twenty_kilojoules = Number(
|
|
285
|
-
unit=Unit('J', name='joule', dimension=Dimension.energy
|
|
292
|
+
unit=Scale.kilo * Unit('J', name='joule', dimension=Dimension.energy),
|
|
286
293
|
quantity=20
|
|
287
294
|
)
|
|
288
295
|
ratio = twenty_kilojoules.as_ratio() / seconds_per_hour
|
tests/ucon/test_units.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
|
|
2
|
+
# © 2025 The Radiativity Company
|
|
3
|
+
# Licensed under the Apache License, Version 2.0
|
|
4
|
+
# See the LICENSE file for details.
|
|
2
5
|
|
|
3
6
|
from unittest import TestCase
|
|
4
7
|
|
|
5
8
|
from ucon import units
|
|
6
9
|
from ucon.core import Dimension
|
|
7
|
-
from ucon.core import Unit
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
class TestUnits(TestCase):
|
ucon/__init__.py
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
# © 2025 The Radiativity Company
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
# See the LICENSE file for details.
|
|
4
|
+
|
|
1
5
|
"""
|
|
2
6
|
ucon
|
|
3
7
|
====
|
|
@@ -34,7 +38,7 @@ Design Philosophy
|
|
|
34
38
|
"""
|
|
35
39
|
from ucon import units
|
|
36
40
|
from ucon.algebra import Exponent
|
|
37
|
-
from ucon.core import Dimension, Scale, Unit
|
|
41
|
+
from ucon.core import Dimension, Scale, Unit, UnitFactor, UnitProduct
|
|
38
42
|
from ucon.quantity import Number, Ratio
|
|
39
43
|
|
|
40
44
|
|
|
@@ -45,5 +49,7 @@ __all__ = [
|
|
|
45
49
|
'Ratio',
|
|
46
50
|
'Scale',
|
|
47
51
|
'Unit',
|
|
52
|
+
'UnitFactor',
|
|
53
|
+
'UnitProduct',
|
|
48
54
|
'units',
|
|
49
55
|
]
|