|
6 | 6 |
|
7 | 7 | import ai.timefold.solver.core.api.score.stream.bi.BiJoiner; |
8 | 8 |
|
| 9 | +import org.jspecify.annotations.NullMarked; |
| 10 | +import org.jspecify.annotations.Nullable; |
| 11 | + |
9 | 12 | /** |
10 | 13 | * Combs an array of {@link BiJoiner} instances into a mergedJoiner and a mergedFiltering. |
11 | 14 | * |
12 | 15 | * @param <A> |
13 | 16 | * @param <B> |
14 | 17 | */ |
| 18 | +@NullMarked |
15 | 19 | public final class BiJoinerComber<A, B> { |
16 | 20 |
|
| 21 | + @SafeVarargs |
17 | 22 | public static <A, B> BiJoinerComber<A, B> comb(BiJoiner<A, B>... joiners) { |
18 | | - List<DefaultBiJoiner<A, B>> defaultJoinerList = new ArrayList<>(joiners.length); |
19 | | - List<BiPredicate<A, B>> filteringList = new ArrayList<>(joiners.length); |
| 23 | + var defaultJoinerList = new ArrayList<DefaultBiJoiner<A, B>>(joiners.length); |
| 24 | + var filteringList = new ArrayList<BiPredicate<A, B>>(joiners.length); |
20 | 25 |
|
21 | | - int indexOfFirstFilter = -1; |
| 26 | + var indexOfFirstFilter = -1; |
22 | 27 | // Make sure all indexing joiners, if any, come before filtering joiners. This is necessary for performance. |
23 | | - for (int i = 0; i < joiners.length; i++) { |
24 | | - BiJoiner<A, B> joiner = joiners[i]; |
| 28 | + for (var i = 0; i < joiners.length; i++) { |
| 29 | + var joiner = joiners[i]; |
25 | 30 | if (joiner instanceof FilteringBiJoiner) { |
26 | 31 | // From now on, only allow filtering joiners. |
27 | 32 | indexOfFirstFilter = i; |
28 | 33 | filteringList.add(((FilteringBiJoiner<A, B>) joiner).getFilter()); |
29 | 34 | } else if (joiner instanceof DefaultBiJoiner) { |
30 | 35 | if (indexOfFirstFilter >= 0) { |
31 | | - throw new IllegalStateException("Indexing joiner (" + joiner + ") must not follow " + |
32 | | - "a filtering joiner (" + joiners[indexOfFirstFilter] + ").\n" + |
33 | | - "Maybe reorder the joiners such that filtering() joiners are later in the parameter list."); |
| 36 | + throw new IllegalStateException(""" |
| 37 | + Indexing joiner (%s) must not follow a filtering joiner (%s). |
| 38 | + Maybe reorder the joiners such that filtering() joiners are later in the parameter list.""" |
| 39 | + .formatted(joiner, joiners[indexOfFirstFilter])); |
34 | 40 | } |
35 | 41 | defaultJoinerList.add((DefaultBiJoiner<A, B>) joiner); |
36 | 42 | } else { |
37 | | - throw new IllegalArgumentException("The joiner class (" + joiner.getClass() + ") is not supported."); |
| 43 | + throw new IllegalArgumentException("The joiner class (%s) is not supported." |
| 44 | + .formatted(joiner.getClass())); |
38 | 45 | } |
39 | 46 | } |
40 | | - DefaultBiJoiner<A, B> mergedJoiner = DefaultBiJoiner.merge(defaultJoinerList); |
41 | | - BiPredicate<A, B> mergedFiltering = mergeFiltering(filteringList); |
| 47 | + var mergedJoiner = DefaultBiJoiner.merge(defaultJoinerList); |
| 48 | + var mergedFiltering = mergeFiltering(filteringList); |
42 | 49 | return new BiJoinerComber<>(mergedJoiner, mergedFiltering); |
43 | 50 | } |
44 | 51 |
|
45 | | - private static <A, B> BiPredicate<A, B> mergeFiltering(List<BiPredicate<A, B>> filteringList) { |
46 | | - if (filteringList.isEmpty()) { |
47 | | - return null; |
48 | | - } |
49 | | - switch (filteringList.size()) { |
50 | | - case 1: |
51 | | - return filteringList.get(0); |
52 | | - case 2: |
53 | | - return filteringList.get(0).and(filteringList.get(1)); |
54 | | - default: |
55 | | - // Avoid predicate.and() when more than 2 predicates for debugging and potentially performance |
56 | | - return (A a, B b) -> { |
57 | | - for (BiPredicate<A, B> predicate : filteringList) { |
| 52 | + @SuppressWarnings("unchecked") |
| 53 | + private static <A, B> @Nullable BiPredicate<A, B> mergeFiltering(List<BiPredicate<A, B>> filteringList) { |
| 54 | + return switch (filteringList.size()) { |
| 55 | + case 0 -> null; |
| 56 | + case 1 -> filteringList.getFirst(); |
| 57 | + default -> { |
| 58 | + // Avoid predicate.and() for debugging and potential performance |
| 59 | + var filteringArray = filteringList.toArray(new BiPredicate[0]); |
| 60 | + yield (A a, B b) -> { |
| 61 | + for (var predicate : filteringArray) { |
58 | 62 | if (!predicate.test(a, b)) { |
59 | 63 | return false; |
60 | 64 | } |
61 | 65 | } |
62 | 66 | return true; |
63 | 67 | }; |
64 | | - } |
| 68 | + } |
| 69 | + }; |
65 | 70 | } |
66 | 71 |
|
67 | 72 | private DefaultBiJoiner<A, B> mergedJoiner; |
68 | | - private final BiPredicate<A, B> mergedFiltering; |
| 73 | + private final @Nullable BiPredicate<A, B> mergedFiltering; |
69 | 74 |
|
70 | | - public BiJoinerComber(DefaultBiJoiner<A, B> mergedJoiner, BiPredicate<A, B> mergedFiltering) { |
| 75 | + public BiJoinerComber(DefaultBiJoiner<A, B> mergedJoiner, @Nullable BiPredicate<A, B> mergedFiltering) { |
71 | 76 | this.mergedJoiner = mergedJoiner; |
72 | 77 | this.mergedFiltering = mergedFiltering; |
73 | 78 | } |
74 | 79 |
|
75 | 80 | /** |
76 | | - * @return never null |
| 81 | + * Returns the merged indexing joiner, |
| 82 | + * reordered equal-first so the indexer chain always has its (merged) equal level at the top. |
| 83 | + * Computed on read to also cover {@link #addJoiner} appends. |
77 | 84 | */ |
78 | 85 | public DefaultBiJoiner<A, B> getMergedJoiner() { |
79 | | - return mergedJoiner; |
| 86 | + return mergedJoiner.reorderedEqualsFirst(); |
80 | 87 | } |
81 | 88 |
|
82 | | - /** |
83 | | - * @return null if not applicable |
84 | | - */ |
85 | | - public BiPredicate<A, B> getMergedFiltering() { |
| 89 | + public @Nullable BiPredicate<A, B> getMergedFiltering() { |
86 | 90 | return mergedFiltering; |
87 | 91 | } |
88 | 92 |
|
|
0 commit comments