001/*-------------------------------------------------------------------------+ 002| | 003| Copyright 2005-2011 The ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016+-------------------------------------------------------------------------*/ 017package org.conqat.lib.commons.assessment; 018 019import java.io.Serializable; 020import java.text.NumberFormat; 021import java.util.ArrayList; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.List; 025import java.util.Locale; 026 027import org.conqat.lib.commons.js_export.ExportToJavaScript; 028import org.conqat.lib.commons.string.StringUtils; 029 030import com.fasterxml.jackson.annotation.JsonCreator; 031import com.fasterxml.jackson.annotation.JsonProperty; 032 033/** 034 * This class stores an assessment. An assessment is a multiset of traffic light 035 * colors (i.e. a mapping from traffic light colors to non-negative integers). 036 */ 037@ExportToJavaScript 038public class Assessment implements Cloneable, Serializable, Comparable<Assessment> { 039 040 /** Version used for serialization. */ 041 private static final long serialVersionUID = 1; 042 043 /** The "multimap". */ 044 @JsonProperty("mapping") 045 private final int[] mapping = new int[ETrafficLightColor.values().length]; 046 047 /** Percent format with minimum and maximum fraction digit of 1 */ 048 public static final NumberFormat PERCENT_FORMAT = NumberFormat.getPercentInstance(Locale.US); 049 050 static { 051 PERCENT_FORMAT.setMinimumFractionDigits(1); 052 PERCENT_FORMAT.setMaximumFractionDigits(1); 053 } 054 055 /** 056 * Creates an empty assessment (i.e. one with all entries set to 0). 057 */ 058 @JsonCreator 059 public Assessment() { 060 // Do nothing, but keep it to have a default constructor. 061 } 062 063 /** 064 * Create an assessment with a single color entry. 065 * 066 * @param color 067 * the color included in this assessment. 068 */ 069 public Assessment(ETrafficLightColor color) { 070 add(color); 071 } 072 073 /** 074 * Returns the size of the assessment, i.e. the sum of values over all colors. 075 */ 076 public int getSize() { 077 return Arrays.stream(mapping).map(Math::abs).sum(); 078 } 079 080 /** 081 * Add a single entry of this color to this assessment. 082 * 083 * @param color 084 * the color added to this assessment. 085 */ 086 public final void add(ETrafficLightColor color) { 087 add(color, 1); 088 } 089 090 /** 091 * Add a single entry of this color to this assessment. 092 * 093 * @param color 094 * the color added to this assessment. 095 * @param count 096 * how often to add this color to the assessment. 097 */ 098 public final void add(ETrafficLightColor color, int count) { 099 if (count < 0) { 100 throw new IllegalArgumentException("Count must be non-negative!"); 101 } 102 mapping[color.ordinal()] += count; 103 } 104 105 /** 106 * Adds the {@link Assessment} by merging the provided assessment into this, 107 * i.e. increase all traffic light color counts by the values in the provided 108 * assessment. 109 * 110 * @param a 111 * the assessment to merge in. 112 */ 113 public final void add(Assessment a) { 114 for (int i = 0; i < mapping.length; ++i) { 115 mapping[i] += a.mapping[i]; 116 } 117 } 118 119 /** 120 * Subtracts the provided assessment from this one, i.e. decreases all traffic 121 * light color counts by the values in the provided assessment. 122 * 123 * @param a 124 * the assessment to merge in. 125 */ 126 public final void subtract(Assessment a) { 127 for (int i = 0; i < mapping.length; ++i) { 128 mapping[i] -= a.mapping[i]; 129 130 // Assessments are always non-negative 131 if (mapping[i] < 0) { 132 mapping[i] = 0; 133 } 134 } 135 } 136 137 /** 138 * @param color 139 * the color whose frequency to read. 140 * @return the number of occurrences of the provided color in this assessment. 141 */ 142 public int getColorFrequency(ETrafficLightColor color) { 143 return mapping[color.ordinal()]; 144 } 145 146 /** 147 * Returns the first color of the {@link ETrafficLightColor} enumeration for 148 * which this assessment has a positive count. The enumeration is ordered in a 149 * way, that more dominant colors are on top. For example the dominant color is 150 * red, if at least one red value is in the assessment. 151 */ 152 public ETrafficLightColor getDominantColor() { 153 for (ETrafficLightColor color : ETrafficLightColor.values()) { 154 if (mapping[color.ordinal()] > 0) { 155 return color; 156 } 157 } 158 159 return ETrafficLightColor.UNKNOWN; 160 } 161 162 /** 163 * @return the color that is most frequent in this assessment. If all 164 * frequencies are 0, UNKNOWN is returned. If there are ties, the more 165 * dominant (see {@link #getDominantColor()}) one is returned. 166 */ 167 public ETrafficLightColor getMostFrequentColor() { 168 ETrafficLightColor result = ETrafficLightColor.UNKNOWN; 169 int bestCount = 0; 170 171 for (ETrafficLightColor color : ETrafficLightColor.values()) { 172 int count = mapping[color.ordinal()]; 173 if (count > bestCount) { 174 bestCount = count; 175 result = color; 176 } 177 } 178 179 return result; 180 } 181 182 /** {@inheritDoc} */ 183 @Override 184 public String toString() { 185 int sum = getSize(); 186 if (sum == 0) { 187 return StringUtils.EMPTY_STRING; 188 } 189 190 if (sum == 1) { 191 return getDominantColor().toString(); 192 } 193 194 StringBuilder builder = new StringBuilder("["); 195 appendColor(builder, ETrafficLightColor.GREEN); 196 builder.append(", "); 197 appendColor(builder, ETrafficLightColor.YELLOW); 198 builder.append(", "); 199 appendColor(builder, ETrafficLightColor.RED); 200 if (getColorFrequency(ETrafficLightColor.BASELINE) > 0) { 201 builder.append(", "); 202 appendColor(builder, ETrafficLightColor.BASELINE); 203 } 204 builder.append("]"); 205 return builder.toString(); 206 } 207 208 /** 209 * Append a string containing the color and its frequency to the given builder. 210 */ 211 private void appendColor(StringBuilder builder, ETrafficLightColor color) { 212 builder.append(color.toString().substring(0, 1)); 213 builder.append(": "); 214 builder.append(getColorFrequency(color)); 215 } 216 217 /** 218 * Return a formatted string of all traffic light colors with absolute and 219 * relative values greater 0. 220 * 221 * Example output: "[R: 2 (20.0%), Y: 3 (30.0%), G: 5 (50.0%)]" 222 */ 223 public String toFormattedColors() { 224 int sum = getSize(); 225 if (sum == 0) { 226 return "[]"; 227 } 228 229 // in the same order as the assessment bar 230 List<String> builder = new ArrayList<>(); 231 232 for (ETrafficLightColor color : ETrafficLightColor.getTrafficLightColors()) { 233 if (getColorFrequency(color) <= 0) { 234 continue; 235 } 236 builder.add(computeFormattedColor(color, sum)); 237 } 238 return "[" + String.join(", ", builder) + "]"; 239 } 240 241 /** 242 * Computes the string output for a color containing the name, absolute value 243 * and relative value. 244 * 245 * Example output: "R: 2 (20.0%)" 246 */ 247 private String computeFormattedColor(ETrafficLightColor color, int sum) { 248 String colorName = color.getShortDisplayText(); 249 int colorAbsoluteValue = getColorFrequency(color); 250 String colorRelativeValue = PERCENT_FORMAT.format(getColorFrequency(color) / (double) sum); 251 return colorName + ": " + colorAbsoluteValue + " (" + colorRelativeValue + ")"; 252 } 253 254 /** {@inheritDoc} */ 255 @Override 256 public boolean equals(Object obj) { 257 if (!(obj instanceof Assessment)) { 258 return false; 259 } 260 261 Assessment a = (Assessment) obj; 262 return Arrays.equals(mapping, a.mapping); 263 } 264 265 /** {@inheritDoc} */ 266 @Override 267 public int hashCode() { 268 int hash = 0; 269 for (int i = 0; i < mapping.length; ++i) { 270 /* 271 * primes taken from http://planetmath.org/goodhashtableprimes 272 */ 273 hash *= 97; 274 hash += mapping[i]; 275 hash %= 50331653; 276 } 277 return hash; 278 } 279 280 /** 281 * Compares assessment values lexicographically, i.e. from most dominant color 282 * to least dominant color. 283 */ 284 @Override 285 public int compareTo(Assessment other) { 286 for (int i = 0; i < mapping.length; i++) { 287 if (mapping[i] != other.mapping[i]) { 288 return mapping[i] - other.mapping[i]; 289 } 290 } 291 return 0; 292 } 293 294 /** 295 * Compares both assessments by the percentage of their dominant colors. If 296 * equal, returns zero. 297 */ 298 public int compareToRelative(Assessment other) { 299 int thisSum = getSize(); 300 int otherSum = other.getSize(); 301 302 // Prohibit division by zero 303 if (thisSum == 0 || otherSum == 0) { 304 return thisSum - otherSum; 305 } 306 307 for (int i = 0; i < mapping.length; i++) { 308 int compareResult = Double.compare((double) mapping[i] / thisSum, (double) other.mapping[i] / otherSum); 309 if (compareResult != 0) { 310 return compareResult; 311 } 312 } 313 return 0; 314 } 315 316 /** Aggregate assessments based on sum of assessment values. */ 317 public static Assessment aggregate(Collection<Assessment> values) { 318 Assessment result = new Assessment(); 319 for (Assessment a : values) { 320 result.add(a); 321 } 322 return result; 323 } 324}