001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2024 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
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      return null;
068    } else if (elements.size() == 1) {
069      return elements.get(0);
070    }
071
072    T candidate = null;
073    List<T> unresolved = null;
074    // Highest rank wins
075    int maxRank = DEFAULT_RANK;
076
077    for (final T element : elements) {
078      if (element.alternate()) {
079        final int elementRank = element.rank();
080        if (elementRank == maxRank) {
081          if (candidate == null || !candidate.alternate()) {
082            // Prefer elements regardless of ranks.
083            candidate = element;
084          } else {
085            assert candidate.rank() == maxRank : "Unexpected rank: " + candidate.rank() + "; was expecting: " + maxRank;
086            // The existing candidate is an alternate and by definition has the highest rank we've seen so far; the
087            // incoming element is also an alternate; both have equal ranks: we can't resolve this.
088            if (unresolved == null) {
089              unresolved = new ArrayList<>(6);
090            }
091            unresolved.add(candidate);
092            unresolved.add(element);
093            candidate = null;
094          }
095        } else if (elementRank > maxRank) {
096          if (candidate == null || !candidate.alternate() || elementRank > candidate.rank()) {
097            // The existing candidate is either null, not an alternate (and alternates are always preferred), or an
098            // alternate with losing rank, so junk it in favor of the incoming element.
099            candidate = element;
100            // We have a new maxRank.
101            maxRank = elementRank;
102          } else if (elementRank == candidate.rank()) {
103            // The existing candidate is also an alternate and has the same rank.
104            if (unresolved == null) {
105              unresolved = new ArrayList<>(6);
106            }
107            unresolved.add(candidate);
108            unresolved.add(element);
109            candidate = null;
110          } else {
111            assert elementRank < candidate.rank() : "elementRank >= candidate.rank(): " + elementRank + " >= " + candidate.rank();
112            // The existing candidate is also an alternate but has a higher rank than the alternate, so keep it (do
113            // nothing).
114          }
115        }
116        // ...else drop element by doing nothing
117      } else if (candidate == null) {
118        // The incoming element is not an alternate, but that doesn't matter; the candidate is null, so accept the
119        // element no matter what.
120        candidate = element;
121      } else if (!candidate.alternate()) {
122        // The existing candidate is not an alternate. The incoming element is not an alternate. Ranks in this case are
123        // irrelevant, perhaps surprisingly. We cannot resolve this.
124        if (unresolved == null) {
125          unresolved = new ArrayList<>(6);
126        }
127        unresolved.add(candidate);
128        unresolved.add(element);
129        candidate = null;
130      }
131      // ...else do nothing
132    }
133
134    if (unresolved != null && !unresolved.isEmpty()) {
135      if (candidate != null) {
136        unresolved.add(candidate);
137      }
138      candidate =
139        failureHandler == null ? Reducer.fail(unmodifiableList(unresolved), c) : failureHandler.apply(unmodifiableList(unresolved), c);
140    }
141
142    return candidate;
143  }
144
145
146  /*
147   * Static methods.
148   */
149
150
151  /**
152   * Returns a {@link RankedReducer} implementation.
153   *
154   * @param <C> the type of criteria
155   *
156   * @param <T> the type of the {@link Ranked} reduction
157   *
158   * @return a {@link RankedReducer} implementation; never {@code null}
159   *
160   * @nullability This method never returns {@code null}.
161   *
162   * @idempotency This method is idempotent and deterministic.
163   *
164   * @threadsafety This method is safe for concurrent use by multiple threads.
165   */
166  @SuppressWarnings("unchecked")
167  public static final <C, T extends Ranked> Reducer<C, T> of() {
168    return (Reducer<C, T>)INSTANCE;
169  }
170
171}