unitx 0.6.1__tar.gz → 0.6.2__tar.gz

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.
Files changed (45) hide show
  1. {unitx-0.6.1 → unitx-0.6.2}/CHANGELOG.md +13 -0
  2. {unitx-0.6.1 → unitx-0.6.2}/PKG-INFO +31 -2
  3. {unitx-0.6.1 → unitx-0.6.2}/README.md +30 -1
  4. {unitx-0.6.1 → unitx-0.6.2}/pyproject.toml +1 -1
  5. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/factor.py +5 -1
  6. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/quantity.py +31 -22
  7. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/unit.py +8 -5
  8. {unitx-0.6.1 → unitx-0.6.2}/tests/test_quantity.py +56 -3
  9. {unitx-0.6.1 → unitx-0.6.2}/tests/test_si.py +5 -0
  10. {unitx-0.6.1 → unitx-0.6.2}/tests/test_unit.py +6 -1
  11. {unitx-0.6.1 → unitx-0.6.2}/tests/typing/check_quantity.py +13 -0
  12. {unitx-0.6.1 → unitx-0.6.2}/tests/typing/check_si.py +1 -0
  13. {unitx-0.6.1 → unitx-0.6.2}/.github/workflows/ci.yml +0 -0
  14. {unitx-0.6.1 → unitx-0.6.2}/.github/workflows/release.yml +0 -0
  15. {unitx-0.6.1 → unitx-0.6.2}/.gitignore +0 -0
  16. {unitx-0.6.1 → unitx-0.6.2}/LICENSE +0 -0
  17. {unitx-0.6.1 → unitx-0.6.2}/conftest.py +0 -0
  18. {unitx-0.6.1 → unitx-0.6.2}/pyrightconfig.json +0 -0
  19. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/__init__.py +0 -0
  20. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/_constclass.py +0 -0
  21. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/_numerics.py +0 -0
  22. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/_unicode.py +0 -0
  23. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/_unit_data.py +0 -0
  24. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/dimension.py +0 -0
  25. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/exceptions.py +0 -0
  26. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/monomial.py +0 -0
  27. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/py.typed +0 -0
  28. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/si/__init__.py +0 -0
  29. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/si/constants.py +0 -0
  30. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/si/units.py +0 -0
  31. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/singleunit.py +0 -0
  32. {unitx-0.6.1 → unitx-0.6.2}/src/unitx/unitparser.py +0 -0
  33. {unitx-0.6.1 → unitx-0.6.2}/tests/__init__.py +0 -0
  34. {unitx-0.6.1 → unitx-0.6.2}/tests/test_dimension.py +0 -0
  35. {unitx-0.6.1 → unitx-0.6.2}/tests/test_factor.py +0 -0
  36. {unitx-0.6.1 → unitx-0.6.2}/tests/test_import.py +0 -0
  37. {unitx-0.6.1 → unitx-0.6.2}/tests/test_monomial.py +0 -0
  38. {unitx-0.6.1 → unitx-0.6.2}/tests/test_numeric.py +0 -0
  39. {unitx-0.6.1 → unitx-0.6.2}/tests/test_singleunit.py +0 -0
  40. {unitx-0.6.1 → unitx-0.6.2}/tests/test_typing.py +0 -0
  41. {unitx-0.6.1 → unitx-0.6.2}/tests/test_unicode.py +0 -0
  42. {unitx-0.6.1 → unitx-0.6.2}/tests/test_unit_data.py +0 -0
  43. {unitx-0.6.1 → unitx-0.6.2}/tests/test_unit_definition.py +0 -0
  44. {unitx-0.6.1 → unitx-0.6.2}/tests/test_unitparser.py +0 -0
  45. {unitx-0.6.1 → unitx-0.6.2}/tests/test_warnings.py +0 -0
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.6.2] - 2026-10-18
4
+
5
+ ### Added
6
+
7
+ - Support multiplication and division between Quantity and Factor or SymbolicConstant in both operand orders. Multiplication and right-hand division preserve the magnitude and its type, combining the exact factor with the unit's scale; left-hand division takes the reciprocal magnitude and scales the reciprocal unit. Ordinary numeric operands continue to operate on the magnitude.
8
+ - Preserve generic magnitude types in the corresponding public type hints, with runtime and static regression coverage for exact scaling and reciprocal operations.
9
+
10
+ ### Changed
11
+
12
+ - Combine the compact exact scale and formal units in ScaledUnit string output, grouping denominator factors into one product, such as `J⋅s/(2⋅π)`.
13
+ - Display quantities with scaled units using the combined product and multiplication dots between adjacent numeric factors, such as `3⋅60⋅s`, without evaluating the exact scale. String output remains readable display rather than parser input; `repr()` retains reconstructible state.
14
+ - Document exact-factor quantity arithmetic and the distinction between a symbolic expression such as `SI.h / (2 * PI)` and predefined `SI.hbar`, which continues to expose its numerical value in ordinary `J*s` units.
15
+
3
16
  ## [0.6.1] - 2026-10-17
4
17
 
5
18
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: unitx
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: A modern, intuitive Python library for units, quantities, and dimensional analysis.
5
5
  Project-URL: Homepage, https://github.com/DaitTHU/unitx
6
6
  Project-URL: Issues, https://github.com/DaitTHU/unitx/issues
@@ -146,7 +146,7 @@ True
146
146
  2
147
147
  >>> double_metre = metre * Factor(2)
148
148
  >>> print(double_metre)
149
- 2 m
149
+ 2⋅m
150
150
  >>> (3 * double_metre).to('m').value
151
151
  6
152
152
  >>> arcmin = si.deg / 60
@@ -161,6 +161,35 @@ Multiplying a `ScaledUnit` by an ordinary number also constructs a quantity,
161
161
  keeping the exact scale in its unit and preserving the supplied magnitude.
162
162
  Combining a Unit with a Quantity continues to return a Quantity.
163
163
 
164
+ Quantities also accept `Factor` and `SymbolicConstant` operands on either side
165
+ of multiplication or division. Multiplication and right-hand division preserve
166
+ the magnitude and its type, combining the exact factor with the unit instead.
167
+ Left-hand division takes the reciprocal magnitude and unit. Ordinary numeric
168
+ operands continue to act on the magnitude: `q * 2` and `q * Factor(2)` represent
169
+ equal physical quantities but can have different `.value` and `.unit` pairs.
170
+
171
+ ```pycon
172
+ >>> from unitx.factor import PI
173
+ >>> hbar = SI.h / (2 * PI)
174
+ >>> print(hbar)
175
+ 6.62607015E-34 J⋅s/(2⋅π)
176
+ >>> hbar.value == SI.h.value
177
+ True
178
+ >>> hbar * (2 * PI) == SI.h
179
+ True
180
+ >>> round(float(hbar.to('J*s').value) / 1e-34, 8)
181
+ 1.05457182
182
+
183
+ ```
184
+
185
+ The local `hbar` above retains the exact scale, whereas predefined `SI.hbar`
186
+ provides its numerical value in ordinary `J*s` units. `.value` is always the
187
+ magnitude paired with `.unit`, including its exact scale. Use
188
+ `.to('J*s').value` to evaluate the local expression in joule seconds;
189
+ conversion involving an irrational factor evaluates it approximately.
190
+ String output combines the scale and formal units into one product, with
191
+ grouped denominator factors and multiplication dots between adjacent numbers.
192
+
164
193
  ## Conversion and arithmetic
165
194
 
166
195
  Use `.to()` to convert between units with the same physical dimensions.
@@ -124,7 +124,7 @@ True
124
124
  2
125
125
  >>> double_metre = metre * Factor(2)
126
126
  >>> print(double_metre)
127
- 2 m
127
+ 2⋅m
128
128
  >>> (3 * double_metre).to('m').value
129
129
  6
130
130
  >>> arcmin = si.deg / 60
@@ -139,6 +139,35 @@ Multiplying a `ScaledUnit` by an ordinary number also constructs a quantity,
139
139
  keeping the exact scale in its unit and preserving the supplied magnitude.
140
140
  Combining a Unit with a Quantity continues to return a Quantity.
141
141
 
142
+ Quantities also accept `Factor` and `SymbolicConstant` operands on either side
143
+ of multiplication or division. Multiplication and right-hand division preserve
144
+ the magnitude and its type, combining the exact factor with the unit instead.
145
+ Left-hand division takes the reciprocal magnitude and unit. Ordinary numeric
146
+ operands continue to act on the magnitude: `q * 2` and `q * Factor(2)` represent
147
+ equal physical quantities but can have different `.value` and `.unit` pairs.
148
+
149
+ ```pycon
150
+ >>> from unitx.factor import PI
151
+ >>> hbar = SI.h / (2 * PI)
152
+ >>> print(hbar)
153
+ 6.62607015E-34 J⋅s/(2⋅π)
154
+ >>> hbar.value == SI.h.value
155
+ True
156
+ >>> hbar * (2 * PI) == SI.h
157
+ True
158
+ >>> round(float(hbar.to('J*s').value) / 1e-34, 8)
159
+ 1.05457182
160
+
161
+ ```
162
+
163
+ The local `hbar` above retains the exact scale, whereas predefined `SI.hbar`
164
+ provides its numerical value in ordinary `J*s` units. `.value` is always the
165
+ magnitude paired with `.unit`, including its exact scale. Use
166
+ `.to('J*s').value` to evaluate the local expression in joule seconds;
167
+ conversion involving an irrational factor evaluates it approximately.
168
+ String output combines the scale and formal units into one product, with
169
+ grouped denominator factors and multiplication dots between adjacent numbers.
170
+
142
171
  ## Conversion and arithmetic
143
172
 
144
173
  Use `.to()` to convert between units with the same physical dimensions.
@@ -4,7 +4,7 @@ build-backend = 'hatchling.build'
4
4
 
5
5
  [project]
6
6
  name = 'unitx'
7
- version = '0.6.1'
7
+ version = '0.6.2'
8
8
  description = 'A modern, intuitive Python library for units, quantities, and dimensional analysis.'
9
9
  readme = 'README.md'
10
10
  requires-python = '>=3.12'
@@ -398,6 +398,10 @@ class Factor:
398
398
  """
399
399
  if not compact:
400
400
  return str(self._monomial)
401
+ return str(self._display_monomial())
402
+
403
+ def _display_monomial(self) -> Monomial[int | str]:
404
+ """Collect numeric bases for compact display, without changing exact state."""
401
405
  rational, irrational = self.decompose(rationalize_denominator=True)
402
406
  display_components: dict[int | str, Fraction] = {}
403
407
  if rational.numerator > 1:
@@ -417,7 +421,7 @@ class Factor:
417
421
  # Numeric roots use string keys to stay distinct from rational coefficients.
418
422
  # Symbol validation and Factor construction prevent collisions with symbols.
419
423
  display_components[str(base)] = exponent
420
- return str(Monomial._from_dict(display_components))
424
+ return Monomial._from_dict(display_components)
421
425
 
422
426
  def __str__(self) -> str:
423
427
  return self.format()
@@ -7,10 +7,11 @@ from fractions import Fraction
7
7
  from typing import Any, Generic, TypeVar, cast, overload
8
8
 
9
9
  from ._numerics import Fractionable, Magnitude, Number, Real, as_fraction, as_int_if_integral
10
+ from ._unicode import DOT
10
11
  from .dimension import DIMENSIONLESS, Dimension
11
12
  from .exceptions import DimensionError
12
- from .factor import ONE, Factor
13
- from .unit import ScaledUnit, Unit
13
+ from .factor import ONE, Factor, SymbolicConstant
14
+ from .unit import Unit
14
15
 
15
16
  # Magnitudes are exposed read-only; operations construct new quantities.
16
17
  T = TypeVar('T', bound=Magnitude, covariant=True)
@@ -22,7 +23,9 @@ class Quantity(Generic[T]):
22
23
  Construction preserves the magnitude's type. Conversion uses Factor's
23
24
  scaling rules and can change that type. Arithmetic converts finite Decimal
24
25
  operands exactly to Fraction; other magnitudes use native arithmetic.
25
- Multiplication or division by a Unit preserves the magnitude unchanged.
26
+ Multiplication or division by a Unit, Factor, or SymbolicConstant preserves
27
+ the magnitude unchanged. Exact factors are combined with the unit's scale,
28
+ while ordinary numeric operands participate in magnitude arithmetic.
26
29
 
27
30
  Magnitudes must be scalars whose equality returns a bool. NumPy scalars,
28
31
  arrays, and NumPy operations are not supported yet; NumPy integration is
@@ -33,9 +36,9 @@ class Quantity(Generic[T]):
33
36
  Scalar multiplication and division accept int, float, Fraction, and Decimal.
34
37
 
35
38
  Type hints preserve the magnitude type at construction and when combining
36
- with a Unit. Numeric conversions and arithmetic use unions where the result
37
- depends on runtime units or values. In particular, float annotations also
38
- accept int inputs, which may produce Fraction after scaling. Custom
39
+ with a Unit or exact factor. Numeric conversions and arithmetic use unions
40
+ where the result depends on runtime units or values. In particular, float
41
+ annotations also accept int inputs, which may produce Fraction after scaling. Custom
39
42
  magnitudes retain their arithmetic protocol rather than an assumed Self.
40
43
 
41
44
  Equality and hashing retain the exact rational coefficient and the unit's
@@ -50,9 +53,9 @@ class Quantity(Generic[T]):
50
53
  Powers normalize the exponent once using as_fraction(), just as Unit does,
51
54
  and use that exponent for both the magnitude and the unit. Nonintegral
52
55
  magnitude powers use native numerical evaluation and can be approximate.
53
- str() is readable display and groups units with an explicit scale in
54
- parentheses without evaluating that scale. repr() uses a reconstructible
55
- Unit expression.
56
+ str() displays the magnitude followed by the unit's exact product expression
57
+ without evaluating its scale. Adjacent numeric factors use a multiplication
58
+ dot. repr() uses a reconstructible Unit expression.
56
59
 
57
60
  >>> Quantity(1, 'm') + Quantity(50, 'cm')
58
61
  Quantity(Fraction(3, 2), Unit('m'))
@@ -106,11 +109,13 @@ class Quantity(Generic[T]):
106
109
  return f'{type(self).__name__}({self.value!r}, {self.unit!r})'
107
110
 
108
111
  def __str__(self) -> str:
109
- if isinstance(self.unit, ScaledUnit) and self.unit.scale != ONE:
110
- return f'{self.value} ({self.unit})'
111
- if self.unit.symbol.startswith('1'):
112
- return f'{self.value}{self.unit.symbol[1:]}'
113
- return f'{self.value} {self.unit}'
112
+ unit = str(self.unit)
113
+ if unit == '1':
114
+ return str(self.value)
115
+ if unit.startswith('1/'):
116
+ return f'{self.value}{unit[1:]}'
117
+ separator = DOT if unit[0].isdecimal() else ' '
118
+ return f'{self.value}{separator}{unit}'
114
119
 
115
120
  def is_dimensionless(self) -> bool:
116
121
  return self.dimension is DIMENSIONLESS
@@ -263,7 +268,7 @@ class Quantity(Generic[T]):
263
268
  return NotImplemented
264
269
 
265
270
  @overload
266
- def __mul__(self, other: Unit, /) -> Quantity[T]: ...
271
+ def __mul__(self, other: Unit | Factor | SymbolicConstant, /) -> Quantity[T]: ...
267
272
 
268
273
  @overload
269
274
  def __mul__(self: Quantity[int], other: Quantity[int] | int, /) -> Quantity[int]: ...
@@ -281,7 +286,7 @@ class Quantity(Generic[T]):
281
286
  quantity = cast(Quantity[Magnitude], other)
282
287
  value = cast(Magnitude, quantity._arithmetic_value)
283
288
  return Quantity(self._arithmetic_value * value, self.unit * quantity.unit)
284
- if isinstance(other, Unit):
289
+ if isinstance(other, (Unit, Factor, SymbolicConstant)):
285
290
  return Quantity(self.value, self.unit * other)
286
291
  if isinstance(other, Fractionable):
287
292
  value = cast(Magnitude, Fraction(other) if isinstance(other, Decimal) else other)
@@ -291,7 +296,7 @@ class Quantity(Generic[T]):
291
296
  __rmul__ = __mul__
292
297
 
293
298
  @overload
294
- def __truediv__(self, other: Unit, /) -> Quantity[T]: ...
299
+ def __truediv__(self, other: Unit | Factor | SymbolicConstant, /) -> Quantity[T]: ...
295
300
 
296
301
  @overload
297
302
  def __truediv__(self: Quantity[int], other: Quantity[int] | int, /) -> Quantity[float]: ...
@@ -309,7 +314,7 @@ class Quantity(Generic[T]):
309
314
  quantity = cast(Quantity[Magnitude], other)
310
315
  value = cast(Magnitude, quantity._arithmetic_value)
311
316
  return Quantity(self._arithmetic_value / value, self.unit / quantity.unit)
312
- if isinstance(other, Unit):
317
+ if isinstance(other, (Unit, Factor, SymbolicConstant)):
313
318
  return Quantity(self.value, self.unit / other)
314
319
  if isinstance(other, Fractionable):
315
320
  value = cast(Magnitude, Fraction(other) if isinstance(other, Decimal) else other)
@@ -317,18 +322,22 @@ class Quantity(Generic[T]):
317
322
  return NotImplemented
318
323
 
319
324
  @overload
320
- def __rtruediv__(self: Quantity[int], other: Unit | int, /) -> Quantity[float]: ...
325
+ def __rtruediv__(
326
+ self: Quantity[int], other: Unit | Factor | SymbolicConstant | int, /
327
+ ) -> Quantity[float]: ...
321
328
 
322
329
  @overload
323
330
  def __rtruediv__[N: Fractionable](
324
- self: Quantity[N], other: Unit | Fractionable, /
331
+ self: Quantity[N], other: Unit | Factor | SymbolicConstant | Fractionable, /
325
332
  ) -> Quantity[Real]: ...
326
333
 
327
334
  @overload
328
- def __rtruediv__(self, other: Unit | Fractionable, /) -> Quantity[Magnitude]: ...
335
+ def __rtruediv__(
336
+ self, other: Unit | Factor | SymbolicConstant | Fractionable, /
337
+ ) -> Quantity[Magnitude]: ...
329
338
 
330
339
  def __rtruediv__(self, other: object, /) -> Quantity[Magnitude]:
331
- if isinstance(other, Unit):
340
+ if isinstance(other, (Unit, Factor, SymbolicConstant)):
332
341
  return Quantity(1 / self._arithmetic_value, other / self.unit)
333
342
  if isinstance(other, Fractionable):
334
343
  value = Fraction(other) if isinstance(other, Decimal) else other
@@ -306,7 +306,8 @@ class ScaledUnit(Unit):
306
306
  involving a ScaledUnit return ScaledUnit, even when the scale becomes one.
307
307
  Equality compares the formal product and explicit scale, so a scale of one
308
308
  compares equal to the unscaled Unit. `symbol` describes only the product;
309
- str() also includes its scale, and repr() reconstructs both.
309
+ str() combines its compact exact scale and formal units into a single
310
+ product with a grouped denominator, and repr() reconstructs both.
310
311
 
311
312
  Copying and pickling preserve the formal units and exact scale. Restoring
312
313
  a unit requires its referenced unit definitions to be registered.
@@ -315,7 +316,7 @@ class ScaledUnit(Unit):
315
316
  >>> minute
316
317
  ScaledUnit('s', scale=Factor.from_dict({2: 2, 3: 1, 5: 1}))
317
318
  >>> str(minute)
318
- '60 s'
319
+ '60⋅s'
319
320
  >>> minute == Unit('s')
320
321
  False
321
322
  """
@@ -358,9 +359,11 @@ class ScaledUnit(Unit):
358
359
  def __str__(self) -> str:
359
360
  if self.scale == ONE:
360
361
  return self.symbol
361
- if self.symbol.startswith('1'):
362
- return f'{self.scale}{self.symbol[1:]}'
363
- return f'{self.scale} {self.symbol}'
362
+ components: dict[SingleUnit | int | str, Fraction] = dict(
363
+ self.scale._display_monomial().components()
364
+ )
365
+ components.update(self._monomial.components())
366
+ return str(Monomial(components))
364
367
 
365
368
  def __hash__(self) -> int:
366
369
  if self.scale == ONE:
@@ -38,9 +38,14 @@ def test_quantity_str(unit: str, expected: str) -> None:
38
38
  @pytest.mark.parametrize(
39
39
  ('unit', 'expected'),
40
40
  [
41
- (Factor(2) * Unit(), '3 (2)'),
42
- (Factor(2) / Unit('s'), '3 (2/s)'),
43
- (Factor(60) * Unit('s'), '3 (60 s)'),
41
+ (Factor(2) * Unit(), '3⋅2'),
42
+ (Factor(2) / Unit('s'), '3⋅2/s'),
43
+ (Factor(60) * Unit('s'), '3⋅60⋅s'),
44
+ (Unit('J*s') / (2 * PI), '3 J⋅s/(2⋅π)'),
45
+ (Unit('1/s') / (2 * PI), '3/(2⋅π⋅s)'),
46
+ (Unit() / PI, '3/π'),
47
+ (PI * Unit('s'), '3 π⋅s'),
48
+ (Factor(12) * Unit('s'), '3⋅12⋅s'),
44
49
  (ScaledUnit(Unit(), ONE), '3'),
45
50
  (ScaledUnit(Unit('1/s'), ONE), '3/s'),
46
51
  ],
@@ -52,6 +57,54 @@ def test_quantity_scaled_str(unit: ScaledUnit, expected: str) -> None:
52
57
  assert (quantity.value, quantity.unit, quantity.unit.factor) == before
53
58
 
54
59
 
60
+ @pytest.mark.parametrize('value', [0, -2, 2**60 + 1, 0.1, Fraction(3, 7), Decimal('1.2345'), 2j])
61
+ @pytest.mark.parametrize('unit', [Unit('cm'), ScaledUnit('cm', PI), Unit('km/m')])
62
+ @pytest.mark.parametrize('factor', [ONE, Factor(2), PI, 1 / (2 * PI), Factor(2) ** Fraction(1, 2)])
63
+ def test_quantity_factor_operations(value, unit: Unit, factor) -> None:
64
+ quantity = Quantity(value, unit)
65
+ before = (repr(quantity), hash(quantity))
66
+ with localcontext() as context:
67
+ context.prec = 2
68
+ products = (quantity * factor, factor * quantity)
69
+ quotient = quantity / factor
70
+ for result in products:
71
+ assert result.value is value
72
+ assert result.unit == unit * factor
73
+ assert (result / factor).unit == unit
74
+ assert result == value * (unit * factor)
75
+ assert quotient.value is value
76
+ assert quotient.unit == unit / factor
77
+ assert (quotient * factor) == quantity
78
+ assert hash(quotient * factor) == hash(quantity)
79
+ assert {quantity: 'found'}[quotient * factor] == 'found'
80
+ assert (repr(quantity), hash(quantity)) == before
81
+
82
+
83
+ @pytest.mark.parametrize('value', [2, Fraction(3, 7), Decimal('1.2345'), 2j])
84
+ @pytest.mark.parametrize('factor', [Factor(2), PI])
85
+ def test_quantity_factor_reciprocal(value, factor) -> None:
86
+ quantity = Quantity(value, ScaledUnit('cm', PI))
87
+ result = factor / quantity
88
+ reciprocal = 1 / quantity
89
+ assert result.value == reciprocal.value
90
+ assert type(result.value) is type(reciprocal.value)
91
+ assert result.unit == factor / quantity.unit
92
+ assert result == reciprocal * factor
93
+
94
+
95
+ def test_quantity_factor_scaling() -> None:
96
+ quantity = Quantity(3, 'cm')
97
+ numeric = quantity * 2
98
+ exact = quantity * Factor(2)
99
+ assert numeric.value == 6
100
+ assert numeric.unit == quantity.unit
101
+ assert exact.value == 3
102
+ assert exact.unit == ScaledUnit('cm', Factor(2))
103
+ assert exact == numeric
104
+ assert hash(exact) == hash(numeric)
105
+ assert (exact.to('m').to(exact.unit)).value == exact.value
106
+
107
+
55
108
  def test_quantity_scaled_operations() -> None:
56
109
  minute = Factor(60) * Unit('s')
57
110
  quantity = Quantity(1, minute)
@@ -132,6 +132,11 @@ def test_si_defining_constants(attribute: str, target: str, value: str) -> None:
132
132
  assert result.value == Decimal(value)
133
133
 
134
134
 
135
+ def test_si_hbar_value() -> None:
136
+ assert SI.hbar.unit == SI.h.unit
137
+ assert SI.hbar.value == pytest.approx(1.0545718176461565e-34, rel=1e-15, abs=0)
138
+
139
+
135
140
  @pytest.mark.parametrize(
136
141
  ('attribute', 'target'),
137
142
  [('Phi0', si.Wb), ('G0', si.S), ('RK', si.ohm), ('a0', si.m), ('lambdaC', si.m), ('Eh', si.J)],
@@ -575,11 +575,16 @@ def test_scaled_unit_exact_power() -> None:
575
575
  @pytest.mark.parametrize(
576
576
  ('base', 'scale', 'expected'),
577
577
  [
578
- ('s', Factor(60), '60 s'),
578
+ ('s', Factor(60), '60⋅s'),
579
579
  ('', Factor(2), '2'),
580
580
  ('1/s', Factor(2), '2/s'),
581
581
  ('1/(m*s)', Factor(2), '2/(m⋅s)'),
582
582
  ('cm', ONE, 'cm'),
583
+ ('J*s', 1 / (2 * PI), 'J⋅s/(2⋅π)'),
584
+ ('m/s', PI / 180, 'π⋅m/(180⋅s)'),
585
+ ('1/(m*s)', 1 / (2 * PI), '1/(2⋅π⋅m⋅s)'),
586
+ ('s', Factor(5040) ** Fraction(1, 2), '12⋅35¹ᐟ²⋅s'),
587
+ ('m^(1/2)/s', PI ** Fraction(1, 2), 'π¹ᐟ²⋅m¹ᐟ²/s'),
583
588
  ],
584
589
  )
585
590
  def test_scaled_unit_str(base: str, scale: Factor, expected: str) -> None:
@@ -143,6 +143,19 @@ def test_quantity_unit_operations[T: Magnitude](value: T) -> Quantity[T]:
143
143
  return quantity
144
144
 
145
145
 
146
+ def test_quantity_factor_operations[T: Magnitude](value: T) -> Quantity[T]:
147
+ quantity = Quantity(value, 'm')
148
+ for factor in (Factor(2), PI):
149
+ assert_type(quantity * factor, Quantity[T])
150
+ assert_type(factor * quantity, Quantity[T])
151
+ assert_type(quantity / factor, Quantity[T])
152
+ assert_type(factor / quantity, Quantity[Magnitude])
153
+ assert_type(Factor(2) / Quantity(2, 's'), Quantity[float])
154
+ assert_type(PI / Quantity(Decimal(2), 's'), Quantity[Real])
155
+ assert_type(Quantity(Decimal(2), 's') / (2 * PI), Quantity[Decimal])
156
+ return quantity
157
+
158
+
146
159
  def test_quantity_numeric_operations() -> None:
147
160
  integer = Quantity(1, 'm')
148
161
  rational = Quantity(Fraction(1), 'm')
@@ -20,5 +20,6 @@ def test_si_unit_types() -> None:
20
20
  def test_si_constant_types() -> None:
21
21
  assert_type(SI.c, Quantity[int])
22
22
  assert_type(SI.h, Quantity[Decimal])
23
+ assert_type(SI.hbar, Quantity[Real])
23
24
  assert_type(SI.e, Quantity[Decimal])
24
25
  assert_type(SI.h.to(si.J * si.s), Quantity[Real])
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes