]> gerrit.simantics Code Review - simantics/platform.git/blob - bundles/org.simantics.utils/src/org/simantics/utils/format/TimeFormat.java
Merge "InputStream returns -1 on EOF instead of throwing IOException"
[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 (x<0) {\r
111                         toAppendTo.append("-");\r
112                         x=-x;\r
113                         initLen = toAppendTo.length();\r
114                 }\r
115 \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
132 \r
133                 double max = Math.max(this.maxValue, x);\r
134                 long xl = needToRound ? (long) Math.round( x ) : (long) Math.floor( x );\r
135 \r
136                 // Write years\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
141                 }\r
142 \r
143                 // Write days\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
149                 }\r
150 \r
151                 // Write HH:mm:ss\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
156                         \r
157                         // Write HH:\r
158                         if (max>=24*60) {\r
159                                 long hh = seconds / 3600;\r
160                                 if (x>3600) {\r
161                                         toAppendTo.append( hh/10 );\r
162                                 }\r
163                                 toAppendTo.append( hh%10 );\r
164                                 toAppendTo.append(":");\r
165                         }\r
166                         \r
167                         // Write mm:\r
168                         if (max>=60) {\r
169                                 long mm = (seconds / 60) % 60;\r
170                                 toAppendTo.append( mm/10 );\r
171                                 toAppendTo.append( mm%10 );\r
172                                 toAppendTo.append(":");\r
173                         }\r
174                         \r
175                         // Write ss\r
176                         {\r
177                                 long ss = seconds % 60;\r
178                                 if (x>=10 || initLen!=toAppendTo.length()) {\r
179                                         toAppendTo.append( ss/10 );\r
180                                 }\r
181                                 toAppendTo.append( ss%10 );\r
182                         }\r
183                         \r
184                         // Write milliseconds and more\r
185                         if (decimals>0) {\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
196                                 }\r
197                                 for (int d = 0; d < trailingZeros; ++d)\r
198                                         toAppendTo.append('0');\r
199                         }\r
200                 }\r
201                 \r
202                 if (toAppendTo.length()==initLen) toAppendTo.append('-');\r
203                 \r
204                 return toAppendTo;\r
205         }\r
206 \r
207         @Override\r
208         public Object parseObject(String source) throws ParseException {\r
209                 Matcher m = PATTERN.matcher(source);\r
210                 if (!m.matches()) {\r
211                         try {\r
212                                 return Double.parseDouble( source );\r
213                         } catch (NumberFormatException nfe) {\r
214                                 throw new ParseException("TimeFormat.parseObject('" + source + "') failed", m.regionStart());\r
215                         }\r
216                 }\r
217                 \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
232 \r
233                 double value = years*31536000. + days*86400. + hours*3600. + minutes*60. + seconds + decimals;\r
234                 if ( negative ) value = -value;\r
235                 return value;\r
236         }\r
237         \r
238         @Override\r
239         public Object parseObject(String source, ParsePosition pos) {\r
240                 try {\r
241             return parseObject(source);\r
242         } catch (ParseException e) {\r
243             pos.setErrorIndex(e.getErrorOffset());\r
244             return null;\r
245         }\r
246         }\r
247         \r
248 }\r