Merge commit '0b471805f017da83d715a0d8409f53bdd009d31e'
[simantics/platform.git] / bundles / org.simantics.utils / src / org / simantics / utils / format / TimeFormat.java
1 /*******************************************************************************\r
2  * Copyright (c) 2007, 2011 Association for Decentralized Information Management in\r
3  * Industry THTH ry.\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
8  *\r
9  * Contributors:\r
10  *     VTT Technical Research Centre of Finland - initial API and implementation\r
11  *******************************************************************************/\r
12 package org.simantics.utils.format;\r
13 \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
23 \r
24 /**\r
25  * Time format consists of four parts [Year Part] [Day part] [Time part] [Decimal part]\r
26  *  <p>\r
27  *  Year part. \r
28  *    "[y]y "\r
29  *    "[yy]y ", and so on.\r
30  *  <p>\r
31  *  Day part. \r
32  *    "[d]d "\r
33  *    "[dd]d ", and so on.\r
34  *   \r
35  *  <p>\r
36  *  Time part five formats:\r
37  *    "HH:mm:ss"\r
38  *    "H:mm:ss"\r
39  *    "mm:ss"\r
40  *    "ss"\r
41  *    "s"\r
42  *  <p>\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
48  *  \r
49  *  <p>\r
50  *  Decimal part has 1->* decimals. It is optional. It cannot exist without time part. \r
51  *    ".d"\r
52  *    ".dd"\r
53  *    ".ddd", and so on.\r
54  * \r
55  * @author Toni Kalajainen\r
56  */\r
57 public class TimeFormat extends Format {\r
58 \r
59         private static final long serialVersionUID = 1L;\r
60         \r
61         public static final Pattern PATTERN = \r
62                         Pattern.compile(\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
68                                         );\r
69 \r
70         private static final BigDecimal TWO = BigDecimal.valueOf(2L);\r
71 \r
72         double maxValue;\r
73         int decimals;\r
74         RoundingMode rounding = RoundingMode.HALF_UP;\r
75         MathContext decimalRoundingContext;\r
76         \r
77         public TimeFormat(double maxValue, int decimals)\r
78         {\r
79                 this.maxValue = maxValue;\r
80                 this.decimals = decimals;\r
81                 this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding);\r
82         }\r
83 \r
84         public void setMaxValue(double maxValue) {\r
85                 this.maxValue = maxValue;\r
86         }\r
87         \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
91         }\r
92         \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
96         }\r
97 \r
98         @Override\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
103                 }\r
104         }\r
105 \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
109 \r
110                 if (Double.isInfinite(x)) {\r
111                         return toAppendTo.append((x == Float.POSITIVE_INFINITY) ? "\u221E" : "-\u221E");\r
112                 } else if (Double.isNaN(x)) {\r
113                         return toAppendTo.append("NaN");\r
114                 }\r
115 \r
116                 if (x<0) {\r
117                         toAppendTo.append("-");\r
118                         x=-x;\r
119                         initLen = toAppendTo.length();\r
120                 }\r
121 \r
122                 // The value of x-floor(x) is between [0,1].\r
123                 // We want use BigDecimal to round to the specified number of decimals.\r
124                 // The problem is that if x is 0.99999... so that it will be rounded to 1.000...\r
125                 // the 1 at the front will count as a decimal in the rounding logic\r
126                 // and we end up losing 1 actual decimal.\r
127                 // Therefore we add 1.0 to x make it be between [1,2] in which case\r
128                 // we can just round to n+1 decimals and it will always work.\r
129                 BigDecimal decimalPart = new BigDecimal(x - Math.floor(x) + 1.0);\r
130                 decimalPart = decimalPart.round(decimalRoundingContext);\r
131                 // decimal is now [1.000..,2.000...].\r
132                 // If decimalPart equals 2.0 it means that the\r
133                 // requested decimal part value was close enough\r
134                 // to 1.0 that it overflows and becomes 000...\r
135                 // This means that the overflow must be added to/subtracted from\r
136                 // the integer part of the input number.\r
137                 boolean needToRound = TWO.compareTo(decimalPart) == 0;\r
138 \r
139                 double max = Math.max(this.maxValue, x);\r
140                 long xl = needToRound ? (long) Math.round( x ) : (long) Math.floor( x );\r
141 \r
142                 // Write years\r
143                 if (xl>=(24L*60L*60L*365L)) {\r
144                         long years = xl / (24L*60L*60L*365L);\r
145                         toAppendTo.append( (long) years );\r
146                         toAppendTo.append("y");\r
147                 }\r
148 \r
149                 // Write days\r
150                 if (xl>=(24L*60L*60L)) {\r
151                         if (toAppendTo.length()!=initLen) toAppendTo.append(' ');\r
152                         long days = (xl % (24L*60L*60L*365L)) / (24L*60L*60L); \r
153                         toAppendTo.append( (long) days );\r
154                         toAppendTo.append("d");\r
155                 }\r
156 \r
157                 // Write HH:mm:ss\r
158                 if (decimals>=-5) {\r
159                         if (toAppendTo.length()!=initLen) toAppendTo.append(' ');\r
160                         // Seconds of the day\r
161                         long seconds = xl % 86400L;\r
162                         \r
163                         // Write HH:\r
164                         if (max>=24*60) {\r
165                                 long hh = seconds / 3600;\r
166                                 if (x>3600) {\r
167                                         toAppendTo.append( hh/10 );\r
168                                 }\r
169                                 toAppendTo.append( hh%10 );\r
170                                 toAppendTo.append(":");\r
171                         }\r
172                         \r
173                         // Write mm:\r
174                         if (max>=60) {\r
175                                 long mm = (seconds / 60) % 60;\r
176                                 toAppendTo.append( mm/10 );\r
177                                 toAppendTo.append( mm%10 );\r
178                                 toAppendTo.append(":");\r
179                         }\r
180                         \r
181                         // Write ss\r
182                         {\r
183                                 long ss = seconds % 60;\r
184                                 if (x>=10 || initLen!=toAppendTo.length()) {\r
185                                         toAppendTo.append( ss/10 );\r
186                                 }\r
187                                 toAppendTo.append( ss%10 );\r
188                         }\r
189                         \r
190                         // Write milliseconds and more\r
191                         if (decimals>0) {\r
192                                 // add the decimal separator and part to the result.\r
193                                 toAppendTo.append('.');\r
194                                 String dps = decimalPart.toString();\r
195                                 int decimalPartLen = dps.length();\r
196                                 int trailingZeros = decimals;\r
197                                 if (decimalPartLen > 2) {\r
198                                         // If the original number was exact (e.g. 1)\r
199                                         // dp will contain only "1"\r
200                                         toAppendTo.append(dps, 2, decimalPartLen);\r
201                                         trailingZeros -= decimalPartLen - 2; \r
202                                 }\r
203                                 for (int d = 0; d < trailingZeros; ++d)\r
204                                         toAppendTo.append('0');\r
205                         }\r
206                 }\r
207                 \r
208                 if (toAppendTo.length()==initLen) toAppendTo.append('-');\r
209                 \r
210                 return toAppendTo;\r
211         }\r
212 \r
213         @Override\r
214         public Object parseObject(String source) throws ParseException {\r
215                 Matcher m = PATTERN.matcher(source);\r
216                 if (!m.matches()) {\r
217                         try {\r
218                                 return Double.parseDouble( source );\r
219                         } catch (NumberFormatException nfe) {\r
220                                 throw new ParseException("TimeFormat.parseObject('" + source + "') failed", m.regionStart());\r
221                         }\r
222                 }\r
223                 \r
224                 String negG = m.group(1);\r
225                 String yearG = m.group(2);\r
226                 String dayG = m.group(3);\r
227                 String hourG = m.group(4);\r
228                 String minuteG = m.group(5);\r
229                 String secondG = m.group(6);\r
230                 String decimalG = m.group(7);\r
231                 boolean negative = negG==null?false:negG.equals("-");\r
232                 double years = yearG==null?0.:Double.parseDouble(yearG);\r
233                 double days = dayG==null?0.:Double.parseDouble(dayG);\r
234                 double hours = hourG==null?0.:Double.parseDouble(hourG);\r
235                 double minutes = minuteG==null?0.:Double.parseDouble(minuteG);\r
236                 double seconds = secondG==null?0.:Double.parseDouble(secondG);\r
237                 double decimals = decimalG==null?0.:Double.parseDouble(decimalG)/Math.pow(10, decimalG.length());\r
238 \r
239                 double value = years*31536000. + days*86400. + hours*3600. + minutes*60. + seconds + decimals;\r
240                 if ( negative ) value = -value;\r
241                 return value;\r
242         }\r
243         \r
244         @Override\r
245         public Object parseObject(String source, ParsePosition pos) {\r
246                 try {\r
247             return parseObject(source);\r
248         } catch (ParseException e) {\r
249             pos.setErrorIndex(e.getErrorOffset());\r
250             return null;\r
251         }\r
252         }\r
253         \r
254 }\r