Skip to content
This repository was archived by the owner on Jun 8, 2025. It is now read-only.

Commit 9ea2165

Browse files
committed
support compare floats with tolerence, some code stole from davidpdrsn#34
1 parent 6b33b82 commit 9ea2165

File tree

4 files changed

+111
-3
lines changed

4 files changed

+111
-3
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ documentation = "https://docs.rs/json-diff"
1616
edition = "2018"
1717

1818
[dependencies]
19+
float-cmp = "0.10.0"
1920
serde_json = "1"
2021
serde = "1"
2122

src/diff.rs

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::core_ext::{Indent, Indexes};
22
use crate::{CompareMode, Config, NumericMode};
3+
use float_cmp::{ApproxEq, FloatMargin};
34
use serde_json::Value;
45
use std::{collections::HashSet, fmt};
56

@@ -58,6 +59,12 @@ impl<'a, 'b> DiffFolder<'a, 'b> {
5859
let is_equal = match self.config.numeric_mode {
5960
NumericMode::Strict => self.rhs == lhs,
6061
NumericMode::AssumeFloat => self.rhs.as_f64() == lhs.as_f64(),
62+
NumericMode::AssumeFloatEpsilon(epsilon) => match (lhs.as_f64(), self.rhs.as_f64()) {
63+
(Some(lhs_f64), Some(rhs_f64)) => {
64+
lhs_f64.approx_eq(rhs_f64, float_cmp::F64Margin::default().epsilon(epsilon))
65+
}
66+
_ => false,
67+
},
6168
};
6269
if !is_equal {
6370
self.acc.push(Difference {
@@ -582,6 +589,27 @@ mod test {
582589

583590
let json = json!({ "a": 1 });
584591
let diffs = diff(&json, &json, Config::new(CompareMode::Strict));
585-
assert_eq!(diffs, vec![]);
592+
assert_eq!(diffs.len(), 0);
593+
}
594+
595+
#[test]
596+
fn test_floats_epsilon() {
597+
let actual = json!(1.15);
598+
let expected = json!(1);
599+
let diffs = diff(
600+
&actual,
601+
&expected,
602+
Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloatEpsilon(0.2)),
603+
);
604+
assert_eq!(diffs.len(), 0);
605+
606+
let actual = json!(1.25);
607+
let expected = json!(1);
608+
let diffs = diff(
609+
&actual,
610+
&expected,
611+
Config::new(CompareMode::Inclusive).numeric_mode(NumericMode::AssumeFloatEpsilon(0.2)),
612+
);
613+
assert_eq!(diffs.len(), 1);
586614
}
587615
}

src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,7 +342,7 @@ where
342342
}
343343

344344
/// Configuration for how JSON values should be compared.
345-
#[derive(Debug, Clone, PartialEq, Eq)]
345+
#[derive(Debug, Clone, Copy, PartialEq)]
346346
#[allow(missing_copy_implementations)]
347347
pub struct Config {
348348
pub(crate) compare_mode: CompareMode,
@@ -390,12 +390,15 @@ pub enum CompareMode {
390390
}
391391

392392
/// How should numbers be compared.
393-
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
393+
#[derive(Debug, Copy, Clone, PartialEq)]
394394
pub enum NumericMode {
395395
/// Different numeric types aren't considered equal.
396396
Strict,
397397
/// All numeric types are converted to float before comparison.
398398
AssumeFloat,
399+
400+
/// All numeric types are converted to integer before comparison, floats are considered equal if they differ by at most this epsilon value.
401+
AssumeFloatEpsilon(f64),
399402
}
400403

401404
#[cfg(test)]

tests/integration_test.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,79 @@ fn eq_with_serializable_ref() {
182182
&user,
183183
);
184184
}
185+
186+
#[derive(Serialize)]
187+
struct Person {
188+
name: String,
189+
height: f64,
190+
}
191+
192+
#[test]
193+
fn can_pass_with_exact_float_comparison() {
194+
let person = Person {
195+
name: "bob".to_string(),
196+
height: 1.79,
197+
};
198+
199+
assert_json_matches!(
200+
&json!({
201+
"name": "bob",
202+
"height": 1.79
203+
}),
204+
&person,
205+
Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat)
206+
);
207+
}
208+
209+
#[test]
210+
#[should_panic]
211+
fn can_fail_with_exact_float_comparison() {
212+
let person = Person {
213+
name: "bob".to_string(),
214+
height: 1.79,
215+
};
216+
217+
assert_json_matches!(
218+
&json!({
219+
"name": "bob",
220+
"height": 1.7900001
221+
}),
222+
&person,
223+
Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloat)
224+
);
225+
}
226+
227+
#[test]
228+
fn can_pass_with_epsilon_based_float_comparison() {
229+
let person = Person {
230+
name: "bob".to_string(),
231+
height: 1.79,
232+
};
233+
234+
assert_json_matches!(
235+
&json!({
236+
"name": "bob",
237+
"height": 1.7900001
238+
}),
239+
&person,
240+
Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloatEpsilon(0.00001))
241+
);
242+
}
243+
244+
#[test]
245+
#[should_panic]
246+
fn can_fail_with_epsilon_based_float_comparison() {
247+
let person = Person {
248+
name: "bob".to_string(),
249+
height: 1.79,
250+
};
251+
252+
assert_json_matches!(
253+
&json!({
254+
"name": "bob",
255+
"height": 1.7901
256+
}),
257+
&person,
258+
Config::new(CompareMode::Strict).numeric_mode(NumericMode::AssumeFloatEpsilon(0.00001))
259+
);
260+
}

0 commit comments

Comments
 (0)