Skip to content
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

Fix match ordering #1101

Merged
merged 1 commit into from
Nov 14, 2024
Merged
Show file tree
Hide file tree
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 @@ -40,12 +40,27 @@
private static final Logger LOGGER = LoggerFactory.getLogger(MultiExpression.class);
private static final Comparator<WithId> BY_ID = Comparator.comparingInt(WithId::id);

/**
* Returns a new multi-expression from {@code expressions} where multiple expressions for the same key get OR'd
* together.
* <p>
* If the order in which expresions match matter, use {@link #ofOrdered(List)} instead.
*/
public static <T> MultiExpression<T> of(List<Entry<T>> expressions) {
LinkedHashMap<T, Expression> map = new LinkedHashMap<>();
for (var expression : expressions) {
map.merge(expression.result, expression.expression, Expression::or);
}
return new MultiExpression<>(map.entrySet().stream().map(e -> entry(e.getKey(), e.getValue())).collect(Collectors.toList()));
return new MultiExpression<>(
map.entrySet().stream().map(e -> entry(e.getKey(), e.getValue())).collect(Collectors.toList()));

Check warning on line 55 in planetiler-core/src/main/java/com/onthegomap/planetiler/expression/MultiExpression.java

View workflow job for this annotation

GitHub Actions / Analyze with Sonar

MAJOR CODE_SMELL

Replace this usage of 'Stream.collect(Collectors.toList())' with 'Stream.toList()' and ensure that the list is unmodified. rule: java:S6204 (https://sonarcloud.io/organizations/onthegomap/rules?open=java%3AS6204&rule_key=java%3AS6204) issue url: https://sonarcloud.io/project/issues?pullRequest=1101&open=AZMqagJlQ-Axrsj-_4wF&id=onthegomap_planetiler
}

/**
* Returns a new multi-expression from {@code expressions} where multiple expressions for the same key stay separate,
* in cases where the order in which expressions matches is important.
*/
public static <T> MultiExpression<T> ofOrdered(List<Entry<T>> expressions) {
return new MultiExpression<>(new ArrayList<>(expressions));
}

public static <T> Entry<T> entry(T result, Expression expression) {
Expand All @@ -63,8 +78,8 @@
case Expression.Not(var child) -> !mustAlwaysEvaluate(child);
case Expression.MatchAny any when any.mustAlwaysEvaluate() -> true;
case null, default -> !(expression instanceof Expression.MatchAny) &&
!(expression instanceof Expression.MatchField) &&
!FALSE.equals(expression);
!(expression instanceof Expression.MatchField) &&
!FALSE.equals(expression);
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -729,33 +729,24 @@ private static SourceFeature point(String source, String layer, Map<String, Obje
return SimpleFeature.create(newPoint(0, 0), tags, source, layer, 1);
}


@Test
void testNestedMatching() {
var index = MultiExpression.of(List.of(
entry("a", matchField("a.b")),
entry("b", matchAny("a.b", "c", "d")),
entry("c", matchAny("a", "e")),
entry("d", matchAny("a[].b", "c"))
void testMatchOrder() {
var index = MultiExpression.ofOrdered(List.of(
entry("green", matchAny("natural", "tree")),
entry("black", matchAny("historic", "memorial")),
entry("green", matchAny("tourism", "viewpoint"))
)).index();

assertSameElements(List.of(), index.getMatches(WithTags.from(Map.of("k", "v"))));
assertSameElements(List.of("c"), index.getMatches(WithTags.from(Map.of("a", "e"))));
assertSameElements(List.of("c"), index.getMatches(WithTags.from(Map.of("a", List.of("e")))));
assertSameElements(List.of("c"), index.getMatches(WithTags.from(Map.of("a", List.of("e", "f")))));
assertSameElements(List.of(), index.getMatches(WithTags.from(Map.of("a", List.of("g", "f")))));


assertSameElements(List.of("a"), index.getMatches(WithTags.from(Map.of("a.b", "e"))));
assertSameElements(List.of("a", "b"), index.getMatches(WithTags.from(Map.of("a.b", "c"))));
assertSameElements(List.of(), index.getMatches(WithTags.from(Map.of("a", Map.of("b", List.of())))));
assertSameElements(List.of("a", "b", "d"), index.getMatches(WithTags.from(Map.of("a", Map.of("b", List.of("c"))))));
assertSameElements(List.of("a", "b"), index.getMatches(WithTags.from(Map.of("a", Map.of("b", List.of("d"))))));
assertSameElements(List.of("a"), index.getMatches(WithTags.from(Map.of("a", Map.of("b", List.of("e"))))));
assertSameElements(List.of("a"), index.getMatches(WithTags.from(Map.of("a", Map.of("b", Map.of("c", "e"))))));
assertEquals(List.of("black", "green"), index.getMatches(WithTags.from(Map.of(
"historic", "memorial",
"tourism", "viewpoint"
))));

assertSameElements(List.of("a", "b", "c", "d"),
index.getMatches(WithTags.from(Map.of("a", List.of("e", Map.of("b", List.of("c")))))));
// multiple matches allowed now when there are duplicate keys
assertEquals(List.of("green", "green"), index.getMatches(WithTags.from(Map.of(
"natural", "tree",
"tourism", "viewpoint"
))));
}

private static <T> void assertSameElements(List<T> a, List<T> b) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ private <O> ConfigExpression.Match<I, O> parseMatch(Object match, boolean allowE
} else {
throw new ParseException("Invalid match block. Expected a list or map, but got: " + match);
}
return ConfigExpression.match(signature(output), MultiExpression.of(List.copyOf(conditions)), fallback);
return ConfigExpression.match(signature(output), MultiExpression.ofOrdered(List.copyOf(conditions)), fallback);
}

private <O> Signature<I, O> signature(Class<O> outputClass) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public ConfigExpression<I, O> simplifyOnce() {
if (Expression.TRUE.equals(expression.expression())) {
return new Match<>(
signature,
MultiExpression.of(expressions.stream().limit(i).toList()),
MultiExpression.ofOrdered(expressions.stream().limit(i).toList()),
expression.result()
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1602,4 +1602,38 @@ void testGeometryAttributesArea(String expression, double expected) {
testFeature(config, sfMatch,
any -> assertEquals(expected, (Double) any.getAttrsAtZoom(14).get("attr"), expected / 1e3), 1);
}


@ParameterizedTest
@ValueSource(booleans = {false, true})
void testMatchOrdering(boolean withFallback) {
var config = """
sources:
osm:
type: osm
url: geofabrik:rhode-island
local_path: data/rhode-island.osm.pbf
layers:
- id: testLayer
features:
- source: osm
attributes:
- key: attr
value:
- if: {natural: tree}
value: green
- if: {historic: memorial}
value: black
- if: {tourism: viewpoint}
value: green
- if: ${%s}
value: fallback

""".formatted(withFallback ? "true" : "false");
testFeature(config, SimpleFeature.createFakeOsmFeature(newPoint(0, 0), Map.of(
"historic", "memorial",
"tourism", "viewpoint"
), "osm", null, 1, emptyList(), OSM_INFO), feature -> assertEquals("black", feature.getAttrsAtZoom(14).get("attr")),
1);
}
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
<importOrder/>
<removeUnusedImports/>
<eclipse>
<version>4.29</version>
<version>4.33</version>
<!--suppress UnresolvedMavenProperty -->
<file>${maven.multiModuleProjectDirectory}/eclipse-formatter.xml</file>
</eclipse>
Expand Down
Loading