flexfloat 0.1.0__tar.gz → 0.1.1__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.
- {flexfloat-0.1.0 → flexfloat-0.1.1}/PKG-INFO +1 -1
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat/__init__.py +1 -1
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat/bitarray.py +12 -8
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat/core.py +89 -39
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat.egg-info/PKG-INFO +1 -1
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat.egg-info/SOURCES.txt +1 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/pyproject.toml +6 -1
- {flexfloat-0.1.0 → flexfloat-0.1.1}/tests/test_addition.py +1 -1
- {flexfloat-0.1.0 → flexfloat-0.1.1}/tests/test_bigfloat.py +10 -5
- {flexfloat-0.1.0 → flexfloat-0.1.1}/tests/test_bitarray.py +9 -11
- {flexfloat-0.1.0 → flexfloat-0.1.1}/tests/test_conversions.py +2 -2
- {flexfloat-0.1.0 → flexfloat-0.1.1}/tests/test_division.py +2 -1
- flexfloat-0.1.1/tests/test_str_representation.py +280 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/tests/test_subtraction.py +8 -6
- {flexfloat-0.1.0 → flexfloat-0.1.1}/README.md +0 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat/py.typed +0 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat/types.py +0 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat.egg-info/dependency_links.txt +0 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat.egg-info/requires.txt +0 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/flexfloat.egg-info/top_level.txt +0 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/setup.cfg +0 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/setup.py +0 -0
- {flexfloat-0.1.0 → flexfloat-0.1.1}/tests/test_multiplication.py +0 -0
@@ -9,8 +9,8 @@ from typing import Iterator, overload
|
|
9
9
|
class BitArray:
|
10
10
|
"""A bit array class that encapsulates a list of booleans with utility methods.
|
11
11
|
|
12
|
-
This class provides all the functionality previously available through utility
|
13
|
-
now encapsulated as methods for better object-oriented design.
|
12
|
+
This class provides all the functionality previously available through utility
|
13
|
+
functions, now encapsulated as methods for better object-oriented design.
|
14
14
|
"""
|
15
15
|
|
16
16
|
def __init__(self, bits: list[bool] | None = None):
|
@@ -119,7 +119,8 @@ class BitArray:
|
|
119
119
|
return sum((1 << i) for i, bit in enumerate(reversed(self._bits)) if bit)
|
120
120
|
|
121
121
|
def to_signed_int(self) -> int:
|
122
|
-
"""Convert a bit array into a signed integer using off-set binary
|
122
|
+
"""Convert a bit array into a signed integer using off-set binary
|
123
|
+
representation.
|
123
124
|
|
124
125
|
Returns:
|
125
126
|
int: The signed integer represented by the bit array.
|
@@ -137,13 +138,16 @@ class BitArray:
|
|
137
138
|
def shift(self, shift_amount: int, fill: bool = False) -> BitArray:
|
138
139
|
"""Shift the bit array left or right by a specified number of bits.
|
139
140
|
|
140
|
-
This function shifts the bits in the array, filling in new bits with the
|
141
|
-
|
142
|
-
If the
|
141
|
+
This function shifts the bits in the array, filling in new bits with the
|
142
|
+
specified fill value.
|
143
|
+
If the value is positive, it shifts left; if negative, it shifts right.
|
144
|
+
Fills the new bits with the specified fill value (default is False).
|
143
145
|
|
144
146
|
Args:
|
145
|
-
shift_amount (int): The number of bits to shift. Positive for left shift,
|
146
|
-
|
147
|
+
shift_amount (int): The number of bits to shift. Positive for left shift,
|
148
|
+
negative for right shift.
|
149
|
+
fill (bool): The value to fill in the new bits created by the shift.
|
150
|
+
Defaults to False.
|
147
151
|
Returns:
|
148
152
|
BitArray: A new BitArray with the bits shifted and filled.
|
149
153
|
"""
|
@@ -2,22 +2,30 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import math
|
6
|
+
from typing import Final
|
7
|
+
|
5
8
|
from .bitarray import BitArray
|
6
9
|
from .types import Number
|
7
10
|
|
11
|
+
LOG10_2: Final[float] = math.log10(2)
|
12
|
+
|
8
13
|
|
9
14
|
class FlexFloat:
|
10
|
-
"""A class to represent a floating-point number with growable exponent and
|
11
|
-
This class is designed to handle very large or very
|
12
|
-
|
15
|
+
"""A class to represent a floating-point number with growable exponent and
|
16
|
+
fixed-size fraction. This class is designed to handle very large or very
|
17
|
+
small numbers by adjusting the exponent dynamically. While keeping the
|
18
|
+
mantissa (fraction) fixed in size.
|
13
19
|
|
14
20
|
This class follows the IEEE 754 double-precision floating-point format,
|
15
21
|
but extends it to allow for a growable exponent and a fixed-size fraction.
|
16
22
|
|
17
23
|
Attributes:
|
18
24
|
sign (bool): The sign of the number (True for negative, False for positive).
|
19
|
-
exponent (BitArray): A growable bit array representing the exponent
|
20
|
-
|
25
|
+
exponent (BitArray): A growable bit array representing the exponent
|
26
|
+
(uses off-set binary representation).
|
27
|
+
fraction (BitArray): A fixed-size bit array representing the fraction
|
28
|
+
(mantissa) of the number.
|
21
29
|
"""
|
22
30
|
|
23
31
|
def __init__(
|
@@ -30,8 +38,8 @@ class FlexFloat:
|
|
30
38
|
|
31
39
|
Args:
|
32
40
|
sign (bool): The sign of the number (True for negative, False for positive).
|
33
|
-
exponent (BitArray | None): The exponent bit array
|
34
|
-
fraction (BitArray | None): The fraction bit array
|
41
|
+
exponent (BitArray | None): The exponent bit array (If None, represents 0).
|
42
|
+
fraction (BitArray | None): The fraction bit array (If None, represents 0).
|
35
43
|
"""
|
36
44
|
self.sign = sign
|
37
45
|
self.exponent = exponent if exponent is not None else BitArray.zeros(11)
|
@@ -54,7 +62,7 @@ class FlexFloat:
|
|
54
62
|
def to_float(self) -> float:
|
55
63
|
"""Convert the FlexFloat instance back to a 64-bit float.
|
56
64
|
|
57
|
-
If float is bigger than 64 bits, it will truncate the value to fit
|
65
|
+
If float is bigger than 64 bits, it will truncate the value to fit.
|
58
66
|
|
59
67
|
Returns:
|
60
68
|
float: The floating-point number represented by the FlexFloat instance.
|
@@ -73,7 +81,12 @@ class FlexFloat:
|
|
73
81
|
Returns:
|
74
82
|
str: A string representation of the FlexFloat instance.
|
75
83
|
"""
|
76
|
-
return
|
84
|
+
return (
|
85
|
+
"FlexFloat("
|
86
|
+
f"sign={self.sign}, "
|
87
|
+
f"exponent={self.exponent}, "
|
88
|
+
f"fraction={self.fraction})"
|
89
|
+
)
|
77
90
|
|
78
91
|
def pretty(self) -> str:
|
79
92
|
"""Return an easier to read string representation of the FlexFloat instance.
|
@@ -103,7 +116,7 @@ class FlexFloat:
|
|
103
116
|
"""Create a FlexFloat instance representing Infinity.
|
104
117
|
|
105
118
|
Args:
|
106
|
-
sign (bool):
|
119
|
+
sign (bool): Indicates if the infinity is negative.
|
107
120
|
Returns:
|
108
121
|
FlexFloat: A new FlexFloat instance representing Infinity.
|
109
122
|
"""
|
@@ -130,7 +143,8 @@ class FlexFloat:
|
|
130
143
|
"""
|
131
144
|
# In IEEE 754, special values have all exponent bits set to 1
|
132
145
|
# This corresponds to the maximum value in the unsigned representation
|
133
|
-
# For signed offset binary, the maximum value is 2^(n-1) - 1
|
146
|
+
# For signed offset binary, the maximum value is 2^(n-1) - 1
|
147
|
+
# where n is the number of bits
|
134
148
|
max_signed_value = (1 << (len(self.exponent) - 1)) - 1
|
135
149
|
return self.exponent.to_signed_int() == max_signed_value
|
136
150
|
|
@@ -162,36 +176,60 @@ class FlexFloat:
|
|
162
176
|
"""Create a copy of the FlexFloat instance.
|
163
177
|
|
164
178
|
Returns:
|
165
|
-
FlexFloat: A new FlexFloat instance with the same
|
179
|
+
FlexFloat: A new FlexFloat instance with the same data as the original.
|
166
180
|
"""
|
167
181
|
return FlexFloat(
|
168
182
|
sign=self.sign, exponent=self.exponent.copy(), fraction=self.fraction.copy()
|
169
183
|
)
|
170
184
|
|
171
185
|
def __str__(self) -> str:
|
172
|
-
"""Float representation of the FlexFloat.
|
173
|
-
|
186
|
+
"""Float representation of the FlexFloat using a generic algorithm.
|
187
|
+
|
188
|
+
This implementation doesn't rely on Python's float conversion and instead
|
189
|
+
implements the formatting logic directly, making it work for any exponent size.
|
190
|
+
|
191
|
+
Currently, it only operates in scientific notation with 5 decimal places.
|
192
|
+
"""
|
193
|
+
sign_str = "-" if self.sign else ""
|
194
|
+
# Handle special cases first
|
195
|
+
if self.is_nan():
|
196
|
+
return "nan"
|
197
|
+
|
198
|
+
if self.is_infinity():
|
199
|
+
return f"{sign_str}inf"
|
174
200
|
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
return f"{sign}Infinity"
|
184
|
-
|
185
|
-
fraction_value: float = 1
|
201
|
+
if self.is_zero():
|
202
|
+
return f"{sign_str}0.00000e+00"
|
203
|
+
|
204
|
+
exponent = self.exponent.to_signed_int() + 1
|
205
|
+
|
206
|
+
# Convert fraction to decimal value between 1 and 2
|
207
|
+
# (starting with 1.0 for the implicit leading bit)
|
208
|
+
mantissa = 1.0
|
186
209
|
for i, bit in enumerate(self.fraction):
|
187
210
|
if bit:
|
188
|
-
|
211
|
+
mantissa += 1.0 / (1 << (i + 1))
|
212
|
+
|
213
|
+
# To avoid overflow with very large exponents, work in log space
|
214
|
+
# log10(mantissa * 2^exponent) = log10(mantissa) + exponent * log10(2)
|
215
|
+
log10_mantissa = math.log10(mantissa)
|
216
|
+
log10_total = log10_mantissa + exponent * LOG10_2
|
217
|
+
|
218
|
+
decimal_exponent = int(log10_total)
|
219
|
+
|
220
|
+
log10_normalized = log10_total - decimal_exponent
|
221
|
+
normalized_mantissa = math.pow(10, log10_normalized)
|
189
222
|
|
190
|
-
|
191
|
-
|
223
|
+
# Ensure the mantissa is properly normalized (between 1.0 and 10.0)
|
224
|
+
while normalized_mantissa >= 10.0:
|
225
|
+
normalized_mantissa /= 10.0
|
226
|
+
decimal_exponent += 1
|
227
|
+
while normalized_mantissa < 1.0:
|
228
|
+
normalized_mantissa *= 10.0
|
229
|
+
decimal_exponent -= 1
|
192
230
|
|
193
|
-
#
|
194
|
-
return ""
|
231
|
+
# Format with 5 decimal places
|
232
|
+
return f"{sign_str}{normalized_mantissa:.5f}e{decimal_exponent:+03d}"
|
195
233
|
|
196
234
|
def __neg__(self) -> FlexFloat:
|
197
235
|
"""Negate the FlexFloat instance."""
|
@@ -209,7 +247,8 @@ class FlexFloat:
|
|
209
247
|
exponent (int): The current exponent value.
|
210
248
|
exponent_length (int): The current length of the exponent in bits.
|
211
249
|
Returns:
|
212
|
-
int: The new exponent length if it needs to be grown, otherwise the same
|
250
|
+
int: The new exponent length if it needs to be grown, otherwise the same
|
251
|
+
length.
|
213
252
|
"""
|
214
253
|
while True:
|
215
254
|
half = 1 << (exponent_length - 1)
|
@@ -239,7 +278,7 @@ class FlexFloat:
|
|
239
278
|
return self - (-other)
|
240
279
|
|
241
280
|
# OBJECTIVE: Add two FlexFloat instances together.
|
242
|
-
#
|
281
|
+
# https://www.sciencedirect.com/topics/computer-science/floating-point-addition
|
243
282
|
# and: https://cse.hkust.edu.hk/~cktang/cs180/notes/lec21.pdf
|
244
283
|
#
|
245
284
|
# Steps:
|
@@ -285,8 +324,13 @@ class FlexFloat:
|
|
285
324
|
mantissa_other = mantissa_other.shift(shift_amount)
|
286
325
|
|
287
326
|
# Step 5: Add mantissas
|
288
|
-
assert
|
289
|
-
|
327
|
+
assert (
|
328
|
+
len(mantissa_self) == 53
|
329
|
+
), "Fraction must be 53 bits long. (1 leading bit + 52 fraction bits)"
|
330
|
+
assert len(mantissa_self) == len(mantissa_other), (
|
331
|
+
f"Mantissas must be the same length. Expected 53 bits, "
|
332
|
+
f"got {len(mantissa_other)} bits."
|
333
|
+
)
|
290
334
|
|
291
335
|
mantissa_result = BitArray.zeros(53) # 1 leading bit + 52 fraction bits
|
292
336
|
carry = False
|
@@ -398,8 +442,13 @@ class FlexFloat:
|
|
398
442
|
result_sign = not self.sign
|
399
443
|
|
400
444
|
# Step 5: Subtract mantissas (larger - smaller)
|
401
|
-
assert
|
402
|
-
|
445
|
+
assert (
|
446
|
+
len(larger_mantissa) == 53
|
447
|
+
), "Mantissa must be 53 bits long. (1 leading bit + 52 fraction bits)"
|
448
|
+
assert len(larger_mantissa) == len(smaller_mantissa), (
|
449
|
+
f"Mantissas must be the same length. Expected 53 bits, "
|
450
|
+
f"got {len(smaller_mantissa)} bits."
|
451
|
+
)
|
403
452
|
|
404
453
|
mantissa_result = BitArray.zeros(53)
|
405
454
|
borrow = False
|
@@ -451,7 +500,7 @@ class FlexFloat:
|
|
451
500
|
raise TypeError("Can only multiply FlexFloat instances.")
|
452
501
|
|
453
502
|
# OBJECTIVE: Multiply two FlexFloat instances together.
|
454
|
-
#
|
503
|
+
# https://www.rfwireless-world.com/tutorials/ieee-754-floating-point-arithmetic
|
455
504
|
#
|
456
505
|
# Steps:
|
457
506
|
# 0. Handle special cases (NaN, Infinity, zero).
|
@@ -567,7 +616,7 @@ class FlexFloat:
|
|
567
616
|
raise TypeError("Can only divide FlexFloat instances.")
|
568
617
|
|
569
618
|
# OBJECTIVE: Divide two FlexFloat instances.
|
570
|
-
#
|
619
|
+
# https://www.rfwireless-world.com/tutorials/ieee-754-floating-point-arithmetic
|
571
620
|
#
|
572
621
|
# Steps:
|
573
622
|
# 0. Handle special cases (NaN, Infinity, zero).
|
@@ -603,7 +652,8 @@ class FlexFloat:
|
|
603
652
|
result_sign = self.sign ^ other.sign
|
604
653
|
|
605
654
|
# Step 2: Extract exponent and fraction bits
|
606
|
-
# Note: The stored exponent needs +1 to get the actual value
|
655
|
+
# Note: The stored exponent needs +1 to get the actual value
|
656
|
+
# (like in multiplication)
|
607
657
|
exponent_self = self.exponent.to_signed_int() + 1
|
608
658
|
exponent_other = other.exponent.to_signed_int() + 1
|
609
659
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "flexfloat"
|
7
|
-
version = "0.1.
|
7
|
+
version = "0.1.1"
|
8
8
|
description = "A library for arbitrary precision floating point arithmetic"
|
9
9
|
readme = "README.md"
|
10
10
|
authors = [
|
@@ -56,6 +56,11 @@ disallow_untyped_defs = true
|
|
56
56
|
|
57
57
|
[tool.pylint.main]
|
58
58
|
py-version = "3.11"
|
59
|
+
ignore-paths = [
|
60
|
+
"venv/",
|
61
|
+
"env/",
|
62
|
+
".venv/"
|
63
|
+
]
|
59
64
|
|
60
65
|
[tool.pylint.messages_control]
|
61
66
|
disable = [
|
@@ -57,7 +57,7 @@ class TestAddition(FlexFloatTestCase):
|
|
57
57
|
def test_flexfloat_addition_rejects_non_flexfloat_operands(self):
|
58
58
|
bf = FlexFloat.from_float(1.0)
|
59
59
|
with self.assertRaises(TypeError):
|
60
|
-
bf + "not a number"
|
60
|
+
bf + "not a number" # type: ignore
|
61
61
|
|
62
62
|
def test_flexfloat_addition_handles_nan_operands(self):
|
63
63
|
bf_normal = FlexFloat.from_float(1.0)
|
@@ -3,6 +3,7 @@
|
|
3
3
|
import unittest
|
4
4
|
|
5
5
|
from flexfloat import FlexFloat
|
6
|
+
from flexfloat.bitarray import BitArray
|
6
7
|
from tests import FlexFloatTestCase
|
7
8
|
|
8
9
|
|
@@ -20,8 +21,8 @@ class TestFlexFloat(FlexFloatTestCase):
|
|
20
21
|
def test_flexfloat_constructor_with_custom_values(self):
|
21
22
|
"""Test FlexFloat constructor with custom sign, exponent, and fraction."""
|
22
23
|
sign = True
|
23
|
-
exponent = [True, False] * 5 + [True] # 11 bits
|
24
|
-
fraction = [False, True] * 26 # 52 bits
|
24
|
+
exponent = BitArray([True, False] * 5 + [True]) # 11 bits
|
25
|
+
fraction = BitArray([False, True] * 26) # 52 bits
|
25
26
|
bf = FlexFloat(sign=sign, exponent=exponent, fraction=fraction)
|
26
27
|
self.assertEqual(bf.sign, sign)
|
27
28
|
self.assertEqual(bf.exponent, exponent)
|
@@ -53,14 +54,18 @@ class TestFlexFloat(FlexFloatTestCase):
|
|
53
54
|
self.assertTrue(bf_nan.is_nan())
|
54
55
|
|
55
56
|
def test_flexfloat_to_float_raises_error_on_wrong_dimensions(self):
|
56
|
-
"
|
57
|
+
"Test that to_float raises error when exponent or fraction have wrong length."
|
57
58
|
# Wrong exponent length
|
58
|
-
bf_wrong_exp = FlexFloat(
|
59
|
+
bf_wrong_exp = FlexFloat(
|
60
|
+
exponent=BitArray([False] * 10), fraction=BitArray([False] * 52)
|
61
|
+
)
|
59
62
|
with self.assertRaises(ValueError):
|
60
63
|
bf_wrong_exp.to_float()
|
61
64
|
|
62
65
|
# Wrong fraction length
|
63
|
-
bf_wrong_frac = FlexFloat(
|
66
|
+
bf_wrong_frac = FlexFloat(
|
67
|
+
exponent=BitArray([False] * 11), fraction=BitArray([False] * 51)
|
68
|
+
)
|
64
69
|
with self.assertRaises(ValueError):
|
65
70
|
bf_wrong_frac.to_float()
|
66
71
|
|
@@ -41,10 +41,8 @@ class TestBitArray(FlexFloatTestCase):
|
|
41
41
|
|
42
42
|
def test_bitarray_to_int_converts_large_number_correctly(self):
|
43
43
|
"""Test conversion of large bit array to integer."""
|
44
|
-
bit_array = BitArray(
|
45
|
-
|
46
|
-
"11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111001"
|
47
|
-
)
|
44
|
+
bit_array = BitArray.parse_bitarray(
|
45
|
+
"11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111001"
|
48
46
|
)
|
49
47
|
expected = 18446744073709551609
|
50
48
|
result = bit_array.to_int()
|
@@ -61,21 +59,21 @@ class TestBitArray(FlexFloatTestCase):
|
|
61
59
|
# === Signed Integer Conversion Tests ===
|
62
60
|
def test_bitarray_to_signed_int_converts_zero_bias_correctly(self):
|
63
61
|
"""Test signed integer conversion with zero as negative bias."""
|
64
|
-
bitarray = BitArray
|
62
|
+
bitarray = BitArray.parse_bitarray("00000000001") # 11-bit array
|
65
63
|
expected = -1023 # -2^(11-1) + 1 = -1024 + 1
|
66
64
|
result = bitarray.to_signed_int()
|
67
65
|
self.assertEqual(result, expected)
|
68
66
|
|
69
67
|
def test_bitarray_to_signed_int_converts_near_zero_correctly(self):
|
70
68
|
"""Test signed integer conversion near zero."""
|
71
|
-
bitarray = BitArray
|
69
|
+
bitarray = BitArray.parse_bitarray("01111111111") # 11-bit array
|
72
70
|
expected = -1 # -2^(11-1) + 1023 = -1024 + 1023
|
73
71
|
result = bitarray.to_signed_int()
|
74
72
|
self.assertEqual(result, expected)
|
75
73
|
|
76
74
|
def test_bitarray_to_signed_int_converts_maximum_value_correctly(self):
|
77
75
|
"""Test signed integer conversion at maximum value."""
|
78
|
-
bitarray = BitArray
|
76
|
+
bitarray = BitArray.parse_bitarray("11111111111") # 11-bit array
|
79
77
|
expected = 1023 # -2^(11-1) + 2047 = -1024 + 2047
|
80
78
|
result = bitarray.to_signed_int()
|
81
79
|
self.assertEqual(result, expected)
|
@@ -88,13 +86,13 @@ class TestBitArray(FlexFloatTestCase):
|
|
88
86
|
def test_bitarray_to_signed_int_handles_different_lengths(self):
|
89
87
|
"""Test signed integer conversion with different bit array lengths."""
|
90
88
|
# 8-bit test: bias = 2^7 = 128
|
91
|
-
bitarray_8bit = BitArray
|
89
|
+
bitarray_8bit = BitArray.parse_bitarray("10000000") # 128 in unsigned
|
92
90
|
expected_8bit = 0 # 128 - 128 = 0
|
93
91
|
result_8bit = bitarray_8bit.to_signed_int()
|
94
92
|
self.assertEqual(result_8bit, expected_8bit)
|
95
93
|
|
96
94
|
# 4-bit test: bias = 2^3 = 8
|
97
|
-
bitarray_4bit = BitArray
|
95
|
+
bitarray_4bit = BitArray.parse_bitarray("1100") # 12 in unsigned
|
98
96
|
expected_4bit = 4 # 12 - 8 = 4
|
99
97
|
result_4bit = bitarray_4bit.to_signed_int()
|
100
98
|
self.assertEqual(result_4bit, expected_4bit)
|
@@ -130,8 +128,8 @@ class TestBitArray(FlexFloatTestCase):
|
|
130
128
|
|
131
129
|
# Test values within range should work
|
132
130
|
try:
|
133
|
-
|
134
|
-
|
131
|
+
BitArray.from_signed_int(127, 8)
|
132
|
+
BitArray.from_signed_int(-128, 8)
|
135
133
|
except AssertionError:
|
136
134
|
self.fail("Valid values should not raise AssertionError")
|
137
135
|
|
@@ -86,7 +86,7 @@ class TestConversions(FlexFloatTestCase):
|
|
86
86
|
"00111111 11110000 00000000 00000000 00000000 00000000 00000000 00000000"
|
87
87
|
)
|
88
88
|
expected = 1.0
|
89
|
-
result =
|
89
|
+
result = bit_array.to_float()
|
90
90
|
self.assertEqual(result, expected)
|
91
91
|
|
92
92
|
def test_bitarray_to_float_converts_negative_number_correctly(self):
|
@@ -95,7 +95,7 @@ class TestConversions(FlexFloatTestCase):
|
|
95
95
|
"11000010 10101100 10111000 01100010 00110000 10011110 00101010 00000000"
|
96
96
|
)
|
97
97
|
expected = -15789123456789.0
|
98
|
-
result =
|
98
|
+
result = bit_array.to_float()
|
99
99
|
self.assertEqual(result, expected)
|
100
100
|
|
101
101
|
def test_bitarray_to_float_raises_error_on_wrong_length(self):
|
@@ -200,7 +200,8 @@ class TestDivision(FlexFloatTestCase):
|
|
200
200
|
# Alternatively, we can check if it matches the expected mathematical result
|
201
201
|
# within a reasonable tolerance for very small numbers
|
202
202
|
if expected == 0.0:
|
203
|
-
# If the expected result underflows to zero, accept either zero or very
|
203
|
+
# If the expected result underflows to zero, accept either zero or very
|
204
|
+
# small result
|
204
205
|
self.assertTrue(result.to_float() < 1e-100 or result.is_zero())
|
205
206
|
else:
|
206
207
|
# Check relative error for non-zero expected results
|
@@ -0,0 +1,280 @@
|
|
1
|
+
"""Tests for FlexFloat string representation (__str__ method)."""
|
2
|
+
|
3
|
+
import math
|
4
|
+
import unittest
|
5
|
+
|
6
|
+
from flexfloat import BitArray, FlexFloat
|
7
|
+
from tests import FlexFloatTestCase
|
8
|
+
|
9
|
+
|
10
|
+
class TestStrRepresentation(FlexFloatTestCase):
|
11
|
+
"""Test FlexFloat string representation operations."""
|
12
|
+
|
13
|
+
def test_str_zero_returns_correct_format(self):
|
14
|
+
"""Test that zero is represented as '0.0'."""
|
15
|
+
ff = FlexFloat.from_float(0.0)
|
16
|
+
expected = f"{0.0:.5e}"
|
17
|
+
self.assertEqual(str(ff), expected)
|
18
|
+
|
19
|
+
def test_str_negative_zero_returns_correct_format(self):
|
20
|
+
"""Test that negative zero is represented as '0.0'."""
|
21
|
+
ff = FlexFloat.from_float(-0.0)
|
22
|
+
expected = f"{-0.0:.5e}"
|
23
|
+
self.assertEqual(str(ff), expected)
|
24
|
+
|
25
|
+
def test_str_one_returns_correct_format(self):
|
26
|
+
"""Test that 1.0 is represented correctly."""
|
27
|
+
ff = FlexFloat.from_float(1.0)
|
28
|
+
expected = f"{1.0:.5e}"
|
29
|
+
self.assertEqual(str(ff), expected)
|
30
|
+
|
31
|
+
def test_str_negative_one_returns_correct_format(self):
|
32
|
+
"""Test that -1.0 is represented correctly."""
|
33
|
+
ff = FlexFloat.from_float(-1.0)
|
34
|
+
expected = f"{-1.0:.5e}"
|
35
|
+
self.assertEqual(str(ff), expected)
|
36
|
+
|
37
|
+
def test_str_small_decimal_returns_correct_format(self):
|
38
|
+
"""Test that small decimal numbers are represented correctly."""
|
39
|
+
ff = FlexFloat.from_float(0.5)
|
40
|
+
expected = f"{0.5:.5e}"
|
41
|
+
self.assertEqual(str(ff), expected)
|
42
|
+
|
43
|
+
ff = FlexFloat.from_float(0.25)
|
44
|
+
expected = f"{0.25:.5e}"
|
45
|
+
self.assertEqual(str(ff), expected)
|
46
|
+
|
47
|
+
ff = FlexFloat.from_float(0.125)
|
48
|
+
expected = f"{0.125:.5e}"
|
49
|
+
self.assertEqual(str(ff), expected)
|
50
|
+
|
51
|
+
def test_str_large_integers_returns_correct_format(self):
|
52
|
+
"""Test that large integers are represented correctly."""
|
53
|
+
ff = FlexFloat.from_float(42.0)
|
54
|
+
expected = f"{42.0:.5e}"
|
55
|
+
self.assertEqual(str(ff), expected)
|
56
|
+
|
57
|
+
ff = FlexFloat.from_float(1000.0)
|
58
|
+
expected = f"{1000.0:.5e}"
|
59
|
+
self.assertEqual(str(ff), expected)
|
60
|
+
|
61
|
+
ff = FlexFloat.from_float(123456.0)
|
62
|
+
expected = f"{123456.0:.5e}"
|
63
|
+
self.assertEqual(str(ff), expected)
|
64
|
+
|
65
|
+
def test_str_decimal_numbers_returns_correct_format(self):
|
66
|
+
"""Test that decimal numbers are represented correctly."""
|
67
|
+
ff = FlexFloat.from_float(3.14159)
|
68
|
+
expected = f"{3.14159:.5e}"
|
69
|
+
self.assertEqual(str(ff), expected)
|
70
|
+
|
71
|
+
ff = FlexFloat.from_float(2.718281828)
|
72
|
+
expected = f"{2.718281828:.5e}"
|
73
|
+
self.assertEqual(str(ff), expected)
|
74
|
+
|
75
|
+
def test_str_very_small_numbers_uses_scientific_notation(self):
|
76
|
+
"""Test that very small numbers use scientific notation."""
|
77
|
+
ff = FlexFloat.from_float(1e-5)
|
78
|
+
expected = f"{1e-5:.5e}"
|
79
|
+
self.assertEqual(str(ff), expected)
|
80
|
+
|
81
|
+
ff = FlexFloat.from_float(1e-10)
|
82
|
+
expected = f"{1e-10:.5e}"
|
83
|
+
self.assertEqual(str(ff), expected)
|
84
|
+
|
85
|
+
ff = FlexFloat.from_float(1.23e-6)
|
86
|
+
expected = f"{1.23e-6:.5e}"
|
87
|
+
self.assertEqual(str(ff), expected)
|
88
|
+
|
89
|
+
def test_str_very_large_numbers_uses_scientific_notation(self):
|
90
|
+
"""Test that very large numbers use scientific notation."""
|
91
|
+
ff = FlexFloat.from_float(1e20)
|
92
|
+
expected = f"{1e20:.5e}"
|
93
|
+
self.assertEqual(str(ff), expected)
|
94
|
+
|
95
|
+
ff = FlexFloat.from_float(1.5e25)
|
96
|
+
expected = f"{1.5e25:.5e}"
|
97
|
+
self.assertEqual(str(ff), expected)
|
98
|
+
|
99
|
+
def test_str_special_values(self):
|
100
|
+
"""Test that special values (NaN, Infinity) are represented correctly."""
|
101
|
+
# Test NaN
|
102
|
+
ff = FlexFloat.nan()
|
103
|
+
self.assertEqual(str(ff), "nan")
|
104
|
+
|
105
|
+
# Test positive infinity
|
106
|
+
ff = FlexFloat.infinity(sign=False)
|
107
|
+
self.assertEqual(str(ff), "inf")
|
108
|
+
|
109
|
+
# Test negative infinity
|
110
|
+
ff = FlexFloat.infinity(sign=True)
|
111
|
+
self.assertEqual(str(ff), "-inf")
|
112
|
+
|
113
|
+
def test_str_negative_numbers(self):
|
114
|
+
"""Test that negative numbers are represented correctly."""
|
115
|
+
ff = FlexFloat.from_float(-3.14159)
|
116
|
+
expected = f"{-3.14159:.5e}"
|
117
|
+
self.assertEqual(str(ff), expected)
|
118
|
+
|
119
|
+
ff = FlexFloat.from_float(-42.0)
|
120
|
+
expected = f"{-42.0:.5e}"
|
121
|
+
self.assertEqual(str(ff), expected)
|
122
|
+
|
123
|
+
ff = FlexFloat.from_float(-1e-5)
|
124
|
+
expected = f"{-1e-5:.5e}"
|
125
|
+
self.assertEqual(str(ff), expected)
|
126
|
+
|
127
|
+
def test_str_edge_cases(self):
|
128
|
+
"""Test edge cases for string representation."""
|
129
|
+
# Test minimum positive normalized number
|
130
|
+
ff = FlexFloat.from_float(2.2250738585072014e-308)
|
131
|
+
expected = f"{2.2250738585072014e-308:.5e}"
|
132
|
+
self.assertEqual(str(ff), expected)
|
133
|
+
|
134
|
+
# Test maximum finite number
|
135
|
+
ff = FlexFloat.from_float(1.7976931348623157e308)
|
136
|
+
expected = f"{1.7976931348623157e308:.5e}"
|
137
|
+
self.assertEqual(str(ff), expected)
|
138
|
+
|
139
|
+
def test_str_consistency_with_python_float(self):
|
140
|
+
"""Test that FlexFloat str() is consistent with Python float str()."""
|
141
|
+
test_values = [
|
142
|
+
0.0,
|
143
|
+
1.0,
|
144
|
+
-1.0,
|
145
|
+
0.5,
|
146
|
+
-0.5,
|
147
|
+
3.14159,
|
148
|
+
-3.14159,
|
149
|
+
42.0,
|
150
|
+
-42.0,
|
151
|
+
1000.0,
|
152
|
+
-1000.0,
|
153
|
+
1e-5,
|
154
|
+
-1e-5,
|
155
|
+
1e-10,
|
156
|
+
-1e-10,
|
157
|
+
1e20,
|
158
|
+
-1e20,
|
159
|
+
1.5e25,
|
160
|
+
-1.5e25,
|
161
|
+
123.456789,
|
162
|
+
-123.456789,
|
163
|
+
0.000123456789,
|
164
|
+
-0.000123456789,
|
165
|
+
]
|
166
|
+
|
167
|
+
for value in test_values:
|
168
|
+
with self.subTest(value=value):
|
169
|
+
ff = FlexFloat.from_float(value)
|
170
|
+
python_str = f"{value:.5e}"
|
171
|
+
flexfloat_str = str(ff)
|
172
|
+
self.assertEqual(
|
173
|
+
flexfloat_str,
|
174
|
+
python_str,
|
175
|
+
f"FlexFloat str({value}) = '{flexfloat_str}' != "
|
176
|
+
f"Python str({value}) = '{python_str}'",
|
177
|
+
)
|
178
|
+
|
179
|
+
def test_str_mathematical_constants(self):
|
180
|
+
"""Test string representation of mathematical constants."""
|
181
|
+
# Test pi
|
182
|
+
ff = FlexFloat.from_float(math.pi)
|
183
|
+
expected = f"{math.pi:.5e}"
|
184
|
+
self.assertEqual(str(ff), expected)
|
185
|
+
|
186
|
+
# Test e
|
187
|
+
ff = FlexFloat.from_float(math.e)
|
188
|
+
expected = f"{math.e:.5e}"
|
189
|
+
self.assertEqual(str(ff), expected)
|
190
|
+
|
191
|
+
# Test sqrt(2)
|
192
|
+
sqrt2 = math.sqrt(2)
|
193
|
+
ff = FlexFloat.from_float(sqrt2)
|
194
|
+
expected = f"{sqrt2:.5e}"
|
195
|
+
self.assertEqual(str(ff), expected)
|
196
|
+
|
197
|
+
def test_str_powers_of_two(self):
|
198
|
+
"""Test string representation of powers of two."""
|
199
|
+
for i in range(-10, 11):
|
200
|
+
value = 2.0**i
|
201
|
+
with self.subTest(power=i, value=value):
|
202
|
+
ff = FlexFloat.from_float(value)
|
203
|
+
expected = f"{value:.5e}"
|
204
|
+
self.assertEqual(str(ff), expected)
|
205
|
+
|
206
|
+
def test_str_powers_of_ten(self):
|
207
|
+
"""Test string representation of powers of ten."""
|
208
|
+
for i in range(-5, 6):
|
209
|
+
value = 10.0**i
|
210
|
+
with self.subTest(power=i, value=value):
|
211
|
+
ff = FlexFloat.from_float(value)
|
212
|
+
expected = f"{value:.5e}"
|
213
|
+
self.assertEqual(str(ff), expected)
|
214
|
+
|
215
|
+
def test_str_fractional_numbers(self):
|
216
|
+
"""Test string representation of various fractional numbers."""
|
217
|
+
fractions = [
|
218
|
+
1.0 / 3.0, # 0.3333...
|
219
|
+
1.0 / 7.0, # 0.142857...
|
220
|
+
22.0 / 7.0, # approximation of pi
|
221
|
+
355.0 / 113.0, # better approximation of pi
|
222
|
+
]
|
223
|
+
|
224
|
+
for value in fractions:
|
225
|
+
with self.subTest(value=value):
|
226
|
+
ff = FlexFloat.from_float(value)
|
227
|
+
expected = f"{value:.5e}"
|
228
|
+
self.assertEqual(str(ff), expected)
|
229
|
+
|
230
|
+
def test_str_extended_exponent_numbers(self):
|
231
|
+
"""Test string representation of numbers with extended exponents."""
|
232
|
+
# Test with large exponent
|
233
|
+
extended_exp = BitArray.from_signed_int(500, 12) # 12-bit exponent
|
234
|
+
frac = BitArray.zeros(52)
|
235
|
+
ff = FlexFloat(sign=False, exponent=extended_exp, fraction=frac)
|
236
|
+
result = str(ff)
|
237
|
+
# This represents 2^501, which in decimal scientific notation is ~6.54678e+150
|
238
|
+
self.assertEqual(result, "6.54678e+150")
|
239
|
+
|
240
|
+
# Test with negative large exponent
|
241
|
+
extended_exp = BitArray.from_signed_int(-500, 12) # 12-bit exponent
|
242
|
+
ff = FlexFloat(sign=False, exponent=extended_exp, fraction=frac)
|
243
|
+
result = str(ff)
|
244
|
+
# This represents 2^(-499), which in decimal scientific notation is 6.10987e-151
|
245
|
+
self.assertEqual(result, "6.10987e-151")
|
246
|
+
|
247
|
+
# Test with negative sign and extended exponent
|
248
|
+
ff = FlexFloat(sign=True, exponent=extended_exp, fraction=frac)
|
249
|
+
result = str(ff)
|
250
|
+
self.assertEqual(result, "-6.10987e-151")
|
251
|
+
|
252
|
+
def test_str_extreme_exponent_numbers(self):
|
253
|
+
"""Test string representation of numbers with extreme exponents."""
|
254
|
+
# Test with very large exponent that causes overflow
|
255
|
+
extended_exp = BitArray.from_signed_int(2000, 15) # 15-bit exponent
|
256
|
+
frac = BitArray.zeros(52)
|
257
|
+
ff = FlexFloat(sign=False, exponent=extended_exp, fraction=frac)
|
258
|
+
result = str(ff)
|
259
|
+
# This represents 2^2001, which in decimal scientific notation is ~2.29626e+602
|
260
|
+
self.assertTrue("e+" in result)
|
261
|
+
self.assertIn("e+602", result)
|
262
|
+
|
263
|
+
def test_str_extended_precision_arithmetic_results(self):
|
264
|
+
"""Test string representation after arithmetic with extended precision."""
|
265
|
+
# Create very large numbers that will result in extended exponents
|
266
|
+
ff1 = FlexFloat.from_float(1e150)
|
267
|
+
ff2 = FlexFloat.from_float(1e150)
|
268
|
+
|
269
|
+
# Multiplication should create a very large result
|
270
|
+
result = ff1 * ff2
|
271
|
+
result_str = str(result)
|
272
|
+
|
273
|
+
# Should be in scientific notation
|
274
|
+
self.assertTrue("e+" in result_str)
|
275
|
+
# Should show approximately 1e300
|
276
|
+
self.assertTrue(result_str.startswith("1.00000e+"))
|
277
|
+
|
278
|
+
|
279
|
+
if __name__ == "__main__":
|
280
|
+
unittest.main()
|
@@ -81,7 +81,7 @@ class TestSubtraction(FlexFloatTestCase):
|
|
81
81
|
def test_flexfloat_subtraction_rejects_non_flexfloat_operands(self):
|
82
82
|
bf = FlexFloat.from_float(1.0)
|
83
83
|
with self.assertRaises(TypeError):
|
84
|
-
bf - "not a number"
|
84
|
+
bf - "not a number" # type: ignore
|
85
85
|
|
86
86
|
def test_flexfloat_subtraction_handles_nan_operands(self):
|
87
87
|
bf_normal = FlexFloat.from_float(1.0)
|
@@ -170,8 +170,10 @@ class TestSubtraction(FlexFloatTestCase):
|
|
170
170
|
self.assertGreater(result2.to_float(), 0)
|
171
171
|
|
172
172
|
def test_flexfloat_subtraction_exponent_growth_on_underflow(self):
|
173
|
-
"""Test that subtraction causes exponent growth on underflow instead of going
|
174
|
-
|
173
|
+
"""Test that subtraction causes exponent growth on underflow instead of going
|
174
|
+
to zero like normal float."""
|
175
|
+
# Test case 1: Subtracting very close small numbers that result in extreme
|
176
|
+
# underflow
|
175
177
|
# Create two very small numbers that are very close to each other
|
176
178
|
small1 = FlexFloat.from_float(1e-300)
|
177
179
|
small2 = FlexFloat.from_float(9.99999999999999e-301) # Very close to small1
|
@@ -192,7 +194,7 @@ class TestSubtraction(FlexFloatTestCase):
|
|
192
194
|
self.assertFalse(result.is_nan())
|
193
195
|
|
194
196
|
# Test case 2: Subtraction that causes normalization shift and underflow
|
195
|
-
# Create numbers where
|
197
|
+
# Create numbers where sub leads to significant leading zero cancellation
|
196
198
|
num1 = FlexFloat.from_float(1.0000000000000002) # Very close to 1.0
|
197
199
|
num2 = FlexFloat.from_float(1.0)
|
198
200
|
|
@@ -226,8 +228,8 @@ class TestSubtraction(FlexFloatTestCase):
|
|
226
228
|
# The result should not be zero and should require exponent growth
|
227
229
|
self.assertFalse(result3.is_zero())
|
228
230
|
|
229
|
-
# Check that the result's exponent is longer than the original standard 11-bit
|
230
|
-
# to handle the extreme underflow scenario
|
231
|
+
# Check that the result's exponent is longer than the original standard 11-bit
|
232
|
+
# exponent to handle the extreme underflow scenario
|
231
233
|
if len(result3.exponent) > 11:
|
232
234
|
# Exponent growth occurred - this is what we're testing for
|
233
235
|
self.assertGreater(len(result3.exponent), 11)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|