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 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