From 5571c1f32e61b1736640ddb340156642073f6ed5 Mon Sep 17 00:00:00 2001 From: Reiner Jung Date: Tue, 14 Oct 2025 10:15:01 +0200 Subject: [PATCH 1/8] Added new unit funtions, yet not available to users. --- cfunits/units.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cfunits/units.py b/cfunits/units.py index b1e4058..69c0cd1 100644 --- a/cfunits/units.py +++ b/cfunits/units.py @@ -185,6 +185,7 @@ _cv_free.restype = None _UT_ASCII = 0 +_UT_UTF8 = 2 _UT_NAMES = 4 _UT_DEFINITION = 8 @@ -2052,6 +2053,14 @@ def formatted(self, names=None, definition=None): return out.decode("utf-8") + @classmethod + def new_unit(cls, name): + base = _ut_new_base_unit(_ut_system) + _ut_map_name_to_unit(name, _UT_ASCII, base) + _ut_map_unit_to_name(base, name, _UT_ASCII) + return base + + @classmethod def conform(cls, x, from_units, to_units, inplace=False): """Conforms values to equivalent values in a compatible unit. From 0e7660c5a76594dc4bc029b179f2256d180a3d1a Mon Sep 17 00:00:00 2001 From: Reiner Jung Date: Fri, 12 Dec 2025 21:18:59 +0100 Subject: [PATCH 2/8] Added feature to add custom units by library users. The rational is that especially with countable amounts, like currency, cups etc. they are not interchangable and units can be used to distinguish them. --- cf_test.py | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ cfunits/units.py | 48 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 cf_test.py diff --git a/cf_test.py b/cf_test.py new file mode 100644 index 0000000..b8413b7 --- /dev/null +++ b/cf_test.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Mon Oct 6 18:53:26 2025 + +The main use for this script is to test and demonstrate the use of user defined +units. + +@author: Reiner Jung +""" + +from cfunits import Units + +def info(unit, name:str): + print(f"Unit {name}") + print(f" Status {unit.isvalid}") + print(f" Reason {unit.reason_notvalid}") + print(f" Unit {unit}") + try: + result=unit.formatted() + print(f" Formatted {result}") + except ValueError as e: + print(f" Formatting failed: {e}") + +# make a new custom unit +Units.new_unit("pebbles") + +# berry is not previously defined +u_unknown = Units("berry") + +# ppm and dB are an addition to udunits, but defined in cf_units +u_ppm = Units("ppm") +u_db = Units("dB") + +# use custom unit +u_pebbles = Units("pebbles") + +# use composed unit of predefined units from udunits +u_m_kg = Units("m/kg") + +# use composed unit of a cf_units unit and an udunits unit +u_db_m = Units("dB/m") + +# use composed unit of a custom unit and an udunits unit +u_pebbles_m = Units("pebbles/m") + +# print info on these units +info(u_ppm, "PPM") +info(u_db, "decibel") + +info(u_unknown, "berry") +info(u_pebbles, "pebbles") + +info(u_m_kg, "m/kg") +info(u_db_m, "decibel/m") +info(u_pebbles_m, "pebbles/m") + +# end + diff --git a/cfunits/units.py b/cfunits/units.py index 69c0cd1..00eb087 100644 --- a/cfunits/units.py +++ b/cfunits/units.py @@ -345,6 +345,46 @@ def add_unit_alias(definition, symbol, singular, plural): # [-99, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]]) +# -------------------------------------------------------------------- +# Function to decode Udunits status codes to text +# -------------------------------------------------------------------- +def decode_status(status): + match status: + case 0: + return "success" + case 1: + return "bad argument" + case 2: + return "already exists" + case 3: + return "no such unit" + case 4: + return "os error, see errno" + case 5: + return "units belong to different unit-systems" + case 6: + return "operation on the unit(s) is meaningless" + case 7: + return "unit-system doesn't have a unit named 'second'" + case 8: + return "error occurred while visiting a unit" + case 9: + return "unit can't be formatted in the desired manner" + case 10: + return "string unit representation contains syntax error" + case 11: + return "string unit representation contains unknown word" + case 12: + return "cannot open argument-specified unit database" + case 13: + return "cannot open environment-specified unit database" + case 14: + return "cannot open installed, default, unit database" + case 15: + return "error parsing unit specification" + case _: + return f"unknown error {status}!" + # -------------------------------------------------------------------- # Function to control Udunits error messages # -------------------------------------------------------------------- @@ -2044,7 +2084,7 @@ def formatted(self, names=None, definition=None): if _ut_format(ut_unit, _string_buffer, _sizeof_buffer, opts) != -1: out = _string_buffer.value else: - raise ValueError(f"Can't format unit {self!r}") + raise ValueError(f"Cannot format unit {self!r} cause: {decode_status(_ut_get_status())}") if self.isreftime: out = str(out, "utf-8") # needs converting from byte-string @@ -2056,8 +2096,10 @@ def formatted(self, names=None, definition=None): @classmethod def new_unit(cls, name): base = _ut_new_base_unit(_ut_system) - _ut_map_name_to_unit(name, _UT_ASCII, base) - _ut_map_unit_to_name(base, name, _UT_ASCII) + + assert 0 == _ut_map_unit_to_name(base, name.encode("utf-8"), _UT_UTF8) + assert 0 == _ut_map_name_to_unit(name.encode("utf-8"), _UT_UTF8, base) + return base From 8b22602ed91e95a4530445d71410a34179451281 Mon Sep 17 00:00:00 2001 From: Reiner Jung Date: Sun, 22 Feb 2026 16:11:25 +0100 Subject: [PATCH 3/8] Update cfunits/units.py Co-authored-by: David Hassell --- cfunits/units.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfunits/units.py b/cfunits/units.py index 00eb087..708881e 100644 --- a/cfunits/units.py +++ b/cfunits/units.py @@ -348,7 +348,7 @@ def add_unit_alias(definition, symbol, singular, plural): # -------------------------------------------------------------------- # Function to decode Udunits status codes to text # -------------------------------------------------------------------- -def decode_status(status): +def _decode_status(status): match status: case 0: return "success" From 347e41ab6d3b0839497347e4aec4b31cf178a5bc Mon Sep 17 00:00:00 2001 From: Reiner Jung Date: Sun, 22 Feb 2026 16:14:00 +0100 Subject: [PATCH 4/8] Update cfunits/units.py Co-authored-by: David Hassell --- cfunits/units.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cfunits/units.py b/cfunits/units.py index 708881e..9f4f894 100644 --- a/cfunits/units.py +++ b/cfunits/units.py @@ -2084,7 +2084,7 @@ def formatted(self, names=None, definition=None): if _ut_format(ut_unit, _string_buffer, _sizeof_buffer, opts) != -1: out = _string_buffer.value else: - raise ValueError(f"Cannot format unit {self!r} cause: {decode_status(_ut_get_status())}") + raise ValueError(f"Cannot format unit {self!r} cause: {_decode_status(_ut_get_status())}") if self.isreftime: out = str(out, "utf-8") # needs converting from byte-string From 4106f1093fa99d34c1a15ce427df8faa1697efb8 Mon Sep 17 00:00:00 2001 From: Reiner Jung Date: Sun, 22 Feb 2026 16:50:08 +0100 Subject: [PATCH 5/8] Update units.py Added comment / documentation to new_unit --- cfunits/units.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cfunits/units.py b/cfunits/units.py index 9f4f894..bcdaa4b 100644 --- a/cfunits/units.py +++ b/cfunits/units.py @@ -2095,6 +2095,21 @@ def formatted(self, names=None, definition=None): @classmethod def new_unit(cls, name): + """Creates a new custom unit in the unit system. + + Returns the new unit. + + :Parameters: + name: str + name of the new unit. + + Returns + the newly created unit. + + **Examples** + >>> pebbles = Units.new_unit("pebbles") + + """ base = _ut_new_base_unit(_ut_system) assert 0 == _ut_map_unit_to_name(base, name.encode("utf-8"), _UT_UTF8) From d4bec206c4c648ecd8b73cca05b2f422404f0001 Mon Sep 17 00:00:00 2001 From: Reiner Jung Date: Sun, 22 Feb 2026 16:38:34 +0100 Subject: [PATCH 6/8] Added comments and a test. --- cfunits/test/test_Units.py | 18 ++++++++++++++++++ cfunits/units.py | 3 ++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/cfunits/test/test_Units.py b/cfunits/test/test_Units.py index cc3d005..98a55cb 100644 --- a/cfunits/test/test_Units.py +++ b/cfunits/test/test_Units.py @@ -140,6 +140,24 @@ def test_Units_equivalent(self): self.assertFalse(Units(" ").equivalent(Units(1))) self.assertFalse(Units("1").equivalent(Units(1))) + def test_Units_new_unit(self): + """Test the `new_unit` class method on `Units`.""" + + # can create a custom unit + u_pebbles = Units("pebbles") + + self.assertIsNotNone(u_pebbles) + self.assertIsTrue(u_pebbles.isvalid) + self.assertEqual(u_pebbles.formatted(), "pebbles") + + # use composed unit of a custom unit and an udunits unit + u_pebbles_m = Units("pebbles/m") + + self.assertIsNotNone(u_pebbles_m) + self.assertIsTrue(u_pebbles.isvalid) + # Note: formatting does not work currently + # self.assertEqual(u_pebbles.formatted(), "pebbles") + def test_Units_conform(self): """Tests the `conform` class method on `Units`.""" self.assertEqual(Units.conform(0.5, Units("km"), Units("m")), 500) diff --git a/cfunits/units.py b/cfunits/units.py index bcdaa4b..84ffbc5 100644 --- a/cfunits/units.py +++ b/cfunits/units.py @@ -2093,10 +2093,11 @@ def formatted(self, names=None, definition=None): return out.decode("utf-8") + @classmethod def new_unit(cls, name): """Creates a new custom unit in the unit system. - + Returns the new unit. :Parameters: From 759aa36159adcc1c24bbfa16bb07a28c962995a5 Mon Sep 17 00:00:00 2001 From: Reiner Jung Date: Sun, 22 Feb 2026 16:39:53 +0100 Subject: [PATCH 7/8] Added myself as contributor. --- docs/source/contributing.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index d1612f4..4b4151d 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -115,3 +115,5 @@ ideas, code, and documentation to the cfunits library: * Jonathan Gregory * Lars Bärring * Sadie Bartholomew +* Reiner Jung + From 9b4632f3076e826460f8f432599dcd3131d5e8ca Mon Sep 17 00:00:00 2001 From: Reiner Jung Date: Sun, 22 Feb 2026 16:57:06 +0100 Subject: [PATCH 8/8] Removed old check and example tool. --- cf_test.py | 59 ------------------------------------------------------ 1 file changed, 59 deletions(-) delete mode 100644 cf_test.py diff --git a/cf_test.py b/cf_test.py deleted file mode 100644 index b8413b7..0000000 --- a/cf_test.py +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -Created on Mon Oct 6 18:53:26 2025 - -The main use for this script is to test and demonstrate the use of user defined -units. - -@author: Reiner Jung -""" - -from cfunits import Units - -def info(unit, name:str): - print(f"Unit {name}") - print(f" Status {unit.isvalid}") - print(f" Reason {unit.reason_notvalid}") - print(f" Unit {unit}") - try: - result=unit.formatted() - print(f" Formatted {result}") - except ValueError as e: - print(f" Formatting failed: {e}") - -# make a new custom unit -Units.new_unit("pebbles") - -# berry is not previously defined -u_unknown = Units("berry") - -# ppm and dB are an addition to udunits, but defined in cf_units -u_ppm = Units("ppm") -u_db = Units("dB") - -# use custom unit -u_pebbles = Units("pebbles") - -# use composed unit of predefined units from udunits -u_m_kg = Units("m/kg") - -# use composed unit of a cf_units unit and an udunits unit -u_db_m = Units("dB/m") - -# use composed unit of a custom unit and an udunits unit -u_pebbles_m = Units("pebbles/m") - -# print info on these units -info(u_ppm, "PPM") -info(u_db, "decibel") - -info(u_unknown, "berry") -info(u_pebbles, "pebbles") - -info(u_m_kg, "m/kg") -info(u_db_m, "decibel/m") -info(u_pebbles_m, "pebbles/m") - -# end -