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 CHANGED
@@ -0,0 +1,3 @@
1
+ # © 2025 The Radiativity Company
2
+ # Licensed under the Apache License, Version 2.0
3
+ # See the LICENSE file for details.
@@ -1,4 +1,6 @@
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 math
4
6
  from unittest import TestCase
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 CompositeUnit, ScaleDescriptor
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 = Unit('m', name='meter', dimension=Dimension.length)
337
+ meter = UnitFactor('m', name='meter', dimension=Dimension.length)
333
338
  kilometer = Scale.kilo * meter
334
- self.assertIsInstance(kilometer, Unit)
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 TestCompositeUnit(unittest.TestCase):
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
- u = Unit("m", name="meter", dimension=Dimension.length)
473
- cu = CompositeUnit({u: 1})
484
+ cu = UnitProduct({self.mf: 1})
474
485
  # should anneal to Unit
475
- self.assertIsInstance(cu, Unit)
476
- self.assertEqual(cu.shorthand, u.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 = CompositeUnit({m: 1, units.second: -1})
491
+ inner = UnitProduct({self.mf: 1, self.sf: -1})
482
492
  # Outer composite sees both `m:1` and `inner:1`
483
- cu = CompositeUnit({m: 1, inner: 1})
493
+ up = UnitProduct({self.mf: 1, inner: 1})
484
494
  # merge_unit should accumulate the exponents → m^(1 + 1) = m^2
485
- self.assertIn(m, cu.components)
486
- self.assertEqual(cu.components[m], 2)
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(accel.components[m], 1)
495
- self.assertEqual(accel.components[s], -2)
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
- m = Unit("m", dimension=Dimension.length)
499
- none = Unit("", dimension=Dimension.none)
500
- cu = CompositeUnit({m: 2, none: 1})
501
- self.assertIn(m, cu.components)
502
- self.assertNotIn(none, cu.components)
503
-
504
- def test_anneal_single_unit(self):
505
- m = Unit("m", dimension=Dimension.length)
506
- cu = CompositeUnit({m: 1})
507
- self.assertIsInstance(cu, Unit)
508
- self.assertEqual(cu.name, m.name)
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
- m = Unit("m", dimension=Dimension.length)
512
- s = Unit("s", dimension=Dimension.time)
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
- m = Unit("m", dimension=Dimension.length)
521
- none = Unit("", dimension=Dimension.none)
522
- cu = CompositeUnit({m: 2})
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
- m = Unit("m", dimension=Dimension.length)
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.components.values()), [-1])
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, Unit)
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, CompositeUnit)
606
- self.assertIsInstance(m * c, CompositeUnit)
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)
@@ -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 CompositeUnit, Dimension, Scale, Unit
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 = Unit(dimension=Dimension.mass, name='gram', scale=Scale.deca)
23
- kibigram = Unit(dimension=Dimension.mass, name='gram', scale=Scale.kibi)
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 = Unit(dimension=Dimension.mass, name='gram', scale=Scale.kilo)
36
- mg = Unit(dimension=Dimension.mass, name='gram', scale=Scale.milli)
37
- kibigram = Unit(dimension=Dimension.mass, name='gram', scale=Scale.kibi)
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, CompositeUnit)
85
- self.assertDictEqual(result.unit.components, {units.gram: 1})
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, CompositeUnit)
93
- self.assertDictEqual(mg_result.unit.components, {mg: 1})
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, CompositeUnit)
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 = Unit(dimension=Dimension.none, name='', scale=Scale.kilo)
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 = Unit(dimension=Dimension.none, name='', scale=Scale.kilo)
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 = Unit('m', name='meter', dimension=Dimension.length, scale=Scale.kilo)
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, CompositeUnit)
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, scale=Scale.kilo)
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, scale=Scale.milli)
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, scale=Scale.kilo),
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
  ]
ucon/algebra.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.algebra
3
7
  ============