Skip to content

Commit 185d48f

Browse files
authored
Merge pull request #128 from timlnx/datamodel-customization
Support for modulo and remainders, capacity planning, etc. Closes #127
2 parents ad480b6 + e4da90d commit 185d48f

10 files changed

Lines changed: 378 additions & 45 deletions

File tree

.github/workflows/python.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
uses: actions/checkout@v6.0.2
1919

2020
- name: Set up Python ${{ matrix.python-version }}
21-
uses: actions/setup-python@v5
21+
uses: actions/setup-python@v6.2.0
2222
with:
2323
python-version: ${{ matrix.python-version }}
2424
cache: 'pip'

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
55

66
## Project Overview
77

8-
**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.
8+
**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.
99

1010
## Project Direction
1111
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:
2020

2121
- Supports **Python 3.9 and newer only** (`requires-python = ">=3.9"` in `pyproject.toml`)
2222
- Uses `hatchling` as the build backend (replaces `setup.py`)
23-
- Uses `pytest` as the test runner (292 tests, 99% coverage — one branch in `system` property intentionally uncovered)
23+
- 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.
2424
- Is published on PyPI as version 2.0.0
2525
- Drop-in compatible with the 1.x public API
2626

@@ -71,5 +71,5 @@ All unit values are normalized to bits internally; conversion between units happ
7171
- Test runner: `pytest`
7272
- All tests are in `tests/` as `test_*.py` files
7373
- Test case names must be unique across the suite — enforced by `tests/test_unique_testcase_names.sh`
74-
- Coverage: 99% (one branch in `system` property intentionally uncovered)
74+
- Coverage is platform-sensitive: Windows and POSIX `query_device_capacity` paths only run on their respective OS
7575
- `unittest.mock` (stdlib) is used for patching in integration tests

NEWS.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,13 +142,32 @@ still works exactly the same way. What 2.0.0 adds on top of that:
142142
NIST is the tiebreaker. Closes `issue #54
143143
<https://github.com/timlnx/bitmath/issues/54>`_.
144144

145+
**Floor division, modulo, and divmod for capacity math**
146+
bitmath objects now implement ``__floordiv__`` (``//``),
147+
``__mod__`` (``%``), and ``__divmod__`` — useful for
148+
chunk-and-remainder capacity planning (*"how many N-sized chunks
149+
fit into this device, and how much is left over?"*).
150+
``bm1 // bm2`` returns an ``int`` (count of whole divisions),
151+
mirroring how ``bm1 / bm2`` returns a unitless ratio.
152+
``bm1 % bm2`` and ``divmod(bm1, bm2)`` return remainders as
153+
bitmath objects of the **left-hand operand's type**, consistent
154+
with every other bitmath arithmetic operator. The identity
155+
``(a // b) * b + (a % b) == a`` holds. See :ref:`capacity_math`
156+
for worked examples including ``best_prefix()`` coercion and
157+
``bitmath.format`` context-manager integration.
158+
145159

146160
Project Infrastructure
147161
======================
148162

149163
The project infrastructure has been rebuilt to reflect how Python
150164
projects are actually maintained in 2026:
151165

166+
**Project Security**
167+
GitHub now has branch protection enabled. Releases are signed with
168+
the maintainers `GPG key
169+
<https://keys.openpgp.org/search?q=bitmath%40lnx.cx>`_.
170+
152171
**Packaging**
153172
``pyproject.toml`` with a hatchling backend replaces the old
154173
``setup.py``/``setup.py.in`` template system. The package is

README.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ focusing on file size unit conversion, functionality now includes:
4646
* Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib**
4747
* Automatic human-readable prefix selection (like in `hurry.filesize <https://pypi.python.org/pypi/hurry.filesize>`_)
4848
* Basic arithmetic operations (subtracting 42KiB from 50GiB)
49+
* Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``)
4950
* Rich comparison operations (``1024 Bytes == 1KiB``)
5051
* Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``)
5152
* Rounding via ``math.floor``, ``math.ceil``, and ``round``
@@ -207,6 +208,53 @@ Arithmetic
207208
2457.6
208209
209210
211+
Capacity Planning
212+
-----------------
213+
214+
Floor division (``//``), modulo (``%``), and ``divmod()`` are handy for
215+
chunk-and-remainder capacity math. ``bm1 // bm2`` returns an ``int``
216+
(how many whole chunks fit); ``bm1 % bm2`` returns a ``bitmath`` of the
217+
**left-hand operand's type** (the leftover).
218+
219+
.. code-block:: python
220+
221+
>>> from bitmath import GiB, MiB, TiB
222+
>>> disk = GiB(1)
223+
>>> chunk = MiB(300)
224+
225+
>>> disk // chunk # how many whole 300 MiB chunks fit?
226+
3
227+
>>> disk % chunk # leftover, typed as the LHS (GiB)
228+
GiB(0.12109375)
229+
>>> divmod(disk, chunk) # both at once
230+
(3, GiB(0.12109375))
231+
232+
Re-express the remainder in a human-readable unit with
233+
``best_prefix()`` (or coerce directly with ``to_MiB()``, etc.):
234+
235+
.. code-block:: python
236+
237+
>>> (GiB(1) % MiB(300)).best_prefix()
238+
MiB(124.0)
239+
240+
Pair with the ``bitmath.format`` context manager for clean reporting
241+
across a block of capacity calculations:
242+
243+
.. code-block:: python
244+
245+
>>> import bitmath
246+
>>> volume = TiB(1)
247+
>>> block = GiB(7)
248+
>>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True):
249+
... whole, leftover = divmod(volume, block)
250+
... print(f"{whole} whole blocks of {block} fit in {volume}")
251+
... print(f"leftover: {leftover}")
252+
146 whole blocks of 7.00 GiB fit in 1.00 TiB
253+
leftover: 2.00 GiB
254+
255+
The identity ``(a // b) * b + (a % b) == a`` holds, so ``divmod`` round-trips.
256+
257+
210258
Convert Units
211259
-------------
212260

bitmath/__init__.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -858,17 +858,40 @@ def __truediv__(self, other):
858858
# bm1 / bm2
859859
return self._byte_value / float(other.bytes)
860860

861-
# def __floordiv__(self, other):
862-
# return NotImplemented
861+
def __floordiv__(self, other):
862+
"""Floor division: Supported operations with result types:
863863
864-
# def __mod__(self, other):
865-
# return NotImplemented
864+
- bm1 // bm2 = int (whole divisions, unitless — mirrors bm1 / bm2 returning a ratio)
865+
- bm // num = bm (LHS type)
866+
"""
867+
if isinstance(other, numbers.Number):
868+
# bm // num
869+
result = self._byte_value // other
870+
return (type(self))(bytes=result)
871+
else:
872+
# bm1 // bm2
873+
return int(self._byte_value // other.bytes)
866874

867-
# def __divmod__(self, other):
868-
# return NotImplemented
875+
def __mod__(self, other):
876+
"""Modulo (remainder): Supported operations with result types:
869877
870-
# def __pow__(self, other, modulo=None):
871-
# return NotImplemented
878+
- bm1 % bm2 = bm (LHS type) — remainder after floor-dividing bm1 by bm2
879+
- bm % num = bm (LHS type)
880+
"""
881+
if isinstance(other, numbers.Number):
882+
# bm % num
883+
result = self._byte_value % other
884+
return (type(self))(bytes=result)
885+
else:
886+
# bm1 % bm2
887+
return (type(self))(bytes=self._byte_value % other.bytes)
888+
889+
def __divmod__(self, other):
890+
"""divmod(bm, other) == (bm // other, bm % other).
891+
892+
Result types match __floordiv__ and __mod__.
893+
"""
894+
return (self.__floordiv__(other), self.__mod__(other))
872895

873896
##################################################################
874897

docsite/source/index.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ focusing on file size unit conversion, functionality now includes:
4040
* Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib**
4141
* Automatic human-readable prefix selection (like in `hurry.filesize <https://pypi.python.org/pypi/hurry.filesize>`_)
4242
* Basic arithmetic operations (subtracting 42KiB from 50GiB)
43+
* Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``)
4344
* Rich comparison operations (``1024 Bytes == 1KiB``)
4445
* Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``)
4546
* Rounding via :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round`
@@ -152,6 +153,53 @@ Arithmetic
152153
2457.6
153154
154155
156+
Capacity Planning
157+
-----------------
158+
159+
Floor division (``//``), modulo (``%``), and ``divmod()`` are handy for
160+
chunk-and-remainder capacity math. ``bm1 // bm2`` returns an ``int``
161+
(how many whole chunks fit); ``bm1 % bm2`` returns a ``bitmath`` of the
162+
**left-hand operand's type** (the leftover).
163+
164+
.. code-block:: python
165+
166+
>>> from bitmath import GiB, MiB, TiB
167+
>>> disk = GiB(1)
168+
>>> chunk = MiB(300)
169+
170+
>>> disk // chunk # how many whole 300 MiB chunks fit?
171+
3
172+
>>> disk % chunk # leftover, typed as the LHS (GiB)
173+
GiB(0.12109375)
174+
>>> divmod(disk, chunk) # both at once
175+
(3, GiB(0.12109375))
176+
177+
Re-express the remainder in a human-readable unit with
178+
``best_prefix()`` (or coerce directly with ``to_MiB()``, etc.):
179+
180+
.. code-block:: python
181+
182+
>>> (GiB(1) % MiB(300)).best_prefix()
183+
MiB(124.0)
184+
185+
Pair with the ``bitmath.format`` context manager for clean reporting
186+
across a block of capacity calculations:
187+
188+
.. code-block:: python
189+
190+
>>> import bitmath
191+
>>> volume = TiB(1)
192+
>>> block = GiB(7)
193+
>>> with bitmath.format(fmt_str="{value:.2f} {unit}", bestprefix=True):
194+
... whole, leftover = divmod(volume, block)
195+
... print(f"{whole} whole blocks of {block} fit in {volume}")
196+
... print(f"leftover: {leftover}")
197+
146 whole blocks of 7.00 GiB fit in 1.00 TiB
198+
leftover: 2.00 GiB
199+
200+
The identity ``(a // b) * b + (a % b) == a`` holds, so ``divmod`` round-trips.
201+
202+
155203
Convert Units
156204
-------------
157205

docsite/source/index.rst.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ focusing on file size unit conversion, functionality now includes:
4040
* Full NIST unit coverage including **ZiB**, **YiB**, **Zib**, and **Yib**
4141
* Automatic human-readable prefix selection (like in `hurry.filesize <https://pypi.python.org/pypi/hurry.filesize>`_)
4242
* Basic arithmetic operations (subtracting 42KiB from 50GiB)
43+
* Capacity math with floor division, modulo, and ``divmod`` (``GiB(1) // MiB(300)``, ``GiB(1) % MiB(300)``)
4344
* Rich comparison operations (``1024 Bytes == 1KiB``)
4445
* Bitwise operations (``<<``, ``>>``, ``&``, ``|``, ``^``)
4546
* Rounding via :py:func:`math.floor`, :py:func:`math.ceil`, and :py:func:`round`

0 commit comments

Comments
 (0)