From 832e0e37ad28485b21954181044ebb47c79a5642 Mon Sep 17 00:00:00 2001 From: Mathieu Deharbe <148252167+Mathieu-Deharbe@users.noreply.github.com> Date: Tue, 26 Nov 2024 16:25:29 +0100 Subject: [PATCH] dynamic rounding of results for comparisons (#151) Signed-off-by: Mathieu DEHARBE --- .../server/dto/ResourceFilterDTO.java | 6 +++- .../specifications/SpecificationUtils.java | 30 ++++++++++++++++--- .../FindPreContingencyLimitViolationTest.java | 11 +++++-- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/gridsuite/securityanalysis/server/dto/ResourceFilterDTO.java b/src/main/java/org/gridsuite/securityanalysis/server/dto/ResourceFilterDTO.java index f75f93a7..2f48e209 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/dto/ResourceFilterDTO.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/dto/ResourceFilterDTO.java @@ -15,9 +15,13 @@ * @param type the type of filter (contains, startsWith...) * @param value the value of the filter * @param column the column / field on which the filter will be applied + * @param tolerance precision/tolerance used for the comparisons (simulates the rounding of the database values) Only useful for numbers. * @author Kevin Le Saulnier */ -public record ResourceFilterDTO(DataType dataType, Type type, Object value, String column) { +public record ResourceFilterDTO(DataType dataType, Type type, Object value, String column, Double tolerance) { + public ResourceFilterDTO(DataType dataType, Type type, Object value, String column) { + this(dataType, type, value, column, null); + } public enum DataType { @JsonProperty("text") diff --git a/src/main/java/org/gridsuite/securityanalysis/server/repositories/specifications/SpecificationUtils.java b/src/main/java/org/gridsuite/securityanalysis/server/repositories/specifications/SpecificationUtils.java index 2a591122..53722a58 100644 --- a/src/main/java/org/gridsuite/securityanalysis/server/repositories/specifications/SpecificationUtils.java +++ b/src/main/java/org/gridsuite/securityanalysis/server/repositories/specifications/SpecificationUtils.java @@ -52,9 +52,17 @@ public static Specification startsWith(String field, String value) { return (root, cq, cb) -> cb.like(cb.upper(getColumnPath(root, field)), EscapeCharacter.DEFAULT.escape(value).toUpperCase() + "%", EscapeCharacter.DEFAULT.getEscapeCharacter()); } + /** + * Returns a specification where the field value is not equal within the given tolerance. + */ public static Specification notEqual(String field, Double value, Double tolerance) { return (root, cq, cb) -> { Expression doubleExpression = getColumnPath(root, field).as(Double.class); + /** + * in order to be equal to doubleExpression, value has to fit : + * value - tolerance <= doubleExpression <= value + tolerance + * therefore in order to be different at least one of the opposite comparison needs to be true : + */ return cb.or( cb.greaterThan(doubleExpression, value + tolerance), cb.lessThan(doubleExpression, value - tolerance) @@ -137,13 +145,27 @@ private static Specification appendTextFilterToSpecification(Specificatio @NotNull private static Specification appendNumberFilterToSpecification(Specification specification, ResourceFilterDTO resourceFilter) { - final double tolerance = 0.00001; // tolerance for comparison String value = resourceFilter.value().toString(); - return createNumberPredicate(specification, resourceFilter, value, tolerance); + return createNumberPredicate(specification, resourceFilter, value); } - private static Specification createNumberPredicate(Specification specification, ResourceFilterDTO resourceFilter, String value, double tolerance) { - Double valueDouble = Double.valueOf(value); + private static Specification createNumberPredicate(Specification specification, ResourceFilterDTO resourceFilter, String filterValue) { + double tolerance; + if (resourceFilter.tolerance() != null) { + tolerance = resourceFilter.tolerance(); + } else { + // the reference for the comparison is the number of digits after the decimal point in filterValue + // extra digits are ignored, but the user may add '0's after the decimal point in order to get a better precision + String[] splitValue = filterValue.split("\\."); + int numberOfDecimalAfterDot = 0; + if (splitValue.length > 1) { + numberOfDecimalAfterDot = splitValue[1].length(); + } + // tolerance is multiplied by 0.5 to simulate the fact that the database value is rounded (in the front, from the user viewpoint) + // more than 13 decimal after dot will likely cause rounding errors due to double precision + tolerance = Math.pow(10, -numberOfDecimalAfterDot) * 0.5; + } + Double valueDouble = Double.valueOf(filterValue); return switch (resourceFilter.type()) { case NOT_EQUAL -> specification.and(notEqual(resourceFilter.column(), valueDouble, tolerance)); case LESS_THAN_OR_EQUAL -> diff --git a/src/test/java/org/gridsuite/securityanalysis/server/FindPreContingencyLimitViolationTest.java b/src/test/java/org/gridsuite/securityanalysis/server/FindPreContingencyLimitViolationTest.java index c9e934f5..8d957d0c 100644 --- a/src/test/java/org/gridsuite/securityanalysis/server/FindPreContingencyLimitViolationTest.java +++ b/src/test/java/org/gridsuite/securityanalysis/server/FindPreContingencyLimitViolationTest.java @@ -75,7 +75,10 @@ void findFilteredPrecontingencyLimitViolationResultsTest(List List preContingencyLimitViolation = securityAnalysisResultService.findNResult(resultEntity.getId(), filters, sort); // assert subject ids to check parent filters - assertThat(preContingencyLimitViolation).extracting(SubjectLimitViolationEntity.Fields.subjectId).containsExactlyElementsOf(expectedResult.stream().map(PreContingencyLimitViolationResultDTO::getSubjectId).toList()); + assertThat(preContingencyLimitViolation).extracting(SubjectLimitViolationEntity.Fields.subjectId) + .containsExactlyElementsOf( + expectedResult.stream().map(PreContingencyLimitViolationResultDTO::getSubjectId).toList() + ); assertSelectCount(expectedSelectCount); } @@ -110,9 +113,11 @@ private static Stream provideChildFilter() { private static Stream provideChildFilterWithTolerance() { return Stream.of( - Arguments.of(List.of(new ResourceFilterDTO(ResourceFilterDTO.DataType.NUMBER, ResourceFilterDTO.Type.NOT_EQUAL, "11.02425", AbstractLimitViolationEntity.Fields.value), + Arguments.of(List.of( + new ResourceFilterDTO(ResourceFilterDTO.DataType.NUMBER, ResourceFilterDTO.Type.NOT_EQUAL, "11.02425", AbstractLimitViolationEntity.Fields.value), new ResourceFilterDTO(ResourceFilterDTO.DataType.NUMBER, ResourceFilterDTO.Type.LESS_THAN_OR_EQUAL, "10.51243", AbstractLimitViolationEntity.Fields.limit), - new ResourceFilterDTO(ResourceFilterDTO.DataType.NUMBER, ResourceFilterDTO.Type.GREATER_THAN_OR_EQUAL, "1.00001", AbstractLimitViolationEntity.Fields.limitReduction)), Sort.by(Sort.Direction.ASC, "limit"), + new ResourceFilterDTO(ResourceFilterDTO.DataType.NUMBER, ResourceFilterDTO.Type.GREATER_THAN_OR_EQUAL, "0.999999", AbstractLimitViolationEntity.Fields.limitReduction) + ), Sort.by(Sort.Direction.ASC, "limit"), getResultPreContingencyWithNestedFilter(p -> p.getLimitViolation().getLimit() <= 10.51243) .stream().sorted(Comparator.comparing(x -> x.getLimitViolation().getLimit())).toList(), 2) );