/* * 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.coregui.client.util; import java.util.HashSet; import java.util.Set; import com.google.gwt.i18n.client.NumberFormat; import org.rhq.core.domain.measurement.MeasurementUnits; import org.rhq.core.domain.measurement.composite.MeasurementNumericValueAndUnits; import org.rhq.core.domain.measurement.util.MeasurementConversionException; import org.rhq.coregui.client.CoreGUI; import org.rhq.coregui.client.Messages; public class MeasurementConverterClient { private static final Messages MSG = CoreGUI.getMessages(); private static final int MAX_PRECISION_DIGITS = 4; private static final String NULL_OR_NAN_FORMATTED_VALUE = MSG.view_measure_nan(); private static NumberFormat getDefaultNumberFormat() { NumberFormat nf = NumberFormat.getFormat("0.0"); 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 { String abbr = getMeasurementUnitAbbreviation(targetUnits); return (abbr.length() > 0) ? (value + " " + abbr) : value; } } public static String getMeasurementUnitAbbreviation(MeasurementUnits units) { switch (units) { case NONE: return ""; case PERCENTAGE: return MSG.common_unit_abbrev_percentage(); case BYTES: return MSG.common_unit_abbrev_bytes(); case KILOBYTES: return MSG.common_unit_abbrev_kilobytes(); case MEGABYTES: return MSG.common_unit_abbrev_megabytes(); case GIGABYTES: return MSG.common_unit_abbrev_gigabytes(); case TERABYTES: return MSG.common_unit_abbrev_terabytes(); case PETABYTES: return MSG.common_unit_abbrev_petabytes(); case BITS: return MSG.common_unit_abbrev_bits(); case KILOBITS: return MSG.common_unit_abbrev_kilobits(); case MEGABITS: return MSG.common_unit_abbrev_megabits(); case GIGABITS: return MSG.common_unit_abbrev_gigabits(); case TERABITS: return MSG.common_unit_abbrev_terabits(); case PETABITS: return MSG.common_unit_abbrev_petabits(); case EPOCH_MILLISECONDS: return ""; // absolute time - no display case EPOCH_SECONDS: return ""; // absolute time - no display case JIFFYS: return MSG.common_unit_abbrev_jiffys(); case NANOSECONDS: return MSG.common_unit_abbrev_nanoseconds(); case MICROSECONDS: return MSG.common_unit_abbrev_microseconds(); case MILLISECONDS: return MSG.common_unit_abbrev_milliseconds(); case SECONDS: return MSG.common_unit_abbrev_seconds(); case MINUTES: return MSG.common_unit_abbrev_minutes(); case HOURS: return MSG.common_unit_abbrev_hours(); case DAYS: return MSG.common_unit_abbrev_days(); case CELSIUS: return MSG.common_unit_abbrev_celsius(); case KELVIN: return MSG.common_unit_abbrev_kelvin(); case FAHRENHEIGHT: return MSG.common_unit_abbrev_fahrenheight(); default: return units.toString(); // unknown units - developer forgot to add a case statement - just use the toString for now } } /** * 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; } 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 = NumberFormat.getFormat(getFormat(0, 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 (targetUnits != null && MeasurementUnits.Family.RELATIVE == targetUnits.getFamily()) { value = MeasurementUnits.scaleUp(value, targetUnits); } NumberFormat numberFormat = NumberFormat.getFormat(getFormat( minimumFractionDigits != null ? minimumFractionDigits : 1, maximumFractionDigits != null ? maximumFractionDigits : 1)); String formatted = numberFormat.format(value); return format(formatted, targetUnits); } public static String scaleAndFormat(Double origin, MeasurementUnits targetUnits, boolean bestFit ) 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, null); } // 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; } public static String getFormat(int minDigits, int maxDigits) { StringBuilder buf = new StringBuilder("0."); for (int i = 0; i < minDigits; i++) { buf.append("0"); } for (int i = 0; i < (maxDigits - minDigits); i++) { buf.append("#"); } return buf.toString(); } }