001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2024–2025 microBean™.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
006 * the License.  You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
011 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
012 * specific language governing permissions and limitations under the License.
013 */
014package org.microbean.bean;
015
016import java.util.ArrayList;
017import java.util.List;
018
019import java.util.function.BiFunction;
020
021import static java.util.Collections.unmodifiableList;
022
023import static org.microbean.bean.Ranked.DEFAULT_RANK;
024
025/**
026 * A {@link Reducer} implementation that works with {@link Ranked} objects.
027 *
028 * @param <C> the type of criteria used in the {@link #reduce(List, Object, BiFunction)} method
029 *
030 * @param <T> a {@link Ranked} type
031 *
032 * @author <a href="https://about.me/lairdnelson" target="_top">Laird Nelson</a>
033 *
034 * @see #reduce(List, Object, BiFunction)
035 */
036public final class RankedReducer<C, T extends Ranked> implements Reducer<C, T> {
037
038
039  /*
040   * Static fields.
041   */
042
043
044  private static final RankedReducer<?, ?> INSTANCE = new RankedReducer<>();
045
046
047  /*
048   * Constructors.
049   */
050
051
052  private RankedReducer() {
053    super();
054  }
055
056
057  /*
058   * Instance methods.
059   */
060
061
062  @Override
063  public final T reduce(final List<? extends T> elements,
064                        final C c,
065                        final BiFunction<? super List<? extends T>, ? super C, ? extends T> failureHandler) {
066    if (elements == null || elements.isEmpty()) {
067      // https://github.com/microbean/microbean-bean/issues/21
068      return failureHandler == null ? Reducer.fail(List.of(), c) : failureHandler.apply(List.of(), c);
069    } else if (elements.size() == 1) {
070      return elements.get(0);
071    }
072
073    T candidate = null;
074    List<T> unresolved = null;
075    // Highest rank wins
076    int maxRank = DEFAULT_RANK;
077
078    for (final T element : elements) {
079      if (alternate(element)) {
080        final int elementRank = rank(element);
081        if (elementRank == maxRank) {
082          if (candidate == null || !alternate(candidate)) {
083            // Prefer elements regardless of ranks.
084            candidate = element;
085          } else {
086            assert rank(candidate) == maxRank : "Unexpected rank: " + rank(candidate) + "; was expecting: " + maxRank;
087            // The existing candidate is an alternate and by definition has the highest rank we've seen so far; the
088            // incoming element is also an alternate; both have equal ranks: we can't resolve this.
089            if (unresolved == null) {
090              unresolved = new ArrayList<>(6);
091            }
092            unresolved.add(candidate);
093            unresolved.add(element);
094            candidate = null;
095          }
096        } else if (elementRank > maxRank) {
097          if (candidate == null || !alternate(candidate) || elementRank > rank(candidate)) {
098            // The existing candidate is either null, not an alternate (and alternates are always preferred), or an
099            // alternate with losing rank, so junk it in favor of the incoming element.
100            candidate = element;
101            // We have a new maxRank.
102            maxRank = elementRank;
103          } else if (elementRank == rank(candidate)) {
104            // The existing candidate is also an alternate and has the same rank.
105            if (unresolved == null) {
106              unresolved = new ArrayList<>(6);
107            }
108            unresolved.add(candidate);
109            unresolved.add(element);
110            candidate = null;
111          } else {
112            assert elementRank < rank(candidate) : "elementRank >= rank(candidate): " + elementRank + " >= " + rank(candidate);
113            // The existing candidate is also an alternate but has a higher rank than the alternate, so keep it (do
114            // nothing).
115          }
116        }
117        // ...else drop element by doing nothing
118      } else if (candidate == null) {
119        // The incoming element is not an alternate, but that doesn't matter; the candidate is null, so accept the
120        // element no matter what.
121        candidate = element;
122      } else if (!alternate(candidate)) {
123        // The existing candidate is not an alternate. The incoming element is not an alternate. Ranks in this case are
124        // irrelevant, perhaps surprisingly. We cannot resolve this.
125        if (unresolved == null) {
126          unresolved = new ArrayList<>(6);
127        }
128        unresolved.add(candidate);
129        unresolved.add(element);
130        candidate = null;
131      }
132      // ...else do nothing
133    }
134
135    if (unresolved != null && !unresolved.isEmpty()) {
136      if (candidate != null) {
137        unresolved.add(candidate);
138      }
139      candidate =
140        failureHandler == null ? Reducer.fail(unmodifiableList(unresolved), c) : failureHandler.apply(unmodifiableList(unresolved), c);
141    }
142
143    return candidate;
144  }
145
146  // Preparing for a future where alternate status is not a first-class citizen.
147  private final boolean alternate(final T t) {
148    return t.alternate();
149  }
150
151  // Preparing for a future where rank is not a first-class citizen.
152  private final int rank(final T t) {
153    return t.rank();
154  }
155
156
157  /*
158   * Static methods.
159   */
160
161
162  /**
163   * Returns a {@link RankedReducer} implementation.
164   *
165   * @param <C> the type of criteria
166   *
167   * @param <T> the type of the {@link Ranked} reduction
168   *
169   * @return a {@link RankedReducer} implementation; never {@code null}
170   *
171   * @microbean.nullability This method never returns {@code null}.
172   *
173   * @microbean.idempotency This method is idempotent and deterministic.
174   *
175   * @microbean.threadsafety This method is safe for concurrent use by multiple threads.
176   */
177  @SuppressWarnings("unchecked")
178  public static final <C, T extends Ranked> Reducer<C, T> of() {
179    return (Reducer<C, T>)INSTANCE;
180  }
181
182}