simple-portable-numeric 0.0.1__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.
@@ -0,0 +1,6 @@
1
+ # coding: utf-8
2
+ from simple_portable_numeric.enum_rounding_mode import EnumRoundingMode
3
+ from simple_portable_numeric.portable_decimal import PortableDecimal
4
+ from simple_portable_numeric.portable_rational import PortableRational
5
+
6
+ __all__ = ["EnumRoundingMode", "PortableDecimal", "PortableRational"]
@@ -0,0 +1,17 @@
1
+ # coding: utf-8
2
+ from enum import Enum
3
+
4
+
5
+ class EnumRoundingMode(Enum):
6
+ """
7
+ (en) Rounding modes used when an exact value must be reduced to fewer digits.
8
+ The member names and behavior are identical to the Dart version.
9
+
10
+ (ja) 正確な値を少ない桁数に丸める際の丸めモードです。
11
+ メンバー名と動作はDart版と同一です。
12
+ """
13
+ halfEven = "halfEven" # Nearest, ties to even (banker's rounding).
14
+ halfUp = "halfUp" # Nearest, ties away from zero.
15
+ down = "down" # Toward zero.
16
+ floor = "floor" # Toward negative infinity.
17
+ ceiling = "ceiling" # Toward positive infinity.
@@ -0,0 +1,313 @@
1
+ # coding: utf-8
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from typing import TYPE_CHECKING
6
+
7
+ from simple_portable_numeric.enum_rounding_mode import EnumRoundingMode
8
+ from simple_portable_numeric.util_int_math import digits_to_int, int_to_digits, ratio_to_float, round_div
9
+
10
+ if TYPE_CHECKING:
11
+ from simple_portable_numeric.portable_rational import PortableRational
12
+
13
+ _DECIMAL_PATTERN = re.compile(r"[+-]?[0-9]+(\.[0-9]+)?")
14
+
15
+
16
+ def _check_int(value: object, name: str) -> None:
17
+ if isinstance(value, bool) or not isinstance(value, int):
18
+ raise TypeError(f"{name} must be int, got {type(value).__name__}")
19
+
20
+
21
+ class PortableDecimal:
22
+ """
23
+ (en) An arbitrary-precision decimal number, represented as
24
+ unscaled_value * 10^(-scale).
25
+ The results are identical to the Dart version of this package.
26
+ Equality is by numeric value (1.50 == 1.5), while to_string keeps the scale.
27
+
28
+ (ja) 任意精度の10進数です。unscaled_value * 10^(-scale) として表現されます。
29
+ 計算結果はこのパッケージのDart版と同一になります。
30
+ 等価判定は数値で行われ(1.50 == 1.5)、文字列化ではscaleが保持されます。
31
+ """
32
+ __slots__ = ("_unscaled_value", "_scale")
33
+
34
+ def __init__(self, unscaled_value: int, scale: int = 0):
35
+ """
36
+ (en) Creates a decimal whose value is unscaled_value * 10^(-scale).
37
+
38
+ (ja) 値が unscaled_value * 10^(-scale) の10進数を作成します。
39
+
40
+ Parameters
41
+ ----------
42
+ unscaled_value: int
43
+ The integer value with the decimal point removed.
44
+ scale: int
45
+ The number of digits after the decimal point. Must be 0 or more.
46
+ """
47
+ _check_int(unscaled_value, "unscaled_value")
48
+ _check_int(scale, "scale")
49
+ if scale < 0:
50
+ raise ValueError(f"scale must be 0 or more, got {scale}")
51
+ self._unscaled_value = unscaled_value
52
+ self._scale = scale
53
+
54
+ @classmethod
55
+ def parse(cls, text: str) -> PortableDecimal:
56
+ """
57
+ (en) Parses a plain decimal string such as "-12.340".
58
+ The number of fractional digits becomes the scale.
59
+ Exponent notation, whitespace and non-ASCII digits are rejected.
60
+
61
+ (ja) "-12.340" のような通常表記の10進文字列を解析します。
62
+ 小数部の桁数がscaleになります。
63
+ 指数表記、空白、ASCII以外の数字は受け付けません。
64
+
65
+ Parameters
66
+ ----------
67
+ text: str
68
+ The string to parse.
69
+
70
+ Returns
71
+ -------
72
+ PortableDecimal
73
+ The parsed value.
74
+
75
+ Raises
76
+ ------
77
+ ValueError
78
+ If the string is not a valid decimal.
79
+ """
80
+ if not isinstance(text, str):
81
+ raise TypeError(f"text must be str, got {type(text).__name__}")
82
+ if _DECIMAL_PATTERN.fullmatch(text) is None:
83
+ raise ValueError(f"Invalid decimal string: {text!r}")
84
+ int_part, _, frac_part = text.partition(".")
85
+ negative = int_part.startswith("-")
86
+ value = digits_to_int(int_part.lstrip("+-") + frac_part)
87
+ return cls(-value if negative else value, len(frac_part))
88
+
89
+ @property
90
+ def unscaled_value(self) -> int:
91
+ """
92
+ (en) The integer value with the decimal point removed.
93
+
94
+ (ja) 小数点を取り除いた整数値です。
95
+ """
96
+ return self._unscaled_value
97
+
98
+ @property
99
+ def scale(self) -> int:
100
+ """
101
+ (en) The number of digits after the decimal point.
102
+
103
+ (ja) 小数点以下の桁数です。
104
+ """
105
+ return self._scale
106
+
107
+ def _aligned(self, other: PortableDecimal) -> tuple[int, int, int]:
108
+ """Returns (self unscaled, other unscaled, common scale)."""
109
+ scale = max(self._scale, other._scale)
110
+ return (self._unscaled_value * 10 ** (scale - self._scale),
111
+ other._unscaled_value * 10 ** (scale - other._scale),
112
+ scale)
113
+
114
+ def __add__(self, other: object) -> PortableDecimal:
115
+ if not isinstance(other, PortableDecimal):
116
+ return NotImplemented
117
+ a, b, scale = self._aligned(other)
118
+ return PortableDecimal(a + b, scale)
119
+
120
+ def __sub__(self, other: object) -> PortableDecimal:
121
+ if not isinstance(other, PortableDecimal):
122
+ return NotImplemented
123
+ a, b, scale = self._aligned(other)
124
+ return PortableDecimal(a - b, scale)
125
+
126
+ def __mul__(self, other: object) -> PortableDecimal:
127
+ if not isinstance(other, PortableDecimal):
128
+ return NotImplemented
129
+ return PortableDecimal(self._unscaled_value * other._unscaled_value, self._scale + other._scale)
130
+
131
+ def __neg__(self) -> PortableDecimal:
132
+ return PortableDecimal(-self._unscaled_value, self._scale)
133
+
134
+ def __abs__(self) -> PortableDecimal:
135
+ return PortableDecimal(abs(self._unscaled_value), self._scale)
136
+
137
+ def divide(self, other: PortableDecimal, scale: int, rounding: EnumRoundingMode) -> PortableDecimal:
138
+ """
139
+ (en) Divides this value by other. The exact quotient is rounded once
140
+ to the specified number of fractional digits.
141
+
142
+ (ja) この値を other で割ります。正確な商を、指定した小数点以下の桁数に
143
+ 1回だけ丸めます。
144
+
145
+ Parameters
146
+ ----------
147
+ other: PortableDecimal
148
+ The divisor.
149
+ scale: int
150
+ The number of digits after the decimal point of the result. Must be 0 or more.
151
+ rounding: EnumRoundingMode
152
+ The rounding mode.
153
+
154
+ Returns
155
+ -------
156
+ PortableDecimal
157
+ The rounded quotient.
158
+
159
+ Raises
160
+ ------
161
+ ZeroDivisionError
162
+ If other is zero.
163
+ """
164
+ if not isinstance(other, PortableDecimal):
165
+ raise TypeError(f"other must be PortableDecimal, got {type(other).__name__}")
166
+ _check_int(scale, "scale")
167
+ if scale < 0:
168
+ raise ValueError(f"scale must be 0 or more, got {scale}")
169
+ if other._unscaled_value == 0:
170
+ raise ZeroDivisionError("Division by zero")
171
+ # result = (a / 10^sa) / (b / 10^sb) * 10^scale = a * 10^(scale + sb - sa) / b
172
+ exp = scale + other._scale - self._scale
173
+ n = self._unscaled_value
174
+ d = other._unscaled_value
175
+ if exp >= 0:
176
+ n *= 10 ** exp
177
+ else:
178
+ d *= 10 ** (-exp)
179
+ return PortableDecimal(round_div(n, d, rounding), scale)
180
+
181
+ def set_scale(self, scale: int, rounding: EnumRoundingMode) -> PortableDecimal:
182
+ """
183
+ (en) Returns a value with the specified scale. Increasing the scale is exact;
184
+ decreasing it rounds once with the specified rounding mode.
185
+
186
+ (ja) 指定したscaleの値を返します。scaleを増やす場合は正確に、
187
+ 減らす場合は指定した丸めモードで1回だけ丸めます。
188
+
189
+ Parameters
190
+ ----------
191
+ scale: int
192
+ The new number of digits after the decimal point. Must be 0 or more.
193
+ rounding: EnumRoundingMode
194
+ The rounding mode used when the scale decreases.
195
+
196
+ Returns
197
+ -------
198
+ PortableDecimal
199
+ The rescaled value.
200
+ """
201
+ _check_int(scale, "scale")
202
+ if scale < 0:
203
+ raise ValueError(f"scale must be 0 or more, got {scale}")
204
+ if scale >= self._scale:
205
+ return PortableDecimal(self._unscaled_value * 10 ** (scale - self._scale), scale)
206
+ return PortableDecimal(round_div(self._unscaled_value, 10 ** (self._scale - scale), rounding), scale)
207
+
208
+ def compare_to(self, other: PortableDecimal) -> int:
209
+ """
210
+ (en) Compares by numeric value.
211
+
212
+ (ja) 数値で比較します。
213
+
214
+ Parameters
215
+ ----------
216
+ other: PortableDecimal
217
+ The value to compare with.
218
+
219
+ Returns
220
+ -------
221
+ int
222
+ -1 if this < other, 0 if equal, 1 if this > other.
223
+ """
224
+ if not isinstance(other, PortableDecimal):
225
+ raise TypeError(f"other must be PortableDecimal, got {type(other).__name__}")
226
+ a, b, _ = self._aligned(other)
227
+ return (a > b) - (a < b)
228
+
229
+ def __eq__(self, other: object) -> bool:
230
+ if not isinstance(other, PortableDecimal):
231
+ return NotImplemented
232
+ return self.compare_to(other) == 0
233
+
234
+ def __lt__(self, other: object) -> bool:
235
+ if not isinstance(other, PortableDecimal):
236
+ return NotImplemented
237
+ return self.compare_to(other) < 0
238
+
239
+ def __le__(self, other: object) -> bool:
240
+ if not isinstance(other, PortableDecimal):
241
+ return NotImplemented
242
+ return self.compare_to(other) <= 0
243
+
244
+ def __gt__(self, other: object) -> bool:
245
+ if not isinstance(other, PortableDecimal):
246
+ return NotImplemented
247
+ return self.compare_to(other) > 0
248
+
249
+ def __ge__(self, other: object) -> bool:
250
+ if not isinstance(other, PortableDecimal):
251
+ return NotImplemented
252
+ return self.compare_to(other) >= 0
253
+
254
+ def __hash__(self) -> int:
255
+ u, s = self._unscaled_value, self._scale
256
+ while s > 0 and u % 10 == 0:
257
+ u //= 10
258
+ s -= 1
259
+ return hash((u, s))
260
+
261
+ def to_rational(self) -> PortableRational:
262
+ """
263
+ (en) Converts this value to an exactly equal rational.
264
+
265
+ (ja) この値を、正確に等しい有理数に変換します。
266
+
267
+ Returns
268
+ -------
269
+ PortableRational
270
+ The equal rational value.
271
+ """
272
+ from simple_portable_numeric.portable_rational import PortableRational
273
+ return PortableRational(self._unscaled_value, 10 ** self._scale)
274
+
275
+ def to_float(self) -> float:
276
+ """
277
+ (en) Converts this value to the nearest double (ties to even).
278
+ Overflow returns +-inf. The result is identical to toDouble in Dart.
279
+
280
+ (ja) この値を最も近い倍精度浮動小数点数に変換します(同距離は偶数側)。
281
+ 範囲外の場合は +-inf を返します。結果はDart版の toDouble と同一です。
282
+
283
+ Returns
284
+ -------
285
+ float
286
+ The converted value.
287
+ """
288
+ return ratio_to_float(self._unscaled_value, 10 ** self._scale)
289
+
290
+ def to_string(self) -> str:
291
+ """
292
+ (en) Returns the plain-notation string keeping the scale, e.g. "-1.50".
293
+ Negative zero is never output.
294
+
295
+ (ja) scaleを保持した通常表記の文字列を返します(例: "-1.50")。
296
+ 負のゼロは出力されません。
297
+
298
+ Returns
299
+ -------
300
+ str
301
+ The string representation.
302
+ """
303
+ digits = int_to_digits(abs(self._unscaled_value))
304
+ if self._scale > 0:
305
+ digits = digits.rjust(self._scale + 1, "0")
306
+ digits = digits[:-self._scale] + "." + digits[-self._scale:]
307
+ return "-" + digits if self._unscaled_value < 0 else digits
308
+
309
+ def __str__(self) -> str:
310
+ return self.to_string()
311
+
312
+ def __repr__(self) -> str:
313
+ return f"PortableDecimal('{self.to_string()}')"
@@ -0,0 +1,255 @@
1
+ # coding: utf-8
2
+ from __future__ import annotations
3
+
4
+ import math
5
+ import re
6
+
7
+ from simple_portable_numeric.enum_rounding_mode import EnumRoundingMode
8
+ from simple_portable_numeric.portable_decimal import PortableDecimal
9
+ from simple_portable_numeric.util_int_math import digits_to_int, int_to_digits, ratio_to_float, round_div
10
+
11
+ _FRACTION_PATTERN = re.compile(r"([+-]?)([0-9]+)/([0-9]+)")
12
+ _DECIMAL_PATTERN = re.compile(r"[+-]?[0-9]+(\.[0-9]+)?")
13
+
14
+
15
+ def _check_int(value: object, name: str) -> None:
16
+ if isinstance(value, bool) or not isinstance(value, int):
17
+ raise TypeError(f"{name} must be int, got {type(value).__name__}")
18
+
19
+
20
+ class PortableRational:
21
+ """
22
+ (en) An exact rational number numerator / denominator.
23
+ It is always kept in canonical form: reduced, with a positive denominator.
24
+ The results are identical to the Dart version of this package.
25
+
26
+ (ja) 分子 / 分母 で表される正確な有理数です。
27
+ 常に約分済みで分母が正の正規形で保持されます。
28
+ 計算結果はこのパッケージのDart版と同一になります。
29
+ """
30
+ __slots__ = ("_numerator", "_denominator")
31
+
32
+ def __init__(self, numerator: int, denominator: int = 1):
33
+ """
34
+ (en) Creates numerator / denominator, reduced to canonical form.
35
+
36
+ (ja) 分子 / 分母 を作成し、正規形に約分します。
37
+
38
+ Parameters
39
+ ----------
40
+ numerator: int
41
+ The numerator.
42
+ denominator: int
43
+ The denominator. Must not be zero.
44
+
45
+ Raises
46
+ ------
47
+ ZeroDivisionError
48
+ If denominator is zero.
49
+ """
50
+ _check_int(numerator, "numerator")
51
+ _check_int(denominator, "denominator")
52
+ if denominator == 0:
53
+ raise ZeroDivisionError("Division by zero")
54
+ if denominator < 0:
55
+ numerator, denominator = -numerator, -denominator
56
+ g = math.gcd(numerator, denominator)
57
+ self._numerator = numerator // g
58
+ self._denominator = denominator // g
59
+
60
+ @classmethod
61
+ def parse(cls, text: str) -> PortableRational:
62
+ """
63
+ (en) Parses "n/d", an integer such as "-3", or a plain decimal such as "0.75".
64
+ The result is reduced to canonical form.
65
+
66
+ (ja) "n/d"、"-3" のような整数、"0.75" のような通常表記の10進数を解析します。
67
+ 結果は正規形に約分されます。
68
+
69
+ Parameters
70
+ ----------
71
+ text: str
72
+ The string to parse.
73
+
74
+ Returns
75
+ -------
76
+ PortableRational
77
+ The parsed value.
78
+
79
+ Raises
80
+ ------
81
+ ValueError
82
+ If the string is not a valid format.
83
+ ZeroDivisionError
84
+ If the denominator is zero.
85
+ """
86
+ if not isinstance(text, str):
87
+ raise TypeError(f"text must be str, got {type(text).__name__}")
88
+ m = _FRACTION_PATTERN.fullmatch(text)
89
+ if m is not None:
90
+ n = digits_to_int(m.group(2))
91
+ return cls(-n if m.group(1) == "-" else n, digits_to_int(m.group(3)))
92
+ if _DECIMAL_PATTERN.fullmatch(text) is not None:
93
+ return PortableDecimal.parse(text).to_rational()
94
+ raise ValueError(f"Invalid rational string: {text!r}")
95
+
96
+ @property
97
+ def numerator(self) -> int:
98
+ """
99
+ (en) The numerator in canonical form.
100
+
101
+ (ja) 正規形での分子です。
102
+ """
103
+ return self._numerator
104
+
105
+ @property
106
+ def denominator(self) -> int:
107
+ """
108
+ (en) The denominator in canonical form (always positive).
109
+
110
+ (ja) 正規形での分母です(常に正)。
111
+ """
112
+ return self._denominator
113
+
114
+ def __add__(self, other: object) -> PortableRational:
115
+ if not isinstance(other, PortableRational):
116
+ return NotImplemented
117
+ return PortableRational(self._numerator * other._denominator + other._numerator * self._denominator,
118
+ self._denominator * other._denominator)
119
+
120
+ def __sub__(self, other: object) -> PortableRational:
121
+ if not isinstance(other, PortableRational):
122
+ return NotImplemented
123
+ return PortableRational(self._numerator * other._denominator - other._numerator * self._denominator,
124
+ self._denominator * other._denominator)
125
+
126
+ def __mul__(self, other: object) -> PortableRational:
127
+ if not isinstance(other, PortableRational):
128
+ return NotImplemented
129
+ return PortableRational(self._numerator * other._numerator, self._denominator * other._denominator)
130
+
131
+ def __truediv__(self, other: object) -> PortableRational:
132
+ if not isinstance(other, PortableRational):
133
+ return NotImplemented
134
+ if other._numerator == 0:
135
+ raise ZeroDivisionError("Division by zero")
136
+ return PortableRational(self._numerator * other._denominator, self._denominator * other._numerator)
137
+
138
+ def __neg__(self) -> PortableRational:
139
+ return PortableRational(-self._numerator, self._denominator)
140
+
141
+ def __abs__(self) -> PortableRational:
142
+ return PortableRational(abs(self._numerator), self._denominator)
143
+
144
+ def compare_to(self, other: PortableRational) -> int:
145
+ """
146
+ (en) Compares by numeric value.
147
+
148
+ (ja) 数値で比較します。
149
+
150
+ Parameters
151
+ ----------
152
+ other: PortableRational
153
+ The value to compare with.
154
+
155
+ Returns
156
+ -------
157
+ int
158
+ -1 if this < other, 0 if equal, 1 if this > other.
159
+ """
160
+ if not isinstance(other, PortableRational):
161
+ raise TypeError(f"other must be PortableRational, got {type(other).__name__}")
162
+ a = self._numerator * other._denominator
163
+ b = other._numerator * self._denominator
164
+ return (a > b) - (a < b)
165
+
166
+ def __eq__(self, other: object) -> bool:
167
+ if not isinstance(other, PortableRational):
168
+ return NotImplemented
169
+ return self._numerator == other._numerator and self._denominator == other._denominator
170
+
171
+ def __lt__(self, other: object) -> bool:
172
+ if not isinstance(other, PortableRational):
173
+ return NotImplemented
174
+ return self.compare_to(other) < 0
175
+
176
+ def __le__(self, other: object) -> bool:
177
+ if not isinstance(other, PortableRational):
178
+ return NotImplemented
179
+ return self.compare_to(other) <= 0
180
+
181
+ def __gt__(self, other: object) -> bool:
182
+ if not isinstance(other, PortableRational):
183
+ return NotImplemented
184
+ return self.compare_to(other) > 0
185
+
186
+ def __ge__(self, other: object) -> bool:
187
+ if not isinstance(other, PortableRational):
188
+ return NotImplemented
189
+ return self.compare_to(other) >= 0
190
+
191
+ def __hash__(self) -> int:
192
+ return hash((self._numerator, self._denominator))
193
+
194
+ def to_decimal(self, scale: int, rounding: EnumRoundingMode) -> PortableDecimal:
195
+ """
196
+ (en) Converts this value to a decimal with the specified scale,
197
+ rounding the exact value once.
198
+
199
+ (ja) この値を指定したscaleの10進数に変換します。正確な値を1回だけ丸めます。
200
+
201
+ Parameters
202
+ ----------
203
+ scale: int
204
+ The number of digits after the decimal point. Must be 0 or more.
205
+ rounding: EnumRoundingMode
206
+ The rounding mode.
207
+
208
+ Returns
209
+ -------
210
+ PortableDecimal
211
+ The converted value.
212
+ """
213
+ _check_int(scale, "scale")
214
+ if scale < 0:
215
+ raise ValueError(f"scale must be 0 or more, got {scale}")
216
+ return PortableDecimal(round_div(self._numerator * 10 ** scale, self._denominator, rounding), scale)
217
+
218
+ def to_float(self) -> float:
219
+ """
220
+ (en) Converts this value to the nearest double (ties to even).
221
+ Overflow returns +-inf. The result is identical to toDouble in Dart.
222
+
223
+ (ja) この値を最も近い倍精度浮動小数点数に変換します(同距離は偶数側)。
224
+ 範囲外の場合は +-inf を返します。結果はDart版の toDouble と同一です。
225
+
226
+ Returns
227
+ -------
228
+ float
229
+ The converted value.
230
+ """
231
+ return ratio_to_float(self._numerator, self._denominator)
232
+
233
+ def to_string(self) -> str:
234
+ """
235
+ (en) Returns "n/d", or "n" when the denominator is 1.
236
+
237
+ (ja) "n/d" を返します。分母が1の場合は "n" を返します。
238
+
239
+ Returns
240
+ -------
241
+ str
242
+ The string representation.
243
+ """
244
+ n = int_to_digits(abs(self._numerator))
245
+ if self._numerator < 0:
246
+ n = "-" + n
247
+ if self._denominator == 1:
248
+ return n
249
+ return n + "/" + int_to_digits(self._denominator)
250
+
251
+ def __str__(self) -> str:
252
+ return self.to_string()
253
+
254
+ def __repr__(self) -> str:
255
+ return f"PortableRational('{self.to_string()}')"
@@ -0,0 +1,135 @@
1
+ # coding: utf-8
2
+ import math
3
+
4
+ from simple_portable_numeric.enum_rounding_mode import EnumRoundingMode
5
+
6
+
7
+ def round_div(n: int, d: int, rounding: EnumRoundingMode) -> int:
8
+ """
9
+ (en) Divides n by d exactly and rounds the quotient to an integer once.
10
+
11
+ (ja) n を d で正確に割り、商を1回だけ丸めて整数にします。
12
+
13
+ Parameters
14
+ ----------
15
+ n: int
16
+ The dividend.
17
+ d: int
18
+ The divisor. Must not be zero.
19
+ rounding: EnumRoundingMode
20
+ The rounding mode.
21
+
22
+ Returns
23
+ -------
24
+ int
25
+ The rounded quotient.
26
+ """
27
+ if d == 0:
28
+ raise ZeroDivisionError("Division by zero")
29
+ if d < 0:
30
+ n, d = -n, -d
31
+ q, r = divmod(n, d) # q = floor(n / d), 0 <= r < d
32
+ if r == 0:
33
+ return q
34
+ if rounding is EnumRoundingMode.floor:
35
+ return q
36
+ if rounding is EnumRoundingMode.ceiling:
37
+ return q + 1
38
+ if rounding is EnumRoundingMode.down:
39
+ return q if n >= 0 else q + 1
40
+ twice = 2 * r
41
+ if twice < d:
42
+ return q
43
+ if twice > d:
44
+ return q + 1
45
+ if rounding is EnumRoundingMode.halfUp:
46
+ return q + 1 if n >= 0 else q
47
+ if rounding is EnumRoundingMode.halfEven:
48
+ return q if q % 2 == 0 else q + 1
49
+ raise ValueError(f"Unsupported rounding mode: {rounding}")
50
+
51
+
52
+ def ratio_to_float(n: int, d: int) -> float:
53
+ """
54
+ (en) Converts the exact ratio n / d to the nearest IEEE 754 double
55
+ (ties to even). Overflow returns +-inf.
56
+
57
+ (ja) 正確な比 n / d を最も近いIEEE 754倍精度浮動小数点数に変換します
58
+ (同距離は偶数側)。範囲外の場合は +-inf を返します。
59
+
60
+ Parameters
61
+ ----------
62
+ n: int
63
+ The numerator.
64
+ d: int
65
+ The denominator. Must be positive.
66
+
67
+ Returns
68
+ -------
69
+ float
70
+ The correctly rounded double value.
71
+ """
72
+ try:
73
+ # CPython's int / int is correctly rounded, including subnormals.
74
+ return n / d
75
+ except OverflowError:
76
+ return math.inf if n > 0 else -math.inf
77
+
78
+
79
+ _CHUNK = 1000
80
+ _CHUNK_BASE = 10 ** _CHUNK
81
+
82
+
83
+ def digits_to_int(digits: str) -> int:
84
+ """
85
+ (en) Converts a string of ASCII digits to a non-negative int.
86
+ Unlike int(), this is not limited by sys.get_int_max_str_digits(),
87
+ so the same strings are accepted as in the Dart version.
88
+
89
+ (ja) ASCII数字の文字列を非負の整数に変換します。
90
+ int() と異なり sys.get_int_max_str_digits() の制限を受けないため、
91
+ Dart版と同じ文字列を受け付けます。
92
+
93
+ Parameters
94
+ ----------
95
+ digits: str
96
+ A non-empty string consisting only of "0"-"9".
97
+
98
+ Returns
99
+ -------
100
+ int
101
+ The converted value.
102
+ """
103
+ result = 0
104
+ for i in range(0, len(digits), _CHUNK):
105
+ chunk = digits[i:i + _CHUNK]
106
+ result = result * 10 ** len(chunk) + int(chunk)
107
+ return result
108
+
109
+
110
+ def int_to_digits(value: int) -> str:
111
+ """
112
+ (en) Converts a non-negative int to a decimal digit string.
113
+ Unlike str(), this is not limited by sys.get_int_max_str_digits().
114
+
115
+ (ja) 非負の整数を10進数字の文字列に変換します。
116
+ str() と異なり sys.get_int_max_str_digits() の制限を受けません。
117
+
118
+ Parameters
119
+ ----------
120
+ value: int
121
+ A non-negative integer.
122
+
123
+ Returns
124
+ -------
125
+ str
126
+ The digit string without leading zeros ("0" for zero).
127
+ """
128
+ if value < _CHUNK_BASE:
129
+ return str(value)
130
+ chunks = []
131
+ while value >= _CHUNK_BASE:
132
+ value, low = divmod(value, _CHUNK_BASE)
133
+ chunks.append(str(low).rjust(_CHUNK, "0"))
134
+ chunks.append(str(value))
135
+ return "".join(reversed(chunks))
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: simple_portable_numeric
3
+ Version: 0.0.1
4
+ Summary: Arbitrary-precision decimal and rational arithmetic that gives identical results in Dart and Python.
5
+ Author: Masahide Mori
6
+ License-Expression: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/MasahideMori-SimpleAppli/simple_portable_numeric
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.12
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Dynamic: license-file
16
+
17
+ # simple_portable_numeric
18
+
19
+ (en)Japanese ver
20
+ is [here](https://github.com/MasahideMori-SimpleAppli/simple_portable_numeric/blob/main/python/README_JA.md).
21
+ (ja)
22
+ この解説の日本語版は[ここ](https://github.com/MasahideMori-SimpleAppli/simple_portable_numeric/blob/main/python/README_JA.md)
23
+ にあります。
24
+
25
+ ## Overview
26
+
27
+ **simple_portable_numeric is an arbitrary-precision decimal and rational arithmetic library
28
+ that gives identical results in Dart and Python.**
29
+
30
+ Both versions are built only on arbitrary-precision integers (Dart `BigInt`, Python `int`)
31
+ and follow one shared [specification](https://github.com/MasahideMori-SimpleAppli/simple_portable_numeric/blob/main/spec/SPEC.md).
32
+ Platform libraries such as Python's `decimal` (28 significant digits by default) are not used,
33
+ so the number of digits, rounding and string format never differ between the languages.
34
+
35
+ ## Features
36
+
37
+ - `PortableDecimal`: exact `+ - *` with no digit limit.
38
+ Division always takes an explicit scale and rounding mode.
39
+ - `PortableRational`: exact `+ - * /`, always kept reduced.
40
+ - Five rounding modes: `halfEven`, `halfUp`, `down`, `floor`, `ceiling`.
41
+ - Strict and stable string format, suitable for storing values as text (e.g. in a database).
42
+ - Equality by value (`1.50 == 1.5`), while the string keeps the scale (`"1.50"`).
43
+ - Correctly rounded conversion to double, identical in both languages.
44
+ - No dependencies.
45
+ - Dart version available.
46
+ → https://pub.dev/packages/simple_portable_numeric
47
+
48
+ ## Quick Start
49
+
50
+ ```python
51
+ from simple_portable_numeric import EnumRoundingMode, PortableDecimal, PortableRational
52
+
53
+ # Decimal
54
+ a = PortableDecimal.parse("1.50")
55
+ b = PortableDecimal.parse("2.3")
56
+ print(a + b) # 3.80
57
+ print(a * b) # 3.450
58
+ print(a == PortableDecimal.parse("1.5")) # True
59
+ print(PortableDecimal.parse("1").divide(
60
+ PortableDecimal.parse("3"), 5, EnumRoundingMode.halfEven)) # 0.33333
61
+ print(PortableDecimal.parse("2.5").set_scale(0, EnumRoundingMode.halfEven)) # 2
62
+
63
+ # Rational
64
+ r = PortableRational.parse("1/3") + PortableRational.parse("1/6")
65
+ print(r) # 1/2
66
+ print(r.to_decimal(2, EnumRoundingMode.halfUp)) # 0.50
67
+ print(PortableRational.parse("0.75")) # 3/4
68
+
69
+ # Conversion
70
+ print(PortableDecimal.parse("0.1").to_rational()) # 1/10
71
+ print(PortableRational.parse("1/3").to_float()) # 0.3333333333333333
72
+ ```
73
+
74
+ ## Notes
75
+
76
+ - Strings must be plain notation such as `"-12.340"` or `"3/4"`.
77
+ Exponent notation (`"1e3"`), whitespace and non-ASCII digits are rejected.
78
+ - `PortableDecimal` and `PortableRational` are never mixed implicitly.
79
+ Convert explicitly with `to_rational()` / `to_decimal(scale, rounding)`.
80
+ - Conversion from double is not provided, because the string forms of doubles differ between languages.
81
+ - Hash values are consistent with equality, but are not the same across languages.
82
+
83
+ ## Support
84
+
85
+ This package is developed and maintained by me personally as an open-source project.
86
+ For bug reports and feature requests, please use GitHub Issues.
87
+
88
+ If you need **paid support, consulting, or custom development**
89
+ (e.g. priority support, design advice, or implementation help),
90
+ please contact my company:
91
+
92
+ **SimpleAppli Inc.**
93
+ https://simpleappli.com/en/index_en.html
94
+
95
+ ## About version control
96
+
97
+ The C part will be changed at the time of version upgrade.
98
+ However, versions less than 1.0.0 may change the calculation results or the string format regardless of the following rules.
99
+
100
+ - Specification changes that change the calculation results or the string format.
101
+ - C.X.X
102
+ - Adding methods, etc.
103
+ - X.C.X
104
+ - Minor changes and bug fixes.
105
+ - X.X.C
106
+
107
+ ## License
108
+
109
+ This software is released under the Apache-2.0 License, see LICENSE file.
110
+
111
+ Copyright 2026 Masahide Mori
112
+
113
+ Licensed under the Apache License, Version 2.0 (the "License");
114
+ you may not use this file except in compliance with the License.
115
+ You may obtain a copy of the License at
116
+
117
+ http://www.apache.org/licenses/LICENSE-2.0
118
+
119
+ Unless required by applicable law or agreed to in writing, software
120
+ distributed under the License is distributed on an "AS IS" BASIS,
121
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
122
+ See the License for the specific language governing permissions and
123
+ limitations under the License.
124
+
125
+ ## Trademarks
126
+
127
+ - “Dart” and “Flutter” are trademarks of Google LLC.
128
+ *This package is not developed or endorsed by Google LLC.*
129
+
130
+ - “Python” is a trademark of the Python Software Foundation.
131
+ *This package is not affiliated with the Python Software Foundation.*
132
+
133
+ - GitHub and the GitHub logo are trademarks of GitHub, Inc.
134
+ *This package is not affiliated with GitHub, Inc.*
@@ -0,0 +1,10 @@
1
+ simple_portable_numeric/__init__.py,sha256=vE_i7vY7XlgWrHM6cFAwSkt6U2ZPCj2MTlzMfIHK1O4,299
2
+ simple_portable_numeric/enum_rounding_mode.py,sha256=g4E6t8VOVHh93t3skGNjpTemmBT7nVqEwpokTFUSFj0,640
3
+ simple_portable_numeric/portable_decimal.py,sha256=Uu97PGvlSSTH54czeM0R6yzqQ58et-MfnQH7v_kc1sQ,11142
4
+ simple_portable_numeric/portable_rational.py,sha256=HDuZn0uwmG4uHt9ikRnqebasgdQ-BCh0gexz8KtDr44,8924
5
+ simple_portable_numeric/util_int_math.py,sha256=dV5Ra1WbM06M1HU6T-ca1TM4DozwvasqvcOG1N88jZs,3671
6
+ simple_portable_numeric-0.0.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
7
+ simple_portable_numeric-0.0.1.dist-info/METADATA,sha256=hfCAMWlqTYtj0vorU_1Lc6BVBaxXm02UVHxRiqAj0VY,5228
8
+ simple_portable_numeric-0.0.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
9
+ simple_portable_numeric-0.0.1.dist-info/top_level.txt,sha256=HDOOBDTyY861z8xHL0INab2vDlaRojz5qrP4QhKegoc,24
10
+ simple_portable_numeric-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
@@ -0,0 +1 @@
1
+ simple_portable_numeric