/* File: TimeDatumFormatter.java * Copyright (C) 2002-2003 The University of Iowa * * Created on September 25, 2003, 1:47 PM * by Edward West * * This file is part of the das2 library. * * das2 is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package org.das2.datum.format; import java.text.Format; import java.text.MessageFormat; import java.text.ParseException; import java.util.logging.Level; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.das2.datum.Datum; import org.das2.datum.DatumRange; import org.das2.datum.DatumVector; import org.das2.datum.TimeUtil; import org.das2.datum.Units; /** * Formatter is configured with strings of SimpleDateFormat (yyyy-MM-DD) * or % percent format (%Y-%m-%d) specifiers. * SimpleDateFormat is discouraged because most other parts of das2 use the * shorter form. * This does not support formats with seconds but no hours and minutes, like "$Y $j $S" * * @author Edward West */ public class TimeDatumFormatter extends DatumFormatter { /** Private constants for referencing an array of timestamp fields */ private static final int YEAR_FIELD_INDEX = 0; private static final int MONTH_FIELD_INDEX = 1; private static final int DAY_FIELD_INDEX = 2; private static final int DOY_FIELD_INDEX = 3; private static final int HOUR_FIELD_INDEX = 4; private static final int MINUTE_FIELD_INDEX = 5; private static final int SECONDS_FIELD_INDEX = 6; private static final int TIMESTAMP_FIELD_COUNT = 7; /** * yyyy-MM-dd'T'HH:mm:ss.SSS'Z */ public static final TimeDatumFormatter DEFAULT; /** * yyyy-MM-dd */ public static final TimeDatumFormatter DAYS; /** * yyyy-JJJ */ public static final TimeDatumFormatter DAY_OF_YEAR; /** * yyyy-MM-dd (JJJ) */ public static final TimeDatumFormatter DOY_DAYS; /** * yyyy */ public static final TimeDatumFormatter YEARS; /** * yyyy-MM */ public static final TimeDatumFormatter MONTHS; /** * yyyy-MM-dd HH:'00' */ public static final TimeDatumFormatter HOURS; /** * HH:mm */ public static final TimeDatumFormatter MINUTES; /** * HH:mm:ss */ public static final TimeDatumFormatter SECONDS; /** * HH:mm:ss.SSS */ public static final TimeDatumFormatter MILLISECONDS; /** * HH:mm:ss.SSSSSS */ public static final TimeDatumFormatter MICROSECONDS; /** * HH:mm:ss.SSSSSSSSS */ public static final TimeDatumFormatter NANOSECONDS; //Initialize final constants static { try { DEFAULT = new TimeDatumFormatter("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"); YEARS= new TimeDatumFormatter("yyyy"); MONTHS= new TimeDatumFormatter("yyyy-MM"); DAYS = new TimeDatumFormatter("yyyy-MM-dd"); DAY_OF_YEAR = new TimeDatumFormatter("%Y-%j"); DOY_DAYS = new TimeDatumFormatter("%Y-%m-%d (%j)"); HOURS = new TimeDatumFormatter("yyyy-MM-dd HH:'00'"); MINUTES = new TimeDatumFormatter("HH:mm"); SECONDS = new TimeDatumFormatter("HH:mm:ss"); MILLISECONDS = new TimeDatumFormatter("HH:mm:ss.SSS"); MICROSECONDS = new TimeDatumFormatter("HH:mm:ss.SSSSSS"); NANOSECONDS = new TimeDatumFormatter("HH:mm:ss.SSSSSSSSS"); } catch (ParseException pe) { throw new RuntimeException(pe); } } private final String formatString; private final MessageFormat format; private int[] scaleSeconds; /** * Creates a new instance of TimeDatumFormatter * @param formatString * @throws java.text.ParseException */ public TimeDatumFormatter(String formatString) throws ParseException { this.formatString = formatString; if ( formatString.contains( "%" ) ) { format = new MessageFormat(parseTimeFormatStringPercent(formatString)); } else { format = new MessageFormat(parseTimeFormatString(formatString)); } } /** * returns a TimeDatumFormatter suitable for the specified scale and context. * Context may be null to indicate that the formatted string will be interpreted * outside of any context. * @param scale the length we wish to represent, such as TimeUtil.HOUR * @param context the context for the formatter, or null if the formatted string * will be interpreted outside of any context. * @return the formatter. * @throws IllegalArgumentException if the scale is TimeUtil.NANOS or is not found in TimeUtil. */ public static TimeDatumFormatter formatterForScale( int scale, DatumRange context ) { return formatterForScale( scale, context, false ); } /** * guess formatted based on the two ticks given * @param ticks values to represent * @param context null or the external context where the data is interpreted. * @return the formatter * @see DateTimeDatumFormatter#axisFormat */ public static TimeDatumFormatter guessFormatter( DatumVector ticks, DatumRange context ) { int scale; double width; if ( ticks.getLength()<2 ) { scale= TimeUtil.MICRO; } else { width= ticks.get(1).subtract(ticks.get(0)).doubleValue(Units.microseconds); if ( width>=60e6 ) { scale= TimeUtil.MINUTE; } else if ( width>=1e6 ) { scale= TimeUtil.SECOND; } else if ( width>=1e3 ) { scale= TimeUtil.MILLI; } else { scale= TimeUtil.MICRO; } } return formatterForScale( scale, context ); } /** * returns a TimeDatumFormatter suitable for the specified scale and context. * Context may be null to indicate that the formatted string will be interpreted * outside of any context. * @param scale the length we wish to represent, such as TimeUtil.HOUR * @param context the context for the formatter, or null if the formatted string * will be interpreted outside of any context. * @param useDOY if true then use day-of-year rather than separate month and day. * @return the formatter. * @throws IllegalArgumentException if the scale is TimeUtil.NANOS or is not found in TimeUtil. */ public static TimeDatumFormatter formatterForScale( int scale, DatumRange context, boolean useDOY ) { try { if ( context!=null ) { switch ( scale ) { case TimeUtil.YEAR: return YEARS; case TimeUtil.MONTH: return useDOY ? DAY_OF_YEAR : MONTHS; case TimeUtil.DAY: return useDOY ? DAY_OF_YEAR : DAYS; case TimeUtil.HOUR: return MINUTES; case TimeUtil.MINUTE: return MINUTES; case TimeUtil.SECOND: return SECONDS; case TimeUtil.MILLI: return MILLISECONDS; case TimeUtil.MICRO: return MICROSECONDS; case TimeUtil.NANO: return NANOSECONDS; default: throw new IllegalArgumentException("unsupported scale: "+scale); } } else { if ( useDOY ) { switch ( scale ) { case TimeUtil.YEAR: return YEARS; case TimeUtil.MONTH: return DAY_OF_YEAR; case TimeUtil.DAY: return DAY_OF_YEAR; case TimeUtil.HOUR: return new TimeDatumFormatter("%Y-%j %H:%M"); case TimeUtil.MINUTE: return new TimeDatumFormatter("%Y-%j %H:%M"); case TimeUtil.SECOND: return new TimeDatumFormatter("%Y-%j %H:%M:%S"); case TimeUtil.MILLI: return new TimeDatumFormatter("%Y-%j %H:%M:%S.%(subsec,places=3)"); case TimeUtil.MICRO: return new TimeDatumFormatter("%Y-%j %H:%M:%S.%(subsec,places=6)"); case TimeUtil.NANO: return new TimeDatumFormatter("%Y-%j %H:%M:%S.%(subsec,places=9)"); default: throw new IllegalArgumentException("unsupported scale: "+scale); } } else { switch ( scale ) { case TimeUtil.YEAR: return YEARS; case TimeUtil.MONTH: return MONTHS; case TimeUtil.DAY: return DAYS; case TimeUtil.HOUR: return new TimeDatumFormatter("yyyy-MM-dd HH:mm"); case TimeUtil.MINUTE: return new TimeDatumFormatter("yyyy-MM-dd HH:mm"); case TimeUtil.SECOND: return new TimeDatumFormatter("yyyy-MM-dd HH:mm:ss"); case TimeUtil.MILLI: return new TimeDatumFormatter("yyyy-MM-dd HH:mm:ss.SSS"); case TimeUtil.MICRO: return new TimeDatumFormatter("yyyy-MM-dd HH:mm:ss.SSSSSS"); case TimeUtil.NANO: return new TimeDatumFormatter("yyyy-MM-dd HH:mm:ss.SSSSSSSSS"); default: throw new IllegalArgumentException("unsupported scale: "+scale); } } } } catch ( ParseException e ) { throw new RuntimeException(e); } } @Override public String toString() { return formatString; } @Override public String format(Datum datum) { if ( datum.isFill() ) return "fill"; int mjd1958= (int)datum.doubleValue( Units.mj1958 ); TimeUtil.TimeStruct ts; if ( mjd1958 < -349909 ) { // year 1000 ts= TimeUtil.toTimeStruct( Units.mj1958.createDatum(-349909) ); } else if ( mjd1958 > 2937279 ) { // year 9999 // should be 2937613 rounding errors? ts= TimeUtil.toTimeStruct( Units.mj1958.createDatum(2937279) ); } else { ts= TimeUtil.toTimeStruct(datum); } Number[] array = timeStructToArray(ts); if ( array.length==8 && array[7].intValue()==-1 ) array[7]=0; // https://github.com/das-developers/das2java/issues/16 return format.format(array); } protected Format getFormat() { return format; } /** * parse message format strings like yyyy-MM-DD * @param input * @return * @throws ParseException */ protected final String parseTimeFormatString(String input) throws ParseException { final String formatPattern = "(([yMDdHmsS])\\2*)"; final String delimiterPattern = "([-/:.,_ \t]+)"; final String literalPattern = "('(?:[^']|'')*')"; Pattern token = Pattern.compile( formatPattern + "|" + delimiterPattern + "|" + literalPattern ); int from = 0; StringBuilder frmtString = new StringBuilder(); Matcher matcher = token.matcher(input); while (matcher.find(from)) { int start = matcher.start(); if (start > from) { char[] dots = new char[start + 1]; java.util.Arrays.fill(dots, from, start, '.'); dots[from] = '^'; dots[start] = '^'; StringBuilder errorString = new StringBuilder("Unrecognized sub-pattern\n"); errorString.append(input).append("\n"); errorString.append(dots); throw new ParseException(errorString.toString(), from); } String frmt = matcher.group(1); String delimiter = matcher.group(3); String literal = matcher.group(4); if (frmt != null) { switch (frmt.charAt(0)) { case 'y': { appendSubFormat(frmtString, YEAR_FIELD_INDEX, frmt.length()); } break; case 'M': { appendSubFormat(frmtString, MONTH_FIELD_INDEX, frmt.length()); } break; case 'D': { appendSubFormat(frmtString, DOY_FIELD_INDEX, frmt.length()); } break; case 'd': { appendSubFormat(frmtString, DAY_FIELD_INDEX, frmt.length()); } break; case 'H': { appendSubFormat(frmtString, HOUR_FIELD_INDEX, frmt.length()); } break; case 'm': { appendSubFormat(frmtString, MINUTE_FIELD_INDEX, frmt.length()); } break; case 's': { appendSubFormat(frmtString, SECONDS_FIELD_INDEX, frmt.length()); }break; case 'S': { int digitCount = frmt.length(); int fieldIndex = addScaleFactor(digitCount); appendSubFormat(frmtString, fieldIndex, digitCount); } break; default: break; } } else if (delimiter != null) { frmtString.append(delimiter); } else if (literal != null) { literal = literal.substring(1, literal.length() - 1); literal = literal.replaceAll("''", "'"); frmtString.append(literal); } from = matcher.end(); } return frmtString.toString(); } /** * create the message format, based on %Y, %m, %d format specification * of the UNIX date command. * @param format the format spec, such as %Y%m%dT%H%M * @return a SimpleDateFormat string. * @throws java.text.ParseException */ protected final String parseTimeFormatStringPercent(String format) throws ParseException { StringBuilder frmtString= new StringBuilder(); String[] ss= format.split("%"); frmtString.append(ss[0]); int offset= ss[0].length(); int lsb=0; for ( int i=1; i2 && lsb-oldLsb > 1 ) { logger.log(Level.WARNING, "gap in time digits detected: {0}", format); } frmtString.append(ss[i].substring(1)); offset+= ss[i].length(); } return frmtString.toString(); } private static void appendSubFormat(StringBuilder buffer, int fieldIndex,int count) { buffer.append("{").append(fieldIndex).append(",number,"); for (int i = 0; i < count; i++) { buffer.append('0'); } buffer.append("}"); } private int addScaleFactor(int scale) { if (scaleSeconds == null) { scaleSeconds = new int[1]; } else { int[] temp = new int[scaleSeconds.length + 1]; System.arraycopy(scaleSeconds, 0, temp, 0, scaleSeconds.length); scaleSeconds = temp; } scaleSeconds[scaleSeconds.length - 1] = scale; return TIMESTAMP_FIELD_COUNT + scaleSeconds.length - 1; } /** * returns Number[] for formatting, truncating the fractional seconds and putting the * remainder in lower sig digits. * * Note: TimeUtil.TimeStruct ts is modified. * @return [ year, month, dayOfMonth, dayOfYear, hour, minute, seconds, ... ] */ private Number[] timeStructToArray(TimeUtil.TimeStruct ts) { int secondsFieldCount = scaleSeconds == null ? 0 : scaleSeconds.length; int maxScale = scaleSeconds == null ? 10 : (int)Math.pow(10, max(scaleSeconds)); int fieldCount = TIMESTAMP_FIELD_COUNT + secondsFieldCount; Number[] array = new Number[fieldCount]; int seconds = (int)( (long)Math.round(ts.seconds * maxScale) / maxScale ); double fracSeconds = ts.seconds - seconds; ts.seconds= seconds; ts.micros= (int)(fracSeconds * 1e6); if ( ts.micros<0 ) { ts.seconds--; ts.micros+=1000000; } TimeUtil.carry(ts); if ( scaleSeconds!=null && scaleSeconds[0]<7 ) { ts= TimeUtil.roundNDigits(ts,scaleSeconds[0]); } array[YEAR_FIELD_INDEX] = ts.year; array[MONTH_FIELD_INDEX] = ts.month; array[DAY_FIELD_INDEX] = ts.day; array[DOY_FIELD_INDEX] = ts.doy; array[HOUR_FIELD_INDEX] = ts.hour; array[MINUTE_FIELD_INDEX] = ts.minute; array[SECONDS_FIELD_INDEX] = (int)ts.seconds; for (int i = TIMESTAMP_FIELD_COUNT; i < array.length; i++) { int value = (int)Math.round(fracSeconds * Math.pow(10, scaleSeconds[i - TIMESTAMP_FIELD_COUNT])); array[i] = value; } return array; } private static int max(int[] list) { int max = Integer.MIN_VALUE; for (int i = 0; i < list.length; i++) { max = Math.max(max, list[i]); } return max; } }