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' diff --git a/CLAUDE.md b/CLAUDE.md index ca9b42f..6830ee3 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 @@ -71,5 +71,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/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 diff --git a/README.rst b/README.rst index dda92b3..27b2ff6 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`` @@ -207,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/bitmath/__init__.py b/bitmath/__init__.py index 6fe98da..b36161e 100644 --- a/bitmath/__init__.py +++ b/bitmath/__init__.py @@ -858,17 +858,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: - # def __pow__(self, other, modulo=None): - # return NotImplemented +- 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)) ################################################################## diff --git a/docsite/source/index.rst b/docsite/source/index.rst index 137cf22..82cd57a 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` @@ -152,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/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..b030488 100644 --- a/docsite/source/simple_examples.rst +++ b/docsite/source/simple_examples.rst @@ -49,33 +49,48 @@ 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`` | -+----------------+-------------------+---------------------+---------------------------------------+ ++----------------+----------------------+----------------------+------------------------------------------------------+ +| 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 + ``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,116 @@ bitmath supports all arithmetic operations True +.. _capacity_math: + +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)