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 * 036 * @deprecated This class is not really needed and is tentatively deprecated. 037 */ 038@Deprecated(since = "0.0.18") 039public final class RankedReducer<C, T extends Ranked> implements Reducer<C, T> { 040 041 042 /* 043 * Static fields. 044 */ 045 046 047 private static final RankedReducer<?, ?> INSTANCE = new RankedReducer<>(); 048 049 050 /* 051 * Constructors. 052 */ 053 054 055 private RankedReducer() { 056 super(); 057 } 058 059 060 /* 061 * Instance methods. 062 */ 063 064 065 @Override 066 public final T reduce(final List<? extends T> elements, 067 final C c, 068 final BiFunction<? super List<? extends T>, ? super C, ? extends T> failureHandler) { 069 if (elements == null || elements.isEmpty()) { 070 // https://github.com/microbean/microbean-bean/issues/21 071 return failureHandler == null ? Reducer.fail(List.of(), c) : failureHandler.apply(List.of(), c); 072 } else if (elements.size() == 1) { 073 return elements.get(0); 074 } 075 076 T candidate = null; 077 List<T> unresolved = null; 078 // Highest rank wins 079 int maxRank = DEFAULT_RANK; 080 081 for (final T element : elements) { 082 if (alternate(element)) { 083 final int elementRank = rank(element); 084 if (elementRank == maxRank) { 085 if (candidate == null || !alternate(candidate)) { 086 // Prefer elements regardless of ranks. 087 candidate = element; 088 } else { 089 assert rank(candidate) == maxRank : "Unexpected rank: " + rank(candidate) + "; was expecting: " + maxRank; 090 // The existing candidate is an alternate and by definition has the highest rank we've seen so far; the 091 // incoming element is also an alternate; both have equal ranks: we can't resolve this. 092 if (unresolved == null) { 093 unresolved = new ArrayList<>(6); 094 } 095 unresolved.add(candidate); 096 unresolved.add(element); 097 candidate = null; 098 } 099 } else if (elementRank > maxRank) { 100 if (candidate == null || !alternate(candidate) || elementRank > rank(candidate)) { 101 // The existing candidate is either null, not an alternate (and alternates are always preferred), or an 102 // alternate with losing rank, so junk it in favor of the incoming element. 103 candidate = element; 104 // We have a new maxRank. 105 maxRank = elementRank; 106 } else if (elementRank == rank(candidate)) { 107 // The existing candidate is also an alternate and has the same rank. 108 if (unresolved == null) { 109 unresolved = new ArrayList<>(6); 110 } 111 unresolved.add(candidate); 112 unresolved.add(element); 113 candidate = null; 114 } else { 115 assert elementRank < rank(candidate) : "elementRank >= rank(candidate): " + elementRank + " >= " + rank(candidate); 116 // The existing candidate is also an alternate but has a higher rank than the alternate, so keep it (do 117 // nothing). 118 } 119 } 120 // ...else drop element by doing nothing 121 } else if (candidate == null) { 122 // The incoming element is not an alternate, but that doesn't matter; the candidate is null, so accept the 123 // element no matter what. 124 candidate = element; 125 } else if (!alternate(candidate)) { 126 // The existing candidate is not an alternate. The incoming element is not an alternate. Ranks in this case are 127 // irrelevant, perhaps surprisingly. We cannot resolve this. 128 if (unresolved == null) { 129 unresolved = new ArrayList<>(6); 130 } 131 unresolved.add(candidate); 132 unresolved.add(element); 133 candidate = null; 134 } 135 // ...else do nothing 136 } 137 138 if (unresolved != null && !unresolved.isEmpty()) { 139 if (candidate != null) { 140 unresolved.add(candidate); 141 } 142 candidate = 143 failureHandler == null ? Reducer.fail(unmodifiableList(unresolved), c) : failureHandler.apply(unmodifiableList(unresolved), c); 144 } 145 146 return candidate; 147 } 148 149 // Preparing for a future where alternate status is not a first-class citizen. 150 private final boolean alternate(final T t) { 151 return t.alternate(); 152 } 153 154 // Preparing for a future where rank is not a first-class citizen. 155 private final int rank(final T t) { 156 return t.rank(); 157 } 158 159 160 /* 161 * Static methods. 162 */ 163 164 165 /** 166 * Returns a {@link RankedReducer} implementation. 167 * 168 * @param <C> the type of criteria 169 * 170 * @param <T> the type of the {@link Ranked} reduction 171 * 172 * @return a {@link RankedReducer} implementation; never {@code null} 173 * 174 * @microbean.nullability This method never returns {@code null}. 175 * 176 * @microbean.idempotency This method is idempotent and deterministic. 177 * 178 * @microbean.threadsafety This method is safe for concurrent use by multiple threads. 179 */ 180 @SuppressWarnings("unchecked") 181 public static final <C, T extends Ranked> Reducer<C, T> of() { 182 return (Reducer<C, T>)INSTANCE; 183 } 184 185}