ucon 0.5.2__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.
@@ -1,615 +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 unittest
6
-
7
- from ucon import units
8
- from ucon.core import UnitProduct, UnitFactor, Dimension, Scale, Unit
9
- from ucon.quantity import Number, Ratio
10
-
11
-
12
- class TestNumber(unittest.TestCase):
13
-
14
- number = Number(unit=units.gram, quantity=1)
15
-
16
- def test_as_ratio(self):
17
- ratio = self.number.as_ratio()
18
- self.assertIsInstance(ratio, Ratio)
19
- self.assertEqual(ratio.numerator, self.number)
20
- self.assertEqual(ratio.denominator, Number())
21
-
22
- def test_simplify_scaled_unit(self):
23
- """Test simplify() removes scale prefix and adjusts quantity."""
24
- decagram = Scale.deca * units.gram
25
- ten_decagrams = Number(unit=decagram, quantity=10)
26
- result = ten_decagrams.simplify()
27
- # 10 decagrams = 100 grams
28
- self.assertAlmostEqual(result.quantity, 100.0, places=10)
29
- # Unit should be base gram (Scale.one)
30
- self.assertEqual(result.unit.shorthand, "g")
31
-
32
- def test_to_converts_between_units(self):
33
- """Test Number.to() converts between compatible units."""
34
- # 1 gram to kilogram
35
- kg = Scale.kilo * units.gram
36
- result = self.number.to(kg)
37
- self.assertAlmostEqual(result.quantity, 0.001, places=10)
38
-
39
- # 1 gram to milligram
40
- mg = Scale.milli * units.gram
41
- result = self.number.to(mg)
42
- self.assertAlmostEqual(result.quantity, 1000.0, places=10)
43
-
44
- # 1 kilogram to gram
45
- one_kg = Number(unit=kg, quantity=1)
46
- result = one_kg.to(units.gram)
47
- self.assertAlmostEqual(result.quantity, 1000.0, places=10)
48
-
49
- def test___repr__(self):
50
- """Test Number repr contains quantity and unit shorthand."""
51
- repr_str = repr(self.number)
52
- self.assertIn(str(self.number.quantity), repr_str)
53
- self.assertIn(self.number.unit.shorthand, repr_str)
54
-
55
- def test___truediv__(self):
56
- dal = Scale.deca * units.gram
57
- mg = Scale.milli * units.gram
58
- kibigram = Scale.kibi * units.gram
59
-
60
- some_number = Number(unit=dal, quantity=10)
61
- another_number = Number(unit=mg, quantity=10)
62
- that_number = Number(unit=kibigram, quantity=10)
63
-
64
- some_quotient = self.number / some_number
65
- another_quotient = self.number / another_number
66
- that_quotient = self.number / that_number
67
-
68
- self.assertEqual(some_quotient.value, 0.01)
69
- self.assertEqual(another_quotient.value, 100.0)
70
- self.assertEqual(that_quotient.value, 0.00009765625)
71
-
72
- def test___eq__(self):
73
- self.assertEqual(self.number, Ratio(self.number)) # 1 gram / 1
74
- with self.assertRaises(TypeError):
75
- self.number == 1
76
-
77
-
78
- class TestNumberEdgeCases(unittest.TestCase):
79
-
80
- def test_density_times_volume_preserves_user_scale(self):
81
- mL = Scale.milli * units.liter
82
- density = Ratio(Number(unit=units.gram, quantity=3.119),
83
- Number(unit=mL, quantity=1))
84
- two_mL = Number(unit=mL, quantity=2)
85
-
86
- result = density.evaluate() * two_mL
87
- self.assertIsInstance(result.unit, UnitProduct)
88
- self.assertDictEqual(result.unit.factors, {units.gram: 1})
89
- self.assertAlmostEqual(result.quantity, 6.238, places=12)
90
-
91
- mg = Scale.milli * units.gram
92
- mg_factor = UnitFactor(unit=units.gram, scale=Scale.milli)
93
- mg_density = Ratio(Number(unit=mg, quantity=3119), Number(unit=mL, quantity=1))
94
-
95
- mg_result = mg_density.evaluate() * two_mL
96
- self.assertIsInstance(mg_result.unit, UnitProduct)
97
- self.assertDictEqual(mg_result.unit.factors, {mg_factor: 1})
98
- self.assertAlmostEqual(mg_result.quantity, 6238, places=12)
99
-
100
- def test_number_mul_asymmetric_density_volume(self):
101
- g = units.gram
102
- mL = Scale.milli * units.liter
103
-
104
- density = Number(unit=g, quantity=3.119) / Number(unit=mL, quantity=1)
105
- two_mL = Number(unit=mL, quantity=2)
106
-
107
- result = density * two_mL
108
-
109
- assert result.unit == g
110
- assert abs(result.quantity - 6.238) < 1e-12
111
-
112
- def test_number_mul_retains_scale_when_scaling_lengths(self):
113
- km = Scale.kilo * units.meter
114
- m = units.meter
115
-
116
- n1 = Number(unit=km, quantity=2) # 2 km
117
- n2 = Number(unit=m, quantity=500) # 500 m
118
-
119
- result = n1 * n2
120
-
121
- assert result.unit.dimension == Dimension.area
122
- # scale stays on unit expression, not folded into numeric
123
- assert "km" in result.unit.shorthand or "m" in result.unit.shorthand
124
-
125
- def test_number_mul_mixed_scales_do_not_auto_cancel(self):
126
- km = Scale.kilo * units.meter
127
- m = units.meter
128
-
129
- result = Number(unit=km, quantity=1) * Number(unit=m, quantity=1)
130
-
131
- # Should remain composite rather than collapsing to base m^2
132
- assert isinstance(result.unit, UnitProduct)
133
- assert "km" in result.unit.shorthand
134
- assert "m" in result.unit.shorthand
135
-
136
- def test_number_div_uses_canonical_rhs_value(self):
137
- dal = Scale.deca * units.gram # 10 g
138
- n = Number(unit=units.gram, quantity=1)
139
-
140
- quotient = n / Number(unit=dal, quantity=10)
141
-
142
- # 1 g / (10 × 10 g) = 0.01
143
- assert abs(quotient.value - 0.01) < 1e-12
144
-
145
- def test_ratio_times_number_preserves_user_scale(self):
146
- mL = Scale.milli * units.liter
147
- density = Ratio(Number(unit=units.gram, quantity=3.119),
148
- Number(unit=mL, quantity=1))
149
- two_mL = Number(unit=mL, quantity=2)
150
-
151
- result = density * two_mL.as_ratio()
152
- evaluated = result.evaluate()
153
-
154
- assert evaluated.unit == units.gram
155
- assert abs(evaluated.quantity - 6.238) < 1e-12
156
-
157
- def test_default_number_is_dimensionless_one(self):
158
- """Default Number() is dimensionless with quantity=1."""
159
- n = Number()
160
- self.assertEqual(n.unit, units.none)
161
- self.assertEqual(n.quantity, 1)
162
- self.assertAlmostEqual(n.value, 1.0)
163
- self.assertIn("1", repr(n))
164
-
165
- def test_to_different_scale_changes_quantity(self):
166
- """Converting to a different scale changes the quantity."""
167
- km = Scale.kilo * units.meter
168
- n = Number(quantity=5, unit=km) # 5 km
169
- converted = n.to(units.meter) # convert to meters
170
- # quantity changes: 5 km = 5000 m
171
- self.assertNotEqual(n.quantity, converted.quantity)
172
- self.assertAlmostEqual(converted.quantity, 5000.0, places=10)
173
-
174
- def test_simplify_uses_value_as_quantity(self):
175
- """Simplify converts scaled quantity to base scale quantity."""
176
- km = Scale.kilo * units.meter
177
- n = Number(quantity=2, unit=km) # 2 km
178
- simplified = n.simplify()
179
- # simplified.quantity should be the canonical magnitude (2 * 1000 = 2000)
180
- self.assertAlmostEqual(simplified.quantity, 2000.0, places=10)
181
- # canonical magnitude (physical quantity) is preserved
182
- self.assertAlmostEqual(simplified._canonical_magnitude, n._canonical_magnitude, places=10)
183
-
184
- def test_multiplication_combines_units_and_quantities(self):
185
- n1 = Number(unit=units.joule, quantity=2)
186
- n2 = Number(unit=units.second, quantity=3)
187
- result = n1 * n2
188
- self.assertEqual(result.quantity, 6)
189
- self.assertEqual(result.unit.dimension, Dimension.energy * Dimension.time)
190
-
191
- def test_division_combines_units_scales_and_quantities(self):
192
- """Division creates composite unit with preserved scales."""
193
- km = Scale.kilo * units.meter
194
- n1 = Number(unit=km, quantity=1000) # 1000 km
195
- n2 = Number(unit=units.second, quantity=2)
196
-
197
- result = n1 / n2 # should yield <500 km/s>
198
-
199
- cu = result.unit
200
- self.assertIsInstance(cu, UnitProduct)
201
-
202
- # --- quantity check ---
203
- self.assertAlmostEqual(result.quantity, 500)
204
-
205
- # --- dimension check ---
206
- self.assertEqual(cu.dimension, Dimension.velocity)
207
-
208
- # --- symbolic shorthand ---
209
- self.assertEqual(cu.shorthand, "km/s")
210
-
211
- # --- convert to base units (m/s) ---
212
- m_per_s = units.meter / units.second
213
- canonical = result.to(m_per_s)
214
- self.assertAlmostEqual(canonical.quantity, 500000, places=5)
215
- self.assertEqual(canonical.unit.shorthand, "m/s")
216
-
217
- def test_equality_with_non_number_raises_value_error(self):
218
- n = Number()
219
- with self.assertRaises(TypeError):
220
- n == '5'
221
-
222
- def test_equality_between_numbers_and_ratios(self):
223
- n1 = Number(quantity=10)
224
- n2 = Number(quantity=10)
225
- r = Ratio(n1, n2)
226
- self.assertTrue(r == Number())
227
-
228
- def test_repr_includes_scale_and_unit(self):
229
- kV = Scale.kilo * Unit(name='volt', dimension=Dimension.voltage, aliases=('V',))
230
- n = Number(unit=kV, quantity=5)
231
- rep = repr(n)
232
- self.assertIn("kV", rep)
233
-
234
-
235
- class TestRatio(unittest.TestCase):
236
-
237
- point_five = Number(quantity=0.5)
238
- one = Number()
239
- two = Number(quantity=2)
240
- three = Number(quantity=3)
241
- four = Number(quantity=4)
242
-
243
- one_half = Ratio(numerator=one, denominator=two)
244
- three_fourths = Ratio(numerator=three, denominator=four)
245
- one_ratio = Ratio(numerator=one)
246
- three_halves = Ratio(numerator=three, denominator=two)
247
- two_ratio = Ratio(numerator=two, denominator=one)
248
-
249
- def test_evaluate(self):
250
- self.assertEqual(self.one_ratio.numerator, self.one)
251
- self.assertEqual(self.one_ratio.denominator, self.one)
252
- self.assertEqual(self.one_ratio.evaluate(), self.one)
253
- self.assertEqual(self.two_ratio.evaluate(), self.two)
254
-
255
- def test_reciprocal(self):
256
- self.assertEqual(self.two_ratio.reciprocal().numerator, self.one)
257
- self.assertEqual(self.two_ratio.reciprocal().denominator, self.two)
258
- self.assertEqual(self.two_ratio.reciprocal().evaluate(), self.point_five)
259
-
260
- def test___mul__commutivity(self):
261
- # Does commutivity hold?
262
- self.assertEqual(self.three_halves * self.one_half, self.three_fourths)
263
- self.assertEqual(self.one_half * self.three_halves, self.three_fourths)
264
-
265
- def test___mul__(self):
266
- mL = Scale.milli * Unit(name='liter', dimension=Dimension.volume, aliases=('L',))
267
- n1 = Number(unit=units.gram, quantity=3.119)
268
- n2 = Number(unit=mL)
269
- bromine_density = Ratio(n1, n2)
270
-
271
- # How many grams of bromine are in 2 milliliters?
272
- two_milliliters_bromine = Number(unit=mL, quantity=2)
273
- ratio = two_milliliters_bromine.as_ratio() * bromine_density
274
- answer = ratio.evaluate()
275
- self.assertEqual(answer.unit.dimension, Dimension.mass)
276
- self.assertEqual(answer.value, 6.238) # Grams
277
-
278
- def test___truediv__(self):
279
- seconds_per_hour = Ratio(
280
- numerator=Number(unit=units.second, quantity=3600),
281
- denominator=Number(unit=units.hour, quantity=1)
282
- )
283
-
284
- # How many Wh from 20 kJ?
285
- twenty_kilojoules = Number(
286
- unit=Scale.kilo * Unit(name='joule', dimension=Dimension.energy, aliases=('J',)),
287
- quantity=20
288
- )
289
- ratio = twenty_kilojoules.as_ratio() / seconds_per_hour
290
- answer = ratio.evaluate()
291
- self.assertEqual(answer.unit.dimension, Dimension.energy)
292
- # When the ConversionGraph is implemented, conversion to watt-hours will be possible.
293
- self.assertEqual(round(answer.value, 5), 0.00556) # kilowatt * hours
294
-
295
- def test___eq__(self):
296
- self.assertEqual(self.one_half, self.point_five)
297
- with self.assertRaises(ValueError):
298
- self.one_half == 1/2
299
-
300
- def test___repr__(self):
301
- self.assertEqual(str(self.one_ratio), '<1.0>')
302
- self.assertEqual(str(self.two_ratio), '<2> / <1.0>')
303
- self.assertEqual(str(self.two_ratio.evaluate()), '<2.0>')
304
-
305
-
306
- class TestRatioEdgeCases(unittest.TestCase):
307
-
308
- def test_default_ratio_is_dimensionless_one(self):
309
- r = Ratio()
310
- self.assertEqual(r.numerator.unit, units.none)
311
- self.assertEqual(r.denominator.unit, units.none)
312
- self.assertAlmostEqual(r.evaluate().value, 1.0)
313
-
314
- def test_reciprocal_swaps_numerator_and_denominator(self):
315
- n1 = Number(quantity=10)
316
- n2 = Number(quantity=2)
317
- r = Ratio(n1, n2)
318
- reciprocal = r.reciprocal()
319
- self.assertEqual(reciprocal.numerator, r.denominator)
320
- self.assertEqual(reciprocal.denominator, r.numerator)
321
-
322
- def test_evaluate_returns_number_division_result(self):
323
- r = Ratio(Number(unit=units.meter), Number(unit=units.second))
324
- result = r.evaluate()
325
- self.assertIsInstance(result, Number)
326
- self.assertEqual(result.unit.dimension, Dimension.velocity)
327
-
328
- def test_multiplication_between_compatible_ratios(self):
329
- r1 = Ratio(Number(unit=units.meter), Number(unit=units.second))
330
- r2 = Ratio(Number(unit=units.second), Number(unit=units.meter))
331
- product = r1 * r2
332
- self.assertIsInstance(product, Ratio)
333
- self.assertEqual(product.evaluate().unit.dimension, Dimension.none)
334
-
335
- def test_multiplication_with_incompatible_units_fallback(self):
336
- r1 = Ratio(Number(unit=units.meter), Number(unit=units.ampere))
337
- r2 = Ratio(Number(unit=units.ampere), Number(unit=units.meter))
338
- result = r1 * r2
339
- self.assertIsInstance(result, Ratio)
340
-
341
- def test_division_between_ratios_yields_new_ratio(self):
342
- r1 = Ratio(Number(quantity=2), Number(quantity=1))
343
- r2 = Ratio(Number(quantity=4), Number(quantity=2))
344
- result = r1 / r2
345
- self.assertIsInstance(result, Ratio)
346
- self.assertAlmostEqual(result.evaluate().value, 1.0)
347
-
348
- def test_equality_with_non_ratio_raises_value_error(self):
349
- r = Ratio()
350
- with self.assertRaises(ValueError):
351
- _ = (r == "not_a_ratio")
352
-
353
- def test_repr_handles_equal_numerator_denominator(self):
354
- r = Ratio()
355
- self.assertEqual(str(r.evaluate().value), "1.0")
356
- rep = repr(r)
357
- self.assertTrue(rep.startswith("<1"))
358
-
359
- def test_repr_of_non_equal_ratio_includes_slash(self):
360
- n1 = Number(quantity=2)
361
- n2 = Number(quantity=1)
362
- r = Ratio(n1, n2)
363
- rep = repr(r)
364
- self.assertIn("/", rep)
365
-
366
-
367
- class TestRatioExponentScaling(unittest.TestCase):
368
- """Tests for Ratio.evaluate() using Exponent-based scaling.
369
-
370
- Ensures Ratio.evaluate() behaves consistently with Number.__truediv__
371
- when units cancel to dimensionless results.
372
- """
373
-
374
- def test_evaluate_dimensionless_with_different_scales(self):
375
- """Ratio of same unit with different scales should fold scales."""
376
- kg = Scale.kilo * units.gram
377
- # 500 g / 1 kg = 0.5 (dimensionless)
378
- ratio = Ratio(units.gram(500), kg(1))
379
- result = ratio.evaluate()
380
- self.assertAlmostEqual(result.quantity, 0.5, places=10)
381
- self.assertEqual(result.unit.dimension, Dimension.none)
382
-
383
- def test_evaluate_matches_number_truediv(self):
384
- """Ratio.evaluate() should match Number.__truediv__ for dimensionless."""
385
- kg = Scale.kilo * units.gram
386
- num = units.gram(500)
387
- den = kg(1)
388
-
389
- ratio_result = Ratio(num, den).evaluate()
390
- truediv_result = num / den
391
-
392
- self.assertAlmostEqual(ratio_result.quantity, truediv_result.quantity, places=10)
393
- self.assertEqual(ratio_result.unit.dimension, truediv_result.unit.dimension)
394
-
395
- def test_evaluate_cross_base_scaling(self):
396
- """Binary and decimal prefixes should combine correctly."""
397
- kibigram = Scale.kibi * units.gram # 1024 g
398
- kg = Scale.kilo * units.gram # 1000 g
399
- # 1 kibigram / 1 kg = 1024/1000 = 1.024
400
- ratio = Ratio(kibigram(1), kg(1))
401
- result = ratio.evaluate()
402
- self.assertAlmostEqual(result.quantity, 1.024, places=10)
403
- self.assertEqual(result.unit.dimension, Dimension.none)
404
-
405
- def test_evaluate_dimensionful_preserves_scales(self):
406
- """Non-cancelling units should preserve symbolic scales."""
407
- km = Scale.kilo * units.meter
408
- # 100 km / 2 h = 50 km/h (scales preserved, not folded)
409
- ratio = Ratio(km(100), units.hour(2))
410
- result = ratio.evaluate()
411
- self.assertAlmostEqual(result.quantity, 50.0, places=10)
412
- self.assertEqual(result.unit.dimension, Dimension.velocity)
413
- self.assertIn("km", result.unit.shorthand)
414
-
415
- def test_evaluate_complex_composition(self):
416
- """Composed ratios should maintain scale semantics."""
417
- mL = Scale.milli * units.liter
418
- # Density: 3.119 g/mL
419
- density = Ratio(units.gram(3.119), mL(1))
420
- # Volume: 2 mL
421
- volume = Ratio(mL(2), Number())
422
- # Mass = density * volume
423
- result = (density * volume).evaluate()
424
- self.assertAlmostEqual(result.quantity, 6.238, places=3)
425
-
426
-
427
- class TestCallableUnits(unittest.TestCase):
428
- """Tests for the callable unit syntax: unit(quantity) -> Number."""
429
-
430
- def test_unit_callable_returns_number(self):
431
- result = units.meter(5)
432
- self.assertIsInstance(result, Number)
433
- self.assertEqual(result.quantity, 5)
434
-
435
- def test_unit_callable_shorthand(self):
436
- result = units.meter(5)
437
- self.assertIn("m", result.unit.shorthand)
438
-
439
- def test_unit_product_callable_returns_number(self):
440
- velocity = units.meter / units.second
441
- result = velocity(10)
442
- self.assertIsInstance(result, Number)
443
- self.assertEqual(result.quantity, 10)
444
- self.assertEqual(result.unit.dimension, Dimension.velocity)
445
-
446
- def test_scaled_unit_callable_returns_number(self):
447
- km = Scale.kilo * units.meter
448
- result = km(5)
449
- self.assertIsInstance(result, Number)
450
- self.assertEqual(result.quantity, 5)
451
- self.assertIn("km", result.unit.shorthand)
452
-
453
- def test_composite_scaled_unit_callable(self):
454
- mph = units.mile / units.hour
455
- result = mph(60)
456
- self.assertIsInstance(result, Number)
457
- self.assertEqual(result.quantity, 60)
458
-
459
-
460
- class TestScaledUnitConversion(unittest.TestCase):
461
- """Tests for conversions involving scaled units.
462
-
463
- Regression tests for bug where scale was applied twice during conversion.
464
- """
465
-
466
- def test_km_to_mile_conversion(self):
467
- """5 km should be approximately 3.10686 miles."""
468
- km = Scale.kilo * units.meter
469
- result = km(5).to(units.mile)
470
- # 5 km = 5000 m = 5000 / 1609.34 miles ≈ 3.10686
471
- self.assertAlmostEqual(result.quantity, 3.10686, places=4)
472
-
473
- def test_km_to_meter_conversion(self):
474
- """1 km should be 1000 meters."""
475
- km = Scale.kilo * units.meter
476
- result = km(1).to(units.meter)
477
- self.assertAlmostEqual(result.quantity, 1000.0, places=6)
478
-
479
- def test_meter_to_mm_conversion(self):
480
- """1 meter should be 1000 millimeters."""
481
- mm = Scale.milli * units.meter
482
- result = units.meter(1).to(mm)
483
- self.assertAlmostEqual(result.quantity, 1000.0, places=6)
484
-
485
- def test_mm_to_inch_conversion(self):
486
- """25.4 mm should be approximately 1 inch."""
487
- mm = Scale.milli * units.meter
488
- result = mm(25.4).to(units.inch)
489
- self.assertAlmostEqual(result.quantity, 1.0, places=4)
490
-
491
- def test_scaled_velocity_conversion(self):
492
- """1 km/h should be approximately 0.27778 m/s."""
493
- km_per_h = (Scale.kilo * units.meter) / units.hour
494
- m_per_s = units.meter / units.second
495
- result = km_per_h(1).to(m_per_s)
496
- # 1 km/h = 1000m / 3600s = 0.27778 m/s
497
- self.assertAlmostEqual(result.quantity, 0.27778, places=4)
498
-
499
- def test_mph_to_m_per_s_conversion(self):
500
- """60 mph should be approximately 26.8224 m/s."""
501
- mph = units.mile / units.hour
502
- m_per_s = units.meter / units.second
503
- result = mph(60).to(m_per_s)
504
- # 60 mph = 60 * 1609.34 / 3600 m/s ≈ 26.8224
505
- self.assertAlmostEqual(result.quantity, 26.8224, places=2)
506
-
507
-
508
- class TestNumberSimplify(unittest.TestCase):
509
- """Tests for Number.simplify() method."""
510
-
511
- def test_simplify_kilo_prefix(self):
512
- """5 km simplifies to 5000 m."""
513
- km = Scale.kilo * units.meter
514
- result = km(5).simplify()
515
- self.assertAlmostEqual(result.quantity, 5000.0, places=10)
516
- self.assertEqual(result.unit.shorthand, "m")
517
-
518
- def test_simplify_milli_prefix(self):
519
- """500 mg simplifies to 0.5 g."""
520
- mg = Scale.milli * units.gram
521
- result = mg(500).simplify()
522
- self.assertAlmostEqual(result.quantity, 0.5, places=10)
523
- self.assertEqual(result.unit.shorthand, "g")
524
-
525
- def test_simplify_binary_prefix(self):
526
- """2 kibibytes simplifies to 2048 bytes."""
527
- kibibyte = Scale.kibi * units.byte
528
- result = kibibyte(2).simplify()
529
- self.assertAlmostEqual(result.quantity, 2048.0, places=10)
530
- self.assertEqual(result.unit.shorthand, "B")
531
-
532
- def test_simplify_composite_unit(self):
533
- """1 km/h simplifies to base scales."""
534
- km_per_h = (Scale.kilo * units.meter) / units.hour
535
- result = km_per_h(1).simplify()
536
- # 1 km/h = 1000 m / 1 h (hour stays hour since it's base unit)
537
- self.assertAlmostEqual(result.quantity, 1000.0, places=10)
538
- self.assertEqual(result.unit.shorthand, "m/h")
539
-
540
- def test_simplify_plain_unit_unchanged(self):
541
- """Plain unit without scale returns equivalent Number."""
542
- result = units.meter(5).simplify()
543
- self.assertAlmostEqual(result.quantity, 5.0, places=10)
544
- self.assertEqual(result.unit.shorthand, "m")
545
-
546
- def test_simplify_preserves_dimension(self):
547
- """Simplified Number has same dimension."""
548
- km = Scale.kilo * units.meter
549
- original = km(5)
550
- simplified = original.simplify()
551
- self.assertEqual(original.unit.dimension, simplified.unit.dimension)
552
-
553
- def test_simplify_idempotent(self):
554
- """Simplifying twice gives same result."""
555
- km = Scale.kilo * units.meter
556
- result1 = km(5).simplify()
557
- result2 = result1.simplify()
558
- self.assertAlmostEqual(result1.quantity, result2.quantity, places=10)
559
- self.assertEqual(result1.unit.shorthand, result2.unit.shorthand)
560
-
561
-
562
- class TestInformationDimension(unittest.TestCase):
563
- """Tests for Dimension.information and information units (bit, byte)."""
564
-
565
- def test_dimension_information_exists(self):
566
- """Dimension.information should be a valid dimension."""
567
- self.assertEqual(Dimension.information.name, 'information')
568
- self.assertNotEqual(Dimension.information, Dimension.none)
569
-
570
- def test_bit_unit_exists(self):
571
- """units.bit should have Dimension.information."""
572
- self.assertEqual(units.bit.dimension, Dimension.information)
573
- self.assertIn('b', units.bit.aliases)
574
-
575
- def test_byte_unit_exists(self):
576
- """units.byte should have Dimension.information."""
577
- self.assertEqual(units.byte.dimension, Dimension.information)
578
- self.assertIn('B', units.byte.aliases)
579
-
580
- def test_byte_to_bit_conversion(self):
581
- """1 byte should be 8 bits."""
582
- result = units.byte(1).to(units.bit)
583
- self.assertAlmostEqual(result.quantity, 8.0, places=10)
584
-
585
- def test_bit_to_byte_conversion(self):
586
- """8 bits should be 1 byte."""
587
- result = units.bit(8).to(units.byte)
588
- self.assertAlmostEqual(result.quantity, 1.0, places=10)
589
-
590
- def test_kibibyte_simplify(self):
591
- """1 kibibyte simplifies to 1024 bytes."""
592
- kibibyte = Scale.kibi * units.byte
593
- result = kibibyte(1).simplify()
594
- self.assertAlmostEqual(result.quantity, 1024.0, places=10)
595
- self.assertEqual(result.unit.shorthand, "B")
596
-
597
- def test_kilobyte_simplify(self):
598
- """1 kilobyte simplifies to 1000 bytes."""
599
- kilobyte = Scale.kilo * units.byte
600
- result = kilobyte(1).simplify()
601
- self.assertAlmostEqual(result.quantity, 1000.0, places=10)
602
- self.assertEqual(result.unit.shorthand, "B")
603
-
604
- def test_data_rate_dimension(self):
605
- """bytes/second should have information/time dimension."""
606
- data_rate = units.byte / units.second
607
- expected_dim = Dimension.information / Dimension.time
608
- self.assertEqual(data_rate.dimension, expected_dim)
609
-
610
- def test_information_orthogonal_to_physical(self):
611
- """Information dimension should be orthogonal to physical dimensions."""
612
- # byte * meter should have both information and length
613
- composite = units.byte * units.meter
614
- self.assertNotEqual(composite.dimension, Dimension.information)
615
- self.assertNotEqual(composite.dimension, Dimension.length)