/*
* 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();
}
}