From c75f9cfec653ca6aa38c4c0f5489ca36c27f15ac Mon Sep 17 00:00:00 2001 From: Tim Case Date: Thu, 23 Apr 2026 10:28:16 -0500 Subject: [PATCH 1/5] Support for modulo and remainders, capacity planning, etc. CLoses #127 --- README.rst | 1 + bitmath/__init__.py | 38 +++++++-- docsite/source/index.rst | 1 + docsite/source/index.rst.in | 1 + docsite/source/simple_examples.rst | 123 +++++++++++++++++++++++++++++ python-bitmath.spec | 16 ++-- tests/test_future_math.py | 65 +++++++++++++++ 7 files changed, 233 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index dda92b3..b015011 100644 --- a/README.rst +++ b/README.rst @@ -46,6 +46,7 @@ focusing on file size unit conversion, functionality now includes: * Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib** * Automatic human-readable prefix selection (like in `hurry.filesize `_) * Basic arithmetic operations (subtracting 42KiB from 50GiB) +* Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``) * Rich comparison operations (``1024 Bytes == 1KiB``) * Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``) * Rounding via ``math.floor``, ``math.ceil``, and ``round`` diff --git a/bitmath/__init__.py b/bitmath/__init__.py index 3739e1c..26dd181 100644 --- a/bitmath/__init__.py +++ b/bitmath/__init__.py @@ -854,14 +854,40 @@ def __truediv__(self, other): # bm1 / bm2 return self._byte_value / float(other.bytes) - # def __floordiv__(self, other): - # return NotImplemented + def __floordiv__(self, other): + """Floor division: Supported operations with result types: - # def __mod__(self, other): - # return NotImplemented +- bm1 // bm2 = int (whole divisions, unitless — mirrors bm1 / bm2 returning a ratio) +- bm // num = bm (LHS type) +""" + if isinstance(other, numbers.Number): + # bm // num + result = self._byte_value // other + return (type(self))(bytes=result) + else: + # bm1 // bm2 + return int(self._byte_value // other.bytes) - # def __divmod__(self, other): - # return NotImplemented + def __mod__(self, other): + """Modulo (remainder): Supported operations with result types: + +- bm1 % bm2 = bm (LHS type) — remainder after floor-dividing bm1 by bm2 +- bm % num = bm (LHS type) +""" + if isinstance(other, numbers.Number): + # bm % num + result = self._byte_value % other + return (type(self))(bytes=result) + else: + # bm1 % bm2 + return (type(self))(bytes=self._byte_value % other.bytes) + + def __divmod__(self, other): + """divmod(bm, other) == (bm // other, bm % other). + +Result types match __floordiv__ and __mod__. +""" + return (self.__floordiv__(other), self.__mod__(other)) # def __pow__(self, other, modulo=None): # return NotImplemented diff --git a/docsite/source/index.rst b/docsite/source/index.rst index 137cf22..b3678d6 100644 --- a/docsite/source/index.rst +++ b/docsite/source/index.rst @@ -40,6 +40,7 @@ focusing on file size unit conversion, functionality now includes: * Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib** * Automatic human-readable prefix selection (like in `hurry.filesize `_) * Basic arithmetic operations (subtracting 42KiB from 50GiB) +* Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``) * Rich comparison operations (``1024 Bytes == 1KiB``) * Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``) * Rounding via :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round` diff --git a/docsite/source/index.rst.in b/docsite/source/index.rst.in index a465614..89e5c0d 100644 --- a/docsite/source/index.rst.in +++ b/docsite/source/index.rst.in @@ -40,6 +40,7 @@ focusing on file size unit conversion, functionality now includes: * Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib** * Automatic human-readable prefix selection (like in `hurry.filesize `_) * Basic arithmetic operations (subtracting 42KiB from 50GiB) +* Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``) * Rich comparison operations (``1024 Bytes == 1KiB``) * Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``) * Rounding via :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round` diff --git a/docsite/source/simple_examples.rst b/docsite/source/simple_examples.rst index 5a421c9..f9a2eb2 100644 --- a/docsite/source/simple_examples.rst +++ b/docsite/source/simple_examples.rst @@ -76,6 +76,21 @@ Math works mostly like you expect it to, except for a few edge-cases: +----------------+-------------------+---------------------+---------------------------------------+ | Division | ``num`` / ``bm`` | ``type(num)`` | ``3 / KiB(2)`` = ``1.5`` | +----------------+-------------------+---------------------+---------------------------------------+ +| Floor Division | ``bm1`` // ``bm2``| ``type(int)`` | ``GiB(1) // MiB(300)`` = ``3`` | ++----------------+-------------------+---------------------+---------------------------------------+ +| Floor Division | ``bm`` // ``num`` | ``type(bm)`` | ``KiB(6) // 4`` = ``KiB(1.5)``\ :sup:`2` | ++----------------+-------------------+---------------------+---------------------------------------+ +| Modulo | ``bm1`` % ``bm2`` | ``type(bm1)`` | ``GiB(1) % MiB(300)`` = ``GiB(0.12...)``| ++----------------+-------------------+---------------------+---------------------------------------+ +| Modulo | ``bm`` % ``num`` | ``type(bm)`` | ``GiB(1) % 1000`` = ``GiB(2.23e-7)``\ :sup:`2` | ++----------------+-------------------+---------------------+---------------------------------------+ +| divmod | ``divmod(bm1,bm2)``| ``(int, type(bm1))``| ``divmod(GiB(1), MiB(300))`` = ``(3, GiB(0.12...))``| ++----------------+-------------------+---------------------+---------------------------------------+ + +2. For ``//`` and ``%`` with a scalar RHS, the scalar operates on the + internal byte count (consistent with how ``/`` is defined for + ``bm / num``). This is mostly useful for ``bm op bm`` forms; the + ``bm op num`` forms are provided for completeness. Bitwise Operations @@ -123,6 +138,114 @@ bitmath supports all arithmetic operations True +Capacity Math: Floor Division and Modulo +**************************************** + +Floor division (``//``), modulo (``%``), and ``divmod()`` are useful for +capacity-planning problems: *"how many N-sized chunks fit into this +device, and how much is left over?"* + +As with the other arithmetic operators, the **left-hand operand's type +is preserved** in the result. The one exception is ``bm1 // bm2``, which +returns a unitless ``int`` — a count of whole divisions — mirroring how +``bm1 / bm2`` returns a unitless ratio. + +.. code-block:: python + :linenos: + + >>> from bitmath import GiB, MiB + >>> disk = GiB(1) + >>> chunk = MiB(300) + + >>> disk // chunk # how many whole 300 MiB chunks fit in 1 GiB? + 3 + >>> disk % chunk # leftover, typed as the LHS (GiB) + GiB(0.12109375) + >>> divmod(disk, chunk) # both at once + (3, GiB(0.12109375)) + + >>> # Exact fits produce a zero-valued bitmath in the LHS unit: + >>> GiB(1) % MiB(1) + GiB(0.0) + +.. note:: + + With a scalar RHS, the scalar operates on the internal byte count + — consistent with how ``bm / num`` is defined. In practice the + ``bm op bm`` forms above are what you want for capacity math; the + ``bm op num`` forms are available but rarely useful. + +The fundamental identity holds — ``(a // b) * b + (a % b) == a``: + +.. code-block:: python + + >>> q, r = divmod(GiB(1), MiB(300)) + >>> (q * MiB(300)) + r == GiB(1) + True + + +Coercing the Remainder with ``best_prefix`` +=========================================== + +Because ``%`` preserves the LHS type, a small remainder can display +with an awkwardly tiny ``value``. Use ``.best_prefix()`` to re-express +the result in a more human-readable unit without changing its size: + +.. code-block:: python + :linenos: + + >>> leftover = GiB(1) % MiB(300) + >>> leftover + GiB(0.12109375) + >>> leftover.best_prefix() + MiB(124.0) + + >>> # Force a specific unit system: + >>> import bitmath + >>> leftover.best_prefix(system=bitmath.SI) + MB(130.02330...) + + >>> # Or coerce directly to a known unit: + >>> leftover.to_MiB() + MiB(124.0) + + +Formatting Remainders in a ``bitmath.format`` Context +===================================================== + +The ``bitmath.format`` context manager pairs nicely with modulo results +when you want consistent, human-readable display throughout a block of +capacity calculations — including automatic best-prefix coercion: + +.. code-block:: python + :linenos: + + >>> import bitmath + >>> from bitmath import GiB, MiB, TiB + + >>> volume = TiB(1) + >>> block = GiB(7) + >>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True): + ... whole = volume // block + ... leftover = volume % block + ... print(f"{whole} whole blocks of {block} fit in {volume}") + ... print(f"leftover: {leftover}") + 146 whole blocks of 7.00 GiB fit in 1.00 TiB + leftover: 2.00 GiB + + >>> # divmod inside a context manager, reporting in plural form. + >>> # Note: bestprefix on a zero remainder collapses to bits ("b"). + >>> with bitmath.format(fmt_str="{value:.1f} {unit}", plural=True, bestprefix=True): + ... q, r = divmod(GiB(10), MiB(256)) + ... print(f"{q} chunks, {r} remaining") + 40 chunks, 0.0 b remaining + +.. note:: + + ``bitmath.format`` is thread-safe — settings are thread-local, so + concurrent contexts in different threads do not interfere. + + Unit Conversion *************** diff --git a/python-bitmath.spec b/python-bitmath.spec index 46de4d4..f2bfb2a 100644 --- a/python-bitmath.spec +++ b/python-bitmath.spec @@ -17,9 +17,11 @@ bitmath simplifies many facets of interacting with file sizes in various units. Examples include: converting between SI and NIST prefix units (GiB to kB), converting between units of the same type (SI to SI, or NIST to NIST), basic arithmetic operations (subtracting 42KiB -from 50GiB), and rich comparison operations (1024 Bytes == 1KiB), -bit-wise operations, sorting, automatic best human-readable prefix -selection, and completely customizable formatting. +from 50GiB), capacity math with floor division, modulo, and divmod +(GiB(1) // MiB(300), GiB(1) % MiB(300)), rich comparison operations +(1024 Bytes == 1KiB), bit-wise operations, sorting, automatic best +human-readable prefix selection, and completely customizable +formatting. In addition to the conversion and math operations, bitmath provides human readable representations of values which are suitable for use in @@ -40,9 +42,11 @@ bitmath simplifies many facets of interacting with file sizes in various units. Examples include: converting between SI and NIST prefix units (GiB to kB), converting between units of the same type (SI to SI, or NIST to NIST), basic arithmetic operations (subtracting 42KiB -from 50GiB), and rich comparison operations (1024 Bytes == 1KiB), -bit-wise operations, sorting, automatic best human-readable prefix -selection, and completely customizable formatting. +from 50GiB), capacity math with floor division, modulo, and divmod +(GiB(1) // MiB(300), GiB(1) % MiB(300)), rich comparison operations +(1024 Bytes == 1KiB), bit-wise operations, sorting, automatic best +human-readable prefix selection, and completely customizable +formatting. In addition to the conversion and math operations, bitmath provides human readable representations of values which are suitable for use in diff --git a/tests/test_future_math.py b/tests/test_future_math.py index 3ba1ba6..3d4f4b7 100644 --- a/tests/test_future_math.py +++ b/tests/test_future_math.py @@ -67,3 +67,68 @@ def test_number_truediv_bitmath_is_number(self): result = bm1.__rtruediv__(num1) self.assertEqual(result, 2.0) self.assertIs(type(result), float) + + # -- Floor division ------------------------------------------------ + + def test_bitmath_floordiv_bitmath_is_int(self): + """floordiv: bitmath // bitmath = int (whole divisions)""" + result = bitmath.GiB(1) // bitmath.MiB(300) + self.assertEqual(result, 3) + self.assertIs(type(result), int) + + def test_bitmath_floordiv_bitmath_exact_fit(self): + """floordiv: exact multiple yields exact int""" + result = bitmath.GiB(1) // bitmath.MiB(1) + self.assertEqual(result, 1024) + self.assertIs(type(result), int) + + def test_bitmath_floordiv_number_preserves_lhs_type(self): + """floordiv: bitmath // num returns LHS type""" + result = bitmath.KiB(6) // 4 + self.assertIs(type(result), bitmath.KiB) + # 6 KiB = 6144 bytes; 6144 // 4 = 1536 bytes = 1.5 KiB + self.assertEqual(result, bitmath.KiB(1.5)) + + # -- Modulo -------------------------------------------------------- + + def test_bitmath_mod_bitmath_preserves_lhs_type(self): + """mod: bitmath % bitmath returns LHS type""" + result = bitmath.GiB(1) % bitmath.MiB(300) + self.assertIs(type(result), bitmath.GiB) + # 1 GiB = 1073741824 bytes; 1073741824 % (300*1048576) = 130023424 bytes + self.assertEqual(result.bytes, 130023424.0) + + def test_bitmath_mod_bitmath_exact_fit_is_zero(self): + """mod: exact multiple yields zero in LHS unit""" + result = bitmath.GiB(1) % bitmath.MiB(1) + self.assertIs(type(result), bitmath.GiB) + self.assertEqual(result.bytes, 0) + + def test_bitmath_mod_number_preserves_lhs_type(self): + """mod: bitmath % num returns LHS type""" + result = bitmath.KiB(5) % 1024 + self.assertIs(type(result), bitmath.KiB) + + def test_mod_roundtrip_identity(self): + """(a // b) * b + (a % b) == a""" + a = bitmath.GiB(1) + b = bitmath.MiB(300) + q = a // b + r = a % b + self.assertEqual((q * b) + r, a) + + # -- divmod -------------------------------------------------------- + + def test_bitmath_divmod_bitmath(self): + """divmod(bitmath, bitmath) = (int, bitmath of LHS type)""" + q, r = divmod(bitmath.GiB(1), bitmath.MiB(300)) + self.assertEqual(q, 3) + self.assertIs(type(q), int) + self.assertIs(type(r), bitmath.GiB) + self.assertEqual(r.bytes, 130023424.0) + + def test_bitmath_divmod_exact_fit(self): + """divmod on exact multiple: remainder is zero""" + q, r = divmod(bitmath.GiB(10), bitmath.MiB(256)) + self.assertEqual(q, 40) + self.assertEqual(r.bytes, 0) From 0ca5628356cde8dbb497dc3bb4f2de5628e37eb7 Mon Sep 17 00:00:00 2001 From: Tim Case Date: Thu, 23 Apr 2026 10:49:03 -0500 Subject: [PATCH 2/5] Docs for div --- README.rst | 47 ++++++++++++++++++ docsite/source/index.rst | 47 ++++++++++++++++++ docsite/source/simple_examples.rst | 76 +++++++++++++++--------------- 3 files changed, 133 insertions(+), 37 deletions(-) diff --git a/README.rst b/README.rst index b015011..27b2ff6 100644 --- a/README.rst +++ b/README.rst @@ -208,6 +208,53 @@ Arithmetic 2457.6 +Capacity Planning +----------------- + +Floor division (``//``), modulo (``%``), and ``divmod()`` are handy for +chunk-and-remainder capacity math. ``bm1 // bm2`` returns an ``int`` +(how many whole chunks fit); ``bm1 % bm2`` returns a ``bitmath`` of the +**left-hand operand's type** (the leftover). + +.. code-block:: python + + >>> from bitmath import GiB, MiB, TiB + >>> disk = GiB(1) + >>> chunk = MiB(300) + + >>> disk // chunk # how many whole 300 MiB chunks fit? + 3 + >>> disk % chunk # leftover, typed as the LHS (GiB) + GiB(0.12109375) + >>> divmod(disk, chunk) # both at once + (3, GiB(0.12109375)) + +Re-express the remainder in a human-readable unit with +``best_prefix()`` (or coerce directly with ``to_MiB()``, etc.): + +.. code-block:: python + + >>> (GiB(1) % MiB(300)).best_prefix() + MiB(124.0) + +Pair with the ``bitmath.format`` context manager for clean reporting +across a block of capacity calculations: + +.. code-block:: python + + >>> import bitmath + >>> volume = TiB(1) + >>> block = GiB(7) + >>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True): + ... whole, leftover = divmod(volume, block) + ... print(f"{whole} whole blocks of {block} fit in {volume}") + ... print(f"leftover: {leftover}") + 146 whole blocks of 7.00 GiB fit in 1.00 TiB + leftover: 2.00 GiB + +The identity ``(a // b) * b + (a % b) == a`` holds, so ``divmod`` round-trips. + + Convert Units ------------- diff --git a/docsite/source/index.rst b/docsite/source/index.rst index b3678d6..82cd57a 100644 --- a/docsite/source/index.rst +++ b/docsite/source/index.rst @@ -153,6 +153,53 @@ Arithmetic 2457.6 +Capacity Planning +----------------- + +Floor division (``//``), modulo (``%``), and ``divmod()`` are handy for +chunk-and-remainder capacity math. ``bm1 // bm2`` returns an ``int`` +(how many whole chunks fit); ``bm1 % bm2`` returns a ``bitmath`` of the +**left-hand operand's type** (the leftover). + +.. code-block:: python + + >>> from bitmath import GiB, MiB, TiB + >>> disk = GiB(1) + >>> chunk = MiB(300) + + >>> disk // chunk # how many whole 300 MiB chunks fit? + 3 + >>> disk % chunk # leftover, typed as the LHS (GiB) + GiB(0.12109375) + >>> divmod(disk, chunk) # both at once + (3, GiB(0.12109375)) + +Re-express the remainder in a human-readable unit with +``best_prefix()`` (or coerce directly with ``to_MiB()``, etc.): + +.. code-block:: python + + >>> (GiB(1) % MiB(300)).best_prefix() + MiB(124.0) + +Pair with the ``bitmath.format`` context manager for clean reporting +across a block of capacity calculations: + +.. code-block:: python + + >>> import bitmath + >>> volume = TiB(1) + >>> block = GiB(7) + >>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True): + ... whole, leftover = divmod(volume, block) + ... print(f"{whole} whole blocks of {block} fit in {volume}") + ... print(f"leftover: {leftover}") + 146 whole blocks of 7.00 GiB fit in 1.00 TiB + leftover: 2.00 GiB + +The identity ``(a // b) * b + (a % b) == a`` holds, so ``divmod`` round-trips. + + Convert Units ------------- diff --git a/docsite/source/simple_examples.rst b/docsite/source/simple_examples.rst index f9a2eb2..b030488 100644 --- a/docsite/source/simple_examples.rst +++ b/docsite/source/simple_examples.rst @@ -49,43 +49,43 @@ Math works mostly like you expect it to, except for a few edge-cases: .. _simple_examples_arithmetic_table: -+----------------+-------------------+---------------------+---------------------------------------+ -| Operation | Parameters | Result Type | Example | -+================+===================+=====================+=======================================+ -| Addition | ``bm1`` + ``bm2`` | ``type(bm1)`` | ``KiB(1) + MiB(2)`` = ``2049.0KiB`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Addition | ``bm`` + ``num`` | ``type(num)`` | ``KiB(1) + 1`` = ``2.0`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Addition | ``num`` + ``bm`` | ``type(num)`` | ``1 + KiB(1)`` = ``2.0`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Subtraction | ``bm1`` - ``bm2`` | ``type(bm1)`` | ``KiB(1) - Byte(2048)`` = ``-1.0KiB`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Subtraction | ``bm`` - ``num`` | ``type(num)`` | ``KiB(4) - 1`` = ``3.0`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Subtraction | ``num`` - ``bm`` | ``type(num)`` | ``10 - KiB(1)`` = ``9.0`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Multiplication | ``bm1`` * ``bm2`` | ``type(bm1)`` | ``KiB(1) * KiB(2)`` = ``2048.0KiB`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Multiplication | ``bm`` * ``num`` | ``type(bm)`` | ``KiB(2) * 3`` = ``6.0KiB`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Multiplication | ``num`` * ``bm`` | ``type(bm)`` | ``2 * KiB(3)`` = ``6.0KiB`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Division | ``bm1`` / ``bm2`` | ``type(num)`` | ``KiB(1) / KiB(2)`` = ``0.5`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Division | ``bm`` / ``num`` | ``type(bm)`` | ``KiB(6) / 4`` = ``KiB(1.5)`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Division | ``num`` / ``bm`` | ``type(num)`` | ``3 / KiB(2)`` = ``1.5`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Floor Division | ``bm1`` // ``bm2``| ``type(int)`` | ``GiB(1) // MiB(300)`` = ``3`` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Floor Division | ``bm`` // ``num`` | ``type(bm)`` | ``KiB(6) // 4`` = ``KiB(1.5)``\ :sup:`2` | -+----------------+-------------------+---------------------+---------------------------------------+ -| Modulo | ``bm1`` % ``bm2`` | ``type(bm1)`` | ``GiB(1) % MiB(300)`` = ``GiB(0.12...)``| -+----------------+-------------------+---------------------+---------------------------------------+ -| Modulo | ``bm`` % ``num`` | ``type(bm)`` | ``GiB(1) % 1000`` = ``GiB(2.23e-7)``\ :sup:`2` | -+----------------+-------------------+---------------------+---------------------------------------+ -| divmod | ``divmod(bm1,bm2)``| ``(int, type(bm1))``| ``divmod(GiB(1), MiB(300))`` = ``(3, GiB(0.12...))``| -+----------------+-------------------+---------------------+---------------------------------------+ ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Operation | Parameters | Result Type | Example | ++================+======================+======================+======================================================+ +| Addition | ``bm1`` + ``bm2`` | ``type(bm1)`` | ``KiB(1) + MiB(2)`` = ``2049.0KiB`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Addition | ``bm`` + ``num`` | ``type(num)`` | ``KiB(1) + 1`` = ``2.0`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Addition | ``num`` + ``bm`` | ``type(num)`` | ``1 + KiB(1)`` = ``2.0`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Subtraction | ``bm1`` - ``bm2`` | ``type(bm1)`` | ``KiB(1) - Byte(2048)`` = ``-1.0KiB`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Subtraction | ``bm`` - ``num`` | ``type(num)`` | ``KiB(4) - 1`` = ``3.0`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Subtraction | ``num`` - ``bm`` | ``type(num)`` | ``10 - KiB(1)`` = ``9.0`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Multiplication | ``bm1`` * ``bm2`` | ``type(bm1)`` | ``KiB(1) * KiB(2)`` = ``2048.0KiB`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Multiplication | ``bm`` * ``num`` | ``type(bm)`` | ``KiB(2) * 3`` = ``6.0KiB`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Multiplication | ``num`` * ``bm`` | ``type(bm)`` | ``2 * KiB(3)`` = ``6.0KiB`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Division | ``bm1`` / ``bm2`` | ``type(num)`` | ``KiB(1) / KiB(2)`` = ``0.5`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Division | ``bm`` / ``num`` | ``type(bm)`` | ``KiB(6) / 4`` = ``KiB(1.5)`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Division | ``num`` / ``bm`` | ``type(num)`` | ``3 / KiB(2)`` = ``1.5`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Floor Division | ``bm1`` // ``bm2`` | ``int`` | ``GiB(1) // MiB(300)`` = ``3`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Floor Division | ``bm`` // ``num`` | ``type(bm)`` | ``KiB(6) // 4`` = ``KiB(1.5)``\ :sup:`2` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Modulo | ``bm1`` % ``bm2`` | ``type(bm1)`` | ``GiB(1) % MiB(300)`` = ``GiB(0.12...)`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| Modulo | ``bm`` % ``num`` | ``type(bm)`` | ``GiB(1) % 1000`` = ``GiB(2.23e-7)``\ :sup:`2` | ++----------------+----------------------+----------------------+------------------------------------------------------+ +| divmod | ``divmod(bm1, bm2)`` | ``(int, type(bm1))`` | ``divmod(GiB(1), MiB(300))`` = ``(3, GiB(0.12...))`` | ++----------------+----------------------+----------------------+------------------------------------------------------+ 2. For ``//`` and ``%`` with a scalar RHS, the scalar operates on the internal byte count (consistent with how ``/`` is defined for @@ -138,6 +138,8 @@ bitmath supports all arithmetic operations True +.. _capacity_math: + Capacity Math: Floor Division and Modulo **************************************** From 98e77b220d3eaa875197e19774cde54023a247e2 Mon Sep 17 00:00:00 2001 From: Tim Case Date: Thu, 23 Apr 2026 10:56:16 -0500 Subject: [PATCH 3/5] Last doc/comment tweaks --- CLAUDE.md | 8 ++++---- bitmath/__init__.py | 3 --- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9b39122..e7fbe6b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -**bitmath** is a pure-Python library (no external runtime dependencies) for representing and converting file sizes across SI (decimal) and NIST (binary) unit systems. It supports arithmetic, rich comparisons, bitwise ops, parsing, formatting, and f-string/format() support. +**bitmath** is a pure-Python library (no external runtime dependencies) for representing and converting file sizes across SI (decimal) and NIST (binary) unit systems. It supports arithmetic (including floor division, modulo, and `divmod` for capacity math), rich comparisons, bitwise ops, parsing, formatting, and f-string/format() support. ## Project Direction bitmath has been around for almost 12 years, and over that lifetime it promised to deliver backwards compatibility. It delivered on that promise and gathered a strong supporting of people and eventual "critical infrastructure" project status on the PyPI.org website. @@ -20,7 +20,7 @@ Phases 1 (maintenance 1.4.0) and 2 (bitmath 2.0.0) are complete. The project: - Supports **Python 3.9 and newer only** (`requires-python = ">=3.9"` in `pyproject.toml`) - Uses `hatchling` as the build backend (replaces `setup.py`) -- Uses `pytest` as the test runner (292 tests, 99% coverage — one branch in `system` property intentionally uncovered) +- Uses `pytest` as the test runner (303 tests). Coverage is high but platform-sensitive: the `query_device_capacity` branches for the *other* OS are naturally uncovered on any single run. - Is published on PyPI as version 2.0.0 - Drop-in compatible with the 1.x public API @@ -61,7 +61,7 @@ All unit values are normalized to bits internally; conversion between units happ - `getsize(path, ...)` — file size with automatic prefix selection - `listdir(search_base, ...)` — recursive directory listing with sizes - `parse_string(s)` / `parse_string_unsafe(s, system=SI)` — string → bitmath object -- `query_device_capacity(device_fd)` — POSIX device capacity (Linux/macOS) +- `query_device_capacity(device_fd)` — block device capacity on Linux (POSIX/`fcntl`) and Windows (`DeviceIoControl`); raises `NotImplementedError` on unsupported platforms (macOS lacks a usable ioctl under SIP) **Constants:** `NIST`, `SI`, `NIST_PREFIXES`, `SI_PREFIXES`, `ALL_UNIT_TYPES` @@ -70,5 +70,5 @@ All unit values are normalized to bits internally; conversion between units happ - Test runner: `pytest` - All tests are in `tests/` as `test_*.py` files - Test case names must be unique across the suite — enforced by `tests/test_unique_testcase_names.sh` -- Coverage: 99% (one branch in `system` property intentionally uncovered) +- Coverage is platform-sensitive: Windows and POSIX `query_device_capacity` paths only run on their respective OS - `unittest.mock` (stdlib) is used for patching in integration tests diff --git a/bitmath/__init__.py b/bitmath/__init__.py index 26dd181..d37bb64 100644 --- a/bitmath/__init__.py +++ b/bitmath/__init__.py @@ -889,9 +889,6 @@ def __divmod__(self, other): """ return (self.__floordiv__(other), self.__mod__(other)) - # def __pow__(self, other, modulo=None): - # return NotImplemented - ################################################################## """These methods are called to implement the binary arithmetic From c3625a70ad4ff7321baca6b8ee98eec89b68a551 Mon Sep 17 00:00:00 2001 From: Tim Case Date: Thu, 23 Apr 2026 11:08:17 -0500 Subject: [PATCH 4/5] News. --- NEWS.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index ea68be2..791519f 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -142,6 +142,20 @@ still works exactly the same way. What 2.0.0 adds on top of that: NIST is the tiebreaker. Closes `issue #54 `_. +**Floor division, modulo, and divmod for capacity math** + bitmath objects now implement ``__floordiv__`` (``//``), + ``__mod__`` (``%``), and ``__divmod__`` — useful for + chunk-and-remainder capacity planning (*"how many N-sized chunks + fit into this device, and how much is left over?"*). + ``bm1 // bm2`` returns an ``int`` (count of whole divisions), + mirroring how ``bm1 / bm2`` returns a unitless ratio. + ``bm1 % bm2`` and ``divmod(bm1, bm2)`` return remainders as + bitmath objects of the **left-hand operand's type**, consistent + with every other bitmath arithmetic operator. The identity + ``(a // b) * b + (a % b) == a`` holds. See :ref:`capacity_math` + for worked examples including ``best_prefix()`` coercion and + ``bitmath.format`` context-manager integration. + Project Infrastructure ====================== @@ -149,6 +163,11 @@ Project Infrastructure The project infrastructure has been rebuilt to reflect how Python projects are actually maintained in 2026: +**Project Security** + GitHub now has branch protection enabled. Releases are signed with + the maintainers `GPG key + `_. + **Packaging** ``pyproject.toml`` with a hatchling backend replaces the old ``setup.py``/``setup.py.in`` template system. The package is From e4da90d6ab8d5c0687c7a452b267002f2397d854 Mon Sep 17 00:00:00 2001 From: Tim Case Date: Thu, 23 Apr 2026 11:16:33 -0500 Subject: [PATCH 5/5] Bump actions to clean up deprecation notice --- .github/workflows/python.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index acb9082..be097ae 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -18,7 +18,7 @@ jobs: uses: actions/checkout@v6.0.2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 + uses: actions/setup-python@v6.2.0 with: python-version: ${{ matrix.python-version }} cache: 'pip'