001/*
002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * 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
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package de.cuioss.benchmarking.common.report;
017
018import de.cuioss.benchmarking.common.constants.BenchmarkConstants;
019import lombok.Getter;
020
021import java.util.Collection;
022import java.util.List;
023import java.util.Objects;
024
025/**
026 * Provides pure statistical computation utilities for benchmark metrics.
027 * <p>
028 * This class is responsible for all statistical calculations including:
029 * <ul>
030 *   <li>Basic statistics (min, max, mean, median)</li>
031 *   <li>Moving averages</li>
032 *   <li>Percentage changes and trends</li>
033 *   <li>Standard deviation and variance</li>
034 * </ul>
035 * <p>
036 * Use this class when you need to perform mathematical/statistical operations on benchmark data.
037 * For metric-specific calculations (performance scores, grades), use {@link MetricsComputer}.
038 * For time-series analysis and trend detection, use {@link TrendDataProcessor}.
039 *
040 * @see MetricsComputer for benchmark-specific metric calculations
041 * @see TrendDataProcessor for time-series and trend analysis
042 */
043public final class StatisticsCalculator {
044
045    public static final String COLLECTION_CANNOT_BE_NULL = "Values collection cannot be null";
046
047    private StatisticsCalculator() {
048        // Utility class with static methods only
049    }
050
051    /**
052     * Calculates the arithmetic mean (average) of a collection of values.
053     *
054     * @param values collection of numeric values
055     * @return the mean value, or 0.0 if the collection is empty
056     * @throws NullPointerException if values is null
057     */
058    public static double calculateMean(Collection<Double> values) {
059        Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL);
060
061        if (values.isEmpty()) {
062            return 0.0;
063        }
064
065        return values.stream()
066                .mapToDouble(Double::doubleValue)
067                .average()
068                .orElse(0.0);
069    }
070
071    /**
072     * Finds the minimum value in a collection.
073     *
074     * @param values collection of numeric values
075     * @return the minimum value, or Double.MAX_VALUE if the collection is empty
076     * @throws NullPointerException if values is null
077     */
078    public static double findMin(Collection<Double> values) {
079        Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL);
080
081        return values.stream()
082                .mapToDouble(Double::doubleValue)
083                .min()
084                .orElse(Double.MAX_VALUE);
085    }
086
087    /**
088     * Finds the maximum value in a collection.
089     *
090     * @param values collection of numeric values
091     * @return the maximum value, or Double.MIN_VALUE if the collection is empty
092     * @throws NullPointerException if values is null
093     */
094    public static double findMax(Collection<Double> values) {
095        Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL);
096
097        return values.stream()
098                .mapToDouble(Double::doubleValue)
099                .max()
100                .orElse(Double.MIN_VALUE);
101    }
102
103    /**
104     * Calculates the median value of a collection.
105     *
106     * @param values collection of numeric values
107     * @return the median value, or 0.0 if the collection is empty
108     * @throws NullPointerException if values is null
109     */
110    public static double calculateMedian(Collection<Double> values) {
111        Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL);
112
113        if (values.isEmpty()) {
114            return 0.0;
115        }
116
117        List<Double> sorted = values.stream()
118                .sorted()
119                .toList();
120
121        int size = sorted.size();
122        if (size % 2 == 0) {
123            return (sorted.get(size / 2 - 1) + sorted.get(size / 2)) / 2.0;
124        } else {
125            return sorted.get(size / 2);
126        }
127    }
128
129    /**
130     * Calculates the percentage change between two values.
131     * <p>
132     * Formula: ((newValue - oldValue) / oldValue) * 100
133     *
134     * @param oldValue the original value
135     * @param newValue the new value
136     * @return the percentage change
137     */
138    public static double calculatePercentageChange(double oldValue, double newValue) {
139        if (oldValue == 0) {
140            return newValue == 0 ? 0.0 : 100.0;
141        }
142        return ((newValue - oldValue) / oldValue) * 100;
143    }
144
145    /**
146     * Calculates a simple moving average for the most recent N values.
147     *
148     * @param values list of values (assumed to be in chronological order)
149     * @param windowSize the number of most recent values to include
150     * @return the moving average
151     * @throws NullPointerException if values is null
152     * @throws IllegalArgumentException if windowSize is less than 1
153     */
154    public static double calculateMovingAverage(List<Double> values, int windowSize) {
155        Objects.requireNonNull(values, "Values list cannot be null");
156        if (windowSize < 1) {
157            throw new IllegalArgumentException("Window size must be at least 1");
158        }
159
160        if (values.isEmpty()) {
161            return 0.0;
162        }
163
164        int actualWindow = Math.min(windowSize, values.size());
165        List<Double> window = values.size() <= actualWindow
166                ? values
167                : values.subList(values.size() - actualWindow, values.size());
168
169        return calculateMean(window);
170    }
171
172    /**
173     * Calculates the standard deviation of a collection of values.
174     *
175     * @param values collection of numeric values
176     * @return the standard deviation, or 0.0 if the collection has less than 2 elements
177     * @throws NullPointerException if values is null
178     */
179    public static double calculateStandardDeviation(Collection<Double> values) {
180        Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL);
181
182        if (values.size() < 2) {
183            return 0.0;
184        }
185
186        double mean = calculateMean(values);
187        double variance = values.stream()
188                .mapToDouble(value -> Math.pow(value - mean, 2))
189                .average()
190                .orElse(0.0);
191
192        return Math.sqrt(variance);
193    }
194
195    /**
196     * Calculates an Exponentially Weighted Moving Average (EWMA) baseline from historical values.
197     * <p>
198     * EWMA gives more weight to recent values while still considering historical context,
199     * making it ideal for performance monitoring and trend detection. The decay factor (lambda)
200     * determines how quickly older values lose influence.
201     * <p>
202     * Formula: EWMA = Σ(value_i × λ^i) / Σ(λ^i)
203     * where i=0 is the most recent value, i=1 is second most recent, etc.
204     *
205     * @param values list of historical values, ordered from newest to oldest
206     * @param lambda decay factor (typically 0.2-0.3 for performance monitoring).
207     *               Smaller values = more weight on recent data.
208     * @return the weighted baseline value
209     * @throws NullPointerException if values is null
210     * @throws IllegalArgumentException if lambda is not in (0, 1] range
211     */
212    public static double calculateEWMA(List<Double> values, double lambda) {
213        Objects.requireNonNull(values, "Values list cannot be null");
214        if (lambda <= 0 || lambda > 1) {
215            throw new IllegalArgumentException("Lambda must be in range (0, 1], got: " + lambda);
216        }
217
218        if (values.isEmpty()) {
219            return 0.0;
220        }
221
222        if (values.size() == 1) {
223            return values.getFirst();
224        }
225
226        double weightedSum = 0.0;
227        double weightSum = 0.0;
228        double currentWeight = 1.0; // Start with weight = λ^0 = 1.0
229
230        for (Double value : values) {
231            weightedSum += value * currentWeight;
232            weightSum += currentWeight;
233            currentWeight *= lambda; // Exponential decay: next weight = current × λ
234        }
235
236        return weightedSum / weightSum;
237    }
238
239    /**
240     * Determines the trend direction based on percentage change and a stability threshold.
241     *
242     * @param percentageChange the percentage change value
243     * @param stabilityThreshold the threshold (as a percentage) below which the trend is considered stable
244     * @return "up", "down", or "stable"
245     */
246    public static String determineTrendDirection(double percentageChange, double stabilityThreshold) {
247        if (Math.abs(percentageChange) < stabilityThreshold) {
248            return BenchmarkConstants.Report.Badge.TrendDirection.STABLE;
249        }
250        return percentageChange > 0 ? BenchmarkConstants.Report.Badge.TrendDirection.UP : BenchmarkConstants.Report.Badge.TrendDirection.DOWN;
251    }
252
253
254    /**
255     * Statistical summary containing basic statistics for a dataset.
256     */
257    @Getter
258    public static class Statistics {
259        private final double min;
260        private final double max;
261        private final double mean;
262        private final double median;
263        private final double stdDev;
264        private final int count;
265
266        public Statistics(double min, double max, double mean, double median, double stdDev, int count) {
267            this.min = min;
268            this.max = max;
269            this.mean = mean;
270            this.median = median;
271            this.stdDev = stdDev;
272            this.count = count;
273        }
274    }
275
276    /**
277     * Computes comprehensive statistics for a dataset.
278     *
279     * @param values collection of numeric values
280     * @return a Statistics object containing min, max, mean, median, standard deviation, and count
281     * @throws NullPointerException if values is null
282     */
283    public static Statistics computeStatistics(Collection<Double> values) {
284        Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL);
285
286        return new Statistics(
287                findMin(values),
288                findMax(values),
289                calculateMean(values),
290                calculateMedian(values),
291                calculateStandardDeviation(values),
292                values.size()
293        );
294    }
295}