org.conqat.lib.commons.assessment.Assessment Maven / Gradle / Ivy
Show all versions of teamscale-lib-commons Show documentation
/*
* Copyright (c) CQSE GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.conqat.lib.commons.assessment;
import java.io.Serializable;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import org.conqat.lib.commons.collections.CounterSet;
import org.conqat.lib.commons.string.StringUtils;
import org.conqat.lib.commons.test.IndexValueClass;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* This class stores an assessment. An assessment is a multiset of traffic light colors (i.e. a
* mapping from traffic light colors to non-negative integers).
*/
@IndexValueClass(containedInBackup = true)
public class Assessment implements Cloneable, Serializable, Comparable {
/** Version used for serialization. */
private static final long serialVersionUID = 1;
/**
* The property name of the mapping field.
*
* Used to identify assessments in the {@literal MetricArrayDeserializer}.
*/
public static final String MAPPING_PROPERTY = "mapping";
/** The "multimap". */
@JsonProperty(MAPPING_PROPERTY)
private final int[] mapping = new int[ETrafficLightColor.values().length];
/** Percent format with minimum and maximum fraction digit of 1 */
public static final NumberFormat PERCENT_FORMAT = NumberFormat.getPercentInstance(Locale.US);
static {
PERCENT_FORMAT.setMinimumFractionDigits(1);
PERCENT_FORMAT.setMaximumFractionDigits(1);
}
/**
* Creates an empty assessment (i.e. one with all entries set to 0).
*/
@JsonCreator
public Assessment() {
// Do nothing, but keep it to have a default constructor.
}
/**
* Create an assessment with a single color entry.
*
* @param color
* the color included in this assessment.
*/
public Assessment(ETrafficLightColor color) {
add(color);
}
/**
* Returns the size of the assessment, i.e. the sum of values over all colors.
*/
public int getSize() {
return Arrays.stream(mapping).map(Math::abs).sum();
}
/**
* Add a single entry of this color to this assessment.
*
* @param color
* the color added to this assessment.
*/
public final void add(ETrafficLightColor color) {
add(color, 1);
}
/**
* Add a single entry of this color to this assessment.
*
* @param color
* the color added to this assessment.
* @param count
* how often to add this color to the assessment.
*/
public final void add(ETrafficLightColor color, int count) {
if (count < 0) {
throw new IllegalArgumentException("Count must be non-negative!");
}
mapping[color.ordinal()] += count;
}
/**
* Adds the {@link Assessment} by merging the provided assessment into this, i.e. increase all
* traffic light color counts by the values in the provided assessment.
*
* @param a
* the assessment to merge in.
*/
public final void add(Assessment a) {
for (int i = 0; i < mapping.length; ++i) {
mapping[i] += a.mapping[i];
}
}
/**
* Subtracts the provided assessment from this one, i.e. decreases all traffic light color counts by
* the values in the provided assessment.
*
* @param a
* the assessment to merge in.
*/
public final void subtract(Assessment a) {
for (int i = 0; i < mapping.length; ++i) {
mapping[i] -= a.mapping[i];
// Assessments are always non-negative
if (mapping[i] < 0) {
mapping[i] = 0;
}
}
}
/**
* @param color
* the color whose frequency to read.
* @return the number of occurrences of the provided color in this assessment.
*/
public int getColorFrequency(ETrafficLightColor color) {
return mapping[color.ordinal()];
}
/**
* @param color
* the color whose frequency to read.
* @return the number of occurrences of the provided color in this assessment, divided by the total
* number of occurrences. If all frequencies are 0, 0 is returned.
*/
public double getRelativeColorFrequency(ETrafficLightColor color) {
int size = getSize();
if (size == 0) {
return 0.0;
} else {
return (double) getColorFrequency(color) / size;
}
}
/**
* Returns the first color of the {@link ETrafficLightColor} enumeration for which this assessment
* has a positive count. The enumeration is ordered in a way, that more dominant colors are on top.
* For example the dominant color is red, if at least one red value is in the assessment.
*/
public ETrafficLightColor getDominantColor() {
for (ETrafficLightColor color : ETrafficLightColor.values()) {
if (mapping[color.ordinal()] > 0) {
return color;
}
}
return ETrafficLightColor.UNKNOWN;
}
/**
* @return the color that is most frequent in this assessment. If all frequencies are 0, UNKNOWN is
* returned. If there are ties, the more dominant (see {@link #getDominantColor()}) one is
* returned.
*/
public ETrafficLightColor getMostFrequentColor() {
ETrafficLightColor result = ETrafficLightColor.UNKNOWN;
int bestCount = 0;
for (ETrafficLightColor color : ETrafficLightColor.values()) {
int count = mapping[color.ordinal()];
if (count > bestCount) {
bestCount = count;
result = color;
}
}
return result;
}
/** {@inheritDoc} */
@Override
public String toString() {
int sum = getSize();
if (sum == 0) {
return StringUtils.EMPTY_STRING;
}
if (sum == 1) {
return getDominantColor().toString();
}
StringBuilder builder = new StringBuilder("[");
appendColor(builder, ETrafficLightColor.GREEN);
builder.append(", ");
appendColor(builder, ETrafficLightColor.YELLOW);
builder.append(", ");
appendColor(builder, ETrafficLightColor.RED);
if (getColorFrequency(ETrafficLightColor.BASELINE) > 0) {
builder.append(", ");
appendColor(builder, ETrafficLightColor.BASELINE);
}
if (getColorFrequency(ETrafficLightColor.ORANGE) > 0) {
builder.append(", ");
appendColor(builder, ETrafficLightColor.ORANGE);
}
builder.append("]");
return builder.toString();
}
/**
* Append a string containing the color and its frequency to the given builder.
*/
private void appendColor(StringBuilder builder, ETrafficLightColor color) {
builder.append(color.getAssessmentDisplayName());
builder.append(": ");
builder.append(getColorFrequency(color));
}
/**
* Return a formatted string of all traffic light colors with absolute and relative values greater
* 0.
*
* Example output: "[Red: 2 (20.0%), Yellow: 3 (30.0%), Green: 5 (50.0%)]"
*/
public String toFormattedColors() {
int sum = getSize();
if (sum == 0) {
return "[]";
}
// in the same order as the assessment bar
List builder = new ArrayList<>();
for (ETrafficLightColor color : ETrafficLightColor.getTrafficLightColors()) {
if (getColorFrequency(color) <= 0) {
continue;
}
builder.add(computeFormattedColor(color, sum));
}
return "[" + String.join(", ", builder) + "]";
}
/**
* Computes the string output for a color containing the name, absolute value and relative value.
*
* Example output: "Red: 2 (20.0%)"
*/
private String computeFormattedColor(ETrafficLightColor color, int sum) {
String colorName = color.getAssessmentDisplayName();
int colorAbsoluteValue = getColorFrequency(color);
String colorRelativeValue = PERCENT_FORMAT.format(getColorFrequency(color) / (double) sum);
return colorName + ": " + colorAbsoluteValue + " (" + colorRelativeValue + ")";
}
/** {@inheritDoc} */
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Assessment)) {
return false;
}
Assessment a = (Assessment) obj;
return Arrays.equals(mapping, a.mapping);
}
/** {@inheritDoc} */
@Override
public int hashCode() {
int hash = 0;
for (int i = 0; i < mapping.length; ++i) {
/*
* primes taken from http://planetmath.org/goodhashtableprimes
*/
hash *= 97;
hash += mapping[i];
hash %= 50331653;
}
return hash;
}
/**
* Compares assessment values lexicographically, i.e. from most dominant color to least dominant
* color.
*/
@Override
public int compareTo(Assessment other) {
for (int i = 0; i < mapping.length; i++) {
if (mapping[i] != other.mapping[i]) {
return mapping[i] - other.mapping[i];
}
}
return 0;
}
/**
* Compares both assessments by the percentage of their dominant colors. If equal, returns zero.
*/
public int compareToRelative(Assessment other) {
int thisSum = getSize();
int otherSum = other.getSize();
// Prohibit division by zero
if (thisSum == 0 || otherSum == 0) {
return thisSum - otherSum;
}
for (int i = 0; i < mapping.length; i++) {
int compareResult = Double.compare((double) mapping[i] / thisSum, (double) other.mapping[i] / otherSum);
if (compareResult != 0) {
return compareResult;
}
}
return 0;
}
/** Aggregate assessments based on sum of assessment values. */
public static Assessment aggregate(Collection values) {
Assessment result = new Assessment();
for (Assessment a : values) {
result.add(a);
}
return result;
}
/**
* @return the given assessment object as a {@link CounterSet} object.
*/
public CounterSet toCounterSet() {
CounterSet result = new CounterSet<>();
for (ETrafficLightColor color : ETrafficLightColor.values()) {
result.inc(color, mapping[color.ordinal()]);
}
return result;
}
}