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.
@@ -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.
@@ -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,10 @@
1
+ include LICENSE
2
+ include README.md
3
+ include CHANGELOG.md
4
+ include CONTRIBUTING.md
5
+ include conftest.py
6
+ include pyconvert/py.typed
7
+ recursive-include tests *.py
8
+ recursive-include examples *.py
9
+ global-exclude __pycache__
10
+ global-exclude *.py[cod]
@@ -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
+ [![Tests](https://github.com/cobrapy-x/pyconvert-package/actions/workflows/tests.yml/badge.svg)](https://github.com/cobrapy-x/pyconvert-package/actions/workflows/tests.yml)
44
+ [![PyPI](https://img.shields.io/pypi/v/pyconvertx.svg)](https://pypi.org/project/pyconvertx/)
45
+ [![Python versions](https://img.shields.io/pypi/pyversions/pyconvertx.svg)](https://pypi.org/project/pyconvertx/)
46
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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).