pyconvertx 1.0.0__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.
- pyconvertx-1.0.0/CHANGELOG.md +103 -0
- pyconvertx-1.0.0/CONTRIBUTING.md +130 -0
- pyconvertx-1.0.0/LICENSE +21 -0
- pyconvertx-1.0.0/MANIFEST.in +10 -0
- pyconvertx-1.0.0/PKG-INFO +315 -0
- pyconvertx-1.0.0/README.md +278 -0
- pyconvertx-1.0.0/conftest.py +12 -0
- pyconvertx-1.0.0/examples/basic_conversions.py +71 -0
- pyconvertx-1.0.0/examples/number_systems.py +72 -0
- pyconvertx-1.0.0/pyconvert/__init__.py +110 -0
- pyconvertx-1.0.0/pyconvert/bases.py +199 -0
- pyconvertx-1.0.0/pyconvert/exceptions.py +139 -0
- pyconvertx-1.0.0/pyconvert/py.typed +0 -0
- pyconvertx-1.0.0/pyconvert/registry.py +759 -0
- pyconvertx-1.0.0/pyconvert/units.py +221 -0
- pyconvertx-1.0.0/pyconvertx.egg-info/PKG-INFO +315 -0
- pyconvertx-1.0.0/pyconvertx.egg-info/SOURCES.txt +24 -0
- pyconvertx-1.0.0/pyconvertx.egg-info/dependency_links.txt +1 -0
- pyconvertx-1.0.0/pyconvertx.egg-info/requires.txt +6 -0
- pyconvertx-1.0.0/pyconvertx.egg-info/top_level.txt +1 -0
- pyconvertx-1.0.0/pyproject.toml +78 -0
- pyconvertx-1.0.0/setup.cfg +4 -0
- pyconvertx-1.0.0/tests/test_bases.py +168 -0
- pyconvertx-1.0.0/tests/test_errors.py +326 -0
- pyconvertx-1.0.0/tests/test_temperature.py +160 -0
- pyconvertx-1.0.0/tests/test_units.py +366 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [1.0.0] - 2026-10-11
|
|
9
|
+
|
|
10
|
+
The first release that actually works. Version 0.1.0 could not be imported at
|
|
11
|
+
all (see below), so although this is a complete rewrite of the public API,
|
|
12
|
+
nothing can have depended on the old one.
|
|
13
|
+
|
|
14
|
+
Published on PyPI as **`pyconvertx`**; the import name remains `pyconvert`.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- 182 units across 15 categories: length, mass, speed, area, volume, energy,
|
|
19
|
+
power, temperature, time, pressure, frequency, digital storage, angle,
|
|
20
|
+
electric current and electric potential. Temperature, time, pressure,
|
|
21
|
+
frequency, storage, angle, current and potential are all new.
|
|
22
|
+
- 390 aliases, so long names, plurals and typeable forms of awkward symbols
|
|
23
|
+
resolve to the same unit (`kilometre`, `kilometer`, `km`; `m2`, `m²`;
|
|
24
|
+
`degC`, `°C`).
|
|
25
|
+
- Both Unicode micro signs — MICRO SIGN (U+00B5) and GREEK SMALL LETTER MU
|
|
26
|
+
(U+03BC) — are mapped onto the ASCII spelling. They are visually identical
|
|
27
|
+
but different characters, so a plain dictionary lookup would accept one
|
|
28
|
+
user's `μs` and reject another's.
|
|
29
|
+
- `convert_base(number, from_base, to_base, precision=16)` for bases 2 to 36,
|
|
30
|
+
with support for fractional parts, negative values, mixed-case input and
|
|
31
|
+
underscore separators.
|
|
32
|
+
- An exception hierarchy rooted at `PyConvertError`, where each error also
|
|
33
|
+
subclasses the builtin you would expect (`KeyError`, `ValueError`,
|
|
34
|
+
`RuntimeError`).
|
|
35
|
+
- Error messages that suggest a fix: wrong case is detected and offered back,
|
|
36
|
+
near misses are fuzzy-matched, and deliberately ambiguous spellings explain
|
|
37
|
+
what to write instead.
|
|
38
|
+
- `convert_all`, `is_convertible`, `list_categories`, `list_units`,
|
|
39
|
+
`category_of` and `base_unit`.
|
|
40
|
+
- Type hints throughout, with a `py.typed` marker so type checkers use them.
|
|
41
|
+
- A test suite of 1072 tests, including every docstring example.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **The conversion API is entirely new.** `convert(value, from_unit, to_unit)`
|
|
46
|
+
replaces `convert(qty, units, value)`. The category argument is gone, and
|
|
47
|
+
unit pairs are no longer expressed as arrow strings:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# 0.1.0
|
|
51
|
+
convert('weight', 'gram-->kilogram', 500)
|
|
52
|
+
# 1.0.0
|
|
53
|
+
convert(500, 'gram', 'kilogram')
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- **Conversion factors are now stored relative to a base unit per category**
|
|
57
|
+
rather than as explicit pairs. Every pair within a category therefore works,
|
|
58
|
+
including ones nobody wrote down. The old design needed one entry per
|
|
59
|
+
direction per pair, which would have required roughly 1,700 entries to cover
|
|
60
|
+
these 15 categories; the registry holds 182.
|
|
61
|
+
- `convertnumber(base, number)` is replaced by
|
|
62
|
+
`convert_base(number, from_base, to_base)`, with the value first and both
|
|
63
|
+
bases given explicitly.
|
|
64
|
+
- `convert_base` always returns a string, including when the target base is 10.
|
|
65
|
+
- The package is now `pyconvert.registry`, `pyconvert.units`,
|
|
66
|
+
`pyconvert.bases` and `pyconvert.exceptions`; the single
|
|
67
|
+
`pyconvert.pyconvert` module is gone.
|
|
68
|
+
- Packaging moved from `setup.py` to `pyproject.toml`.
|
|
69
|
+
|
|
70
|
+
### Fixed
|
|
71
|
+
|
|
72
|
+
- **The package could not be imported.** `__init__.py` did
|
|
73
|
+
`from .pyconvert import convert, convert_number`, but the function was named
|
|
74
|
+
`convertnumber`, so `import pyconvert` raised `ImportError` for every user of
|
|
75
|
+
0.1.0.
|
|
76
|
+
- **`meter-->feet` was inverted.** It held 0.3048, the number of metres in a
|
|
77
|
+
foot, so converting metres to feet divided by 3.28 instead of multiplying.
|
|
78
|
+
The base-relative registry makes this class of mistake impossible to express,
|
|
79
|
+
and a test pins it.
|
|
80
|
+
- **`quintal-->kilogram` was 1000.** A quintal is 100 kg, so the factor was
|
|
81
|
+
wrong by a factor of ten.
|
|
82
|
+
- Missing reverse conversions: `solarmass-->kilogram` and `amu-->gram` had no
|
|
83
|
+
inverse, so those conversions failed in one direction. All pairs are now
|
|
84
|
+
symmetric by construction.
|
|
85
|
+
|
|
86
|
+
### Security
|
|
87
|
+
|
|
88
|
+
- Nothing applicable; the library performs arithmetic on numbers and strings
|
|
89
|
+
and makes no network, filesystem or subprocess calls.
|
|
90
|
+
|
|
91
|
+
## [0.1.0] - 2025-10-20
|
|
92
|
+
|
|
93
|
+
Initial release, kept as the `v0.1.0` git tag for history. Never published to
|
|
94
|
+
PyPI.
|
|
95
|
+
|
|
96
|
+
- Unit conversion across length, weight, speed, area, energy, power and volume
|
|
97
|
+
via a dictionary of `from-->to` pairs.
|
|
98
|
+
- `convertnumber` for converting a decimal number into any base from 2 to 36.
|
|
99
|
+
- **Known broken:** `import pyconvert` raised `ImportError`, so the package was
|
|
100
|
+
unusable as published. See the Fixed section above.
|
|
101
|
+
|
|
102
|
+
[1.0.0]: https://github.com/cobrapy-x/pyconvert-package/releases/tag/v1.0.0
|
|
103
|
+
[0.1.0]: https://github.com/cobrapy-x/pyconvert-package/releases/tag/v0.1.0
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Contributing to PyConvert
|
|
2
|
+
|
|
3
|
+
Thanks for your interest. This is a small library, so the bar is simple: a
|
|
4
|
+
change should be correct, covered by a test, and leave `pytest` and
|
|
5
|
+
`ruff check .` green.
|
|
6
|
+
|
|
7
|
+
## Getting set up
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git clone https://github.com/cobrapy-x/pyconvert-package.git
|
|
11
|
+
cd pyconvert-package
|
|
12
|
+
python -m pip install -e ".[dev]"
|
|
13
|
+
|
|
14
|
+
pytest # 1072 tests, including every docstring example
|
|
15
|
+
ruff check .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`pytest` with no arguments runs the suite in `tests/` *and* the doctests in
|
|
19
|
+
`pyconvert/`, because the examples in the docstrings are the same ones in the
|
|
20
|
+
README — they are checked rather than trusted.
|
|
21
|
+
|
|
22
|
+
## How the library is laid out
|
|
23
|
+
|
|
24
|
+
| File | Responsibility |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `pyconvert/registry.py` | The data: all 15 category tables, the aliases, and the index built from them |
|
|
27
|
+
| `pyconvert/units.py` | The conversion engine and the introspection helpers |
|
|
28
|
+
| `pyconvert/bases.py` | Number-base conversion |
|
|
29
|
+
| `pyconvert/exceptions.py` | The error hierarchy |
|
|
30
|
+
|
|
31
|
+
`bases.py` is named that way rather than `numbers.py` because the standard
|
|
32
|
+
library already has a `numbers` module — the one holding `numbers.Real`, which
|
|
33
|
+
`units.py` imports.
|
|
34
|
+
|
|
35
|
+
## Adding a unit
|
|
36
|
+
|
|
37
|
+
Units are stored as **how big one of them is, in the category's base unit**.
|
|
38
|
+
That is the whole trick: a conversion is then one ratio,
|
|
39
|
+
`value * f_source / f_target`, and every pair in the category works without
|
|
40
|
+
being written down.
|
|
41
|
+
|
|
42
|
+
1. Add an entry to the right table in `registry.py`. One nautical mile is
|
|
43
|
+
1852 metres, so under `LENGTH` it is `"nmi": 1852.0`.
|
|
44
|
+
|
|
45
|
+
2. **If the factor is exact by definition, write the definition, not the
|
|
46
|
+
decimal.** An inch *is* 25.4 mm — that is a definition, not a measurement.
|
|
47
|
+
Those are built from exact rationals and rounded to `float` once:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
"ac": float(4840 * _YD_Q ** 2), # an acre is 4840 square yards, exactly
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Computing the same thing in floating point gives a different answer:
|
|
54
|
+
`231 * 0.0254 ** 3` is `0.0037854117839999997`, three roundings away from
|
|
55
|
+
the correctly rounded `0.003785411784`. Mark genuinely measured values
|
|
56
|
+
(particle masses, the solar mass) with a comment saying so.
|
|
57
|
+
|
|
58
|
+
3. **Add aliases** for the spellings people will actually type — the long name,
|
|
59
|
+
the plural, and an ASCII form if the symbol needs a character they cannot
|
|
60
|
+
easily reach. Micro-prefixed units get their Unicode variants generated
|
|
61
|
+
automatically; do not add those by hand.
|
|
62
|
+
|
|
63
|
+
4. **Check the symbol is not already taken.** All 15 categories share one
|
|
64
|
+
namespace. If it collides, `import pyconvert` fails immediately with a
|
|
65
|
+
`RegistryError` naming both categories — that check runs at import, so you
|
|
66
|
+
will know in one second rather than from a bug report.
|
|
67
|
+
|
|
68
|
+
5. **If the spelling is genuinely ambiguous, do not pick a winner.** Add it to
|
|
69
|
+
`AMBIGUOUS` with a message saying what to use instead. `ton` means three
|
|
70
|
+
masses that differ by up to 12%; `oz` is both a mass and a volume. Returning
|
|
71
|
+
one of them silently is worse than refusing.
|
|
72
|
+
|
|
73
|
+
6. Add a test. The most valuable kind asserts a relationship fixed by
|
|
74
|
+
definition and independent of the metre or the kilogram:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
(1, "mi", "ft", 5280),
|
|
78
|
+
(1, "gal_us", "in3", 231),
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
An inverted factor still round-trips perfectly, so a round-trip test will
|
|
82
|
+
not catch it — but `1 mi == 5280 ft` will. That is exactly the bug that
|
|
83
|
+
shipped in 0.1.0.
|
|
84
|
+
|
|
85
|
+
## Adding a category
|
|
86
|
+
|
|
87
|
+
Add the table, add it to `CATEGORIES`, and declare its base unit in
|
|
88
|
+
`BASE_UNITS`. The base unit must have scale 1 and offset 0; `build_index`
|
|
89
|
+
enforces that at import.
|
|
90
|
+
|
|
91
|
+
If any unit in the category needs an **offset** rather than just a scale (as
|
|
92
|
+
temperature does), store its entry as a `(scale, offset)` tuple of
|
|
93
|
+
`Fraction`s, where `base_value = x * scale + offset`. The engine routes any
|
|
94
|
+
conversion involving a tuple entry through exact rational arithmetic, which is
|
|
95
|
+
what makes `100 °C → °F` return exactly `212.0` instead of
|
|
96
|
+
`211.99999999999997`.
|
|
97
|
+
|
|
98
|
+
## Invariants the suite enforces
|
|
99
|
+
|
|
100
|
+
These hold for every unit, and the tests will tell you if a change breaks one:
|
|
101
|
+
|
|
102
|
+
- Every unit round-trips through its category's base unit.
|
|
103
|
+
- For every pair `a`, `b` in a multiplicative category,
|
|
104
|
+
`convert(1, a, b) * convert(1, b, a) == 1`.
|
|
105
|
+
- Conversion is transitive: `a → c` agrees with `a → b → c`.
|
|
106
|
+
- No symbol is claimed by two categories, no alias shadows a canonical symbol,
|
|
107
|
+
and no alias points at a unit that does not exist.
|
|
108
|
+
- Every cross-category pair raises `IncompatibleUnitsError`.
|
|
109
|
+
- Every spelling in `AMBIGUOUS` is genuinely unresolvable, so its hint is
|
|
110
|
+
reachable.
|
|
111
|
+
- Every unit symbol appears in `README.md`.
|
|
112
|
+
|
|
113
|
+
## Style
|
|
114
|
+
|
|
115
|
+
`ruff check .` is the whole style guide; its configuration lives in
|
|
116
|
+
`pyproject.toml`. Comments are welcome where they explain *why* — a surprising
|
|
117
|
+
factor, a definition chosen over an alternative, a deliberate refusal. Prefer
|
|
118
|
+
those over comments restating what the code does.
|
|
119
|
+
|
|
120
|
+
## Submitting
|
|
121
|
+
|
|
122
|
+
1. Fork the repository and branch off `main`.
|
|
123
|
+
2. Make the change, with tests.
|
|
124
|
+
3. Run `pytest` and `ruff check .`.
|
|
125
|
+
4. Add a line to `CHANGELOG.md` under an `## [Unreleased]` heading.
|
|
126
|
+
5. Open a pull request describing what changed and why.
|
|
127
|
+
|
|
128
|
+
Bug reports are just as useful as patches. For a wrong conversion factor,
|
|
129
|
+
please include the units, the result you got, the result you expected, and a
|
|
130
|
+
source for the correct value.
|
pyconvertx-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 cobrapy-x
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyconvertx
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight unit and number-base conversion library with no dependencies
|
|
5
|
+
Author: cobrapy-x
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/cobrapy-x/pyconvert-package
|
|
8
|
+
Project-URL: Repository, https://github.com/cobrapy-x/pyconvert-package
|
|
9
|
+
Project-URL: Issues, https://github.com/cobrapy-x/pyconvert-package/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/cobrapy-x/pyconvert-package/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: units,unit-conversion,measurement,convert,converter,temperature,metric,imperial,number-base,radix
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Education
|
|
15
|
+
Classifier: Intended Audience :: Science/Research
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Topic :: Utilities
|
|
27
|
+
Classifier: Typing :: Typed
|
|
28
|
+
Requires-Python: >=3.9
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
34
|
+
Requires-Dist: build; extra == "dev"
|
|
35
|
+
Requires-Dist: twine; extra == "dev"
|
|
36
|
+
Dynamic: license-file
|
|
37
|
+
|
|
38
|
+
# PyConvert
|
|
39
|
+
|
|
40
|
+
A lightweight unit and number-base conversion library for Python, with **no
|
|
41
|
+
dependencies** beyond the standard library.
|
|
42
|
+
|
|
43
|
+
[](https://github.com/cobrapy-x/pyconvert-package/actions/workflows/tests.yml)
|
|
44
|
+
[](https://pypi.org/project/pyconvertx/)
|
|
45
|
+
[](https://pypi.org/project/pyconvertx/)
|
|
46
|
+
[](LICENSE)
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install pyconvertx
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
> **The install name and the import name differ.**
|
|
55
|
+
> Install **`pyconvertx`**, import **`pyconvert`**.
|
|
56
|
+
>
|
|
57
|
+
> ```python
|
|
58
|
+
> import pyconvert # not "import pyconvertx"
|
|
59
|
+
> ```
|
|
60
|
+
>
|
|
61
|
+
> The name `pyconvert` was already taken on PyPI in 2015 by an unrelated
|
|
62
|
+
> XML/JSON library, so this project is distributed under `pyconvertx`. A
|
|
63
|
+
> distribution name that differs from the import name is common — `pip install
|
|
64
|
+
> beautifulsoup4` gives you `import bs4`, and `pip install pillow` gives you
|
|
65
|
+
> `import PIL`.
|
|
66
|
+
|
|
67
|
+
## Quick start
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
import pyconvert
|
|
71
|
+
|
|
72
|
+
pyconvert.convert(100, "km", "mi") # 62.13711922373339
|
|
73
|
+
pyconvert.convert(100, "celsius", "fahrenheit") # 212.0
|
|
74
|
+
pyconvert.convert(1024, "MiB", "GiB") # 1.0
|
|
75
|
+
pyconvert.convert(1, "gal_us", "L") # 3.785411784
|
|
76
|
+
|
|
77
|
+
pyconvert.convert_base(100, 10, 2) # '1100100'
|
|
78
|
+
pyconvert.convert_base("10101111", 2, 16) # 'AF'
|
|
79
|
+
pyconvert.convert_base("FF", 16, 10) # '255'
|
|
80
|
+
pyconvert.convert_base(1000, 10, 36) # 'RS'
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
No setup, no classes to instantiate, no unit objects to construct.
|
|
84
|
+
|
|
85
|
+
## Unit conversion
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
convert(value, from_unit, to_unit) -> float
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
There is **no category argument**. Every unit symbol is unique across the whole
|
|
92
|
+
library, so the category is looked up from the symbols you give. A consequence
|
|
93
|
+
worth having: mixing categories is an error rather than a plausible-looking
|
|
94
|
+
wrong number.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
>>> pyconvert.convert(1, "kg", "m")
|
|
98
|
+
IncompatibleUnitsError: cannot convert 'kg' to 'm': 'kg' measures mass, 'm' measures length
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Supported units
|
|
102
|
+
|
|
103
|
+
182 units across 15 categories. Factors are stored relative to
|
|
104
|
+
each category's base unit, so **every pair within a category works**, including
|
|
105
|
+
combinations nobody thought to write down (`furlong` to `nmi`, `therm` to `eV`).
|
|
106
|
+
|
|
107
|
+
| Category | Base | Units | Count |
|
|
108
|
+
|---|---|---|---|
|
|
109
|
+
| **Length** | `m` | `angstrom`, `au`, `chain`, `cm`, `dam`, `dm`, `fathom`, `ft`, `furlong`, `hm`, `in`, `km`, `ly`, `m`, `mi`, `mm`, `nm`, `nmi`, `pc`, `pm`, `rod`, `thou`, `um`, `yd` | 24 |
|
|
110
|
+
| **Mass** | `kg` | `carat`, `g`, `grain`, `kg`, `lb`, `m_e`, `m_earth`, `m_sun`, `mg`, `oz`, `quintal`, `st`, `t`, `ton_uk`, `ton_us`, `u`, `ug` | 17 |
|
|
111
|
+
| **Speed** | `m/s` | `c`, `cm/s`, `ft/s`, `km/h`, `kn`, `m/s`, `mm/s`, `mph` | 8 |
|
|
112
|
+
| **Area** | `m2` | `ac`, `are`, `cm2`, `ft2`, `ha`, `in2`, `km2`, `m2`, `mi2`, `mm2`, `yd2` | 11 |
|
|
113
|
+
| **Volume** | `m3` | `bbl`, `cL`, `cm3`, `cup_us`, `dL`, `floz_imp`, `floz_us`, `ft3`, `gal_imp`, `gal_us`, `in3`, `kL`, `km3`, `L`, `m3`, `mL`, `mm3`, `pt_imp`, `pt_us`, `qt_imp`, `qt_us`, `tbsp_us`, `tsp_us`, `uL`, `yd3` | 25 |
|
|
114
|
+
| **Energy** | `J` | `BTU`, `cal`, `cal_it`, `erg`, `eV`, `ft_lbf`, `GeV`, `GJ`, `J`, `kcal`, `keV`, `kJ`, `kWh`, `MeV`, `MJ`, `mJ`, `MWh`, `therm`, `Wh` | 19 |
|
|
115
|
+
| **Power** | `W` | `BTU/h`, `GW`, `hp`, `hp_metric`, `kW`, `MW`, `mW`, `TW`, `uW`, `W` | 10 |
|
|
116
|
+
| **Temperature** | `kelvin` | `celsius`, `fahrenheit`, `kelvin`, `rankine` | 4 |
|
|
117
|
+
| **Time** | `s` | `d`, `fortnight`, `h`, `min`, `ms`, `ns`, `ps`, `s`, `us`, `week`, `yr` | 11 |
|
|
118
|
+
| **Pressure** | `Pa` | `atm`, `bar`, `GPa`, `hPa`, `inHg`, `kPa`, `mbar`, `mmHg`, `MPa`, `Pa`, `psi`, `torr` | 12 |
|
|
119
|
+
| **Frequency** | `Hz` | `GHz`, `Hz`, `kHz`, `MHz`, `mHz`, `rpm`, `THz` | 7 |
|
|
120
|
+
| **Digital storage** | `B` | `B`, `bit`, `GB`, `Gbit`, `GiB`, `kB`, `kbit`, `KiB`, `MB`, `Mbit`, `MiB`, `PB`, `PiB`, `TB`, `TiB` | 15 |
|
|
121
|
+
| **Angle** | `rad` | `arcmin`, `arcsec`, `deg`, `grad`, `mrad`, `rad`, `turn` | 7 |
|
|
122
|
+
| **Electric current** | `A` | `A`, `kA`, `mA`, `nA`, `pA`, `uA` | 6 |
|
|
123
|
+
| **Electric potential** | `V` | `kV`, `MV`, `mV`, `nV`, `uV`, `V` | 6 |
|
|
124
|
+
|
|
125
|
+
### Alternative spellings
|
|
126
|
+
|
|
127
|
+
Long names, plurals and typeable forms of awkward symbols all resolve to the
|
|
128
|
+
same unit — 390 aliases in total:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
pyconvert.convert(1, "kilometre", "mile") # British or American spelling
|
|
132
|
+
pyconvert.convert(1, "meters", "feet") # plurals
|
|
133
|
+
pyconvert.convert(5000, "m2", "hectare") # ASCII for m^2
|
|
134
|
+
pyconvert.convert(5000, "m²", "hectare") # or the real character
|
|
135
|
+
pyconvert.convert(100, "degC", "degF") # ASCII for °C
|
|
136
|
+
pyconvert.convert(100, "°C", "°F") # or the real character
|
|
137
|
+
pyconvert.convert(500, "μs", "ms") # either Unicode micro sign
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
That last one matters more than it looks. The micro prefix is written two ways
|
|
141
|
+
that are visually identical: MICRO SIGN (U+00B5), which is what a Mac's
|
|
142
|
+
`Option-M` produces, and GREEK SMALL LETTER MU (U+03BC). They are different
|
|
143
|
+
characters, so a dictionary treats them as different keys. Both map to the ASCII
|
|
144
|
+
spelling (`us`, `um`, `ug`, `uA`, `uV`, `uW`, `uL`), as does the plain ASCII form.
|
|
145
|
+
|
|
146
|
+
### Lookup is case-sensitive
|
|
147
|
+
|
|
148
|
+
It has to be. These pairs are standard notation and differ only by case:
|
|
149
|
+
|
|
150
|
+
| | | |
|
|
151
|
+
|---|---|---|
|
|
152
|
+
| `mW` milliwatt | `MW` megawatt | factor of 10⁹ |
|
|
153
|
+
| `mHz` millihertz | `MHz` megahertz | factor of 10⁹ |
|
|
154
|
+
| `mV` millivolt | `MV` megavolt | factor of 10⁹ |
|
|
155
|
+
| `kB` kilobyte (1000) | `KiB` kibibyte (1024) | 2.4% apart |
|
|
156
|
+
| `ps` picosecond | `PS` metric horsepower | different categories |
|
|
157
|
+
|
|
158
|
+
Folding case would make those indistinguishable. Wrong case is caught and
|
|
159
|
+
suggested back instead:
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
>>> pyconvert.convert(1, "KM", "mi")
|
|
163
|
+
UnknownUnitError: unknown unit 'KM' (did you mean 'km'?)
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Units this library refuses on purpose
|
|
167
|
+
|
|
168
|
+
Some spellings have more than one real meaning, and resolving them to one
|
|
169
|
+
arbitrary choice would return a confidently wrong number. These raise with the
|
|
170
|
+
fix in the message rather than guessing:
|
|
171
|
+
|
|
172
|
+
| Spelling | Why, and what to use |
|
|
173
|
+
|---|---|
|
|
174
|
+
| `C` | use 'celsius' or 'degC' (bare 'C' is the SI symbol for coulomb) |
|
|
175
|
+
| `F` | use 'fahrenheit' or 'degF' (bare 'F' is the SI symbol for farad) |
|
|
176
|
+
| `R` | use 'rankine' or 'degR' (bare 'R' is the symbol for roentgen) |
|
|
177
|
+
| `b` | use 'bit' or 'B' (byte) -- 'b' and 'B' differ only by case |
|
|
178
|
+
| `cal_cap` | use 'kcal' for the food Calorie, or 'cal' for the gram calorie |
|
|
179
|
+
| `fl_ounce` | use 'floz_us' or 'floz_imp' |
|
|
180
|
+
| `gb` | use 'Gbit' (10**9 bits) or 'GB' (10**9 bytes) |
|
|
181
|
+
| `hp_imp` | use 'hp' (mechanical) or 'hp_metric' (PS) |
|
|
182
|
+
| `kb` | use 'kbit' (1000 bits) or 'kB' (1000 bytes) |
|
|
183
|
+
| `mb` | use 'Mbit' (10**6 bits) or 'MB' (10**6 bytes) |
|
|
184
|
+
| `pt` | 'pt' is both point and pint: use 'pt_us' or 'pt_imp' for pints |
|
|
185
|
+
| `tb` | use 'TB' (10**12 bytes) or 'TiB' (2**40 bytes) |
|
|
186
|
+
| `ton` | 'ton' means three different masses: use 't' (tonne, 1000 kg), 'ton_us' (short, 907.18474 kg) or 'ton_uk' (long, 1016.0469088 kg) |
|
|
187
|
+
| `tons` | see 'ton': use 't', 'ton_us' or 'ton_uk' |
|
|
188
|
+
|
|
189
|
+
`C`, `F` and `R` are also kept free so that charge, capacitance and radiation
|
|
190
|
+
can be added later without a breaking change.
|
|
191
|
+
|
|
192
|
+
One default *is* chosen: bare `gal`, `gallon`, `quart`, `pint`, `cup`, `floz`,
|
|
193
|
+
`tbsp` and `tsp` mean the **US** measures. The imperial ones are available
|
|
194
|
+
explicitly as `gal_imp`, `qt_imp`, `pt_imp` and `floz_imp`.
|
|
195
|
+
|
|
196
|
+
Where a unit has competing definitions, the one picked is documented:
|
|
197
|
+
|
|
198
|
+
| Unit | This library uses | Also available |
|
|
199
|
+
|---|---|---|
|
|
200
|
+
| `cal` | thermochemical, 4.184 J exactly | `cal_it` (4.1868 J) |
|
|
201
|
+
| `hp` | mechanical, 745.6998… W | `hp_metric` / `PS` (735.49875 W) |
|
|
202
|
+
| `BTU` | International Table, 1055.05585262 J | — |
|
|
203
|
+
| `yr` | Julian year, exactly 365.25 days | — |
|
|
204
|
+
| `t` | tonne, 1000 kg | `ton_us`, `ton_uk` |
|
|
205
|
+
|
|
206
|
+
## Number bases
|
|
207
|
+
|
|
208
|
+
```python
|
|
209
|
+
convert_base(number, from_base, to_base, precision=16) -> str
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Bases 2 to 36. Input may be an `int` or a `str`, in either case, with
|
|
213
|
+
underscores allowed as separators. Output digits are always uppercase.
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
pyconvert.convert_base("ff", 16, 2) # '11111111'
|
|
217
|
+
pyconvert.convert_base("1111_1111", 2, 16) # 'FF'
|
|
218
|
+
pyconvert.convert_base(-255, 10, 16) # '-FF'
|
|
219
|
+
pyconvert.convert_base(1100100, 2, 10) # '100' (an int is read as its digits)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Fractional parts are converted exactly where the result terminates:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
pyconvert.convert_base("0.5", 10, 2) # '0.1'
|
|
226
|
+
pyconvert.convert_base("A.8", 16, 10) # '10.5'
|
|
227
|
+
pyconvert.convert_base("0.1", 10, 2) # '0.0001100110011001'
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
That last result is truncated, not wrong — one tenth has no finite binary form,
|
|
231
|
+
so it runs to `precision` digits and stops. Pass `precision` to change that.
|
|
232
|
+
|
|
233
|
+
**The result is always a string**, including when the target base is 10. Once
|
|
234
|
+
`'AF'` is a string, returning an `int` for base 10 alone would make the return
|
|
235
|
+
*type* depend on an argument's *value*. Call `int(result)` when you want a
|
|
236
|
+
number back.
|
|
237
|
+
|
|
238
|
+
## Accuracy
|
|
239
|
+
|
|
240
|
+
Factors that are exact by international definition are stored as exact rationals
|
|
241
|
+
and rounded to `float` once, rather than being computed in floating point. The
|
|
242
|
+
difference is small but real: `231 * 0.0254 ** 3` gives
|
|
243
|
+
`0.0037854117839999997`, three roundings away from the correctly rounded
|
|
244
|
+
`0.003785411784`.
|
|
245
|
+
|
|
246
|
+
**Temperature is computed in exact rational arithmetic**, because it is the one
|
|
247
|
+
category with an offset, and 5/9 has no finite binary form. In plain floats,
|
|
248
|
+
`100 °C → °F` returns `211.99999999999997`. Here:
|
|
249
|
+
|
|
250
|
+
```python
|
|
251
|
+
pyconvert.convert(100, "celsius", "fahrenheit") # 212.0 exactly
|
|
252
|
+
pyconvert.convert(0, "celsius", "fahrenheit") # 32.0 exactly
|
|
253
|
+
pyconvert.convert(-40, "celsius", "fahrenheit") # -40.0 exactly
|
|
254
|
+
pyconvert.convert(0, "kelvin", "fahrenheit") # -459.67 exactly
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Everything else uses a single float ratio, which is about ten times faster and
|
|
258
|
+
is what nearly every conversion needs. Results are ordinary IEEE-754 doubles, so
|
|
259
|
+
between two non-base units you can still see a last-digit artefact:
|
|
260
|
+
`convert(1, "gal_imp", "L")` is `4.5460899999999995` rather than `4.54609`.
|
|
261
|
+
Conversions *to* a category's base unit are exact.
|
|
262
|
+
|
|
263
|
+
## Errors
|
|
264
|
+
|
|
265
|
+
Everything this library raises derives from `PyConvertError`, so one `except`
|
|
266
|
+
catches all of it. Each error also subclasses the builtin you would expect, so
|
|
267
|
+
existing `except KeyError` or `except ValueError` code keeps working.
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
PyConvertError
|
|
271
|
+
├── UnknownUnitError (also KeyError) unit not in the registry
|
|
272
|
+
├── UnknownCategoryError (also KeyError) no such category
|
|
273
|
+
├── IncompatibleUnitsError the two units measure different things
|
|
274
|
+
├── InvalidBaseError (also ValueError) base outside 2-36
|
|
275
|
+
├── InvalidDigitError (also ValueError) digit invalid for its base
|
|
276
|
+
└── RegistryError (also RuntimeError) the registry itself is inconsistent
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`RegistryError` is raised at import time, never from a call. It means two
|
|
280
|
+
categories claim the same symbol, or an alias points at a unit that does not
|
|
281
|
+
exist — a bug in the library, not in your input.
|
|
282
|
+
|
|
283
|
+
## Introspection
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
pyconvert.list_categories() # ['angle', 'area', 'current', ...]
|
|
287
|
+
pyconvert.list_units("temperature") # ['celsius', 'fahrenheit', 'kelvin', 'rankine']
|
|
288
|
+
pyconvert.list_units() # all 182 canonical symbols
|
|
289
|
+
pyconvert.category_of("psi") # 'pressure'
|
|
290
|
+
pyconvert.base_unit("pressure") # 'Pa'
|
|
291
|
+
pyconvert.is_convertible("km", "kg") # False
|
|
292
|
+
pyconvert.convert_all([0, 100], "celsius", "fahrenheit") # [32.0, 212.0]
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Requirements
|
|
296
|
+
|
|
297
|
+
Python 3.9 or newer. No third-party dependencies.
|
|
298
|
+
|
|
299
|
+
## Development
|
|
300
|
+
|
|
301
|
+
```bash
|
|
302
|
+
git clone https://github.com/cobrapy-x/pyconvert-package.git
|
|
303
|
+
cd pyconvert-package
|
|
304
|
+
pip install -e ".[dev]"
|
|
305
|
+
|
|
306
|
+
pytest # 1072 tests, including the docstring examples
|
|
307
|
+
ruff check .
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add a unit, and the
|
|
311
|
+
invariants the test suite enforces.
|
|
312
|
+
|
|
313
|
+
## License
|
|
314
|
+
|
|
315
|
+
MIT — see [LICENSE](LICENSE).
|