Skip to content

Commit 6270344

Browse files
Johanyouknowone
authored andcommitted
Update isclose module of math (RustPython#1532)
- Implement `math.isclose()` function with test case - Add `IsCloseArgs` struct for `keyword_only` argument: `rel_tol`, `abs_tol` - Change order of test case on `math_module.py` for readability - Clippy recommends using `std::f64::EPSILON` instead of `==` but purpose is different - Fix multiline declaration on the function parameters
1 parent 81302ce commit 6270344

File tree

2 files changed

+173
-31
lines changed

2 files changed

+173
-31
lines changed

tests/snippets/stdlib_math.py

Lines changed: 119 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -34,37 +34,6 @@
3434
assert math.ceil(3.3) == 4
3535
assert math.floor(4.4) == 4
3636

37-
assert math.copysign(1, 42) == 1.0
38-
assert math.copysign(0., 42) == 0.0
39-
assert math.copysign(1., -42) == -1.0
40-
assert math.copysign(3, 0.) == 3.0
41-
assert math.copysign(4., -0.) == -4.0
42-
assert_raises(TypeError, math.copysign)
43-
# copysign should let us distinguish signs of zeros
44-
assert math.copysign(1., 0.) == 1.
45-
assert math.copysign(1., -0.) == -1.
46-
assert math.copysign(INF, 0.) == INF
47-
assert math.copysign(INF, -0.) == NINF
48-
assert math.copysign(NINF, 0.) == INF
49-
assert math.copysign(NINF, -0.) == NINF
50-
# and of infinities
51-
assert math.copysign(1., INF) == 1.
52-
assert math.copysign(1., NINF) == -1.
53-
assert math.copysign(INF, INF) == INF
54-
assert math.copysign(INF, NINF) == NINF
55-
assert math.copysign(NINF, INF) == INF
56-
assert math.copysign(NINF, NINF) == NINF
57-
assert math.isnan(math.copysign(NAN, 1.))
58-
assert math.isnan(math.copysign(NAN, INF))
59-
assert math.isnan(math.copysign(NAN, NINF))
60-
assert math.isnan(math.copysign(NAN, NAN))
61-
# copysign(INF, NAN) may be INF or it may be NINF, since
62-
# we don't know whether the sign bit of NAN is set on any
63-
# given platform.
64-
assert math.isinf(math.copysign(INF, NAN))
65-
# similarly, copysign(2., NAN) could be 2. or -2.
66-
assert abs(math.copysign(2., NAN)) == 2.
67-
6837
class A(object):
6938
def __trunc__(self):
7039
return 2
@@ -114,6 +83,125 @@ def __floor__(self):
11483
with assert_raises(TypeError):
11584
math.floor(object())
11685

86+
isclose = math.isclose
87+
88+
def assertIsClose(a, b, *args, **kwargs):
89+
assert isclose(a, b, *args, **kwargs) == True, "%s and %s should be close!" % (a, b)
90+
91+
def assertIsNotClose(a, b, *args, **kwargs):
92+
assert isclose(a, b, *args, **kwargs) == False, "%s and %s should not be close!" % (a, b)
93+
94+
def assertAllClose(examples, *args, **kwargs):
95+
for a, b in examples:
96+
assertIsClose(a, b, *args, **kwargs)
97+
98+
def assertAllNotClose(examples, *args, **kwargs):
99+
for a, b in examples:
100+
assertIsNotClose(a, b, *args, **kwargs)
101+
102+
# test_negative_tolerances: ValueError should be raised if either tolerance is less than zero
103+
assert_raises(ValueError, lambda: isclose(1, 1, rel_tol=-1e-100))
104+
assert_raises(ValueError, lambda: isclose(1, 1, rel_tol=1e-100, abs_tol=-1e10))
105+
106+
# test_identical: identical values must test as close
107+
identical_examples = [(2.0, 2.0),
108+
(0.1e200, 0.1e200),
109+
(1.123e-300, 1.123e-300),
110+
(12345, 12345.0),
111+
(0.0, -0.0),
112+
(345678, 345678)]
113+
assertAllClose(identical_examples, rel_tol=0.0, abs_tol=0.0)
114+
115+
# test_eight_decimal_places: examples that are close to 1e-8, but not 1e-9
116+
eight_decimal_places_examples = [(1e8, 1e8 + 1),
117+
(-1e-8, -1.000000009e-8),
118+
(1.12345678, 1.12345679)]
119+
assertAllClose(eight_decimal_places_examples, rel_tol=1e-08)
120+
assertAllNotClose(eight_decimal_places_examples, rel_tol=1e-09)
121+
122+
# test_near_zero: values close to zero
123+
near_zero_examples = [(1e-9, 0.0),
124+
(-1e-9, 0.0),
125+
(-1e-150, 0.0)]
126+
# these should not be close to any rel_tol
127+
assertAllNotClose(near_zero_examples, rel_tol=0.9)
128+
# these should be close to abs_tol=1e-8
129+
assertAllClose(near_zero_examples, abs_tol=1e-8)
130+
131+
# test_identical_infinite: these are close regardless of tolerance -- i.e. they are equal
132+
assertIsClose(INF, INF)
133+
assertIsClose(INF, INF, abs_tol=0.0)
134+
assertIsClose(NINF, NINF)
135+
assertIsClose(NINF, NINF, abs_tol=0.0)
136+
137+
# test_inf_ninf_nan(self): these should never be close (following IEEE 754 rules for equality)
138+
not_close_examples = [(NAN, NAN),
139+
(NAN, 1e-100),
140+
(1e-100, NAN),
141+
(INF, NAN),
142+
(NAN, INF),
143+
(INF, NINF),
144+
(INF, 1.0),
145+
(1.0, INF),
146+
(INF, 1e308),
147+
(1e308, INF)]
148+
# use largest reasonable tolerance
149+
assertAllNotClose(not_close_examples, abs_tol=0.999999999999999)
150+
151+
# test_zero_tolerance: test with zero tolerance
152+
zero_tolerance_close_examples = [(1.0, 1.0),
153+
(-3.4, -3.4),
154+
(-1e-300, -1e-300)]
155+
assertAllClose(zero_tolerance_close_examples, rel_tol=0.0)
156+
zero_tolerance_not_close_examples = [(1.0, 1.000000000000001),
157+
(0.99999999999999, 1.0),
158+
(1.0e200, .999999999999999e200)]
159+
assertAllNotClose(zero_tolerance_not_close_examples, rel_tol=0.0)
160+
161+
# test_asymmetry: test the asymmetry example from PEP 485
162+
assertAllClose([(9, 10), (10, 9)], rel_tol=0.1)
163+
164+
# test_integers: test with integer values
165+
integer_examples = [(100000001, 100000000),
166+
(123456789, 123456788)]
167+
168+
assertAllClose(integer_examples, rel_tol=1e-8)
169+
assertAllNotClose(integer_examples, rel_tol=1e-9)
170+
171+
# test_decimals: test with Decimal values
172+
# test_fractions: test with Fraction values
173+
174+
assert math.copysign(1, 42) == 1.0
175+
assert math.copysign(0., 42) == 0.0
176+
assert math.copysign(1., -42) == -1.0
177+
assert math.copysign(3, 0.) == 3.0
178+
assert math.copysign(4., -0.) == -4.0
179+
assert_raises(TypeError, math.copysign)
180+
# copysign should let us distinguish signs of zeros
181+
assert math.copysign(1., 0.) == 1.
182+
assert math.copysign(1., -0.) == -1.
183+
assert math.copysign(INF, 0.) == INF
184+
assert math.copysign(INF, -0.) == NINF
185+
assert math.copysign(NINF, 0.) == INF
186+
assert math.copysign(NINF, -0.) == NINF
187+
# and of infinities
188+
assert math.copysign(1., INF) == 1.
189+
assert math.copysign(1., NINF) == -1.
190+
assert math.copysign(INF, INF) == INF
191+
assert math.copysign(INF, NINF) == NINF
192+
assert math.copysign(NINF, INF) == INF
193+
assert math.copysign(NINF, NINF) == NINF
194+
assert math.isnan(math.copysign(NAN, 1.))
195+
assert math.isnan(math.copysign(NAN, INF))
196+
assert math.isnan(math.copysign(NAN, NINF))
197+
assert math.isnan(math.copysign(NAN, NAN))
198+
# copysign(INF, NAN) may be INF or it may be NINF, since
199+
# we don't know whether the sign bit of NAN is set on any
200+
# given platform.
201+
assert math.isinf(math.copysign(INF, NAN))
202+
# similarly, copysign(2., NAN) could be 2. or -2.
203+
assert abs(math.copysign(2., NAN)) == 2.
204+
117205
assert str(math.frexp(0.0)) == str((+0.0, 0))
118206
assert str(math.frexp(-0.0)) == str((-0.0, 0))
119207
assert math.frexp(1) == (0.5, 1)

vm/src/stdlib/math.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,59 @@ make_math_func_bool!(math_isfinite, is_finite);
4040
make_math_func_bool!(math_isinf, is_infinite);
4141
make_math_func_bool!(math_isnan, is_nan);
4242

43+
#[derive(FromArgs)]
44+
struct IsCloseArgs {
45+
#[pyarg(positional_only, optional = false)]
46+
a: IntoPyFloat,
47+
#[pyarg(positional_only, optional = false)]
48+
b: IntoPyFloat,
49+
#[pyarg(keyword_only, optional = true)]
50+
rel_tol: OptionalArg<IntoPyFloat>,
51+
#[pyarg(keyword_only, optional = true)]
52+
abs_tol: OptionalArg<IntoPyFloat>,
53+
}
54+
55+
#[allow(clippy::float_cmp)]
56+
fn math_isclose(args: IsCloseArgs, vm: &VirtualMachine) -> PyResult<bool> {
57+
let a = args.a.to_f64();
58+
let b = args.b.to_f64();
59+
let rel_tol = match args.rel_tol {
60+
OptionalArg::Missing => 1e-09,
61+
OptionalArg::Present(ref value) => value.to_f64(),
62+
};
63+
64+
let abs_tol = match args.abs_tol {
65+
OptionalArg::Missing => 0.0,
66+
OptionalArg::Present(ref value) => value.to_f64(),
67+
};
68+
69+
if rel_tol < 0.0 || abs_tol < 0.0 {
70+
return Err(vm.new_value_error("tolerances must be non-negative".to_string()));
71+
}
72+
73+
if a == b {
74+
/* short circuit exact equality -- needed to catch two infinities of
75+
the same sign. And perhaps speeds things up a bit sometimes.
76+
*/
77+
return Ok(true);
78+
}
79+
80+
/* This catches the case of two infinities of opposite sign, or
81+
one infinity and one finite number. Two infinities of opposite
82+
sign would otherwise have an infinite relative tolerance.
83+
Two infinities of the same sign are caught by the equality check
84+
above.
85+
*/
86+
87+
if a.is_infinite() || b.is_infinite() {
88+
return Ok(false);
89+
}
90+
91+
let diff = (b - a).abs();
92+
93+
Ok((diff <= (rel_tol * b).abs()) || (diff <= (rel_tol * a).abs()) || (diff <= abs_tol))
94+
}
95+
4396
fn math_copysign(a: IntoPyFloat, b: IntoPyFloat, _vm: &VirtualMachine) -> f64 {
4497
let a = a.to_f64();
4598
let b = b.to_f64();
@@ -223,6 +276,7 @@ pub fn make_module(vm: &VirtualMachine) -> PyObjectRef {
223276
"isfinite" => ctx.new_rustfunc(math_isfinite),
224277
"isinf" => ctx.new_rustfunc(math_isinf),
225278
"isnan" => ctx.new_rustfunc(math_isnan),
279+
"isclose" => ctx.new_rustfunc(math_isclose),
226280
"copysign" => ctx.new_rustfunc(math_copysign),
227281

228282
// Power and logarithmic functions:

0 commit comments

Comments
 (0)