u 0.1__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.
u-0.1/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ # python generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # venv
10
+ .venv
u-0.1/.python-version ADDED
@@ -0,0 +1 @@
1
+ 3.12.2
@@ -0,0 +1,7 @@
1
+ {
2
+ "python.testing.pytestArgs": [
3
+ "tests"
4
+ ],
5
+ "python.testing.unittestEnabled": false,
6
+ "python.testing.pytestEnabled": true
7
+ }
u-0.1/PKG-INFO ADDED
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.3
2
+ Name: u
3
+ Version: 0.1
4
+ Summary: Statically typed units
5
+ Author-email: Aran-Fey <rawing7@gmail.com>
6
+ Requires-Python: >=3.8
7
+ Requires-Dist: typing-extensions
8
+ Description-Content-Type: text/markdown
9
+
10
+ # u
11
+
12
+ Statically typed units.
13
+
14
+ ## Quickstart
15
+
16
+ ```python
17
+ # Type safe assignments
18
+ duration: u.Duration = u.seconds(5) # Ok
19
+ distance: u.Distance = u.amperes(5) # Type checking error
20
+
21
+ # Type safe math
22
+ print(u.seconds(120) + u.minutes(3)) # Ok
23
+ print(u.seconds(120) + u.amperes(3)) # Type checking error
24
+
25
+ # Type safe compound units (with some caveats)
26
+ speed: u.Div[u.Distance, u.Duration] = u.km(5) / u.hours(1) # Ok
27
+
28
+ # Reusable prefixes
29
+ print(u.bytes(1000) == u.mega(u.bytes)(1))
30
+
31
+ # Define your own Quantities and Units
32
+ class Tastiness(u.Quantity):
33
+ pass
34
+
35
+ mmm = u.Unit[Tastiness](symbol='mmm', multiplier=1)
36
+ yum = u.Unit[Tastiness]('yum', 10)
37
+ ```
u-0.1/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # u
2
+
3
+ Statically typed units.
4
+
5
+ ## Quickstart
6
+
7
+ ```python
8
+ # Type safe assignments
9
+ duration: u.Duration = u.seconds(5) # Ok
10
+ distance: u.Distance = u.amperes(5) # Type checking error
11
+
12
+ # Type safe math
13
+ print(u.seconds(120) + u.minutes(3)) # Ok
14
+ print(u.seconds(120) + u.amperes(3)) # Type checking error
15
+
16
+ # Type safe compound units (with some caveats)
17
+ speed: u.Div[u.Distance, u.Duration] = u.km(5) / u.hours(1) # Ok
18
+
19
+ # Reusable prefixes
20
+ print(u.bytes(1000) == u.mega(u.bytes)(1))
21
+
22
+ # Define your own Quantities and Units
23
+ class Tastiness(u.Quantity):
24
+ pass
25
+
26
+ mmm = u.Unit[Tastiness](symbol='mmm', multiplier=1)
27
+ yum = u.Unit[Tastiness]('yum', 10)
28
+ ```
u-0.1/__init__.py ADDED
@@ -0,0 +1,18 @@
1
+
2
+ # fake module that resides in my lib folder and
3
+ # imports the actual implementation
4
+
5
+ from pathlib import Path
6
+
7
+ here = Path(__file__).absolute().parent
8
+ name = here.stem
9
+
10
+
11
+ import sys
12
+ sys.path.insert(0, str(here))
13
+ del sys.modules[name]
14
+ module = __import__(name)
15
+ del sys.path[0]
16
+
17
+ del Path, here, name, sys
18
+ globals().update(module.__dict__)
u-0.1/pyproject.toml ADDED
@@ -0,0 +1,22 @@
1
+ [project]
2
+ name = "u"
3
+ description = "Statically typed units"
4
+ authors = [{ name = "Aran-Fey", email = "rawing7@gmail.com" }]
5
+ dependencies = ["typing-extensions"]
6
+ readme = "README.md"
7
+ requires-python = ">= 3.8"
8
+ dynamic = ["version"]
9
+
10
+ [build-system]
11
+ requires = ["hatchling"]
12
+ build-backend = "hatchling.build"
13
+
14
+ [tool.hatch.version]
15
+ path = "u/__init__.py"
16
+
17
+ [tool.rye]
18
+ managed = true
19
+ dev-dependencies = ["pytest"]
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["u"]
@@ -0,0 +1,19 @@
1
+ # generated by rye
2
+ # use `rye lock` or `rye sync` to update this lockfile
3
+ #
4
+ # last locked with the following flags:
5
+ # pre: false
6
+ # features: []
7
+ # all-features: false
8
+ # with-sources: false
9
+
10
+ -e file:.
11
+ iniconfig==2.0.0
12
+ # via pytest
13
+ packaging==24.0
14
+ # via pytest
15
+ pluggy==1.5.0
16
+ # via pytest
17
+ pytest==8.1.1
18
+ typing-extensions==4.11.0
19
+ # via u
@@ -0,0 +1,12 @@
1
+ # generated by rye
2
+ # use `rye lock` or `rye sync` to update this lockfile
3
+ #
4
+ # last locked with the following flags:
5
+ # pre: false
6
+ # features: []
7
+ # all-features: false
8
+ # with-sources: false
9
+
10
+ -e file:.
11
+ typing-extensions==4.11.0
12
+ # via u
@@ -0,0 +1,38 @@
1
+ import pytest
2
+
3
+ import u
4
+ from u import Quantity, Unit
5
+
6
+
7
+ @pytest.mark.parametrize(
8
+ "value, unit, expected_result",
9
+ [
10
+ (u.meters(7_000), u.kilometers, 7),
11
+ (u.kilometers(3.5), u.meters, 3_500),
12
+ # (u.celsius(10), u.kelvin, 283.15),
13
+ # (u.kelvin(100), u.celsius, -173.15),
14
+ # (u.kelvin(0), u.fahrenheit, -459.67),
15
+ # (u.fahrenheit(50), u.kelvin, 283.15),
16
+ (u.meters_per_second(1234), u.kilometers / u.seconds, 1.234),
17
+ (u.meters_per_second(1), u.meters / u.minutes, 60),
18
+ (u.kilometers_per_hour(90), u.meters_per_second, 25),
19
+ (u.meters_per_second(30), u.kilometers_per_hour, 108),
20
+ (u.square_meters(1_000_000), u.square_kilometers, 1),
21
+ ],
22
+ )
23
+ def test_to_number(value: Quantity, unit: Unit, expected_result: float):
24
+ result = value.to_number(unit)
25
+ assert result == pytest.approx(expected_result)
26
+
27
+
28
+ @pytest.mark.parametrize(
29
+ "value, expected_result",
30
+ [
31
+ (u.meters(7), "7m"),
32
+ (u.kilometers(3.5), "3.5km"),
33
+ (u.meters_per_second(5), "5m/s"),
34
+ (u.square_meters(3), "3m²"),
35
+ ],
36
+ )
37
+ def test_to_string(value: Quantity, expected_result: str):
38
+ assert str(value) == expected_result
@@ -0,0 +1,48 @@
1
+ import pytest
2
+
3
+ import u
4
+
5
+
6
+ @pytest.mark.parametrize(
7
+ "result, expected_result",
8
+ [
9
+ (u.seconds(10) + u.seconds(5), u.seconds(15)),
10
+ (u.seconds(10) + u.seconds(50), u.minutes(1)),
11
+ (u.hours(0.25) + u.hours(0.5), u.minutes(45)),
12
+ ],
13
+ )
14
+ def test_addition(result: u.Quantity, expected_result: u.Quantity):
15
+ assert result == expected_result
16
+
17
+
18
+ @pytest.mark.parametrize(
19
+ "result, expected_result",
20
+ [
21
+ (u.seconds(10) - u.seconds(3), u.seconds(7)),
22
+ (u.seconds(70) - u.seconds(10), u.minutes(1)),
23
+ (u.hours(0.75) - u.hours(0.5), u.minutes(15)),
24
+ ],
25
+ )
26
+ def test_subtraction(result: u.Quantity, expected_result: u.Quantity):
27
+ assert result == expected_result
28
+
29
+
30
+ @pytest.mark.parametrize(
31
+ "result, expected_result",
32
+ [
33
+ (u.meters(10) * u.meters(5), u.square_meters(50)),
34
+ (u.meters(10) * u.meters(5), u.square_meters(50)),
35
+ ],
36
+ )
37
+ def test_multiplication(result: u.Quantity, expected_result: u.Quantity):
38
+ assert result == expected_result
39
+
40
+
41
+ @pytest.mark.parametrize(
42
+ "result, expected_result",
43
+ [
44
+ (u.meters(10) / u.seconds(5), u.meters_per_second(2)),
45
+ ],
46
+ )
47
+ def test_division(result: u.Quantity, expected_result: u.Quantity):
48
+ assert result == expected_result
u-0.1/u/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ __version__ = "0.1"
2
+
3
+ from .prefixes import *
4
+ from .quantity_and_unit import *
5
+ from .quantities import *
u-0.1/u/_utils.py ADDED
@@ -0,0 +1,21 @@
1
+ import functools
2
+ from typing import Callable, TypeVar
3
+
4
+
5
+ C = TypeVar("C", bound=Callable)
6
+
7
+
8
+ def cached(func: C) -> C:
9
+ cache = {}
10
+
11
+ @functools.wraps(func)
12
+ def wrapper(*args):
13
+ try:
14
+ return cache[args]
15
+ except KeyError:
16
+ pass
17
+
18
+ result = cache[args] = func(*args)
19
+ return result
20
+
21
+ return wrapper # type: ignore
u-0.1/u/prefixes.py ADDED
@@ -0,0 +1,83 @@
1
+ from ._utils import cached
2
+ from .quantity_and_unit import Unit, Q
3
+
4
+ __all__ = [
5
+ "Prefix",
6
+ "atto",
7
+ "femto",
8
+ "pico",
9
+ "nano",
10
+ "micro",
11
+ "milli",
12
+ "centi",
13
+ "deci",
14
+ "deka",
15
+ "hecto",
16
+ "kilo",
17
+ "mega",
18
+ "giga",
19
+ "tera",
20
+ "peta",
21
+ "exa",
22
+ "zetta",
23
+ "yotta",
24
+ "kibi",
25
+ "mebi",
26
+ "gibi",
27
+ "tebi",
28
+ "pebi",
29
+ "exbi",
30
+ "zebi",
31
+ "yobi",
32
+ ]
33
+
34
+
35
+ class Prefix:
36
+ def __init__(self, symbol: str, multiplier: float):
37
+ self.symbol = symbol
38
+ self.multiplier = multiplier
39
+
40
+ @cached
41
+ def __call__(self, unit: Unit[Q]) -> Unit[Q]:
42
+ return Unit(
43
+ self.symbol + unit.symbol,
44
+ self.multiplier * unit.multiplier,
45
+ )
46
+
47
+ def __repr__(self) -> str:
48
+ return f"<Prefix {self.symbol}>"
49
+
50
+
51
+ quecto = Prefix("q", 1e-30)
52
+ ronto = Prefix("r", 1e-27)
53
+ yocto = Prefix("y", 1e-24)
54
+ zepto = Prefix("z", 1e-21)
55
+ atto = Prefix("a", 1e-18)
56
+ femto = Prefix("f", 1e-15)
57
+ pico = Prefix("p", 1e-12)
58
+ nano = Prefix("n", 1e-9)
59
+ micro = Prefix("µ", 1e-6)
60
+ milli = Prefix("m", 1e-3)
61
+ centi = Prefix("c", 1e-2)
62
+ deci = Prefix("d", 1e-1)
63
+ deka = Prefix("da", 1e1)
64
+ hecto = Prefix("h", 1e2)
65
+ kilo = Prefix("k", 1e3)
66
+ mega = Prefix("M", 1e6)
67
+ giga = Prefix("G", 1e9)
68
+ tera = Prefix("T", 1e12)
69
+ peta = Prefix("P", 1e15)
70
+ exa = Prefix("E", 1e18)
71
+ zetta = Prefix("Z", 1e21)
72
+ yotta = Prefix("Y", 1e24)
73
+ ronna = Prefix("R", 1e27)
74
+ quetta = Prefix("Q", 1e30)
75
+
76
+ kibi = Prefix("ki", 2**10)
77
+ mebi = Prefix("Mi", 2**20)
78
+ gibi = Prefix("Gi", 2**30)
79
+ tebi = Prefix("Ti", 2**40)
80
+ pebi = Prefix("Pi", 2**50)
81
+ exbi = Prefix("Ei", 2**60)
82
+ zebi = Prefix("Zi", 2**70)
83
+ yobi = Prefix("Yi", 2**80)
@@ -0,0 +1,14 @@
1
+ from .acceleration import *
2
+ from .amount_of_substance import *
3
+ from .area import *
4
+ from .data_volume import *
5
+ from .distance import *
6
+ from .duration import *
7
+ from .electric_current import *
8
+ from .data_volume import *
9
+ from .frequency import *
10
+ from .luminous_intensity import *
11
+ from .mass import *
12
+ from .one import *
13
+ from .speed import *
14
+ from .temperature import *
@@ -0,0 +1,18 @@
1
+ from ..quantity_and_unit import Div
2
+ from .duration import Duration, second, hour
3
+ from .speed import Speed, meters_per_second, kilometers_per_hour
4
+
5
+
6
+ # fmt: off
7
+ __all__ = [
8
+ "Acceleration",
9
+ "meters_per_second_squared", "mps2",
10
+ "kilometers_per_hour_squared", "kph2",
11
+ ]
12
+ # fmt: on
13
+
14
+
15
+ Acceleration = Div[Speed, Duration]
16
+
17
+ meters_per_second_squared = mps2 = meters_per_second / second
18
+ kilometers_per_hour_squared = kph2 = kilometers_per_hour / hour
@@ -0,0 +1,16 @@
1
+ from ..quantity_and_unit import Quantity, Unit
2
+
3
+
4
+ # fmt: off
5
+ __all__ = [
6
+ "AmountOfSubstance",
7
+ "moles", "mole", "mol",
8
+ ]
9
+ # fmt: on
10
+
11
+
12
+ class AmountOfSubstance(Quantity):
13
+ pass
14
+
15
+
16
+ moles = mole = mol = Unit[AmountOfSubstance]("mol", 1)
@@ -0,0 +1,24 @@
1
+ from math import pi
2
+
3
+ from ..quantity_and_unit import Quantity, Unit
4
+
5
+
6
+ # fmt: off
7
+ __all__ = [
8
+ "Angle",
9
+ "radians", "radian", "rad",
10
+ "degrees", "degree", "deg",
11
+ "gradians", "gradian", "gons", "gon",
12
+ "turns", "turn", "tr",
13
+ ]
14
+ # fmt: on
15
+
16
+
17
+ class Angle(Quantity):
18
+ pass
19
+
20
+
21
+ radians = radian = rad = Unit[Angle]("rad", 1)
22
+ degrees = degree = deg = Unit[Angle]("°", 2 * pi / 360)
23
+ gradians = gradian = gons = gon = Unit[Angle]("ᵍ", 2 * pi / 400)
24
+ turns = turn = tr = Unit[Angle]("tr", 2 * pi)
@@ -0,0 +1,17 @@
1
+ from ..quantity_and_unit import Mul
2
+ from .distance import Distance, meter, kilometer
3
+
4
+
5
+ # fmt: off
6
+ __all__ = [
7
+ "Area",
8
+ "square_meters", "square_meter", "m2",
9
+ "square_kilometers", "square_kilometer", "km2",
10
+ ]
11
+ # fmt: on
12
+
13
+
14
+ Area = Mul[Distance, Distance]
15
+
16
+ square_meters = square_meter = m2 = meter * meter
17
+ square_kilometers = square_kilometer = km2 = kilometer * kilometer
@@ -0,0 +1,24 @@
1
+ from ..quantity_and_unit import Div
2
+ from .data_volume import DataVolume, bytes, kilobytes, megabytes, gigabytes, terabytes
3
+ from .duration import Duration, second
4
+
5
+
6
+ # fmt: off
7
+ __all__ = [
8
+ "DataTransferSpeed",
9
+ "bytes_per_second", "Bps",
10
+ "kilobytes_per_second", "Bps",
11
+ "megabytes_per_second", "MBps",
12
+ "gigabytes_per_second", "GBps",
13
+ "terabytes_per_second", "TBps",
14
+ ]
15
+ # fmt: on
16
+
17
+
18
+ DataTransferSpeed = Div[DataVolume, Duration]
19
+
20
+ bytes_per_second = Bps = bytes / second
21
+ kilobytes_per_second = KBps = kilobytes / second
22
+ megabytes_per_second = MBps = megabytes / second
23
+ gigabytes_per_second = GBps = gigabytes / second
24
+ terabytes_per_second = TBps = terabytes / second
@@ -0,0 +1,44 @@
1
+ # fmt: off
2
+ from ..prefixes import (
3
+ kilo, mega, giga, tera, peta, exa, zetta, yotta,
4
+ kibi, mebi, gibi, tebi, pebi, exbi, zebi, yobi,
5
+ )
6
+ # fmt: on
7
+ from ..quantity_and_unit import Quantity, Unit
8
+
9
+
10
+ # fmt: off
11
+ __all__ = [
12
+ "DataVolume",
13
+ "bytes", "byte", "B",
14
+ ]
15
+ # fmt: on
16
+
17
+
18
+ class DataVolume(Quantity):
19
+ pass
20
+
21
+
22
+ # TODO: Bits don't really fit in here. I initially wanted to define a byte as 8 bits, but that's not
23
+ # true - 8 bits is an octet. Bytes don't have a fixed amount of bits.
24
+ # bits = bit = b = Unit[FileSize]('b', 1)
25
+
26
+ bytes = byte = B = Unit[DataVolume]("B", 1)
27
+
28
+ kilobytes = kilobyte = kB = kilo(bytes)
29
+ megabytes = megabyte = MB = mega(bytes)
30
+ gigabytes = gigabyte = GB = giga(bytes)
31
+ terabytes = terabyte = TB = tera(bytes)
32
+ petabytes = petabyte = PB = peta(bytes)
33
+ exabytes = exabyte = EB = exa(bytes)
34
+ zettabytes = zettabyte = ZB = zetta(bytes)
35
+ yottabytes = yottabyte = YB = yotta(bytes)
36
+
37
+ kibibytes = kibibyte = kiB = kibi(bytes)
38
+ mebibytes = mebibyte = MiB = mebi(bytes)
39
+ gibibytes = gibibyte = GiB = gibi(bytes)
40
+ tebibytes = tebibyte = TiB = tebi(bytes)
41
+ pebibytes = pebibyte = PiB = pebi(bytes)
42
+ exbibytes = exbibyte = EiB = exbi(bytes)
43
+ zebibytes = zebibyte = ZiB = zebi(bytes)
44
+ yobibytes = yobibyte = YiB = yobi(bytes)
@@ -0,0 +1,38 @@
1
+ from ..prefixes import nano, micro, milli, centi, deci, kilo
2
+ from ..quantity_and_unit import Quantity, Unit
3
+
4
+
5
+ # fmt: off
6
+ __all__ = [
7
+ "Distance",
8
+ "nanometers", "nanometer", "nm",
9
+ "micrometers", "micrometer", "μm",
10
+ "millimeters", "millimeter", "mm",
11
+ "centimeters", "centimeter", "cm",
12
+ "decimeters", "decimeter", "dm",
13
+ "meters", "meter", "m",
14
+ "kilometers", "kilometer", "km",
15
+ "light_seconds", "light_second", "ls",
16
+ "light_years", "light_year", "ly",
17
+ "astronomical_units", "astronomical_unit", "au",
18
+ "parsecs", "parsec", "pc",
19
+ ]
20
+ # fmt: on
21
+
22
+
23
+ class Distance(Quantity):
24
+ pass
25
+
26
+
27
+ meters = meter = m = Unit[Distance]("m", 1)
28
+ light_seconds = light_second = ls = Unit[Distance]("ls", 299_792_458)
29
+ light_years = light_year = ly = Unit[Distance]("ly", 9_460_730_472_580_800)
30
+ astronomical_units = astronomical_unit = au = Unit[Distance]("au", 149_597_870_700)
31
+ parsecs = parsec = pc = Unit[Distance]("pc", 3.085_677_581_491_367_3e16)
32
+
33
+ nanometers = nanometer = nm = nano(meter)
34
+ micrometers = micrometer = μm = micro(meter)
35
+ millimeters = millimeter = mm = milli(meter)
36
+ centimeters = centimeter = cm = centi(meter)
37
+ decimeters = decimeter = dm = deci(meter)
38
+ kilometers = kilometer = km = kilo(meter)
@@ -0,0 +1,32 @@
1
+ from ..quantity_and_unit import Quantity, Unit
2
+
3
+
4
+ # fmt: off
5
+ __all__ = [
6
+ "Duration",
7
+ "seconds", "second", "sec", "s",
8
+ "minutes", "minute", "min",
9
+ "hours", "hour", "h",
10
+ "days", "day", "d",
11
+ "weeks", "week", "wk", "w",
12
+ "years", "year", "yr", "y",
13
+ "decades", "decade", "dec",
14
+ "centuries", "century", "cent", "c",
15
+ "millenia", "millenium", "mil", "ka", "ky",
16
+ ]
17
+ # fmt: on
18
+
19
+
20
+ class Duration(Quantity):
21
+ pass
22
+
23
+
24
+ seconds = second = sec = s = Unit[Duration]("s", 1)
25
+ minutes = minute = min = Unit[Duration]("min", 60)
26
+ hours = hour = h = Unit[Duration]("h", 3_600)
27
+ days = day = d = Unit[Duration]("d", 86_400)
28
+ weeks = week = wk = w = Unit[Duration]("wk", 86_400)
29
+ years = year = yr = y = Unit[Duration]("yr", 31_557_600)
30
+ decades = decade = dec = Unit[Duration]("dec", 315_576_000)
31
+ centuries = century = cent = c = Unit[Duration]("cent", 3_155_760_000)
32
+ millenia = millenium = mil = ka = ky = Unit[Duration]("mil", 31_557_600_000)
@@ -0,0 +1,17 @@
1
+ from ..quantity_and_unit import Mul
2
+ from .duration import Duration, seconds
3
+ from .electric_current import ElectricCurrent, amperes
4
+
5
+
6
+ # fmt: off
7
+ __all__ = [
8
+ "ElectricCharge",
9
+ "coulombs", "coulomb", "C",
10
+ ]
11
+ # fmt: on
12
+
13
+
14
+ ElectricCharge = Mul[Duration, ElectricCurrent]
15
+
16
+ coulombs = coulomb = C = seconds * amperes
17
+ coulombs.symbol = "C"
@@ -0,0 +1,16 @@
1
+ from ..quantity_and_unit import Quantity, Unit
2
+
3
+
4
+ # fmt: off
5
+ __all__ = [
6
+ "ElectricCurrent",
7
+ "amperes", "ampere", "A",
8
+ ]
9
+ # fmt: on
10
+
11
+
12
+ class ElectricCurrent(Quantity):
13
+ pass
14
+
15
+
16
+ amperes = ampere = A = Unit[ElectricCurrent]("A", 1)
@@ -0,0 +1,12 @@
1
+ from ..quantity_and_unit import Div
2
+ from .duration import Duration, seconds
3
+ from .one import One, one
4
+
5
+
6
+ __all__ = ["Frequency", "hertz", "hertzes", "Hz"]
7
+
8
+
9
+ Frequency = Div[One, Duration]
10
+
11
+ hertz = hertzes = Hz = one / seconds
12
+ hertz.symbol = "Hz"
@@ -0,0 +1,11 @@
1
+ from ..quantity_and_unit import Quantity, Unit
2
+
3
+
4
+ __all__ = ["LuminousIntensity", "candelas", "candela", "cd"]
5
+
6
+
7
+ class LuminousIntensity(Quantity):
8
+ pass
9
+
10
+
11
+ candelas = candela = cd = Unit[LuminousIntensity]("cd", 1)
@@ -0,0 +1,37 @@
1
+ from ..prefixes import nano, micro, milli, centi, deci, kilo, mega
2
+ from ..quantity_and_unit import Quantity, Unit
3
+
4
+
5
+ # fmt: off
6
+ __all__ = [
7
+ "Mass",
8
+ "nanograms", "nanogram", "ng",
9
+ "micrograms", "microgram", "μg",
10
+ "milligrams", "milligram", "mg",
11
+ "centigrams", "centigram", "cg",
12
+ "decigrams", "decigram", "dg",
13
+ "grams", "gram", "g",
14
+ "kilograms", "kilogram", "kg",
15
+ "tons", "ton", "t",
16
+ "kilotons", "kiloton", "kt",
17
+ "megatons", "megaton", "Mt",
18
+ ]
19
+ # fmt: on
20
+
21
+
22
+ class Mass(Quantity):
23
+ pass
24
+
25
+
26
+ grams = gram = g = Unit[Mass]("g", 1)
27
+ tons = ton = t = Unit[Mass]("t", 1_000_000)
28
+
29
+ nanograms = nanogram = ng = nano(grams)
30
+ micrograms = microgram = μg = micro(grams)
31
+ milligrams = milligram = mg = milli(grams)
32
+ centigrams = centigram = cg = centi(grams)
33
+ decigrams = decigram = dg = deci(grams)
34
+ kilograms = kilogram = kg = kilo(grams)
35
+
36
+ kilotons = kiloton = kt = kilo(tons)
37
+ megatons = megaton = Mt = mega(tons)
@@ -0,0 +1,14 @@
1
+ from ..quantity_and_unit import Quantity, Unit
2
+
3
+
4
+ __all__ = ["One", "ones", "one"]
5
+
6
+
7
+ class One(Quantity):
8
+ """
9
+ Represents the absence of a quantity. Used to create "inverted" quantities like `Frequency =
10
+ Div[One, Duration]`.
11
+ """
12
+
13
+
14
+ ones = one = Unit[One]("1", 1)
@@ -0,0 +1,11 @@
1
+ from ..quantity_and_unit import Quantity, Unit
2
+
3
+
4
+ __all__ = ["Pixels", "pixels", "pixel", "px"]
5
+
6
+
7
+ class Pixels(Quantity):
8
+ pass
9
+
10
+
11
+ pixels = pixel = px = Unit[Pixels]("px", 1)
@@ -0,0 +1,12 @@
1
+ from ..quantity_and_unit import Div
2
+ from .duration import Duration, seconds, hours
3
+ from .distance import Distance, meters, kilometers
4
+
5
+
6
+ __all__ = ["Speed", "meters_per_second", "mps", "kilometers_per_hour", "kph"]
7
+
8
+
9
+ Speed = Div[Distance, Duration]
10
+
11
+ meters_per_second = mps = meters / seconds
12
+ kilometers_per_hour = kph = kilometers / hours
@@ -0,0 +1,13 @@
1
+ from ..quantity_and_unit import Quantity, Unit
2
+
3
+ __all__ = ["Temperature", "kelvins", "kelvin", "K"]
4
+
5
+
6
+ class Temperature(Quantity):
7
+ pass
8
+
9
+
10
+ # FIXME: What happens if you add two temperatures?
11
+ kelvins = kelvin = K = Unit[Temperature]("K", 1)
12
+ # celsius = C = Unit[Temperature]("°C", offset=273.15)
13
+ # fahrenheit = F = Unit[Temperature]("°F", formula=((BASE_UNIT - 273.15) * 1.8) + 32)
@@ -0,0 +1,157 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from typing import Generic, TypeVar, Union
5
+ from typing_extensions import Self
6
+
7
+ from ._utils import cached
8
+
9
+
10
+ __all__ = ["Quantity", "Mul", "Div", "Unit"]
11
+
12
+
13
+ class Quantity:
14
+ """
15
+ Represents a measurable quantity, like mass or acceleration.
16
+
17
+ FIXME: Correct docstring
18
+
19
+ In order to be able to represent compound quantities (like speed, which is distance divided by
20
+ time), this is a generic class that accepts an arbitrary number of type arguments. Each type
21
+ argument represents either a multiplication or, through the use of `Inv`, a division.
22
+
23
+ Examples:
24
+
25
+ ```
26
+ Distance = Quantity[DISTANCE] # just distance
27
+ Area = Quantity[DISTANCE, DISTANCE] # distance squared
28
+ Speed = Quantity[DISTANCE, Inv[DURATION]] # distance divided by time
29
+ ```
30
+
31
+ Unfortunately this isn't ideal: Because type checkers care about the order of the arguments,
32
+ they treat `Quantity[A, B]` as different from `Quantity[B, A]`.
33
+ """
34
+
35
+ def __init__(self, value: float, unit: Unit[Self]):
36
+ self._value = value
37
+ self.unit = unit
38
+
39
+ def to(self, unit: Unit[Self]) -> Self:
40
+ return Quantity(self.to_number(unit), unit) # type: ignore
41
+
42
+ def to_number(self, unit: Unit[Self]) -> float:
43
+ return self._value * (self.unit.multiplier / unit.multiplier)
44
+
45
+ def __eq__(self, quantity: object, /) -> bool:
46
+ if not isinstance(quantity, __class__):
47
+ return NotImplemented
48
+
49
+ num = self.to_number(quantity.unit) # type: ignore
50
+ return math.isclose(num, quantity._value)
51
+
52
+ def __lt__(self, quantity: object, /) -> bool:
53
+ if not isinstance(quantity, __class__):
54
+ return NotImplemented
55
+
56
+ num = self.to_number(quantity.unit) # type: ignore
57
+ return num < quantity._value and not math.isclose(num, quantity._value)
58
+
59
+ def __le__(self, quantity: object, /) -> bool:
60
+ if not isinstance(quantity, __class__):
61
+ return NotImplemented
62
+
63
+ num = self.to_number(quantity.unit) # type: ignore
64
+ return num < quantity._value or math.isclose(num, quantity._value)
65
+
66
+ def __gt__(self, quantity: object, /) -> bool:
67
+ if not isinstance(quantity, __class__):
68
+ return NotImplemented
69
+
70
+ num = self.to_number(quantity.unit) # type: ignore
71
+ return num > quantity._value and not math.isclose(num, quantity._value)
72
+
73
+ def __ge__(self, quantity: object, /) -> bool:
74
+ if not isinstance(quantity, __class__):
75
+ return NotImplemented
76
+
77
+ num = self.to_number(quantity.unit) # type: ignore
78
+ return num > quantity._value or math.isclose(num, quantity._value)
79
+
80
+ def __add__(self, quantity: Self) -> Self:
81
+ return Quantity(
82
+ self._value + quantity.to_number(self.unit),
83
+ self.unit, # type: ignore
84
+ ) # type: ignore
85
+
86
+ def __sub__(self, quantity: Self) -> Self:
87
+ return Quantity(
88
+ self._value - quantity.to_number(self.unit),
89
+ self.unit, # type: ignore
90
+ ) # type: ignore
91
+
92
+ def __mul__(self, quantity: Quantity) -> Self:
93
+ return Quantity(
94
+ self._value * quantity._value,
95
+ self.unit * quantity.unit, # type: ignore
96
+ ) # type: ignore
97
+
98
+ def __truediv__(self, quantity: Quantity) -> Self:
99
+ return Quantity(
100
+ self._value / quantity._value,
101
+ self.unit / quantity.unit, # type: ignore
102
+ ) # type: ignore
103
+
104
+ def __repr__(self) -> str:
105
+ return f"{self._value}{self.unit.symbol}"
106
+
107
+
108
+ Q = TypeVar("Q", bound=Quantity)
109
+ Q2 = TypeVar("Q2", bound=Quantity)
110
+
111
+
112
+ class _Mul(Generic[Q, Q2], Quantity): ...
113
+
114
+
115
+ # Make multiplication commutative, otherwise type checkers would complain every time you do a
116
+ # multiplication in the "wrong" order. For example:
117
+ #
118
+ # electric_charge: Mul[Duration, ElectricCurrent] = ampere(1) * second(1)
119
+ Mul = Union[_Mul[Q, Q2], _Mul[Q2, Q]]
120
+
121
+
122
+ class Div(Generic[Q, Q2], Quantity): ...
123
+
124
+
125
+ class Unit(Generic[Q]):
126
+ """
127
+ Represents a unit of measurement for a certain `Quantity`.
128
+
129
+ To create your own unit, simply create an instance of this class. Make sure to pass the
130
+ corresponding `Quantity` as a type argument as seen here:
131
+
132
+ ```
133
+ seconds = Unit[Duration]("s", 1)
134
+ minutes = Unit[Duration]("min", 60)
135
+ ```
136
+ """
137
+
138
+ def __init__(self, symbol: str, multiplier: float):
139
+ self.symbol = symbol
140
+ self.multiplier = multiplier
141
+
142
+ @cached
143
+ def __mul__(self, other: Unit[Q2]) -> Unit[Mul[Q, Q2]]:
144
+ return Unit(
145
+ f"{self.symbol}*{other.symbol}",
146
+ self.multiplier * other.multiplier,
147
+ )
148
+
149
+ @cached
150
+ def __truediv__(self, other: Unit[Q2]) -> Unit[Div[Q, Q2]]:
151
+ return Unit(f"{self.symbol}/{other.symbol}", self.multiplier / other.multiplier)
152
+
153
+ def __call__(self, value: float) -> Q:
154
+ return Quantity(value, self) # type: ignore
155
+
156
+ def __rmul__(self, value: float) -> Q:
157
+ return Quantity(value, self) # type: ignore