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:
Tunde Jaiyesmi
2026-08-20 09:58:39 +01:00
committed by GitHub
parent 1e6a1d9eac
commit b30925e091
11 changed files with 307 additions and 23 deletions
@@ -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
@@ -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) {
@@ -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;
@@ -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;
@@ -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);
@@ -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;
@@ -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;
@@ -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) {
@@ -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);
@@ -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();
}
}
@@ -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);
}
}