Skip to content

[8.19] Add an error margin when comparing floats. (#129721) #129870

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 24, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.function.Supplier;

import static org.elasticsearch.datageneration.matchers.Messages.formatErrorMessage;
import static org.elasticsearch.datageneration.matchers.Messages.prettyPrintCollections;

class DynamicFieldMatcher {
private static final double FLOAT_ERROR_MARGIN = 1e-8;
private final XContentBuilder actualMappings;
private final Settings.Builder actualSettings;
private final XContentBuilder expectedMappings;
Expand Down Expand Up @@ -62,31 +62,51 @@ public MatchResult match(List<Object> actual, List<Object> expected) {

var normalizedActual = normalizeDoubles(actual);
var normalizedExpected = normalizeDoubles(expected);
Supplier<MatchResult> noMatchSupplier = () -> MatchResult.noMatch(
formatErrorMessage(
actualMappings,
actualSettings,
expectedMappings,
expectedSettings,
"Values of dynamically mapped field containing double values don't match after normalization, normalized "
+ prettyPrintCollections(normalizedActual, normalizedExpected)
)
);

return normalizedActual.equals(normalizedExpected)
? MatchResult.match()
: MatchResult.noMatch(
formatErrorMessage(
actualMappings,
actualSettings,
expectedMappings,
expectedSettings,
"Values of dynamically mapped field containing double values don't match after normalization, normalized "
+ prettyPrintCollections(normalizedActual, normalizedExpected)
)
);
if (normalizedActual.size() != normalizedExpected.size()) {
return noMatchSupplier.get();
}

for (int i = 0; i < normalizedActual.size(); i++) {
if (floatEquals(normalizedActual.get(i), normalizedExpected.get(i)) == false) {
return noMatchSupplier.get();
}
}

return MatchResult.match();
}

return matchWithGenericMatcher(actual, expected);
}

private static Set<Float> normalizeDoubles(List<Object> values) {
/**
* We make the normalisation of double values stricter than {@link SourceTransforms#normalizeValues} to facilitate the equality of the
* values within a margin of error. Synthetic source does support duplicate values and preserves the order, but it loses some accuracy,
* this is why the margin of error is very important. In the future, we can make {@link SourceTransforms#normalizeValues} also stricter.
*/
private static List<Float> normalizeDoubles(List<Object> values) {
if (values == null) {
return Set.of();
return List.of();
}

Function<Object, Float> toFloat = (o) -> o instanceof Number n ? n.floatValue() : Float.parseFloat((String) o);
return values.stream().filter(Objects::nonNull).map(toFloat).collect(Collectors.toSet());

// We skip nulls because they trip the pretty print collections.
return values.stream().filter(Objects::nonNull).map(toFloat).toList();
}

private static boolean floatEquals(Float actual, Float expected) {
return Math.abs(actual - expected) < FLOAT_ERROR_MARGIN;
}

private MatchResult matchWithGenericMatcher(List<Object> actualValues, List<Object> expectedValues) {
Expand Down