ucon 0.3.2rc6__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 +0 -0
- tests/ucon/test_core.py +535 -0
- tests/ucon/test_dimension.py +206 -0
- tests/ucon/test_unit.py +143 -0
- tests/ucon/test_units.py +21 -0
- ucon/__init__.py +49 -0
- ucon/core.py +353 -0
- ucon/dimension.py +172 -0
- ucon/unit.py +92 -0
- ucon/units.py +84 -0
- ucon-0.3.2rc6.dist-info/METADATA +219 -0
- ucon-0.3.2rc6.dist-info/RECORD +15 -0
- ucon-0.3.2rc6.dist-info/WHEEL +5 -0
- ucon-0.3.2rc6.dist-info/licenses/LICENSE +21 -0
- ucon-0.3.2rc6.dist-info/top_level.txt +2 -0
tests/ucon/__init__.py
ADDED
|
File without changes
|
tests/ucon/test_core.py
ADDED
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from unittest import TestCase
|
|
3
|
+
|
|
4
|
+
from ucon import Number
|
|
5
|
+
from ucon import Exponent
|
|
6
|
+
from ucon import Ratio
|
|
7
|
+
from ucon import Scale
|
|
8
|
+
from ucon import Dimension
|
|
9
|
+
from ucon import units
|
|
10
|
+
from ucon.unit import Unit
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestExponent(TestCase):
|
|
14
|
+
|
|
15
|
+
thousand = Exponent(10, 3)
|
|
16
|
+
thousandth = Exponent(10, -3)
|
|
17
|
+
kibibyte = Exponent(2, 10)
|
|
18
|
+
mebibyte = Exponent(2, 20)
|
|
19
|
+
|
|
20
|
+
def test___init__(self):
|
|
21
|
+
with self.assertRaises(ValueError):
|
|
22
|
+
Exponent(5, 3) # no support for base 5 logarithms
|
|
23
|
+
|
|
24
|
+
def test_parts(self):
|
|
25
|
+
self.assertEqual((10, 3), self.thousand.parts())
|
|
26
|
+
self.assertEqual((10, -3), self.thousandth.parts())
|
|
27
|
+
|
|
28
|
+
def test_evaluated_property(self):
|
|
29
|
+
self.assertEqual(1000, self.thousand.evaluated)
|
|
30
|
+
self.assertAlmostEqual(0.001, self.thousandth.evaluated)
|
|
31
|
+
self.assertEqual(1024, self.kibibyte.evaluated)
|
|
32
|
+
self.assertEqual(1048576, self.mebibyte.evaluated)
|
|
33
|
+
|
|
34
|
+
def test___truediv__(self):
|
|
35
|
+
# same base returns a new Exponent
|
|
36
|
+
ratio = self.thousand / self.thousandth
|
|
37
|
+
self.assertIsInstance(ratio, Exponent)
|
|
38
|
+
self.assertEqual(ratio.base, 10)
|
|
39
|
+
self.assertEqual(ratio.power, 6)
|
|
40
|
+
self.assertEqual(ratio.evaluated, 1_000_000)
|
|
41
|
+
|
|
42
|
+
# different base returns numeric float
|
|
43
|
+
val = self.thousand / self.kibibyte
|
|
44
|
+
self.assertIsInstance(val, float)
|
|
45
|
+
self.assertAlmostEqual(1000 / 1024, val)
|
|
46
|
+
|
|
47
|
+
def test___mul__(self):
|
|
48
|
+
product = self.kibibyte * self.mebibyte
|
|
49
|
+
self.assertIsInstance(product, Exponent)
|
|
50
|
+
self.assertEqual(product.base, 2)
|
|
51
|
+
self.assertEqual(product.power, 30)
|
|
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))
|
|
76
|
+
|
|
77
|
+
with self.assertRaises(TypeError):
|
|
78
|
+
_ = self.thousand == 1000 # comparison to non-Exponent
|
|
79
|
+
|
|
80
|
+
def test___repr__(self):
|
|
81
|
+
self.assertIn("Exponent", repr(Exponent(10, -3)))
|
|
82
|
+
|
|
83
|
+
def test___str__(self):
|
|
84
|
+
self.assertEqual(str(self.thousand), '10^3')
|
|
85
|
+
self.assertEqual(str(self.thousandth), '10^-3')
|
|
86
|
+
|
|
87
|
+
def test_to_base(self):
|
|
88
|
+
e = Exponent(2, 10)
|
|
89
|
+
converted = e.to_base(10)
|
|
90
|
+
self.assertIsInstance(converted, Exponent)
|
|
91
|
+
self.assertEqual(converted.base, 10)
|
|
92
|
+
self.assertAlmostEqual(converted.power, math.log10(1024), places=10)
|
|
93
|
+
|
|
94
|
+
with self.assertRaises(ValueError):
|
|
95
|
+
e.to_base(5)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestScale(TestCase):
|
|
99
|
+
|
|
100
|
+
def test___truediv__(self):
|
|
101
|
+
self.assertEqual(Scale.deca, Scale.one / Scale.deci)
|
|
102
|
+
self.assertEqual(Scale.deci, Scale.one / Scale.deca)
|
|
103
|
+
self.assertEqual(Scale.kibi, Scale.mebi / Scale.kibi)
|
|
104
|
+
self.assertEqual(Scale.milli, Scale.one / Scale.deca / Scale.deca / Scale.deca)
|
|
105
|
+
self.assertEqual(Scale.deca, Scale.kilo / Scale.hecto)
|
|
106
|
+
self.assertEqual(Scale._kibi, Scale.one / Scale.kibi)
|
|
107
|
+
self.assertEqual(Scale.kibi, Scale.kibi / Scale.one)
|
|
108
|
+
self.assertEqual(Scale.one, Scale.one / Scale.one)
|
|
109
|
+
self.assertEqual(Scale.one, Scale.kibi / Scale.kibi)
|
|
110
|
+
self.assertEqual(Scale.one, Scale.kibi / Scale.kilo)
|
|
111
|
+
|
|
112
|
+
def test___lt__(self):
|
|
113
|
+
self.assertLess(Scale.one, Scale.kilo)
|
|
114
|
+
|
|
115
|
+
def test___gt__(self):
|
|
116
|
+
self.assertGreater(Scale.kilo, Scale.one)
|
|
117
|
+
|
|
118
|
+
def test_all(self):
|
|
119
|
+
for scale in Scale:
|
|
120
|
+
self.assertTrue(isinstance(scale.value, Exponent))
|
|
121
|
+
self.assertIsInstance(Scale.all(), dict)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestScaleDivisionAdditional(TestCase):
|
|
125
|
+
|
|
126
|
+
def test_division_same_base_large_gap(self):
|
|
127
|
+
# kilo / milli = mega
|
|
128
|
+
self.assertEqual(Scale.kilo / Scale.milli, Scale.mega)
|
|
129
|
+
# milli / kilo = micro
|
|
130
|
+
self.assertEqual(Scale.milli / Scale.kilo, Scale.micro)
|
|
131
|
+
|
|
132
|
+
def test_division_cross_base_scales(self):
|
|
133
|
+
# Decimal vs binary cross-base — should return nearest matching scale
|
|
134
|
+
result = Scale.kilo / Scale.kibi
|
|
135
|
+
self.assertIsInstance(result, Scale)
|
|
136
|
+
# They’re roughly equal, so nearest should be Scale.one
|
|
137
|
+
self.assertEqual(result, Scale.one)
|
|
138
|
+
|
|
139
|
+
def test_division_binary_inverse_scales(self):
|
|
140
|
+
self.assertEqual(Scale.kibi / Scale.kibi, Scale.one)
|
|
141
|
+
self.assertEqual(Scale.kibi / Scale.mebi, Scale._kibi)
|
|
142
|
+
self.assertEqual(Scale.mebi / Scale.kibi, Scale.kibi)
|
|
143
|
+
|
|
144
|
+
def test_division_unmatched_returns_nearest(self):
|
|
145
|
+
# giga / kibi is a weird combo → nearest mega or similar
|
|
146
|
+
result = Scale.giga / Scale.kibi
|
|
147
|
+
self.assertIsInstance(result, Scale)
|
|
148
|
+
self.assertIn(result, Scale)
|
|
149
|
+
|
|
150
|
+
def test_division_type_safety(self):
|
|
151
|
+
# Ensure non-Scale raises NotImplemented
|
|
152
|
+
with self.assertRaises(TypeError):
|
|
153
|
+
Scale.kilo / 42
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class TestScaleNearestAdditional(TestCase):
|
|
157
|
+
|
|
158
|
+
def test_nearest_handles_zero(self):
|
|
159
|
+
self.assertEqual(Scale.nearest(0), Scale.one)
|
|
160
|
+
|
|
161
|
+
def test_nearest_handles_negative_values(self):
|
|
162
|
+
# Only magnitude matters, not sign
|
|
163
|
+
self.assertEqual(Scale.nearest(-1000), Scale.kilo)
|
|
164
|
+
self.assertEqual(Scale.nearest(-0.001), Scale.milli)
|
|
165
|
+
|
|
166
|
+
def test_nearest_with_undershoot_bias_effect(self):
|
|
167
|
+
# Lower bias should make undershoot (ratios < 1) less penalized
|
|
168
|
+
# This test ensures the bias argument doesn’t break ordering
|
|
169
|
+
s_default = Scale.nearest(50_000, undershoot_bias=0.75)
|
|
170
|
+
s_stronger_bias = Scale.nearest(50_000, undershoot_bias=0.9)
|
|
171
|
+
# The result shouldn't flip to something wildly different
|
|
172
|
+
self.assertIn(s_default, [Scale.kilo, Scale.mega])
|
|
173
|
+
self.assertIn(s_stronger_bias, [Scale.kilo, Scale.mega])
|
|
174
|
+
|
|
175
|
+
def test_nearest_respects_binary_preference_flag(self):
|
|
176
|
+
# Confirm that enabling binary changes candidate set
|
|
177
|
+
decimal_result = Scale.nearest(2**10)
|
|
178
|
+
binary_result = Scale.nearest(2**10, include_binary=True)
|
|
179
|
+
self.assertNotEqual(decimal_result, binary_result)
|
|
180
|
+
self.assertEqual(binary_result, Scale.kibi)
|
|
181
|
+
|
|
182
|
+
def test_nearest_upper_and_lower_extremes(self):
|
|
183
|
+
self.assertEqual(Scale.nearest(10**9), Scale.giga)
|
|
184
|
+
self.assertEqual(Scale.nearest(10**-9), Scale.nano)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class TestScaleInternals(TestCase):
|
|
188
|
+
|
|
189
|
+
def test_decimal_and_binary_sets_are_disjoint(self):
|
|
190
|
+
decimal_bases = {s.value.base for s in Scale._decimal_scales()}
|
|
191
|
+
binary_bases = {s.value.base for s in Scale._binary_scales()}
|
|
192
|
+
self.assertNotEqual(decimal_bases, binary_bases)
|
|
193
|
+
self.assertEqual(decimal_bases, {10})
|
|
194
|
+
self.assertEqual(binary_bases, {2})
|
|
195
|
+
|
|
196
|
+
def test_all_and_by_value_consistency(self):
|
|
197
|
+
mapping = Scale.all()
|
|
198
|
+
value_map = Scale.by_value()
|
|
199
|
+
# Each value’s evaluated form should appear in by_value keys
|
|
200
|
+
for (base, power), name in mapping.items():
|
|
201
|
+
val = Scale[name].value.evaluated
|
|
202
|
+
self.assertIn(round(val, 15), value_map)
|
|
203
|
+
|
|
204
|
+
def test_all_and_by_value_are_cached(self):
|
|
205
|
+
# Call multiple times and ensure they’re same object (cached)
|
|
206
|
+
self.assertIs(Scale.all(), Scale.all())
|
|
207
|
+
self.assertIs(Scale.by_value(), Scale.by_value())
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestNumber(TestCase):
|
|
211
|
+
|
|
212
|
+
number = Number(unit=units.gram, quantity=1)
|
|
213
|
+
|
|
214
|
+
def test_as_ratio(self):
|
|
215
|
+
ratio = self.number.as_ratio()
|
|
216
|
+
self.assertIsInstance(ratio, Ratio)
|
|
217
|
+
self.assertEqual(ratio.numerator, self.number)
|
|
218
|
+
self.assertEqual(ratio.denominator, Number())
|
|
219
|
+
|
|
220
|
+
def test_simplify(self):
|
|
221
|
+
ten_decagrams = Number(unit=units.gram, scale=Scale.deca, quantity=10)
|
|
222
|
+
point_one_decagrams = Number(unit=units.gram, scale=Scale.deca, quantity=0.1)
|
|
223
|
+
two_kibigrams = Number(unit=units.gram, scale=Scale.kibi, quantity=2)
|
|
224
|
+
|
|
225
|
+
self.assertEqual(Number(unit=units.gram, quantity=100), ten_decagrams.simplify())
|
|
226
|
+
self.assertEqual(Number(unit=units.gram, quantity=1), point_one_decagrams.simplify())
|
|
227
|
+
self.assertEqual(Number(unit=units.gram, quantity=2048), two_kibigrams.simplify())
|
|
228
|
+
|
|
229
|
+
def test_to(self):
|
|
230
|
+
thousandth_of_a_kilogram = Number(unit=units.gram, scale=Scale.kilo, quantity=0.001)
|
|
231
|
+
thousand_milligrams = Number(unit=units.gram, scale=Scale.milli, quantity=1000)
|
|
232
|
+
kibigram_fraction = Number(unit=units.gram, scale=Scale.kibi, quantity=0.0009765625)
|
|
233
|
+
|
|
234
|
+
self.assertEqual(thousandth_of_a_kilogram, self.number.to(Scale.kilo))
|
|
235
|
+
self.assertEqual(thousand_milligrams, self.number.to(Scale.milli))
|
|
236
|
+
self.assertEqual(kibigram_fraction, self.number.to(Scale.kibi))
|
|
237
|
+
|
|
238
|
+
def test___repr__(self):
|
|
239
|
+
self.assertIn(str(self.number.quantity), str(self.number))
|
|
240
|
+
self.assertIn(str(self.number.scale.value.evaluated), str(self.number))
|
|
241
|
+
self.assertIn(self.number.unit.name, str(self.number))
|
|
242
|
+
|
|
243
|
+
def test___truediv__(self):
|
|
244
|
+
some_number = Number(unit=units.gram, scale=Scale.deca, quantity=10)
|
|
245
|
+
another_number = Number(unit=units.gram, scale=Scale.milli, quantity=10)
|
|
246
|
+
that_number = Number(unit=units.gram, scale=Scale.kibi, quantity=10)
|
|
247
|
+
|
|
248
|
+
some_quotient = self.number / some_number
|
|
249
|
+
another_quotient = self.number / another_number
|
|
250
|
+
that_quotient = self.number / that_number
|
|
251
|
+
|
|
252
|
+
self.assertEqual(some_quotient.value, 0.01)
|
|
253
|
+
self.assertEqual(another_quotient.value, 100.0)
|
|
254
|
+
self.assertEqual(that_quotient.value, 0.00009765625)
|
|
255
|
+
|
|
256
|
+
def test___eq__(self):
|
|
257
|
+
self.assertEqual(self.number, Ratio(self.number)) # 1 gram / 1
|
|
258
|
+
with self.assertRaises(ValueError):
|
|
259
|
+
self.number == 1
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class TestRatio(TestCase):
|
|
263
|
+
|
|
264
|
+
point_five = Number(quantity=0.5)
|
|
265
|
+
one = Number()
|
|
266
|
+
two = Number(quantity=2)
|
|
267
|
+
three = Number(quantity=3)
|
|
268
|
+
four = Number(quantity=4)
|
|
269
|
+
|
|
270
|
+
one_half = Ratio(numerator=one, denominator=two)
|
|
271
|
+
three_fourths = Ratio(numerator=three, denominator=four)
|
|
272
|
+
one_ratio = Ratio(numerator=one)
|
|
273
|
+
three_halves = Ratio(numerator=three, denominator=two)
|
|
274
|
+
two_ratio = Ratio(numerator=two, denominator=one)
|
|
275
|
+
|
|
276
|
+
def test_evaluate(self):
|
|
277
|
+
self.assertEqual(self.one_ratio.numerator, self.one)
|
|
278
|
+
self.assertEqual(self.one_ratio.denominator, self.one)
|
|
279
|
+
self.assertEqual(self.one_ratio.evaluate(), self.one)
|
|
280
|
+
self.assertEqual(self.two_ratio.evaluate(), self.two)
|
|
281
|
+
|
|
282
|
+
def test_reciprocal(self):
|
|
283
|
+
self.assertEqual(self.two_ratio.reciprocal().numerator, self.one)
|
|
284
|
+
self.assertEqual(self.two_ratio.reciprocal().denominator, self.two)
|
|
285
|
+
self.assertEqual(self.two_ratio.reciprocal().evaluate(), self.point_five)
|
|
286
|
+
|
|
287
|
+
def test___mul__commutivity(self):
|
|
288
|
+
# Does commutivity hold?
|
|
289
|
+
self.assertEqual(self.three_halves * self.one_half, self.three_fourths)
|
|
290
|
+
self.assertEqual(self.one_half * self.three_halves, self.three_fourths)
|
|
291
|
+
|
|
292
|
+
def test___mul__(self):
|
|
293
|
+
bromine_density = Ratio(Number(units.gram, quantity=3.119), Number(units.liter, Scale.milli))
|
|
294
|
+
|
|
295
|
+
# How many grams of bromine are in 2 milliliters?
|
|
296
|
+
two_milliliters_bromine = Number(units.liter, Scale.milli, 2)
|
|
297
|
+
ratio = two_milliliters_bromine.as_ratio() * bromine_density
|
|
298
|
+
answer = ratio.evaluate()
|
|
299
|
+
self.assertEqual(answer.unit.dimension, Dimension.mass)
|
|
300
|
+
self.assertEqual(answer.value, 6.238) # Grams
|
|
301
|
+
|
|
302
|
+
def test___truediv__(self):
|
|
303
|
+
seconds_per_hour = Ratio(
|
|
304
|
+
numerator=Number(unit=units.second, quantity=3600),
|
|
305
|
+
denominator=Number(unit=units.hour, quantity=1)
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# How many Wh from 20 kJ?
|
|
309
|
+
twenty_kilojoules = Number(unit=units.joule, scale=Scale.kilo, quantity=20)
|
|
310
|
+
ratio = twenty_kilojoules.as_ratio() / seconds_per_hour
|
|
311
|
+
answer = ratio.evaluate()
|
|
312
|
+
self.assertEqual(answer.unit.dimension, Dimension.energy)
|
|
313
|
+
self.assertEqual(round(answer.value, 5), 5.55556) # Watt * hours
|
|
314
|
+
|
|
315
|
+
def test___eq__(self):
|
|
316
|
+
self.assertEqual(self.one_half, self.point_five)
|
|
317
|
+
with self.assertRaises(ValueError):
|
|
318
|
+
self.one_half == 1/2
|
|
319
|
+
|
|
320
|
+
def test___repr__(self):
|
|
321
|
+
self.assertEqual(str(self.one_ratio), '<1.0 >')
|
|
322
|
+
self.assertEqual(str(self.two_ratio), '<2 > / <1 >')
|
|
323
|
+
self.assertEqual(str(self.two_ratio.evaluate()), '<2.0 >')
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
class TestExponentEdgeCases(TestCase):
|
|
327
|
+
|
|
328
|
+
def test_extreme_powers(self):
|
|
329
|
+
e = Exponent(10, 308)
|
|
330
|
+
self.assertTrue(math.isfinite(e.evaluated))
|
|
331
|
+
e_small = Exponent(10, -308)
|
|
332
|
+
self.assertGreater(e.evaluated, e_small.evaluated)
|
|
333
|
+
|
|
334
|
+
def test_precision_rounding_in_hash(self):
|
|
335
|
+
a = Exponent(10, 6)
|
|
336
|
+
b = Exponent(10, 6 + 1e-16)
|
|
337
|
+
# rounding in hash avoids floating drift
|
|
338
|
+
self.assertEqual(hash(a), hash(b))
|
|
339
|
+
|
|
340
|
+
def test_negative_and_zero_power(self):
|
|
341
|
+
e0 = Exponent(10, 0)
|
|
342
|
+
e_neg = Exponent(10, -1)
|
|
343
|
+
self.assertEqual(e0.evaluated, 1.0)
|
|
344
|
+
self.assertEqual(e_neg.evaluated, 0.1)
|
|
345
|
+
self.assertLess(e_neg, e0)
|
|
346
|
+
|
|
347
|
+
def test_valid_exponent_evaluates_correctly(self):
|
|
348
|
+
base, power = 10, 3
|
|
349
|
+
e = Exponent(base, power)
|
|
350
|
+
self.assertEqual(e.evaluated, 1000)
|
|
351
|
+
self.assertEqual(e.parts(), (base, power))
|
|
352
|
+
self.assertEqual(f'{base}^{power}', str(e))
|
|
353
|
+
self.assertEqual(f'Exponent(base={base}, power={power})', repr(e))
|
|
354
|
+
|
|
355
|
+
def test_invalid_base_raises_value_error(self):
|
|
356
|
+
with self.assertRaises(ValueError):
|
|
357
|
+
Exponent(5, 2)
|
|
358
|
+
|
|
359
|
+
def test_exponent_comparisons(self):
|
|
360
|
+
e1 = Exponent(10, 2)
|
|
361
|
+
e2 = Exponent(10, 3)
|
|
362
|
+
self.assertTrue(e1 < e2)
|
|
363
|
+
self.assertTrue(e2 > e1)
|
|
364
|
+
self.assertFalse(e1 == e2)
|
|
365
|
+
|
|
366
|
+
def test_division_returns_exponent(self):
|
|
367
|
+
e1 = Exponent(10, 3)
|
|
368
|
+
e2 = Exponent(10, 2)
|
|
369
|
+
self.assertEqual(e1 / e2, Exponent(10, 1))
|
|
370
|
+
|
|
371
|
+
def test_equality_with_different_type(self):
|
|
372
|
+
with self.assertRaises(TypeError):
|
|
373
|
+
Exponent(10, 2) == "10^2"
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
class TestScaleEdgeCases(TestCase):
|
|
377
|
+
|
|
378
|
+
def test_nearest_prefers_decimal_by_default(self):
|
|
379
|
+
self.assertEqual(Scale.nearest(1024), Scale.kilo)
|
|
380
|
+
self.assertEqual(Scale.nearest(50_000), Scale.kilo)
|
|
381
|
+
self.assertEqual(Scale.nearest(1/1024), Scale.milli)
|
|
382
|
+
|
|
383
|
+
def test_nearest_includes_binary_when_opted_in(self):
|
|
384
|
+
self.assertEqual(Scale.nearest(1/1024, include_binary=True), Scale._kibi)
|
|
385
|
+
self.assertEqual(Scale.nearest(1024, include_binary=True), Scale.kibi)
|
|
386
|
+
self.assertEqual(Scale.nearest(50_000, include_binary=True), Scale.kibi)
|
|
387
|
+
self.assertEqual(Scale.nearest(2**20, include_binary=True), Scale.mebi)
|
|
388
|
+
|
|
389
|
+
def test_nearest_subunit_behavior(self):
|
|
390
|
+
self.assertEqual(Scale.nearest(0.0009), Scale.milli)
|
|
391
|
+
self.assertEqual(Scale.nearest(1e-7), Scale.micro)
|
|
392
|
+
|
|
393
|
+
def test_division_same_base_scales(self):
|
|
394
|
+
result = Scale.kilo / Scale.milli
|
|
395
|
+
self.assertIsInstance(result, Scale)
|
|
396
|
+
self.assertEqual(result.value.evaluated, 10 ** 6)
|
|
397
|
+
|
|
398
|
+
def test_division_same_scale_returns_one(self):
|
|
399
|
+
self.assertEqual(Scale.kilo / Scale.kilo, Scale.one)
|
|
400
|
+
|
|
401
|
+
def test_division_different_bases_returns_valid_scale(self):
|
|
402
|
+
result = Scale.kibi / Scale.kilo
|
|
403
|
+
self.assertIsInstance(result, Scale)
|
|
404
|
+
self.assertIn(result, Scale)
|
|
405
|
+
|
|
406
|
+
def test_division_with_one(self):
|
|
407
|
+
result = Scale.one / Scale.kilo
|
|
408
|
+
self.assertIsInstance(result, Scale)
|
|
409
|
+
self.assertTrue(hasattr(result, "value"))
|
|
410
|
+
|
|
411
|
+
def test_comparisons_and_equality(self):
|
|
412
|
+
self.assertTrue(Scale.kilo > Scale.deci)
|
|
413
|
+
self.assertTrue(Scale.milli < Scale.one)
|
|
414
|
+
self.assertTrue(Scale.kilo == Scale.kilo)
|
|
415
|
+
|
|
416
|
+
def test_all_and_by_value_cover_all_enum_members(self):
|
|
417
|
+
all_map = Scale.all()
|
|
418
|
+
by_val = Scale.by_value()
|
|
419
|
+
self.assertTrue(all((val in by_val.values()) for _, val in all_map.items()))
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
class TestNumberEdgeCases(TestCase):
|
|
423
|
+
|
|
424
|
+
def test_default_number_is_dimensionless_one(self):
|
|
425
|
+
n = Number()
|
|
426
|
+
self.assertEqual(n.unit, units.none)
|
|
427
|
+
self.assertEqual(n.scale, Scale.one)
|
|
428
|
+
self.assertEqual(n.quantity, 1)
|
|
429
|
+
self.assertAlmostEqual(n.value, 1.0)
|
|
430
|
+
self.assertIn("1", repr(n))
|
|
431
|
+
|
|
432
|
+
def test_to_new_scale_changes_value(self):
|
|
433
|
+
n = Number(quantity=1000, scale=Scale.kilo)
|
|
434
|
+
converted = n.to(Scale.one)
|
|
435
|
+
self.assertNotEqual(n.value, converted.value)
|
|
436
|
+
self.assertAlmostEqual(converted.value, 1000)
|
|
437
|
+
|
|
438
|
+
def test_simplify_uses_value_as_quantity(self):
|
|
439
|
+
n = Number(quantity=2, scale=Scale.kilo)
|
|
440
|
+
simplified = n.simplify()
|
|
441
|
+
self.assertEqual(simplified.quantity, n.value)
|
|
442
|
+
self.assertEqual(simplified.unit, n.unit)
|
|
443
|
+
|
|
444
|
+
def test_multiplication_combines_units_and_quantities(self):
|
|
445
|
+
n1 = Number(unit=units.joule, quantity=2)
|
|
446
|
+
n2 = Number(unit=units.second, quantity=3)
|
|
447
|
+
result = n1 * n2
|
|
448
|
+
self.assertEqual(result.quantity, 6)
|
|
449
|
+
self.assertEqual(result.unit.dimension, Dimension.energy * Dimension.time)
|
|
450
|
+
|
|
451
|
+
def test_division_combines_units_scales_and_quantities(self):
|
|
452
|
+
n1 = Number(unit=units.meter, scale=Scale.kilo, quantity=1000)
|
|
453
|
+
n2 = Number(unit=units.second, scale=Scale.one, quantity=2)
|
|
454
|
+
result = n1 / n2
|
|
455
|
+
self.assertEqual(result.scale, Scale.kilo / Scale.one)
|
|
456
|
+
self.assertEqual(result.unit.dimension, Dimension.velocity)
|
|
457
|
+
self.assertAlmostEqual(result.quantity, 500)
|
|
458
|
+
|
|
459
|
+
def test_equality_with_non_number_raises_value_error(self):
|
|
460
|
+
n = Number()
|
|
461
|
+
with self.assertRaises(ValueError):
|
|
462
|
+
_ = (n == "5")
|
|
463
|
+
|
|
464
|
+
def test_equality_between_numbers_and_ratios(self):
|
|
465
|
+
n1 = Number(quantity=10)
|
|
466
|
+
n2 = Number(quantity=10)
|
|
467
|
+
r = Ratio(n1, n2)
|
|
468
|
+
self.assertTrue(r == Number())
|
|
469
|
+
|
|
470
|
+
def test_repr_includes_scale_and_unit(self):
|
|
471
|
+
n = Number(unit=units.volt, scale=Scale.kilo, quantity=5)
|
|
472
|
+
rep = repr(n)
|
|
473
|
+
self.assertIn("kilo", rep)
|
|
474
|
+
self.assertIn("volt", rep)
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
class TestRatioEdgeCases(TestCase):
|
|
478
|
+
|
|
479
|
+
def test_default_ratio_is_dimensionless_one(self):
|
|
480
|
+
r = Ratio()
|
|
481
|
+
self.assertEqual(r.numerator.unit, units.none)
|
|
482
|
+
self.assertEqual(r.denominator.unit, units.none)
|
|
483
|
+
self.assertAlmostEqual(r.evaluate().value, 1.0)
|
|
484
|
+
|
|
485
|
+
def test_reciprocal_swaps_numerator_and_denominator(self):
|
|
486
|
+
n1 = Number(quantity=10)
|
|
487
|
+
n2 = Number(quantity=2)
|
|
488
|
+
r = Ratio(n1, n2)
|
|
489
|
+
reciprocal = r.reciprocal()
|
|
490
|
+
self.assertEqual(reciprocal.numerator, r.denominator)
|
|
491
|
+
self.assertEqual(reciprocal.denominator, r.numerator)
|
|
492
|
+
|
|
493
|
+
def test_evaluate_returns_number_division_result(self):
|
|
494
|
+
r = Ratio(Number(unit=units.meter), Number(unit=units.second))
|
|
495
|
+
result = r.evaluate()
|
|
496
|
+
self.assertIsInstance(result, Number)
|
|
497
|
+
self.assertEqual(result.unit.dimension, Dimension.velocity)
|
|
498
|
+
|
|
499
|
+
def test_multiplication_between_compatible_ratios(self):
|
|
500
|
+
r1 = Ratio(Number(unit=units.meter), Number(unit=units.second))
|
|
501
|
+
r2 = Ratio(Number(unit=units.second), Number(unit=units.meter))
|
|
502
|
+
product = r1 * r2
|
|
503
|
+
self.assertIsInstance(product, Ratio)
|
|
504
|
+
self.assertEqual(product.evaluate().unit.dimension, Dimension.none)
|
|
505
|
+
|
|
506
|
+
def test_multiplication_with_incompatible_units_fallback(self):
|
|
507
|
+
r1 = Ratio(Number(unit=units.meter), Number(unit=units.ampere))
|
|
508
|
+
r2 = Ratio(Number(unit=units.ampere), Number(unit=units.meter))
|
|
509
|
+
result = r1 * r2
|
|
510
|
+
self.assertIsInstance(result, Ratio)
|
|
511
|
+
|
|
512
|
+
def test_division_between_ratios_yields_new_ratio(self):
|
|
513
|
+
r1 = Ratio(Number(quantity=2), Number(quantity=1))
|
|
514
|
+
r2 = Ratio(Number(quantity=4), Number(quantity=2))
|
|
515
|
+
result = r1 / r2
|
|
516
|
+
self.assertIsInstance(result, Ratio)
|
|
517
|
+
self.assertAlmostEqual(result.evaluate().value, 1.0)
|
|
518
|
+
|
|
519
|
+
def test_equality_with_non_ratio_raises_value_error(self):
|
|
520
|
+
r = Ratio()
|
|
521
|
+
with self.assertRaises(ValueError):
|
|
522
|
+
_ = (r == "not_a_ratio")
|
|
523
|
+
|
|
524
|
+
def test_repr_handles_equal_numerator_denominator(self):
|
|
525
|
+
r = Ratio()
|
|
526
|
+
self.assertEqual(str(r.evaluate().value), "1.0")
|
|
527
|
+
rep = repr(r)
|
|
528
|
+
self.assertTrue(rep.startswith("<1"))
|
|
529
|
+
|
|
530
|
+
def test_repr_of_non_equal_ratio_includes_slash(self):
|
|
531
|
+
n1 = Number(quantity=2)
|
|
532
|
+
n2 = Number(quantity=1)
|
|
533
|
+
r = Ratio(n1, n2)
|
|
534
|
+
rep = repr(r)
|
|
535
|
+
self.assertIn("/", rep)
|