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