ucon 0.5.1__py3-none-any.whl → 0.6.0__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.
- ucon/__init__.py +24 -3
- ucon/algebra.py +36 -14
- ucon/core.py +414 -2
- ucon/graph.py +167 -10
- ucon/mcp/__init__.py +8 -0
- ucon/mcp/server.py +250 -0
- ucon/pydantic.py +199 -0
- ucon/units.py +286 -11
- {ucon-0.5.1.dist-info → ucon-0.6.0.dist-info}/METADATA +88 -31
- ucon-0.6.0.dist-info/RECORD +17 -0
- ucon-0.6.0.dist-info/entry_points.txt +2 -0
- ucon-0.6.0.dist-info/top_level.txt +1 -0
- tests/ucon/__init__.py +0 -3
- tests/ucon/conversion/__init__.py +0 -0
- tests/ucon/conversion/test_graph.py +0 -409
- tests/ucon/conversion/test_map.py +0 -409
- tests/ucon/test_algebra.py +0 -239
- tests/ucon/test_core.py +0 -827
- tests/ucon/test_default_graph_conversions.py +0 -443
- tests/ucon/test_dimensionless_units.py +0 -248
- tests/ucon/test_quantity.py +0 -615
- tests/ucon/test_uncertainty.py +0 -264
- tests/ucon/test_units.py +0 -25
- ucon-0.5.1.dist-info/RECORD +0 -24
- ucon-0.5.1.dist-info/top_level.txt +0 -2
- {ucon-0.5.1.dist-info → ucon-0.6.0.dist-info}/WHEEL +0 -0
- {ucon-0.5.1.dist-info → ucon-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {ucon-0.5.1.dist-info → ucon-0.6.0.dist-info}/licenses/NOTICE +0 -0
tests/ucon/test_core.py
DELETED
|
@@ -1,827 +0,0 @@
|
|
|
1
|
-
# © 2025 The Radiativity Company
|
|
2
|
-
# Licensed under the Apache License, Version 2.0
|
|
3
|
-
# See the LICENSE file for details.
|
|
4
|
-
|
|
5
|
-
import math
|
|
6
|
-
import unittest
|
|
7
|
-
|
|
8
|
-
from ucon import Number
|
|
9
|
-
from ucon import Exponent
|
|
10
|
-
from ucon import Ratio
|
|
11
|
-
from ucon import Scale
|
|
12
|
-
from ucon import Dimension
|
|
13
|
-
from ucon import Unit
|
|
14
|
-
from ucon import units
|
|
15
|
-
from ucon.algebra import Vector
|
|
16
|
-
from ucon.core import UnitFactor, UnitProduct, ScaleDescriptor
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class TestDimension(unittest.TestCase):
|
|
20
|
-
|
|
21
|
-
def test_basic_dimensions_are_unique(self):
|
|
22
|
-
seen = set()
|
|
23
|
-
for dim in Dimension:
|
|
24
|
-
self.assertNotIn(dim.value, seen, f'Duplicate vector found for {dim.name}')
|
|
25
|
-
seen.add(dim.value)
|
|
26
|
-
|
|
27
|
-
def test_multiplication_adds_exponents(self):
|
|
28
|
-
self.assertEqual(
|
|
29
|
-
Dimension.mass * Dimension.acceleration,
|
|
30
|
-
Dimension.force,
|
|
31
|
-
)
|
|
32
|
-
self.assertEqual(
|
|
33
|
-
Dimension.length * Dimension.length,
|
|
34
|
-
Dimension.area,
|
|
35
|
-
)
|
|
36
|
-
self.assertEqual(
|
|
37
|
-
Dimension.length * Dimension.length * Dimension.length,
|
|
38
|
-
Dimension.volume,
|
|
39
|
-
)
|
|
40
|
-
|
|
41
|
-
def test_division_subtracts_exponents(self):
|
|
42
|
-
self.assertEqual(
|
|
43
|
-
Dimension.length / Dimension.time,
|
|
44
|
-
Dimension.velocity,
|
|
45
|
-
)
|
|
46
|
-
self.assertEqual(
|
|
47
|
-
Dimension.force / Dimension.area,
|
|
48
|
-
Dimension.pressure,
|
|
49
|
-
)
|
|
50
|
-
|
|
51
|
-
# def test_none_dimension_behaves_neutrally(self):
|
|
52
|
-
# base = Dimension.mass
|
|
53
|
-
# self.assertEqual(base * Dimension.none, base)
|
|
54
|
-
# self.assertEqual(base / Dimension.none, base)
|
|
55
|
-
# self.assertEqual(Dimension.none * base, base)
|
|
56
|
-
# with self.assertRaises(ValueError) as exc:
|
|
57
|
-
# Dimension.none / base
|
|
58
|
-
# assert type(exc.exception) == ValueError
|
|
59
|
-
# assert str(exc.exception).endswith('is not a valid Dimension')
|
|
60
|
-
|
|
61
|
-
def test_hash_and_equality_consistency(self):
|
|
62
|
-
d1 = Dimension.mass
|
|
63
|
-
d2 = Dimension.mass
|
|
64
|
-
d3 = Dimension.length
|
|
65
|
-
self.assertEqual(d1, d2)
|
|
66
|
-
self.assertNotEqual(d1, d3)
|
|
67
|
-
self.assertEqual(hash(d1), hash(d2))
|
|
68
|
-
self.assertNotEqual(hash(d1), hash(d3))
|
|
69
|
-
|
|
70
|
-
def test_composite_quantities_examples(self):
|
|
71
|
-
# Energy = Force * Length
|
|
72
|
-
self.assertEqual(
|
|
73
|
-
Dimension.force * Dimension.length,
|
|
74
|
-
Dimension.energy,
|
|
75
|
-
)
|
|
76
|
-
# Power = Energy / Time
|
|
77
|
-
self.assertEqual(
|
|
78
|
-
Dimension.energy / Dimension.time,
|
|
79
|
-
Dimension.power,
|
|
80
|
-
)
|
|
81
|
-
# Pressure = Force / Area
|
|
82
|
-
self.assertEqual(
|
|
83
|
-
Dimension.force / Dimension.area,
|
|
84
|
-
Dimension.pressure,
|
|
85
|
-
)
|
|
86
|
-
# Charge = Current * Time
|
|
87
|
-
self.assertEqual(
|
|
88
|
-
Dimension.current * Dimension.time,
|
|
89
|
-
Dimension.charge,
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
def test_vector_equality_reflects_dimension_equality(self):
|
|
93
|
-
self.assertEqual(Dimension.mass.value, Dimension.mass.value)
|
|
94
|
-
self.assertNotEqual(Dimension.mass.value, Dimension.time.value)
|
|
95
|
-
self.assertEqual(Dimension.mass, Dimension.mass)
|
|
96
|
-
self.assertNotEqual(Dimension.mass, Dimension.time)
|
|
97
|
-
|
|
98
|
-
def test_pow_identity_and_zero(self):
|
|
99
|
-
self.assertIs(Dimension.length ** 1, Dimension.length)
|
|
100
|
-
self.assertIs(Dimension.mass ** 0, Dimension.none)
|
|
101
|
-
|
|
102
|
-
def test_pow_known_results(self):
|
|
103
|
-
self.assertEqual(Dimension.length ** 2, Dimension.area)
|
|
104
|
-
self.assertEqual(Dimension.time ** -1, Dimension.frequency)
|
|
105
|
-
|
|
106
|
-
def test_pow_returns_derived_dimension_for_unknown(self):
|
|
107
|
-
jerk = Dimension.length * (Dimension.time ** -3) # length / time^3
|
|
108
|
-
self.assertTrue(jerk.name.startswith("derived("))
|
|
109
|
-
self.assertNotIn(jerk.name, Dimension.__members__)
|
|
110
|
-
|
|
111
|
-
def test_resolve_known_vector_returns_enum_member(self):
|
|
112
|
-
dim = Dimension._resolve(Vector(0, 1, 0, 0, 0, 0, 0))
|
|
113
|
-
self.assertIs(dim, Dimension.length)
|
|
114
|
-
|
|
115
|
-
def test_resolve_unknown_vector_returns_dynamic_dimension(self):
|
|
116
|
-
vec = Vector(T=1, L=-1, M=0, I=0, Θ=0, J=0, N=0) # “speed per time”, not an enum member
|
|
117
|
-
dyn = Dimension._resolve(vec)
|
|
118
|
-
self.assertNotIn(dyn.name, Dimension.__members__)
|
|
119
|
-
self.assertEqual(dyn.value, vec)
|
|
120
|
-
self.assertEqual(dyn.name, f"derived({vec})")
|
|
121
|
-
|
|
122
|
-
def test_resolve_returns_same_dynamic_for_same_vector(self):
|
|
123
|
-
vec = Vector(T=2, L=-2, M=0, I=0, Θ=0, J=0, N=0)
|
|
124
|
-
first = Dimension._resolve(vec)
|
|
125
|
-
second = Dimension._resolve(vec)
|
|
126
|
-
self.assertEqual(first.value, second.value)
|
|
127
|
-
self.assertEqual(first.name, second.name)
|
|
128
|
-
|
|
129
|
-
def test_dynamic_dimensions_compare_by_vector(self):
|
|
130
|
-
v1 = Vector(T=2, L=-2, M=0, I=0, Θ=0, J=0, N=0)
|
|
131
|
-
v2 = Vector(T=2, L=-2, M=0, I=0, Θ=0, J=0, N=0)
|
|
132
|
-
d1 = Dimension._resolve(v1)
|
|
133
|
-
d2 = Dimension._resolve(v2)
|
|
134
|
-
self.assertEqual(d1.value, d2.value)
|
|
135
|
-
self.assertEqual(d1 == d2, True)
|
|
136
|
-
self.assertEqual(hash(d1), hash(d2))
|
|
137
|
-
|
|
138
|
-
def test_pow_zero_returns_none(self):
|
|
139
|
-
# Dimension ** 0 should always return Dimension.none
|
|
140
|
-
self.assertIs(Dimension.length ** 0, Dimension.none)
|
|
141
|
-
|
|
142
|
-
def test_pow_fractional(self):
|
|
143
|
-
# Fractional powers = derived dimensions not equal to any registered one
|
|
144
|
-
d = Dimension.length ** 0.5
|
|
145
|
-
self.assertIsInstance(d, Dimension)
|
|
146
|
-
self.assertNotIn(d, list(Dimension))
|
|
147
|
-
|
|
148
|
-
def test_invalid_operand_multiply(self):
|
|
149
|
-
with self.assertRaises(TypeError):
|
|
150
|
-
Dimension.length * 10
|
|
151
|
-
|
|
152
|
-
def test_invalid_operand_divide(self):
|
|
153
|
-
with self.assertRaises(TypeError):
|
|
154
|
-
Dimension.time / "bad"
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
class TestDimensionResolve(unittest.TestCase):
|
|
158
|
-
|
|
159
|
-
def test_registered_multiplication(self):
|
|
160
|
-
# velocity = length / time
|
|
161
|
-
v = Dimension.length / Dimension.time
|
|
162
|
-
self.assertIs(v, Dimension.velocity)
|
|
163
|
-
self.assertEqual(v.value, Vector(-1, 1, 0, 0, 0, 0, 0))
|
|
164
|
-
|
|
165
|
-
def test_registered_power(self):
|
|
166
|
-
# area = length ** 2
|
|
167
|
-
a = Dimension.length ** 2
|
|
168
|
-
self.assertIs(a, Dimension.area)
|
|
169
|
-
self.assertEqual(a.value, Vector(0, 2, 0, 0, 0, 0, 0))
|
|
170
|
-
|
|
171
|
-
def test_unregistered_multiplication_creates_derived(self):
|
|
172
|
-
# L * M should yield derived(Vector(L=1, M=1))
|
|
173
|
-
d = Dimension.length * Dimension.mass
|
|
174
|
-
self.assertIsInstance(d, Dimension)
|
|
175
|
-
self.assertNotIn(d, list(Dimension))
|
|
176
|
-
self.assertIn("derived", d.name)
|
|
177
|
-
self.assertEqual(d.value, Vector(0, 1, 1, 0, 0, 0, 0))
|
|
178
|
-
|
|
179
|
-
def test_unregistered_division_creates_derived(self):
|
|
180
|
-
# M / T should yield derived(Vector(M=1, T=-1))
|
|
181
|
-
d = Dimension.mass / Dimension.time
|
|
182
|
-
self.assertIsInstance(d, Dimension)
|
|
183
|
-
self.assertNotIn(d, list(Dimension))
|
|
184
|
-
self.assertIn("derived", d.name)
|
|
185
|
-
self.assertEqual(d.value, Vector(-1, 0, 1, 0, 0, 0, 0))
|
|
186
|
-
|
|
187
|
-
def test_unregistered_power_creates_derived(self):
|
|
188
|
-
# (L * M)^2 → derived(Vector(L=2, M=2))
|
|
189
|
-
d1 = Dimension.length * Dimension.mass
|
|
190
|
-
d2 = d1 ** 2
|
|
191
|
-
self.assertIsInstance(d2, Dimension)
|
|
192
|
-
self.assertIn("derived", d2.name)
|
|
193
|
-
self.assertEqual(d2.value, Vector(0, 2, 2, 0, 0, 0, 0))
|
|
194
|
-
|
|
195
|
-
def test_registered_vs_derived_equality(self):
|
|
196
|
-
# Ensure derived dimensions only equal themselves
|
|
197
|
-
derived = Dimension.length * Dimension.mass
|
|
198
|
-
again = Dimension._resolve(Vector(0, 1, 1, 0, 0, 0, 0))
|
|
199
|
-
self.assertEqual(derived, again)
|
|
200
|
-
self.assertNotEqual(derived, Dimension.length)
|
|
201
|
-
self.assertNotEqual(derived, Dimension.mass)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
class TestDimensionEdgeCases(unittest.TestCase):
|
|
205
|
-
|
|
206
|
-
def test_invalid_multiplication_type(self):
|
|
207
|
-
with self.assertRaises(TypeError):
|
|
208
|
-
Dimension.length * 5
|
|
209
|
-
with self.assertRaises(TypeError):
|
|
210
|
-
"mass" * Dimension.time
|
|
211
|
-
|
|
212
|
-
def test_invalid_division_type(self):
|
|
213
|
-
with self.assertRaises(TypeError):
|
|
214
|
-
Dimension.time / "length"
|
|
215
|
-
with self.assertRaises(TypeError):
|
|
216
|
-
5 / Dimension.mass
|
|
217
|
-
|
|
218
|
-
def test_equality_with_non_dimension(self):
|
|
219
|
-
with self.assertRaises(TypeError):
|
|
220
|
-
Dimension.mass == "mass"
|
|
221
|
-
|
|
222
|
-
def test_enum_uniqueness_and_hash(self):
|
|
223
|
-
# Hashes should be unique per distinct dimension
|
|
224
|
-
hashes = {hash(d) for d in Dimension}
|
|
225
|
-
self.assertEqual(len(hashes), len(Dimension))
|
|
226
|
-
# All Dimension.value entries must be distinct Vectors
|
|
227
|
-
values = [d.value for d in Dimension]
|
|
228
|
-
self.assertEqual(len(values), len(set(values)))
|
|
229
|
-
|
|
230
|
-
def test_combined_chained_operations(self):
|
|
231
|
-
# (mass * acceleration) / area = pressure
|
|
232
|
-
result = (Dimension.mass * Dimension.acceleration) / Dimension.area
|
|
233
|
-
self.assertEqual(result, Dimension.pressure)
|
|
234
|
-
|
|
235
|
-
def test_dimension_round_trip_equality(self):
|
|
236
|
-
# Multiplying and dividing by the same dimension returns self
|
|
237
|
-
d = Dimension.energy
|
|
238
|
-
self.assertEqual((d * Dimension.none) / Dimension.none, d)
|
|
239
|
-
self.assertEqual(d / Dimension.none, d)
|
|
240
|
-
self.assertEqual(Dimension.none * d, d)
|
|
241
|
-
|
|
242
|
-
def test_enum_is_hashable_and_iterable(self):
|
|
243
|
-
seen = {d for d in Dimension}
|
|
244
|
-
self.assertIn(Dimension.mass, seen)
|
|
245
|
-
self.assertEqual(len(seen), len(Dimension))
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
class TestScaleDescriptor(unittest.TestCase):
|
|
249
|
-
|
|
250
|
-
def test_scale_descriptor_power_and_repr(self):
|
|
251
|
-
exp = Exponent(10, 3)
|
|
252
|
-
desc = ScaleDescriptor(exp, "k", "kilo")
|
|
253
|
-
|
|
254
|
-
# power property should reflect Exponent.power
|
|
255
|
-
assert desc.power == 3
|
|
256
|
-
assert desc.base == 10
|
|
257
|
-
assert math.isclose(desc.evaluated, 1e3)
|
|
258
|
-
|
|
259
|
-
# repr should include alias and power
|
|
260
|
-
r = repr(desc)
|
|
261
|
-
assert "kilo" in r or "k" in r
|
|
262
|
-
assert "10^3" in r
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
class TestScale(unittest.TestCase):
|
|
266
|
-
|
|
267
|
-
def test___truediv__(self):
|
|
268
|
-
self.assertEqual(Scale.deca, Scale.one / Scale.deci)
|
|
269
|
-
self.assertEqual(Scale.deci, Scale.one / Scale.deca)
|
|
270
|
-
self.assertEqual(Scale.kibi, Scale.mebi / Scale.kibi)
|
|
271
|
-
self.assertEqual(Scale.milli, Scale.one / Scale.deca / Scale.deca / Scale.deca)
|
|
272
|
-
self.assertEqual(Scale.deca, Scale.kilo / Scale.hecto)
|
|
273
|
-
self.assertEqual(Scale.kibi, Scale.kibi / Scale.one)
|
|
274
|
-
self.assertEqual(Scale.one, Scale.one / Scale.one)
|
|
275
|
-
self.assertEqual(Scale.one, Scale.kibi / Scale.kibi)
|
|
276
|
-
self.assertEqual(Scale.one, Scale.kibi / Scale.kilo)
|
|
277
|
-
|
|
278
|
-
def test___mul__(self):
|
|
279
|
-
self.assertEqual(Scale.kilo, Scale.kilo * Scale.one)
|
|
280
|
-
self.assertEqual(Scale.kilo, Scale.one * Scale.kilo)
|
|
281
|
-
self.assertEqual(Scale.one, Scale.kilo * Scale.milli)
|
|
282
|
-
self.assertEqual(Scale.deca, Scale.hecto * Scale.deci)
|
|
283
|
-
self.assertEqual(Scale.mega, Scale.kilo * Scale.kibi)
|
|
284
|
-
self.assertEqual(Scale.giga, Scale.mega * Scale.kilo)
|
|
285
|
-
self.assertEqual(Scale.one, Scale.one * Scale.one)
|
|
286
|
-
|
|
287
|
-
def test___lt__(self):
|
|
288
|
-
self.assertLess(Scale.one, Scale.kilo)
|
|
289
|
-
|
|
290
|
-
def test___gt__(self):
|
|
291
|
-
self.assertGreater(Scale.kilo, Scale.one)
|
|
292
|
-
|
|
293
|
-
def test_all(self):
|
|
294
|
-
for scale in Scale:
|
|
295
|
-
self.assertTrue(isinstance(scale.value.exponent, Exponent))
|
|
296
|
-
self.assertIsInstance(Scale.all(), dict)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
class TestScaleMultiplicationAdditional(unittest.TestCase):
|
|
300
|
-
|
|
301
|
-
def test_decimal_combinations(self):
|
|
302
|
-
self.assertEqual(Scale.kilo * Scale.centi, Scale.deca)
|
|
303
|
-
self.assertEqual(Scale.kilo * Scale.milli, Scale.one)
|
|
304
|
-
self.assertEqual(Scale.hecto * Scale.deci, Scale.deca)
|
|
305
|
-
|
|
306
|
-
def test_binary_combinations(self):
|
|
307
|
-
# kibi (2^10) * mebi (2^20) = 2^30 (should round to nearest known)
|
|
308
|
-
result = Scale.kibi * Scale.mebi
|
|
309
|
-
self.assertEqual(result.value.base, 2)
|
|
310
|
-
self.assertTrue(isinstance(result, Scale))
|
|
311
|
-
|
|
312
|
-
def test_mixed_base_combination(self):
|
|
313
|
-
self.assertEqual(Scale.mega, Scale.kilo * Scale.kibi)
|
|
314
|
-
|
|
315
|
-
def test_result_has_no_exact_match_fallbacks_to_nearest(self):
|
|
316
|
-
# Suppose the exponent product is not in Scale.all()
|
|
317
|
-
# e.g. kilo (10^3) * deci (10^-1) = 10^2 = hecto
|
|
318
|
-
result = Scale.kilo * Scale.deci
|
|
319
|
-
self.assertEqual(result, Scale.hecto)
|
|
320
|
-
|
|
321
|
-
def test_order_independence(self):
|
|
322
|
-
# Associativity of multiplication
|
|
323
|
-
self.assertEqual(Scale.kilo * Scale.centi, Scale.centi * Scale.kilo)
|
|
324
|
-
|
|
325
|
-
def test_non_scale_operand_returns_not_implemented(self):
|
|
326
|
-
with self.assertRaises(TypeError):
|
|
327
|
-
Scale.kilo * 2
|
|
328
|
-
|
|
329
|
-
def test_large_exponent_clamping(self):
|
|
330
|
-
# simulate a very large multiplication, should still resolve
|
|
331
|
-
result = Scale.mega * Scale.mega # 10^12, not defined -> nearest Scale
|
|
332
|
-
self.assertIsInstance(result, Scale)
|
|
333
|
-
self.assertEqual(result.value.base, 10)
|
|
334
|
-
|
|
335
|
-
def test_scale_multiplication_with_unit(self):
|
|
336
|
-
"""Scale * Unit returns a UnitProduct with the scaled unit."""
|
|
337
|
-
kilometer = Scale.kilo * units.meter
|
|
338
|
-
self.assertIsInstance(kilometer, UnitProduct)
|
|
339
|
-
self.assertEqual(kilometer.dimension, Dimension.length)
|
|
340
|
-
self.assertEqual(kilometer.shorthand, "km")
|
|
341
|
-
self.assertAlmostEqual(kilometer.fold_scale(), 1000.0, places=10)
|
|
342
|
-
|
|
343
|
-
def test_scale_multiplication_with_unit_returns_not_implemented_for_invalid_type(self):
|
|
344
|
-
with self.assertRaises(TypeError):
|
|
345
|
-
Scale.kilo * 1
|
|
346
|
-
|
|
347
|
-
def test_scale_mul_with_unknown_exponent_hits_nearest(self):
|
|
348
|
-
# Construct two strange scales (base10^7 * base10^5 = base10^12 = tera)
|
|
349
|
-
s = Scale.nearest(10**7) * Scale.nearest(10**5)
|
|
350
|
-
self.assertIs(s, Scale.tera)
|
|
351
|
-
|
|
352
|
-
def test_scale_mul_non_unit_non_scale(self):
|
|
353
|
-
self.assertEqual(Scale.kilo.__mul__("nope"), NotImplemented)
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
class TestScaleDivisionAdditional(unittest.TestCase):
|
|
357
|
-
|
|
358
|
-
def test_division_same_base_large_gap(self):
|
|
359
|
-
# kilo / milli = mega
|
|
360
|
-
self.assertEqual(Scale.kilo / Scale.milli, Scale.mega)
|
|
361
|
-
# milli / kilo = micro
|
|
362
|
-
self.assertEqual(Scale.milli / Scale.kilo, Scale.micro)
|
|
363
|
-
|
|
364
|
-
def test_division_cross_base_scales(self):
|
|
365
|
-
# Decimal vs binary cross-base — should return nearest matching scale
|
|
366
|
-
result = Scale.kilo / Scale.kibi
|
|
367
|
-
self.assertIsInstance(result, Scale)
|
|
368
|
-
# They’re roughly equal, so nearest should be Scale.one
|
|
369
|
-
self.assertEqual(result, Scale.one)
|
|
370
|
-
|
|
371
|
-
def test_division_binary_inverse_scales(self):
|
|
372
|
-
self.assertEqual(Scale.kibi / Scale.kibi, Scale.one)
|
|
373
|
-
self.assertEqual(Scale.mebi / Scale.kibi, Scale.kibi)
|
|
374
|
-
|
|
375
|
-
def test_division_unmatched_returns_nearest(self):
|
|
376
|
-
# giga / kibi is a weird combo → nearest mega or similar
|
|
377
|
-
result = Scale.giga / Scale.kibi
|
|
378
|
-
self.assertIsInstance(result, Scale)
|
|
379
|
-
self.assertIn(result, Scale)
|
|
380
|
-
|
|
381
|
-
def test_division_type_safety(self):
|
|
382
|
-
# Ensure non-Scale raises NotImplemented
|
|
383
|
-
with self.assertRaises(TypeError):
|
|
384
|
-
Scale.kilo / 42
|
|
385
|
-
|
|
386
|
-
def test_scale_div_hits_nearest(self):
|
|
387
|
-
# giga / kilo = 10^(9-3) = 10^6 = mega
|
|
388
|
-
self.assertIs(Scale.giga / Scale.kilo, Scale.mega)
|
|
389
|
-
|
|
390
|
-
def test_scale_div_non_scale(self):
|
|
391
|
-
self.assertEqual(Scale.kilo.__truediv__("bad"), NotImplemented)
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
class TestScaleNearestAdditional(unittest.TestCase):
|
|
395
|
-
|
|
396
|
-
def test_nearest_handles_zero(self):
|
|
397
|
-
self.assertEqual(Scale.nearest(0), Scale.one)
|
|
398
|
-
|
|
399
|
-
def test_nearest_handles_negative_values(self):
|
|
400
|
-
# Only magnitude matters, not sign
|
|
401
|
-
self.assertEqual(Scale.nearest(-1000), Scale.kilo)
|
|
402
|
-
self.assertEqual(Scale.nearest(-0.001), Scale.milli)
|
|
403
|
-
|
|
404
|
-
def test_nearest_with_undershoot_bias_effect(self):
|
|
405
|
-
# Lower bias should make undershoot (ratios < 1) less penalized
|
|
406
|
-
# This test ensures the bias argument doesn’t break ordering
|
|
407
|
-
s_default = Scale.nearest(50_000, undershoot_bias=0.75)
|
|
408
|
-
s_stronger_bias = Scale.nearest(50_000, undershoot_bias=0.9)
|
|
409
|
-
# The result shouldn't flip to something wildly different
|
|
410
|
-
self.assertIn(s_default, [Scale.kilo, Scale.mega])
|
|
411
|
-
self.assertIn(s_stronger_bias, [Scale.kilo, Scale.mega])
|
|
412
|
-
|
|
413
|
-
def test_nearest_respects_binary_preference_flag(self):
|
|
414
|
-
# Confirm that enabling binary changes candidate set
|
|
415
|
-
decimal_result = Scale.nearest(2**10)
|
|
416
|
-
binary_result = Scale.nearest(2**10, include_binary=True)
|
|
417
|
-
self.assertNotEqual(decimal_result, binary_result)
|
|
418
|
-
self.assertEqual(binary_result, Scale.kibi)
|
|
419
|
-
|
|
420
|
-
def test_nearest_upper_and_lower_extremes(self):
|
|
421
|
-
self.assertEqual(Scale.nearest(10**9), Scale.giga)
|
|
422
|
-
self.assertEqual(Scale.nearest(10**-9), Scale.nano)
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
class TestScaleInternals(unittest.TestCase):
|
|
426
|
-
|
|
427
|
-
def test_decimal_and_binary_sets_are_disjoint(self):
|
|
428
|
-
decimal_bases = {s.value.base for s in Scale._decimal_scales()}
|
|
429
|
-
binary_bases = {s.value.base for s in Scale._binary_scales()}
|
|
430
|
-
self.assertNotEqual(decimal_bases, binary_bases)
|
|
431
|
-
self.assertEqual(decimal_bases, {10})
|
|
432
|
-
self.assertEqual(binary_bases, {2})
|
|
433
|
-
|
|
434
|
-
def test_all_and_by_value_consistency(self):
|
|
435
|
-
mapping = Scale.all()
|
|
436
|
-
value_map = Scale.by_value()
|
|
437
|
-
# Each value’s evaluated form should appear in by_value keys
|
|
438
|
-
for (base, power), name in mapping.items():
|
|
439
|
-
val = Scale[name].value.evaluated
|
|
440
|
-
self.assertIn(round(val, 15), value_map)
|
|
441
|
-
|
|
442
|
-
def test_all_and_by_value_are_cached(self):
|
|
443
|
-
# Call multiple times and ensure they’re same object (cached)
|
|
444
|
-
self.assertIs(Scale.all(), Scale.all())
|
|
445
|
-
self.assertIs(Scale.by_value(), Scale.by_value())
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
class TestUnit(unittest.TestCase):
|
|
449
|
-
|
|
450
|
-
unit_name = 'second'
|
|
451
|
-
unit_type = 'time'
|
|
452
|
-
unit_aliases = ('seconds', 'secs', 's', 'S')
|
|
453
|
-
unit = Unit(name=unit_name, dimension=Dimension.time, aliases=unit_aliases)
|
|
454
|
-
|
|
455
|
-
def test___repr__(self):
|
|
456
|
-
self.assertEqual(f'<Unit {self.unit_aliases[0]}>', str(self.unit))
|
|
457
|
-
|
|
458
|
-
def test_unit_repr_has_dimension_when_no_shorthand(self):
|
|
459
|
-
u = Unit(name="", dimension=Dimension.force)
|
|
460
|
-
r = repr(u)
|
|
461
|
-
self.assertIn("force", r)
|
|
462
|
-
self.assertTrue(r.startswith("<Unit"))
|
|
463
|
-
|
|
464
|
-
def test_unit_equality_alias_normalization(self):
|
|
465
|
-
# ('',) should normalize to () under _norm
|
|
466
|
-
u1 = Unit(name="x", dimension=Dimension.length, aliases=("",))
|
|
467
|
-
u2 = Unit(name="x", dimension=Dimension.length)
|
|
468
|
-
self.assertEqual(u1, u2)
|
|
469
|
-
|
|
470
|
-
def test_unit_invalid_eq_type(self):
|
|
471
|
-
self.assertFalse(Unit(name="meter", dimension=Dimension.length, aliases=("m",)) == "meter")
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
class TestUnitProduct(unittest.TestCase):
|
|
475
|
-
|
|
476
|
-
mf = UnitFactor(unit=units.meter, scale=Scale.one)
|
|
477
|
-
sf = UnitFactor(unit=units.second, scale=Scale.one)
|
|
478
|
-
nf = UnitFactor(unit=units.none, scale=Scale.one)
|
|
479
|
-
velocity = UnitProduct({mf: 1, sf: -1})
|
|
480
|
-
acceleration = UnitProduct({mf: 1, sf: -2})
|
|
481
|
-
|
|
482
|
-
def test_composite_unit_collapses_to_unit(self):
|
|
483
|
-
cu = UnitProduct({self.mf: 1})
|
|
484
|
-
# should anneal to Unit
|
|
485
|
-
self.assertIsInstance(cu, UnitProduct)
|
|
486
|
-
self.assertEqual(cu.shorthand, self.mf.shorthand)
|
|
487
|
-
|
|
488
|
-
def test_merge_of_identical_units(self):
|
|
489
|
-
# Inner composite that already has m^1
|
|
490
|
-
inner = UnitProduct({self.mf: 1, self.sf: -1})
|
|
491
|
-
# Outer composite sees both `m:1` and `inner:1`
|
|
492
|
-
up = UnitProduct({self.mf: 1, inner: 1})
|
|
493
|
-
# merge_unit should accumulate the exponents → m^(1 + 1) = m^2
|
|
494
|
-
self.assertIn(self.mf, up.factors)
|
|
495
|
-
self.assertEqual(up.factors[self.mf], 2)
|
|
496
|
-
|
|
497
|
-
def test_merge_of_nested_composite_units(self):
|
|
498
|
-
# expect m*s^-2
|
|
499
|
-
self.assertEqual(self.acceleration.factors[self.mf], 1)
|
|
500
|
-
self.assertEqual(self.acceleration.factors[self.sf], -2)
|
|
501
|
-
|
|
502
|
-
def test_drop_dimensionless_component(self):
|
|
503
|
-
up = UnitProduct({self.mf: 2, self.nf: 1})
|
|
504
|
-
self.assertIn(self.mf, up.factors)
|
|
505
|
-
self.assertNotIn(self.nf, up.factors)
|
|
506
|
-
|
|
507
|
-
def test_unitproduct_can_behave_like_single_unit(self):
|
|
508
|
-
"""
|
|
509
|
-
A UnitProduct with only one factor should seem like that factor.
|
|
510
|
-
"""
|
|
511
|
-
up = UnitProduct({self.mf: 1})
|
|
512
|
-
self.assertEqual(up.shorthand, self.mf.shorthand)
|
|
513
|
-
self.assertEqual(up.dimension, self.mf.dimension)
|
|
514
|
-
|
|
515
|
-
def test_composite_mul_with_scale(self):
|
|
516
|
-
up = UnitProduct({self.mf: 1, self.sf: -1})
|
|
517
|
-
result = Scale.kilo * up
|
|
518
|
-
# equivalent to scale multiplication on RMUL path
|
|
519
|
-
self.assertIsNotNone(result)
|
|
520
|
-
self.assertIsNotNone(result.shorthand, "km/s")
|
|
521
|
-
|
|
522
|
-
def test_composite_div_dimensionless(self):
|
|
523
|
-
up = UnitProduct({self.mf: 2})
|
|
524
|
-
out = up / UnitProduct({})
|
|
525
|
-
self.assertEqual(out.factors[self.mf], 2)
|
|
526
|
-
|
|
527
|
-
def test_truediv_composite_by_composite(self):
|
|
528
|
-
jerk = self.acceleration / self.velocity
|
|
529
|
-
# jerk = m^1 s^-2 / m^1 s^-1 = s^-1
|
|
530
|
-
self.assertEqual(list(jerk.factors.values()), [-1])
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
class TestUnitEdgeCases(unittest.TestCase):
|
|
534
|
-
|
|
535
|
-
# --- Initialization & representation -----------------------------------
|
|
536
|
-
|
|
537
|
-
def test_default_unit_is_dimensionless(self):
|
|
538
|
-
u = Unit()
|
|
539
|
-
self.assertEqual(u.dimension, Dimension.none)
|
|
540
|
-
self.assertEqual(u.name, '')
|
|
541
|
-
self.assertEqual(u.aliases, ())
|
|
542
|
-
self.assertEqual(u.shorthand, '')
|
|
543
|
-
self.assertEqual(repr(u), '<Unit>')
|
|
544
|
-
|
|
545
|
-
def test_unit_with_aliases_and_name(self):
|
|
546
|
-
u = Unit(name='meter', dimension=Dimension.length, aliases=('m', 'M'))
|
|
547
|
-
self.assertEqual(u.shorthand, 'm')
|
|
548
|
-
self.assertIn('m', u.aliases)
|
|
549
|
-
self.assertIn('M', u.aliases)
|
|
550
|
-
self.assertIn('length', u.dimension.name)
|
|
551
|
-
self.assertIn('meter', u.name)
|
|
552
|
-
self.assertIn('<Unit m>', repr(u))
|
|
553
|
-
|
|
554
|
-
def test_hash_and_equality_consistency(self):
|
|
555
|
-
u1 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
556
|
-
u2 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
557
|
-
u3 = Unit(name='second', dimension=Dimension.time, aliases=('s',))
|
|
558
|
-
self.assertEqual(u1, u2)
|
|
559
|
-
self.assertEqual(hash(u1), hash(u2))
|
|
560
|
-
self.assertNotEqual(u1, u3)
|
|
561
|
-
self.assertNotEqual(hash(u1), hash(u3))
|
|
562
|
-
|
|
563
|
-
def test_units_with_same_name_but_different_dimension_not_equal(self):
|
|
564
|
-
u1 = Unit(name='amp', dimension=Dimension.current)
|
|
565
|
-
u2 = Unit(name='amp', dimension=Dimension.time)
|
|
566
|
-
self.assertNotEqual(u1, u2)
|
|
567
|
-
|
|
568
|
-
# --- arithmetic behavior ----------------------------------------------
|
|
569
|
-
|
|
570
|
-
def test_multiplication_produces_composite_unit(self):
|
|
571
|
-
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
572
|
-
s = Unit(name='second', dimension=Dimension.time, aliases=('s',))
|
|
573
|
-
v = m / s
|
|
574
|
-
self.assertIsInstance(v, UnitProduct)
|
|
575
|
-
self.assertEqual(v.dimension, Dimension.velocity)
|
|
576
|
-
self.assertIn('/', repr(v))
|
|
577
|
-
|
|
578
|
-
def test_division_with_dimensionless_denominator_returns_self(self):
|
|
579
|
-
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
580
|
-
none = Unit(name='none', dimension=Dimension.none)
|
|
581
|
-
result = m / none
|
|
582
|
-
self.assertEqual(result, m)
|
|
583
|
-
|
|
584
|
-
def test_division_of_identical_units_returns_dimensionless(self):
|
|
585
|
-
m1 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
586
|
-
m2 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
587
|
-
result = m1 / m2
|
|
588
|
-
self.assertEqual(result.dimension, Dimension.none)
|
|
589
|
-
self.assertEqual(result.name, '')
|
|
590
|
-
|
|
591
|
-
def test_multiplying_with_dimensionless_returns_self(self):
|
|
592
|
-
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
593
|
-
none = Unit(name='none', dimension=Dimension.none)
|
|
594
|
-
result = m * none
|
|
595
|
-
self.assertEqual(result.dimension, Dimension.length)
|
|
596
|
-
self.assertEqual('m', result.shorthand)
|
|
597
|
-
|
|
598
|
-
def test_invalid_dimension_combinations_raise_value_error(self):
|
|
599
|
-
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
600
|
-
c = Unit(name='coulomb', dimension=Dimension.charge, aliases=('C',))
|
|
601
|
-
# The result of combination gives CompositeUnit
|
|
602
|
-
self.assertIsInstance(m / c, UnitProduct)
|
|
603
|
-
self.assertIsInstance(m * c, UnitProduct)
|
|
604
|
-
|
|
605
|
-
# --- equality, hashing, immutability ----------------------------------
|
|
606
|
-
|
|
607
|
-
def test_equality_with_non_unit(self):
|
|
608
|
-
self.assertFalse(Unit(name='meter', dimension=Dimension.length, aliases=('m',)) == 'meter')
|
|
609
|
-
|
|
610
|
-
def test_hash_stability_in_collections(self):
|
|
611
|
-
m1 = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
612
|
-
s = set([m1])
|
|
613
|
-
self.assertIn(Unit(name='meter', dimension=Dimension.length, aliases=('m',)), s)
|
|
614
|
-
|
|
615
|
-
def test_operations_do_not_mutate_operands(self):
|
|
616
|
-
m = Unit(name='meter', dimension=Dimension.length, aliases=('m',))
|
|
617
|
-
s = Unit(name='second', dimension=Dimension.time, aliases=('s',))
|
|
618
|
-
_ = m / s
|
|
619
|
-
self.assertEqual(m.dimension, Dimension.length)
|
|
620
|
-
self.assertEqual(s.dimension, Dimension.time)
|
|
621
|
-
|
|
622
|
-
# --- operator edge cases ----------------------------------------------
|
|
623
|
-
|
|
624
|
-
def test_repr_contains_dimension_name_even_without_name(self):
|
|
625
|
-
u = Unit(dimension=Dimension.force)
|
|
626
|
-
self.assertIn('force', repr(u))
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
class TestScaleEdgeCases(unittest.TestCase):
|
|
630
|
-
|
|
631
|
-
def test_nearest_prefers_decimal_by_default(self):
|
|
632
|
-
self.assertEqual(Scale.nearest(1024), Scale.kilo)
|
|
633
|
-
self.assertEqual(Scale.nearest(50_000), Scale.kilo)
|
|
634
|
-
self.assertEqual(Scale.nearest(1/1024), Scale.milli)
|
|
635
|
-
|
|
636
|
-
def test_nearest_includes_binary_when_opted_in(self):
|
|
637
|
-
self.assertEqual(Scale.nearest(1024, include_binary=True), Scale.kibi)
|
|
638
|
-
self.assertEqual(Scale.nearest(50_000, include_binary=True), Scale.kibi)
|
|
639
|
-
self.assertEqual(Scale.nearest(2**20, include_binary=True), Scale.mebi)
|
|
640
|
-
|
|
641
|
-
def test_nearest_subunit_behavior(self):
|
|
642
|
-
self.assertEqual(Scale.nearest(0.0009), Scale.milli)
|
|
643
|
-
self.assertEqual(Scale.nearest(1e-7), Scale.micro)
|
|
644
|
-
|
|
645
|
-
def test_division_same_base_scales(self):
|
|
646
|
-
result = Scale.kilo / Scale.milli
|
|
647
|
-
self.assertIsInstance(result, Scale)
|
|
648
|
-
self.assertEqual(result.value.evaluated, 10 ** 6)
|
|
649
|
-
|
|
650
|
-
def test_division_same_scale_returns_one(self):
|
|
651
|
-
self.assertEqual(Scale.kilo / Scale.kilo, Scale.one)
|
|
652
|
-
|
|
653
|
-
def test_division_different_bases_returns_valid_scale(self):
|
|
654
|
-
result = Scale.kibi / Scale.kilo
|
|
655
|
-
self.assertIsInstance(result, Scale)
|
|
656
|
-
self.assertIn(result, Scale)
|
|
657
|
-
|
|
658
|
-
def test_division_with_one(self):
|
|
659
|
-
result = Scale.one / Scale.kilo
|
|
660
|
-
self.assertIsInstance(result, Scale)
|
|
661
|
-
self.assertTrue(hasattr(result, "value"))
|
|
662
|
-
|
|
663
|
-
def test_comparisons_and_equality(self):
|
|
664
|
-
self.assertTrue(Scale.kilo > Scale.deci)
|
|
665
|
-
self.assertTrue(Scale.milli < Scale.one)
|
|
666
|
-
self.assertTrue(Scale.kilo == Scale.kilo)
|
|
667
|
-
|
|
668
|
-
def test_all_and_by_value_cover_all_enum_members(self):
|
|
669
|
-
all_map = Scale.all()
|
|
670
|
-
by_val = Scale.by_value()
|
|
671
|
-
self.assertTrue(all((val in by_val.values()) for _, val in all_map.items()))
|
|
672
|
-
|
|
673
|
-
def test_descriptor_property(self):
|
|
674
|
-
self.assertIsInstance(Scale.kilo.descriptor, ScaleDescriptor)
|
|
675
|
-
self.assertEqual(Scale.kilo.descriptor, Scale.kilo.value)
|
|
676
|
-
|
|
677
|
-
def test_alias_property(self):
|
|
678
|
-
self.assertEqual(Scale.kilo.alias, "kilo")
|
|
679
|
-
self.assertEqual(Scale.one.alias, "")
|
|
680
|
-
|
|
681
|
-
def test_scale_descriptor_parts(self):
|
|
682
|
-
self.assertEqual(Scale.kilo.value.parts(), (10, 3))
|
|
683
|
-
self.assertEqual(Scale.kibi.value.parts(), (2, 10))
|
|
684
|
-
|
|
685
|
-
def test_scale_hash_used_in_sets(self):
|
|
686
|
-
s = {Scale.kilo, Scale.milli}
|
|
687
|
-
self.assertIn(Scale.kilo, s)
|
|
688
|
-
self.assertNotIn(Scale.one, s)
|
|
689
|
-
|
|
690
|
-
def test_scale_mul_nonmatching_falls_to_nearest(self):
|
|
691
|
-
# kilo * kibi → no exact match, falls through to Scale.nearest
|
|
692
|
-
result = Scale.kilo * Scale.kibi
|
|
693
|
-
self.assertIsInstance(result, Scale)
|
|
694
|
-
|
|
695
|
-
def test_scale_pow(self):
|
|
696
|
-
result = Scale.kilo ** 2
|
|
697
|
-
self.assertEqual(result, Scale.mega)
|
|
698
|
-
|
|
699
|
-
def test_scale_pow_binary(self):
|
|
700
|
-
result = Scale.kibi ** 2
|
|
701
|
-
self.assertEqual(result, Scale.mebi)
|
|
702
|
-
|
|
703
|
-
def test_scale_pow_nonmatching_falls_to_nearest(self):
|
|
704
|
-
result = Scale.kilo ** 0.5
|
|
705
|
-
self.assertIsInstance(result, Scale)
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
class TestUnitAlgebra(unittest.TestCase):
|
|
709
|
-
|
|
710
|
-
def test_unit_mul_unitproduct(self):
|
|
711
|
-
m = units.meter
|
|
712
|
-
velocity = UnitProduct({m: 1, units.second: -1})
|
|
713
|
-
result = m * velocity
|
|
714
|
-
self.assertIsInstance(result, UnitProduct)
|
|
715
|
-
# m * (m/s) = m²/s
|
|
716
|
-
self.assertEqual(result.dimension, Dimension.area / Dimension.time)
|
|
717
|
-
|
|
718
|
-
def test_unit_mul_non_unit_returns_not_implemented(self):
|
|
719
|
-
result = units.meter.__mul__("not a unit")
|
|
720
|
-
self.assertIs(result, NotImplemented)
|
|
721
|
-
|
|
722
|
-
def test_unit_truediv_non_unit_returns_not_implemented(self):
|
|
723
|
-
result = units.meter.__truediv__("not a unit")
|
|
724
|
-
self.assertIs(result, NotImplemented)
|
|
725
|
-
|
|
726
|
-
def test_unit_pow(self):
|
|
727
|
-
m = units.meter
|
|
728
|
-
result = m ** 2
|
|
729
|
-
self.assertIsInstance(result, UnitProduct)
|
|
730
|
-
self.assertEqual(result.dimension, Dimension.area)
|
|
731
|
-
|
|
732
|
-
def test_unit_pow_3(self):
|
|
733
|
-
m = units.meter
|
|
734
|
-
result = m ** 3
|
|
735
|
-
self.assertEqual(result.dimension, Dimension.volume)
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
class TestUnitFactorCoverage(unittest.TestCase):
|
|
739
|
-
|
|
740
|
-
def test_shorthand_name_fallback(self):
|
|
741
|
-
# UnitFactor where unit has no aliases but has a name
|
|
742
|
-
u = Unit(name='gram', dimension=Dimension.mass)
|
|
743
|
-
fu = UnitFactor(unit=u, scale=Scale.milli)
|
|
744
|
-
self.assertEqual(fu.shorthand, 'mgram')
|
|
745
|
-
|
|
746
|
-
def test_repr(self):
|
|
747
|
-
fu = UnitFactor(unit=units.meter, scale=Scale.kilo)
|
|
748
|
-
self.assertIn('UnitFactor', repr(fu))
|
|
749
|
-
self.assertIn('kilo', repr(fu))
|
|
750
|
-
|
|
751
|
-
def test_eq_non_unit_returns_not_implemented(self):
|
|
752
|
-
fu = UnitFactor(unit=units.meter, scale=Scale.one)
|
|
753
|
-
self.assertIs(fu.__eq__("string"), NotImplemented)
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
class TestUnitProductAlgebra(unittest.TestCase):
|
|
757
|
-
|
|
758
|
-
def test_mul_unitproduct_by_unitproduct(self):
|
|
759
|
-
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
760
|
-
time_sq = UnitProduct({units.second: 2})
|
|
761
|
-
result = velocity * time_sq
|
|
762
|
-
self.assertIsInstance(result, UnitProduct)
|
|
763
|
-
# (m/s) * s² = m·s
|
|
764
|
-
self.assertEqual(result.dimension, Dimension.length * Dimension.time)
|
|
765
|
-
|
|
766
|
-
def test_mul_unitproduct_by_scale_returns_not_implemented(self):
|
|
767
|
-
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
768
|
-
result = velocity.__mul__(Scale.kilo)
|
|
769
|
-
self.assertIs(result, NotImplemented)
|
|
770
|
-
|
|
771
|
-
def test_mul_unitproduct_by_non_unit_returns_not_implemented(self):
|
|
772
|
-
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
773
|
-
result = velocity.__mul__("string")
|
|
774
|
-
self.assertIs(result, NotImplemented)
|
|
775
|
-
|
|
776
|
-
def test_truediv_unitproduct_by_unitproduct(self):
|
|
777
|
-
acceleration = UnitProduct({units.meter: 1, units.second: -2})
|
|
778
|
-
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
779
|
-
result = acceleration / velocity
|
|
780
|
-
self.assertIsInstance(result, UnitProduct)
|
|
781
|
-
# (m/s²) / (m/s) = 1/s
|
|
782
|
-
self.assertEqual(result.dimension, Dimension.frequency)
|
|
783
|
-
|
|
784
|
-
def test_rmul_unit_times_unitproduct(self):
|
|
785
|
-
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
786
|
-
result = units.meter * velocity
|
|
787
|
-
self.assertIsInstance(result, UnitProduct)
|
|
788
|
-
|
|
789
|
-
def test_rmul_scale_on_empty_unitproduct(self):
|
|
790
|
-
empty = UnitProduct({})
|
|
791
|
-
result = Scale.kilo * empty
|
|
792
|
-
self.assertIs(result, empty)
|
|
793
|
-
|
|
794
|
-
def test_rmul_scale_applies_to_sink_unit(self):
|
|
795
|
-
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
796
|
-
result = Scale.kilo * velocity
|
|
797
|
-
self.assertIsInstance(result, UnitProduct)
|
|
798
|
-
self.assertIn('km', result.shorthand)
|
|
799
|
-
|
|
800
|
-
def test_rmul_scale_combines_with_existing_scale(self):
|
|
801
|
-
km_per_s = Scale.kilo * UnitProduct({units.meter: 1, units.second: -1})
|
|
802
|
-
# Apply another scale on top → should combine scales
|
|
803
|
-
result = Scale.milli * km_per_s
|
|
804
|
-
self.assertIsInstance(result, UnitProduct)
|
|
805
|
-
|
|
806
|
-
def test_rmul_non_unit_returns_not_implemented(self):
|
|
807
|
-
velocity = UnitProduct({units.meter: 1, units.second: -1})
|
|
808
|
-
result = velocity.__rmul__("string")
|
|
809
|
-
self.assertIs(result, NotImplemented)
|
|
810
|
-
|
|
811
|
-
def test_append_dimensionless_skipped(self):
|
|
812
|
-
# UnitProduct with only dimensionless factor → empty shorthand
|
|
813
|
-
up = UnitProduct({})
|
|
814
|
-
self.assertEqual(up.shorthand, "")
|
|
815
|
-
|
|
816
|
-
def test_shorthand_with_negative_non_unit_exponent(self):
|
|
817
|
-
# e.g. m/s² should show superscript on denominator
|
|
818
|
-
accel = UnitProduct({units.meter: 1, units.second: -2})
|
|
819
|
-
sh = accel.shorthand
|
|
820
|
-
self.assertIn('m', sh)
|
|
821
|
-
self.assertIn('s', sh)
|
|
822
|
-
|
|
823
|
-
def test_shorthand_numerator_exponent(self):
|
|
824
|
-
area = UnitProduct({units.meter: 2})
|
|
825
|
-
self.assertIn('m', area.shorthand)
|
|
826
|
-
# Should contain superscript 2
|
|
827
|
-
self.assertIn('²', area.shorthand)
|