/******************************************************************************* * Copyright (c) 2007, 2011 Association for Decentralized Information Management in * Industry THTH ry. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * VTT Technical Research Centre of Finland - initial API and implementation *******************************************************************************/ package org.simantics.utils.format; import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; import java.text.FieldPosition; import java.text.Format; import java.text.ParseException; import java.text.ParsePosition; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Time format consists of four parts [Year Part] [Day part] [Time part] [Decimal part] *

* Year part. * "[y]y " * "[yy]y ", and so on. *

* Day part. * "[d]d " * "[dd]d ", and so on. * *

* Time part five formats: * "HH:mm:ss" * "H:mm:ss" * "mm:ss" * "ss" * "s" *

* When time values are formatted into Strings, hours will be * formatted with at most two digits and the the rest is converted * into days and years. However, while parsing TimeFormat-Strings * into time values (double seconds), the hour part (H) can consist * of one or more digits. This is simply for parsing convenience. * *

* Decimal part has 1->* decimals. It is optional. It cannot exist without time part. * ".d" * ".dd" * ".ddd", and so on. * * @author Toni Kalajainen */ public class TimeFormat extends Format { private static final long serialVersionUID = 1L; public static final Pattern PATTERN = Pattern.compile( "(-)?" + // Minus (-) "(?:(\\d+)y *)?" + // Year part "[y]y" "(?:(\\d+)d *)?" + // Day part "[d]d" "(?:(?:(\\d{1,}):)??(?:(\\d{1,2}):)?(\\d{1,2}))?" + // Time part "[H*]H:mm:ss" "(?:\\.(\\d+))?" // Decimal part ".ddd" ); private static final BigDecimal TWO = BigDecimal.valueOf(2L); double maxValue; int decimals; RoundingMode rounding = RoundingMode.HALF_UP; MathContext decimalRoundingContext; public TimeFormat(double maxValue, int decimals) { this.maxValue = maxValue; this.decimals = decimals; this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding); } public void setMaxValue(double maxValue) { this.maxValue = maxValue; } public void setDecimals(int decimals) { this.decimals = decimals; this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding); } public void setRounding(RoundingMode rounding) { this.rounding = rounding; this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding); } @Override public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) { // Prevent recurrent locking when invoking toAppendTo-methods. synchronized (toAppendTo) { return formatSync(obj, toAppendTo, pos); } } private StringBuffer formatSync(Object obj, StringBuffer toAppendTo, FieldPosition pos) { double x = ( (Number) obj ).doubleValue(); int initLen = toAppendTo.length(); if (Double.isInfinite(x)) { return toAppendTo.append((x == Float.POSITIVE_INFINITY) ? "\u221E" : "-\u221E"); } else if (Double.isNaN(x)) { return toAppendTo.append("NaN"); } if (x<0) { toAppendTo.append("-"); x=-x; initLen = toAppendTo.length(); } // The value of x-floor(x) is between [0,1]. // We want use BigDecimal to round to the specified number of decimals. // The problem is that if x is 0.99999... so that it will be rounded to 1.000... // the 1 at the front will count as a decimal in the rounding logic // and we end up losing 1 actual decimal. // Therefore we add 1.0 to x make it be between [1,2] in which case // we can just round to n+1 decimals and it will always work. BigDecimal decimalPart = new BigDecimal(x - Math.floor(x) + 1.0); decimalPart = decimalPart.round(decimalRoundingContext); // decimal is now [1.000..,2.000...]. // If decimalPart equals 2.0 it means that the // requested decimal part value was close enough // to 1.0 that it overflows and becomes 000... // This means that the overflow must be added to/subtracted from // the integer part of the input number. boolean needToRound = TWO.compareTo(decimalPart) == 0; double max = Math.max(this.maxValue, x); long xl = needToRound ? (long) Math.round( x ) : (long) Math.floor( x ); // Write years if (xl>=(24L*60L*60L*365L)) { long years = xl / (24L*60L*60L*365L); toAppendTo.append( (long) years ); toAppendTo.append("y"); } // Write days if (xl>=(24L*60L*60L)) { if (toAppendTo.length()!=initLen) toAppendTo.append(' '); long days = (xl % (24L*60L*60L*365L)) / (24L*60L*60L); toAppendTo.append( (long) days ); toAppendTo.append("d"); } // Write HH:mm:ss if (decimals>=-5) { if (toAppendTo.length()!=initLen) toAppendTo.append(' '); // Seconds of the day long seconds = xl % 86400L; // Write HH: if (max>=24*60) { long hh = seconds / 3600; if (x>3600) { toAppendTo.append( hh/10 ); } toAppendTo.append( hh%10 ); toAppendTo.append(":"); } // Write mm: if (max>=60) { long mm = (seconds / 60) % 60; toAppendTo.append( mm/10 ); toAppendTo.append( mm%10 ); toAppendTo.append(":"); } // Write ss { long ss = seconds % 60; if (x>=10 || initLen!=toAppendTo.length()) { toAppendTo.append( ss/10 ); } toAppendTo.append( ss%10 ); } // Write milliseconds and more if (decimals>0) { // add the decimal separator and part to the result. toAppendTo.append('.'); String dps = decimalPart.toString(); int decimalPartLen = dps.length(); int trailingZeros = decimals; if (decimalPartLen > 2) { // If the original number was exact (e.g. 1) // dp will contain only "1" toAppendTo.append(dps, 2, decimalPartLen); trailingZeros -= decimalPartLen - 2; } for (int d = 0; d < trailingZeros; ++d) toAppendTo.append('0'); } } if (toAppendTo.length()==initLen) toAppendTo.append('-'); return toAppendTo; } @Override public Object parseObject(String source) throws ParseException { Matcher m = PATTERN.matcher(source); if (!m.matches()) { try { return Double.parseDouble( source ); } catch (NumberFormatException nfe) { throw new ParseException("TimeFormat.parseObject('" + source + "') failed", m.regionStart()); } } String negG = m.group(1); String yearG = m.group(2); String dayG = m.group(3); String hourG = m.group(4); String minuteG = m.group(5); String secondG = m.group(6); String decimalG = m.group(7); boolean negative = negG==null?false:negG.equals("-"); double years = yearG==null?0.:Double.parseDouble(yearG); double days = dayG==null?0.:Double.parseDouble(dayG); double hours = hourG==null?0.:Double.parseDouble(hourG); double minutes = minuteG==null?0.:Double.parseDouble(minuteG); double seconds = secondG==null?0.:Double.parseDouble(secondG); double decimals = decimalG==null?0.:Double.parseDouble(decimalG)/Math.pow(10, decimalG.length()); double value = years*31536000. + days*86400. + hours*3600. + minutes*60. + seconds + decimals; if ( negative ) value = -value; return value; } @Override public Object parseObject(String source, ParsePosition pos) { try { return parseObject(source); } catch (ParseException e) { pos.setErrorIndex(e.getErrorOffset()); return null; } } }