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.
ucon/core.py ADDED
@@ -0,0 +1,353 @@
1
+ """
2
+ ucon.core
3
+ ==========
4
+
5
+ Implements the **quantitative core** of the *ucon* system — the machinery that
6
+ manages numeric quantities, scaling prefixes, and dimensional relationships.
7
+
8
+ Classes
9
+ -------
10
+ - :class:`Exponent` — Represents an exponential base/power pair (e.g., 10³).
11
+ - :class:`Scale` — Enumerates SI and binary magnitude prefixes (kilo, milli, etc.).
12
+ - :class:`Number` — Couples a numeric value with a unit and scale.
13
+ - :class:`Ratio` — Represents a ratio between two :class:`Number` objects.
14
+
15
+ Together, these classes allow full arithmetic, conversion, and introspection
16
+ of physical quantities with explicit dimensional semantics.
17
+ """
18
+ from enum import Enum
19
+ from functools import lru_cache, reduce, total_ordering
20
+ from math import log2
21
+ from math import log10
22
+ from typing import Dict, Tuple, Union
23
+
24
+ from ucon import units
25
+ from ucon.unit import Unit
26
+
27
+
28
+ # TODO -- consider using a dataclass
29
+ @total_ordering
30
+ class Exponent:
31
+ """
32
+ Represents a **base–exponent pair** (e.g., 10³ or 2¹⁰).
33
+
34
+ Provides comparison and division semantics used internally to represent
35
+ magnitude prefixes (e.g., kilo, mega, micro).
36
+ """
37
+ bases = {2: log2, 10: log10}
38
+
39
+ __slots__ = ("base", "power")
40
+
41
+ def __init__(self, base: int, power: Union[int, float]):
42
+ if base not in self.bases.keys():
43
+ raise ValueError(f'Only the following bases are supported: {reduce(lambda a,b: f"{a}, {b}", self.bases.keys())}')
44
+ self.base = base
45
+ self.power = power
46
+
47
+ @property
48
+ def evaluated(self) -> float:
49
+ """Return the numeric value of base ** power."""
50
+ return self.base ** self.power
51
+
52
+ def parts(self) -> Tuple[int, Union[int, float]]:
53
+ """Return (base, power) tuple, used for Scale lookups."""
54
+ return self.base, self.power
55
+
56
+ def __eq__(self, other: 'Exponent'):
57
+ if not isinstance(other, Exponent):
58
+ raise TypeError(f'Cannot compare Exponent to non-Exponent type: {type(other)}')
59
+ return self.evaluated == other.evaluated
60
+
61
+ def __lt__(self, other: 'Exponent'):
62
+ if not isinstance(other, Exponent):
63
+ return NotImplemented
64
+ return self.evaluated < other.evaluated
65
+
66
+ def __hash__(self):
67
+ # Hash by rounded numeric equivalence to maintain cross-base consistency
68
+ return hash(round(self.evaluated, 15))
69
+
70
+ # ---------- Arithmetic Semantics ----------
71
+
72
+ def __truediv__(self, other: 'Exponent'):
73
+ """
74
+ Divide two Exponents.
75
+ - If bases match, returns a relative Exponent.
76
+ - If bases differ, returns a numeric ratio (float).
77
+ """
78
+ if not isinstance(other, Exponent):
79
+ return NotImplemented
80
+ if self.base == other.base:
81
+ return Exponent(self.base, self.power - other.power)
82
+ return self.evaluated / other.evaluated
83
+
84
+ def __mul__(self, other: 'Exponent'):
85
+ if not isinstance(other, Exponent):
86
+ return NotImplemented
87
+ if self.base == other.base:
88
+ return Exponent(self.base, self.power + other.power)
89
+ return float(self.evaluated * other.evaluated)
90
+
91
+ # ---------- Conversion Utilities ----------
92
+
93
+ def to_base(self, new_base: int) -> "Exponent":
94
+ """
95
+ Convert this Exponent to another base representation.
96
+
97
+ Example:
98
+ Exponent(2, 10).to_base(10)
99
+ # → Exponent(base=10, power=3.010299956639812)
100
+ """
101
+ if new_base not in self.bases:
102
+ supported = ", ".join(map(str, self.bases))
103
+ raise ValueError(f"Unsupported base {new_base!r}. Supported bases: {supported}")
104
+ new_power = self.bases[new_base](self.evaluated)
105
+ return Exponent(new_base, new_power)
106
+
107
+ # ---------- Numeric Interop ----------
108
+
109
+ def __float__(self) -> float:
110
+ return float(self.evaluated)
111
+
112
+ def __int__(self) -> int:
113
+ return int(self.evaluated)
114
+
115
+ # ---------- Representation ----------
116
+
117
+ def __repr__(self) -> str:
118
+ return f"Exponent(base={self.base}, power={self.power})"
119
+
120
+ def __str__(self) -> str:
121
+ return f"{self.base}^{self.power}"
122
+
123
+
124
+ class Scale(Enum):
125
+ """
126
+ Enumerates common **magnitude prefixes** for units and quantities.
127
+
128
+ Examples include:
129
+ - Binary prefixes (kibi, mebi)
130
+ - Decimal prefixes (milli, kilo, mega)
131
+
132
+ Each entry stores its numeric scaling factor (e.g., `kilo = 10³`).
133
+ """
134
+ gibi = Exponent(2, 30)
135
+ mebi = Exponent(2, 20)
136
+ kibi = Exponent(2, 10)
137
+ giga = Exponent(10, 9)
138
+ mega = Exponent(10, 6)
139
+ kilo = Exponent(10, 3)
140
+ hecto = Exponent(10, 2)
141
+ deca = Exponent(10, 1)
142
+ one = Exponent(10, 0)
143
+ deci = Exponent(10,-1)
144
+ centi = Exponent(10,-2)
145
+ milli = Exponent(10,-3)
146
+ micro = Exponent(10,-6)
147
+ nano = Exponent(10,-9)
148
+ _kibi = Exponent(2,-10) # "kibi" inverse
149
+ _mebi = Exponent(2,-20) # "mebi" inverse
150
+ _gibi = Exponent(2,-30) # "gibi" inverse
151
+
152
+ @staticmethod
153
+ @lru_cache(maxsize=1)
154
+ def all() -> Dict[Tuple[int, int], str]:
155
+ """Return a map from (base, power) → Scale name."""
156
+ return {(s.value.base, s.value.power): s.name for s in Scale}
157
+
158
+ @staticmethod
159
+ @lru_cache(maxsize=1)
160
+ def by_value() -> Dict[float, str]:
161
+ """
162
+ Return a map from evaluated numeric value → Scale name.
163
+ Cached after first access.
164
+ """
165
+ return {round(s.value.evaluated, 15): s.name for s in Scale}
166
+
167
+ @classmethod
168
+ @lru_cache(maxsize=1)
169
+ def _decimal_scales(cls):
170
+ """Return decimal (base-10) scales only."""
171
+ return list(s for s in cls if s.value.base == 10)
172
+
173
+ @classmethod
174
+ @lru_cache(maxsize=1)
175
+ def _binary_scales(cls):
176
+ """Return binary (base-2) scales only."""
177
+ return list(s for s in cls if s.value.base == 2)
178
+
179
+ @classmethod
180
+ def nearest(cls, value: float, include_binary: bool = False, undershoot_bias: float = 0.75) -> "Scale":
181
+ """
182
+ Return the Scale that best normalizes `value` toward 1 in log-space.
183
+ Optionally restricts to base-10 prefixes unless `include_binary=True`.
184
+ """
185
+ if value == 0:
186
+ return Scale.one
187
+
188
+ abs_val = abs(value)
189
+ candidates = cls._decimal_scales() if not include_binary else list(cls)
190
+
191
+ def distance(scale: "Scale") -> float:
192
+ ratio = abs_val / scale.value.evaluated
193
+ diff = log10(ratio)
194
+ # Bias overshoots slightly more than undershoots
195
+ if ratio < 1:
196
+ diff /= undershoot_bias
197
+ return abs(diff)
198
+
199
+ return min(candidates, key=distance)
200
+
201
+ def __truediv__(self, other: 'Scale'):
202
+ """
203
+ Divide one Scale by another.
204
+
205
+ Always returns a `Scale`, representing the resulting order of magnitude.
206
+ If no exact prefix match exists, returns the nearest known Scale.
207
+ """
208
+ if not isinstance(other, Scale):
209
+ return NotImplemented
210
+
211
+ if self == other:
212
+ return Scale.one
213
+
214
+ if other is Scale.one:
215
+ return self
216
+
217
+ should_consider_binary = (self.value.base == 2) or (other.value.base == 2)
218
+
219
+ if self is Scale.one:
220
+ result = Exponent(other.value.base, -other.value.power)
221
+ name = Scale.all().get((result.base, result.power))
222
+ if name:
223
+ return Scale[name]
224
+ return Scale.nearest(float(result), include_binary=should_consider_binary)
225
+
226
+ result: Union[Exponent, float] = self.value / other.value
227
+ if isinstance(result, Exponent):
228
+ match = Scale.all().get(result.parts())
229
+ if match:
230
+ return Scale[match]
231
+ else:
232
+ return Scale.nearest(float(result), include_binary=should_consider_binary)
233
+
234
+ def __lt__(self, other: 'Scale'):
235
+ return self.value < other.value
236
+
237
+ def __gt__(self, other: 'Scale'):
238
+ return self.value > other.value
239
+
240
+ def __eq__(self, other: 'Scale'):
241
+ return self.value == other.value
242
+
243
+
244
+ # TODO -- consider using a dataclass
245
+ class Number:
246
+ """
247
+ Represents a **numeric quantity** with an associated :class:`Unit` and :class:`Scale`.
248
+
249
+ Combines magnitude, unit, and scale into a single, composable object that
250
+ supports dimensional arithmetic and conversion:
251
+
252
+ >>> from ucon import core, units
253
+ >>> length = core.Number(unit=units.meter, quantity=5)
254
+ >>> time = core.Number(unit=units.second, quantity=2)
255
+ >>> speed = length / time
256
+ >>> speed
257
+ <2.5 (m/s)>
258
+ """
259
+ def __init__(self, unit: Unit = units.none, scale: Scale = Scale.one, quantity = 1):
260
+ self.unit = unit
261
+ self.scale = scale
262
+ self.quantity = quantity
263
+ self.value = round(self.quantity * self.scale.value.evaluated, 15)
264
+
265
+ def simplify(self):
266
+ return Number(unit=self.unit, quantity=self.value)
267
+
268
+ def to(self, new_scale: Scale):
269
+ new_quantity = self.quantity / new_scale.value.evaluated
270
+ return Number(unit=self.unit, scale=new_scale, quantity=new_quantity)
271
+
272
+ def as_ratio(self):
273
+ return Ratio(self)
274
+
275
+ def __mul__(self, another_number: 'Number') -> 'Number':
276
+ return Number(
277
+ unit=self.unit * another_number.unit,
278
+ scale=self.scale,
279
+ quantity=self.quantity * another_number.quantity,
280
+ )
281
+
282
+ def __truediv__(self, another_number: 'Number') -> 'Number':
283
+ unit = self.unit / another_number.unit
284
+ scale = self.scale / another_number.scale
285
+ quantity = self.quantity / another_number.quantity
286
+ return Number(unit, scale, quantity)
287
+
288
+ def __eq__(self, another_number):
289
+ if isinstance(another_number, Number):
290
+ return (self.unit == another_number.unit) and \
291
+ (self.quantity == another_number.quantity) and \
292
+ (self.value == another_number.value)
293
+ elif isinstance(another_number, Ratio):
294
+ return self == another_number.evaluate()
295
+ else:
296
+ raise ValueError(f'"{another_number}" is not a Number or Ratio. Comparison not possible.')
297
+
298
+ def __repr__(self):
299
+ return f'<{self.quantity} {"" if self.scale.name == "one" else self.scale.name}{self.unit.name}>'
300
+
301
+
302
+ # TODO -- consider using a dataclass
303
+ class Ratio:
304
+ """
305
+ Represents a **ratio of two Numbers**, preserving their unit semantics.
306
+
307
+ Useful for expressing physical relationships like efficiency, density,
308
+ or dimensionless comparisons:
309
+
310
+ >>> ratio = Ratio(length, time)
311
+ >>> ratio.evaluate()
312
+ <2.5 (m/s)>
313
+ """
314
+ def __init__(self, numerator: Number = Number(), denominator: Number = Number()):
315
+ self.numerator = numerator
316
+ self.denominator = denominator
317
+
318
+ def reciprocal(self) -> 'Ratio':
319
+ return Ratio(numerator=self.denominator, denominator=self.numerator)
320
+
321
+ def evaluate(self) -> Number:
322
+ return self.numerator / self.denominator
323
+
324
+ def __mul__(self, another_ratio: 'Ratio') -> 'Ratio':
325
+ if self.numerator.unit == another_ratio.denominator.unit:
326
+ factor = self.numerator / another_ratio.denominator
327
+ numerator, denominator = factor * another_ratio.numerator, self.denominator
328
+ elif self.denominator.unit == another_ratio.numerator.unit:
329
+ factor = another_ratio.numerator / self.denominator
330
+ numerator, denominator = factor * self.numerator, another_ratio.denominator
331
+ else:
332
+ factor = Number()
333
+ another_number = another_ratio.evaluate()
334
+ numerator, denominator = self.numerator * another_number, self.denominator
335
+ return Ratio(numerator=numerator, denominator=denominator)
336
+
337
+ def __truediv__(self, another_ratio: 'Ratio') -> 'Ratio':
338
+ return Ratio(
339
+ numerator=self.numerator * another_ratio.denominator,
340
+ denominator=self.denominator * another_ratio.numerator,
341
+ )
342
+
343
+ def __eq__(self, another_ratio: 'Ratio') -> bool:
344
+ if isinstance(another_ratio, Ratio):
345
+ return self.evaluate() == another_ratio.evaluate()
346
+ elif isinstance(another_ratio, Number):
347
+ return self.evaluate() == another_ratio
348
+ else:
349
+ raise ValueError(f'"{another_ratio}" is not a Ratio or Number. Comparison not possible.')
350
+
351
+ def __repr__(self):
352
+ # TODO -- resolve int/float inconsistency
353
+ return f'{self.evaluate()}' if self.numerator == self.denominator else f'{self.numerator} / {self.denominator}'
ucon/dimension.py ADDED
@@ -0,0 +1,172 @@
1
+ """
2
+ ucon.dimension
3
+ ===============
4
+
5
+ Defines the algebra of **physical dimensions**--the foundation of all unit
6
+ relationships and dimensional analysis in *ucon*.
7
+
8
+ Each :class:`Dimension` represents a physical quantity (time, mass, length, etc.)
9
+ expressed as a 7-element exponent vector following the SI base system:
10
+
11
+ (T, L, M, I, Θ, J, N) :: (s * m * kg * A * K * cd * mol)
12
+ time, length, mass, current, temperature, luminous intensity, substance
13
+
14
+ Derived dimensions are expressed as algebraic sums or differences of these base
15
+ vectors (e.g., `velocity = length / time`, `force = mass * acceleration`).
16
+
17
+ Classes
18
+ -------
19
+ - :class:`Vector` — Represents the exponent vector of a physical quantity.
20
+ - :class:`Dimension` — Enum of known physical quantities, each with a `Vector`
21
+ value and operator overloads for dimensional algebra.
22
+ """
23
+ from dataclasses import dataclass
24
+ from enum import Enum
25
+ from functools import partial, reduce
26
+ from operator import __sub__ as subtraction
27
+ from typing import Callable, Iterable, Iterator
28
+
29
+
30
+ diff: Callable[[Iterable], int] = partial(reduce, subtraction)
31
+
32
+ @dataclass
33
+ class Vector:
34
+ """
35
+ Represents the **exponent vector** of a physical quantity.
36
+
37
+ Each component corresponds to the power of a base dimension in the SI system:
38
+ time (T), length (L), mass (M), current (I), temperature (Θ),
39
+ luminous intensity (J), and amount of substance (N).
40
+
41
+ Arithmetic operations correspond to dimensional composition:
42
+ - Addition (`+`) → multiplication of quantities
43
+ - Subtraction (`-`) → division of quantities
44
+
45
+ e.g.
46
+ Vector(T=1, L=0, M=0, I=0, Θ=0, J=0, N=0) => "time"
47
+ Vector(T=0, L=2, M=0, I=0, Θ=0, J=0, N=0) => "area"
48
+ Vector(T=-2, L=1, M=1, I=0, Θ=0, J=0, N=0) => "force"
49
+ """
50
+ T: int = 0 # time
51
+ L: int = 0 # length
52
+ M: int = 0 # mass
53
+ I: int = 0 # current
54
+ Θ: int = 0 # temperature
55
+ J: int = 0 # luminous intensity
56
+ N: int = 0 # amount of substance
57
+
58
+ def __iter__(self) -> Iterator[int]:
59
+ yield self.T
60
+ yield self.L
61
+ yield self.M
62
+ yield self.I
63
+ yield self.Θ
64
+ yield self.J
65
+ yield self.N
66
+
67
+ def __len__(self) -> int:
68
+ return sum(tuple(1 for x in self))
69
+
70
+ def __add__(self, vector: 'Vector') -> 'Vector':
71
+ """
72
+ Addition, here, comes from the multiplication of base quantities
73
+
74
+ e.g. F = m * a
75
+ F =
76
+ (s^-2 * m^1 * kg * A * K * cd * mol) +
77
+ (s * m * kg^1 * A * K * cd * mol)
78
+ """
79
+ values = tuple(sum(pair) for pair in zip(tuple(self), tuple(vector)))
80
+ return Vector(*values)
81
+
82
+ def __sub__(self, vector: 'Vector') -> 'Vector':
83
+ """
84
+ Subtraction, here, comes from the division of base quantities
85
+ """
86
+ values = tuple(diff(pair) for pair in zip(tuple(self), tuple(vector)))
87
+ return Vector(*values)
88
+
89
+ def __eq__(self, vector: 'Vector') -> bool:
90
+ assert isinstance(vector, Vector), "Can only compare Vector to another Vector"
91
+ return tuple(self) == tuple(vector)
92
+
93
+ def __hash__(self) -> int:
94
+ # Hash based on the string because tuples have been shown to collide
95
+ # Not the most performant, but effective
96
+ return hash(str(tuple(self)))
97
+
98
+
99
+ class Dimension(Enum):
100
+ """
101
+ Represents a **physical dimension** defined by a :class:`Vector`.
102
+
103
+ Each dimension corresponds to a distinct combination of base exponents.
104
+ Dimensions are algebraically composable via multiplication and division:
105
+
106
+ >>> Dimension.length / Dimension.time
107
+ <Dimension.velocity: Vector(T=-1, L=1, M=0, I=0, Θ=0, J=0, N=0)>
108
+
109
+ This algebra forms the foundation for unit compatibility and conversion.
110
+ """
111
+ none = Vector()
112
+
113
+ # -- BASIS ---------------------------------------
114
+ time = Vector(1, 0, 0, 0, 0, 0, 0)
115
+ length = Vector(0, 1, 0, 0, 0, 0, 0)
116
+ mass = Vector(0, 0, 1, 0, 0, 0, 0)
117
+ current = Vector(0, 0, 0, 1, 0, 0, 0)
118
+ temperature = Vector(0, 0, 0, 0, 1, 0, 0)
119
+ luminous_intensity = Vector(0, 0, 0, 0, 0, 1, 0)
120
+ amount_of_substance = Vector(0, 0, 0, 0, 0, 0, 1)
121
+ # ------------------------------------------------
122
+
123
+ acceleration = Vector(-2, 1, 0, 0, 0, 0, 0)
124
+ angular_momentum = Vector(-1, 2, 1, 0, 0, 0, 0)
125
+ area = Vector(0, 2, 0, 0, 0, 0, 0)
126
+ capacitance = Vector(4, -2, -1, 2, 0, 0, 0)
127
+ charge = Vector(1, 0, 0, 1, 0, 0, 0)
128
+ conductance = Vector(3, -2, -1, 2, 0, 0, 0)
129
+ conductivity = Vector(3, -3, -1, 2, 0, 0, 0)
130
+ density = Vector(0, -3, 1, 0, 0, 0, 0)
131
+ electric_field_strength = Vector(-3, 1, 1, -1, 0, 0, 0)
132
+ energy = Vector(-2, 2, 1, 0, 0, 0, 0)
133
+ entropy = Vector(-2, 2, 1, 0, -1, 0, 0)
134
+ force = Vector(-2, 1, 1, 0, 0, 0, 0)
135
+ frequency = Vector(-1, 0, 0, 0, 0, 0, 0)
136
+ gravitation = Vector(-2, 3, -1, 0, 0, 0, 0)
137
+ illuminance = Vector(0, -2, 0, 0, 0, 1, 0)
138
+ inductance = Vector(-2, 2, 1, -2, 0, 0, 0)
139
+ magnetic_flux = Vector(-2, 2, 1, -1, 0, 0, 0)
140
+ magnetic_flux_density = Vector(-2, 0, 1, -1, 0, 0, 0)
141
+ magnetic_permeability = Vector(-2, 1, 1, -2, 0, 0, 0)
142
+ molar_mass = Vector(0, 0, 1, 0, 0, 0, -1)
143
+ molar_volume = Vector(0, 3, 0, 0, 0, 0, -1)
144
+ momentum = Vector(-1, 1, 1, 0, 0, 0, 0)
145
+ permittivity = Vector(4, -3, -1, 2, 0, 0, 0)
146
+ power = Vector(-3, 2, 1, 0, 0, 0, 0)
147
+ pressure = Vector(-2, -1, 1, 0, 0, 0, 0)
148
+ resistance = Vector(-3, 2, 1, -2, 0, 0, 0)
149
+ resistivity = Vector(-3, 3, 1, -2, 0, 0, 0)
150
+ specific_heat_capacity = Vector(-2, 2, 0, 0, -1, 0, 0)
151
+ thermal_conductivity = Vector(-3, 1, 1, 0, -1, 0, 0)
152
+ velocity = Vector(-1, 1, 0, 0, 0, 0, 0)
153
+ voltage = Vector(-3, 2, 1, -1, 0, 0, 0)
154
+ volume = Vector(0, 3, 0, 0, 0, 0, 0)
155
+
156
+ def __truediv__(self, dimension: 'Dimension') -> 'Dimension':
157
+ if not isinstance(dimension, Dimension):
158
+ raise TypeError(f"Cannot divide Dimension by non-Dimension type: {type(dimension)}")
159
+ return Dimension(self.value - dimension.value)
160
+
161
+ def __mul__(self, dimension: 'Dimension') -> 'Dimension':
162
+ if not isinstance(dimension, Dimension):
163
+ raise TypeError(f"Cannot multiply Dimension by non-Dimension type: {type(dimension)}")
164
+ return Dimension(self.value + dimension.value)
165
+
166
+ def __eq__(self, dimension) -> bool:
167
+ if not isinstance(dimension, Dimension):
168
+ raise TypeError(f"Cannot compare Dimension with non-Dimension type: {type(dimension)}")
169
+ return self.value == dimension.value
170
+
171
+ def __hash__(self) -> int:
172
+ return hash(self.value)
ucon/unit.py ADDED
@@ -0,0 +1,92 @@
1
+ """
2
+ ucon.unit
3
+ ==========
4
+
5
+ Defines the **Unit** abstraction — the symbolic and algebraic representation of
6
+ a measurable quantity associated with a :class:`ucon.dimension.Dimension`.
7
+
8
+ A :class:`Unit` pairs a human-readable name and aliases with its underlying
9
+ dimension.
10
+
11
+ Units are composable:
12
+
13
+ >>> from ucon import units
14
+ >>> units.meter / units.second
15
+ <velocity | (m/s)>
16
+
17
+ They can be multiplied or divided to form compound units, and their dimensional
18
+ relationships are preserved algebraically.
19
+ """
20
+ from ucon.dimension import Dimension
21
+
22
+
23
+ class Unit:
24
+ """
25
+ Represents a **unit of measure** associated with a :class:`Dimension`.
26
+
27
+ Parameters
28
+ ----------
29
+ *aliases : str
30
+ Optional shorthand symbols (e.g., "m", "sec").
31
+ name : str
32
+ Canonical name of the unit (e.g., "meter").
33
+ dimension : Dimension
34
+ The physical dimension this unit represents.
35
+
36
+ Notes
37
+ -----
38
+ Units participate in algebraic operations that produce new compound units:
39
+
40
+ >>> density = units.gram / units.liter
41
+ >>> density.dimension
42
+ <Dimension.density: Vector(T=0, L=-3, M=1, I=0, Θ=0, J=0, N=0)>
43
+
44
+ The combination rules follow the same algebra as :class:`Dimension`.
45
+ """
46
+ def __init__(self, *aliases: str, name: str = '', dimension: Dimension = Dimension.none):
47
+ self.dimension = dimension
48
+ self.name = name
49
+ self.aliases = aliases
50
+ self.shorthand = aliases[0] if aliases else self.name
51
+
52
+ def __repr__(self):
53
+ addendum = f' | {self.name}' if self.name else ''
54
+ return f'<{self.dimension.name}{addendum}>'
55
+
56
+ # TODO -- limit `operator` param choices
57
+ def generate_name(self, unit: 'Unit', operator: str):
58
+ if (self.dimension is Dimension.none) and not (unit.dimension is Dimension.none):
59
+ return unit.name
60
+ if not (self.dimension is Dimension.none) and (unit.dimension is Dimension.none):
61
+ return self.name
62
+
63
+ if not self.shorthand and not unit.shorthand:
64
+ name = ''
65
+ elif self.shorthand and not unit.shorthand:
66
+ name = f'({self.shorthand}{operator}?)'
67
+ elif not self.shorthand and unit.shorthand:
68
+ name = f'(?{operator}{unit.shorthand})'
69
+ else:
70
+ name = f'({self.shorthand}{operator}{unit.shorthand})'
71
+ return name
72
+
73
+ def __truediv__(self, unit: 'Unit') -> 'Unit':
74
+ # TODO -- define __eq__ for simplification, here
75
+ if (self.name == unit.name) and (self.dimension == unit.dimension):
76
+ return Unit()
77
+
78
+ if (unit.dimension is Dimension.none):
79
+ return self
80
+
81
+ return Unit(name=self.generate_name(unit, '/'), dimension=self.dimension / unit.dimension)
82
+
83
+ def __mul__(self, unit: 'Unit') -> 'Unit':
84
+ return Unit(name=self.generate_name(unit, '*'), dimension=self.dimension * unit.dimension)
85
+
86
+ def __eq__(self, unit: 'Unit') -> bool:
87
+ if not isinstance(unit, Unit):
88
+ raise TypeError(f'Cannot compare Unit to non-Unit type: {type(unit)}')
89
+ return (self.name == unit.name) and (self.dimension == unit.dimension)
90
+
91
+ def __hash__(self) -> int:
92
+ return hash(tuple([self.name, self.dimension,]))