--- /dev/null
+/*******************************************************************************\r
+ * Copyright (c) 2007, 2011 Association for Decentralized Information Management in\r
+ * Industry THTH ry.\r
+ * All rights reserved. This program and the accompanying materials\r
+ * are made available under the terms of the Eclipse Public License v1.0\r
+ * which accompanies this distribution, and is available at\r
+ * http://www.eclipse.org/legal/epl-v10.html\r
+ *\r
+ * Contributors:\r
+ * VTT Technical Research Centre of Finland - initial API and implementation\r
+ *******************************************************************************/\r
+package org.simantics.utils.format;\r
+\r
+import java.math.BigDecimal;\r
+import java.math.MathContext;\r
+import java.math.RoundingMode;\r
+import java.text.FieldPosition;\r
+import java.text.Format;\r
+import java.text.ParseException;\r
+import java.text.ParsePosition;\r
+import java.util.regex.Matcher;\r
+import java.util.regex.Pattern;\r
+\r
+/**\r
+ * Time format consists of four parts [Year Part] [Day part] [Time part] [Decimal part]\r
+ * <p>\r
+ * Year part. \r
+ * "[y]y "\r
+ * "[yy]y ", and so on.\r
+ * <p>\r
+ * Day part. \r
+ * "[d]d "\r
+ * "[dd]d ", and so on.\r
+ * \r
+ * <p>\r
+ * Time part five formats:\r
+ * "HH:mm:ss"\r
+ * "H:mm:ss"\r
+ * "mm:ss"\r
+ * "ss"\r
+ * "s"\r
+ * <p>\r
+ * When time values are formatted into Strings, hours will be\r
+ * formatted with at most two digits and the the rest is converted\r
+ * into days and years. However, while parsing TimeFormat-Strings\r
+ * into time values (double seconds), the hour part (H) can consist\r
+ * of one or more digits. This is simply for parsing convenience.\r
+ * \r
+ * <p>\r
+ * Decimal part has 1->* decimals. It is optional. It cannot exist without time part. \r
+ * ".d"\r
+ * ".dd"\r
+ * ".ddd", and so on.\r
+ * \r
+ * @author Toni Kalajainen\r
+ */\r
+public class TimeFormat extends Format {\r
+\r
+ private static final long serialVersionUID = 1L;\r
+ \r
+ public static final Pattern PATTERN = \r
+ Pattern.compile(\r
+ "(-)?" + // Minus (-)\r
+ "(?:(\\d+)y *)?" + // Year part "[y]y"\r
+ "(?:(\\d+)d *)?" + // Day part "[d]d"\r
+ "(?:(?:(\\d{1,}):)??(?:(\\d{1,2}):)?(\\d{1,2}))?" + // Time part "[H*]H:mm:ss"\r
+ "(?:\\.(\\d+))?" // Decimal part ".ddd"\r
+ );\r
+\r
+ private static final BigDecimal TWO = BigDecimal.valueOf(2L);\r
+\r
+ double maxValue;\r
+ int decimals;\r
+ RoundingMode rounding = RoundingMode.HALF_UP;\r
+ MathContext decimalRoundingContext;\r
+ \r
+ public TimeFormat(double maxValue, int decimals)\r
+ {\r
+ this.maxValue = maxValue;\r
+ this.decimals = decimals;\r
+ this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding);\r
+ }\r
+\r
+ public void setMaxValue(double maxValue) {\r
+ this.maxValue = maxValue;\r
+ }\r
+ \r
+ public void setDecimals(int decimals) {\r
+ this.decimals = decimals;\r
+ this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding);\r
+ }\r
+ \r
+ public void setRounding(RoundingMode rounding) {\r
+ this.rounding = rounding;\r
+ this.decimalRoundingContext = new MathContext(Math.max(1, decimals+1), rounding);\r
+ }\r
+\r
+ @Override\r
+ public StringBuffer format(Object obj, StringBuffer toAppendTo, FieldPosition pos) {\r
+ // Prevent recurrent locking when invoking toAppendTo-methods.\r
+ synchronized (toAppendTo) {\r
+ return formatSync(obj, toAppendTo, pos);\r
+ }\r
+ }\r
+\r
+ private StringBuffer formatSync(Object obj, StringBuffer toAppendTo, FieldPosition pos) {\r
+ double x = ( (Number) obj ).doubleValue(); \r
+ int initLen = toAppendTo.length();\r
+ \r
+ if (x<0) {\r
+ toAppendTo.append("-");\r
+ x=-x;\r
+ initLen = toAppendTo.length();\r
+ }\r
+\r
+ // The value of x-floor(x) is between [0,1].\r
+ // We want use BigDecimal to round to the specified number of decimals.\r
+ // The problem is that if x is 0.99999... so that it will be rounded to 1.000...\r
+ // the 1 at the front will count as a decimal in the rounding logic\r
+ // and we end up losing 1 actual decimal.\r
+ // Therefore we add 1.0 to x make it be between [1,2] in which case\r
+ // we can just round to n+1 decimals and it will always work.\r
+ BigDecimal decimalPart = new BigDecimal(x - Math.floor(x) + 1.0);\r
+ decimalPart = decimalPart.round(decimalRoundingContext);\r
+ // decimal is now [1.000..,2.000...].\r
+ // If decimalPart equals 2.0 it means that the\r
+ // requested decimal part value was close enough\r
+ // to 1.0 that it overflows and becomes 000...\r
+ // This means that the overflow must be added to/subtracted from\r
+ // the integer part of the input number.\r
+ boolean needToRound = TWO.compareTo(decimalPart) == 0;\r
+\r
+ double max = Math.max(this.maxValue, x);\r
+ long xl = needToRound ? (long) Math.round( x ) : (long) Math.floor( x );\r
+\r
+ // Write years\r
+ if (xl>=(24L*60L*60L*365L)) {\r
+ long years = xl / (24L*60L*60L*365L);\r
+ toAppendTo.append( (long) years );\r
+ toAppendTo.append("y");\r
+ }\r
+\r
+ // Write days\r
+ if (xl>=(24L*60L*60L)) {\r
+ if (toAppendTo.length()!=initLen) toAppendTo.append(' ');\r
+ long days = (xl % (24L*60L*60L*365L)) / (24L*60L*60L); \r
+ toAppendTo.append( (long) days );\r
+ toAppendTo.append("d");\r
+ }\r
+\r
+ // Write HH:mm:ss\r
+ if (decimals>=-5) {\r
+ if (toAppendTo.length()!=initLen) toAppendTo.append(' ');\r
+ // Seconds of the day\r
+ long seconds = xl % 86400L;\r
+ \r
+ // Write HH:\r
+ if (max>=24*60) {\r
+ long hh = seconds / 3600;\r
+ if (x>3600) {\r
+ toAppendTo.append( hh/10 );\r
+ }\r
+ toAppendTo.append( hh%10 );\r
+ toAppendTo.append(":");\r
+ }\r
+ \r
+ // Write mm:\r
+ if (max>=60) {\r
+ long mm = (seconds / 60) % 60;\r
+ toAppendTo.append( mm/10 );\r
+ toAppendTo.append( mm%10 );\r
+ toAppendTo.append(":");\r
+ }\r
+ \r
+ // Write ss\r
+ {\r
+ long ss = seconds % 60;\r
+ if (x>=10 || initLen!=toAppendTo.length()) {\r
+ toAppendTo.append( ss/10 );\r
+ }\r
+ toAppendTo.append( ss%10 );\r
+ }\r
+ \r
+ // Write milliseconds and more\r
+ if (decimals>0) {\r
+ // add the decimal separator and part to the result.\r
+ toAppendTo.append('.');\r
+ String dps = decimalPart.toString();\r
+ int decimalPartLen = dps.length();\r
+ int trailingZeros = decimals;\r
+ if (decimalPartLen > 2) {\r
+ // If the original number was exact (e.g. 1)\r
+ // dp will contain only "1"\r
+ toAppendTo.append(dps, 2, decimalPartLen);\r
+ trailingZeros -= decimalPartLen - 2; \r
+ }\r
+ for (int d = 0; d < trailingZeros; ++d)\r
+ toAppendTo.append('0');\r
+ }\r
+ }\r
+ \r
+ if (toAppendTo.length()==initLen) toAppendTo.append('-');\r
+ \r
+ return toAppendTo;\r
+ }\r
+\r
+ @Override\r
+ public Object parseObject(String source) throws ParseException {\r
+ Matcher m = PATTERN.matcher(source);\r
+ if (!m.matches()) {\r
+ try {\r
+ return Double.parseDouble( source );\r
+ } catch (NumberFormatException nfe) {\r
+ throw new ParseException("TimeFormat.parseObject('" + source + "') failed", m.regionStart());\r
+ }\r
+ }\r
+ \r
+ String negG = m.group(1);\r
+ String yearG = m.group(2);\r
+ String dayG = m.group(3);\r
+ String hourG = m.group(4);\r
+ String minuteG = m.group(5);\r
+ String secondG = m.group(6);\r
+ String decimalG = m.group(7);\r
+ boolean negative = negG==null?false:negG.equals("-");\r
+ double years = yearG==null?0.:Double.parseDouble(yearG);\r
+ double days = dayG==null?0.:Double.parseDouble(dayG);\r
+ double hours = hourG==null?0.:Double.parseDouble(hourG);\r
+ double minutes = minuteG==null?0.:Double.parseDouble(minuteG);\r
+ double seconds = secondG==null?0.:Double.parseDouble(secondG);\r
+ double decimals = decimalG==null?0.:Double.parseDouble(decimalG)/Math.pow(10, decimalG.length());\r
+\r
+ double value = years*31536000. + days*86400. + hours*3600. + minutes*60. + seconds + decimals;\r
+ if ( negative ) value = -value;\r
+ return value;\r
+ }\r
+ \r
+ @Override\r
+ public Object parseObject(String source, ParsePosition pos) {\r
+ try {\r
+ return parseObject(source);\r
+ } catch (ParseException e) {\r
+ pos.setErrorIndex(e.getErrorOffset());\r
+ return null;\r
+ }\r
+ }\r
+ \r
+}\r