fraction-py 1.0.0__py3-none-any.whl
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.
- fraction/__init__.py +22 -0
- fraction/fraction.py +371 -0
- fraction/py.typed +0 -0
- fraction_py-1.0.0.dist-info/METADATA +1628 -0
- fraction_py-1.0.0.dist-info/RECORD +8 -0
- fraction_py-1.0.0.dist-info/WHEEL +5 -0
- fraction_py-1.0.0.dist-info/licenses/LICENSE +21 -0
- fraction_py-1.0.0.dist-info/top_level.txt +1 -0
fraction/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
fraction
|
|
3
|
+
~~~~~~~~
|
|
4
|
+
|
|
5
|
+
A lightweight, pure-Python Fraction data type.
|
|
6
|
+
|
|
7
|
+
Example
|
|
8
|
+
-------
|
|
9
|
+
>>> from fraction import fraction
|
|
10
|
+
>>> a = fraction(1, 2)
|
|
11
|
+
>>> b = fraction(3, 4)
|
|
12
|
+
>>> print(a + b)
|
|
13
|
+
5/4
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from .fraction import fraction
|
|
17
|
+
|
|
18
|
+
__all__ = ["fraction"]
|
|
19
|
+
|
|
20
|
+
__version__ = "1.0.0"
|
|
21
|
+
__author__ = "Shravan Kumar Pandey"
|
|
22
|
+
__license__ = "MIT"
|
fraction/fraction.py
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import numbers
|
|
5
|
+
import types
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from fractions import Fraction
|
|
8
|
+
from typing import Any, Self, overload
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class fraction(numbers.Rational):
|
|
12
|
+
"""
|
|
13
|
+
Represents a mathematical fraction (Rational number).
|
|
14
|
+
|
|
15
|
+
The fraction is strictly immutable and automatically reduced to its simplest form.
|
|
16
|
+
It integrates seamlessly with Python's numeric tower.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__slots__ = ("_num", "_den")
|
|
20
|
+
|
|
21
|
+
def __init__(self, numerator: int, denominator: int = 1) -> None:
|
|
22
|
+
if denominator == 0:
|
|
23
|
+
raise ZeroDivisionError("Denominator cannot be zero.")
|
|
24
|
+
|
|
25
|
+
if not isinstance(numerator, int) or not isinstance(denominator, int):
|
|
26
|
+
raise TypeError("Numerator and denominator must be integers.")
|
|
27
|
+
|
|
28
|
+
if denominator < 0:
|
|
29
|
+
numerator = -numerator
|
|
30
|
+
denominator = -denominator
|
|
31
|
+
|
|
32
|
+
g = math.gcd(numerator, denominator)
|
|
33
|
+
|
|
34
|
+
self._num: int = numerator // g
|
|
35
|
+
self._den: int = denominator // g
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def numerator(self) -> int:
|
|
39
|
+
return self._num
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def denominator(self) -> int:
|
|
43
|
+
return self._den
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def num(self) -> int:
|
|
47
|
+
return self._num
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def den(self) -> int:
|
|
51
|
+
return self._den
|
|
52
|
+
|
|
53
|
+
# =====================================================
|
|
54
|
+
# Alternative Constructors
|
|
55
|
+
# =====================================================
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_string(cls, s: str) -> Self:
|
|
59
|
+
if not isinstance(s, str):
|
|
60
|
+
raise TypeError(f"Argument must be a string, got {type(s).__name__}.")
|
|
61
|
+
|
|
62
|
+
if not s or not s.strip():
|
|
63
|
+
raise ValueError("String cannot be empty.")
|
|
64
|
+
|
|
65
|
+
parts = s.split("/")
|
|
66
|
+
if len(parts) == 1:
|
|
67
|
+
return cls(int(parts[0].strip()), 1)
|
|
68
|
+
if len(parts) == 2:
|
|
69
|
+
return cls(int(parts[0].strip()), int(parts[1].strip()))
|
|
70
|
+
|
|
71
|
+
raise ValueError(f"Invalid fraction string format: {s}")
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_float(cls, f: float) -> Self:
|
|
75
|
+
if not isinstance(f, float):
|
|
76
|
+
raise TypeError(f"Argument must be a float, got {type(f).__name__}.")
|
|
77
|
+
|
|
78
|
+
if math.isnan(f) or math.isinf(f):
|
|
79
|
+
raise ValueError("Cannot convert NaN or Infinity to fraction.")
|
|
80
|
+
|
|
81
|
+
n, d = f.as_integer_ratio()
|
|
82
|
+
return cls(n, d)
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def from_decimal(cls, dec: Decimal) -> Self:
|
|
86
|
+
if not isinstance(dec, Decimal):
|
|
87
|
+
raise TypeError(f"Argument must be a Decimal, got {type(dec).__name__}.")
|
|
88
|
+
|
|
89
|
+
if not dec.is_finite():
|
|
90
|
+
raise ValueError("Cannot convert NaN or Infinity to fraction.")
|
|
91
|
+
|
|
92
|
+
n, d = dec.as_integer_ratio()
|
|
93
|
+
return cls(n, d)
|
|
94
|
+
|
|
95
|
+
# =====================================================
|
|
96
|
+
# Magic Methods: Lifecycle & Representation
|
|
97
|
+
# =====================================================
|
|
98
|
+
|
|
99
|
+
def __str__(self) -> str:
|
|
100
|
+
if self._num == 0:
|
|
101
|
+
return "0/1"
|
|
102
|
+
if self._den == 1:
|
|
103
|
+
return str(self._num)
|
|
104
|
+
return f"{self._num}/{self._den}"
|
|
105
|
+
|
|
106
|
+
def __repr__(self) -> str:
|
|
107
|
+
return f"fraction({self._num}, {self._den})"
|
|
108
|
+
|
|
109
|
+
def __hash__(self) -> int: # type: ignore[override]
|
|
110
|
+
return hash(Fraction(self._num, self._den))
|
|
111
|
+
|
|
112
|
+
def __bool__(self) -> bool:
|
|
113
|
+
return self._num != 0
|
|
114
|
+
|
|
115
|
+
def __reduce__(self) -> tuple[type, tuple[int, int]]:
|
|
116
|
+
return self.__class__, (self._num, self._den)
|
|
117
|
+
|
|
118
|
+
def __copy__(self) -> Self:
|
|
119
|
+
return self
|
|
120
|
+
|
|
121
|
+
def __deepcopy__(self, memo: dict[Any, Any]) -> Self:
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
# =====================================================
|
|
125
|
+
# Arithmetic Operators
|
|
126
|
+
# =====================================================
|
|
127
|
+
|
|
128
|
+
def __add__(self, other: Any) -> Self | Any:
|
|
129
|
+
if isinstance(other, int):
|
|
130
|
+
return self.__class__(self._num + other * self._den, self._den)
|
|
131
|
+
if isinstance(other, (Fraction, fraction)):
|
|
132
|
+
n = self._num * other.denominator + other.numerator * self._den
|
|
133
|
+
d = self._den * other.denominator
|
|
134
|
+
return self.__class__(n, d)
|
|
135
|
+
return NotImplemented # pragma: no cover
|
|
136
|
+
|
|
137
|
+
def __radd__(self, other: Any) -> Self | Any:
|
|
138
|
+
return self + other
|
|
139
|
+
|
|
140
|
+
def __sub__(self, other: Any) -> Self | Any:
|
|
141
|
+
if isinstance(other, int):
|
|
142
|
+
return self.__class__(self._num - other * self._den, self._den)
|
|
143
|
+
if isinstance(other, (Fraction, fraction)):
|
|
144
|
+
n = self._num * other.denominator - other.numerator * self._den
|
|
145
|
+
d = self._den * other.denominator
|
|
146
|
+
return self.__class__(n, d)
|
|
147
|
+
return NotImplemented # pragma: no cover
|
|
148
|
+
|
|
149
|
+
def __rsub__(self, other: Any) -> Self | Any:
|
|
150
|
+
if isinstance(other, int):
|
|
151
|
+
return self.__class__(other * self._den - self._num, self._den)
|
|
152
|
+
return NotImplemented # pragma: no cover
|
|
153
|
+
|
|
154
|
+
def __mul__(self, other: Any) -> Self | Any:
|
|
155
|
+
if isinstance(other, int):
|
|
156
|
+
return self.__class__(self._num * other, self._den)
|
|
157
|
+
if isinstance(other, (Fraction, fraction)):
|
|
158
|
+
return self.__class__(
|
|
159
|
+
self._num * other.numerator, self._den * other.denominator
|
|
160
|
+
)
|
|
161
|
+
return NotImplemented # pragma: no cover
|
|
162
|
+
|
|
163
|
+
def __rmul__(self, other: Any) -> Self | Any:
|
|
164
|
+
return self * other
|
|
165
|
+
|
|
166
|
+
def __truediv__(self, other: Any) -> Self | Any:
|
|
167
|
+
if isinstance(other, int):
|
|
168
|
+
if other == 0:
|
|
169
|
+
raise ZeroDivisionError("Cannot divide by zero.")
|
|
170
|
+
return self.__class__(self._num, self._den * other)
|
|
171
|
+
if isinstance(other, (Fraction, fraction)):
|
|
172
|
+
if other.numerator == 0:
|
|
173
|
+
raise ZeroDivisionError("Cannot divide by zero.")
|
|
174
|
+
return self.__class__(
|
|
175
|
+
self._num * other.denominator, self._den * other.numerator
|
|
176
|
+
)
|
|
177
|
+
return NotImplemented # pragma: no cover
|
|
178
|
+
|
|
179
|
+
def __rtruediv__(self, other: Any) -> Self | Any:
|
|
180
|
+
if self._num == 0:
|
|
181
|
+
raise ZeroDivisionError("Cannot divide by zero.")
|
|
182
|
+
if isinstance(other, int):
|
|
183
|
+
return self.__class__(other * self._den, self._num)
|
|
184
|
+
return NotImplemented # pragma: no cover
|
|
185
|
+
|
|
186
|
+
# =====================================================
|
|
187
|
+
# Abstract Methods required by numbers.Rational
|
|
188
|
+
# =====================================================
|
|
189
|
+
|
|
190
|
+
def __floor__(self) -> int: # pragma: no cover
|
|
191
|
+
return self._num // self._den
|
|
192
|
+
|
|
193
|
+
def __ceil__(self) -> int: # pragma: no cover
|
|
194
|
+
return -(-self._num // self._den)
|
|
195
|
+
|
|
196
|
+
def __trunc__(self) -> int: # pragma: no cover
|
|
197
|
+
res = abs(self._num) // self._den
|
|
198
|
+
return res if self._num >= 0 else -res
|
|
199
|
+
|
|
200
|
+
def __floordiv__(self, other: Any) -> int | Any: # pragma: no cover
|
|
201
|
+
res = self / other
|
|
202
|
+
if res is NotImplemented:
|
|
203
|
+
return NotImplemented
|
|
204
|
+
return math.floor(res)
|
|
205
|
+
|
|
206
|
+
def __rfloordiv__(self, other: Any) -> int | Any: # pragma: no cover
|
|
207
|
+
res = other / self
|
|
208
|
+
if res is NotImplemented:
|
|
209
|
+
return NotImplemented
|
|
210
|
+
return math.floor(res)
|
|
211
|
+
|
|
212
|
+
def __mod__(self, other: Any) -> Self | Any: # pragma: no cover
|
|
213
|
+
floor_div = self // other
|
|
214
|
+
if floor_div is NotImplemented:
|
|
215
|
+
return NotImplemented
|
|
216
|
+
return self - (other * floor_div)
|
|
217
|
+
|
|
218
|
+
def __rmod__(self, other: Any) -> Self | Any: # pragma: no cover
|
|
219
|
+
floor_div = other // self
|
|
220
|
+
if floor_div is NotImplemented:
|
|
221
|
+
return NotImplemented
|
|
222
|
+
return other - (self * floor_div)
|
|
223
|
+
|
|
224
|
+
def __rpow__(self, base: Any) -> Any: # pragma: no cover
|
|
225
|
+
return base ** (self._num / self._den)
|
|
226
|
+
|
|
227
|
+
# =====================================================
|
|
228
|
+
# Comparison Operators
|
|
229
|
+
# =====================================================
|
|
230
|
+
|
|
231
|
+
def __eq__(self, other: object) -> bool | types.NotImplementedType:
|
|
232
|
+
if isinstance(other, int):
|
|
233
|
+
return self._den == 1 and self._num == other
|
|
234
|
+
if isinstance(other, (Fraction, fraction)):
|
|
235
|
+
return self._num == other.numerator and self._den == other.denominator
|
|
236
|
+
if isinstance(other, float):
|
|
237
|
+
return float(self) == other
|
|
238
|
+
return NotImplemented # pragma: no cover
|
|
239
|
+
|
|
240
|
+
def __lt__(self, other: Any) -> bool | types.NotImplementedType:
|
|
241
|
+
if isinstance(other, int):
|
|
242
|
+
return self._num < other * self._den
|
|
243
|
+
if isinstance(other, (Fraction, fraction)):
|
|
244
|
+
return self._num * other.denominator < other.numerator * self._den
|
|
245
|
+
if isinstance(other, float):
|
|
246
|
+
return float(self) < other
|
|
247
|
+
return NotImplemented # pragma: no cover
|
|
248
|
+
|
|
249
|
+
def __le__(self, other: Any) -> bool | types.NotImplementedType:
|
|
250
|
+
if isinstance(other, int):
|
|
251
|
+
return self._num <= other * self._den
|
|
252
|
+
if isinstance(other, (Fraction, fraction)):
|
|
253
|
+
return self._num * other.denominator <= other.numerator * self._den
|
|
254
|
+
if isinstance(other, float):
|
|
255
|
+
return float(self) <= other
|
|
256
|
+
return NotImplemented # pragma: no cover
|
|
257
|
+
|
|
258
|
+
def __gt__(self, other: Any) -> bool | types.NotImplementedType:
|
|
259
|
+
if isinstance(other, int):
|
|
260
|
+
return self._num > other * self._den
|
|
261
|
+
if isinstance(other, (Fraction, fraction)):
|
|
262
|
+
return self._num * other.denominator > other.numerator * self._den
|
|
263
|
+
if isinstance(other, float):
|
|
264
|
+
return float(self) > other
|
|
265
|
+
return NotImplemented # pragma: no cover
|
|
266
|
+
|
|
267
|
+
def __ge__(self, other: Any) -> bool | types.NotImplementedType:
|
|
268
|
+
if isinstance(other, int):
|
|
269
|
+
return self._num >= other * self._den
|
|
270
|
+
if isinstance(other, (Fraction, fraction)):
|
|
271
|
+
return self._num * other.denominator >= other.numerator * self._den
|
|
272
|
+
if isinstance(other, float):
|
|
273
|
+
return float(self) >= other
|
|
274
|
+
return NotImplemented # pragma: no cover
|
|
275
|
+
|
|
276
|
+
# =====================================================
|
|
277
|
+
# Unary & Power Operators
|
|
278
|
+
# =====================================================
|
|
279
|
+
|
|
280
|
+
def __abs__(self) -> Self:
|
|
281
|
+
return self.__class__(abs(self._num), self._den)
|
|
282
|
+
|
|
283
|
+
def __neg__(self) -> Self:
|
|
284
|
+
return self.__class__(-self._num, self._den)
|
|
285
|
+
|
|
286
|
+
def __pos__(self) -> Self:
|
|
287
|
+
return self.__class__(self._num, self._den)
|
|
288
|
+
|
|
289
|
+
def __pow__(self, power: Any) -> Self | Any:
|
|
290
|
+
if not isinstance(power, int):
|
|
291
|
+
return NotImplemented # pragma: no cover
|
|
292
|
+
|
|
293
|
+
if power == 0:
|
|
294
|
+
return self.__class__(1, 1)
|
|
295
|
+
if power > 0:
|
|
296
|
+
return self.__class__(self._num**power, self._den**power)
|
|
297
|
+
|
|
298
|
+
if self._num == 0:
|
|
299
|
+
raise ZeroDivisionError("Cannot raise zero fraction to a negative power.")
|
|
300
|
+
|
|
301
|
+
abs_p = abs(power)
|
|
302
|
+
return self.__class__(self._den**abs_p, self._num**abs_p)
|
|
303
|
+
|
|
304
|
+
# =====================================================
|
|
305
|
+
# Type Conversions & Formatting
|
|
306
|
+
# =====================================================
|
|
307
|
+
|
|
308
|
+
def __float__(self) -> float:
|
|
309
|
+
return self._num / self._den
|
|
310
|
+
|
|
311
|
+
def __int__(self) -> int:
|
|
312
|
+
"""
|
|
313
|
+
Return the integer part by truncating toward zero
|
|
314
|
+
without converting to float.
|
|
315
|
+
"""
|
|
316
|
+
res = abs(self._num) // self._den
|
|
317
|
+
return res if self._num >= 0 else -res
|
|
318
|
+
|
|
319
|
+
@overload
|
|
320
|
+
def __round__(self, ndigits: None = None) -> int: ... # pragma: no cover
|
|
321
|
+
|
|
322
|
+
@overload
|
|
323
|
+
def __round__(self, ndigits: int) -> Self: ... # pragma: no cover
|
|
324
|
+
|
|
325
|
+
def __round__(self, ndigits: int | None = None) -> int | Self:
|
|
326
|
+
if ndigits is None:
|
|
327
|
+
return round(Fraction(self._num, self._den))
|
|
328
|
+
|
|
329
|
+
res = round(Fraction(self._num, self._den), ndigits)
|
|
330
|
+
return self.__class__(res.numerator, res.denominator)
|
|
331
|
+
|
|
332
|
+
def __format__(self, format_spec: str) -> str:
|
|
333
|
+
return format(Fraction(self._num, self._den), format_spec)
|
|
334
|
+
|
|
335
|
+
# =====================================================
|
|
336
|
+
# Utility Methods
|
|
337
|
+
# =====================================================
|
|
338
|
+
|
|
339
|
+
def reciprocal(self) -> Self:
|
|
340
|
+
if self._num == 0:
|
|
341
|
+
raise ZeroDivisionError("Zero has no reciprocal.")
|
|
342
|
+
return self.__class__(self._den, self._num)
|
|
343
|
+
|
|
344
|
+
def to_decimal(self, places: int = 2) -> float:
|
|
345
|
+
if places < 0:
|
|
346
|
+
raise ValueError("Places must be non-negative.")
|
|
347
|
+
return round(float(self), places)
|
|
348
|
+
|
|
349
|
+
def as_tuple(self) -> tuple[int, int]:
|
|
350
|
+
return self._num, self._den
|
|
351
|
+
|
|
352
|
+
def mixed(self) -> tuple[int, Self]:
|
|
353
|
+
"""
|
|
354
|
+
Convert an improper fraction into a mixed number.
|
|
355
|
+
|
|
356
|
+
For negative fractions where the whole part is non-zero,
|
|
357
|
+
the whole part carries the sign and the fractional part is positive
|
|
358
|
+
(e.g., -7/3 -> -2, 1/3).
|
|
359
|
+
If the whole part is zero, the fractional part carries the sign
|
|
360
|
+
(e.g., -1/3 -> 0, -1/3).
|
|
361
|
+
"""
|
|
362
|
+
whole = abs(self._num) // self._den
|
|
363
|
+
remainder = abs(self._num) % self._den
|
|
364
|
+
|
|
365
|
+
if self._num < 0:
|
|
366
|
+
if whole != 0:
|
|
367
|
+
whole = -whole
|
|
368
|
+
else:
|
|
369
|
+
remainder = -remainder
|
|
370
|
+
|
|
371
|
+
return whole, self.__class__(remainder, self._den)
|
fraction/py.typed
ADDED
|
File without changes
|