diff --git a/packages/units/LICENSE b/packages/units/LICENSE
new file mode 100644
index 0000000000..8dada3edaf
--- /dev/null
+++ b/packages/units/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "{}"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright {yyyy} {name of copyright owner}
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/packages/units/README.md b/packages/units/README.md
new file mode 100644
index 0000000000..35d0a3d8b8
--- /dev/null
+++ b/packages/units/README.md
@@ -0,0 +1,409 @@
+
+
+# ![@thi.ng/units](https://media.thi.ng/umbrella/banners-20220914/thing-units.svg?576826a0)
+
+[![npm version](https://img.shields.io/npm/v/@thi.ng/units.svg)](https://www.npmjs.com/package/@thi.ng/units)
+![npm downloads](https://img.shields.io/npm/dm/@thi.ng/units.svg)
+[![Mastodon Follow](https://img.shields.io/mastodon/follow/109331703950160316?domain=https%3A%2F%2Fmastodon.thi.ng&style=social)](https://mastodon.thi.ng/@toxi)
+
+This project is part of the
+[@thi.ng/umbrella](https://github.com/thi-ng/umbrella/) monorepo.
+
+- [About](#about)
+ - [Unit definitions](#unit-definitions)
+ - [Predefined units](#predefined-units)
+ - [Acceleration](#acceleration)
+ - [Angle](#angle)
+ - [Area](#area)
+ - [Data](#data)
+ - [Electric current](#electric-current)
+ - [Energy](#energy)
+ - [Force](#force)
+ - [Frequency](#frequency)
+ - [Length](#length)
+ - [Luminous intensity](#luminous-intensity)
+ - [Mass](#mass)
+ - [Power](#power)
+ - [Pressure](#pressure)
+ - [Speed](#speed)
+ - [Substance](#substance)
+ - [Temperature](#temperature)
+ - [Time](#time)
+ - [Volume](#volume)
+ - [Creating & deriving units](#creating--deriving-units)
+ - [Using standard metric prefixes](#using-standard-metric-prefixes)
+ - [Unit conversions](#unit-conversions)
+- [Status](#status)
+- [Installation](#installation)
+- [Dependencies](#dependencies)
+- [API](#api)
+- [Authors](#authors)
+- [License](#license)
+
+## About
+
+Extensible SI unit creation & conversions (130+ units predefined).
+
+All unit definitions & conversions are based on the SI units described here:
+
+- https://en.wikipedia.org/wiki/International_System_of_Units
+
+### Unit definitions
+
+Each unit is defined via a 7-dimensional vector representing individual exponents
+for each of the base SI dimensions, in order:
+
+| SI dimension | Base unit | Base unit symbol |
+|---------------------|-----------|------------------|
+| mass | kilogram | kg |
+| length | meter | m |
+| time | second | s |
+| current | ampere | A |
+| temperature | kelvin | K |
+| amount of substance | mole | mol |
+| luminous intensity | candela | cd |
+
+Dimensionless units are supported too, and for those all dimensions are set to
+zero.
+
+Additionally, we also define a scale factor and zero offset for each unit, with
+most dimensions' base units usually using a factor of 1 and no offset.
+
+For example, here's how we can define kilograms and meters:
+
+```ts
+const KG = unit(1, 1); // SI dimension 0
+// { dim: [ 1, 0, 0, 0, 0, 0, 0 ], scale: 1, offset: 0, prefix: false }
+
+const M = unit(1, 1); // SI dimension 1
+// { dim: [ 0, 1, 0, 0, 0, 0, 0 ], scale: 1, offset: 0, prefix: false }
+```
+
+More complex units like electrical resistance (e.g. kΩ) are based on more than a
+single dimension:
+
+```ts
+// { dim: [ 1, 2, -3, -2, 0, 0, 0 ], scale: 1000, offset: 0, prefix: true }
+```
+
+This dimension vector represents the unit definition for (see [SI derived
+units](https://en.wikipedia.org/wiki/SI_derived_unit)):
+
+> 1 Ohm = kg⋅m2⋅s−3⋅A−2
+
+### Predefined units
+
+The following units are provided as "builtins", here grouped by dimension:
+
+#### Acceleration
+
+| Unit name | JS name | Description |
+|-----------|----------|---------------------------|
+| `m/s2` | `M_S2` | meter per second squared |
+| `ft/s2` | `FT_S2` | foot per second squared |
+| `rad/s2` | `RAD_S2` | radian per second squared |
+| `g0` | `G0` | standard gravity |
+
+#### Angle
+
+| Unit name | JS name | Description |
+|-----------|----------|-------------|
+| `arcmin` | `ARCMIN` | arc minute |
+| `arcsec` | `ARCSEC` | arc second |
+| `deg` | `DEG` | degree |
+| `gon` | `GON` | gradian |
+| `rad` | `RAD` | radian |
+| `sr` | `SR` | steradian |
+| `turn` | `TURN` | turn |
+
+#### Area
+
+| Unit name | JS name | Description |
+|-----------|---------|-------------------|
+| `m2` | `M2` | square meter |
+| `cm2` | `CM2` | square centimeter |
+| `mm2` | `MM2` | square millimeter |
+| `km2` | `KM2` | square kilometer |
+| `ha` | `HA` | hectar |
+| `ac` | `AC` | acre |
+| `sqin` | `SQIN` | square inch |
+| `sqft` | `SQFT` | square foot |
+| `sqmi` | `SQMI` | square mile |
+
+#### Data
+
+| Unit name | JS name | Description |
+|-----------|---------|-----------------|
+| `bit` | `BIT` | bit |
+| `kbit` | `KBIT` | kilobit |
+| `Mbit` | `MBIT` | megabit |
+| `Gbit` | `GBIT` | gigabit |
+| `Tbit` | `TBIT` | terabit |
+| `B` | `BYTE` | byte (8 bit) |
+| `KB` | `KBYTE` | kilobyte (1024) |
+| `MB` | `MBYTE` | megabyte (1024) |
+| `GB` | `GBYTE` | gigabyte (1024) |
+| `TB` | `TBYTE` | terabyte (1024) |
+| `PB` | `PBYTE` | petabyte (1024) |
+| `EB` | `EBYTE` | exabyte (1024) |
+
+#### Electric current
+
+| Unit | JS name | Description |
+|------|-----------|-------------------|
+| A | `A` | ampere |
+| mA | `MA` | milliampere |
+| mAh | `MA_H` | milliampere-hours |
+| C | `C` | coulomb |
+| V | `V` | volt |
+| mV | `MV` | millivolt |
+| kV | `KV` | kilovolt |
+| F | `F` | farad |
+| pF | `PF` | picofarad |
+| µF | `µF` | microfarad |
+| Ω | `OHM` | ohm |
+| kΩ | `KOHM` | kiloohm |
+| MΩ | `MOHM` | megaohm |
+| GΩ | `GOHM` | gigaohm |
+| S | `SIEMENS` | siemens |
+| Wb | `WB` | weber |
+| T | `TESLA` | tesla |
+| H | `HENRY` | henry |
+
+#### Energy
+
+| Unit | JS name | Description |
+|------|---------|-------------|
+| J | `J` | joule |
+| kJ | `KJ` | kilojoule |
+| MJ | `MJ` | megajoule |
+| GJ | `GJ` | gigajoule |
+| cal | `CAL` | calorie |
+| kcal | `KCAL` | kilocalorie |
+
+#### Force
+
+| Unit | JS name | Description |
+|------|---------|-------------|
+| `N` | `N` | newton |
+
+#### Frequency
+
+| Unit | JS name | Description |
+|-------|---------|---------------------|
+| `Hz` | `HZ` | hertz |
+| `kHz` | `KHZ` | kilohertz |
+| `MHz` | `MHZ` | megahertz |
+| `GHz` | `GHZ` | gigahertz |
+| `THz` | `THZ` | terahertz |
+| `rpm` | `RPM` | rotation per minute |
+| `ω` | `OMEGA` | radian per second |
+
+#### Length
+
+| Unit name | JS name | Description |
+|-----------|---------|-------------------|
+| `m` | `M` | meter |
+| `cm` | `CM` | centimeter |
+| `mm` | `MM` | millimeter |
+| `µm` | `µM` | micrometer |
+| `nm` | `NM` | nanometer |
+| `km` | `KM` | kilometer |
+| `au` | `AU` | astronomical unit |
+| `in` | `IN` | inch |
+| `ft` | `FT` | foot |
+| `yd` | `YD` | yard |
+| `mi` | `MI` | mile |
+| `nmi` | `NMI` | nautical mile |
+| `pica` | `PICA` | pica |
+| `point` | `POINT` | point |
+
+#### Luminous intensity
+
+| Unit | JS name | Description |
+|------|---------|-------------|
+| `cd` | `CD` | candela |
+| `lm` | `LM` | lumen |
+| `lx` | `LX` | lux |
+
+#### Mass
+
+| Unit name | JS name | Description |
+|-----------|---------|----------------|
+| `µg` | `µG` | microgram |
+| `mg` | `mG` | milligram |
+| `g` | `G` | gram |
+| `kg` | `KG` | kilogram |
+| `t` | `T` | tonne |
+| `kt` | `KT` | kilotonne |
+| `Mt` | `MT` | megatonne |
+| `Gt` | `GT` | gigatonne |
+| `lb` | `LB` | imperial pound |
+| `st` | `ST` | stone |
+
+#### Power
+
+| Unit name | JS name | Description |
+|-----------|---------|---------------|
+| `W` | `W` | watt |
+| `mW` | `MW` | milliwatt |
+| `kW` | `KW` | kilowatt |
+| `MW` | `MW` | megawatt |
+| `GW` | `GW` | gigawatt |
+| `TW` | `TW` | terawatt |
+| `Wh` | `WH` | watt-hour |
+| `kWh` | `KWH` | kilowatt-hour |
+
+#### Pressure
+
+| Unit name | JS name | Description |
+|-----------|---------|-----------------------|
+| `Pa` | `PA` | pascal |
+| `kPa` | `KPA` | kilopascal |
+| `MPa` | `MPA` | megapascal |
+| `GPa` | `GPA` | gigapascal |
+| `at` | `AT` | technical atmosphere |
+| `atm` | `ATM` | atmosphere |
+| `bar` | `BAR` | bar |
+| `psi` | `PSI` | pound per square inch |
+
+#### Speed
+
+| Unit | JS name | Description |
+|--------|---------|--------------------|
+| `m/s` | `M_S` | meter per second |
+| `km/h` | `KM_H` | kilometer per hour |
+| `mph` | `MPH` | mile per hour |
+| `kn` | `KN` | knot |
+
+#### Substance
+
+| Unit | JS name | Description |
+|-------|---------|-------------|
+| `mol` | `MOL` | mole |
+
+#### Temperature
+
+| Unit | JS name | Description |
+|------|---------|-------------------|
+| `K` | `K` | kelvin |
+| `℃` | `DEG_C` | degree celsius |
+| `℉` | `DEG_F` | degree fahrenheit |
+
+#### Time
+
+| Unit | JS name | Description |
+|-------|---------|--------------------|
+| s | `S` | second |
+| ms | `MS` | millisecond |
+| µs | `µS` | microsecond |
+| ns | `NS` | nanosecond |
+| min | `MIN` | minute |
+| h | `H` | hour |
+| day | `DAY` | day |
+| week | `WEEK` | week |
+| month | `MONTH` | month (30 days) |
+| year | `YEAR` | year (365.25 days) |
+
+#### Volume
+
+| Unit | JS name | Description |
+|-------------|-----------|----------------------|
+| `m3` | `M3` | cubic meter |
+| `mm3` | `MM3` | cubic millimeter |
+| `cm3` | `CM3` | cubic centimeter |
+| `km3` | `KM3` | cubic kilometer |
+| `l` | `L` | liter |
+| `cl` | `CL` | centiliter |
+| `ml` | `ML` | milliliter |
+| `imp gal` | `GAL` | imperial gallon |
+| `imp pt` | `PT` | imperial pint |
+| `imp fl oz` | `FLOZ` | imperial fluid ounce |
+| `us gal` | `US_GAL` | US gallon |
+| `us pt` | `US_PT` | US pint |
+| `us cup` | `US_CUP` | US cup |
+| `us fl oz` | `US_FLOZ` | US fluid ounce |
+
+### Creating & deriving units
+
+#### Using standard metric prefixes
+
+Existing coherent
+
+### Unit conversions
+
+Only units with compatible (incl. reciprocal) dimensions can be converted,
+otherwise an error will be thrown. Units can be given as
+
+```ts
+// convert from km/h to mph using unit names
+convert(100, "km/h", "mph");
+// 62.13711922373341
+
+// using predefined unit constants directly
+convert(60, MPH, KM_H);
+// 96.56063999999998
+
+// or using anonymous units (meter/second ⇒ yard/hour)
+convert(1, "m/s", div(YD, H))
+// 3937.007874015749
+```
+
+## Status
+
+**BETA** - possibly breaking changes forthcoming
+
+[Search or submit any issues for this package](https://github.com/thi-ng/umbrella/issues?q=%5Bunits%5D+in%3Atitle)
+
+## Installation
+
+```bash
+yarn add @thi.ng/units
+```
+
+ES module import:
+
+```html
+
+```
+
+[Skypack documentation](https://docs.skypack.dev/)
+
+For Node.js REPL:
+
+```js
+const units = await import("@thi.ng/units");
+```
+
+Package sizes (brotli'd, pre-treeshake): ESM: 3.17 KB
+
+## Dependencies
+
+- [@thi.ng/checks](https://github.com/thi-ng/umbrella/tree/develop/packages/checks)
+- [@thi.ng/equiv](https://github.com/thi-ng/umbrella/tree/develop/packages/equiv)
+- [@thi.ng/errors](https://github.com/thi-ng/umbrella/tree/develop/packages/errors)
+
+## API
+
+[Generated API docs](https://docs.thi.ng/umbrella/units/)
+
+TODO
+
+## Authors
+
+- [Karsten Schmidt](https://thi.ng)
+
+If this project contributes to an academic publication, please cite it as:
+
+```bibtex
+@misc{thing-units,
+ title = "@thi.ng/units",
+ author = "Karsten Schmidt",
+ note = "https://thi.ng/units",
+ year = 2021
+}
+```
+
+## License
+
+© 2021 - 2023 Karsten Schmidt // Apache License 2.0
diff --git a/packages/units/api-extractor.json b/packages/units/api-extractor.json
new file mode 100644
index 0000000000..bc73f2cc02
--- /dev/null
+++ b/packages/units/api-extractor.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../../api-extractor.json"
+}
diff --git a/packages/units/package.json b/packages/units/package.json
new file mode 100644
index 0000000000..563b6b2fe0
--- /dev/null
+++ b/packages/units/package.json
@@ -0,0 +1,157 @@
+{
+ "name": "@thi.ng/units",
+ "version": "0.0.1",
+ "description": "Extensible SI unit creation & conversions (130+ units predefined)",
+ "type": "module",
+ "module": "./index.js",
+ "typings": "./index.d.ts",
+ "sideEffects": false,
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/thi-ng/umbrella.git"
+ },
+ "homepage": "https://github.com/thi-ng/umbrella/tree/develop/packages/units#readme",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/postspectacular"
+ },
+ {
+ "type": "patreon",
+ "url": "https://patreon.com/thing_umbrella"
+ }
+ ],
+ "author": "Karsten Schmidt (https://thi.ng)",
+ "license": "Apache-2.0",
+ "scripts": {
+ "build": "yarn clean && tsc --declaration",
+ "clean": "rimraf '*.js' '*.d.ts' '*.map' doc",
+ "doc": "typedoc --excludePrivate --excludeInternal --out doc src/index.ts",
+ "doc:ae": "mkdir -p .ae/doc .ae/temp && api-extractor run --local --verbose",
+ "doc:readme": "yarn doc:stats && tools:readme",
+ "doc:stats": "tools:module-stats",
+ "pub": "yarn npm publish --access public",
+ "test": "testament test"
+ },
+ "dependencies": {
+ "@thi.ng/checks": "^3.3.9",
+ "@thi.ng/equiv": "^2.1.19",
+ "@thi.ng/errors": "^2.2.12"
+ },
+ "devDependencies": {
+ "@microsoft/api-extractor": "^7.34.4",
+ "@thi.ng/testament": "^0.3.12",
+ "rimraf": "^4.4.0",
+ "tools": "workspace:^",
+ "typedoc": "^0.23.26",
+ "typescript": "^4.9.5"
+ },
+ "keywords": [
+ "acceleration",
+ "angle",
+ "area",
+ "bits",
+ "bytes",
+ "capacitance",
+ "current",
+ "voltage",
+ "resistance",
+ "converter",
+ "energy",
+ "force",
+ "frequency",
+ "length",
+ "mass",
+ "power",
+ "si",
+ "speed",
+ "temperature",
+ "time",
+ "typescript",
+ "units",
+ "volume"
+ ],
+ "publishConfig": {
+ "access": "public"
+ },
+ "browser": {
+ "process": false,
+ "setTimeout": false
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "files": [
+ "./*.js",
+ "./*.d.ts"
+ ],
+ "exports": {
+ ".": {
+ "default": "./index.js"
+ },
+ "./accel": {
+ "default": "./accel.js"
+ },
+ "./angle": {
+ "default": "./angle.js"
+ },
+ "./api": {
+ "default": "./api.js"
+ },
+ "./area": {
+ "default": "./area.js"
+ },
+ "./data": {
+ "default": "./data.js"
+ },
+ "./electric": {
+ "default": "./electric.js"
+ },
+ "./energy": {
+ "default": "./energy.js"
+ },
+ "./force": {
+ "default": "./force.js"
+ },
+ "./frequency": {
+ "default": "./frequency.js"
+ },
+ "./length": {
+ "default": "./length.js"
+ },
+ "./luminous": {
+ "default": "./luminous.js"
+ },
+ "./mass": {
+ "default": "./mass.js"
+ },
+ "./power": {
+ "default": "./power.js"
+ },
+ "./pressure": {
+ "default": "./pressure.js"
+ },
+ "./speed": {
+ "default": "./speed.js"
+ },
+ "./substance": {
+ "default": "./substance.js"
+ },
+ "./temperature": {
+ "default": "./temperature.js"
+ },
+ "./time": {
+ "default": "./time.js"
+ },
+ "./unit": {
+ "default": "./unit.js"
+ },
+ "./volume": {
+ "default": "./volume.js"
+ }
+ },
+ "thi.ng": {
+ "status": "beta",
+ "year": 2021
+ }
+}
diff --git a/packages/units/src/accel.ts b/packages/units/src/accel.ts
new file mode 100644
index 0000000000..3198a3419f
--- /dev/null
+++ b/packages/units/src/accel.ts
@@ -0,0 +1,24 @@
+import { RAD } from "./angle.js";
+import { FT, M } from "./length.js";
+import { S } from "./time.js";
+import { defUnit, div, mul, pow } from "./unit.js";
+
+export const M_S2 = defUnit(
+ "m/s2",
+ "meter per second squared",
+ div(M, pow(S, 2))
+);
+
+export const FT_S2 = defUnit(
+ "ft/s2",
+ "foot per second squared",
+ div(FT, pow(S, 2))
+);
+
+export const RAD_S2 = defUnit(
+ "rad/s2",
+ "radian per second squared",
+ div(RAD, pow(S, 2))
+);
+
+export const G0 = defUnit("g0", "standard gravity", mul(M_S2, 9.80665));
diff --git a/packages/units/src/angle.ts b/packages/units/src/angle.ts
new file mode 100644
index 0000000000..4f65b6d66b
--- /dev/null
+++ b/packages/units/src/angle.ts
@@ -0,0 +1,12 @@
+import { defUnit, dimensionless, mul } from "./unit.js";
+
+const PI = Math.PI;
+
+export const RAD = defUnit("rad", "radian", dimensionless(1, 0, true));
+export const DEG = defUnit("deg", "degree", mul(RAD, PI / 180));
+export const GON = defUnit("gon", "gradian", mul(RAD, PI / 200));
+export const TURN = defUnit("turn", "turn", mul(RAD, 2 * PI));
+export const ARCMIN = defUnit("arcmin", "arc minute", mul(RAD, PI / 10800));
+export const ARCSEC = defUnit("arcsec", "arc second", mul(RAD, PI / 648000));
+
+export const SR = defUnit("sr", "steradian", dimensionless(1, 0, true));
diff --git a/packages/units/src/api.ts b/packages/units/src/api.ts
new file mode 100644
index 0000000000..b58621f030
--- /dev/null
+++ b/packages/units/src/api.ts
@@ -0,0 +1,99 @@
+export interface Unit {
+ /**
+ * SI dimension vector ({@link Dimensions})
+ */
+ dim: Dimensions;
+ /**
+ * Scaling factor relative to the coherent unit
+ */
+ scale: number;
+ /**
+ * Zero offset value
+ */
+ offset: number;
+ /**
+ * True, if this unit is coherent.
+ *
+ * @remarks
+ * Reference:
+ * - https://en.wikipedia.org/wiki/Coherence_(units_of_measurement)
+ */
+ coherent: boolean;
+}
+
+export interface NamedUnit extends Unit {
+ /**
+ * Symbol under which this unit can be looked up with via {@link asUnit} and
+ * others.
+ */
+ sym: string;
+ /**
+ * This unit's human readable description/name.
+ */
+ name: string;
+}
+
+/**
+ * Vector of the 7 basic SI unit dimensions.
+ *
+ * @remarks
+ * In order:
+ *
+ * - 0 = mass
+ * - 1 = length
+ * - 2 = time
+ * - 3 = current
+ * - 4 = temperature
+ * - 5 = amount of substance
+ * - 6 = luminous intensity
+ *
+ * Note: For dimensionless units, all dimensions are zero.
+ *
+ * Reference:
+ * - https://en.wikipedia.org/wiki/International_System_of_Units
+ */
+export type Dimensions = [
+ number,
+ number,
+ number,
+ number,
+ number,
+ number,
+ number
+];
+
+/**
+ * A known metric prefix.
+ */
+export type Prefix = keyof typeof PREFIXES;
+
+/**
+ * @remarks
+ * Reference:
+ * - https://en.wikipedia.org/wiki/Metric_prefix
+ */
+export const PREFIXES = {
+ Q: 1e30,
+ R: 1e27,
+ Y: 1e24,
+ Z: 1e21,
+ E: 1e18,
+ P: 1e15,
+ T: 1e12,
+ G: 1e9,
+ M: 1e6,
+ k: 1e3,
+ h: 1e2,
+ d: 1e-1,
+ c: 1e-2,
+ m: 1e-3,
+ µ: 1e-6,
+ n: 1e-9,
+ p: 1e-12,
+ f: 1e-15,
+ a: 1e-18,
+ z: 1e-21,
+ y: 1e-24,
+ r: 1e-27,
+ q: 1e-30,
+};
diff --git a/packages/units/src/area.ts b/packages/units/src/area.ts
new file mode 100644
index 0000000000..c064377b3c
--- /dev/null
+++ b/packages/units/src/area.ts
@@ -0,0 +1,13 @@
+import { CM, FT, IN, KM, M, MI, MM } from "./length.js";
+import { defUnit, mul, pow } from "./unit.js";
+
+export const M2 = defUnit("m2", "square meter", pow(M, 2));
+export const MM2 = defUnit("mm2", "square millimeter", pow(MM, 2));
+export const CM2 = defUnit("cm2", "square centimeter", pow(CM, 2));
+export const KM2 = defUnit("km2", "square kilometer", pow(KM, 2));
+export const HA = defUnit("ha", "hectar", mul(M2, 1e4));
+
+export const AC = defUnit("ac", "acre", mul(M2, 4046.86));
+export const SQIN = defUnit("sqin", "square inch", pow(IN, 2));
+export const SQFT = defUnit("sqft", "square foot", pow(FT, 2));
+export const SQMI = defUnit("sqmi", "square mile", pow(MI, 2));
diff --git a/packages/units/src/data.ts b/packages/units/src/data.ts
new file mode 100644
index 0000000000..9c8868c091
--- /dev/null
+++ b/packages/units/src/data.ts
@@ -0,0 +1,15 @@
+import { defUnit, dimensionless, mul, prefix } from "./unit.js";
+
+export const BIT = defUnit("bit", "bit", dimensionless(1, 0, true));
+export const KBIT = defUnit("kbit", "kilobit", prefix("k", BIT));
+export const MBIT = defUnit("Mbit", "megabit", prefix("M", BIT));
+export const GBIT = defUnit("Gbit", "gigabit", prefix("G", BIT));
+export const TBIT = defUnit("Tbit", "terabit", prefix("T", BIT));
+
+export const BYTE = defUnit("B", "byte", mul(BIT, 8, true));
+export const KBYTE = defUnit("KB", "kilobyte", mul(BYTE, 1024));
+export const MBYTE = defUnit("MB", "megabyte", mul(KBYTE, 1024));
+export const GBYTE = defUnit("GB", "gigabyte", mul(MBYTE, 1024));
+export const TBYTE = defUnit("TB", "terabyte", mul(GBYTE, 1024));
+export const PBYTE = defUnit("PB", "petabyte", mul(TBYTE, 1024));
+export const EBYTE = defUnit("EB", "exabyte", mul(PBYTE, 1024));
diff --git a/packages/units/src/electric.ts b/packages/units/src/electric.ts
new file mode 100644
index 0000000000..d055c8ed2d
--- /dev/null
+++ b/packages/units/src/electric.ts
@@ -0,0 +1,28 @@
+import { M2 } from "./area.js";
+import { J } from "./energy.js";
+import { H, S } from "./time.js";
+import { defUnit, div, mul, prefix, unit } from "./unit.js";
+
+export const A = defUnit("A", "ampere", unit(3, 1, 0, true));
+export const MA = defUnit("mA", "milliampere", prefix("m", A));
+export const MA_H = defUnit("mAh", "milliampere-hour", mul(MA, H));
+
+export const C = defUnit("C", "coulomb", mul(A, S, true));
+
+export const V = defUnit("V", "volt", div(J, C, true));
+export const MV = defUnit("mV", "millivolt", prefix("m", V));
+export const KV = defUnit("kV", "kilovolt", prefix("k", V));
+
+export const F = defUnit("F", "farad", div(C, V, true));
+export const PF = defUnit("pF", "picofarad", prefix("p", F));
+export const µF = defUnit("µF", "microfarad", prefix("µ", F));
+
+export const OHM = defUnit("Ω", "ohm", div(V, A, true));
+export const KOHM = defUnit("kΩ", "kiloohm", prefix("k", OHM));
+export const MOHM = defUnit("MΩ", "megaohm", prefix("M", OHM));
+export const GOHM = defUnit("GΩ", "gigaohm", prefix("G", OHM));
+
+export const SIEMENS = defUnit("S", "siemens", div(A, V, true));
+export const WB = defUnit("Wb", "weber", mul(V, S, true));
+export const TESLA = defUnit("T", "tesla", div(WB, M2, true));
+export const HENRY = defUnit("H", "henry", div(WB, A, true));
diff --git a/packages/units/src/energy.ts b/packages/units/src/energy.ts
new file mode 100644
index 0000000000..b47235991e
--- /dev/null
+++ b/packages/units/src/energy.ts
@@ -0,0 +1,10 @@
+import { N } from "./force.js";
+import { M } from "./length.js";
+import { defUnit, mul, prefix } from "./unit.js";
+
+export const J = defUnit("J", "joule", mul(N, M, true));
+export const KJ = defUnit("kJ", "kilojoule", prefix("k", J));
+export const MJ = defUnit("MJ", "megajoule", prefix("M", J));
+export const GJ = defUnit("GJ", "gigajoule", prefix("G", J));
+export const CAL = defUnit("cal", "calorie", mul(J, 4.184, true));
+export const KCAL = defUnit("kcal", "kilocalorie", prefix("k", CAL));
diff --git a/packages/units/src/force.ts b/packages/units/src/force.ts
new file mode 100644
index 0000000000..919acf172a
--- /dev/null
+++ b/packages/units/src/force.ts
@@ -0,0 +1,5 @@
+import { M_S2 } from "./accel.js";
+import { KG } from "./mass.js";
+import { defUnit, mul } from "./unit.js";
+
+export const N = defUnit("N", "newton", mul(KG, M_S2, true));
diff --git a/packages/units/src/frequency.ts b/packages/units/src/frequency.ts
new file mode 100644
index 0000000000..aef97d587f
--- /dev/null
+++ b/packages/units/src/frequency.ts
@@ -0,0 +1,11 @@
+import { S } from "./time.js";
+import { defUnit, div, mul, prefix, reciprocal } from "./unit.js";
+
+export const HZ = defUnit("Hz", "hertz", reciprocal(S, true));
+export const KHZ = defUnit("kHz", "kilohertz", prefix("k", HZ));
+export const MHZ = defUnit("MHz", "megahertz", prefix("M", HZ));
+export const GHZ = defUnit("GHz", "gigahertz", prefix("G", HZ));
+export const THZ = defUnit("THz", "terahertz", prefix("T", HZ));
+export const RPM = defUnit("rpm", "rotation per minute", mul(HZ, 1 / 60));
+
+export const OMEGA = defUnit("ω", "radian per second", div(HZ, 2 * Math.PI));
diff --git a/packages/units/src/index.ts b/packages/units/src/index.ts
new file mode 100644
index 0000000000..8163e41792
--- /dev/null
+++ b/packages/units/src/index.ts
@@ -0,0 +1,21 @@
+export * from "./api.js";
+export * from "./unit.js";
+
+export * from "./accel.js";
+export * from "./angle.js";
+export * from "./area.js";
+export * from "./data.js";
+export * from "./electric.js";
+export * from "./energy.js";
+export * from "./force.js";
+export * from "./frequency.js";
+export * from "./length.js";
+export * from "./luminous.js";
+export * from "./mass.js";
+export * from "./power.js";
+export * from "./pressure.js";
+export * from "./speed.js";
+export * from "./substance.js";
+export * from "./temperature.js";
+export * from "./time.js";
+export * from "./volume.js";
diff --git a/packages/units/src/length.ts b/packages/units/src/length.ts
new file mode 100644
index 0000000000..c7135351cf
--- /dev/null
+++ b/packages/units/src/length.ts
@@ -0,0 +1,18 @@
+import { defUnit, mul, prefix, unit } from "./unit.js";
+
+export const M = defUnit("m", "meter", unit(1, 1, 0, true));
+export const CM = defUnit("cm", "centimeter", prefix("c", M));
+export const MM = defUnit("mm", "millimeter", prefix("m", M));
+export const µM = defUnit("µm", "micrometer", prefix("µ", M));
+export const NM = defUnit("nm", "nanometer", prefix("n", M));
+export const KM = defUnit("km", "kilometer", prefix("k", M));
+export const AU = defUnit("au", "astronomical unit", mul(M, 149597870700));
+
+export const IN = defUnit("in", "inch", mul(M, 0.0254));
+export const FT = defUnit("ft", "foot", mul(IN, 12));
+export const YD = defUnit("yd", "yard", mul(FT, 3));
+export const MI = defUnit("mi", "mile", mul(YD, 1760));
+export const NMI = defUnit("nmi", "nautical mile", mul(M, 1852));
+
+export const PICA = defUnit("pica", "pica", mul(IN, 1 / 6));
+export const POINT = defUnit("point", "point", mul(IN, 1 / 72));
diff --git a/packages/units/src/luminous.ts b/packages/units/src/luminous.ts
new file mode 100644
index 0000000000..cc90c84ac2
--- /dev/null
+++ b/packages/units/src/luminous.ts
@@ -0,0 +1,8 @@
+import { SR } from "./angle.js";
+import { M2 } from "./area.js";
+import { defUnit, div, mul, unit } from "./unit.js";
+
+export const CD = defUnit("cd", "candela", unit(6, 1, 0, true));
+
+export const LM = defUnit("lm", "lumen", mul(CD, SR));
+export const LX = defUnit("lx", "lux", div(LM, M2));
diff --git a/packages/units/src/mass.ts b/packages/units/src/mass.ts
new file mode 100644
index 0000000000..cdb77121c0
--- /dev/null
+++ b/packages/units/src/mass.ts
@@ -0,0 +1,14 @@
+import { defUnit, mul, prefix, unit } from "./unit.js";
+
+export const G = defUnit("g", "gram", unit(0, 1e-3, 0, true));
+export const KG = defUnit("kg", "kilogram", prefix("k", G));
+export const MG = defUnit("mg", "milligram", prefix("m", G));
+export const µG = defUnit("µg", "microgram", prefix("µ", G));
+
+export const T = defUnit("t", "tonne", prefix("M", G, true));
+export const KT = defUnit("kt", "kilotonne", prefix("k", T));
+export const MT = defUnit("Mt", "megatonne", prefix("M", T));
+export const GT = defUnit("Gt", "gigatonne", prefix("G", T));
+
+export const LB = defUnit("lb", "imperial pound", mul(KG, 0.45359237));
+export const ST = defUnit("st", "stone", mul(LB, 14));
diff --git a/packages/units/src/power.ts b/packages/units/src/power.ts
new file mode 100644
index 0000000000..fb48215f25
--- /dev/null
+++ b/packages/units/src/power.ts
@@ -0,0 +1,13 @@
+import { J } from "./energy.js";
+import { H, S } from "./time.js";
+import { defUnit, div, mul, prefix } from "./unit.js";
+
+export const W = defUnit("W", "watt", div(J, S, true));
+export const MILLI_W = defUnit("mW", "milliwatt", prefix("m", W));
+export const KW = defUnit("kW", "kilowatt", prefix("k", W));
+export const MW = defUnit("MW", "megawatt", prefix("M", W));
+export const GW = defUnit("GW", "gigawatt", prefix("G", W));
+export const TW = defUnit("TW", "terawatt", prefix("T", W));
+
+export const W_H = defUnit("Wh", "watt-hour", mul(W, H, true));
+export const KW_H = defUnit("kWh", "kilowatt-hour", prefix("k", W_H));
diff --git a/packages/units/src/pressure.ts b/packages/units/src/pressure.ts
new file mode 100644
index 0000000000..eec018ea55
--- /dev/null
+++ b/packages/units/src/pressure.ts
@@ -0,0 +1,13 @@
+import { M2 } from "./area.js";
+import { N } from "./force.js";
+import { defUnit, div, mul, prefix } from "./unit.js";
+
+export const PA = defUnit("Pa", "pascal", div(N, M2, true));
+export const KPA = defUnit("kPa", "kilopascal", prefix("k", PA));
+export const MPA = defUnit("MPa", "megapascal", prefix("M", PA));
+export const GPA = defUnit("GPa", "gigapascal", prefix("G", PA));
+
+export const AT = defUnit("at", "technical atmosphere", mul(PA, 98066.5));
+export const ATM = defUnit("atm", "atmosphere", mul(PA, 101325));
+export const BAR = defUnit("bar", "bar", mul(PA, 1e5, true));
+export const PSI = defUnit("psi", "pound per square inch", mul(PA, 6894.757));
diff --git a/packages/units/src/speed.ts b/packages/units/src/speed.ts
new file mode 100644
index 0000000000..487ce82382
--- /dev/null
+++ b/packages/units/src/speed.ts
@@ -0,0 +1,9 @@
+import { defUnit, div } from "./unit.js";
+import { FT, KM, M, MI, NMI } from "./length.js";
+import { H, S } from "./time.js";
+
+export const M_S = defUnit("m/s", "meter per second", div(M, S));
+export const KM_H = defUnit("km/h", "kilometer per hour", div(KM, H));
+export const FT_S = defUnit("ft/s", "foot per second", div(FT, S));
+export const MPH = defUnit("mph", "mile per hour", div(MI, H));
+export const KNOT = defUnit("kn", "knot", div(NMI, H));
diff --git a/packages/units/src/substance.ts b/packages/units/src/substance.ts
new file mode 100644
index 0000000000..80fecbda4a
--- /dev/null
+++ b/packages/units/src/substance.ts
@@ -0,0 +1,3 @@
+import { defUnit, unit } from "./unit.js";
+
+export const MOL = defUnit("mol", "mole", unit(5, 1, 0, true));
diff --git a/packages/units/src/temperature.ts b/packages/units/src/temperature.ts
new file mode 100644
index 0000000000..198963a5a3
--- /dev/null
+++ b/packages/units/src/temperature.ts
@@ -0,0 +1,9 @@
+import { defUnit, unit } from "./unit.js";
+
+export const K = defUnit("K", "kelvin", unit(4, 1));
+export const DEG_C = defUnit("℃", "degree celsius", unit(4, 1, 273.15));
+export const DEG_F = defUnit(
+ "℉",
+ "degree fahrenheit",
+ unit(4, 1 / 1.8, 459.67 / 1.8)
+);
diff --git a/packages/units/src/time.ts b/packages/units/src/time.ts
new file mode 100644
index 0000000000..20bd612f06
--- /dev/null
+++ b/packages/units/src/time.ts
@@ -0,0 +1,12 @@
+import { defUnit, mul, prefix, unit } from "./unit.js";
+
+export const S = defUnit("s", "second", unit(2, 1, 0, true));
+export const MS = defUnit("ms", "millisecond", prefix("m", S));
+export const µS = defUnit("µs", "microsecond", prefix("µ", S));
+export const NS = defUnit("ns", "nanosecond", prefix("n", S));
+export const MIN = defUnit("min", "minute", mul(S, 60));
+export const H = defUnit("h", "hour", mul(MIN, 60));
+export const DAY = defUnit("day", "day", mul(H, 24));
+export const WEEK = defUnit("week", "week", mul(DAY, 7));
+export const MONTH = defUnit("month", "month", mul(DAY, 30));
+export const YEAR = defUnit("year", "year", mul(DAY, 365.25));
diff --git a/packages/units/src/unit.ts b/packages/units/src/unit.ts
new file mode 100644
index 0000000000..14168e8d7d
--- /dev/null
+++ b/packages/units/src/unit.ts
@@ -0,0 +1,254 @@
+import { isNumber } from "@thi.ng/checks/is-number";
+import { isString } from "@thi.ng/checks/is-string";
+import { equivArrayLike } from "@thi.ng/equiv";
+import { assert } from "@thi.ng/errors/assert";
+import { illegalArgs } from "@thi.ng/errors/illegal-arguments";
+import { Dimensions, NamedUnit, Prefix, PREFIXES, Unit } from "./api.js";
+
+export const UNITS: Record = {};
+
+/**
+ * Defines a "raw" (anonymous) unit using given dimension(s), scale factor, zero
+ * offset and `coherent` flag indicating if the unit is the coherent one for
+ * given dimensions and can later be used for deriving prefixed versions (see
+ * {@link coherent}).
+ *
+ * @param dim
+ * @param scale
+ * @param offset
+ * @param coherent
+ */
+export const unit = (
+ dim: Dimensions | number,
+ scale: number,
+ offset = 0,
+ coherent = false
+): Unit => ({
+ dim: isNumber(dim) ? __oneHot(dim) : dim,
+ scale,
+ offset,
+ coherent,
+});
+
+/**
+ * Returns a new dimensionless unit (i.e. all SI dimensions are zero) with given
+ * `scale` factor.
+ *
+ * @param scale
+ * @param offset
+ * @param coherent
+ */
+export const dimensionless = (scale: number, offset = 0, coherent = false) =>
+ unit([0, 0, 0, 0, 0, 0, 0], scale, offset, coherent);
+
+/**
+ * Takes a unit symbol, full unit name and pre-defined {@link Unit} impl and
+ * registers it in the {@link UNITS} cache for further lookups by symbol name.
+ *
+ * @remarks
+ * By default throws an error if attempting to register a unit with an existing
+ * symbol. If `force` is true, the existing unit will be overwritten.
+ *
+ * @param sym
+ * @param name
+ * @param unit
+ * @param force
+ */
+export const defUnit = (
+ sym: string,
+ name: string,
+ unit: Unit,
+ force = false
+): NamedUnit => {
+ if (UNITS[sym] && !force) illegalArgs(`attempt to override unit: ${sym}`);
+ return (UNITS[sym] = { ...unit, sym, name });
+};
+
+/**
+ * Attempts to find a unit by given symbol ID/name. Throws error if unit is
+ * unknown.
+ *
+ * @param id
+ */
+export const asUnit = (id: string) => {
+ for (let i = 0; i < id.length; i++) {
+ const pre = id.substring(0, i);
+ const unit = UNITS[id.substring(i)];
+ if (unit) {
+ return PREFIXES[pre] !== undefined
+ ? prefix(pre, unit)
+ : unit;
+ }
+ }
+ for (let u in UNITS) {
+ if (UNITS[u].name === id) return UNITS[u];
+ }
+ illegalArgs(`unknown unit: ${id}`);
+};
+
+/**
+ * Creates a new re-scaled version of given unit (only coherent ones are
+ * allowed), using the scale factor associated with given standard metric prefix
+ * (see {@link PREFIXES}). If `coherent` is true (default: false), the new unit
+ * itself is considered coherent and can be prefixed later.
+ *
+ * @example
+ * ```ts
+ * // create kilometer unit from (builtin) meter
+ * const KM = prefix("k", M);
+ * ```
+ *
+ * @param id
+ * @param unit
+ * @param coherent
+ */
+export const prefix = (id: Prefix, unit: Unit, coherent = false) =>
+ unit.coherent
+ ? mul(unit, PREFIXES[id], coherent)
+ : illegalArgs(`unit isn't coherent: ${id}`);
+
+/**
+ * Derives a new unit as the product of the given units. If `coherent` is true
+ * (default: false), the new unit itself is considered coherent and can be
+ * prefixed later.
+ *
+ * @param a
+ * @param b
+ * @param coherent
+ */
+export const mul = (a: Unit, b: Unit | number, coherent = false) =>
+ isNumber(b)
+ ? unit(a.dim, a.scale * b, a.offset, coherent)
+ : unit(
+ a.dim.map((x, i) => x + b.dim[i]),
+ a.scale * b.scale,
+ 0,
+ coherent
+ );
+
+/**
+ * Derives a new unit via the division of the given units. If `coherent` is true
+ * (default: false), the new unit itself is considered coherent and can be
+ * prefixed later.
+ *
+ * @param a
+ * @param b
+ * @param coherent
+ */
+export const div = (a: Unit, b: Unit | number, coherent = false) =>
+ isNumber(b)
+ ? unit(a.dim, a.scale / b, a.offset, coherent)
+ : unit(
+ a.dim.map((x, i) => x - b.dim[i]),
+ a.scale / b.scale,
+ 0,
+ coherent
+ );
+
+/**
+ * Creates the reciprocal version of given unit (i.e. all SI dimensions will
+ * flip sign) and the scale factor of the new unit will be `1/scale`.
+ *
+ * @example
+ * ```ts
+ * const HZ = reciprocal(S, true);
+ * ```
+ *
+ * @param u
+ * @param coherent
+ */
+export const reciprocal = (u: Unit, coherent = false) =>
+ div(dimensionless(1), u, coherent);
+
+/**
+ * Raises given unit to power `k`.
+ *
+ * ```ts
+ * // create kilometer unit from (builtin) meter
+ * const SQ_METER = pow(M, 2);
+ *
+ * // acceleration aka m/s^2
+ * const M_S2 = div(M, pow(S, 2));
+ * ```
+ *
+ * @param u
+ * @param k
+ * @param coherent
+ */
+export const pow = (u: Unit, k: number, coherent = false) =>
+ unit(u.dim.map((x) => x * k), u.scale ** k, 0, coherent);
+
+/**
+ * Attempts to convert `x` from `src` unit into `dest` unit. Throws an error if
+ * units are incompatible.
+ *
+ * @remarks
+ * Units can only be converted if their SI dimensions are compatible. See
+ * {@link isConvertible}.
+ *
+ * @param x
+ * @param src
+ * @param dest
+ */
+export const convert = (x: number, src: Unit | string, dest: Unit | string) => {
+ const $src = __ensureUnit(src);
+ const $dest = __ensureUnit(dest);
+ const xnorm = x * $src.scale + $src.offset;
+ if (isReciprocal($src, $dest))
+ return (1 / xnorm - $dest.offset) / $dest.scale;
+ assert(equivArrayLike($src.dim, $dest.dim), "incompatible dimensions");
+ return (xnorm - $dest.offset) / $dest.scale;
+};
+
+/**
+ * Returns true if the two given units are reciprocal to each other (and
+ * therefore can be used for conversion).
+ *
+ * @param a
+ * @param b
+ */
+export const isReciprocal = (src: Unit | string, dest: Unit | string) => {
+ const { dim: a } = __ensureUnit(src);
+ const { dim: b } = __ensureUnit(dest);
+ let ok = false;
+ for (let i = 0; i < 7; i++) {
+ const xa = a[i];
+ const xb = b[i];
+ if (xa === 0 && xb === 0) continue;
+ if (xa !== -xb) return false;
+ ok = true;
+ }
+ return ok;
+};
+
+/**
+ * Returns true if `src` unit is convertible to `dest`.
+ *
+ * @param src
+ * @param dest
+ */
+export const isConvertible = (src: Unit | string, dest: Unit | string) => {
+ const $src = __ensureUnit(src);
+ const $dest = __ensureUnit(dest);
+ return isReciprocal($src, $dest) || equivArrayLike($src.dim, $dest.dim);
+};
+
+export const formatSI = ({ dim }: Unit) => {
+ const SI = ["kg", "m", "s", "A", "K", "mol", "cd"];
+ const acc: string[] = [];
+ for (let i = 0; i < 7; i++) {
+ const x = dim[i];
+ if (x !== 0) acc.push(SI[i] + (x !== 1 ? x : ""));
+ }
+ return acc.length ? acc.join("·") : "";
+};
+
+/** @internal */
+const __ensureUnit = (x: Unit | string) => (isString(x) ? asUnit(x) : x);
+
+/** @internal */
+const __oneHot = (x: number) => {
+ const dims = new Array(7).fill(0);
+ dims[x] = 1;
+ return dims;
+};
diff --git a/packages/units/src/volume.ts b/packages/units/src/volume.ts
new file mode 100644
index 0000000000..b4e4fb7165
--- /dev/null
+++ b/packages/units/src/volume.ts
@@ -0,0 +1,27 @@
+import { CM, KM, M, MM } from "./length.js";
+import { defUnit, mul, pow, prefix } from "./unit.js";
+
+export const M3 = defUnit("m3", "cubic meter", pow(M, 3));
+export const MM3 = defUnit("mm3", "cubic millimeter", pow(MM, 3));
+export const CM3 = defUnit("cm3", "cubic centimeter", pow(CM, 3));
+export const KM3 = defUnit("km3", "cubic kilometer", pow(KM, 3));
+export const L = defUnit("l", "liter", mul(M3, 1e-3, true));
+export const CL = defUnit("cl", "centiliter", prefix("c", L));
+export const ML = defUnit("ml", "milliliter", prefix("m", L));
+
+export const GAL = defUnit("imp gal", "imperial gallon", mul(L, 4.54609));
+export const PT = defUnit("imp pt", "imperial pint", mul(GAL, 1 / 8));
+export const FLOZ = defUnit(
+ "imp fl oz",
+ "imperial fluid ounce",
+ mul(GAL, 1 / 160)
+);
+
+export const US_GAL = defUnit("us gal", "us gallon", mul(L, 3.785411784));
+export const US_PT = defUnit("us pt", "us pint", mul(US_GAL, 1 / 8));
+export const US_CUP = defUnit("us cup", "us cup", mul(US_GAL, 1 / 16));
+export const US_FLOZ = defUnit(
+ "us fl oz",
+ "us fluid ounce",
+ mul(US_GAL, 1 / 128)
+);
diff --git a/packages/units/test/index.ts b/packages/units/test/index.ts
new file mode 100644
index 0000000000..d894436529
--- /dev/null
+++ b/packages/units/test/index.ts
@@ -0,0 +1,50 @@
+import { eqDelta } from "@thi.ng/math";
+import { group } from "@thi.ng/testament";
+import * as assert from "assert";
+import {
+ ARCMIN,
+ CM,
+ convert,
+ DEG,
+ FT,
+ GON,
+ IN,
+ KM,
+ M,
+ MI,
+ MM,
+ NM,
+ RAD,
+ TURN,
+ Unit,
+ YD,
+} from "../src/index.js";
+
+const PI = Math.PI;
+const TAU = 2 * PI;
+
+const check = (x: number, src: Unit, y: number, dest: Unit) => {
+ const res = convert(x, src, dest);
+ assert.ok(eqDelta(res, y), `${x} => ${res} (expected: ${y})`);
+};
+
+group("units", {
+ angle: () => {
+ check(1, RAD, 180 / PI, DEG);
+ check(TAU, RAD, 1, TURN);
+ check(360, DEG, 400, GON);
+ check(1 / 60, DEG, 1, ARCMIN);
+ },
+
+ length: () => {
+ check(1, M, 1000, MM);
+ check(1000, MM, 1, M);
+ check(1, M, 100, CM);
+ check(1, KM, 1000, M);
+ check(25.4e6, NM, 1, IN);
+ check(25.4, MM, 1, IN);
+ check(12, IN, 1, FT);
+ check(36, IN, 1, YD);
+ check(1760 * 36, IN, 1, MI);
+ },
+});
diff --git a/packages/units/tpl.readme.md b/packages/units/tpl.readme.md
new file mode 100644
index 0000000000..976c06d6f7
--- /dev/null
+++ b/packages/units/tpl.readme.md
@@ -0,0 +1,340 @@
+
+
+
+
+## About
+
+{{pkg.description}}
+
+All unit definitions & conversions are based on the SI units described here:
+
+- https://en.wikipedia.org/wiki/International_System_of_Units
+
+### Unit definitions
+
+Each unit is defined via a 7-dimensional vector representing individual exponents
+for each of the base SI dimensions, in order:
+
+| SI dimension | Base unit | Base unit symbol |
+|---------------------|-----------|------------------|
+| mass | kilogram | kg |
+| length | meter | m |
+| time | second | s |
+| current | ampere | A |
+| temperature | kelvin | K |
+| amount of substance | mole | mol |
+| luminous intensity | candela | cd |
+
+Dimensionless units are supported too, and for those all dimensions are set to
+zero.
+
+Additionally, we also define a scale factor and zero offset for each unit, with
+most dimensions' base units usually using a factor of 1 and no offset.
+
+For example, here's how we can define kilograms and meters:
+
+```ts
+const KG = unit(1, 1); // SI dimension 0
+// { dim: [ 1, 0, 0, 0, 0, 0, 0 ], scale: 1, offset: 0, prefix: false }
+
+const M = unit(1, 1); // SI dimension 1
+// { dim: [ 0, 1, 0, 0, 0, 0, 0 ], scale: 1, offset: 0, prefix: false }
+```
+
+More complex units like electrical resistance (e.g. kΩ) are based on more than a
+single dimension:
+
+```ts
+// { dim: [ 1, 2, -3, -2, 0, 0, 0 ], scale: 1000, offset: 0, prefix: true }
+```
+
+This dimension vector represents the unit definition for (see [SI derived
+units](https://en.wikipedia.org/wiki/SI_derived_unit)):
+
+> 1 Ohm = kg⋅m2⋅s−3⋅A−2
+
+### Predefined units
+
+The following units are provided as "builtins", here grouped by dimension:
+
+#### Acceleration
+
+| Unit name | JS name | Description |
+|-----------|----------|---------------------------|
+| `m/s2` | `M_S2` | meter per second squared |
+| `ft/s2` | `FT_S2` | foot per second squared |
+| `rad/s2` | `RAD_S2` | radian per second squared |
+| `g0` | `G0` | standard gravity |
+
+#### Angle
+
+| Unit name | JS name | Description |
+|-----------|----------|-------------|
+| `arcmin` | `ARCMIN` | arc minute |
+| `arcsec` | `ARCSEC` | arc second |
+| `deg` | `DEG` | degree |
+| `gon` | `GON` | gradian |
+| `rad` | `RAD` | radian |
+| `sr` | `SR` | steradian |
+| `turn` | `TURN` | turn |
+
+#### Area
+
+| Unit name | JS name | Description |
+|-----------|---------|-------------------|
+| `m2` | `M2` | square meter |
+| `cm2` | `CM2` | square centimeter |
+| `mm2` | `MM2` | square millimeter |
+| `km2` | `KM2` | square kilometer |
+| `ha` | `HA` | hectar |
+| `ac` | `AC` | acre |
+| `sqin` | `SQIN` | square inch |
+| `sqft` | `SQFT` | square foot |
+| `sqmi` | `SQMI` | square mile |
+
+#### Data
+
+| Unit name | JS name | Description |
+|-----------|---------|-----------------|
+| `bit` | `BIT` | bit |
+| `kbit` | `KBIT` | kilobit |
+| `Mbit` | `MBIT` | megabit |
+| `Gbit` | `GBIT` | gigabit |
+| `Tbit` | `TBIT` | terabit |
+| `B` | `BYTE` | byte (8 bit) |
+| `KB` | `KBYTE` | kilobyte (1024) |
+| `MB` | `MBYTE` | megabyte (1024) |
+| `GB` | `GBYTE` | gigabyte (1024) |
+| `TB` | `TBYTE` | terabyte (1024) |
+| `PB` | `PBYTE` | petabyte (1024) |
+| `EB` | `EBYTE` | exabyte (1024) |
+
+#### Electric current
+
+| Unit | JS name | Description |
+|------|-----------|-------------------|
+| A | `A` | ampere |
+| mA | `MA` | milliampere |
+| mAh | `MA_H` | milliampere-hours |
+| C | `C` | coulomb |
+| V | `V` | volt |
+| mV | `MV` | millivolt |
+| kV | `KV` | kilovolt |
+| F | `F` | farad |
+| pF | `PF` | picofarad |
+| µF | `µF` | microfarad |
+| Ω | `OHM` | ohm |
+| kΩ | `KOHM` | kiloohm |
+| MΩ | `MOHM` | megaohm |
+| GΩ | `GOHM` | gigaohm |
+| S | `SIEMENS` | siemens |
+| Wb | `WB` | weber |
+| T | `TESLA` | tesla |
+| H | `HENRY` | henry |
+
+#### Energy
+
+| Unit | JS name | Description |
+|------|---------|-------------|
+| J | `J` | joule |
+| kJ | `KJ` | kilojoule |
+| MJ | `MJ` | megajoule |
+| GJ | `GJ` | gigajoule |
+| cal | `CAL` | calorie |
+| kcal | `KCAL` | kilocalorie |
+
+#### Force
+
+| Unit | JS name | Description |
+|------|---------|-------------|
+| `N` | `N` | newton |
+
+#### Frequency
+
+| Unit | JS name | Description |
+|-------|---------|---------------------|
+| `Hz` | `HZ` | hertz |
+| `kHz` | `KHZ` | kilohertz |
+| `MHz` | `MHZ` | megahertz |
+| `GHz` | `GHZ` | gigahertz |
+| `THz` | `THZ` | terahertz |
+| `rpm` | `RPM` | rotation per minute |
+| `ω` | `OMEGA` | radian per second |
+
+#### Length
+
+| Unit name | JS name | Description |
+|-----------|---------|-------------------|
+| `m` | `M` | meter |
+| `cm` | `CM` | centimeter |
+| `mm` | `MM` | millimeter |
+| `µm` | `µM` | micrometer |
+| `nm` | `NM` | nanometer |
+| `km` | `KM` | kilometer |
+| `au` | `AU` | astronomical unit |
+| `in` | `IN` | inch |
+| `ft` | `FT` | foot |
+| `yd` | `YD` | yard |
+| `mi` | `MI` | mile |
+| `nmi` | `NMI` | nautical mile |
+| `pica` | `PICA` | pica |
+| `point` | `POINT` | point |
+
+#### Luminous intensity
+
+| Unit | JS name | Description |
+|------|---------|-------------|
+| `cd` | `CD` | candela |
+| `lm` | `LM` | lumen |
+| `lx` | `LX` | lux |
+
+#### Mass
+
+| Unit name | JS name | Description |
+|-----------|---------|----------------|
+| `µg` | `µG` | microgram |
+| `mg` | `mG` | milligram |
+| `g` | `G` | gram |
+| `kg` | `KG` | kilogram |
+| `t` | `T` | tonne |
+| `kt` | `KT` | kilotonne |
+| `Mt` | `MT` | megatonne |
+| `Gt` | `GT` | gigatonne |
+| `lb` | `LB` | imperial pound |
+| `st` | `ST` | stone |
+
+#### Power
+
+| Unit name | JS name | Description |
+|-----------|---------|---------------|
+| `W` | `W` | watt |
+| `mW` | `MW` | milliwatt |
+| `kW` | `KW` | kilowatt |
+| `MW` | `MW` | megawatt |
+| `GW` | `GW` | gigawatt |
+| `TW` | `TW` | terawatt |
+| `Wh` | `WH` | watt-hour |
+| `kWh` | `KWH` | kilowatt-hour |
+
+#### Pressure
+
+| Unit name | JS name | Description |
+|-----------|---------|-----------------------|
+| `Pa` | `PA` | pascal |
+| `kPa` | `KPA` | kilopascal |
+| `MPa` | `MPA` | megapascal |
+| `GPa` | `GPA` | gigapascal |
+| `at` | `AT` | technical atmosphere |
+| `atm` | `ATM` | atmosphere |
+| `bar` | `BAR` | bar |
+| `psi` | `PSI` | pound per square inch |
+
+#### Speed
+
+| Unit | JS name | Description |
+|--------|---------|--------------------|
+| `m/s` | `M_S` | meter per second |
+| `km/h` | `KM_H` | kilometer per hour |
+| `mph` | `MPH` | mile per hour |
+| `kn` | `KN` | knot |
+
+#### Substance
+
+| Unit | JS name | Description |
+|-------|---------|-------------|
+| `mol` | `MOL` | mole |
+
+#### Temperature
+
+| Unit | JS name | Description |
+|------|---------|-------------------|
+| `K` | `K` | kelvin |
+| `℃` | `DEG_C` | degree celsius |
+| `℉` | `DEG_F` | degree fahrenheit |
+
+#### Time
+
+| Unit | JS name | Description |
+|-------|---------|--------------------|
+| s | `S` | second |
+| ms | `MS` | millisecond |
+| µs | `µS` | microsecond |
+| ns | `NS` | nanosecond |
+| min | `MIN` | minute |
+| h | `H` | hour |
+| day | `DAY` | day |
+| week | `WEEK` | week |
+| month | `MONTH` | month (30 days) |
+| year | `YEAR` | year (365.25 days) |
+
+#### Volume
+
+| Unit | JS name | Description |
+|-------------|-----------|----------------------|
+| `m3` | `M3` | cubic meter |
+| `mm3` | `MM3` | cubic millimeter |
+| `cm3` | `CM3` | cubic centimeter |
+| `km3` | `KM3` | cubic kilometer |
+| `l` | `L` | liter |
+| `cl` | `CL` | centiliter |
+| `ml` | `ML` | milliliter |
+| `imp gal` | `GAL` | imperial gallon |
+| `imp pt` | `PT` | imperial pint |
+| `imp fl oz` | `FLOZ` | imperial fluid ounce |
+| `us gal` | `US_GAL` | US gallon |
+| `us pt` | `US_PT` | US pint |
+| `us cup` | `US_CUP` | US cup |
+| `us fl oz` | `US_FLOZ` | US fluid ounce |
+
+### Creating & deriving units
+
+#### Using standard metric prefixes
+
+Existing coherent
+
+### Unit conversions
+
+Only units with compatible (incl. reciprocal) dimensions can be converted,
+otherwise an error will be thrown. Units can be given as
+
+```ts
+// convert from km/h to mph using unit names
+convert(100, "km/h", "mph");
+// 62.13711922373341
+
+// using predefined unit constants directly
+convert(60, MPH, KM_H);
+// 96.56063999999998
+
+// or using anonymous units (meter/second ⇒ yard/hour)
+convert(1, "m/s", div(YD, H))
+// 3937.007874015749
+```
+
+{{meta.status}}
+
+{{repo.supportPackages}}
+
+{{repo.relatedPackages}}
+
+{{meta.blogPosts}}
+
+## Installation
+
+{{pkg.install}}
+
+{{pkg.size}}
+
+## Dependencies
+
+{{pkg.deps}}
+
+{{repo.examples}}
+
+## API
+
+{{pkg.docs}}
+
+TODO
+
+
diff --git a/packages/units/tsconfig.json b/packages/units/tsconfig.json
new file mode 100644
index 0000000000..e19642bf9a
--- /dev/null
+++ b/packages/units/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "outDir": "."
+ },
+ "include": ["./src/**/*.ts"]
+}
diff --git a/yarn.lock b/yarn.lock
index ad66bdc3cb..6cc847b4ad 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5419,6 +5419,22 @@ __metadata:
languageName: unknown
linkType: soft
+"@thi.ng/units@workspace:packages/units":
+ version: 0.0.0-use.local
+ resolution: "@thi.ng/units@workspace:packages/units"
+ dependencies:
+ "@microsoft/api-extractor": ^7.34.4
+ "@thi.ng/checks": ^3.3.9
+ "@thi.ng/equiv": ^2.1.19
+ "@thi.ng/errors": ^2.2.12
+ "@thi.ng/testament": ^0.3.12
+ rimraf: ^4.4.0
+ tools: "workspace:^"
+ typedoc: ^0.23.26
+ typescript: ^4.9.5
+ languageName: unknown
+ linkType: soft
+
"@thi.ng/vclock@workspace:packages/vclock":
version: 0.0.0-use.local
resolution: "@thi.ng/vclock@workspace:packages/vclock"