fix: numeric metadata filters no longer throw for NaN and Infinity (#6108)
## Issue Closes #6107 ## Change ### The bug `Metadata` accepts `Float` and `Double` and does not exclude non-finite values, but `NumberComparator` routed every numeric comparison through `new BigDecimal(number.toString())`. `BigDecimal` has no representation for `NaN`/`Infinity`, and `Double.toString` renders them as `"NaN"`/`"Infinity"`, which the `BigDecimal(String)` constructor rejects. So `Filter.test(...)` — a predicate — threw `NumberFormatException` instead of returning a boolean. One document with a `NaN` score broke filtering for the whole query. This hit all eight comparators: `IsEqualTo`, `IsNotEqualTo`, `IsGreaterThan`, `IsGreaterThanOrEqualTo`, `IsLessThan`, `IsLessThanOrEqualTo`, `IsIn`, `IsNotIn`. ### The fix Non-finite values are compared as `double`s instead of `BigDecimal`s: - **Infinities keep their natural ordering.** `-Infinity` is smaller and `+Infinity` is greater than any finite value, and an infinite value equals itself. - **`NaN` is not comparable to anything**, not even to itself, so every comparison involving it is `false`. Only the negated filters match it. | metadata value | `IsGreaterThan(0.5)` | `IsLessThan(0.5)` | `IsEqualTo(0.5)` | `IsEqualTo(sameValue)` | `IsNotEqualTo(0.5)` | |---|---|---|---|---|---| | `+Infinity` | `true` | `false` | `false` | `true` | `true` | | `-Infinity` | `false` | `true` | `false` | `true` | `true` | | `NaN` | `false` | `false` | `false` | **`false`** | `true` | `IsIn`/`IsNotIn` follow `IsEqualTo`/`IsNotEqualTo`, so `IsNotEqualTo` and `IsNotIn` stay exact complements of their positive counterparts. **Finite comparisons are untouched** and still go through `BigDecimal`. That is deliberate, and there is a test pinning it: `9007199254740992L` and `9007199254740993L` are distinct but collapse onto the same `double`, so a blanket switch to `Double.compare` would wrongly call them equal. `NumberComparator` now exposes one predicate per filter (`isEqualTo`, `isGreaterThan`, …) instead of returning a raw `int`. This is required rather than cosmetic: `NaN` needs `>`, `>=`, `<`, `<=` and `==` to all be `false` while `!=` is `true`, and no single `int` return value can express that for all six operators at once. The class is package-private and `@Internal`, so there is no API change — `revapi:check` compares clean against `1.19.0`. `containsAsBigDecimals` became `isIn` and delegates to `isEqualTo` instead of building its own `BigDecimal`s. That removes the duplicated conversion and means `IsIn`/`IsNotIn` inherit the same handling automatically — the drift between those two paths is what #5716/#5717 had to correct before. ### Why `NaN` is not ordered Two alternatives were considered and rejected: - **Giving `NaN` a total order via `Double.compare`** (`NaN` equals itself and sorts above `+Infinity`). It matches how PostgreSQL orders native `float8` columns, but nothing else we map filters onto reproduces it: JSON has no `NaN` literal, and Elasticsearch and most vector stores reject non-finite numbers outright. It would also make a garbage `NaN` score *match* `score > 0.5` and land at the top of results, which is the opposite of what a user wants from a bad value. - **Rejecting non-finite values in `Metadata`.** Fail-fast at the boundary is attractive, but `Metadata` is also constructed on the **read** path — pgvector, MariaDB, Elasticsearch, OpenSearch, Weaviate and Qdrant all rebuild it via `new Metadata(Map)` when mapping results. PostgreSQL stores `NaN` and `Infinity` in `float4`/`float8` columns quite happily, so validation there would turn already-persisted rows into exceptions on every search — exactly what the "changing an existing embedding store integration" guideline forbids. `false` for every `NaN` comparison is what Java's own `<`/`>` operators do, what SQL does, and what the stores these filters are translated into do. ### Known limitation This fixes the predicate contract: `Filter.test(...)` returns a boolean for anything `Metadata` holds. It does **not** make non-finite metadata survive persistence. `InMemoryEmbeddingStore.serializeToJson()` writes `Double.NaN` as the JSON string `"NaN"`, and `fromJson` reads it back as a `String`, after which filtering that key fails with a type mismatch: ``` IllegalArgumentException: Type mismatch: actual value of metadata key "score" (NaN) has type java.lang.String, while comparison value (0.5) has type java.lang.Double ``` That is a separate pre-existing bug in the JSON round-trip and is deliberately out of scope here. ## Tests 19 tests in `NumberComparatorNonFiniteTest`, covering: - every comparator against `NaN`/`±Infinity` as the metadata value **and** as the comparison value, with no exception thrown (the original regression) - infinity ordering, self-equality, and `IsIn`/`IsNotIn` membership - `NaN` matching nothing, not even itself, and not being ordered against `±Infinity` - `Float` as well as `Double`, including `Float` metadata compared against a `Double` comparison value - integral (`Long`) metadata values against an infinite comparison value - two guards for finite behaviour: mixed numeric types, and `BigDecimal` precision beyond `double` Verified failing without the fix: on unmodified `main` the non-finite cases error with `NumberFormatException`; the finite guards pass either way. ## Verification - `langchain4j-core`: **1267 tests, 0 failures, 0 errors** - `langchain4j`: **1339 tests, 0 failures, 0 errors** - `./mvnw spotless:check` green on `langchain4j-core` - `./mvnw revapi:check` on `langchain4j-core`: compares `1.19.0` against `1.20.0-SNAPSHOT`, no API problems Integration tests that need containers or API keys were not run locally. Note on the diff size: the eight comparator classes were never spotless-formatted, so touching them pulls them into the `ratchetFrom=origin/main` ratchet. The functional change is 2 lines per file; the rest is the formatter reordering the import block and joining one line in each `equals()`. ## General checklist - [X] There are no breaking changes (API, behaviour) — only inputs that previously threw `NumberFormatException` behave differently - [X] I have added unit and/or integration tests for my change - [X] The tests cover both positive and negative cases - [X] I have manually run all the unit and integration tests in the module I have added/changed, and they are all green (unit tests; ITs need containers/keys) - [X] I have manually run all the unit and integration tests in the [core](https://github.com/langchain4j/langchain4j/tree/main/langchain4j-core) and [main](https://github.com/langchain4j/langchain4j/tree/main/langchain4j) modules, and they are all green (unit tests; ITs need containers/keys) - [X] I have added/updated the [documentation](https://github.com/langchain4j/langchain4j/tree/main/docs/docs) — Javadoc on the `Filter` interface, which every comparison filter links to; no `docs/docs` page covers numeric filter semantics - [ ] I have added an example in the [examples repo](https://github.com/langchain4j/langchain4j-examples) (only for "big" features) - [ ] I have added/updated [Spring Boot starter(s)](https://github.com/langchain4j/langchain4j-spring) (if applicable) --------- Co-authored-by: Dmytro Liubarskyi <ljubarskij@gmail.com>
This commit is contained in:
@@ -26,6 +26,13 @@ import dev.langchain4j.store.embedding.filter.logical.Or;
|
||||
* <br>
|
||||
* Each {@link EmbeddingStore} implementation that supports metadata filtering is mapping {@link Filter}
|
||||
* into it's native filter expression.
|
||||
* <br>
|
||||
* Metadata values can be {@code Float} or {@code Double}, and those can be infinite or {@code NaN}.
|
||||
* Infinite values are compared as one would expect: {@code -Infinity} is smaller and {@code +Infinity} is
|
||||
* greater than any other number. {@code NaN} is not comparable to anything, not even to itself, so every
|
||||
* comparison with it is {@code false}: a {@code NaN} metadata value satisfies neither
|
||||
* {@code IsEqualTo("key", NaN)} nor {@code IsGreaterThan("key", 0)}, and only {@link IsNotEqualTo} and
|
||||
* {@link IsNotIn} match it.
|
||||
*
|
||||
* @see IsEqualTo
|
||||
* @see IsNotEqualTo
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import java.util.UUID;
|
||||
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.compareAsBigDecimals;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.isEqualTo;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.TypeChecker.ensureTypesAreCompatible;
|
||||
|
||||
public class IsEqualTo implements Filter {
|
||||
@@ -43,7 +43,7 @@ public class IsEqualTo implements Filter {
|
||||
ensureTypesAreCompatible(actualValue, comparisonValue, key);
|
||||
|
||||
if (actualValue instanceof Number) {
|
||||
return compareAsBigDecimals(actualValue, comparisonValue) == 0;
|
||||
return isEqualTo(actualValue, comparisonValue);
|
||||
}
|
||||
|
||||
if (comparisonValue instanceof UUID && actualValue instanceof String) {
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import java.util.Objects;
|
||||
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.compareAsBigDecimals;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.isGreaterThan;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.TypeChecker.ensureTypesAreCompatible;
|
||||
|
||||
public class IsGreaterThan implements Filter {
|
||||
@@ -42,7 +42,7 @@ public class IsGreaterThan implements Filter {
|
||||
ensureTypesAreCompatible(actualValue, comparisonValue, key);
|
||||
|
||||
if (actualValue instanceof Number) {
|
||||
return compareAsBigDecimals(actualValue, comparisonValue) > 0;
|
||||
return isGreaterThan(actualValue, comparisonValue);
|
||||
}
|
||||
|
||||
return ((Comparable) actualValue).compareTo(comparisonValue) > 0;
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import java.util.Objects;
|
||||
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.compareAsBigDecimals;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.isGreaterThanOrEqualTo;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.TypeChecker.ensureTypesAreCompatible;
|
||||
|
||||
public class IsGreaterThanOrEqualTo implements Filter {
|
||||
@@ -42,7 +42,7 @@ public class IsGreaterThanOrEqualTo implements Filter {
|
||||
ensureTypesAreCompatible(actualValue, comparisonValue, key);
|
||||
|
||||
if (actualValue instanceof Number) {
|
||||
return compareAsBigDecimals(actualValue, comparisonValue) >= 0;
|
||||
return isGreaterThanOrEqualTo(actualValue, comparisonValue);
|
||||
}
|
||||
|
||||
return ((Comparable) actualValue).compareTo(comparisonValue) >= 0;
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import java.util.UUID;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotEmpty;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.containsAsBigDecimals;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.isIn;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.TypeChecker.ensureTypesAreCompatible;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.UUIDComparator.containsAsUUID;
|
||||
import static java.util.Collections.unmodifiableSet;
|
||||
@@ -51,7 +51,7 @@ public class IsIn implements Filter {
|
||||
ensureTypesAreCompatible(actualValue, comparisonValues.iterator().next(), key);
|
||||
|
||||
if (comparisonValues.iterator().next() instanceof Number) {
|
||||
return containsAsBigDecimals(actualValue, comparisonValues);
|
||||
return isIn(actualValue, comparisonValues);
|
||||
}
|
||||
if (comparisonValues.iterator().next() instanceof UUID) {
|
||||
return containsAsUUID(actualValue, comparisonValues);
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import java.util.Objects;
|
||||
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.compareAsBigDecimals;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.isLessThan;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.TypeChecker.ensureTypesAreCompatible;
|
||||
|
||||
public class IsLessThan implements Filter {
|
||||
@@ -42,7 +42,7 @@ public class IsLessThan implements Filter {
|
||||
ensureTypesAreCompatible(actualValue, comparisonValue, key);
|
||||
|
||||
if (actualValue instanceof Number) {
|
||||
return compareAsBigDecimals(actualValue, comparisonValue) < 0;
|
||||
return isLessThan(actualValue, comparisonValue);
|
||||
}
|
||||
|
||||
return ((Comparable) actualValue).compareTo(comparisonValue) < 0;
|
||||
|
||||
+2
-2
@@ -7,7 +7,7 @@ import java.util.Objects;
|
||||
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.compareAsBigDecimals;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.isLessThanOrEqualTo;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.TypeChecker.ensureTypesAreCompatible;
|
||||
|
||||
public class IsLessThanOrEqualTo implements Filter {
|
||||
@@ -42,7 +42,7 @@ public class IsLessThanOrEqualTo implements Filter {
|
||||
ensureTypesAreCompatible(actualValue, comparisonValue, key);
|
||||
|
||||
if (actualValue instanceof Number) {
|
||||
return compareAsBigDecimals(actualValue, comparisonValue) <= 0;
|
||||
return isLessThanOrEqualTo(actualValue, comparisonValue);
|
||||
}
|
||||
|
||||
return ((Comparable) actualValue).compareTo(comparisonValue) <= 0;
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import java.util.UUID;
|
||||
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.compareAsBigDecimals;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.isEqualTo;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.TypeChecker.ensureTypesAreCompatible;
|
||||
|
||||
public class IsNotEqualTo implements Filter {
|
||||
@@ -43,7 +43,7 @@ public class IsNotEqualTo implements Filter {
|
||||
ensureTypesAreCompatible(actualValue, comparisonValue, key);
|
||||
|
||||
if (actualValue instanceof Number) {
|
||||
return compareAsBigDecimals(actualValue, comparisonValue) != 0;
|
||||
return !isEqualTo(actualValue, comparisonValue);
|
||||
}
|
||||
|
||||
if (comparisonValue instanceof UUID && actualValue instanceof String) {
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ import java.util.UUID;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotEmpty;
|
||||
import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.containsAsBigDecimals;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.NumberComparator.isIn;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.TypeChecker.ensureTypesAreCompatible;
|
||||
import static dev.langchain4j.store.embedding.filter.comparison.UUIDComparator.containsAsUUID;
|
||||
import static java.util.Collections.unmodifiableSet;
|
||||
@@ -51,7 +51,7 @@ public class IsNotIn implements Filter {
|
||||
ensureTypesAreCompatible(actualValue, comparisonValues.iterator().next(), key);
|
||||
|
||||
if (comparisonValues.iterator().next() instanceof Number) {
|
||||
return !containsAsBigDecimals(actualValue, comparisonValues);
|
||||
return !isIn(actualValue, comparisonValues);
|
||||
}
|
||||
if (comparisonValues.iterator().next() instanceof UUID) {
|
||||
return !containsAsUUID(actualValue, comparisonValues);
|
||||
|
||||
+56
-7
@@ -4,18 +4,67 @@ import dev.langchain4j.Internal;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Compares numeric metadata values.
|
||||
*
|
||||
* <p>Finite values are compared as {@link BigDecimal}s, so that values which differ but collapse onto the same
|
||||
* {@code double} (for example {@code 9007199254740992L} and {@code 9007199254740993L}) are not mistaken for
|
||||
* each other.
|
||||
*
|
||||
* <p>{@code BigDecimal} can represent neither infinity nor {@code NaN}, so non-finite values are compared as
|
||||
* {@code double}s instead: infinities keep their natural ordering, while {@code NaN} is not comparable to
|
||||
* anything at all, not even to itself. Every comparison involving {@code NaN} is therefore {@code false},
|
||||
* which is how {@code NaN} behaves in Java, in SQL and in most embedding stores.
|
||||
*/
|
||||
@Internal
|
||||
class NumberComparator {
|
||||
|
||||
static int compareAsBigDecimals(Object actualNumber, Object comparisonNumber) {
|
||||
static boolean isEqualTo(Object actualNumber, Object comparisonNumber) {
|
||||
return neitherIsNaN(actualNumber, comparisonNumber) && compare(actualNumber, comparisonNumber) == 0;
|
||||
}
|
||||
|
||||
static boolean isGreaterThan(Object actualNumber, Object comparisonNumber) {
|
||||
return neitherIsNaN(actualNumber, comparisonNumber) && compare(actualNumber, comparisonNumber) > 0;
|
||||
}
|
||||
|
||||
static boolean isGreaterThanOrEqualTo(Object actualNumber, Object comparisonNumber) {
|
||||
return neitherIsNaN(actualNumber, comparisonNumber) && compare(actualNumber, comparisonNumber) >= 0;
|
||||
}
|
||||
|
||||
static boolean isLessThan(Object actualNumber, Object comparisonNumber) {
|
||||
return neitherIsNaN(actualNumber, comparisonNumber) && compare(actualNumber, comparisonNumber) < 0;
|
||||
}
|
||||
|
||||
static boolean isLessThanOrEqualTo(Object actualNumber, Object comparisonNumber) {
|
||||
return neitherIsNaN(actualNumber, comparisonNumber) && compare(actualNumber, comparisonNumber) <= 0;
|
||||
}
|
||||
|
||||
static boolean isIn(Object actualNumber, Collection<?> comparisonNumbers) {
|
||||
return comparisonNumbers.stream().anyMatch(comparisonNumber -> isEqualTo(actualNumber, comparisonNumber));
|
||||
}
|
||||
|
||||
private static int compare(Object actualNumber, Object comparisonNumber) {
|
||||
if (isInfinite(actualNumber) || isInfinite(comparisonNumber)) {
|
||||
return Double.compare(doubleValue(actualNumber), doubleValue(comparisonNumber));
|
||||
}
|
||||
return new BigDecimal(actualNumber.toString()).compareTo(new BigDecimal(comparisonNumber.toString()));
|
||||
}
|
||||
|
||||
static boolean containsAsBigDecimals(Object actualNumber, Collection<?> comparisonNumbers) {
|
||||
BigDecimal actualNumberAsBigDecimal = new BigDecimal(actualNumber.toString());
|
||||
return comparisonNumbers.stream()
|
||||
.map(comparisonNumber -> new BigDecimal(comparisonNumber.toString()))
|
||||
.anyMatch(comparisonNumberAsBigDecimal ->
|
||||
comparisonNumberAsBigDecimal.compareTo(actualNumberAsBigDecimal) == 0);
|
||||
private static boolean neitherIsNaN(Object actualNumber, Object comparisonNumber) {
|
||||
return !isNaN(actualNumber) && !isNaN(comparisonNumber);
|
||||
}
|
||||
|
||||
private static boolean isNaN(Object number) {
|
||||
return number instanceof Double doubleValue && doubleValue.isNaN()
|
||||
|| number instanceof Float floatValue && floatValue.isNaN();
|
||||
}
|
||||
|
||||
private static boolean isInfinite(Object number) {
|
||||
return number instanceof Double doubleValue && doubleValue.isInfinite()
|
||||
|| number instanceof Float floatValue && floatValue.isInfinite();
|
||||
}
|
||||
|
||||
private static double doubleValue(Object number) {
|
||||
return ((Number) number).doubleValue();
|
||||
}
|
||||
}
|
||||
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
package dev.langchain4j.store.embedding.filter.comparison;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
import dev.langchain4j.data.document.Metadata;
|
||||
import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
/**
|
||||
* {@link Metadata} accepts {@link Float} and {@link Double} without excluding infinity and NaN, so numeric
|
||||
* filters have to return a boolean for those values instead of throwing.
|
||||
*
|
||||
* <p>Infinities keep their natural ordering. NaN is not comparable to anything, not even to itself, so every
|
||||
* comparison involving it is false and only the negated filters match.
|
||||
*/
|
||||
class NumberComparatorNonFiniteTest {
|
||||
|
||||
private static final double FINITE = 0.5;
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY})
|
||||
void shouldNotThrowForNonFiniteMetadataValue(double value) {
|
||||
Metadata metadata = metadata(value);
|
||||
|
||||
assertThatCode(() -> {
|
||||
new IsEqualTo("score", FINITE).test(metadata);
|
||||
new IsNotEqualTo("score", FINITE).test(metadata);
|
||||
new IsGreaterThan("score", FINITE).test(metadata);
|
||||
new IsGreaterThanOrEqualTo("score", FINITE).test(metadata);
|
||||
new IsLessThan("score", FINITE).test(metadata);
|
||||
new IsLessThanOrEqualTo("score", FINITE).test(metadata);
|
||||
new IsIn("score", List.of(FINITE)).test(metadata);
|
||||
new IsNotIn("score", List.of(FINITE)).test(metadata);
|
||||
})
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(doubles = {Double.NaN, Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY})
|
||||
void shouldNotThrowForNonFiniteComparisonValue(double value) {
|
||||
Metadata metadata = metadata(FINITE);
|
||||
|
||||
assertThatCode(() -> {
|
||||
new IsEqualTo("score", value).test(metadata);
|
||||
new IsNotEqualTo("score", value).test(metadata);
|
||||
new IsGreaterThan("score", value).test(metadata);
|
||||
new IsGreaterThanOrEqualTo("score", value).test(metadata);
|
||||
new IsLessThan("score", value).test(metadata);
|
||||
new IsLessThanOrEqualTo("score", value).test(metadata);
|
||||
new IsIn("score", List.of(value)).test(metadata);
|
||||
new IsNotIn("score", List.of(value)).test(metadata);
|
||||
})
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTreatPositiveInfinityAsGreaterThanFiniteValues() {
|
||||
Metadata metadata = metadata(Double.POSITIVE_INFINITY);
|
||||
|
||||
assertThat(new IsGreaterThan("score", FINITE).test(metadata)).isTrue();
|
||||
assertThat(new IsGreaterThanOrEqualTo("score", FINITE).test(metadata)).isTrue();
|
||||
assertThat(new IsLessThan("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsLessThanOrEqualTo("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsEqualTo("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsNotEqualTo("score", FINITE).test(metadata)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldTreatNegativeInfinityAsSmallerThanFiniteValues() {
|
||||
Metadata metadata = metadata(Double.NEGATIVE_INFINITY);
|
||||
|
||||
assertThat(new IsLessThan("score", FINITE).test(metadata)).isTrue();
|
||||
assertThat(new IsLessThanOrEqualTo("score", FINITE).test(metadata)).isTrue();
|
||||
assertThat(new IsGreaterThan("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsGreaterThanOrEqualTo("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsEqualTo("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsNotEqualTo("score", FINITE).test(metadata)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldConsiderInfinityEqualToItself() {
|
||||
Metadata metadata = metadata(Double.POSITIVE_INFINITY);
|
||||
|
||||
assertThat(new IsEqualTo("score", Double.POSITIVE_INFINITY).test(metadata))
|
||||
.isTrue();
|
||||
assertThat(new IsEqualTo("score", Double.NEGATIVE_INFINITY).test(metadata))
|
||||
.isFalse();
|
||||
assertThat(new IsGreaterThan("score", Double.NEGATIVE_INFINITY).test(metadata))
|
||||
.isTrue();
|
||||
assertThat(new IsIn("score", List.of(FINITE, Double.POSITIVE_INFINITY)).test(metadata))
|
||||
.isTrue();
|
||||
assertThat(new IsNotIn("score", List.of(FINITE, Double.POSITIVE_INFINITY)).test(metadata))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOrderFiniteValuesAgainstInfiniteComparisonValue() {
|
||||
Metadata metadata = metadata(FINITE);
|
||||
|
||||
assertThat(new IsLessThan("score", Double.POSITIVE_INFINITY).test(metadata))
|
||||
.isTrue();
|
||||
assertThat(new IsGreaterThan("score", Double.NEGATIVE_INFINITY).test(metadata))
|
||||
.isTrue();
|
||||
assertThat(new IsEqualTo("score", Double.POSITIVE_INFINITY).test(metadata))
|
||||
.isFalse();
|
||||
assertThat(new IsIn("score", List.of(Double.POSITIVE_INFINITY)).test(metadata))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOrderIntegralValuesAgainstInfiniteComparisonValue() {
|
||||
Metadata metadata = new Metadata().put("score", Long.MAX_VALUE);
|
||||
|
||||
assertThat(new IsLessThan("score", Double.POSITIVE_INFINITY).test(metadata))
|
||||
.isTrue();
|
||||
assertThat(new IsGreaterThan("score", Double.NEGATIVE_INFINITY).test(metadata))
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchNaNMetadataValue() {
|
||||
Metadata metadata = metadata(Double.NaN);
|
||||
|
||||
assertThat(new IsEqualTo("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsGreaterThan("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsGreaterThanOrEqualTo("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsLessThan("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsLessThanOrEqualTo("score", FINITE).test(metadata)).isFalse();
|
||||
assertThat(new IsIn("score", List.of(FINITE)).test(metadata)).isFalse();
|
||||
|
||||
assertThat(new IsNotEqualTo("score", FINITE).test(metadata)).isTrue();
|
||||
assertThat(new IsNotIn("score", List.of(FINITE)).test(metadata)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchNaNComparisonValue() {
|
||||
Metadata metadata = metadata(FINITE);
|
||||
|
||||
assertThat(new IsEqualTo("score", Double.NaN).test(metadata)).isFalse();
|
||||
assertThat(new IsGreaterThan("score", Double.NaN).test(metadata)).isFalse();
|
||||
assertThat(new IsGreaterThanOrEqualTo("score", Double.NaN).test(metadata))
|
||||
.isFalse();
|
||||
assertThat(new IsLessThan("score", Double.NaN).test(metadata)).isFalse();
|
||||
assertThat(new IsLessThanOrEqualTo("score", Double.NaN).test(metadata)).isFalse();
|
||||
assertThat(new IsIn("score", List.of(Double.NaN)).test(metadata)).isFalse();
|
||||
|
||||
assertThat(new IsNotEqualTo("score", Double.NaN).test(metadata)).isTrue();
|
||||
assertThat(new IsNotIn("score", List.of(Double.NaN)).test(metadata)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotConsiderNaNEqualToItself() {
|
||||
Metadata metadata = metadata(Double.NaN);
|
||||
|
||||
assertThat(new IsEqualTo("score", Double.NaN).test(metadata)).isFalse();
|
||||
assertThat(new IsIn("score", List.of(Double.NaN)).test(metadata)).isFalse();
|
||||
|
||||
assertThat(new IsNotEqualTo("score", Double.NaN).test(metadata)).isTrue();
|
||||
assertThat(new IsNotIn("score", List.of(Double.NaN)).test(metadata)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotOrderNaNAgainstInfinity() {
|
||||
Metadata metadata = metadata(Double.NaN);
|
||||
|
||||
assertThat(new IsGreaterThan("score", Double.POSITIVE_INFINITY).test(metadata))
|
||||
.isFalse();
|
||||
assertThat(new IsLessThan("score", Double.POSITIVE_INFINITY).test(metadata))
|
||||
.isFalse();
|
||||
assertThat(new IsGreaterThan("score", Double.NEGATIVE_INFINITY).test(metadata))
|
||||
.isFalse();
|
||||
assertThat(new IsLessThan("score", Double.NEGATIVE_INFINITY).test(metadata))
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleFloatValuesLikeDoubleValues() {
|
||||
assertThat(new IsGreaterThan("score", 0.5f).test(metadata(Float.POSITIVE_INFINITY)))
|
||||
.isTrue();
|
||||
assertThat(new IsLessThan("score", 0.5f).test(metadata(Float.NEGATIVE_INFINITY)))
|
||||
.isTrue();
|
||||
assertThat(new IsEqualTo("score", Float.POSITIVE_INFINITY).test(metadata(Float.POSITIVE_INFINITY)))
|
||||
.isTrue();
|
||||
assertThat(new IsEqualTo("score", 0.5f).test(metadata(Float.NaN))).isFalse();
|
||||
assertThat(new IsNotEqualTo("score", 0.5f).test(metadata(Float.NaN))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCompareFloatAndDoubleNonFiniteValues() {
|
||||
assertThat(new IsEqualTo("score", Double.POSITIVE_INFINITY).test(metadata(Float.POSITIVE_INFINITY)))
|
||||
.isTrue();
|
||||
assertThat(new IsGreaterThan("score", Double.NEGATIVE_INFINITY).test(metadata(Float.POSITIVE_INFINITY)))
|
||||
.isTrue();
|
||||
assertThat(new IsEqualTo("score", Double.NaN).test(metadata(Float.NaN))).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotChangeComparisonOfFiniteValues() {
|
||||
Metadata metadata = new Metadata().put("score", 1L);
|
||||
|
||||
assertThat(new IsEqualTo("score", 1).test(metadata)).isTrue();
|
||||
assertThat(new IsEqualTo("score", 1.0).test(metadata)).isTrue();
|
||||
assertThat(new IsGreaterThan("score", 0).test(metadata)).isTrue();
|
||||
assertThat(new IsIn("score", List.of(1.0, 2.0)).test(metadata)).isTrue();
|
||||
assertThat(new IsNotIn("score", List.of(2.0, 3.0)).test(metadata)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepPrecisionBeyondDoubleForFiniteValues() {
|
||||
// 9007199254740992 and 9007199254740993 are distinct longs that collapse onto the same double,
|
||||
// so comparing them as doubles would wrongly report them as equal
|
||||
Metadata metadata = new Metadata().put("score", 9007199254740993L);
|
||||
|
||||
assertThat(new IsEqualTo("score", 9007199254740992L).test(metadata)).isFalse();
|
||||
assertThat(new IsGreaterThan("score", 9007199254740992L).test(metadata)).isTrue();
|
||||
}
|
||||
|
||||
private static Metadata metadata(double value) {
|
||||
return new Metadata().put("score", value);
|
||||
}
|
||||
|
||||
private static Metadata metadata(float value) {
|
||||
return new Metadata().put("score", value);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user