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}