1 /*******************************************************************************
2 * Copyright (c) 2007, 2011 Association for Decentralized Information Management in
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
10 * VTT Technical Research Centre of Finland - initial API and implementation
11 *******************************************************************************/
12 package org.simantics.utils.format;
14 import java.math.BigDecimal;
15 import java.math.MathContext;
16 import java.math.RoundingMode;
17 import java.text.FieldPosition;
18 import java.text.Format;
19 import java.text.ParseException;
20 import java.text.ParsePosition;
21 import java.util.regex.Matcher;
22 import java.util.regex.Pattern;
25 * Time format consists of four parts [Year Part] [Day part] [Time part] [Decimal part]
29 * "[yy]y ", and so on.
33 * "[dd]d ", and so on.
36 * Time part five formats:
43 * When time values are formatted into Strings, hours will be
44 * formatted with at most two digits and the the rest is converted
45 * into days and years. However, while parsing TimeFormat-Strings
46 * into time values (double seconds), the hour part (H) can consist
47 * of one or more digits. This is simply for parsing convenience.
50 * Decimal part has 1->* decimals. It is optional. It cannot exist without time part.
55 * @author Toni Kalajainen
57 public class TimeFormat extends Format {
59 private static final long serialVersionUID = 1L;
61 public static final Pattern PATTERN =
64 "(?:(\\d+)y *)?" + // Year part "[y]y"
65 "(?:(\\d+)d *)?" + // Day part "[d]d"
66 "(?:(?:(\\d{1,}):)??(?:(\\d{1,2}):)?(\\d{1,2}))?" + // Time part "[H*]H:mm:ss"
67 "(?:\\.(\\d+))?" // Decimal part ".ddd"
70 private static final BigDecimal TWO = BigDecimal.valueOf(2L);
74 RoundingMode rounding = RoundingMode.HALF_UP;
75 MathContext decimalRoundingContext;
77 public TimeFormat(double maxValue, int decimals)
79 this.maxValue = maxValue;
80 this.decimals = decimals;
81 this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding);
84 public void setMaxValue(double maxValue) {
85 this.maxValue = maxValue;
88 public void setDecimals(int decimals) {
89 this.decimals = decimals;
90 this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding);
93 public void setRounding(RoundingMode rounding) {
94 this.rounding = rounding;
95 this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding);
99 public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
100 // Prevent recurrent locking when invoking toAppendTo-methods.
101 synchronized (toAppendTo) {
102 return formatSync(obj, toAppendTo, pos);
106 private StringBuffer formatSync(Object obj, StringBuffer toAppendTo, FieldPosition pos) {
107 double x = ( (Number) obj ).doubleValue();
108 int initLen = toAppendTo.length();
110 if (Double.isInfinite(x)) {
111 return toAppendTo.append((x == Float.POSITIVE_INFINITY) ? "\u221E" : "-\u221E");
112 } else if (Double.isNaN(x)) {
113 return toAppendTo.append("NaN");
117 toAppendTo.append("-");
119 initLen = toAppendTo.length();
122 // The value of x-floor(x) is between [0,1].
123 // We want use BigDecimal to round to the specified number of decimals.
124 // The problem is that if x is 0.99999... so that it will be rounded to 1.000...
125 // the 1 at the front will count as a decimal in the rounding logic
126 // and we end up losing 1 actual decimal.
127 // Therefore we add 1.0 to x make it be between [1,2] in which case
128 // we can just round to n+1 decimals and it will always work.
129 BigDecimal decimalPart = new BigDecimal(x - Math.floor(x) + 1.0);
130 decimalPart = decimalPart.round(decimalRoundingContext);
131 // decimal is now [1.000..,2.000...].
132 // If decimalPart equals 2.0 it means that the
133 // requested decimal part value was close enough
134 // to 1.0 that it overflows and becomes 000...
135 // This means that the overflow must be added to/subtracted from
136 // the integer part of the input number.
137 boolean needToRound = TWO.compareTo(decimalPart) == 0;
139 double max = Math.max(this.maxValue, x);
140 long xl = needToRound ? (long) Math.round( x ) : (long) Math.floor( x );
143 if (xl>=(24L*60L*60L*365L)) {
144 long years = xl / (24L*60L*60L*365L);
145 toAppendTo.append( (long) years );
146 toAppendTo.append("y");
150 if (xl>=(24L*60L*60L)) {
151 if (toAppendTo.length()!=initLen) toAppendTo.append(' ');
152 long days = (xl % (24L*60L*60L*365L)) / (24L*60L*60L);
153 toAppendTo.append( (long) days );
154 toAppendTo.append("d");
159 if (toAppendTo.length()!=initLen) toAppendTo.append(' ');
160 // Seconds of the day
161 long seconds = xl % 86400L;
165 long hh = seconds / 3600;
167 toAppendTo.append( hh/10 );
169 toAppendTo.append( hh%10 );
170 toAppendTo.append(":");
175 long mm = (seconds / 60) % 60;
176 toAppendTo.append( mm/10 );
177 toAppendTo.append( mm%10 );
178 toAppendTo.append(":");
183 long ss = seconds % 60;
184 if (x>=10 || initLen!=toAppendTo.length()) {
185 toAppendTo.append( ss/10 );
187 toAppendTo.append( ss%10 );
190 // Write milliseconds and more
192 // add the decimal separator and part to the result.
193 toAppendTo.append('.');
194 String dps = decimalPart.toString();
195 int decimalPartLen = dps.length();
196 int trailingZeros = decimals;
197 if (decimalPartLen > 2) {
198 // If the original number was exact (e.g. 1)
199 // dp will contain only "1"
200 toAppendTo.append(dps, 2, decimalPartLen);
201 trailingZeros -= decimalPartLen - 2;
203 for (int d = 0; d < trailingZeros; ++d)
204 toAppendTo.append('0');
208 if (toAppendTo.length()==initLen) toAppendTo.append('-');
214 public Object parseObject(String source) throws ParseException {
215 Matcher m = PATTERN.matcher(source);
218 return Double.parseDouble( source );
219 } catch (NumberFormatException nfe) {
220 throw new ParseException("TimeFormat.parseObject('" + source + "') failed", m.regionStart());
224 String negG = m.group(1);
225 String yearG = m.group(2);
226 String dayG = m.group(3);
227 String hourG = m.group(4);
228 String minuteG = m.group(5);
229 String secondG = m.group(6);
230 String decimalG = m.group(7);
231 boolean negative = negG==null?false:negG.equals("-");
232 double years = yearG==null?0.:Double.parseDouble(yearG);
233 double days = dayG==null?0.:Double.parseDouble(dayG);
234 double hours = hourG==null?0.:Double.parseDouble(hourG);
235 double minutes = minuteG==null?0.:Double.parseDouble(minuteG);
236 double seconds = secondG==null?0.:Double.parseDouble(secondG);
237 double decimals = decimalG==null?0.:Double.parseDouble(decimalG)/Math.pow(10, decimalG.length());
239 double value = years*31536000. + days*86400. + hours*3600. + minutes*60. + seconds + decimals;
240 if ( negative ) value = -value;
245 public Object parseObject(String source, ParsePosition pos) {
247 return parseObject(source);
248 } catch (ParseException e) {
249 pos.setErrorIndex(e.getErrorOffset());