From b3d75b35e4731c9e93026b4d36eb00e59617f23a Mon Sep 17 00:00:00 2001 From: arimu1 <19286898+arimu1@users.noreply.github.com> Date: Sat, 8 Aug 2026 12:33:19 +0700 Subject: [PATCH] fix: allow closeTo to match any Number (Float, Integer, etc.) TypeSafeMatcher rejected boxed Float/Integer values before comparing, so hasProperty("x", closeTo(...)) failed for float bean properties even when the numeric value was within delta. Fixes #444 --- CHANGES.md | 3 + .../src/main/java/org/hamcrest/Matchers.java | 8 ++- .../java/org/hamcrest/number/IsCloseTo.java | 18 ++--- .../org/hamcrest/number/IsCloseToTest.java | 65 ++++++++++++++++++- 4 files changed, 82 insertions(+), 12 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 851be4dc..0b664302 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -35,6 +35,9 @@ these methods will need to be updated. The following methods are affected: * Allow matching against polymorphic collections ([#252](https://github.com/hamcrest/JavaHamcrest/issues/252), [PR #422](https://github.com/hamcrest/JavaHamcrest/pull/422)) +* `closeTo` accepts any `Number` (e.g. Float properties with `hasProperty`/`is` chaining), + not only Double ([#444](https://github.com/hamcrest/JavaHamcrest/issues/444)) + ## Version 3.0 (1st August 2024) diff --git a/hamcrest/src/main/java/org/hamcrest/Matchers.java b/hamcrest/src/main/java/org/hamcrest/Matchers.java index 60d8256a..add64039 100644 --- a/hamcrest/src/main/java/org/hamcrest/Matchers.java +++ b/hamcrest/src/main/java/org/hamcrest/Matchers.java @@ -1662,18 +1662,20 @@ public static org.hamcrest.Matcher oneOf(T... elements) { } /** - * Creates a matcher of {@link Double}s that matches when an examined double is equal + * Creates a matcher of {@link Number}s that matches when an examined number is equal * to the specified operand, within a range of +/- error. + * Comparison is performed using {@link Number#doubleValue()}. * For example: *
assertThat(1.03, is(closeTo(1.0, 0.03)))
+ *
assertThat(1.0f, is(closeTo(0.99, 0.1)))
* * @param operand - * the expected value of matching doubles + * the expected value of matching numbers * @param error * the delta (+/-) within which matches will be allowed * @return The matcher. */ - public static org.hamcrest.Matcher closeTo(double operand, double error) { + public static org.hamcrest.Matcher closeTo(double operand, double error) { return org.hamcrest.number.IsCloseTo.closeTo(operand, error); } diff --git a/hamcrest/src/main/java/org/hamcrest/number/IsCloseTo.java b/hamcrest/src/main/java/org/hamcrest/number/IsCloseTo.java index 34c0aa53..0e130fed 100644 --- a/hamcrest/src/main/java/org/hamcrest/number/IsCloseTo.java +++ b/hamcrest/src/main/java/org/hamcrest/number/IsCloseTo.java @@ -9,7 +9,7 @@ /** * Is the value a number equal to a value within some range of acceptable error? */ -public class IsCloseTo extends TypeSafeMatcher { +public class IsCloseTo extends TypeSafeMatcher { private final double delta; private final double value; @@ -25,12 +25,12 @@ public IsCloseTo(double value, double error) { } @Override - public boolean matchesSafely(Double item) { + public boolean matchesSafely(Number item) { return actualDelta(item) <= 0.0; } @Override - public void describeMismatchSafely(Double item, Description mismatchDescription) { + public void describeMismatchSafely(Number item, Description mismatchDescription) { mismatchDescription.appendValue(item) .appendText(" differed by ") .appendValue(actualDelta(item)) @@ -46,23 +46,25 @@ public void describeTo(Description description) { .appendValue(value); } - private double actualDelta(Double item) { - return abs(item - value) - delta; + private double actualDelta(Number item) { + return abs(item.doubleValue() - value) - delta; } /** - * Creates a matcher of {@link Double}s that matches when an examined double is equal + * Creates a matcher of {@link Number}s that matches when an examined number is equal * to the specified operand, within a range of +/- error. + * Comparison is performed using {@link Number#doubleValue()}. * For example: *
assertThat(1.03, is(closeTo(1.0, 0.03)))
+ *
assertThat(1.0f, is(closeTo(0.99, 0.1)))
* * @param operand - * the expected value of matching doubles + * the expected value of matching numbers * @param error * the delta (+/-) within which matches will be allowed * @return The matcher. */ - public static Matcher closeTo(double operand, double error) { + public static Matcher closeTo(double operand, double error) { return new IsCloseTo(operand, error); } diff --git a/hamcrest/src/test/java/org/hamcrest/number/IsCloseToTest.java b/hamcrest/src/test/java/org/hamcrest/number/IsCloseToTest.java index 4f90490e..12e4e223 100644 --- a/hamcrest/src/test/java/org/hamcrest/number/IsCloseToTest.java +++ b/hamcrest/src/test/java/org/hamcrest/number/IsCloseToTest.java @@ -4,12 +4,15 @@ import org.hamcrest.Matcher; import org.junit.jupiter.api.Test; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; import static org.hamcrest.test.MatcherAssertions.*; import static org.hamcrest.number.IsCloseTo.closeTo; +import static org.junit.jupiter.api.Assertions.assertTrue; public class IsCloseToTest extends AbstractMatcherTest { - private final Matcher matcher = closeTo(1.0d, 0.5d); + private final Matcher matcher = closeTo(1.0d, 0.5d); @Override protected Matcher createMatcher() { @@ -29,9 +32,69 @@ public void test_matchesIfArgumentIsEqualToADoubleValueWithinSomeError() { assertMismatchDescription("<0.1> differed by <0.4> more than delta <0.5>", matcher, 0.1); } + @Test + public void test_matchesIfArgumentIsEqualToAFloatValueWithinSomeError() { + assertMatches("1.0f", matcher, 1.0f); + assertMatches("0.5f", matcher, 0.5f); + assertMatches("1.5f", matcher, 1.5f); + + assertDoesNotMatch("too large", matcher, 2.0f); + assertMismatchDescription("<3.0F> differed by <1.5> more than delta <0.5>", matcher, 3.0f); + assertDoesNotMatch("number too small", matcher, 0.0f); + assertMismatchDescription("<0.0F> differed by <0.5> more than delta <0.5>", matcher, 0.0f); + } + + @Test + public void test_matchesIfArgumentIsEqualToAnIntegerValueWithinSomeError() { + assertMatches("1", matcher, 1); + assertDoesNotMatch("too large", matcher, 2); + assertMismatchDescription("<3> differed by <1.5> more than delta <0.5>", matcher, 3); + assertDoesNotMatch("number too small", matcher, 0); + assertMismatchDescription("<0> differed by <0.5> more than delta <0.5>", matcher, 0); + } + + @Test + public void test_matchesIfArgumentIsEqualToALongValueWithinSomeError() { + assertMatches("1L", matcher, 1L); + assertDoesNotMatch("too large", matcher, 2L); + assertMismatchDescription("<3L> differed by <1.5> more than delta <0.5>", matcher, 3L); + assertDoesNotMatch("number too small", matcher, 0L); + assertMismatchDescription("<0L> differed by <0.5> more than delta <0.5>", matcher, 0L); + } + + /** + * Regression for #444: float bean properties failed when closeTo was used with + * hasProperty / is chaining because TypeSafeMatcher only accepted Double. + */ + @Test + public void test_matchesFloatPropertyWhenChainedWithHasProperty() { + final BeanWithFloatNumber subject = new BeanWithFloatNumber(1.0f); + + assertMatches(hasProperty("number", closeTo(0.99, 0.1)), subject); + assertMatches(hasProperty("number", is(closeTo(0.99, 0.1))), subject); + assertDoesNotMatch(hasProperty("number", closeTo(0.5, 0.1)), subject); + // Must report numeric distance, not "was a java.lang.Float" type rejection (#444). + final String mismatch = mismatchDescription(hasProperty("number", closeTo(0.5, 0.1)), subject); + assertTrue(mismatch.contains("differed by"), "unexpected mismatch: " + mismatch); + assertTrue(!mismatch.contains("was a java.lang.Float"), "unexpected type rejection: " + mismatch); + } + @Test public void test_is_self_describing() { assertDescription("a numeric value within <0.5> of <1.0>", matcher); } + public static final class BeanWithFloatNumber { + private final float number; + + public BeanWithFloatNumber(float number) { + this.number = number; + } + + public float getNumber() { + return number; + } + } + } +