Repository: chrisguidry/measured (language: python)

Issue:
Horsepower is still commonly used, but it has many definitions
https://en.wikipedia.org/wiki/Horsepower#Definitions

`measured` should definitely include horsepower, and it should probably include all of these


Discussion / hints:

Fix patch:
diff --git a/src/measured/_parser.py b/src/measured/_parser.py
index b565d11..30e2c13 100644
--- a/src/measured/_parser.py
+++ b/src/measured/_parser.py
@@ -3556,79 +3556,79 @@ class PythonIndenter(Indenter):
         },
         "parser": {
             "tokens": {
-                0: "_MULTIPLY",
-                1: "__unit_sequence_plus_1",
-                2: "$END",
-                3: "SYMBOL",
+                0: "__unit_sequence_plus_1",
+                1: "_MULTIPLY",
+                2: "SYMBOL",
+                3: "$END",
                 4: "_DIVIDE",
                 5: "term",
                 6: "__unit_sequence_plus_0",
                 7: "unit_sequence",
-                8: "unit",
+                8: "exponent",
                 9: "CARAT_EXPONENT",
-                10: "exponent",
-                11: "SUPERSCRIPT_EXPONENT",
+                10: "SUPERSCRIPT_EXPONENT",
+                11: "magnitude",
                 12: "SIGNED_FLOAT",
-                13: "magnitude",
+                13: "quantity",
                 14: "SIGNED_INT",
-                15: "quantity",
+                15: "unit",
             },
             "states": {
                 0: {
                     0: (0, 22),
-                    1: (0, 4),
+                    1: (0, 6),
                     2: (1, {"@": 19}),
                     3: (1, {"@": 19}),
                     4: (1, {"@": 19}),
                 },
-                1: {3: (1, {"@": 9})},
-                2: {},
-                3: {},
-                4: {0: (0, 21), 2: (1, {"@": 14}), 4: (1, {"@": 14})},
-                5: {2: (1, {"@": 20}), 3: (1, {"@": 20}), 4: (1, {"@": 20})},
-                6: {
-                    2: (1, {"@": 17}),
-                    3: (1, {"@": 17}),
-                    0: (1, {"@": 17}),
-                    4: (1, {"@": 17}),
+                1: {3: (1, {"@": 22}), 4: (1, {"@": 22}), 1: (1, {"@": 22})},
+                2: {2: (0, 10), 5: (0, 0), 6: (0, 7), 7: (0, 13)},
+                3: {3: (1, {"@": 21}), 4: (1, {"@": 21}), 1: (1, {"@": 21})},
+                4: {},
+                5: {4: (0, 2), 3: (1, {"@": 12})},
+                6: {5: (0, 3), 2: (0, 10)},
+                7: {2: (0, 10), 5: (0, 11), 3: (1, {"@": 13}), 4: (1, {"@": 13})},
+                8: {3: (1, {"@": 8})},
+                9: {2: (0, 10), 5: (0, 1)},
+                10: {
+                    8: (0, 12),
+                    9: (0, 17),
+                    10: (0, 16),
+                    2: (1, {"@": 16}),
+                    3: (1, {"@": 16}),
+                    1: (1, {"@": 16}),
+                    4: (1, {"@": 16}),
                 },
-                7: {
+                11: {2: (1, {"@": 20}), 3: (1, {"@": 20}), 4: (1, {"@": 20})},
+                12: {
                     2: (1, {"@": 15}),
                     3: (1, {"@": 15}),
+                    1: (1, {"@": 15}),
                     4: (1, {"@": 15}),
-                    0: (1, {"@": 15}),
-                },
-                8: {2: (1, {"@": 11})},
-                9: {3: (0, 13), 5: (0, 5), 2: (1, {"@": 13}), 4: (1, {"@": 13})},
-                10: {6: (0, 9), 5: (0, 0), 3: (0, 13), 7: (0, 12), 8: (0, 17)},
-                11: {7: (0, 8), 6: (0, 9), 5: (0, 0), 3: (0, 13)},
-                12: {4: (0, 11), 2: (1, {"@": 12})},
-                13: {
-                    9: (0, 6),
-                    10: (0, 7),
-                    11: (0, 19),
-                    2: (1, {"@": 16}),
-                    3: (1, {"@": 16}),
-                    4: (1, {"@": 16}),
-                    0: (1, {"@": 16}),
                 },
-                14: {6: (0, 9), 8: (0, 3), 5: (0, 0), 3: (0, 13), 7: (0, 12)},
-                15: {3: (1, {"@": 10})},
-                16: {2: (1, {"@": 21}), 0: (1, {"@": 21}), 4: (1, {"@": 21})},
-                17: {2: (1, {"@": 8})},
-                18: {12: (0, 15), 13: (0, 10), 14: (0, 1), 15: (0, 2)},
-                19: {
+                13: {3: (1, {"@": 11})},
+                14: {},
+                15: {11: (0, 18), 12: (0, 21), 13: (0, 14), 14: (0, 20)},
+                16: {
                     2: (1, {"@": 18}),
                     3: (1, {"@": 18}),
-                    0: (1, {"@": 18}),
                     4: (1, {"@": 18}),
+                    1: (1, {"@": 18}),
+                },
+                17: {
+                    2: (1, {"@": 17}),
+                    3: (1, {"@": 17}),
+                    4: (1, {"@": 17}),
+                    1: (1, {"@": 17}),
                 },
-                20: {2: (1, {"@": 22}), 0: (1, {"@": 22}), 4: (1, {"@": 22})},
-                21: {3: (0, 13), 5: (0, 20)},
-                22: {5: (0, 16), 3: (0, 13)},
+                18: {6: (0, 7), 15: (0, 8), 5: (0, 0), 7: (0, 5), 2: (0, 10)},
+                19: {6: (0, 7), 15: (0, 4), 5: (0, 0), 7: (0, 5), 2: (0, 10)},
+                20: {2: (1, {"@": 9})},
+                21: {2: (1, {"@": 10})},
+                22: {1: (0, 9), 3: (1, {"@": 14}), 4: (1, {"@": 14})},
             },
-            "start_states": {"unit": 14, "quantity": 18},
-            "end_states": {"quantity": 2, "unit": 3},
+            "start_states": {"unit": 19, "quantity": 15},
+            "end_states": {"unit": 4, "quantity": 14},
         },
         "__type__": "ParsingFrontend",
     },
@@ -3750,7 +3750,7 @@ class PythonIndenter(Indenter):
     7: {
         "name": "SYMBOL",
         "pattern": {
-            "value": "(?:(?:(?:[A-Z]|Å)|(?:\\.|°|\\-)|[a-z]|[ₐ-ₜ]|[Α-ω]|1|☉))+",
+            "value": "(?:(?:(?:\\.|°|\\-|\\(|\\))|(?:[A-Z]|Å)|[a-z]|[ₐ-ₜ]|[Α-ω]|1|☉))+",
             "flags": [],
             "_width": [1, 4294967295],
             "__type__": "PatternRE",
diff --git a/src/measured/apocrypha.py b/src/measured/apocrypha.py
new file mode 100644
index 0000000..0b79e66
--- /dev/null
+++ b/src/measured/apocrypha.py
@@ -0,0 +1,6 @@
+from measured import Length
+from measured.us import Foot, Inch
+
+# https://en.wikipedia.org/wiki/Smoot
+Smoot = Length.unit("smoot", "smoot")
+Smoot.equals(5 * Foot + 7 * Inch)
diff --git a/src/measured/astronomical.py b/src/measured/astronomical.py
index fb306bc..1fc926e 100644
--- a/src/measured/astronomical.py
+++ b/src/measured/astronomical.py
@@ -35,10 +35,10 @@
     JupiterMass (Unit): The mass of Jupiter
 """
 
-from measured import Area, Length, Mass, Power, Time, Unit, si
+from measured import Area, Length, Mass, Power, Time, Unit, Volume, si
 from measured.geometry import π
 from measured.physics import c
-from measured.si import Kilogram, Meter, Second, Watt
+from measured.si import Kilogram, Mega, Meter, Second, Watt
 
 # Measures of Time
 
@@ -86,5 +86,29 @@
 
 # Derived
 
+# https://en.wikipedia.org/wiki/Jansky
 Jansky = (Power * Time / Area).unit("jansky", "Jy")
 Jansky.equals(1e-26 * Watt * Second / Meter**2)
+
+# https://en.wikipedia.org/wiki/Crab_(unit)
+Crab = (Power / Area).unit("crab", "crab")
+Crab.equals(2.4e-11 * Watt / Meter**2)
+
+
+# Hubble's Law
+
+# https://en.wikipedia.org/wiki/Hubble%27s_law#Time-dependence_of_Hubble_parameter
+# H0 is today's value of the Hubble parameter
+H0 = 67400 * ((Meter / Second) / (Mega * Parsec))
+
+# https://en.wikipedia.org/wiki/Hubble%27s_law#Units_derived_from_the_Hubble_constant
+HubbleTime = Time.unit("hubble time", "hubble-time")
+HubbleTime.equals(H0**-1)
+HubbleTime.equals((H0**-1).in_unit(Second))
+
+HubbleLength = Length.unit("hubble length", "hubble-length")
+HubbleLength.equals(c / H0)
+HubbleLength.equals(c * HubbleTime)
+
+HubbleVolume = Volume.unit("hubble volume", "hubble-volume")
+HubbleVolume.equals(4 / 3 * π * (1 * HubbleLength) ** 3)
diff --git a/src/measured/computing.py b/src/measured/computing.py
index 1135798..f674dc9 100644
--- a/src/measured/computing.py
+++ b/src/measured/computing.py
@@ -1,8 +1,13 @@
-from measured import PlaneAngle
+from measured import Information, PlaneAngle
 from measured.geometry import π
+from measured.iec import Bit
 from measured.si import Radian
 
 # https://en.wikipedia.org/wiki/List_of_unusual_units_of_measurement
 # The Furman is a unit of angular measure equal to 1/65,536 of a circle
 Furman = PlaneAngle.unit("Furman", "furman")
 Furman.equals(1 / 65536 * 2 * π * Radian)
+
+# https://en.wikipedia.org/wiki/Nibble
+Nibble = Information.unit("nibble", "nibble")
+Nibble.equals(4 * Bit)
diff --git a/src/measured/energy.py b/src/measured/energy.py
index a72029d..5664fcf 100644
--- a/src/measured/energy.py
+++ b/src/measured/energy.py
@@ -1,5 +1,10 @@
 from measured import Area, Energy, Power
-from measured.si import Hour, Joule, Kilo, Meter, Watt
+from measured.si import Hour, Joule, Kilo, Meter, Second, Watt
+from measured.us import Foot, PoundForce
+
+# https://en.wikipedia.org/wiki/Erg
+Erg = Energy.unit(name="erg", symbol="erg")
+Erg.equals(1e-7 * Joule)
 
 # https://en.wikipedia.org/wiki/Calorie
 Calorie = Energy.unit(name="calorie", symbol="cal")
@@ -22,3 +27,20 @@
 # https://en.wikipedia.org/wiki/Langley_(unit)
 Langley = (Energy / Area).unit("langley", "Ly")
 Langley.equals(41840 * Joule / Meter**2)
+
+
+# https://en.wikipedia.org/wiki/Horsepower#Definitions
+Horsepower = Power.unit("horsepower", "hp")
+Horsepower.equals(550 * Foot * PoundForce / Second)
+
+Donkeypower = Power.unit("donkeypower", "donkeypower")
+Donkeypower.equals(1 / 3 * Horsepower)
+
+MetricHorsepower = Power.unit("metric horsepower", "hp(M)")
+MetricHorsepower.equals(735.49875 * Watt)
+
+ElectricalHorsepower = Power.unit("electrical horsepower", "hp(E)")
+ElectricalHorsepower.equals(746 * Watt)
+
+BoilerHorsepower = Power.unit("boiler horsepower", "hp(S)")
+BoilerHorsepower.equals(33475 * BritishThermalUnit / Hour)
diff --git a/src/measured/hypothesis.py b/src/measured/hypothesis.py
index a0265cd..7b04c7a 100644
--- a/src/measured/hypothesis.py
+++ b/src/measured/hypothesis.py
@@ -11,7 +11,7 @@
 from measured import Dimension, Prefix, Quantity, Unit
 
 DIMENSIONS = sorted(Dimension.fundamental(), key=lambda d: d.exponents)
-PREFIXES = sorted(Prefix._known.values(), key=lambda p: (p.base, p.exponent))
+PREFIXES = sorted(Prefix._by_name.values(), key=lambda p: (p.base, p.exponent))
 BASE_UNITS = sorted(Unit.base(), key=lambda u: u.name or "")
 UNITS = sorted(Unit._known.values(), key=lambda u: u.name or "")
 for this in BASE_UNITS:
diff --git a/src/measured/measured.lark b/src/measured/measured.lark
index 1064151..bda8008 100644
--- a/src/measured/measured.lark
+++ b/src/measured/measured.lark
@@ -26,7 +26,7 @@ UPPER: "A".."Z" | "Å"
 SUBSCRIPTS: "ₐ".."ₜ"
 GREEK: "Α".."ω"
 OBJECTS: "☉"
-PUNCTUATION: "." | "°" | "-"
+PUNCTUATION: "." | "°" | "-" | "(" | ")"
 SYMBOL: ("1" | LOWER | UPPER | SUBSCRIPTS | GREEK | OBJECTS | PUNCTUATION)+
 
 %import common.SIGNED_INT
diff --git a/src/measured/si.py b/src/measured/si.py
index 77aaa11..44e4bd0 100644
--- a/src/measured/si.py
+++ b/src/measured/si.py
@@ -229,7 +229,7 @@
 
 # Length, Area, Volume
 Hectare = Area.unit(name="hectare", symbol="ha")
-Hectare.equals(1 * (Hecto * Meter) ** 2)
+Hectare.equals(10000 * Meter**2)
 
 Liter = Volume.unit(name="liter", symbol="L")
 Liter.equals(0.001 * Meter**3)


Test patch:
diff --git a/tests/test_apocrypha.py b/tests/test_apocrypha.py
new file mode 100644
index 0000000..23a5fd3
--- /dev/null
+++ b/tests/test_apocrypha.py
@@ -0,0 +1,8 @@
+from measured.apocrypha import Smoot
+from measured.si import Meter
+from measured.us import Foot
+
+
+def test_smoot() -> None:
+    (364.4 * Smoot).assert_approximates(2035 * Foot, within=3e-4)
+    (364.4 * Smoot).assert_approximates(620.1 * Meter, within=3e-4)
diff --git a/tests/test_astronomical.py b/tests/test_astronomical.py
index 06f6bf3..4bd12ac 100644
--- a/tests/test_astronomical.py
+++ b/tests/test_astronomical.py
@@ -1,6 +1,11 @@
 from measured.astronomical import (
+    H0,
     AstronomicalUnit,
+    Crab,
     EarthMass,
+    HubbleLength,
+    HubbleTime,
+    HubbleVolume,
     Jansky,
     JulianYear,
     JupiterMass,
@@ -10,9 +15,25 @@
     Siriometer,
     SolarMass,
 )
+from measured.energy import Erg
 from measured.geometry import π
+from measured.metric import Barn
 from measured.physics import G
-from measured.si import Hertz, Hour, Meter, Watt
+from measured.si import (
+    Atto,
+    Centi,
+    ElectronVolt,
+    Hertz,
+    Hour,
+    Kilo,
+    Liter,
+    Mega,
+    Meter,
+    Milli,
+    Second,
+    Watt,
+)
+from measured.us import Inch
 
 
 def test_astronomical_unit() -> None:
@@ -33,6 +54,15 @@ def test_light_year() -> None:
     (1 * LightYear).assert_approximates(0.306601 * Parsec, within=2e-6)
 
 
+def test_attoparsec() -> None:
+    (1 * Atto * Parsec).assert_approximates(3.086 * Centi * Meter, within=2e-4)
+    (1 * Atto * Parsec).assert_approximates(1.215 * Inch, within=2e-4)
+
+
+def test_barn_megaparsec() -> None:
+    (1 * Barn * (Mega * Parsec)).assert_approximates(3 * Milli * Liter, within=0.03)
+
+
 def test_deriving_solar_mass() -> None:
     # https://en.wikipedia.org/wiki/Solar_mass#Calculation
     calculated = (4 * π**2 * AstronomicalUnit**3) / (G * JulianYear**2)
@@ -47,8 +77,39 @@ def test_solar_system_masses() -> None:
 
 
 def test_sidereal_day() -> None:
+    (1 * SiderealDay).assert_approximates(86164.0905 * Second, within=0)
     (1 * SiderealDay).assert_approximates(23.9344696 * Hour, within=7e-10)
 
 
 def test_jansky() -> None:
     assert 1 * Jansky == 1e-26 * Watt / Meter**2 / Hertz
+
+
+def test_crab() -> None:
+    keV = Kilo * ElectronVolt
+    cm = Centi * Meter
+    s = Second
+
+    assert 1 * Crab == 2.4e-8 * Erg / (cm**2 * s)
+    (1 * Crab).assert_approximates(15 * keV / (cm**2 * s), within=1e-2)
+
+
+def test_hubble_constant() -> None:
+    H0.assert_approximates(((67400 * Meter) / (1000000 * Parsec)) / Second)
+
+
+def test_hubble_time() -> None:
+    (1 * HubbleTime).assert_approximates(14.4e9 * JulianYear, within=0.008)
+
+
+def test_hubble_length() -> None:
+    (1 * HubbleLength).assert_approximates(14.4e9 * LightYear, within=0.008)
+
+
+def test_hubble_volume() -> None:
+    (1 * HubbleVolume).assert_approximates(1e79 * Meter**3, within=0.09)
+    (1 * HubbleVolume).assert_approximates(1e31 * LightYear**3, within=0.28)
+
+
+def test_hubble_barn() -> None:
+    (1 * HubbleLength * Barn).assert_approximates(13.1 * Liter, within=0.05)
diff --git a/tests/test_computing.py b/tests/test_computing.py
index cd3129c..7962c7b 100644
--- a/tests/test_computing.py
+++ b/tests/test_computing.py
@@ -1,5 +1,6 @@
-from measured.computing import Furman
+from measured.computing import Furman, Nibble
 from measured.geometry import π
+from measured.iec import Bit, Byte
 from measured.si import Arcsecond, Degree, Radian
 
 
@@ -7,3 +8,8 @@ def test_furman() -> None:
     assert 65536 * Furman == 2 * π * Radian
     assert 65536 * Furman == 360 * Degree
     assert 19.77 * Arcsecond < 1 * Furman < 20 * Arcsecond
+
+
+def test_nibblee() -> None:
+    assert 1 * Nibble == 4 * Bit
+    assert 2 * Nibble == 1 * Byte
diff --git a/tests/test_energy.py b/tests/test_energy.py
index 4da54ff..8504bdc 100644
--- a/tests/test_energy.py
+++ b/tests/test_energy.py
@@ -1,7 +1,14 @@
 import pytest
 
 from measured import Quantity
-from measured.energy import BritishThermalUnit, Calorie, TonneOfTNT, TonOfRefrigeration
+from measured.energy import (
+    BritishThermalUnit,
+    Calorie,
+    Donkeypower,
+    Horsepower,
+    TonneOfTNT,
+    TonOfRefrigeration,
+)
 from measured.si import ElectronVolt, Hour, Joule, Kilo, Watt
 from measured.us import Foot, PoundForce
 
@@ -55,3 +62,11 @@ def test_refrigeration() -> None:
 def test_tnt_equivalent(equivalent: Quantity, within: float) -> None:
     (1 * TonneOfTNT).assert_approximates(equivalent, within=within)
     equivalent.assert_approximates(1 * TonneOfTNT, within=within)
+
+
+def test_horsepower() -> None:
+    (1 * Horsepower).assert_approximates(745.69987 * Watt)
+
+
+def test_donkeypower() -> None:
+    (1 * Donkeypower).assert_approximates(1 / 3 * Horsepower)