1- from collections .abc import Sequence
2- from dataclasses import dataclass
1+ import re
32from fractions import Fraction
43from typing import Self
54
65import numpy as np
7- from unicode_superscript import int_as_unicode_superscript
6+ from unicode_superscript import int_as_unicode_superscript # type: ignore[import-untyped]
87
98
109class DimensionError (Exception ):
@@ -111,15 +110,15 @@ def __pow__(self, power: int | float):
111110 (self .moles_hint * numerator ) // denominator ,
112111 (self .angle_hint * numerator ) // denominator )
113112
114- def __eq__ (self : Self , other : Self ) :
113+ def __eq__ (self : Self , other : object ) -> bool :
115114 if isinstance (other , Dimensions ):
116- return (self .length == other .length and
117- self .time == other .time and
118- self .mass == other .mass and
119- self .current == other .current and
120- self .temperature == other .temperature and
121- self .moles_hint == other .moles_hint and
122- self .angle_hint == other .angle_hint )
115+ return (self .length == other .length
116+ and self .time == other .time
117+ and self .mass == other .mass
118+ and self .current == other .current
119+ and self .temperature == other .temperature
120+ and self .moles_hint == other .moles_hint
121+ and self .angle_hint == other .angle_hint )
123122
124123 return NotImplemented
125124
@@ -210,9 +209,6 @@ def __init__(self,
210209 self .scale = si_scaling_factor
211210 self .dimensions = dimensions
212211
213- def _components (self , tokens : Sequence ["UnitToken" ]):
214- pass
215-
216212 def __mul__ (self : Self , other : "Unit" ):
217213 if isinstance (other , Unit ):
218214 return Unit (self .scale * other .scale , self .dimensions * other .dimensions )
@@ -246,17 +242,15 @@ def __pow__(self, power: int | float):
246242 def equivalent (self : Self , other : "Unit" ):
247243 return self .dimensions == other .dimensions
248244
249- def __eq__ (self : Self , other : "Unit" ):
250- return self .equivalent (other ) and np .abs (np .log (self .scale / other .scale )) < 1e-5
245+ def __eq__ (self : Self , other : object ) -> bool :
246+ if isinstance (other , Unit ):
247+ return self .equivalent (other ) and np .abs (np .log (self .scale / other .scale )) < 1e-5
248+ return False
251249
252250 def si_equivalent (self ):
253251 """ Get the SI unit corresponding to this unit"""
254252 return Unit (1 , self .dimensions )
255253
256- def _format_unit (self , format_process : list ["UnitFormatProcessor" ]):
257- for processor in format_process :
258- pass
259-
260254 def __repr__ (self ):
261255 if self .scale == 1 :
262256 # We're in SI
@@ -265,9 +259,6 @@ def __repr__(self):
265259 else :
266260 return f"Unit[{ self .scale } , { self .dimensions } ]"
267261
268- @staticmethod
269- def parse (unit_string : str ) -> "Unit" :
270- pass
271262
272263class NamedUnit (Unit ):
273264 """ Units, but they have a name, and a symbol
@@ -308,57 +299,204 @@ def __eq__(self, other):
308299 case _:
309300 return False
310301
311-
312302 def startswith (self , prefix : str ) -> bool :
313303 """Check if any representation of the unit begins with the prefix string"""
314304 prefix = prefix .lower ()
315305 return (self .name is not None and self .name .lower ().startswith (prefix )) \
316- or (self .ascii_symbol is not None and self .ascii_symbol .lower ().startswith (prefix )) \
317- or (self .symbol is not None and self .symbol .lower ().startswith (prefix ))
318-
319- #
320- # Parsing plan:
321- # Require unknown amounts of units to be explicitly positive or negative?
322- #
323- #
324-
325-
326-
327- @dataclass
328- class ProcessedUnitToken :
329- """ Mid processing representation of formatted units """
330- base_string : str
331- exponent_string : str
332- latex_exponent_string : str
333- exponent : int
334-
335- class UnitFormatProcessor :
336- """ Represents a step in the unit processing pipeline"""
337- def apply (self , scale , dimensions ) -> tuple [ProcessedUnitToken , float , Dimensions ]:
338- """ This will be called to deal with each processing stage"""
339-
340- class RequiredUnitFormatProcessor (UnitFormatProcessor ):
341- """ This unit is required to exist in the formatting """
342- def __init__ (self , unit : Unit , power : int = 1 ):
343- self .unit = unit
344- self .power = power
345- def apply (self , scale , dimensions ) -> tuple [float , Dimensions , ProcessedUnitToken ]:
346- new_scale = scale / (self .unit .scale * self .power )
347- new_dimensions = self .unit .dimensions / (dimensions ** self .power )
348- token = ProcessedUnitToken (self .unit , self .power )
349-
350- return new_scale , new_dimensions , token
351- class GreedyAbsDimensionUnitFormatProcessor (UnitFormatProcessor ):
352- """ This processor minimises the dimensionality of the unit by multiplying by as many
353- units of the specified type as needed """
354- def __init__ (self , unit : Unit ):
355- self .unit = unit
356-
357- def apply (self , scale , dimensions ) -> tuple [ProcessedUnitToken , float , Dimensions ]:
358- pass
359-
360- class GreedyAbsDimensionUnitFormatProcessor (UnitFormatProcessor ):
361- pass
306+ or (self .ascii_symbol is not None and self .ascii_symbol .lower ().startswith (prefix )) \
307+ or (self .symbol is not None and self .symbol .lower ().startswith (prefix ))
308+
309+
310+ class UnknownUnit (NamedUnit ):
311+ """A unit for an unknown quantity
312+
313+ While this library attempts to handle all known SI units, it is
314+ likely that users will want to express quantities of arbitrary
315+ units (for example, calculating donuts per person for a meeting).
316+ The arbitrary unit allows for these unforseeable quantities."""
317+
318+ def __init__ (self ,
319+ numerator : str | list [str ] | dict [str , int | float ],
320+ denominator : None | list [str ] | dict [str , int | float ] = None ):
321+ if numerator is None :
322+ return TypeError
323+ self ._numerator = UnknownUnit ._parse_arg (numerator )
324+ self ._denominator = UnknownUnit ._parse_arg (denominator )
325+ self ._unit = NamedUnit (1 , Dimensions (), "" ) # Unitless
326+
327+ super ().__init__ (si_scaling_factor = 1 , dimensions = self ._unit .dimensions , symbol = self ._name ())
328+
329+ @staticmethod
330+ def _parse_arg (arg : str | list [str ] | dict [str , int | float ] | None ) -> dict [str , int | float ]:
331+ """Parse the different possibilities for constructor arguments
332+
333+ Both the numerator and the denominator could be a string, a
334+ list of strings, or a dict. Parse any of these values into a
335+ dictionary of names and powers.
336+
337+ """
338+ match arg :
339+ case None :
340+ return {}
341+ case str ():
342+ return {UnknownUnit ._valid_name (arg ): 1 }
343+ case list ():
344+ result : dict [str , int | float ] = {}
345+ for key in arg :
346+ if key in result :
347+ result [key ] += 1
348+ else :
349+ UnknownUnit ._valid_name (key )
350+ result [key ] = 1
351+ return result
352+ case dict ():
353+ for key in arg :
354+ UnknownUnit ._valid_name (key )
355+ return arg
356+ case _:
357+ raise TypeError
358+
359+ @staticmethod
360+ def _valid_name (name : str ) -> str :
361+ """Confirms that the name of a unit is appropriate
362+
363+ This mostly confirms that the unit does not contain math
364+ operators that would act on other units, like / or ^
365+ """
366+
367+ if re .search (r"[*/^\s]" , name ):
368+ raise RuntimeError (f'Unit name "{ name } " contains invalid characters (*, /, ^, or whitespace)' )
369+
370+ return name
371+
372+ def _name (self ):
373+ num = []
374+ for key , value in self ._numerator .items ():
375+ if value == 1 :
376+ num .append (key )
377+ else :
378+ num .append (f"{ key } ^{ value } " )
379+ den = []
380+ for key , value in self ._denominator .items ():
381+ den .append (f"{ key } ^{ - value } " )
382+ num .sort ()
383+ den .sort ()
384+ return " " .join (num + den )
385+
386+ def __eq__ (self , other ):
387+ match other :
388+ case UnknownUnit ():
389+ return self ._numerator == other ._numerator and self ._denominator == other ._denominator and self ._unit == other ._unit
390+ case Unit ():
391+ return not self ._numerator and not self ._denominator and self ._unit == other
392+ case _:
393+ return False
394+
395+ def __mul__ (self : Self , other : "Unit" ):
396+ match other :
397+ case UnknownUnit ():
398+ num = dict (self ._numerator )
399+ for key in other ._numerator :
400+ if key in num :
401+ num [key ] += other ._numerator [key ]
402+ else :
403+ num [key ] = other ._numerator [key ]
404+ den = dict (self ._denominator )
405+ for key in other ._denominator :
406+ if key in den :
407+ den [key ] += other ._denominator [key ]
408+ else :
409+ den [key ] = other ._denominator [key ]
410+ result = UnknownUnit (num , den )
411+ result ._unit *= other ._unit
412+ return result ._reduce ()
413+ case NamedUnit () | Unit () | int () | float ():
414+ result = UnknownUnit (self ._numerator , self ._denominator )
415+ result ._unit *= other
416+ return result
417+ case _:
418+ return NotImplemented
419+
420+ def __rmul__ (self : Self , other ):
421+ return self * other
422+
423+ def __truediv__ (self : Self , other : "Unit" ) -> "UnknownUnit" :
424+ match other :
425+ case UnknownUnit ():
426+ num = dict (self ._numerator )
427+ for key in other ._denominator :
428+ if key in num :
429+ num [key ] += other ._denominator [key ]
430+ else :
431+ num [key ] = other ._denominator [key ]
432+ den = dict (self ._denominator )
433+ for key in other ._numerator :
434+ if key in den :
435+ den [key ] += other ._numerator [key ]
436+ else :
437+ den [key ] = other ._numerator [key ]
438+ result = UnknownUnit (num , den )
439+ result ._unit /= other ._unit
440+ return result ._reduce ()
441+ case NamedUnit () | Unit () | int () | float ():
442+ result = UnknownUnit (self ._numerator , self ._denominator )
443+ result ._unit /= other
444+ return result
445+ case _:
446+ return NotImplemented
447+
448+ def __rtruediv__ (self : Self , other : "Unit" ) -> "UnknownUnit" :
449+ return (self / other ) ** - 1
450+
451+ def __pow__ (self , power : int | float ) -> "UnknownUnit" :
452+ match power :
453+ case int () | float ():
454+ num = {key : value * power for key , value in self ._numerator .items ()}
455+ den = {key : value * power for key , value in self ._denominator .items ()}
456+ if power < 0 :
457+ num , den = den , num
458+ num = {k : - v for k ,v in num .items ()}
459+ den = {k : - v for k ,v in den .items ()}
460+
461+ result = UnknownUnit (num , den )
462+ result ._unit = self ._unit ** power
463+ return result
464+ case _:
465+ return NotImplemented
466+
467+ def equivalent (self : Self , other : "Unit" ):
468+ match other :
469+ case UnknownUnit ():
470+ return self ._unit .equivalent (other ._unit ) and sorted (self ._numerator ) == sorted (other ._numerator ) and sorted (self ._denominator ) == sorted (other ._denominator )
471+ case _:
472+ return False
473+
474+ def _reduce (self ):
475+ """Remove redundant units"""
476+ for k in self ._denominator :
477+ if k in self ._numerator :
478+ common = min (self ._numerator [k ], self ._denominator [k ])
479+ self ._numerator [k ] -= common
480+ self ._denominator [k ] -= common
481+ dead_nums = [k for k in self ._numerator if self ._numerator [k ] == 0 ]
482+ for k in dead_nums :
483+ del self ._numerator [k ]
484+ dead_dens = [k for k in self ._denominator if self ._denominator [k ] == 0 ]
485+ for k in dead_dens :
486+ del self ._denominator [k ]
487+ return self
488+
489+ def __str__ (self ):
490+ result = self ._name ()
491+ if type (self ._unit ) is NamedUnit and self ._unit .name .strip ():
492+ result += f" { self ._unit .name .strip ()} "
493+ if type (self ._unit ) is Unit and str (self ._unit ).strip ():
494+ result += f" { str (self ._unit ).strip ()} "
495+ return result
496+
497+ def __repr__ (self ):
498+ return str (self )
499+
362500
363501class UnitGroup :
364502 """ A group of units that all have the same dimensionality """
0 commit comments