ucon 0.3.1__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 ADDED
File without changes
@@ -0,0 +1,433 @@
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.kibi / Scale.kibi)
109
+ self.assertEqual(Scale.one, Scale.kibi / Scale.kilo)
110
+
111
+ def test___lt__(self):
112
+ self.assertLess(Scale.one, Scale.kilo)
113
+
114
+ def test___gt__(self):
115
+ self.assertGreater(Scale.kilo, Scale.one)
116
+
117
+ def test_all(self):
118
+ for scale in Scale:
119
+ self.assertTrue(isinstance(scale.value, Exponent))
120
+ self.assertIsInstance(Scale.all(), dict)
121
+
122
+
123
+ class TestNumber(TestCase):
124
+
125
+ number = Number(unit=units.gram, quantity=1)
126
+
127
+ def test_as_ratio(self):
128
+ ratio = self.number.as_ratio()
129
+ self.assertIsInstance(ratio, Ratio)
130
+ self.assertEqual(ratio.numerator, self.number)
131
+ self.assertEqual(ratio.denominator, Number())
132
+
133
+ def test_simplify(self):
134
+ ten_decagrams = Number(unit=units.gram, scale=Scale.deca, quantity=10)
135
+ point_one_decagrams = Number(unit=units.gram, scale=Scale.deca, quantity=0.1)
136
+ two_kibigrams = Number(unit=units.gram, scale=Scale.kibi, quantity=2)
137
+
138
+ self.assertEqual(Number(unit=units.gram, quantity=100), ten_decagrams.simplify())
139
+ self.assertEqual(Number(unit=units.gram, quantity=1), point_one_decagrams.simplify())
140
+ self.assertEqual(Number(unit=units.gram, quantity=2048), two_kibigrams.simplify())
141
+
142
+ def test_to(self):
143
+ thousandth_of_a_kilogram = Number(unit=units.gram, scale=Scale.kilo, quantity=0.001)
144
+ thousand_milligrams = Number(unit=units.gram, scale=Scale.milli, quantity=1000)
145
+ kibigram_fraction = Number(unit=units.gram, scale=Scale.kibi, quantity=0.0009765625)
146
+
147
+ self.assertEqual(thousandth_of_a_kilogram, self.number.to(Scale.kilo))
148
+ self.assertEqual(thousand_milligrams, self.number.to(Scale.milli))
149
+ self.assertEqual(kibigram_fraction, self.number.to(Scale.kibi))
150
+
151
+ def test___repr__(self):
152
+ self.assertIn(str(self.number.quantity), str(self.number))
153
+ self.assertIn(str(self.number.scale.value.evaluated), str(self.number))
154
+ self.assertIn(self.number.unit.name, str(self.number))
155
+
156
+ def test___truediv__(self):
157
+ some_number = Number(unit=units.gram, scale=Scale.deca, quantity=10)
158
+ another_number = Number(unit=units.gram, scale=Scale.milli, quantity=10)
159
+ that_number = Number(unit=units.gram, scale=Scale.kibi, quantity=10)
160
+
161
+ some_quotient = self.number / some_number
162
+ another_quotient = self.number / another_number
163
+ that_quotient = self.number / that_number
164
+
165
+ self.assertEqual(some_quotient.value, 0.01)
166
+ self.assertEqual(another_quotient.value, 100.0)
167
+ self.assertEqual(that_quotient.value, 0.00009765625)
168
+
169
+ def test___eq__(self):
170
+ self.assertEqual(self.number, Ratio(self.number)) # 1 gram / 1
171
+ with self.assertRaises(ValueError):
172
+ self.number == 1
173
+
174
+
175
+ class TestRatio(TestCase):
176
+
177
+ point_five = Number(quantity=0.5)
178
+ one = Number()
179
+ two = Number(quantity=2)
180
+ three = Number(quantity=3)
181
+ four = Number(quantity=4)
182
+
183
+ one_half = Ratio(numerator=one, denominator=two)
184
+ three_fourths = Ratio(numerator=three, denominator=four)
185
+ one_ratio = Ratio(numerator=one)
186
+ three_halves = Ratio(numerator=three, denominator=two)
187
+ two_ratio = Ratio(numerator=two, denominator=one)
188
+
189
+ def test_evaluate(self):
190
+ self.assertEqual(self.one_ratio.numerator, self.one)
191
+ self.assertEqual(self.one_ratio.denominator, self.one)
192
+ self.assertEqual(self.one_ratio.evaluate(), self.one)
193
+ self.assertEqual(self.two_ratio.evaluate(), self.two)
194
+
195
+ def test_reciprocal(self):
196
+ self.assertEqual(self.two_ratio.reciprocal().numerator, self.one)
197
+ self.assertEqual(self.two_ratio.reciprocal().denominator, self.two)
198
+ self.assertEqual(self.two_ratio.reciprocal().evaluate(), self.point_five)
199
+
200
+ def test___mul__commutivity(self):
201
+ # Does commutivity hold?
202
+ self.assertEqual(self.three_halves * self.one_half, self.three_fourths)
203
+ self.assertEqual(self.one_half * self.three_halves, self.three_fourths)
204
+
205
+ def test___mul__(self):
206
+ bromine_density = Ratio(Number(units.gram, quantity=3.119), Number(units.liter, Scale.milli))
207
+
208
+ # How many grams of bromine are in 2 milliliters?
209
+ two_milliliters_bromine = Number(units.liter, Scale.milli, 2)
210
+ ratio = two_milliliters_bromine.as_ratio() * bromine_density
211
+ answer = ratio.evaluate()
212
+ self.assertEqual(answer.unit.dimension, Dimension.mass)
213
+ self.assertEqual(answer.value, 6.238) # Grams
214
+
215
+ def test___truediv__(self):
216
+ seconds_per_hour = Ratio(
217
+ numerator=Number(unit=units.second, quantity=3600),
218
+ denominator=Number(unit=units.hour, quantity=1)
219
+ )
220
+
221
+ # How many Wh from 20 kJ?
222
+ twenty_kilojoules = Number(unit=units.joule, scale=Scale.kilo, quantity=20)
223
+ ratio = twenty_kilojoules.as_ratio() / seconds_per_hour
224
+ answer = ratio.evaluate()
225
+ self.assertEqual(answer.unit.dimension, Dimension.energy)
226
+ self.assertEqual(round(answer.value, 5), 5.55556) # Watt * hours
227
+
228
+ def test___eq__(self):
229
+ self.assertEqual(self.one_half, self.point_five)
230
+ with self.assertRaises(ValueError):
231
+ self.one_half == 1/2
232
+
233
+ def test___repr__(self):
234
+ self.assertEqual(str(self.one_ratio), '<1.0 >')
235
+ self.assertEqual(str(self.two_ratio), '<2 > / <1 >')
236
+ self.assertEqual(str(self.two_ratio.evaluate()), '<2.0 >')
237
+
238
+
239
+ class TestExponentEdgeCases(TestCase):
240
+
241
+ def test_extreme_powers(self):
242
+ e = Exponent(10, 308)
243
+ self.assertTrue(math.isfinite(e.evaluated))
244
+ e_small = Exponent(10, -308)
245
+ self.assertGreater(e.evaluated, e_small.evaluated)
246
+
247
+ def test_precision_rounding_in_hash(self):
248
+ a = Exponent(10, 6)
249
+ b = Exponent(10, 6 + 1e-16)
250
+ # rounding in hash avoids floating drift
251
+ self.assertEqual(hash(a), hash(b))
252
+
253
+ def test_negative_and_zero_power(self):
254
+ e0 = Exponent(10, 0)
255
+ e_neg = Exponent(10, -1)
256
+ self.assertEqual(e0.evaluated, 1.0)
257
+ self.assertEqual(e_neg.evaluated, 0.1)
258
+ self.assertLess(e_neg, e0)
259
+
260
+ def test_valid_exponent_evaluates_correctly(self):
261
+ base, power = 10, 3
262
+ e = Exponent(base, power)
263
+ self.assertEqual(e.evaluated, 1000)
264
+ self.assertEqual(e.parts(), (base, power))
265
+ self.assertEqual(f'{base}^{power}', str(e))
266
+ self.assertEqual(f'Exponent(base={base}, power={power})', repr(e))
267
+
268
+ def test_invalid_base_raises_value_error(self):
269
+ with self.assertRaises(ValueError):
270
+ Exponent(5, 2)
271
+
272
+ def test_exponent_comparisons(self):
273
+ e1 = Exponent(10, 2)
274
+ e2 = Exponent(10, 3)
275
+ self.assertTrue(e1 < e2)
276
+ self.assertTrue(e2 > e1)
277
+ self.assertFalse(e1 == e2)
278
+
279
+ def test_division_returns_exponent(self):
280
+ e1 = Exponent(10, 3)
281
+ e2 = Exponent(10, 2)
282
+ self.assertEqual(e1 / e2, Exponent(10, 1))
283
+
284
+ def test_equality_with_different_type(self):
285
+ with self.assertRaises(TypeError):
286
+ Exponent(10, 2) == "10^2"
287
+
288
+
289
+ class TestScaleEdgeCases(TestCase):
290
+
291
+ def test_division_same_base_scales(self):
292
+ result = Scale.kilo / Scale.milli
293
+ self.assertIsInstance(result, Scale)
294
+ self.assertEqual(result.value.evaluated, 10 ** 6)
295
+
296
+ def test_division_same_scale_returns_one(self):
297
+ self.assertEqual(Scale.kilo / Scale.kilo, Scale.one)
298
+
299
+ def test_division_different_bases_returns_valid_scale(self):
300
+ result = Scale.kibi / Scale.kilo
301
+ self.assertIsInstance(result, Scale)
302
+ self.assertIn(result, Scale)
303
+
304
+ def test_division_with_one(self):
305
+ result = Scale.one / Scale.kilo
306
+ self.assertIsInstance(result, Scale)
307
+ self.assertTrue(hasattr(result, "value"))
308
+
309
+ def test_comparisons_and_equality(self):
310
+ self.assertTrue(Scale.kilo > Scale.deci)
311
+ self.assertTrue(Scale.milli < Scale.one)
312
+ self.assertTrue(Scale.kilo == Scale.kilo)
313
+
314
+ def test_all_and_by_value_cover_all_enum_members(self):
315
+ all_map = Scale.all()
316
+ by_val = Scale.by_value()
317
+ self.assertTrue(all((val in by_val.values()) for _, val in all_map.items()))
318
+
319
+
320
+ class TestNumberEdgeCases(TestCase):
321
+
322
+ def test_default_number_is_dimensionless_one(self):
323
+ n = Number()
324
+ self.assertEqual(n.unit, units.none)
325
+ self.assertEqual(n.scale, Scale.one)
326
+ self.assertEqual(n.quantity, 1)
327
+ self.assertAlmostEqual(n.value, 1.0)
328
+ self.assertIn("1", repr(n))
329
+
330
+ def test_to_new_scale_changes_value(self):
331
+ n = Number(quantity=1000, scale=Scale.kilo)
332
+ converted = n.to(Scale.one)
333
+ self.assertNotEqual(n.value, converted.value)
334
+ self.assertAlmostEqual(converted.value, 1000)
335
+
336
+ def test_simplify_uses_value_as_quantity(self):
337
+ n = Number(quantity=2, scale=Scale.kilo)
338
+ simplified = n.simplify()
339
+ self.assertEqual(simplified.quantity, n.value)
340
+ self.assertEqual(simplified.unit, n.unit)
341
+
342
+ def test_multiplication_combines_units_and_quantities(self):
343
+ n1 = Number(unit=units.joule, quantity=2)
344
+ n2 = Number(unit=units.second, quantity=3)
345
+ result = n1 * n2
346
+ self.assertEqual(result.quantity, 6)
347
+ self.assertEqual(result.unit.dimension, Dimension.energy * Dimension.time)
348
+
349
+ def test_division_combines_units_scales_and_quantities(self):
350
+ n1 = Number(unit=units.meter, scale=Scale.kilo, quantity=1000)
351
+ n2 = Number(unit=units.second, scale=Scale.one, quantity=2)
352
+ result = n1 / n2
353
+ self.assertEqual(result.scale, Scale.kilo / Scale.one)
354
+ self.assertEqual(result.unit.dimension, Dimension.velocity)
355
+ self.assertAlmostEqual(result.quantity, 500)
356
+
357
+ def test_equality_with_non_number_raises_value_error(self):
358
+ n = Number()
359
+ with self.assertRaises(ValueError):
360
+ _ = (n == "5")
361
+
362
+ def test_equality_between_numbers_and_ratios(self):
363
+ n1 = Number(quantity=10)
364
+ n2 = Number(quantity=10)
365
+ r = Ratio(n1, n2)
366
+ self.assertTrue(r == Number())
367
+
368
+ def test_repr_includes_scale_and_unit(self):
369
+ n = Number(unit=units.volt, scale=Scale.kilo, quantity=5)
370
+ rep = repr(n)
371
+ self.assertIn("kilo", rep)
372
+ self.assertIn("volt", rep)
373
+
374
+
375
+ class TestRatioEdgeCases(TestCase):
376
+
377
+ def test_default_ratio_is_dimensionless_one(self):
378
+ r = Ratio()
379
+ self.assertEqual(r.numerator.unit, units.none)
380
+ self.assertEqual(r.denominator.unit, units.none)
381
+ self.assertAlmostEqual(r.evaluate().value, 1.0)
382
+
383
+ def test_reciprocal_swaps_numerator_and_denominator(self):
384
+ n1 = Number(quantity=10)
385
+ n2 = Number(quantity=2)
386
+ r = Ratio(n1, n2)
387
+ reciprocal = r.reciprocal()
388
+ self.assertEqual(reciprocal.numerator, r.denominator)
389
+ self.assertEqual(reciprocal.denominator, r.numerator)
390
+
391
+ def test_evaluate_returns_number_division_result(self):
392
+ r = Ratio(Number(unit=units.meter), Number(unit=units.second))
393
+ result = r.evaluate()
394
+ self.assertIsInstance(result, Number)
395
+ self.assertEqual(result.unit.dimension, Dimension.velocity)
396
+
397
+ def test_multiplication_between_compatible_ratios(self):
398
+ r1 = Ratio(Number(unit=units.meter), Number(unit=units.second))
399
+ r2 = Ratio(Number(unit=units.second), Number(unit=units.meter))
400
+ product = r1 * r2
401
+ self.assertIsInstance(product, Ratio)
402
+ self.assertEqual(product.evaluate().unit.dimension, Dimension.none)
403
+
404
+ def test_multiplication_with_incompatible_units_fallback(self):
405
+ r1 = Ratio(Number(unit=units.meter), Number(unit=units.ampere))
406
+ r2 = Ratio(Number(unit=units.ampere), Number(unit=units.meter))
407
+ result = r1 * r2
408
+ self.assertIsInstance(result, Ratio)
409
+
410
+ def test_division_between_ratios_yields_new_ratio(self):
411
+ r1 = Ratio(Number(quantity=2), Number(quantity=1))
412
+ r2 = Ratio(Number(quantity=4), Number(quantity=2))
413
+ result = r1 / r2
414
+ self.assertIsInstance(result, Ratio)
415
+ self.assertAlmostEqual(result.evaluate().value, 1.0)
416
+
417
+ def test_equality_with_non_ratio_raises_value_error(self):
418
+ r = Ratio()
419
+ with self.assertRaises(ValueError):
420
+ _ = (r == "not_a_ratio")
421
+
422
+ def test_repr_handles_equal_numerator_denominator(self):
423
+ r = Ratio()
424
+ self.assertEqual(str(r.evaluate().value), "1.0")
425
+ rep = repr(r)
426
+ self.assertTrue(rep.startswith("<1"))
427
+
428
+ def test_repr_of_non_equal_ratio_includes_slash(self):
429
+ n1 = Number(quantity=2)
430
+ n2 = Number(quantity=1)
431
+ r = Ratio(n1, n2)
432
+ rep = repr(r)
433
+ self.assertIn("/", rep)
@@ -0,0 +1,206 @@
1
+ import unittest
2
+ from ucon.dimension import Vector, Dimension
3
+
4
+
5
+ class TestVector(unittest.TestCase):
6
+
7
+ def test_vector_iteration_and_length(self):
8
+ v = Vector(1, 0, 0, 0, 0, 0, 0)
9
+ self.assertEqual(tuple(v), (1, 0, 0, 0, 0, 0, 0))
10
+ self.assertEqual(len(v), 7) # always 7 components
11
+
12
+ def test_vector_addition(self):
13
+ v1 = Vector(1, 0, 0, 0, 0, 0, 0)
14
+ v2 = Vector(0, 2, 0, 0, 0, 0, 0)
15
+ result = v1 + v2
16
+ self.assertEqual(result, Vector(1, 2, 0, 0, 0, 0, 0))
17
+
18
+ def test_vector_subtraction(self):
19
+ v1 = Vector(2, 1, 0, 0, 0, 0, 0)
20
+ v2 = Vector(1, 1, 0, 0, 0, 0, 0)
21
+ self.assertEqual(v1 - v2, Vector(1, 0, 0, 0, 0, 0, 0))
22
+
23
+ def test_vector_equality_and_hash(self):
24
+ v1 = Vector(1, 0, 0, 0, 0, 0, 0)
25
+ v2 = Vector(1, 0, 0, 0, 0, 0, 0)
26
+ v3 = Vector(0, 1, 0, 0, 0, 0, 0)
27
+ self.assertTrue(v1 == v2)
28
+ self.assertFalse(v1 == v3)
29
+ self.assertEqual(hash(v1), hash(v2))
30
+ self.assertNotEqual(hash(v1), hash(v3))
31
+
32
+
33
+ class TestDimension(unittest.TestCase):
34
+
35
+ def test_basic_dimensions_are_unique(self):
36
+ seen = set()
37
+ for dim in Dimension:
38
+ self.assertNotIn(dim.value, seen, f'Duplicate vector found for {dim.name}')
39
+ seen.add(dim.value)
40
+
41
+ def test_multiplication_adds_exponents(self):
42
+ self.assertEqual(
43
+ Dimension.mass * Dimension.acceleration,
44
+ Dimension.force,
45
+ )
46
+ self.assertEqual(
47
+ Dimension.length * Dimension.length,
48
+ Dimension.area,
49
+ )
50
+ self.assertEqual(
51
+ Dimension.length * Dimension.length * Dimension.length,
52
+ Dimension.volume,
53
+ )
54
+
55
+ def test_division_subtracts_exponents(self):
56
+ self.assertEqual(
57
+ Dimension.length / Dimension.time,
58
+ Dimension.velocity,
59
+ )
60
+ self.assertEqual(
61
+ Dimension.force / Dimension.area,
62
+ Dimension.pressure,
63
+ )
64
+
65
+ def test_none_dimension_behaves_neutrally(self):
66
+ base = Dimension.mass
67
+ self.assertEqual(base * Dimension.none, base)
68
+ self.assertEqual(base / Dimension.none, base)
69
+ self.assertEqual(Dimension.none * base, base)
70
+ with self.assertRaises(ValueError) as exc:
71
+ Dimension.none / base
72
+ assert type(exc.exception) == ValueError
73
+ assert str(exc.exception).endswith('is not a valid Dimension')
74
+
75
+ def test_hash_and_equality_consistency(self):
76
+ d1 = Dimension.mass
77
+ d2 = Dimension.mass
78
+ d3 = Dimension.length
79
+ self.assertEqual(d1, d2)
80
+ self.assertNotEqual(d1, d3)
81
+ self.assertEqual(hash(d1), hash(d2))
82
+ self.assertNotEqual(hash(d1), hash(d3))
83
+
84
+ def test_composite_quantities_examples(self):
85
+ # Energy = Force * Length
86
+ self.assertEqual(
87
+ Dimension.force * Dimension.length,
88
+ Dimension.energy,
89
+ )
90
+ # Power = Energy / Time
91
+ self.assertEqual(
92
+ Dimension.energy / Dimension.time,
93
+ Dimension.power,
94
+ )
95
+ # Pressure = Force / Area
96
+ self.assertEqual(
97
+ Dimension.force / Dimension.area,
98
+ Dimension.pressure,
99
+ )
100
+ # Charge = Current * Time
101
+ self.assertEqual(
102
+ Dimension.current * Dimension.time,
103
+ Dimension.charge,
104
+ )
105
+
106
+ def test_vector_equality_reflects_dimension_equality(self):
107
+ self.assertEqual(Dimension.mass.value, Dimension.mass.value)
108
+ self.assertNotEqual(Dimension.mass.value, Dimension.time.value)
109
+ self.assertEqual(Dimension.mass, Dimension.mass)
110
+ self.assertNotEqual(Dimension.mass, Dimension.time)
111
+
112
+
113
+ class TestVectorEdgeCases(unittest.TestCase):
114
+
115
+ def test_zero_vector_equality_and_additivity(self):
116
+ zero = Vector()
117
+ self.assertEqual(zero, Vector(0, 0, 0, 0, 0, 0, 0))
118
+ # Adding or subtracting zero should yield same vector
119
+ v = Vector(1, 2, 3, 4, 5, 6, 7)
120
+ self.assertEqual(v + zero, v)
121
+ self.assertEqual(v - zero, v)
122
+
123
+ def test_vector_with_negative_exponents(self):
124
+ v1 = Vector(1, -2, 3, 0, 0, 0, 0)
125
+ v2 = Vector(-1, 2, -3, 0, 0, 0, 0)
126
+ result = v1 + v2
127
+ self.assertEqual(result, Vector(0, 0, 0, 0, 0, 0, 0))
128
+ self.assertEqual(v1 - v1, Vector()) # perfect cancellation
129
+
130
+ def test_vector_equality_with_non_vector(self):
131
+ v = Vector()
132
+ with self.assertRaises(AssertionError):
133
+ v == "not a vector"
134
+ with self.assertRaises(AssertionError):
135
+ v == None
136
+
137
+ def test_hash_consistency_for_equal_vectors(self):
138
+ v1 = Vector(1, 0, 0, 0, 0, 0, 0)
139
+ v2 = Vector(1, 0, 0, 0, 0, 0, 0)
140
+ self.assertEqual(hash(v1), hash(v2))
141
+ self.assertEqual(len({v1, v2}), 1)
142
+
143
+ def test_iter_length_order_consistency(self):
144
+ v = Vector(1, 2, 3, 4, 5, 6, 7)
145
+ components = list(v)
146
+ self.assertEqual(len(components), len(v))
147
+ # Ensure order of iteration is fixed (T→L→M→I→Θ→J→N)
148
+ self.assertEqual(components, [1, 2, 3, 4, 5, 6, 7])
149
+
150
+ def test_vector_arithmetic_does_not_mutate_operands(self):
151
+ v1 = Vector(1, 0, 0, 0, 0, 0, 0)
152
+ v2 = Vector(0, 1, 0, 0, 0, 0, 0)
153
+ _ = v1 + v2
154
+ self.assertEqual(v1, Vector(1, 0, 0, 0, 0, 0, 0))
155
+ self.assertEqual(v2, Vector(0, 1, 0, 0, 0, 0, 0))
156
+
157
+ def test_invalid_addition_type_raises(self):
158
+ v = Vector(1, 0, 0, 0, 0, 0, 0)
159
+ with self.assertRaises(TypeError):
160
+ _ = v + "length"
161
+ with self.assertRaises(TypeError):
162
+ _ = v - 5
163
+
164
+
165
+ class TestDimensionEdgeCases(unittest.TestCase):
166
+
167
+ def test_invalid_multiplication_type(self):
168
+ with self.assertRaises(TypeError):
169
+ Dimension.length * 5
170
+ with self.assertRaises(TypeError):
171
+ "mass" * Dimension.time
172
+
173
+ def test_invalid_division_type(self):
174
+ with self.assertRaises(TypeError):
175
+ Dimension.time / "length"
176
+ with self.assertRaises(TypeError):
177
+ 5 / Dimension.mass
178
+
179
+ def test_equality_with_non_dimension(self):
180
+ with self.assertRaises(TypeError):
181
+ Dimension.mass == "mass"
182
+
183
+ def test_enum_uniqueness_and_hash(self):
184
+ # Hashes should be unique per distinct dimension
185
+ hashes = {hash(d) for d in Dimension}
186
+ self.assertEqual(len(hashes), len(Dimension))
187
+ # All Dimension.value entries must be distinct Vectors
188
+ values = [d.value for d in Dimension]
189
+ self.assertEqual(len(values), len(set(values)))
190
+
191
+ def test_combined_chained_operations(self):
192
+ # (mass * acceleration) / area = pressure
193
+ result = (Dimension.mass * Dimension.acceleration) / Dimension.area
194
+ self.assertEqual(result, Dimension.pressure)
195
+
196
+ def test_dimension_round_trip_equality(self):
197
+ # Multiplying and dividing by the same dimension returns self
198
+ d = Dimension.energy
199
+ self.assertEqual((d * Dimension.none) / Dimension.none, d)
200
+ self.assertEqual(d / Dimension.none, d)
201
+ self.assertEqual(Dimension.none * d, d)
202
+
203
+ def test_enum_is_hashable_and_iterable(self):
204
+ seen = {d for d in Dimension}
205
+ self.assertIn(Dimension.mass, seen)
206
+ self.assertEqual(len(seen), len(Dimension))