Skip to content

Commit cf9e721

Browse files
authored
Merge pull request #588 from akshat62/feature/add-fraction-numeric-support
Add fractions.Fraction as a recognized numeric type
2 parents f6f1117 + bd41bc3 commit cf9e721

5 files changed

Lines changed: 379 additions & 6 deletions

File tree

deepdiff/helper.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from collections.abc import Mapping, Sequence, Generator
1414
from ast import literal_eval
1515
from decimal import Decimal, localcontext, InvalidOperation as InvalidDecimalOperation
16+
from fractions import Fraction
1617
from itertools import repeat
1718
from orderly_set import StableSetEq as SetOrderedBase # median: 1.0867 s for cache test, 5.63s for all tests
1819
from threading import Timer
@@ -187,14 +188,14 @@ def get_semvar_as_integer(version: str) -> int:
187188
unicode_type = str
188189
bytes_type = bytes
189190
only_complex_number: Tuple[Type[Any], ...] = (complex,) + numpy_complex_numbers
190-
only_numbers: Tuple[Type[Any], ...] = (int, float, complex, Decimal) + numpy_numbers
191+
only_numbers: Tuple[Type[Any], ...] = (int, float, complex, Decimal, Fraction) + numpy_numbers
191192
datetimes: Tuple[Type[Any], ...] = (datetime.datetime, datetime.date, datetime.timedelta, datetime.time, np_datetime64)
192193
ipranges: Tuple[Type[Any], ...] = (ipaddress.IPv4Interface, ipaddress.IPv6Interface, ipaddress.IPv4Network, ipaddress.IPv6Network, ipaddress.IPv4Address, ipaddress.IPv6Address)
193194
uuids: Tuple[Type[uuid.UUID]] = (uuid.UUID, )
194195
times: Tuple[Type[Any], ...] = (datetime.datetime, datetime.time, np_datetime64)
195196
numbers: Tuple[Type[Any], ...] = only_numbers + datetimes
196197
# Type alias for use in type annotations
197-
NumberType = Union[int, float, complex, Decimal, datetime.datetime, datetime.date, datetime.timedelta, datetime.time, Any]
198+
NumberType = Union[int, float, complex, Decimal, Fraction, datetime.datetime, datetime.date, datetime.timedelta, datetime.time, Any]
198199
booleans: Tuple[Type[bool], Type[Any]] = (bool, np_bool_)
199200

200201
basic_types: Tuple[Type[Any], ...] = strings + numbers + uuids + booleans + (type(None), )
@@ -433,6 +434,11 @@ def number_to_string(number: Any, significant_digits: int, number_format_notatio
433434
# For example '999.99999999' will become '1000.000000' after quantize
434435
ctx.prec += 1
435436
number = number.quantize(Decimal('0.' + '0' * significant_digits))
437+
elif isinstance(number, Fraction):
438+
# Convert Fraction to float so that string formatting works on Python < 3.12
439+
number = round(float(number), significant_digits)
440+
if significant_digits == 0:
441+
number = int(number)
436442
elif isinstance(number, only_complex_number): # type: ignore
437443
# Case for complex numbers.
438444
number = number.__class__(

deepdiff/serialization.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import decimal # NOQA
1212
import orderly_set # NOQA
1313
import collections # NOQA
14+
import fractions
1415
import ipaddress
1516
import base64
1617
from copy import deepcopy, copy
@@ -80,6 +81,7 @@ class UnsupportedFormatErr(TypeError):
8081
'datetime.time',
8182
'datetime.timedelta',
8283
'decimal.Decimal',
84+
'fractions.Fraction',
8385
'uuid.UUID',
8486
'orderly_set.sets.OrderedSet',
8587
'orderly_set.sets.OrderlySet',
@@ -646,6 +648,13 @@ def _serialize_decimal(value):
646648
return float(value)
647649

648650

651+
def _serialize_fraction(value):
652+
if value.denominator == 1:
653+
return value.numerator
654+
else:
655+
return float(value)
656+
657+
649658
def _serialize_tuple(value):
650659
if hasattr(value, '_asdict'): # namedtuple
651660
return value._asdict()
@@ -666,6 +675,7 @@ def _serialize_bytes(value):
666675

667676
JSON_CONVERTOR = {
668677
decimal.Decimal: _serialize_decimal,
678+
fractions.Fraction: _serialize_fraction,
669679
SetOrdered: list,
670680
orderly_set.StableSetEq: list,
671681
set: list,

docs/ignore_types_or_values.rst

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,18 +65,30 @@ Ignore Numeric Type Changes
6565
ignore_numeric_type_changes: Boolean, default = False
6666
Whether to ignore numeric type changes or not. For example 10 vs. 10.0 are considered the same if ignore_numeric_type_changes is set to True.
6767

68-
Example
68+
Example with Decimal
6969
>>> from decimal import Decimal
7070
>>> from deepdiff import DeepDiff
71-
>>>
71+
>>>
7272
>>> t1 = Decimal('10.01')
7373
>>> t2 = 10.01
74-
>>>
74+
>>>
7575
>>> DeepDiff(t1, t2)
7676
{'type_changes': {'root': {'old_type': <class 'decimal.Decimal'>, 'new_type': <class 'float'>, 'old_value': Decimal('10.01'), 'new_value': 10.01}}}
7777
>>> DeepDiff(t1, t2, ignore_numeric_type_changes=True)
7878
{}
7979

80+
Example with Fraction
81+
>>> from fractions import Fraction
82+
>>> from deepdiff import DeepDiff
83+
>>>
84+
>>> t1 = Fraction(1, 2)
85+
>>> t2 = 0.5
86+
>>>
87+
>>> DeepDiff(t1, t2)
88+
{'type_changes': {'root': {'old_type': <class 'fractions.Fraction'>, 'new_type': <class 'float'>, 'old_value': Fraction(1, 2), 'new_value': 0.5}}}
89+
>>> DeepDiff(t1, t2, ignore_numeric_type_changes=True)
90+
{}
91+
8092
Note that this parameter only works for comparing numbers with numbers. If you compare a number to a string value of the number, this parameter does not solver your problem:
8193

8294
Example:

docs/numbers.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ Approximate decimals comparison (Significant digits after the point):
4141
>>> DeepDiff(t1, t2, significant_digits=1)
4242
{'values_changed': {'root': {'new_value': Decimal('1.57'), 'old_value': Decimal('1.52')}}}
4343

44+
Approximate fractions comparison (Significant digits after the point):
45+
>>> from fractions import Fraction
46+
>>> t1 = Fraction(22, 7) # 3.142857...
47+
>>> t2 = Fraction(355, 113) # 3.141592...
48+
>>> DeepDiff(t1, t2, significant_digits=2)
49+
{}
50+
>>> DeepDiff(t1, t2, significant_digits=3)
51+
{'values_changed': {'root': {'new_value': Fraction(355, 113), 'old_value': Fraction(22, 7)}}}
52+
4453
Approximate float comparison (Significant digits after the point):
4554
>>> t1 = [ 1.1129, 1.3359 ]
4655
>>> t2 = [ 1.113, 1.3362 ]
@@ -131,13 +140,20 @@ math_epsilon: Decimal, default = None
131140

132141
To check against that the math core module provides the valuable isclose() function. It evaluates the being close of two numbers to each other, with reference to an epsilon (abs_tol). This is superior to the format function, as it evaluates the mathematical representation and not the string representation.
133142

134-
Example:
143+
Example with Decimal:
135144
>>> from decimal import Decimal
136145
>>> d1 = {"a": Decimal("7.175")}
137146
>>> d2 = {"a": Decimal("7.174")}
138147
>>> DeepDiff(d1, d2, math_epsilon=0.01)
139148
{}
140149

150+
Example with Fraction:
151+
>>> from fractions import Fraction
152+
>>> d1 = {"a": Fraction(7175, 1000)}
153+
>>> d2 = {"a": Fraction(7174, 1000)}
154+
>>> DeepDiff(d1, d2, math_epsilon=0.01)
155+
{}
156+
141157
.. note::
142158
math_epsilon cannot currently handle the hashing of values, which is done when :ref:`ignore_order_label` is True.
143159

0 commit comments

Comments
 (0)