/* * RHQ Management Platform * Copyright (C) 2005-2008 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2, as * published by the Free Software Foundation, and/or the GNU Lesser * General Public License, version 2.1, also as published by the Free * Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License and the GNU Lesser General Public License * for more details. * * You should have received a copy of the GNU General Public License * and the GNU Lesser General Public License along with this program; * if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.rhq.core.server; import java.text.NumberFormat; import java.util.HashSet; import java.util.Set; import org.rhq.core.domain.measurement.MeasurementSchedule; import org.rhq.core.domain.measurement.MeasurementUnits; import org.rhq.core.domain.measurement.composite.MeasurementNumericValueAndUnits; import org.rhq.core.domain.measurement.util.MeasurementConversionException; public class MeasurementConverter { private static final int MAX_PRECISION_DIGITS = 4; private static final String NULL_OR_NAN_FORMATTED_VALUE = "--no data available--"; private static NumberFormat getDefaultNumberFormat() { NumberFormat nf = NumberFormat.getNumberInstance(); nf.setMinimumFractionDigits(1); nf.setMaximumFractionDigits(1); return nf; } public static Double scale(MeasurementNumericValueAndUnits origin, MeasurementUnits targetUnits) throws MeasurementConversionException { MeasurementUnits originUnits = origin.getUnits(); Double originValue = origin.getValue(); return originValue * MeasurementUnits.calculateOffset(originUnits, targetUnits); } public static Double scale(Double origin, MeasurementUnits targetUnits) throws MeasurementConversionException { boolean wasNegative = false; if (origin < 0) { wasNegative = true; origin = -origin; } MeasurementUnits baseUnit = targetUnits.getBaseUnits(); MeasurementNumericValueAndUnits valueAndUnits = new MeasurementNumericValueAndUnits(origin, baseUnit); Double results = scale(valueAndUnits, targetUnits); if (wasNegative) { results = -results; } return results; } public static String format(String value, MeasurementUnits targetUnits) { if (targetUnits == null) { return value; } else { return value + targetUnits; } } /** * Formats the given array of double values: determines the necessary precision such that when formatted, they are * distinct and reasonable to look at. For example, for values { 1.45 1.46 1.47 1.48 1.49 } the desired precision is * 2 - less precision loses significant digits, and more precision provides no added benefit. Max precision is * bounded for presentation considerations. * * @param values the values to be formatted * @param targetUnits the target units for the values * @param bestFit whether or not to use a normalized scale for the family of units * * @return the formatted values */ public static String[] formatToSignificantPrecision(double[] values, MeasurementUnits targetUnits, boolean bestFit) { if ((null == values) || (values.length == 0)) { return null; } MeasurementUnits originalUnits = targetUnits; /* * in the overwhelming majority of cases, you're going to want to apply a bestFit * to the passed data, but it's not required; it's perfectly possible to allow a * list of doubles to be formatted without being fit, in which case the targetUnits * will be part of the formatted display for each result element */ if (bestFit) { // find bestFit units by taking the average Double average = 0.0; for (int i = 0, sz = values.length; i < sz; i++) { /* * adding fractional amount iterative leads to greater * error, but prevents overflow on large data sets */ average += (values[i] / sz); } MeasurementNumericValueAndUnits fittedAverage = fit(average, targetUnits); //noinspection UnnecessaryLocalVariable MeasurementUnits fittedUnits = fittedAverage.getUnits(); /* * and change the local reference to targetUnits, so that the same logic * can be used both for the bestFit and non-bestFit computations */ targetUnits = fittedUnits; } @SuppressWarnings("unused") Set<String> existingStrings; // technically this *is* unused because int precisionDigits = 0; boolean scaleWithMorePrecision = true; String[] results = new String[values.length]; NumberFormat nf = getDefaultNumberFormat(); /* * we scale at most to MAX_PRECISION_DIGITS to allow for presentation limits * * increase the maxPrecisionDigits in the while condition * itself to ensure it gets done for every loop */ while (scaleWithMorePrecision && (++precisionDigits <= MAX_PRECISION_DIGITS)) { /* * make the assumption that we no longer need to scale beyond this iteration */ scaleWithMorePrecision = false; /* * we need to record the uniquely formatted values so we can determine */ existingStrings = new HashSet<String>(); nf.setMinimumFractionDigits(0); nf.setMaximumFractionDigits(precisionDigits); Double[] scaledValues = new Double[values.length]; for (int i = 0; i < scaledValues.length; i++) { /* * For relative units apply the scale now, prior to the nf.format(), since we are not using format( Double...). * Otherwise, apply standard multi-unit scaling. */ if (MeasurementUnits.Family.RELATIVE == originalUnits.getFamily()) { scaledValues[i] = MeasurementUnits.scaleUp(values[i], originalUnits); } else { scaledValues[i] = scale(new MeasurementNumericValueAndUnits(values[i], originalUnits), targetUnits); } } for (int i = 0; i < results.length; i++) { /* * JUST get the formatted value, specifically DON'T tack on the formatted units yet; * we do this to see how many units we'll have to scale to afterwards (outside this * while loop) to make the array of values passed to us unique */ String formatted = nf.format(scaledValues[i]); /* * check whether formatted value was in the set or not; if it was, we have to * loop, but only if we're not not already at our maximum precision */ boolean wasNewElement = existingStrings.add(formatted); if ((!wasNewElement) && (precisionDigits < MAX_PRECISION_DIGITS)) { scaleWithMorePrecision = true; break; } results[i] = formatted; } } /* * we did the best we could in terms of trying to find a precision that adds the most * uniqueness to the given set of values, NOW tack on the formatted value for the units */ for (int i = 0; i < results.length; i++) { results[i] = format(results[i], targetUnits); } return results; } public static String format(Double value, MeasurementUnits targetUnits, boolean bestFit) { return format(value, targetUnits, bestFit, null, null); } public static String format(Double value, MeasurementUnits targetUnits, boolean bestFit, Integer minimumFractionDigits, Integer maximumFractionDigits) { if (value == null || Double.isNaN(value)) { return NULL_OR_NAN_FORMATTED_VALUE; } if (bestFit) { MeasurementNumericValueAndUnits valueAndUnits = fit(value, targetUnits); value = valueAndUnits.getValue(); targetUnits = valueAndUnits.getUnits(); } // apply relative scale at presentation time if (MeasurementUnits.Family.RELATIVE == targetUnits.getFamily()) { value = MeasurementUnits.scaleUp(value, targetUnits); } NumberFormat numberFormat = getDefaultNumberFormat(); if (null != minimumFractionDigits) { numberFormat.setMinimumFractionDigits(minimumFractionDigits); } if (null != maximumFractionDigits) { numberFormat.setMaximumFractionDigits(maximumFractionDigits); } String formatted = numberFormat.format(value); return format(formatted, targetUnits); } public static String scaleAndFormat(Double origin, MeasurementSchedule targetSchedule, boolean bestFit) throws MeasurementConversionException { MeasurementUnits targetUnits = targetSchedule.getDefinition().getUnits(); return scaleAndFormat(origin, targetUnits, bestFit, null, null); } public static String scaleAndFormat(Double origin, MeasurementUnits targetUnits, boolean bestFit) throws MeasurementConversionException { return scaleAndFormat(origin, targetUnits, bestFit, null, null); } public static String scaleAndFormat(Double origin, MeasurementUnits targetUnits, boolean bestFit, Integer minimumFractionDigits, Integer maximumFractionDigits) throws MeasurementConversionException { MeasurementUnits baseUnits = targetUnits.getBaseUnits(); MeasurementNumericValueAndUnits valueAndUnits = new MeasurementNumericValueAndUnits(origin, baseUnits); Double scaledMagnitude = scale(valueAndUnits, targetUnits); return format(scaledMagnitude, targetUnits, bestFit); } public static MeasurementNumericValueAndUnits fit(Double origin, MeasurementUnits units) { return fit(origin, units, null, null); } public static MeasurementNumericValueAndUnits fit(Double origin, MeasurementUnits units, MeasurementUnits lowUnits, MeasurementUnits highUnits) { // work-around for the various Chart descendants not properly setting their units field; if (null == units) { return new MeasurementNumericValueAndUnits(origin, units); } // by definition, absolutely specified units don't scale to anything if ((MeasurementUnits.Family.ABSOLUTE == units.getFamily()) || (MeasurementUnits.Family.DURATION == units.getFamily())) { return new MeasurementNumericValueAndUnits(origin, units); } // by definition relative-valued units are self-scaled (converted at formatting) if (MeasurementUnits.Family.RELATIVE == units.getFamily()) { return new MeasurementNumericValueAndUnits(origin, units); } if (MeasurementUnits.Family.TEMPERATURE == units.getFamily()) { return new MeasurementNumericValueAndUnits(origin, units); } // if the magnitude is zero, the best-fit also will spin around forever since it won't change if (Math.abs(origin) < 1e-9) { return new MeasurementNumericValueAndUnits(origin, units); } boolean wasNegative = false; if (origin < 0) { wasNegative = true; origin = -origin; } MeasurementNumericValueAndUnits currentValueAndUnits; MeasurementNumericValueAndUnits nextValueAndUnits = new MeasurementNumericValueAndUnits(origin, units); // first, make the value smaller if it's too big int maxOrdinal = (highUnits != null) ? (highUnits.ordinal() + 1) : MeasurementUnits.values().length; do { currentValueAndUnits = nextValueAndUnits; int nextOrdinal = currentValueAndUnits.getUnits().ordinal() + 1; if (nextOrdinal == maxOrdinal) { // we could theoretically get bigger, but we don't have any units to represent that break; } MeasurementUnits biggerUnits = MeasurementUnits.values()[nextOrdinal]; if (biggerUnits.getFamily() != currentValueAndUnits.getUnits().getFamily()) { // we're as big as we can get, break out of the loop so we can return break; } Double smallerValue = scale(currentValueAndUnits, biggerUnits); nextValueAndUnits = new MeasurementNumericValueAndUnits(smallerValue, biggerUnits); } while (nextValueAndUnits.getValue() > 1.0); // next, make the value bigger if it's too small int minOrdinal = (lowUnits != null) ? (lowUnits.ordinal() - 1) : -1; while (currentValueAndUnits.getValue() < 1.0) { int nextOrdinal = currentValueAndUnits.getUnits().ordinal() - 1; if (nextOrdinal == minOrdinal) { // we could theoretically get smaller, but we don't have any units to represent that break; } MeasurementUnits smallerUnits = MeasurementUnits.values()[nextOrdinal]; if (smallerUnits.getFamily() != currentValueAndUnits.getUnits().getFamily()) { // we're as small as we can get, break out of the loop so we can return break; } Double biggerValue = scale(currentValueAndUnits, smallerUnits); nextValueAndUnits = new MeasurementNumericValueAndUnits(biggerValue, smallerUnits); currentValueAndUnits = nextValueAndUnits; } if (wasNegative) { return new MeasurementNumericValueAndUnits(-currentValueAndUnits.getValue(), currentValueAndUnits .getUnits()); } return currentValueAndUnits; } }