mobius-number 0.1.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,13 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ *.egg-info/
5
+ dist/
6
+ build/
7
+ .eggs/
8
+ *.egg
9
+ .pytest_cache/
10
+ .mypy_cache/
11
+ .tox/
12
+ .venv/
13
+ venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jay Carpenter
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 all
13
+ 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 THE
21
+ SOFTWARE.
@@ -0,0 +1,202 @@
1
+ Metadata-Version: 2.4
2
+ Name: mobius-number
3
+ Version: 0.1.0
4
+ Summary: Complementary residue arithmetic — 0.1 + 0.2 = 0.3, exactly.
5
+ Project-URL: Homepage, https://github.com/JustNothingJay/mobius-number
6
+ Project-URL: Repository, https://github.com/JustNothingJay/mobius-number
7
+ Project-URL: Issues, https://github.com/JustNothingJay/mobius-number/issues
8
+ Author: Jay Carpenter
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: IEEE-754,arithmetic,complementary-residue,exact,floating-point,mobius,precision,rational
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: Financial and Insurance Industry
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
24
+ Classifier: Topic :: Software Development :: Libraries
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+
29
+ # MöbiusNumber
30
+
31
+ **0.1 + 0.2 = 0.3. Exactly.**
32
+
33
+ ```python
34
+ from mobius_number import M
35
+
36
+ >>> 0.1 + 0.2 == 0.3
37
+ False
38
+
39
+ >>> M("0.1") + M("0.2") == M("0.3")
40
+ True
41
+ ```
42
+
43
+ A number type that carries its own correction. No rounding error. No epsilon comparisons. No workarounds.
44
+
45
+ ## Install
46
+
47
+ ```
48
+ pip install mobius-number
49
+ ```
50
+
51
+ ## The Problem
52
+
53
+ Every computer in the world gets this wrong:
54
+
55
+ ```python
56
+ >>> 0.1 + 0.2
57
+ 0.30000000000000004
58
+
59
+ >>> 0.1 + 0.2 == 0.3
60
+ False
61
+ ```
62
+
63
+ This has been true since 1985 when IEEE 754 was published. The reason: computers store numbers in base 2. The number 0.1 in binary is a repeating fraction — like 1/3 in decimal (0.333...). It gets rounded. Then 0.2 gets rounded. The rounding errors stack.
64
+
65
+ Every workaround — arbitrary precision, interval arithmetic, posit numbers — tries to fix the math layer while leaving the foundation untouched: the number is stored in the form the transistor speaks, and the human adapts.
66
+
67
+ **What if the representation served the number instead?**
68
+
69
+ ## The Idea
70
+
71
+ DNA has two strands. Every base carries its complement — A pairs with T, G pairs with C. There is no excess. The complement consumes what the original doesn't cover.
72
+
73
+ A Möbius strip has one surface. Traversing the full loop covers both "sides" and returns to the origin.
74
+
75
+ A MöbiusNumber stores two strands:
76
+
77
+ - **The binary strand** — `float64`, hardware-fast, carries rounding error
78
+ - **The rational strand** — exact `Fraction`, no loss, no repeating
79
+
80
+ They are not two separate representations. They are one object. The binary strand is the shadow. The rational strand is the substance. **On collapse, the rational governs.**
81
+
82
+ ```
83
+ CURRENT (IEEE 754):
84
+ Store 0.1 → 0.1000000000000000055511...
85
+ Residue: 0.0000000000000000055511... ← THROWN AWAY
86
+
87
+ MöbiusNumber:
88
+ Binary strand: 0.1000000000000000055511...
89
+ Rational strand: 1/10
90
+ Anti (residue): exact_value − binary_value
91
+ Collapse: the rational governs → 0.1 exactly
92
+ ```
93
+
94
+ Arithmetic propagates both strands. When you add two MöbiusNumbers, the rationals add exactly. The floats add approximately. **The error exists but is never consulted for truth.** The anti-strand is always present, always correct, and annihilates the rounding error on collapse.
95
+
96
+ ## Proof
97
+
98
+ ```
99
+ $ python -c "from mobius_number import M; print(M('0.1') + M('0.2') == M('0.3'))"
100
+ True
101
+ ```
102
+
103
+ ### The Famous Failures — All Fixed
104
+
105
+ | Test | IEEE 754 | MöbiusNumber |
106
+ |------|----------|--------------|
107
+ | `0.1 + 0.2 == 0.3` | **False** | **True** |
108
+ | `(1/49) * 49 == 1` | **False** | **True** |
109
+ | `(1 + 1e-16) - 1` | **0.0** (total loss) | **1e-16** (exact) |
110
+ | `0.1 * 10 == 1.0` | True¹ | True |
111
+ | `$10 / 3 * 3 == $10` | True¹ | True |
112
+ | `0.01 * 100 == 1.0` | True¹ | True |
113
+ | `(1/7) * 7 == 1` | **False** | **True** |
114
+ | `0.001 added 1000×` | **False** | **True** |
115
+
116
+ ¹ IEEE 754 gets these by luck — the rounding errors happen to cancel. The MöbiusNumber gets them by construction.
117
+
118
+ ### Strand Anatomy
119
+
120
+ ```python
121
+ from mobius_number import M
122
+
123
+ n = M("0.1")
124
+ print(n.diagnose())
125
+ # {
126
+ # 'binary_strand': 0.1,
127
+ # 'rational_strand': '1/10',
128
+ # 'residue': '-1/180143985094819840',
129
+ # 'residue_float': -5.55e-18,
130
+ # 'collapsed': 0.1
131
+ # }
132
+ ```
133
+
134
+ The residue is the anti-strand — the exact complement of the binary error. It exists. It is never discarded. On collapse, the Möbius strip closes.
135
+
136
+ ## Usage
137
+
138
+ ```python
139
+ from mobius_number import M
140
+
141
+ # Basic arithmetic
142
+ a = M("0.1")
143
+ b = M("0.2")
144
+ c = a + b # M('3/10')
145
+ c.collapse() # 0.3
146
+
147
+ # Financial
148
+ price = M("19.99")
149
+ tax = price * M("0.0825")
150
+ total = price + tax # Exact
151
+
152
+ # Comparison — the rational governs
153
+ M("0.1") + M("0.2") == M("0.3") # True — always
154
+
155
+ # Interop with plain numbers
156
+ M("0.5") + 1 # M('3/2')
157
+ 3 * M("0.1") # M('3/10')
158
+
159
+ # Inspect the strands
160
+ n = M("0.1")
161
+ n.approx # 0.1 (the float — fast, lossy)
162
+ n.exact # Fraction(1, 10) (the truth)
163
+ n.residue # Fraction(-1, 180143985094819840)
164
+ ```
165
+
166
+ ## Why Not Just Use `Fraction`?
167
+
168
+ You can. Python's `fractions.Fraction` gives exact rational arithmetic. But:
169
+
170
+ 1. **Speed** — MöbiusNumber carries a float for fast approximate work. Use `.approx` in hot loops, `.collapse()` when you need truth.
171
+ 2. **Drop-in intent** — `M("0.1")` reads like a number. `Fraction("0.1")` reads like a workaround.
172
+ 3. **The conceptual point** — the number and its correction are one object. The anti-strand is not a separate operation. It is intrinsic. Like DNA. Like a Möbius strip.
173
+
174
+ ## How It Works
175
+
176
+ Every MöbiusNumber is internally:
177
+
178
+ ```
179
+ (_approx: float, _exact: Fraction)
180
+ ```
181
+
182
+ - **Construction from string**: `M("0.1")` → `_exact = Fraction("0.1") = 1/10`; `_approx = float(1/10)`
183
+ - **Construction from float**: `M(0.1)` → recovers the rational intent via `limit_denominator`
184
+ - **Arithmetic**: both strands propagate independently through `+`, `-`, `*`, `/`, `**`
185
+ - **Comparison**: always uses `_exact` — the rational strand governs all equality and ordering
186
+ - **Collapse**: `float(_exact)` — the Möbius traversal returns the exact value
187
+
188
+ No external dependencies. Pure Python. Works on 3.9+.
189
+
190
+ ## The Name
191
+
192
+ A Möbius strip is a surface with one side. If you trace a line along it, you cover both "sides" and return to the origin having traversed the whole thing. There is no front and back — only one continuous surface.
193
+
194
+ A MöbiusNumber is a number with one identity. The binary approximation and the exact rational are not two things — they are one object that, when fully traversed, resolves to the truth. The representation IS the correction.
195
+
196
+ ## Author
197
+
198
+ Jay Carpenter — [SECS Research](https://github.com/JustNothingJay/SECS_Research)
199
+
200
+ ## License
201
+
202
+ MIT
@@ -0,0 +1,174 @@
1
+ # MöbiusNumber
2
+
3
+ **0.1 + 0.2 = 0.3. Exactly.**
4
+
5
+ ```python
6
+ from mobius_number import M
7
+
8
+ >>> 0.1 + 0.2 == 0.3
9
+ False
10
+
11
+ >>> M("0.1") + M("0.2") == M("0.3")
12
+ True
13
+ ```
14
+
15
+ A number type that carries its own correction. No rounding error. No epsilon comparisons. No workarounds.
16
+
17
+ ## Install
18
+
19
+ ```
20
+ pip install mobius-number
21
+ ```
22
+
23
+ ## The Problem
24
+
25
+ Every computer in the world gets this wrong:
26
+
27
+ ```python
28
+ >>> 0.1 + 0.2
29
+ 0.30000000000000004
30
+
31
+ >>> 0.1 + 0.2 == 0.3
32
+ False
33
+ ```
34
+
35
+ This has been true since 1985 when IEEE 754 was published. The reason: computers store numbers in base 2. The number 0.1 in binary is a repeating fraction — like 1/3 in decimal (0.333...). It gets rounded. Then 0.2 gets rounded. The rounding errors stack.
36
+
37
+ Every workaround — arbitrary precision, interval arithmetic, posit numbers — tries to fix the math layer while leaving the foundation untouched: the number is stored in the form the transistor speaks, and the human adapts.
38
+
39
+ **What if the representation served the number instead?**
40
+
41
+ ## The Idea
42
+
43
+ DNA has two strands. Every base carries its complement — A pairs with T, G pairs with C. There is no excess. The complement consumes what the original doesn't cover.
44
+
45
+ A Möbius strip has one surface. Traversing the full loop covers both "sides" and returns to the origin.
46
+
47
+ A MöbiusNumber stores two strands:
48
+
49
+ - **The binary strand** — `float64`, hardware-fast, carries rounding error
50
+ - **The rational strand** — exact `Fraction`, no loss, no repeating
51
+
52
+ They are not two separate representations. They are one object. The binary strand is the shadow. The rational strand is the substance. **On collapse, the rational governs.**
53
+
54
+ ```
55
+ CURRENT (IEEE 754):
56
+ Store 0.1 → 0.1000000000000000055511...
57
+ Residue: 0.0000000000000000055511... ← THROWN AWAY
58
+
59
+ MöbiusNumber:
60
+ Binary strand: 0.1000000000000000055511...
61
+ Rational strand: 1/10
62
+ Anti (residue): exact_value − binary_value
63
+ Collapse: the rational governs → 0.1 exactly
64
+ ```
65
+
66
+ Arithmetic propagates both strands. When you add two MöbiusNumbers, the rationals add exactly. The floats add approximately. **The error exists but is never consulted for truth.** The anti-strand is always present, always correct, and annihilates the rounding error on collapse.
67
+
68
+ ## Proof
69
+
70
+ ```
71
+ $ python -c "from mobius_number import M; print(M('0.1') + M('0.2') == M('0.3'))"
72
+ True
73
+ ```
74
+
75
+ ### The Famous Failures — All Fixed
76
+
77
+ | Test | IEEE 754 | MöbiusNumber |
78
+ |------|----------|--------------|
79
+ | `0.1 + 0.2 == 0.3` | **False** | **True** |
80
+ | `(1/49) * 49 == 1` | **False** | **True** |
81
+ | `(1 + 1e-16) - 1` | **0.0** (total loss) | **1e-16** (exact) |
82
+ | `0.1 * 10 == 1.0` | True¹ | True |
83
+ | `$10 / 3 * 3 == $10` | True¹ | True |
84
+ | `0.01 * 100 == 1.0` | True¹ | True |
85
+ | `(1/7) * 7 == 1` | **False** | **True** |
86
+ | `0.001 added 1000×` | **False** | **True** |
87
+
88
+ ¹ IEEE 754 gets these by luck — the rounding errors happen to cancel. The MöbiusNumber gets them by construction.
89
+
90
+ ### Strand Anatomy
91
+
92
+ ```python
93
+ from mobius_number import M
94
+
95
+ n = M("0.1")
96
+ print(n.diagnose())
97
+ # {
98
+ # 'binary_strand': 0.1,
99
+ # 'rational_strand': '1/10',
100
+ # 'residue': '-1/180143985094819840',
101
+ # 'residue_float': -5.55e-18,
102
+ # 'collapsed': 0.1
103
+ # }
104
+ ```
105
+
106
+ The residue is the anti-strand — the exact complement of the binary error. It exists. It is never discarded. On collapse, the Möbius strip closes.
107
+
108
+ ## Usage
109
+
110
+ ```python
111
+ from mobius_number import M
112
+
113
+ # Basic arithmetic
114
+ a = M("0.1")
115
+ b = M("0.2")
116
+ c = a + b # M('3/10')
117
+ c.collapse() # 0.3
118
+
119
+ # Financial
120
+ price = M("19.99")
121
+ tax = price * M("0.0825")
122
+ total = price + tax # Exact
123
+
124
+ # Comparison — the rational governs
125
+ M("0.1") + M("0.2") == M("0.3") # True — always
126
+
127
+ # Interop with plain numbers
128
+ M("0.5") + 1 # M('3/2')
129
+ 3 * M("0.1") # M('3/10')
130
+
131
+ # Inspect the strands
132
+ n = M("0.1")
133
+ n.approx # 0.1 (the float — fast, lossy)
134
+ n.exact # Fraction(1, 10) (the truth)
135
+ n.residue # Fraction(-1, 180143985094819840)
136
+ ```
137
+
138
+ ## Why Not Just Use `Fraction`?
139
+
140
+ You can. Python's `fractions.Fraction` gives exact rational arithmetic. But:
141
+
142
+ 1. **Speed** — MöbiusNumber carries a float for fast approximate work. Use `.approx` in hot loops, `.collapse()` when you need truth.
143
+ 2. **Drop-in intent** — `M("0.1")` reads like a number. `Fraction("0.1")` reads like a workaround.
144
+ 3. **The conceptual point** — the number and its correction are one object. The anti-strand is not a separate operation. It is intrinsic. Like DNA. Like a Möbius strip.
145
+
146
+ ## How It Works
147
+
148
+ Every MöbiusNumber is internally:
149
+
150
+ ```
151
+ (_approx: float, _exact: Fraction)
152
+ ```
153
+
154
+ - **Construction from string**: `M("0.1")` → `_exact = Fraction("0.1") = 1/10`; `_approx = float(1/10)`
155
+ - **Construction from float**: `M(0.1)` → recovers the rational intent via `limit_denominator`
156
+ - **Arithmetic**: both strands propagate independently through `+`, `-`, `*`, `/`, `**`
157
+ - **Comparison**: always uses `_exact` — the rational strand governs all equality and ordering
158
+ - **Collapse**: `float(_exact)` — the Möbius traversal returns the exact value
159
+
160
+ No external dependencies. Pure Python. Works on 3.9+.
161
+
162
+ ## The Name
163
+
164
+ A Möbius strip is a surface with one side. If you trace a line along it, you cover both "sides" and return to the origin having traversed the whole thing. There is no front and back — only one continuous surface.
165
+
166
+ A MöbiusNumber is a number with one identity. The binary approximation and the exact rational are not two things — they are one object that, when fully traversed, resolves to the truth. The representation IS the correction.
167
+
168
+ ## Author
169
+
170
+ Jay Carpenter — [SECS Research](https://github.com/JustNothingJay/SECS_Research)
171
+
172
+ ## License
173
+
174
+ MIT
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "mobius-number"
7
+ version = "0.1.0"
8
+ description = "Complementary residue arithmetic — 0.1 + 0.2 = 0.3, exactly."
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ requires-python = ">=3.9"
12
+ authors = [
13
+ { name = "Jay Carpenter" },
14
+ ]
15
+ keywords = [
16
+ "floating-point",
17
+ "arithmetic",
18
+ "precision",
19
+ "IEEE-754",
20
+ "exact",
21
+ "rational",
22
+ "mobius",
23
+ "complementary-residue",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "Intended Audience :: Financial and Insurance Industry",
29
+ "Intended Audience :: Science/Research",
30
+ "License :: OSI Approved :: MIT License",
31
+ "Programming Language :: Python :: 3",
32
+ "Programming Language :: Python :: 3.9",
33
+ "Programming Language :: Python :: 3.10",
34
+ "Programming Language :: Python :: 3.11",
35
+ "Programming Language :: Python :: 3.12",
36
+ "Programming Language :: Python :: 3.13",
37
+ "Topic :: Scientific/Engineering :: Mathematics",
38
+ "Topic :: Software Development :: Libraries",
39
+ "Typing :: Typed",
40
+ ]
41
+
42
+ [project.urls]
43
+ Homepage = "https://github.com/JustNothingJay/mobius-number"
44
+ Repository = "https://github.com/JustNothingJay/mobius-number"
45
+ Issues = "https://github.com/JustNothingJay/mobius-number/issues"
46
+
47
+ [tool.hatch.build.targets.wheel]
48
+ packages = ["src/mobius_number"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
@@ -0,0 +1,29 @@
1
+ """
2
+ MöbiusNumber — Complementary Residue Arithmetic
3
+
4
+ Every number is stored as two strands:
5
+ - The binary approximation (float64, hardware-fast)
6
+ - The exact rational identity (numerator/denominator, no loss)
7
+
8
+ They are not two representations. They are one object — like DNA's
9
+ double helix, like a Möbius strip traversed fully. The approximation
10
+ and its anti coexist. On collapse, the rational governs.
11
+
12
+ The float gives you speed.
13
+ The rational gives you truth.
14
+ Collapse gives you both.
15
+
16
+ >>> from mobius_number import M
17
+ >>> M("0.1") + M("0.2") == M("0.3")
18
+ True
19
+
20
+ Jay Carpenter, 2026
21
+ """
22
+
23
+ from mobius_number.core import MobiusNumber
24
+
25
+ # Convenience alias
26
+ M = MobiusNumber
27
+
28
+ __version__ = "0.1.0"
29
+ __all__ = ["MobiusNumber", "M"]
@@ -0,0 +1,223 @@
1
+ """
2
+ MöbiusNumber core implementation.
3
+
4
+ A number that carries its own correction. Two strands — the binary
5
+ approximation and the exact rational identity — coexist as one object.
6
+ On collapse, the rational governs.
7
+
8
+ The structure mirrors DNA: every base carries its complement.
9
+ The topology is Möbius: traversing the full loop returns the exact value.
10
+ """
11
+
12
+ from fractions import Fraction
13
+ from typing import Union
14
+
15
+
16
+ class MobiusNumber:
17
+ """
18
+ A number that carries its own correction.
19
+
20
+ Internally:
21
+ _approx : float — the binary strand (fast, lossy)
22
+ _exact : Fraction — the anti strand (exact, complete)
23
+
24
+ The float is the shadow. The Fraction is the substance.
25
+ Collapse = the Möbius traversal → exact output.
26
+
27
+ Construction:
28
+ MobiusNumber("0.1") — from string (purest: "0.1" means exactly 1/10)
29
+ MobiusNumber(0.1) — from float (recovers rational intent)
30
+ MobiusNumber(1) — from int (exact)
31
+ MobiusNumber(Fraction(1, 10)) — from Fraction (exact)
32
+ """
33
+
34
+ __slots__ = ('_approx', '_exact')
35
+
36
+ def __init__(self, value: Union[int, float, str, Fraction, 'MobiusNumber']):
37
+ if isinstance(value, MobiusNumber):
38
+ self._approx = value._approx
39
+ self._exact = value._exact
40
+ elif isinstance(value, Fraction):
41
+ self._exact = value
42
+ self._approx = float(value)
43
+ elif isinstance(value, int):
44
+ self._exact = Fraction(value)
45
+ self._approx = float(value)
46
+ elif isinstance(value, float):
47
+ self._approx = value
48
+ self._exact = Fraction(value).limit_denominator(10**15)
49
+ elif isinstance(value, str):
50
+ self._exact = Fraction(value)
51
+ self._approx = float(self._exact)
52
+ else:
53
+ raise TypeError(f"Cannot create MobiusNumber from {type(value)}")
54
+
55
+ # ------------------------------------------------------------------
56
+ # Strand access
57
+ # ------------------------------------------------------------------
58
+
59
+ @property
60
+ def approx(self) -> float:
61
+ """The binary strand — hardware-fast, carries rounding error."""
62
+ return self._approx
63
+
64
+ @property
65
+ def exact(self) -> Fraction:
66
+ """The rational strand — exact, no loss."""
67
+ return self._exact
68
+
69
+ @property
70
+ def residue(self) -> Fraction:
71
+ """The anti-strand: exact_value − binary_approximation."""
72
+ return self._exact - Fraction(self._approx)
73
+
74
+ # ------------------------------------------------------------------
75
+ # Collapse — the Möbius traversal
76
+ # ------------------------------------------------------------------
77
+
78
+ def collapse(self) -> float:
79
+ """
80
+ Traverse the full Möbius loop.
81
+ Binary approximation + anti-strand residue → exact value.
82
+ """
83
+ return float(self._exact)
84
+
85
+ # ------------------------------------------------------------------
86
+ # Arithmetic — both strands propagate
87
+ # ------------------------------------------------------------------
88
+
89
+ def __add__(self, other):
90
+ other = _coerce(other)
91
+ result = MobiusNumber.__new__(MobiusNumber)
92
+ result._exact = self._exact + other._exact
93
+ result._approx = self._approx + other._approx
94
+ return result
95
+
96
+ def __radd__(self, other):
97
+ return self.__add__(other)
98
+
99
+ def __sub__(self, other):
100
+ other = _coerce(other)
101
+ result = MobiusNumber.__new__(MobiusNumber)
102
+ result._exact = self._exact - other._exact
103
+ result._approx = self._approx - other._approx
104
+ return result
105
+
106
+ def __rsub__(self, other):
107
+ other = _coerce(other)
108
+ return other.__sub__(self)
109
+
110
+ def __mul__(self, other):
111
+ other = _coerce(other)
112
+ result = MobiusNumber.__new__(MobiusNumber)
113
+ result._exact = self._exact * other._exact
114
+ result._approx = self._approx * other._approx
115
+ return result
116
+
117
+ def __rmul__(self, other):
118
+ return self.__mul__(other)
119
+
120
+ def __truediv__(self, other):
121
+ other = _coerce(other)
122
+ if other._exact == 0:
123
+ raise ZeroDivisionError("MobiusNumber division by zero")
124
+ result = MobiusNumber.__new__(MobiusNumber)
125
+ result._exact = self._exact / other._exact
126
+ result._approx = self._approx / other._approx
127
+ return result
128
+
129
+ def __rtruediv__(self, other):
130
+ other = _coerce(other)
131
+ return other.__truediv__(self)
132
+
133
+ def __neg__(self):
134
+ result = MobiusNumber.__new__(MobiusNumber)
135
+ result._exact = -self._exact
136
+ result._approx = -self._approx
137
+ return result
138
+
139
+ def __abs__(self):
140
+ result = MobiusNumber.__new__(MobiusNumber)
141
+ result._exact = abs(self._exact)
142
+ result._approx = abs(self._approx)
143
+ return result
144
+
145
+ def __pow__(self, exp):
146
+ if isinstance(exp, int):
147
+ result = MobiusNumber.__new__(MobiusNumber)
148
+ result._exact = self._exact ** exp
149
+ result._approx = self._approx ** exp
150
+ return result
151
+ return NotImplemented
152
+
153
+ # ------------------------------------------------------------------
154
+ # Comparison — the rational governs, always
155
+ # ------------------------------------------------------------------
156
+
157
+ def __eq__(self, other):
158
+ if not isinstance(other, (MobiusNumber, int, float, str, Fraction)):
159
+ return NotImplemented
160
+ other = _coerce(other)
161
+ return self._exact == other._exact
162
+
163
+ def __ne__(self, other):
164
+ if not isinstance(other, (MobiusNumber, int, float, str, Fraction)):
165
+ return NotImplemented
166
+ other = _coerce(other)
167
+ return self._exact != other._exact
168
+
169
+ def __lt__(self, other):
170
+ other = _coerce(other)
171
+ return self._exact < other._exact
172
+
173
+ def __le__(self, other):
174
+ other = _coerce(other)
175
+ return self._exact <= other._exact
176
+
177
+ def __gt__(self, other):
178
+ other = _coerce(other)
179
+ return self._exact > other._exact
180
+
181
+ def __ge__(self, other):
182
+ other = _coerce(other)
183
+ return self._exact >= other._exact
184
+
185
+ # ------------------------------------------------------------------
186
+ # Display
187
+ # ------------------------------------------------------------------
188
+
189
+ def __repr__(self):
190
+ return f"M('{self._exact}')"
191
+
192
+ def __str__(self):
193
+ return str(float(self._exact))
194
+
195
+ def __float__(self):
196
+ return float(self._exact)
197
+
198
+ def __int__(self):
199
+ return int(self._exact)
200
+
201
+ def __hash__(self):
202
+ return hash(self._exact)
203
+
204
+ # ------------------------------------------------------------------
205
+ # Diagnostic
206
+ # ------------------------------------------------------------------
207
+
208
+ def diagnose(self) -> dict:
209
+ """Return both strands and the residue between them."""
210
+ return {
211
+ "binary_strand": self._approx,
212
+ "rational_strand": str(self._exact),
213
+ "residue": str(self.residue),
214
+ "residue_float": float(self.residue),
215
+ "collapsed": self.collapse(),
216
+ }
217
+
218
+
219
+ def _coerce(value) -> MobiusNumber:
220
+ """Convert a raw value into a MobiusNumber."""
221
+ if isinstance(value, MobiusNumber):
222
+ return value
223
+ return MobiusNumber(value)
@@ -0,0 +1,293 @@
1
+ """Tests for MöbiusNumber — complementary residue arithmetic."""
2
+
3
+ import pytest
4
+ from fractions import Fraction
5
+ from mobius_number import MobiusNumber, M
6
+
7
+
8
+ # =====================================================================
9
+ # THE PROOF: 0.1 + 0.2 = 0.3
10
+ # =====================================================================
11
+
12
+ class TestTheProof:
13
+ """The problem that started it all."""
14
+
15
+ def test_ieee754_fails(self):
16
+ """IEEE 754 gets this wrong. This is not a bug — it's the spec."""
17
+ assert 0.1 + 0.2 != 0.3 # The 41-year-old problem
18
+
19
+ def test_mobius_solves_it(self):
20
+ """MöbiusNumber gets it right."""
21
+ assert M("0.1") + M("0.2") == M("0.3")
22
+
23
+ def test_collapse_is_exact(self):
24
+ result = M("0.1") + M("0.2")
25
+ assert result.collapse() == 0.3
26
+
27
+ def test_error_is_zero(self):
28
+ result = M("0.1") + M("0.2")
29
+ target = M("0.3")
30
+ assert result.exact - target.exact == Fraction(0)
31
+
32
+
33
+ # =====================================================================
34
+ # CONSTRUCTION
35
+ # =====================================================================
36
+
37
+ class TestConstruction:
38
+ """Every input path must produce the correct rational strand."""
39
+
40
+ def test_from_string(self):
41
+ n = M("0.1")
42
+ assert n.exact == Fraction(1, 10)
43
+
44
+ def test_from_int(self):
45
+ n = M(42)
46
+ assert n.exact == Fraction(42)
47
+ assert n.approx == 42.0
48
+
49
+ def test_from_float(self):
50
+ n = M(0.5)
51
+ assert n.exact == Fraction(1, 2)
52
+
53
+ def test_from_fraction(self):
54
+ n = M(Fraction(1, 7))
55
+ assert n.exact == Fraction(1, 7)
56
+
57
+ def test_from_mobius(self):
58
+ a = M("3.14")
59
+ b = M(a)
60
+ assert b.exact == a.exact
61
+
62
+ def test_from_negative_string(self):
63
+ n = M("-0.1")
64
+ assert n.exact == Fraction(-1, 10)
65
+
66
+ def test_from_zero(self):
67
+ n = M(0)
68
+ assert n.exact == Fraction(0)
69
+ assert n.approx == 0.0
70
+
71
+ def test_invalid_type_raises(self):
72
+ with pytest.raises(TypeError):
73
+ M([1, 2, 3])
74
+
75
+
76
+ # =====================================================================
77
+ # ARITHMETIC
78
+ # =====================================================================
79
+
80
+ class TestArithmetic:
81
+ """Both strands must propagate through every operation."""
82
+
83
+ def test_addition(self):
84
+ assert (M("0.1") + M("0.2")).exact == Fraction(3, 10)
85
+
86
+ def test_subtraction(self):
87
+ assert (M("0.3") - M("0.1")).exact == Fraction(1, 5)
88
+
89
+ def test_multiplication(self):
90
+ assert (M("0.1") * M("10")).exact == Fraction(1)
91
+
92
+ def test_division(self):
93
+ assert (M("1") / M("3")).exact == Fraction(1, 3)
94
+
95
+ def test_negation(self):
96
+ assert (-M("0.5")).exact == Fraction(-1, 2)
97
+
98
+ def test_abs(self):
99
+ assert abs(M("-7")).exact == Fraction(7)
100
+
101
+ def test_power(self):
102
+ assert (M("3") ** 2).exact == Fraction(9)
103
+
104
+ def test_division_by_zero(self):
105
+ with pytest.raises(ZeroDivisionError):
106
+ M("1") / M("0")
107
+
108
+ def test_radd(self):
109
+ result = 1 + M("0.5")
110
+ assert result.exact == Fraction(3, 2)
111
+
112
+ def test_rsub(self):
113
+ result = 1 - M("0.3")
114
+ assert result.exact == Fraction(7, 10)
115
+
116
+ def test_rmul(self):
117
+ result = 3 * M("0.1")
118
+ assert result.exact == Fraction(3, 10)
119
+
120
+ def test_rtruediv(self):
121
+ result = 1 / M("3")
122
+ assert result.exact == Fraction(1, 3)
123
+
124
+
125
+ # =====================================================================
126
+ # THE FAMOUS FAILURES
127
+ # =====================================================================
128
+
129
+ class TestFamousFailures:
130
+ """Every classic IEEE 754 failure, corrected."""
131
+
132
+ def test_sum_point_one_ten_times(self):
133
+ """0.1 added 10 times should equal 1.0."""
134
+ total = M("0.0")
135
+ for _ in range(10):
136
+ total = total + M("0.1")
137
+ assert total == M("1.0")
138
+
139
+ def test_one_forty_ninth_times_forty_nine(self):
140
+ """(1/49) * 49 should equal 1."""
141
+ assert (M("1") / M("49")) * M("49") == M("1")
142
+
143
+ def test_one_third_times_three(self):
144
+ """(1/3) * 3 should equal 1."""
145
+ assert (M("1") / M("3")) * M("3") == M("1")
146
+
147
+ def test_ten_divided_by_three_times_three(self):
148
+ """$10.00 / 3 * 3 should equal $10.00."""
149
+ assert (M("10") / M("3")) * M("3") == M("10")
150
+
151
+ def test_catastrophic_cancellation(self):
152
+ """(1 + 1e-16) - 1 should equal 1e-16, not 0."""
153
+ result = (M("1") + M("0.0000000000000001")) - M("1")
154
+ assert result.exact == Fraction(1, 10**16)
155
+ assert result.collapse() == 1e-16
156
+
157
+ def test_ieee_catastrophic_cancellation_loses(self):
158
+ """IEEE 754 loses this entirely — returns 0."""
159
+ assert (1.0 + 1e-16) - 1.0 == 0.0 # Total information loss
160
+
161
+ def test_one_seventh_round_trip(self):
162
+ """1/7 * 7 = 1 exactly."""
163
+ assert (M("1") / M("7")) * M("7") == M("1")
164
+
165
+ def test_penny_accumulation(self):
166
+ """Add $0.01 one hundred times = $1.00."""
167
+ total = M("0.0")
168
+ for _ in range(100):
169
+ total = total + M("0.01")
170
+ assert total == M("1.0")
171
+
172
+
173
+ # =====================================================================
174
+ # COMPARISON — THE RATIONAL GOVERNS
175
+ # =====================================================================
176
+
177
+ class TestComparison:
178
+ """Equality and ordering use the rational strand, not the float."""
179
+
180
+ def test_equality(self):
181
+ assert M("0.1") == M("0.1")
182
+
183
+ def test_inequality(self):
184
+ assert M("0.1") != M("0.2")
185
+
186
+ def test_less_than(self):
187
+ assert M("0.1") < M("0.2")
188
+
189
+ def test_less_equal(self):
190
+ assert M("0.1") <= M("0.1")
191
+ assert M("0.1") <= M("0.2")
192
+
193
+ def test_greater_than(self):
194
+ assert M("0.2") > M("0.1")
195
+
196
+ def test_greater_equal(self):
197
+ assert M("0.2") >= M("0.2")
198
+ assert M("0.2") >= M("0.1")
199
+
200
+ def test_cross_type_equality(self):
201
+ assert M("5") == M(5)
202
+
203
+ def test_hash_consistency(self):
204
+ """Equal MöbiusNumbers must have equal hashes."""
205
+ a = M("0.1") + M("0.2")
206
+ b = M("0.3")
207
+ assert hash(a) == hash(b)
208
+
209
+ def test_usable_as_dict_key(self):
210
+ d = {M("0.3"): "found"}
211
+ key = M("0.1") + M("0.2")
212
+ assert d[key] == "found"
213
+
214
+
215
+ # =====================================================================
216
+ # STRAND ANATOMY
217
+ # =====================================================================
218
+
219
+ class TestStrands:
220
+ """The residue (anti-strand) must be the exact complement."""
221
+
222
+ def test_residue_of_point_one(self):
223
+ n = M("0.1")
224
+ # residue = exact - Fraction(approx)
225
+ # exact is 1/10, approx is the nearest float
226
+ assert n.residue == Fraction(1, 10) - Fraction(0.1)
227
+
228
+ def test_residue_of_integer_is_zero(self):
229
+ n = M(1)
230
+ assert n.residue == Fraction(0)
231
+
232
+ def test_residue_of_half_is_zero(self):
233
+ n = M("0.5")
234
+ assert n.residue == Fraction(0) # 0.5 is exact in binary
235
+
236
+ def test_diagnose_returns_dict(self):
237
+ d = M("0.1").diagnose()
238
+ assert "binary_strand" in d
239
+ assert "rational_strand" in d
240
+ assert "residue" in d
241
+ assert "collapsed" in d
242
+
243
+
244
+ # =====================================================================
245
+ # DISPLAY
246
+ # =====================================================================
247
+
248
+ class TestDisplay:
249
+ """String output must show the collapsed (exact) value."""
250
+
251
+ def test_str(self):
252
+ assert str(M("0.3")) == "0.3"
253
+
254
+ def test_repr(self):
255
+ assert repr(M("0.1")) == "M('1/10')"
256
+
257
+ def test_float_conversion(self):
258
+ assert float(M("0.5")) == 0.5
259
+
260
+ def test_int_conversion(self):
261
+ assert int(M("42")) == 42
262
+
263
+
264
+ # =====================================================================
265
+ # ACCUMULATION STRESS
266
+ # =====================================================================
267
+
268
+ class TestAccumulation:
269
+ """Errors must not compound across long chains."""
270
+
271
+ def test_thousand_additions(self):
272
+ """0.001 * 1000 = 1.0 exactly."""
273
+ total = M("0.0")
274
+ for _ in range(1000):
275
+ total = total + M("0.001")
276
+ assert total == M("1.0")
277
+
278
+ def test_chained_division_multiplication(self):
279
+ """(((1 / 3) / 7) / 11) * 11 * 7 * 3 = 1."""
280
+ n = M("1")
281
+ n = n / M("3")
282
+ n = n / M("7")
283
+ n = n / M("11")
284
+ n = n * M("11")
285
+ n = n * M("7")
286
+ n = n * M("3")
287
+ assert n == M("1")
288
+
289
+ def test_financial_chain(self):
290
+ """$100 split 7 ways, then reassembled."""
291
+ share = M("100") / M("7")
292
+ total = share * M("7")
293
+ assert total == M("100")