ucon 0.3.3rc2__py3-none-any.whl → 0.3.4__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/test_algebra.py +237 -0
- tests/ucon/test_core.py +455 -364
- tests/ucon/test_quantity.py +363 -0
- tests/ucon/test_units.py +5 -3
- ucon/__init__.py +3 -3
- ucon/algebra.py +212 -0
- ucon/core.py +691 -286
- ucon/quantity.py +249 -0
- ucon/units.py +1 -2
- {ucon-0.3.3rc2.dist-info → ucon-0.3.4.dist-info}/METADATA +6 -5
- ucon-0.3.4.dist-info/RECORD +15 -0
- tests/ucon/test_dimension.py +0 -206
- tests/ucon/test_unit.py +0 -143
- ucon/dimension.py +0 -172
- ucon/unit.py +0 -92
- ucon-0.3.3rc2.dist-info/RECORD +0 -15
- {ucon-0.3.3rc2.dist-info → ucon-0.3.4.dist-info}/WHEEL +0 -0
- {ucon-0.3.3rc2.dist-info → ucon-0.3.4.dist-info}/licenses/LICENSE +0 -0
- {ucon-0.3.3rc2.dist-info → ucon-0.3.4.dist-info}/top_level.txt +0 -0
tests/ucon/test_core.py
CHANGED
|
@@ -1,101 +1,264 @@
|
|
|
1
1
|
import math
|
|
2
|
-
|
|
2
|
+
import unittest
|
|
3
3
|
|
|
4
4
|
from ucon import Number
|
|
5
5
|
from ucon import Exponent
|
|
6
6
|
from ucon import Ratio
|
|
7
7
|
from ucon import Scale
|
|
8
8
|
from ucon import Dimension
|
|
9
|
+
from ucon import Unit
|
|
9
10
|
from ucon import units
|
|
10
|
-
from ucon.
|
|
11
|
+
from ucon.algebra import Vector
|
|
12
|
+
from ucon.core import CompositeUnit, ScaleDescriptor
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
class
|
|
15
|
+
class TestDimension(unittest.TestCase):
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
def test_basic_dimensions_are_unique(self):
|
|
18
|
+
seen = set()
|
|
19
|
+
for dim in Dimension:
|
|
20
|
+
self.assertNotIn(dim.value, seen, f'Duplicate vector found for {dim.name}')
|
|
21
|
+
seen.add(dim.value)
|
|
19
22
|
|
|
20
|
-
def
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
def test_multiplication_adds_exponents(self):
|
|
24
|
+
self.assertEqual(
|
|
25
|
+
Dimension.mass * Dimension.acceleration,
|
|
26
|
+
Dimension.force,
|
|
27
|
+
)
|
|
28
|
+
self.assertEqual(
|
|
29
|
+
Dimension.length * Dimension.length,
|
|
30
|
+
Dimension.area,
|
|
31
|
+
)
|
|
32
|
+
self.assertEqual(
|
|
33
|
+
Dimension.length * Dimension.length * Dimension.length,
|
|
34
|
+
Dimension.volume,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def test_division_subtracts_exponents(self):
|
|
38
|
+
self.assertEqual(
|
|
39
|
+
Dimension.length / Dimension.time,
|
|
40
|
+
Dimension.velocity,
|
|
41
|
+
)
|
|
42
|
+
self.assertEqual(
|
|
43
|
+
Dimension.force / Dimension.area,
|
|
44
|
+
Dimension.pressure,
|
|
45
|
+
)
|
|
23
46
|
|
|
24
|
-
def
|
|
25
|
-
|
|
26
|
-
|
|
47
|
+
# def test_none_dimension_behaves_neutrally(self):
|
|
48
|
+
# base = Dimension.mass
|
|
49
|
+
# self.assertEqual(base * Dimension.none, base)
|
|
50
|
+
# self.assertEqual(base / Dimension.none, base)
|
|
51
|
+
# self.assertEqual(Dimension.none * base, base)
|
|
52
|
+
# with self.assertRaises(ValueError) as exc:
|
|
53
|
+
# Dimension.none / base
|
|
54
|
+
# assert type(exc.exception) == ValueError
|
|
55
|
+
# assert str(exc.exception).endswith('is not a valid Dimension')
|
|
56
|
+
|
|
57
|
+
def test_hash_and_equality_consistency(self):
|
|
58
|
+
d1 = Dimension.mass
|
|
59
|
+
d2 = Dimension.mass
|
|
60
|
+
d3 = Dimension.length
|
|
61
|
+
self.assertEqual(d1, d2)
|
|
62
|
+
self.assertNotEqual(d1, d3)
|
|
63
|
+
self.assertEqual(hash(d1), hash(d2))
|
|
64
|
+
self.assertNotEqual(hash(d1), hash(d3))
|
|
65
|
+
|
|
66
|
+
def test_composite_quantities_examples(self):
|
|
67
|
+
# Energy = Force * Length
|
|
68
|
+
self.assertEqual(
|
|
69
|
+
Dimension.force * Dimension.length,
|
|
70
|
+
Dimension.energy,
|
|
71
|
+
)
|
|
72
|
+
# Power = Energy / Time
|
|
73
|
+
self.assertEqual(
|
|
74
|
+
Dimension.energy / Dimension.time,
|
|
75
|
+
Dimension.power,
|
|
76
|
+
)
|
|
77
|
+
# Pressure = Force / Area
|
|
78
|
+
self.assertEqual(
|
|
79
|
+
Dimension.force / Dimension.area,
|
|
80
|
+
Dimension.pressure,
|
|
81
|
+
)
|
|
82
|
+
# Charge = Current * Time
|
|
83
|
+
self.assertEqual(
|
|
84
|
+
Dimension.current * Dimension.time,
|
|
85
|
+
Dimension.charge,
|
|
86
|
+
)
|
|
27
87
|
|
|
28
|
-
def
|
|
29
|
-
self.assertEqual(
|
|
30
|
-
self.
|
|
31
|
-
self.assertEqual(
|
|
32
|
-
self.
|
|
88
|
+
def test_vector_equality_reflects_dimension_equality(self):
|
|
89
|
+
self.assertEqual(Dimension.mass.value, Dimension.mass.value)
|
|
90
|
+
self.assertNotEqual(Dimension.mass.value, Dimension.time.value)
|
|
91
|
+
self.assertEqual(Dimension.mass, Dimension.mass)
|
|
92
|
+
self.assertNotEqual(Dimension.mass, Dimension.time)
|
|
93
|
+
|
|
94
|
+
def test_pow_identity_and_zero(self):
|
|
95
|
+
self.assertIs(Dimension.length ** 1, Dimension.length)
|
|
96
|
+
self.assertIs(Dimension.mass ** 0, Dimension.none)
|
|
97
|
+
|
|
98
|
+
def test_pow_known_results(self):
|
|
99
|
+
self.assertEqual(Dimension.length ** 2, Dimension.area)
|
|
100
|
+
self.assertEqual(Dimension.time ** -1, Dimension.frequency)
|
|
101
|
+
|
|
102
|
+
def test_pow_returns_derived_dimension_for_unknown(self):
|
|
103
|
+
jerk = Dimension.length * (Dimension.time ** -3) # length / time^3
|
|
104
|
+
self.assertTrue(jerk.name.startswith("derived("))
|
|
105
|
+
self.assertNotIn(jerk.name, Dimension.__members__)
|
|
106
|
+
|
|
107
|
+
def test_resolve_known_vector_returns_enum_member(self):
|
|
108
|
+
dim = Dimension._resolve(Vector(0, 1, 0, 0, 0, 0, 0))
|
|
109
|
+
self.assertIs(dim, Dimension.length)
|
|
110
|
+
|
|
111
|
+
def test_resolve_unknown_vector_returns_dynamic_dimension(self):
|
|
112
|
+
vec = Vector(T=1, L=-1, M=0, I=0, Θ=0, J=0, N=0) # “speed per time”, not an enum member
|
|
113
|
+
dyn = Dimension._resolve(vec)
|
|
114
|
+
self.assertNotIn(dyn.name, Dimension.__members__)
|
|
115
|
+
self.assertEqual(dyn.value, vec)
|
|
116
|
+
self.assertEqual(dyn.name, f"derived({vec})")
|
|
117
|
+
|
|
118
|
+
def test_resolve_returns_same_dynamic_for_same_vector(self):
|
|
119
|
+
vec = Vector(T=2, L=-2, M=0, I=0, Θ=0, J=0, N=0)
|
|
120
|
+
first = Dimension._resolve(vec)
|
|
121
|
+
second = Dimension._resolve(vec)
|
|
122
|
+
self.assertEqual(first.value, second.value)
|
|
123
|
+
self.assertEqual(first.name, second.name)
|
|
124
|
+
|
|
125
|
+
def test_dynamic_dimensions_compare_by_vector(self):
|
|
126
|
+
v1 = Vector(T=2, L=-2, M=0, I=0, Θ=0, J=0, N=0)
|
|
127
|
+
v2 = Vector(T=2, L=-2, M=0, I=0, Θ=0, J=0, N=0)
|
|
128
|
+
d1 = Dimension._resolve(v1)
|
|
129
|
+
d2 = Dimension._resolve(v2)
|
|
130
|
+
self.assertEqual(d1.value, d2.value)
|
|
131
|
+
self.assertEqual(d1 == d2, True)
|
|
132
|
+
self.assertEqual(hash(d1), hash(d2))
|
|
133
|
+
|
|
134
|
+
def test_pow_zero_returns_none(self):
|
|
135
|
+
# Dimension ** 0 should always return Dimension.none
|
|
136
|
+
self.assertIs(Dimension.length ** 0, Dimension.none)
|
|
137
|
+
|
|
138
|
+
def test_pow_fractional(self):
|
|
139
|
+
# Fractional powers = derived dimensions not equal to any registered one
|
|
140
|
+
d = Dimension.length ** 0.5
|
|
141
|
+
self.assertIsInstance(d, Dimension)
|
|
142
|
+
self.assertNotIn(d, list(Dimension))
|
|
143
|
+
|
|
144
|
+
def test_invalid_operand_multiply(self):
|
|
145
|
+
with self.assertRaises(TypeError):
|
|
146
|
+
Dimension.length * 10
|
|
33
147
|
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
#
|
|
43
|
-
|
|
44
|
-
self.
|
|
45
|
-
self.
|
|
148
|
+
def test_invalid_operand_divide(self):
|
|
149
|
+
with self.assertRaises(TypeError):
|
|
150
|
+
Dimension.time / "bad"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class TestDimensionResolve(unittest.TestCase):
|
|
154
|
+
|
|
155
|
+
def test_registered_multiplication(self):
|
|
156
|
+
# velocity = length / time
|
|
157
|
+
v = Dimension.length / Dimension.time
|
|
158
|
+
self.assertIs(v, Dimension.velocity)
|
|
159
|
+
self.assertEqual(v.value, Vector(-1, 1, 0, 0, 0, 0, 0))
|
|
160
|
+
|
|
161
|
+
def test_registered_power(self):
|
|
162
|
+
# area = length ** 2
|
|
163
|
+
a = Dimension.length ** 2
|
|
164
|
+
self.assertIs(a, Dimension.area)
|
|
165
|
+
self.assertEqual(a.value, Vector(0, 2, 0, 0, 0, 0, 0))
|
|
166
|
+
|
|
167
|
+
def test_unregistered_multiplication_creates_derived(self):
|
|
168
|
+
# L * M should yield derived(Vector(L=1, M=1))
|
|
169
|
+
d = Dimension.length * Dimension.mass
|
|
170
|
+
self.assertIsInstance(d, Dimension)
|
|
171
|
+
self.assertNotIn(d, list(Dimension))
|
|
172
|
+
self.assertIn("derived", d.name)
|
|
173
|
+
self.assertEqual(d.value, Vector(0, 1, 1, 0, 0, 0, 0))
|
|
174
|
+
|
|
175
|
+
def test_unregistered_division_creates_derived(self):
|
|
176
|
+
# M / T should yield derived(Vector(M=1, T=-1))
|
|
177
|
+
d = Dimension.mass / Dimension.time
|
|
178
|
+
self.assertIsInstance(d, Dimension)
|
|
179
|
+
self.assertNotIn(d, list(Dimension))
|
|
180
|
+
self.assertIn("derived", d.name)
|
|
181
|
+
self.assertEqual(d.value, Vector(-1, 0, 1, 0, 0, 0, 0))
|
|
182
|
+
|
|
183
|
+
def test_unregistered_power_creates_derived(self):
|
|
184
|
+
# (L * M)^2 → derived(Vector(L=2, M=2))
|
|
185
|
+
d1 = Dimension.length * Dimension.mass
|
|
186
|
+
d2 = d1 ** 2
|
|
187
|
+
self.assertIsInstance(d2, Dimension)
|
|
188
|
+
self.assertIn("derived", d2.name)
|
|
189
|
+
self.assertEqual(d2.value, Vector(0, 2, 2, 0, 0, 0, 0))
|
|
190
|
+
|
|
191
|
+
def test_registered_vs_derived_equality(self):
|
|
192
|
+
# Ensure derived dimensions only equal themselves
|
|
193
|
+
derived = Dimension.length * Dimension.mass
|
|
194
|
+
again = Dimension._resolve(Vector(0, 1, 1, 0, 0, 0, 0))
|
|
195
|
+
self.assertEqual(derived, again)
|
|
196
|
+
self.assertNotEqual(derived, Dimension.length)
|
|
197
|
+
self.assertNotEqual(derived, Dimension.mass)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class TestDimensionEdgeCases(unittest.TestCase):
|
|
201
|
+
|
|
202
|
+
def test_invalid_multiplication_type(self):
|
|
203
|
+
with self.assertRaises(TypeError):
|
|
204
|
+
Dimension.length * 5
|
|
205
|
+
with self.assertRaises(TypeError):
|
|
206
|
+
"mass" * Dimension.time
|
|
46
207
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
self.
|
|
51
|
-
|
|
52
|
-
self.assertEqual(product.evaluated, 2**30)
|
|
53
|
-
|
|
54
|
-
# cross-base multiplication returns numeric
|
|
55
|
-
val = self.kibibyte * self.thousand
|
|
56
|
-
self.assertIsInstance(val, float)
|
|
57
|
-
self.assertAlmostEqual(1024 * 1000, val)
|
|
58
|
-
|
|
59
|
-
def test___hash__(self):
|
|
60
|
-
a = Exponent(10, 3)
|
|
61
|
-
b = Exponent(10, 3)
|
|
62
|
-
self.assertEqual(hash(a), hash(b))
|
|
63
|
-
self.assertEqual(len({a, b}), 1) # both should hash to same value
|
|
64
|
-
|
|
65
|
-
def test___float__(self):
|
|
66
|
-
self.assertEqual(float(self.thousand), 1000.0)
|
|
67
|
-
|
|
68
|
-
def test___int__(self):
|
|
69
|
-
self.assertEqual(int(self.thousand), 1000)
|
|
70
|
-
|
|
71
|
-
def test_comparisons(self):
|
|
72
|
-
self.assertTrue(self.thousand > self.thousandth)
|
|
73
|
-
self.assertTrue(self.thousandth < self.thousand)
|
|
74
|
-
self.assertTrue(self.kibibyte < self.mebibyte)
|
|
75
|
-
self.assertTrue(self.kibibyte == Exponent(2, 10))
|
|
208
|
+
def test_invalid_division_type(self):
|
|
209
|
+
with self.assertRaises(TypeError):
|
|
210
|
+
Dimension.time / "length"
|
|
211
|
+
with self.assertRaises(TypeError):
|
|
212
|
+
5 / Dimension.mass
|
|
76
213
|
|
|
214
|
+
def test_equality_with_non_dimension(self):
|
|
77
215
|
with self.assertRaises(TypeError):
|
|
78
|
-
|
|
216
|
+
Dimension.mass == "mass"
|
|
79
217
|
|
|
80
|
-
def
|
|
81
|
-
|
|
218
|
+
def test_enum_uniqueness_and_hash(self):
|
|
219
|
+
# Hashes should be unique per distinct dimension
|
|
220
|
+
hashes = {hash(d) for d in Dimension}
|
|
221
|
+
self.assertEqual(len(hashes), len(Dimension))
|
|
222
|
+
# All Dimension.value entries must be distinct Vectors
|
|
223
|
+
values = [d.value for d in Dimension]
|
|
224
|
+
self.assertEqual(len(values), len(set(values)))
|
|
225
|
+
|
|
226
|
+
def test_combined_chained_operations(self):
|
|
227
|
+
# (mass * acceleration) / area = pressure
|
|
228
|
+
result = (Dimension.mass * Dimension.acceleration) / Dimension.area
|
|
229
|
+
self.assertEqual(result, Dimension.pressure)
|
|
230
|
+
|
|
231
|
+
def test_dimension_round_trip_equality(self):
|
|
232
|
+
# Multiplying and dividing by the same dimension returns self
|
|
233
|
+
d = Dimension.energy
|
|
234
|
+
self.assertEqual((d * Dimension.none) / Dimension.none, d)
|
|
235
|
+
self.assertEqual(d / Dimension.none, d)
|
|
236
|
+
self.assertEqual(Dimension.none * d, d)
|
|
237
|
+
|
|
238
|
+
def test_enum_is_hashable_and_iterable(self):
|
|
239
|
+
seen = {d for d in Dimension}
|
|
240
|
+
self.assertIn(Dimension.mass, seen)
|
|
241
|
+
self.assertEqual(len(seen), len(Dimension))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class TestScaleDescriptor(unittest.TestCase):
|
|
82
245
|
|
|
83
|
-
def
|
|
84
|
-
|
|
85
|
-
|
|
246
|
+
def test_scale_descriptor_power_and_repr(self):
|
|
247
|
+
exp = Exponent(10, 3)
|
|
248
|
+
desc = ScaleDescriptor(exp, "k", "kilo")
|
|
86
249
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
self.assertEqual(converted.base, 10)
|
|
92
|
-
self.assertAlmostEqual(converted.power, math.log10(1024), places=10)
|
|
250
|
+
# power property should reflect Exponent.power
|
|
251
|
+
assert desc.power == 3
|
|
252
|
+
assert desc.base == 10
|
|
253
|
+
assert math.isclose(desc.evaluated, 1e3)
|
|
93
254
|
|
|
94
|
-
|
|
95
|
-
|
|
255
|
+
# repr should include alias and power
|
|
256
|
+
r = repr(desc)
|
|
257
|
+
assert "kilo" in r or "k" in r
|
|
258
|
+
assert "10^3" in r
|
|
96
259
|
|
|
97
260
|
|
|
98
|
-
class TestScale(TestCase):
|
|
261
|
+
class TestScale(unittest.TestCase):
|
|
99
262
|
|
|
100
263
|
def test___truediv__(self):
|
|
101
264
|
self.assertEqual(Scale.deca, Scale.one / Scale.deci)
|
|
@@ -103,7 +266,6 @@ class TestScale(TestCase):
|
|
|
103
266
|
self.assertEqual(Scale.kibi, Scale.mebi / Scale.kibi)
|
|
104
267
|
self.assertEqual(Scale.milli, Scale.one / Scale.deca / Scale.deca / Scale.deca)
|
|
105
268
|
self.assertEqual(Scale.deca, Scale.kilo / Scale.hecto)
|
|
106
|
-
self.assertEqual(Scale._kibi, Scale.one / Scale.kibi)
|
|
107
269
|
self.assertEqual(Scale.kibi, Scale.kibi / Scale.one)
|
|
108
270
|
self.assertEqual(Scale.one, Scale.one / Scale.one)
|
|
109
271
|
self.assertEqual(Scale.one, Scale.kibi / Scale.kibi)
|
|
@@ -126,11 +288,11 @@ class TestScale(TestCase):
|
|
|
126
288
|
|
|
127
289
|
def test_all(self):
|
|
128
290
|
for scale in Scale:
|
|
129
|
-
self.assertTrue(isinstance(scale.value, Exponent))
|
|
291
|
+
self.assertTrue(isinstance(scale.value.exponent, Exponent))
|
|
130
292
|
self.assertIsInstance(Scale.all(), dict)
|
|
131
293
|
|
|
132
294
|
|
|
133
|
-
class TestScaleMultiplicationAdditional(TestCase):
|
|
295
|
+
class TestScaleMultiplicationAdditional(unittest.TestCase):
|
|
134
296
|
|
|
135
297
|
def test_decimal_combinations(self):
|
|
136
298
|
self.assertEqual(Scale.kilo * Scale.centi, Scale.deca)
|
|
@@ -166,8 +328,28 @@ class TestScaleMultiplicationAdditional(TestCase):
|
|
|
166
328
|
self.assertIsInstance(result, Scale)
|
|
167
329
|
self.assertEqual(result.value.base, 10)
|
|
168
330
|
|
|
331
|
+
def test_scale_multiplication_with_unit(self):
|
|
332
|
+
meter = Unit('m', name='meter', dimension=Dimension.length)
|
|
333
|
+
kilometer = Scale.kilo * meter
|
|
334
|
+
self.assertIsInstance(kilometer, Unit)
|
|
335
|
+
self.assertEqual(kilometer.scale, Scale.kilo)
|
|
336
|
+
self.assertEqual(kilometer.dimension, Dimension.length)
|
|
337
|
+
self.assertIn('meter', kilometer.name)
|
|
338
|
+
|
|
339
|
+
def test_scale_multiplication_with_unit_returns_not_implemented_for_invalid_type(self):
|
|
340
|
+
with self.assertRaises(TypeError):
|
|
341
|
+
Scale.kilo * 1
|
|
342
|
+
|
|
343
|
+
def test_scale_mul_with_unknown_exponent_hits_nearest(self):
|
|
344
|
+
# Construct two strange scales (base10^7 * base10^5 = base10^12 = tera)
|
|
345
|
+
s = Scale.nearest(10**7) * Scale.nearest(10**5)
|
|
346
|
+
self.assertIs(s, Scale.tera)
|
|
347
|
+
|
|
348
|
+
def test_scale_mul_non_unit_non_scale(self):
|
|
349
|
+
self.assertEqual(Scale.kilo.__mul__("nope"), NotImplemented)
|
|
169
350
|
|
|
170
|
-
|
|
351
|
+
|
|
352
|
+
class TestScaleDivisionAdditional(unittest.TestCase):
|
|
171
353
|
|
|
172
354
|
def test_division_same_base_large_gap(self):
|
|
173
355
|
# kilo / milli = mega
|
|
@@ -184,7 +366,6 @@ class TestScaleDivisionAdditional(TestCase):
|
|
|
184
366
|
|
|
185
367
|
def test_division_binary_inverse_scales(self):
|
|
186
368
|
self.assertEqual(Scale.kibi / Scale.kibi, Scale.one)
|
|
187
|
-
self.assertEqual(Scale.kibi / Scale.mebi, Scale._kibi)
|
|
188
369
|
self.assertEqual(Scale.mebi / Scale.kibi, Scale.kibi)
|
|
189
370
|
|
|
190
371
|
def test_division_unmatched_returns_nearest(self):
|
|
@@ -198,8 +379,15 @@ class TestScaleDivisionAdditional(TestCase):
|
|
|
198
379
|
with self.assertRaises(TypeError):
|
|
199
380
|
Scale.kilo / 42
|
|
200
381
|
|
|
382
|
+
def test_scale_div_hits_nearest(self):
|
|
383
|
+
# giga / kilo = 10^(9-3) = 10^6 = mega
|
|
384
|
+
self.assertIs(Scale.giga / Scale.kilo, Scale.mega)
|
|
385
|
+
|
|
386
|
+
def test_scale_div_non_scale(self):
|
|
387
|
+
self.assertEqual(Scale.kilo.__truediv__("bad"), NotImplemented)
|
|
201
388
|
|
|
202
|
-
|
|
389
|
+
|
|
390
|
+
class TestScaleNearestAdditional(unittest.TestCase):
|
|
203
391
|
|
|
204
392
|
def test_nearest_handles_zero(self):
|
|
205
393
|
self.assertEqual(Scale.nearest(0), Scale.one)
|
|
@@ -230,7 +418,7 @@ class TestScaleNearestAdditional(TestCase):
|
|
|
230
418
|
self.assertEqual(Scale.nearest(10**-9), Scale.nano)
|
|
231
419
|
|
|
232
420
|
|
|
233
|
-
class TestScaleInternals(TestCase):
|
|
421
|
+
class TestScaleInternals(unittest.TestCase):
|
|
234
422
|
|
|
235
423
|
def test_decimal_and_binary_sets_are_disjoint(self):
|
|
236
424
|
decimal_bases = {s.value.base for s in Scale._decimal_scales()}
|
|
@@ -253,175 +441,195 @@ class TestScaleInternals(TestCase):
|
|
|
253
441
|
self.assertIs(Scale.by_value(), Scale.by_value())
|
|
254
442
|
|
|
255
443
|
|
|
256
|
-
class
|
|
257
|
-
|
|
258
|
-
number = Number(unit=units.gram, quantity=1)
|
|
444
|
+
class TestUnit(unittest.TestCase):
|
|
259
445
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
self.assertEqual(ratio.denominator, Number())
|
|
265
|
-
|
|
266
|
-
def test_simplify(self):
|
|
267
|
-
ten_decagrams = Number(unit=units.gram, scale=Scale.deca, quantity=10)
|
|
268
|
-
point_one_decagrams = Number(unit=units.gram, scale=Scale.deca, quantity=0.1)
|
|
269
|
-
two_kibigrams = Number(unit=units.gram, scale=Scale.kibi, quantity=2)
|
|
270
|
-
|
|
271
|
-
self.assertEqual(Number(unit=units.gram, quantity=100), ten_decagrams.simplify())
|
|
272
|
-
self.assertEqual(Number(unit=units.gram, quantity=1), point_one_decagrams.simplify())
|
|
273
|
-
self.assertEqual(Number(unit=units.gram, quantity=2048), two_kibigrams.simplify())
|
|
274
|
-
|
|
275
|
-
def test_to(self):
|
|
276
|
-
thousandth_of_a_kilogram = Number(unit=units.gram, scale=Scale.kilo, quantity=0.001)
|
|
277
|
-
thousand_milligrams = Number(unit=units.gram, scale=Scale.milli, quantity=1000)
|
|
278
|
-
kibigram_fraction = Number(unit=units.gram, scale=Scale.kibi, quantity=0.0009765625)
|
|
279
|
-
|
|
280
|
-
self.assertEqual(thousandth_of_a_kilogram, self.number.to(Scale.kilo))
|
|
281
|
-
self.assertEqual(thousand_milligrams, self.number.to(Scale.milli))
|
|
282
|
-
self.assertEqual(kibigram_fraction, self.number.to(Scale.kibi))
|
|
283
|
-
|
|
284
|
-
def test___repr__(self):
|
|
285
|
-
self.assertIn(str(self.number.quantity), str(self.number))
|
|
286
|
-
self.assertIn(str(self.number.scale.value.evaluated), str(self.number))
|
|
287
|
-
self.assertIn(self.number.unit.name, str(self.number))
|
|
288
|
-
|
|
289
|
-
def test___truediv__(self):
|
|
290
|
-
some_number = Number(unit=units.gram, scale=Scale.deca, quantity=10)
|
|
291
|
-
another_number = Number(unit=units.gram, scale=Scale.milli, quantity=10)
|
|
292
|
-
that_number = Number(unit=units.gram, scale=Scale.kibi, quantity=10)
|
|
293
|
-
|
|
294
|
-
some_quotient = self.number / some_number
|
|
295
|
-
another_quotient = self.number / another_number
|
|
296
|
-
that_quotient = self.number / that_number
|
|
297
|
-
|
|
298
|
-
self.assertEqual(some_quotient.value, 0.01)
|
|
299
|
-
self.assertEqual(another_quotient.value, 100.0)
|
|
300
|
-
self.assertEqual(that_quotient.value, 0.00009765625)
|
|
301
|
-
|
|
302
|
-
def test___eq__(self):
|
|
303
|
-
self.assertEqual(self.number, Ratio(self.number)) # 1 gram / 1
|
|
304
|
-
with self.assertRaises(TypeError):
|
|
305
|
-
self.number == 1
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
class TestRatio(TestCase):
|
|
309
|
-
|
|
310
|
-
point_five = Number(quantity=0.5)
|
|
311
|
-
one = Number()
|
|
312
|
-
two = Number(quantity=2)
|
|
313
|
-
three = Number(quantity=3)
|
|
314
|
-
four = Number(quantity=4)
|
|
315
|
-
|
|
316
|
-
one_half = Ratio(numerator=one, denominator=two)
|
|
317
|
-
three_fourths = Ratio(numerator=three, denominator=four)
|
|
318
|
-
one_ratio = Ratio(numerator=one)
|
|
319
|
-
three_halves = Ratio(numerator=three, denominator=two)
|
|
320
|
-
two_ratio = Ratio(numerator=two, denominator=one)
|
|
321
|
-
|
|
322
|
-
def test_evaluate(self):
|
|
323
|
-
self.assertEqual(self.one_ratio.numerator, self.one)
|
|
324
|
-
self.assertEqual(self.one_ratio.denominator, self.one)
|
|
325
|
-
self.assertEqual(self.one_ratio.evaluate(), self.one)
|
|
326
|
-
self.assertEqual(self.two_ratio.evaluate(), self.two)
|
|
327
|
-
|
|
328
|
-
def test_reciprocal(self):
|
|
329
|
-
self.assertEqual(self.two_ratio.reciprocal().numerator, self.one)
|
|
330
|
-
self.assertEqual(self.two_ratio.reciprocal().denominator, self.two)
|
|
331
|
-
self.assertEqual(self.two_ratio.reciprocal().evaluate(), self.point_five)
|
|
332
|
-
|
|
333
|
-
def test___mul__commutivity(self):
|
|
334
|
-
# Does commutivity hold?
|
|
335
|
-
self.assertEqual(self.three_halves * self.one_half, self.three_fourths)
|
|
336
|
-
self.assertEqual(self.one_half * self.three_halves, self.three_fourths)
|
|
337
|
-
|
|
338
|
-
def test___mul__(self):
|
|
339
|
-
n1 = Number(unit=units.gram, quantity=3.119)
|
|
340
|
-
n2 = Number(unit=units.liter, scale=Scale.milli)
|
|
341
|
-
bromine_density = Ratio(n1, n2)
|
|
342
|
-
|
|
343
|
-
# How many grams of bromine are in 2 milliliters?
|
|
344
|
-
two_milliliters_bromine = Number(unit=units.liter, scale=Scale.milli, quantity=2)
|
|
345
|
-
ratio = two_milliliters_bromine.as_ratio() * bromine_density
|
|
346
|
-
answer = ratio.evaluate()
|
|
347
|
-
self.assertEqual(answer.unit.dimension, Dimension.mass)
|
|
348
|
-
self.assertEqual(answer.value, 6.238) # Grams
|
|
349
|
-
|
|
350
|
-
def test___truediv__(self):
|
|
351
|
-
seconds_per_hour = Ratio(
|
|
352
|
-
numerator=Number(unit=units.second, quantity=3600),
|
|
353
|
-
denominator=Number(unit=units.hour, quantity=1)
|
|
354
|
-
)
|
|
355
|
-
|
|
356
|
-
# How many Wh from 20 kJ?
|
|
357
|
-
twenty_kilojoules = Number(unit=units.joule, scale=Scale.kilo, quantity=20)
|
|
358
|
-
ratio = twenty_kilojoules.as_ratio() / seconds_per_hour
|
|
359
|
-
answer = ratio.evaluate()
|
|
360
|
-
self.assertEqual(answer.unit.dimension, Dimension.energy)
|
|
361
|
-
self.assertEqual(round(answer.value, 5), 5.55556) # Watt * hours
|
|
362
|
-
|
|
363
|
-
def test___eq__(self):
|
|
364
|
-
self.assertEqual(self.one_half, self.point_five)
|
|
365
|
-
with self.assertRaises(ValueError):
|
|
366
|
-
self.one_half == 1/2
|
|
446
|
+
unit_name = 'second'
|
|
447
|
+
unit_type = 'time'
|
|
448
|
+
unit_aliases = ('seconds', 'secs', 's', 'S')
|
|
449
|
+
unit = Unit(*unit_aliases, name=unit_name, dimension=Dimension.time)
|
|
367
450
|
|
|
368
451
|
def test___repr__(self):
|
|
369
|
-
self.assertEqual(
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
self.
|
|
393
|
-
self.
|
|
394
|
-
|
|
395
|
-
def
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
self.
|
|
412
|
-
self.
|
|
413
|
-
|
|
414
|
-
def
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
452
|
+
self.assertEqual(f'<Unit {self.unit_aliases[0]}>', str(self.unit))
|
|
453
|
+
|
|
454
|
+
def test_unit_repr_has_dimension_when_no_shorthand(self):
|
|
455
|
+
u = Unit(name="", dimension=Dimension.force)
|
|
456
|
+
r = repr(u)
|
|
457
|
+
self.assertIn("force", r)
|
|
458
|
+
self.assertTrue(r.startswith("<Unit"))
|
|
459
|
+
|
|
460
|
+
def test_unit_equality_alias_normalization(self):
|
|
461
|
+
# ('',) should normalize to () under _norm
|
|
462
|
+
u1 = Unit("", name="x", dimension=Dimension.length)
|
|
463
|
+
u2 = Unit(name="x", dimension=Dimension.length)
|
|
464
|
+
self.assertEqual(u1, u2)
|
|
465
|
+
|
|
466
|
+
def test_unit_invalid_eq_type(self):
|
|
467
|
+
self.assertFalse(Unit("m", dimension=Dimension.length) == "meter")
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
class TestCompositeUnit(unittest.TestCase):
|
|
471
|
+
def test_composite_unit_collapses_to_unit(self):
|
|
472
|
+
u = Unit("m", name="meter", dimension=Dimension.length)
|
|
473
|
+
cu = CompositeUnit({u: 1})
|
|
474
|
+
# should anneal to Unit
|
|
475
|
+
self.assertIsInstance(cu, Unit)
|
|
476
|
+
self.assertEqual(cu.shorthand, u.shorthand)
|
|
477
|
+
|
|
478
|
+
def test_merge_of_identical_units(self):
|
|
479
|
+
m = Unit("m", name="meter", dimension=Dimension.length)
|
|
480
|
+
# Inner composite that already has m^1
|
|
481
|
+
inner = CompositeUnit({m: 1, units.second: -1})
|
|
482
|
+
# Outer composite sees both `m:1` and `inner:1`
|
|
483
|
+
cu = CompositeUnit({m: 1, inner: 1})
|
|
484
|
+
# merge_unit should accumulate the exponents → m^(1 + 1) = m^2
|
|
485
|
+
self.assertIn(m, cu.components)
|
|
486
|
+
self.assertEqual(cu.components[m], 2)
|
|
487
|
+
|
|
488
|
+
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
|
+
# expect m*s^-2
|
|
494
|
+
self.assertEqual(accel.components[m], 1)
|
|
495
|
+
self.assertEqual(accel.components[s], -2)
|
|
496
|
+
|
|
497
|
+
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)
|
|
509
|
+
|
|
510
|
+
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
|
|
515
|
+
# equivalent to scale multiplication on RMUL path
|
|
516
|
+
self.assertIsNotNone(result)
|
|
517
|
+
self.assertIsNotNone(result.shorthand, "km/s")
|
|
518
|
+
|
|
519
|
+
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)
|
|
525
|
+
|
|
526
|
+
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
|
|
532
|
+
# jerk = m^1 s^-2 / m^1 s^-1 = s^-1
|
|
533
|
+
self.assertEqual(list(jerk.components.values()), [-1])
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class TestUnitEdgeCases(unittest.TestCase):
|
|
537
|
+
|
|
538
|
+
# --- Initialization & representation -----------------------------------
|
|
539
|
+
|
|
540
|
+
def test_default_unit_is_dimensionless(self):
|
|
541
|
+
u = Unit()
|
|
542
|
+
self.assertEqual(u.dimension, Dimension.none)
|
|
543
|
+
self.assertEqual(u.name, '')
|
|
544
|
+
self.assertEqual(u.aliases, ())
|
|
545
|
+
self.assertEqual(u.shorthand, '')
|
|
546
|
+
self.assertEqual(repr(u), '<Unit>')
|
|
547
|
+
|
|
548
|
+
def test_unit_with_aliases_and_name(self):
|
|
549
|
+
u = Unit('m', 'M', name='meter', dimension=Dimension.length)
|
|
550
|
+
self.assertEqual(u.shorthand, 'm')
|
|
551
|
+
self.assertIn('m', u.aliases)
|
|
552
|
+
self.assertIn('M', u.aliases)
|
|
553
|
+
self.assertIn('length', u.dimension.name)
|
|
554
|
+
self.assertIn('meter', u.name)
|
|
555
|
+
self.assertIn('<Unit m>', repr(u))
|
|
556
|
+
|
|
557
|
+
def test_hash_and_equality_consistency(self):
|
|
558
|
+
u1 = Unit('m', name='meter', dimension=Dimension.length)
|
|
559
|
+
u2 = Unit('m', name='meter', dimension=Dimension.length)
|
|
560
|
+
u3 = Unit('s', name='second', dimension=Dimension.time)
|
|
561
|
+
self.assertEqual(u1, u2)
|
|
562
|
+
self.assertEqual(hash(u1), hash(u2))
|
|
563
|
+
self.assertNotEqual(u1, u3)
|
|
564
|
+
self.assertNotEqual(hash(u1), hash(u3))
|
|
565
|
+
|
|
566
|
+
def test_units_with_same_name_but_different_dimension_not_equal(self):
|
|
567
|
+
u1 = Unit(name='amp', dimension=Dimension.current)
|
|
568
|
+
u2 = Unit(name='amp', dimension=Dimension.time)
|
|
569
|
+
self.assertNotEqual(u1, u2)
|
|
570
|
+
|
|
571
|
+
# --- arithmetic behavior ----------------------------------------------
|
|
572
|
+
|
|
573
|
+
def test_multiplication_produces_composite_unit(self):
|
|
574
|
+
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
575
|
+
s = Unit('s', name='second', dimension=Dimension.time)
|
|
576
|
+
v = m / s
|
|
577
|
+
self.assertIsInstance(v, Unit)
|
|
578
|
+
self.assertEqual(v.dimension, Dimension.velocity)
|
|
579
|
+
self.assertIn('/', repr(v))
|
|
580
|
+
|
|
581
|
+
def test_division_with_dimensionless_denominator_returns_self(self):
|
|
582
|
+
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
583
|
+
none = Unit(name='none', dimension=Dimension.none)
|
|
584
|
+
result = m / none
|
|
585
|
+
self.assertEqual(result, m)
|
|
586
|
+
|
|
587
|
+
def test_division_of_identical_units_returns_dimensionless(self):
|
|
588
|
+
m1 = Unit('m', name='meter', dimension=Dimension.length)
|
|
589
|
+
m2 = Unit('m', name='meter', dimension=Dimension.length)
|
|
590
|
+
result = m1 / m2
|
|
591
|
+
self.assertEqual(result.dimension, Dimension.none)
|
|
592
|
+
self.assertEqual(result.name, '')
|
|
593
|
+
|
|
594
|
+
def test_multiplying_with_dimensionless_returns_self(self):
|
|
595
|
+
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
596
|
+
none = Unit(name='none', dimension=Dimension.none)
|
|
597
|
+
result = m * none
|
|
598
|
+
self.assertEqual(result.dimension, Dimension.length)
|
|
599
|
+
self.assertEqual('m', result.shorthand)
|
|
600
|
+
|
|
601
|
+
def test_invalid_dimension_combinations_raise_value_error(self):
|
|
602
|
+
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
603
|
+
c = Unit('C', name='coulomb', dimension=Dimension.charge)
|
|
604
|
+
# The result of combination gives CompositeUnit
|
|
605
|
+
self.assertIsInstance(m / c, CompositeUnit)
|
|
606
|
+
self.assertIsInstance(m * c, CompositeUnit)
|
|
607
|
+
|
|
608
|
+
# --- equality, hashing, immutability ----------------------------------
|
|
609
|
+
|
|
610
|
+
def test_equality_with_non_unit(self):
|
|
611
|
+
self.assertFalse(Unit('m', name='meter', dimension=Dimension.length) == 'meter')
|
|
612
|
+
|
|
613
|
+
def test_hash_stability_in_collections(self):
|
|
614
|
+
m1 = Unit('m', name='meter', dimension=Dimension.length)
|
|
615
|
+
s = set([m1])
|
|
616
|
+
self.assertIn(Unit('m', name='meter', dimension=Dimension.length), s)
|
|
617
|
+
|
|
618
|
+
def test_operations_do_not_mutate_operands(self):
|
|
619
|
+
m = Unit('m', name='meter', dimension=Dimension.length)
|
|
620
|
+
s = Unit('s', name='second', dimension=Dimension.time)
|
|
621
|
+
_ = m / s
|
|
622
|
+
self.assertEqual(m.dimension, Dimension.length)
|
|
623
|
+
self.assertEqual(s.dimension, Dimension.time)
|
|
624
|
+
|
|
625
|
+
# --- operator edge cases ----------------------------------------------
|
|
626
|
+
|
|
627
|
+
def test_repr_contains_dimension_name_even_without_name(self):
|
|
628
|
+
u = Unit(dimension=Dimension.force)
|
|
629
|
+
self.assertIn('force', repr(u))
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class TestScaleEdgeCases(unittest.TestCase):
|
|
425
633
|
|
|
426
634
|
def test_nearest_prefers_decimal_by_default(self):
|
|
427
635
|
self.assertEqual(Scale.nearest(1024), Scale.kilo)
|
|
@@ -429,7 +637,6 @@ class TestScaleEdgeCases(TestCase):
|
|
|
429
637
|
self.assertEqual(Scale.nearest(1/1024), Scale.milli)
|
|
430
638
|
|
|
431
639
|
def test_nearest_includes_binary_when_opted_in(self):
|
|
432
|
-
self.assertEqual(Scale.nearest(1/1024, include_binary=True), Scale._kibi)
|
|
433
640
|
self.assertEqual(Scale.nearest(1024, include_binary=True), Scale.kibi)
|
|
434
641
|
self.assertEqual(Scale.nearest(50_000, include_binary=True), Scale.kibi)
|
|
435
642
|
self.assertEqual(Scale.nearest(2**20, include_binary=True), Scale.mebi)
|
|
@@ -465,119 +672,3 @@ class TestScaleEdgeCases(TestCase):
|
|
|
465
672
|
all_map = Scale.all()
|
|
466
673
|
by_val = Scale.by_value()
|
|
467
674
|
self.assertTrue(all((val in by_val.values()) for _, val in all_map.items()))
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
class TestNumberEdgeCases(TestCase):
|
|
471
|
-
|
|
472
|
-
def test_default_number_is_dimensionless_one(self):
|
|
473
|
-
n = Number()
|
|
474
|
-
self.assertEqual(n.unit, units.none)
|
|
475
|
-
self.assertEqual(n.scale, Scale.one)
|
|
476
|
-
self.assertEqual(n.quantity, 1)
|
|
477
|
-
self.assertAlmostEqual(n.value, 1.0)
|
|
478
|
-
self.assertIn("1", repr(n))
|
|
479
|
-
|
|
480
|
-
def test_to_new_scale_changes_value(self):
|
|
481
|
-
n = Number(quantity=1000, scale=Scale.kilo)
|
|
482
|
-
converted = n.to(Scale.one)
|
|
483
|
-
self.assertNotEqual(n.value, converted.value)
|
|
484
|
-
self.assertAlmostEqual(converted.value, 1000)
|
|
485
|
-
|
|
486
|
-
def test_simplify_uses_value_as_quantity(self):
|
|
487
|
-
n = Number(quantity=2, scale=Scale.kilo)
|
|
488
|
-
simplified = n.simplify()
|
|
489
|
-
self.assertEqual(simplified.quantity, n.value)
|
|
490
|
-
self.assertEqual(simplified.unit, n.unit)
|
|
491
|
-
|
|
492
|
-
def test_multiplication_combines_units_and_quantities(self):
|
|
493
|
-
n1 = Number(unit=units.joule, quantity=2)
|
|
494
|
-
n2 = Number(unit=units.second, quantity=3)
|
|
495
|
-
result = n1 * n2
|
|
496
|
-
self.assertEqual(result.quantity, 6)
|
|
497
|
-
self.assertEqual(result.unit.dimension, Dimension.energy * Dimension.time)
|
|
498
|
-
|
|
499
|
-
def test_division_combines_units_scales_and_quantities(self):
|
|
500
|
-
n1 = Number(unit=units.meter, scale=Scale.kilo, quantity=1000)
|
|
501
|
-
n2 = Number(unit=units.second, scale=Scale.one, quantity=2)
|
|
502
|
-
result = n1 / n2
|
|
503
|
-
self.assertEqual(result.scale, Scale.kilo / Scale.one)
|
|
504
|
-
self.assertEqual(result.unit.dimension, Dimension.velocity)
|
|
505
|
-
self.assertAlmostEqual(result.quantity, 500)
|
|
506
|
-
|
|
507
|
-
def test_equality_with_non_number_raises_value_error(self):
|
|
508
|
-
n = Number()
|
|
509
|
-
with self.assertRaises(TypeError):
|
|
510
|
-
n == '5'
|
|
511
|
-
|
|
512
|
-
def test_equality_between_numbers_and_ratios(self):
|
|
513
|
-
n1 = Number(quantity=10)
|
|
514
|
-
n2 = Number(quantity=10)
|
|
515
|
-
r = Ratio(n1, n2)
|
|
516
|
-
self.assertTrue(r == Number())
|
|
517
|
-
|
|
518
|
-
def test_repr_includes_scale_and_unit(self):
|
|
519
|
-
n = Number(unit=units.volt, scale=Scale.kilo, quantity=5)
|
|
520
|
-
rep = repr(n)
|
|
521
|
-
self.assertIn("kilo", rep)
|
|
522
|
-
self.assertIn("volt", rep)
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
class TestRatioEdgeCases(TestCase):
|
|
526
|
-
|
|
527
|
-
def test_default_ratio_is_dimensionless_one(self):
|
|
528
|
-
r = Ratio()
|
|
529
|
-
self.assertEqual(r.numerator.unit, units.none)
|
|
530
|
-
self.assertEqual(r.denominator.unit, units.none)
|
|
531
|
-
self.assertAlmostEqual(r.evaluate().value, 1.0)
|
|
532
|
-
|
|
533
|
-
def test_reciprocal_swaps_numerator_and_denominator(self):
|
|
534
|
-
n1 = Number(quantity=10)
|
|
535
|
-
n2 = Number(quantity=2)
|
|
536
|
-
r = Ratio(n1, n2)
|
|
537
|
-
reciprocal = r.reciprocal()
|
|
538
|
-
self.assertEqual(reciprocal.numerator, r.denominator)
|
|
539
|
-
self.assertEqual(reciprocal.denominator, r.numerator)
|
|
540
|
-
|
|
541
|
-
def test_evaluate_returns_number_division_result(self):
|
|
542
|
-
r = Ratio(Number(unit=units.meter), Number(unit=units.second))
|
|
543
|
-
result = r.evaluate()
|
|
544
|
-
self.assertIsInstance(result, Number)
|
|
545
|
-
self.assertEqual(result.unit.dimension, Dimension.velocity)
|
|
546
|
-
|
|
547
|
-
def test_multiplication_between_compatible_ratios(self):
|
|
548
|
-
r1 = Ratio(Number(unit=units.meter), Number(unit=units.second))
|
|
549
|
-
r2 = Ratio(Number(unit=units.second), Number(unit=units.meter))
|
|
550
|
-
product = r1 * r2
|
|
551
|
-
self.assertIsInstance(product, Ratio)
|
|
552
|
-
self.assertEqual(product.evaluate().unit.dimension, Dimension.none)
|
|
553
|
-
|
|
554
|
-
def test_multiplication_with_incompatible_units_fallback(self):
|
|
555
|
-
r1 = Ratio(Number(unit=units.meter), Number(unit=units.ampere))
|
|
556
|
-
r2 = Ratio(Number(unit=units.ampere), Number(unit=units.meter))
|
|
557
|
-
result = r1 * r2
|
|
558
|
-
self.assertIsInstance(result, Ratio)
|
|
559
|
-
|
|
560
|
-
def test_division_between_ratios_yields_new_ratio(self):
|
|
561
|
-
r1 = Ratio(Number(quantity=2), Number(quantity=1))
|
|
562
|
-
r2 = Ratio(Number(quantity=4), Number(quantity=2))
|
|
563
|
-
result = r1 / r2
|
|
564
|
-
self.assertIsInstance(result, Ratio)
|
|
565
|
-
self.assertAlmostEqual(result.evaluate().value, 1.0)
|
|
566
|
-
|
|
567
|
-
def test_equality_with_non_ratio_raises_value_error(self):
|
|
568
|
-
r = Ratio()
|
|
569
|
-
with self.assertRaises(ValueError):
|
|
570
|
-
_ = (r == "not_a_ratio")
|
|
571
|
-
|
|
572
|
-
def test_repr_handles_equal_numerator_denominator(self):
|
|
573
|
-
r = Ratio()
|
|
574
|
-
self.assertEqual(str(r.evaluate().value), "1.0")
|
|
575
|
-
rep = repr(r)
|
|
576
|
-
self.assertTrue(rep.startswith("<1"))
|
|
577
|
-
|
|
578
|
-
def test_repr_of_non_equal_ratio_includes_slash(self):
|
|
579
|
-
n1 = Number(quantity=2)
|
|
580
|
-
n2 = Number(quantity=1)
|
|
581
|
-
r = Ratio(n1, n2)
|
|
582
|
-
rep = repr(r)
|
|
583
|
-
self.assertIn("/", rep)
|