/* * 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.clientapi.util.units; import java.math.BigDecimal; import java.text.NumberFormat; import java.text.ParseException; import java.util.Locale; import org.rhq.core.clientapi.util.ArrayUtil; import org.rhq.core.util.StringUtil; /** * Format a value into a duration. */ public class DurationFormatter implements Formatter { private static final int GRANULAR_YEARS = 1; // Time > 1 year private static final int GRANULAR_DAYS = 2; // 1 day < time < 1 year private static final int GRANULAR_HOURS = 3; // 1 hour < time < 1 day private static final int GRANULAR_MINS = 4; // 1 min < time < 1 hour private static final int GRANULAR_SECS = 5; // 0 < time < 1 min private static final int MILLISEC_DIGITS = 3; // Standard 3 digits to the // right of the decimal point private class TimeBreakDown { long nYears; long nDays; long nHours; long nMins; long nSecs; long nMilli; } public FormattedNumber format(UnitNumber val, Locale locale, FormatSpecifics specifics) { BigDecimal baseTime; int granularity; baseTime = val.getBaseValue(); granularity = this.getGranularity(baseTime); return this.format(baseTime, granularity, MILLISEC_DIGITS, locale); } public FormattedNumber[] formatSame(double[] val, UnitsConstants unitType, ScaleConstants scale, Locale locale, FormatSpecifics specifics) { FormattedNumber[] res; double[] secs; UnitNumber tmpNum; int maxIdx; int granularity; int milliDigits; TimeBreakDown tbd; boolean wholeNum; res = new FormattedNumber[val.length]; if ((maxIdx = ArrayUtil.max(val)) == -1) { return res; } tmpNum = new UnitNumber(val[maxIdx], unitType, scale); granularity = this.getGranularity(tmpNum.getBaseValue()); // Determine the number scale (right of the decimal point) that is // needed to ensure that every formatted number is unique and a // linear scale wholeNum = true; secs = new double[val.length]; for (int i = 0; i < secs.length; i++) { tbd = this.breakDownTime(UnitsFormat.getBaseValue(val[i], unitType, scale)); secs[i] = tbd.nSecs + ((double) tbd.nMilli / 1000); if (tbd.nMilli > 0) { wholeNum = false; } } milliDigits = UnitsUtil.getUniqueDigits(secs, locale); if ((milliDigits == 0) && (wholeNum == false)) { milliDigits = 1; } // Format the numbers for (int i = 0; i < val.length; i++) { tmpNum = new UnitNumber(val[i], unitType, scale); res[i] = this.format(tmpNum.getBaseValue(), granularity, milliDigits, locale); } return res; } private FormattedNumber format(BigDecimal baseTime, int granularity, int milliDigits, Locale locale) { TimeBreakDown tbd; String res; // We work in a few different time ranges depending on the // magnitude of the duration. Quite hardcoded if (granularity == GRANULAR_YEARS) { tbd = this.breakDownTime(baseTime); res = tbd.nYears + "y " + tbd.nDays + "d"; } else if (granularity == GRANULAR_DAYS) { long nDays; tbd = this.breakDownTime(baseTime); nDays = (tbd.nYears * 365) + tbd.nDays; res = nDays + ((nDays == 1) ? " day " : " days ") + StringUtil.formatDuration((tbd.nHours * 60 * 60 * 1000) + (tbd.nMins * 60 * 1000) + (tbd.nSecs * 1000) + tbd.nMilli); } else if ((granularity == GRANULAR_HOURS) || (granularity == GRANULAR_MINS) || (granularity == GRANULAR_SECS)) { long nMillis; nMillis = baseTime.divide(UnitsUtil.FACT_MILLIS, BigDecimal.ROUND_HALF_EVEN).longValue(); res = StringUtil.formatDuration(nMillis, milliDigits, granularity == GRANULAR_SECS); if (granularity == GRANULAR_SECS) { res = res + 's'; } } else { throw new IllegalStateException("Unexpected granularity"); } return new FormattedNumber(res.trim(), ""); } private TimeBreakDown breakDownTime(BigDecimal val) { TimeBreakDown r = new TimeBreakDown(); r.nYears = val.divide(UnitsUtil.FACT_YEARS, BigDecimal.ROUND_DOWN).intValue(); if (r.nYears > 0) { val = val.subtract(UnitsUtil.FACT_YEARS.multiply(new BigDecimal(r.nYears))); } r.nDays = val.divide(UnitsUtil.FACT_DAYS, BigDecimal.ROUND_DOWN).intValue(); if (r.nDays > 0) { val = val.subtract(UnitsUtil.FACT_DAYS.multiply(new BigDecimal(r.nDays))); } r.nHours = val.divide(UnitsUtil.FACT_HOURS, BigDecimal.ROUND_DOWN).intValue(); if (r.nHours > 0) { val = val.subtract(UnitsUtil.FACT_HOURS.multiply(new BigDecimal(r.nHours))); } r.nMins = val.divide(UnitsUtil.FACT_MINS, BigDecimal.ROUND_DOWN).intValue(); if (r.nMins > 0) { val = val.subtract(UnitsUtil.FACT_MINS.multiply(new BigDecimal(r.nMins))); } r.nSecs = val.divide(UnitsUtil.FACT_SECS, BigDecimal.ROUND_DOWN).intValue(); if (r.nSecs > 0) { val = val.subtract(UnitsUtil.FACT_SECS.multiply(new BigDecimal(r.nSecs))); } r.nMilli = val.divide(UnitsUtil.FACT_MILLIS, BigDecimal.ROUND_DOWN).intValue(); return r; } private int getGranularity(BigDecimal nanoSecs) { TimeBreakDown tbd = this.breakDownTime(nanoSecs); if (tbd.nYears > 0) { return GRANULAR_YEARS; } else if (tbd.nDays > 0) { return GRANULAR_DAYS; } else if (tbd.nHours > 0) { return GRANULAR_HOURS; } else if (tbd.nMins > 0) { return GRANULAR_MINS; } else { return GRANULAR_SECS; } } public BigDecimal getBaseValue(double value, ScaleConstants scale) { return DateFormatter.getBaseTime(value, scale); } public BigDecimal getScaledValue(BigDecimal value, ScaleConstants targScale) { return DateFormatter.getScaledTime(value, targScale); } /** * Returns the # of seconds in a string in the form of "xx.xs" or "xx:yy:zz.a" */ private double parseTimeStr(String duration) throws ParseException { double nHours; double nMins; double nSecs; String[] vals; vals = StringUtil.explodeToArray(duration, ":"); if (vals.length != 3) { throw new ParseException(duration, 0); } try { nHours = Double.parseDouble(vals[0]); nMins = Double.parseDouble(vals[1]); nSecs = Double.parseDouble(vals[2]); return (nHours * 60 * 60) + (nMins * 60) + nSecs; } catch (NumberFormatException exc) { throw new ParseException(duration, 0); } } /** * Parse a string which is of the same format that we output, when outputting durations. */ private UnitNumber parseRegular(String val, Locale locale, ParseSpecifics specifics) throws ParseException { String[] vals; vals = StringUtil.explodeToArray(val, " "); if ((vals.length == 2) && (vals[0].charAt(vals[0].length() - 1) == 'y') && (vals[1].charAt(vals[1].length() - 1) == 'd')) { try { String yStr = vals[0].substring(0, vals[0].length() - 1); String dStr = vals[1].substring(0, vals[1].length() - 1); return new UnitNumber((Integer.parseInt(yStr) * 365) + Integer.parseInt(dStr), UnitsConstants.UNIT_DURATION, ScaleConstants.SCALE_DAY); } catch (NumberFormatException exc) { // TODO what do we do here? } } else if ((vals.length == 3) && (vals[1].equals("day") || vals[1].equals("days"))) { try { return new UnitNumber((Integer.parseInt(vals[0]) * 24 * 60 * 60) + this.parseTimeStr(vals[2]), UnitsConstants.UNIT_DURATION, ScaleConstants.SCALE_SEC); } catch (NumberFormatException exc) { // TODO what do we do here? } } else if (vals.length == 1) { return new UnitNumber(this.parseTimeStr(vals[0]), UnitsConstants.UNIT_DURATION, ScaleConstants.SCALE_SEC); } throw new ParseException(val, 0); } public UnitNumber parse(String val, Locale locale, ParseSpecifics specifics) throws ParseException { NumberFormat fmt = NumberFormat.getInstance(locale); double numberPart; String tagPart; int nonIdx; ScaleConstants scale; try { return this.parseRegular(val, locale, specifics); } catch (ParseException exc) { // That's fine, try another format } nonIdx = UnitsUtil.findNonNumberIdx(val, fmt); if (nonIdx == -1) { throw new ParseException("Number had no units with it", val.length()); } if (nonIdx == 0) { throw new ParseException("Invalid number specified", 0); } numberPart = fmt.parse(val.substring(0, nonIdx)).doubleValue(); tagPart = val.substring(nonIdx, val.length()).trim(); if (tagPart.equalsIgnoreCase("y") // TODO hwr: how does I18N work here? || tagPart.equalsIgnoreCase("yr") || tagPart.equalsIgnoreCase("yrs") || tagPart.equalsIgnoreCase("year") || tagPart.equalsIgnoreCase("years")) { scale = ScaleConstants.SCALE_YEAR; } else if (tagPart.equalsIgnoreCase("w") || tagPart.equalsIgnoreCase("wk") || tagPart.equalsIgnoreCase("wks") || tagPart.equalsIgnoreCase("week") || tagPart.equalsIgnoreCase("weeks")) { scale = ScaleConstants.SCALE_WEEK; } else if (tagPart.equalsIgnoreCase("d") || tagPart.equalsIgnoreCase("day") || tagPart.equalsIgnoreCase("days")) { scale = ScaleConstants.SCALE_DAY; } else if (tagPart.equalsIgnoreCase("h") || tagPart.equalsIgnoreCase("hr") || tagPart.equalsIgnoreCase("hrs") || tagPart.equalsIgnoreCase("hour") || tagPart.equalsIgnoreCase("hours")) { scale = ScaleConstants.SCALE_HOUR; } else if (tagPart.equalsIgnoreCase("m") || tagPart.equalsIgnoreCase("min") || tagPart.equalsIgnoreCase("mins") || tagPart.equalsIgnoreCase("minute") || tagPart.equalsIgnoreCase("minutes")) { scale = ScaleConstants.SCALE_MIN; } else if (tagPart.equalsIgnoreCase("s") || tagPart.equalsIgnoreCase("sec") || tagPart.equalsIgnoreCase("secs") || tagPart.equalsIgnoreCase("second") || tagPart.equalsIgnoreCase("seconds")) { scale = ScaleConstants.SCALE_SEC; } else if (tagPart.equalsIgnoreCase("j") || tagPart.equalsIgnoreCase("jif") || tagPart.equalsIgnoreCase("jifs") || tagPart.equalsIgnoreCase("jiffy") || tagPart.equalsIgnoreCase("jiffys") || tagPart.equalsIgnoreCase("jifferoonies")) { scale = ScaleConstants.SCALE_JIFFY; } else if (tagPart.equalsIgnoreCase("ms") || tagPart.equalsIgnoreCase("milli") || tagPart.equalsIgnoreCase("millis") || tagPart.equalsIgnoreCase("millisecond") || tagPart.equalsIgnoreCase("milliseconds")) { scale = ScaleConstants.SCALE_MILLI; } else if (tagPart.equalsIgnoreCase("us") || tagPart.equalsIgnoreCase("micro") || tagPart.equalsIgnoreCase("micros") || tagPart.equalsIgnoreCase("microsecond") || tagPart.equalsIgnoreCase("microseconds")) { scale = ScaleConstants.SCALE_MICRO; } else if (tagPart.equalsIgnoreCase("ns") || tagPart.equalsIgnoreCase("nano") || tagPart.equalsIgnoreCase("nanos") || tagPart.equalsIgnoreCase("nanosecond") || tagPart.equalsIgnoreCase("nanoseconds")) { scale = ScaleConstants.SCALE_NANO; } else { throw new ParseException("Unknown duration '" + tagPart + "'", nonIdx); } return new UnitNumber(numberPart, UnitsConstants.UNIT_DURATION, scale); } }