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}