瀏覽代碼

Merge pull request #568 from Goju-Ryu/mixed-units-conversion

Mixed unit list conversion
David Peter 1 年之前
父節點
當前提交
ced476bf33

+ 4 - 4
book/build.py

@@ -16,14 +16,14 @@ def generate_example(
     print(path_out)
 
     code = []
-    with open(path_in, "r") as fin:
+    with open(path_in, "r", encoding="utf-8") as fin:
         for line in fin:
             if not (strip_asserts and "assert_eq" in line):
                 code.append(line)
 
     url = f"https://numbat.dev/?q={urllib.parse.quote_plus(''.join(code))}"
 
-    with open(path_out, "w") as fout:
+    with open(path_out, "w", encoding="utf-8") as fout:
         fout.write("<!-- This file is autogenerated! Do not modify it -->\n")
         fout.write("\n")
         fout.write(f"# {title}\n")
@@ -81,7 +81,7 @@ generate_example(
 )
 
 path_units = SCRIPT_DIR / "src" / "list-units.md"
-with open(path_units, "w") as f:
+with open(path_units, "w", encoding="utf-8") as f:
     print("Generating list of units...", flush=True)
     subprocess.run(
         ["cargo", "run", "--release", "--quiet", "--example=inspect", "units"],
@@ -92,7 +92,7 @@ with open(path_units, "w") as f:
 
 def list_of_functions(file_name, document):
     path = SCRIPT_DIR / "src" / f"list-functions-{file_name}.md"
-    with open(path, "w") as f:
+    with open(path, "w", encoding="utf-8") as f:
         print(f"# {document['title']}\n", file=f, flush=True)
 
         if introduction := document.get("introduction"):

+ 25 - 8
book/src/list-functions-other.md

@@ -155,12 +155,29 @@ Get the ionization energy of hydrogen.
 
 Defined in: `units::mixed`
 
+### `unit_list` (Unit list)
+Convert a value to a mixed representation using the provided units.
+
+```nbt
+fn unit_list<D: Dim>(units: List<D>, value: D) -> List<D>
+```
+
+<details>
+<summary>Examples</summary>
+
+<pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=5500%20m%20%7C%3E%20unit%5Flist%28%5Bmiles%2C%20yards%2C%20feet%2C%20inches%5D%29')""></button></div><code class="language-nbt hljs numbat">>>> 5500 m |> unit_list([miles, yards, feet, inches])
+
+    = [3 mi, 734 yd, 2 ft, 7.43307 in]    [List<Length>]
+</code></pre>
+
+</details>
+
 ### `DMS` (Degrees, minutes, seconds)
 Convert an angle to a mixed degrees, (arc)minutes, and (arc)seconds representation. Also called sexagesimal degree notation.
 More information [here](https://en.wikipedia.org/wiki/Sexagesimal_degree).
 
 ```nbt
-fn DMS(alpha: Angle) -> String
+fn DMS(alpha: Angle) -> List<Angle>
 ```
 
 <details>
@@ -168,7 +185,7 @@ fn DMS(alpha: Angle) -> String
 
 <pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=46%2E5858%C2%B0%20%2D%3E%20DMS')""></button></div><code class="language-nbt hljs numbat">>>> 46.5858° -> DMS
 
-    = "46° 35′ 9″"    [String]
+    = [46°, 35′, 8.88″]    [List<Scalar>]
 </code></pre>
 
 </details>
@@ -178,7 +195,7 @@ Convert an angle to a mixed degrees and decimal minutes representation.
 More information [here](https://en.wikipedia.org/wiki/Decimal_degrees).
 
 ```nbt
-fn DM(alpha: Angle) -> String
+fn DM(alpha: Angle) -> List<Angle>
 ```
 
 <details>
@@ -186,7 +203,7 @@ fn DM(alpha: Angle) -> String
 
 <pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=46%2E5858%C2%B0%20%2D%3E%20DM')""></button></div><code class="language-nbt hljs numbat">>>> 46.5858° -> DM
 
-    = "46° 35.148′"    [String]
+    = [46°, 35.148′]    [List<Scalar>]
 </code></pre>
 
 </details>
@@ -196,7 +213,7 @@ Convert a length to a mixed feet and inches representation.
 More information [here](https://en.wikipedia.org/wiki/Foot_(unit)).
 
 ```nbt
-fn feet_and_inches(length: Length) -> String
+fn feet_and_inches(length: Length) -> List<Length>
 ```
 
 <details>
@@ -204,7 +221,7 @@ fn feet_and_inches(length: Length) -> String
 
 <pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=180%20cm%20%2D%3E%20feet%5Fand%5Finches')""></button></div><code class="language-nbt hljs numbat">>>> 180 cm -> feet_and_inches
 
-    = "5 ft 10.8661 in"    [String]
+    = [5 ft, 10.8661 in]    [List<Length>]
 </code></pre>
 
 </details>
@@ -214,7 +231,7 @@ Convert a mass to a mixed pounds and ounces representation.
 More information [here](https://en.wikipedia.org/wiki/Pound_(mass)).
 
 ```nbt
-fn pounds_and_ounces(mass: Mass) -> String
+fn pounds_and_ounces(mass: Mass) -> List<Mass>
 ```
 
 <details>
@@ -222,7 +239,7 @@ fn pounds_and_ounces(mass: Mass) -> String
 
 <pre><div class="buttons"><button class="fa fa-play play-button" title="Run this code" aria-label="Run this code"  onclick=" window.open('https://numbat.dev/?q=1%20kg%20%2D%3E%20pounds%5Fand%5Founces')""></button></div><code class="language-nbt hljs numbat">>>> 1 kg -> pounds_and_ounces
 
-    = "2 lb 3.27396 oz"    [String]
+    = [2 lb, 3.27396 oz]    [List<Mass>]
 </code></pre>
 
 </details>

+ 31 - 16
examples/tests/mixed_units.nbt

@@ -4,33 +4,48 @@
 assert_eq(38° + 53′ + 23″, 38.8897°, 1e-4°)
 assert_eq(-(77° + 0′ + 32″), -77.0089°, 1e-4°)
 
-assert_eq(38.8897° -> DMS, "38° 53′ 23″")
-assert_eq(-77.0089° -> DMS, "-77° 0′ 32″")
+assert_eq("{38.8897° -> DMS}", "{[38°, 53′, 22.92″]}")
+assert_eq("{-77.0089° -> DMS}", "{[-77°, -0′, -32.04]}")
 
 ## Stuttgart
 assert_eq(48° + 46′ + 32″, 48.7756°, 1e-4°)
 assert_eq(9° + 10′ + 58″, 9.1828°, 1e-4°)
 
-assert_eq(48.7756° -> DMS, "48° 46′ 32″")
-assert_eq(9.1828° -> DMS, "9° 10′ 58″")
+assert_eq("{48.7756° -> DMS}", "{[48°, 46′, 32.16]}")
+assert_eq("{9.1828° -> DMS}", "{[, 10′, 58.08]}")
 
 # Degrees, decimal minutes (DM)
 
-assert_eq(38.8897° -> DM, "38° 53.382′")
-assert_eq(-77.0089° -> DM, "-77° 0.534′")
+assert_eq("{38.8897° -> DM}", "{[38°, 53.382′]}")
+assert_eq("{-77.0089° -> DM}", "{[-77°, -0.534′]}")
 
 # Feet and inches
 
-assert_eq(5.5 ft -> feet_and_inches, "5 ft 6 in")
-assert_eq(6.75 ft -> feet_and_inches, "6 ft 9 in")
-assert_eq(-5.5 ft -> feet_and_inches, "-5 ft 6 in")
-assert_eq(0 -> feet_and_inches, "0 ft 0 in")
-assert_eq(1 ft -> feet_and_inches, "1 ft 0 in")
-assert_eq(2.345 inch -> feet_and_inches, "0 ft 2.345 in")
+assert_eq("{5.5 ft -> feet_and_inches}", "{[5 ft, 6 in]}")
+assert_eq("{6.75 ft -> feet_and_inches}", "{[6 ft, 9 in]}")
+assert_eq("{-5.5 ft -> feet_and_inches}", "{[-5 ft, -6 in]}")
+assert_eq("{0 -> feet_and_inches}", "{[0 ft, 0 in]}")
+assert_eq("{1 ft -> feet_and_inches}", "{[1 ft, 0 in]}")
+assert_eq("{2.345 inch -> feet_and_inches}", "{[0 ft, 2.345 in]}")
 
 # Pounds and ounces
 
-assert_eq(5 lb -> pounds_and_ounces, "5 lb 0 oz")
-assert_eq(5.5 lb -> pounds_and_ounces, "5 lb 8 oz")
-assert_eq(6.75 lb -> pounds_and_ounces, "6 lb 12 oz")
-assert_eq(-5.5 lb -> pounds_and_ounces, "-5 lb 8 oz")
+assert_eq("{5 lb -> pounds_and_ounces}", "{[5 lb, 0 oz]}")
+assert_eq("{5.5 lb -> pounds_and_ounces}", "{[5 lb, 8 oz]}")
+assert_eq("{6.75 lb -> pounds_and_ounces}", "{[6 lb, 12 oz]}")
+assert_eq("{-5.5 lb -> pounds_and_ounces}", "{[-5 lb, -8 oz]}")
+
+# Unit list
+
+let test1 = 12 m + 34 cm + 5 mm + 678 µm
+assert_eq(test1 |> unit_list([m]) |> head, test1)
+assert_eq(test1 |> unit_list([m, cm]) |> sum, test1)
+assert_eq(test1 |> unit_list([m, cm, mm]) |> sum, test1)
+assert_eq(test1 |> unit_list([m, cm, mm, µm]) |> sum, test1)
+
+let test2 = 12 degree + 34 arcminute + 5 arcsec
+assert_eq(test2 |> unit_list([degree]) |> head, test2)
+assert_eq(test2 |> unit_list([degree, arcmin]) |> sum, test2)
+assert_eq(test2 |> unit_list([degree, arcmin, arcsec]) |> sum, test2)
+
+

+ 11 - 21
numbat/modules/core/mixed_units.nbt

@@ -3,25 +3,15 @@ use core::lists
 
 # Helper functions for mixed-unit conversions. See units::mixed for more.
 
-fn _mixed_units_helper<D: Dim>(q: D, units: List<D>, names: List<String>, round_last: Bool) -> List<String> =
-  if is_empty(units)
-    then
-      []
-    else
-      cons(
-        if len(units) == 1
-          then
-            if round_last
-              then "{round(q / head(units))}{head(names)}"
-              else "{q / head(units)}{head(names)}"
-          else "{trunc(q / head(units))}{head(names)}",
-        _mixed_units_helper(
-          q - trunc(q / head(units)) * head(units),
-          tail(units),
-          tail(names),
-          round_last))
+fn _zero_length<A: Dim>(val: A) -> A = val * 0 -> val
 
-fn _mixed_units<D: Dim>(q: D, units: List<D>, names: List<String>, round_last: Bool) -> String =
-  if q < 0
-    then str_append("-", _mixed_units(-q, units, names, round_last))
-    else join(_mixed_units_helper(q, units, names, round_last), "")
+fn _mixed_unit_list<D: Dim>(val: D, units: List<D>, acc: List<D>) -> List<D> =
+  if val == 0
+    then concat(acc, map(_zero_length, units))
+    else if len(units) == 1
+      then cons_end(val -> head(units), acc)
+      else _mixed_unit_list(val - unit_val, tail(units), cons_end(unit_val, acc))
+  where unit_val: D =
+    if (len(units) > 0)
+      then (val |> trunc_in(head(units)))
+      else error("Units list cannot be empty")

+ 15 - 9
numbat/modules/units/mixed.nbt

@@ -2,30 +2,36 @@ use core::mixed_units
 use units::si
 use units::imperial
 
+@name("Unit list")
+@description("Convert a value to a mixed representation using the provided units.")
+@example("5500 m |> unit_list([miles, yards, feet, inches])")
+fn unit_list<D: Dim>(units: List<D>, value: D) -> List<D> = _mixed_unit_list(value, units, [])
+
 @name("Degrees, minutes, seconds")
 @description("Convert an angle to a mixed degrees, (arc)minutes, and (arc)seconds representation. Also called sexagesimal degree notation.")
 @url("https://en.wikipedia.org/wiki/Sexagesimal_degree")
 @example("46.5858° -> DMS")
-fn DMS(alpha: Angle) -> String =
-  _mixed_units(alpha, [deg, arcmin, arcsec], ["° ", "′ ", "″"], true)
+fn DMS(alpha: Angle) -> List<Angle> =
+  unit_list([degree, arcminute, arcsecond], alpha)
 
 @name("Degrees, decimal minutes")
 @description("Convert an angle to a mixed degrees and decimal minutes representation.")
-@example("46.5858° -> DM")
 @url("https://en.wikipedia.org/wiki/Decimal_degrees")
-fn DM(alpha: Angle) -> String =
-  _mixed_units(alpha, [deg, arcmin], ["° ", "′"], false)
+@example("46.5858° -> DM")
+fn DM(alpha: Angle) -> List<Angle> =
+  unit_list([degree, arcminute], alpha)
 
 @name("Feet and inches")
 @description("Convert a length to a mixed feet and inches representation.")
 @url("https://en.wikipedia.org/wiki/Foot_(unit)")
 @example("180 cm -> feet_and_inches")
-fn feet_and_inches(length: Length) -> String =
-  _mixed_units(length, [foot, inch], [" ft ", " in"], false)
+fn feet_and_inches(length: Length) -> List<Length> =
+  unit_list([foot, inch], length)
 
 @name("Pounds and ounces")
 @description("Convert a mass to a mixed pounds and ounces representation.")
 @url("https://en.wikipedia.org/wiki/Pound_(mass)")
 @example("1 kg -> pounds_and_ounces")
-fn pounds_and_ounces(mass: Mass) -> String =
-  _mixed_units(mass, [pound, ounce], [" lb ", " oz"], false)
+fn pounds_and_ounces(mass: Mass) -> List<Mass> =
+  unit_list([pound, ounce], mass)
+

+ 2 - 2
numbat/modules/units/si.nbt

@@ -202,12 +202,12 @@ unit degree: Angle = π / 180 × radian
 
 @name("Minute of arc")
 @url("https://en.wikipedia.org/wiki/Minute_and_second_of_arc")
-@aliases(arcminutes, arcmin, ′)
+@aliases(arcminutes, arcmin, ′: short)
 unit arcminute: Angle = 1 / 60 × degree
 
 @name("Second of arc")
 @url("https://en.wikipedia.org/wiki/Minute_and_second_of_arc")
-@aliases(arcseconds, arcsec, ″)
+@aliases(arcseconds, arcsec, ″: short)
 unit arcsecond: Angle = 1 / 60 × arcminute
 
 @name("Are")