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 java.util.Locale;
019
020import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Metrics.Conversions;
021import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Metrics.Units;
022import static de.cuioss.benchmarking.common.constants.BenchmarkConstants.Report.Grades;
023
024/**
025 * Central utility for converting benchmark metrics between different units.
026 * This is the SINGLE source of truth for all metric conversions.
027 */
028public final class MetricConversionUtil {
029
030    private MetricConversionUtil() {
031        // Utility class
032    }
033
034    /**
035     * Converts a benchmark score to operations per second.
036     * 
037     * @param score the raw score value
038     * @param unit the unit string from JMH
039     * @return throughput in operations per second
040     */
041    public static double convertToOpsPerSecond(double score, String unit) {
042        // Handle throughput units directly
043        // IMPORTANT: Check more specific units first to avoid partial matches!
044        if (unit.contains(Units.OPS_PER_NS)) {
045            return score * Conversions.NANOS_TO_SECONDS;
046        } else if (unit.contains(Units.OPS_PER_US)) {
047            return score * Conversions.MICROS_TO_SECONDS;
048        } else if (unit.contains(Units.OPS_PER_MS)) {
049            return score * Conversions.MILLIS_TO_SECONDS;
050        } else if (unit.contains(Units.OPS_PER_SEC) || unit.contains(Units.OPS_PER_SEC_ALT)) {
051            return score;
052        } else if (unit.contains(Units.NS_PER_OP)) {
053            return Conversions.NANOS_TO_SECONDS / score;
054        } else if (unit.contains(Units.US_PER_OP)) {
055            return Conversions.MICROS_TO_SECONDS / score;
056        } else if (unit.contains(Units.MS_PER_OP)) {
057            return Conversions.MILLIS_TO_SECONDS / score;
058        } else if (unit.contains(Units.SEC_PER_OP)) {
059            return 1.0 / score;
060        }
061        return score;
062    }
063
064    /**
065     * Converts a benchmark score to milliseconds per operation.
066     * This is the CENTRALIZED method for all latency conversions.
067     * 
068     * @param score the raw score value
069     * @param unit the unit string from JMH
070     * @return latency in milliseconds per operation
071     */
072    public static double convertToMillisecondsPerOp(double score, String unit) {
073        // Handle latency units (time per operation)
074        // IMPORTANT: Check more specific units first to avoid partial matches!
075        // "us/op" contains "s/op", so must check "us/op" before "s/op"
076        // "ns/op" contains "s/op", so must check "ns/op" before "s/op"
077        if (unit.contains(Units.NS_PER_OP)) {
078            return score / Conversions.NANOS_TO_MILLIS; // Convert nanoseconds to milliseconds
079        } else if (unit.contains(Units.US_PER_OP)) {
080            return score / Conversions.MICROS_TO_MILLIS; // Convert microseconds to milliseconds
081        } else if (unit.contains(Units.MS_PER_OP)) {
082            return score; // Already in ms/op
083        } else if (unit.contains(Units.SEC_PER_OP)) {
084            return score * Conversions.MILLIS_TO_SECONDS; // Convert seconds to milliseconds
085        } else if (unit.contains(Units.OPS_PER_NS)) {
086            return 1.0 / (score * Conversions.NANOS_TO_MILLIS); // ops/ns -> ns/op -> ms/op
087        } else if (unit.contains(Units.OPS_PER_US)) {
088            return 1.0 / (score * Conversions.MICROS_TO_MILLIS); // ops/us -> us/op -> ms/op
089        } else if (unit.contains(Units.OPS_PER_MS)) {
090            return 1.0 / score; // ops/ms -> ms/op
091        } else if (unit.contains(Units.OPS_PER_SEC) || unit.contains(Units.OPS_PER_SEC_ALT)) {
092            return Conversions.MILLIS_TO_SECONDS / score; // ops/s -> s/op -> ms/op
093        }
094        // Unknown unit, return 0 to filter out in calculations
095        return 0;
096    }
097
098    /**
099     * Calculates a performance grade based on throughput.
100     * 
101     * @param throughput operations per second
102     * @return performance grade string
103     */
104    public static String calculatePerformanceGrade(double throughput) {
105        return switch ((int) Math.log10(Math.max(1, throughput))) {
106            case 6, 7, 8, 9 -> Grades.A_PLUS;
107            case 5 -> Grades.A;
108            case 4 -> Grades.B;
109            case 3 -> Grades.C;
110            default -> Grades.D;
111        };
112    }
113
114    /**
115     * Central method for formatting numeric values for display.
116     * Rules:
117     * - Values less than 2: 2 fraction digits
118     * - Values less than 10: 1 fraction digit
119     * - Values greater than or equal to 10: No fraction digits
120     * 
121     * @param value the numeric value to format
122     * @return formatted string representation
123     */
124    public static String formatForDisplay(double value) {
125        if (value < 2) {
126            return String.format(Locale.US, "%.2f", value);
127        } else if (value < 10) {
128            return String.format(Locale.US, "%.1f", value);
129        } else {
130            return String.format(Locale.US, "%d", Math.round(value));
131        }
132    }
133
134    /**
135     * Formats throughput value with appropriate units.
136     * 
137     * @param value throughput in ops/s
138     * @return formatted string with units
139     */
140    public static String formatThroughput(double value) {
141        if (value >= 1_000_000) {
142            return formatForDisplay(value / 1_000_000) + Units.M_OPS_S;
143        } else if (value >= 1000) {
144            return formatForDisplay(value / 1000) + Units.K_OPS_S;
145        } else {
146            return formatForDisplay(value) + Units.SPACE_OPS_S;
147        }
148    }
149
150    /**
151     * Formats latency value with appropriate units.
152     * 
153     * @param ms latency in milliseconds
154     * @return formatted string with units
155     */
156    public static String formatLatency(double ms) {
157        if (ms >= 1000) {
158            return formatForDisplay(ms / 1000) + Units.SUFFIX_S;
159        } else {
160            return formatForDisplay(ms) + Units.SUFFIX_MS;
161        }
162    }
163}