/******************************************************************************* * Copyright (c) 2011 The Board of Trustees of the Leland Stanford Junior University * as Operator of the SLAC National Accelerator Laboratory. * Copyright (c) 2011 Brookhaven National Laboratory. * EPICS archiver appliance is distributed subject to a Software License Agreement found * in file LICENSE that is included with this distribution. *******************************************************************************/ package org.epics.archiverappliance.common; import java.sql.Timestamp; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; /** * There are various versions of timestamps in the archiver appliance. * The most commonly used one is java.sql.Timestamp and is the one exposed in the APIs. * The other versions are follows * <ol> * <li>A long epoch seconds + optional nanos - The seconds part is the java epoch seconds, as returned by System.currentMillis()/1000</li> * <li>A long epoch milliseconds - This is the java epoch milliseconds, as returned by System.currentMillis()</li> * <li>A JCA timestamp - This is what comes out of JCA</li> * <li>A year/secondsintoyear/nanos combination - This is what is used in the protocol buffer storage plugin.</li> * <li>A ISO 8601 date time - We use JODA to convert from/to a ISO 8601 string</li> * </ol> * * This class contains utilities to convert various forms of timestamps to/from the java.sql.Timestamp. * @author mshankar * */ public class TimeUtils { /** * EPICS epoch starts at January 1, 1990 UTC. * This constant contains the offset that must be added to epicstimestamps to generate java timestamps. */ public static final long EPICS_EPOCH_2_JAVA_EPOCH_OFFSET = computeEpicsEpochSecondsOffset(); public static java.sql.Timestamp convertFromEpochSeconds(long epochSeconds, int nanos) { Timestamp ts = new Timestamp(epochSeconds*1000); ts.setNanos(nanos); return ts; } public static java.sql.Timestamp convertFromEpochMillis(long epochMillis) { Timestamp ts = new Timestamp(epochMillis); return ts; } public static java.sql.Timestamp convertFromJCATimeStamp(gov.aps.jca.dbr.TimeStamp jcats) { Timestamp ts = new Timestamp((jcats.secPastEpoch()+EPICS_EPOCH_2_JAVA_EPOCH_OFFSET)*1000); ts.setNanos((int) jcats.nsec()); return ts; } public static java.sql.Timestamp convertFromYearSecondTimestamp(YearSecondTimestamp ysts) { Timestamp ts = new Timestamp((TimeUtils.getStartOfYearInSeconds(ysts.getYear()) + ysts.getSecondsintoyear())*1000); ts.setNanos(ysts.getNanos()); return ts; } public static java.sql.Timestamp convertFromISO8601String(String tsstr) { // Sample ISO8601 string 2011-02-01T08:00:00.000Z DateTimeFormatter fmt = ISODateTimeFormat.dateTime(); DateTime dt = fmt.parseDateTime(tsstr); Timestamp ts = new Timestamp(dt.getMillis()); return ts; } public static java.sql.Timestamp convertFromDateTimeStringWithOffset(String tsstr) { // Sample string 2012-11-03T00:00:00-07:00 DateTimeFormatter fmt = DateTimeFormat.forPattern("YYYY-MM-dd'T'HH:mm:ssZ").withOffsetParsed(); DateTime dt = fmt.parseDateTime(tsstr); Timestamp ts = new Timestamp(dt.getMillis()); return ts; } public static long convertToEpochSeconds(java.sql.Timestamp ts) { return ts.getTime()/1000; } public static long convertToEpochMillis(java.sql.Timestamp ts) { return ts.getTime(); } public static YearSecondTimestamp convertToYearSecondTimestamp(java.sql.Timestamp ts) { DateTime dateTime = new DateTime(ts.getTime(), DateTimeZone.UTC); DateTime startoftheYear = new DateTime(dateTime.getYear(), 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); long startOfYearInSeconds = startoftheYear.getMillis()/1000; long epochSeconds = dateTime.getMillis()/1000; assert((epochSeconds - startOfYearInSeconds) < Integer.MAX_VALUE); int secondsIntoYear = (int) (epochSeconds - startOfYearInSeconds); return new YearSecondTimestamp((short) (dateTime.getYear()), secondsIntoYear, ts.getNanos()); } public static YearSecondTimestamp convertToYearSecondTimestamp(gov.aps.jca.dbr.TimeStamp jcats) { DateTime dateTime = new DateTime((jcats.secPastEpoch()+EPICS_EPOCH_2_JAVA_EPOCH_OFFSET)*1000, DateTimeZone.UTC); DateTime startoftheYear = new DateTime(dateTime.getYear(), 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); long startOfYearInSeconds = startoftheYear.getMillis()/1000; long epochSeconds = dateTime.getMillis()/1000; assert((epochSeconds - startOfYearInSeconds) < Integer.MAX_VALUE); int secondsIntoYear = (int) (epochSeconds - startOfYearInSeconds); return new YearSecondTimestamp((short) (dateTime.getYear()), secondsIntoYear, (int) jcats.nsec()); } public static YearSecondTimestamp convertToYearSecondTimestamp(long epochSeconds) { DateTime dateTime = new DateTime(epochSeconds*1000, DateTimeZone.UTC); DateTime startoftheYear = new DateTime(dateTime.getYear(), 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); long startOfYearInSeconds = startoftheYear.getMillis()/1000; assert((epochSeconds - startOfYearInSeconds) < Integer.MAX_VALUE); int secondsIntoYear = (int) (epochSeconds - startOfYearInSeconds); return new YearSecondTimestamp((short) (dateTime.getYear()), secondsIntoYear, 0); } public static String convertToISO8601String(java.sql.Timestamp ts) { DateTimeFormatter fmt = ISODateTimeFormat.dateTime(); DateTime dateTime = new DateTime(ts.getTime(), DateTimeZone.UTC); String retval = fmt.print(dateTime); return retval; } public static String convertToISO8601String(long epochSeconds) { DateTimeFormatter fmt = ISODateTimeFormat.dateTime(); DateTime dateTime = new DateTime(epochSeconds*1000, DateTimeZone.UTC); String retval = fmt.print(dateTime); return retval; } public static String convertToHumanReadableString(java.sql.Timestamp ts) { if(ts == null || ts.getTime() == 0) return "Never"; DateTimeFormatter fmt = DateTimeFormat.forPattern("MMM/dd/yyyy HH:mm:ss z"); DateTime dateTime = new DateTime(ts.getTime(), DateTimeZone.getDefault()); String retval = fmt.print(dateTime); return retval; } public static String convertToHumanReadableString(long epochSeconds) { if(epochSeconds == 0) return "Never"; DateTimeFormatter fmt = DateTimeFormat.forPattern("MMM/dd/yyyy HH:mm:ss z"); DateTime dateTime = new DateTime(epochSeconds*1000, DateTimeZone.getDefault()); String retval = fmt.print(dateTime); return retval; } public static long convertToLocalEpochMillis(long epochMillis) { if(epochMillis == 0) return 0; return epochMillis + DateTimeZone.getDefault().getOffset(epochMillis); } public static long getStartOfCurrentYearInSeconds() { DateTime now = new DateTime(DateTimeZone.UTC); DateTime startoftheYear = new DateTime(now.getYear(), 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); return startoftheYear.getMillis()/1000; } public static long getStartOfYearInSeconds(int year) { DateTime startoftheYear = new DateTime(year, 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); return startoftheYear.getMillis()/1000; } public static Timestamp getStartOfYear(int year) { DateTime startoftheYear = new DateTime(year, 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); return new Timestamp(startoftheYear.getMillis()); } public static long getStartOfYearInSeconds(long epochseconds) { // The JODA DateTime constructor takes millis DateTime dateTime = new DateTime(epochseconds*1000, DateTimeZone.UTC); DateTime startoftheYear = new DateTime(dateTime.getYear(), 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); return startoftheYear.getMillis()/1000; } // We cache the start of the year for about 16000 years.. static long[] startOfYearInEpochSeconds = new long[16*1024]; private static short START_OF_CACHE_FOR_YEAR_STARTEPOCHSECONDS = 1970; static { for(short i=0; i < startOfYearInEpochSeconds.length; i++) { DateTime startoftheYear = new DateTime(START_OF_CACHE_FOR_YEAR_STARTEPOCHSECONDS + i, 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); startOfYearInEpochSeconds[i] = startoftheYear.getMillis()/1000; } } /** * In the protocol buffer storage plugin, we send the year as a short * @param year Year * @return startOfYearInEpochSeconds   */ public static long getStartOfYearInSeconds(short year) { return startOfYearInEpochSeconds[year - START_OF_CACHE_FOR_YEAR_STARTEPOCHSECONDS]; } public static Timestamp getEndOfYear(int year) { DateTime endoftheYear = new DateTime(year, 12, 31, 23, 59, 59, 999, DateTimeZone.UTC); return new Timestamp(endoftheYear.getMillis()); } public static long getStartOfCurrentDayInEpochSeconds() { DateTime now = new DateTime(DateTimeZone.UTC); DateTime startofCurrentDay = now.withHourOfDay(0).withMinuteOfHour(0).withSecondOfMinute(0); return startofCurrentDay.getMillis()/1000; } /** * Convert Java EPOCH seconds to a seconds into year given the start of the year in EPOCH seconds * @param epochseconds   * @param startOfYearInSeconds   * @return SecondsIntoYear The difference in Seconds */ public static int getSecondsIntoYear(long epochseconds, long startOfYearInSeconds) { long diffInSecs = epochseconds - startOfYearInSeconds; assert(diffInSecs <= Integer.MAX_VALUE); return (int)(diffInSecs); } /** * Convert Java EPOCH seconds to a seconds into year * @param epochseconds   * @return SecondsIntoYear The difference in Seconds */ public static int getSecondsIntoYear(long epochseconds) { long startOfYearInSeconds = getStartOfYearInSeconds(epochseconds); long diffInSecs = epochseconds - startOfYearInSeconds; assert(diffInSecs <= Integer.MAX_VALUE); return (int)(diffInSecs); } /** * Determine year from java epoch seconds. * @param epochseconds   * @return YearForEpochSeconds   */ public static short computeYearForEpochSeconds(long epochseconds) { // The JODA DateTime constructor takes millis DateTime dateTime = new DateTime(epochseconds*1000, DateTimeZone.UTC); return (short) dateTime.year().get(); } /** * Get the current year in the UTC timezone * @return CurrentYear   */ public static short getCurrentYear() { DateTime dateTime = new DateTime(DateTimeZone.UTC); return (short) dateTime.year().get(); } /** * Gets the current epoch seconds in the UTC timezone * @return currentEpochSeconds   */ public static long getCurrentEpochSeconds() { DateTime dateTime = new DateTime(DateTimeZone.UTC); return dateTime.getMillis()/1000; } /** * Gets the current epoch milli seconds in the UTC timezone * @return currentEpochMilliSeconds   */ public static long getCurrentEpochMilliSeconds() { DateTime dateTime = new DateTime(DateTimeZone.UTC); return dateTime.getMillis(); } /** * Gets "now" as a Timestamp in the UTC timezone * @return now A Timestamp in the UTC timezone */ public static Timestamp now() { DateTime dateTime = new DateTime(DateTimeZone.UTC); return new Timestamp(dateTime.getMillis()); } /** * Gets "now" as a YearSecondTimestamp in the UTC timezone * @return now A YearSecondTimestamp in the UTC timezone */ public static YearSecondTimestamp nowYTS() { return convertToYearSecondTimestamp(now()); } public static Timestamp plusHours(Timestamp ts, int hours) { DateTime dateTime = new DateTime(ts.getTime(), DateTimeZone.UTC); DateTime retval = dateTime.plusHours(hours); Timestamp retts = new Timestamp(retval.getMillis()); retts.setNanos(ts.getNanos()); return retts; } public static Timestamp minusHours(Timestamp ts, int hours) { DateTime dateTime = new DateTime(ts.getTime(), DateTimeZone.UTC); DateTime retval = dateTime.minusHours(hours); Timestamp retts = new Timestamp(retval.getMillis()); retts.setNanos(ts.getNanos()); return retts; } public static Timestamp plusDays(Timestamp ts, int days) { DateTime dateTime = new DateTime(ts.getTime(), DateTimeZone.UTC); DateTime retval = dateTime.plusDays(days); Timestamp retts = new Timestamp(retval.getMillis()); retts.setNanos(ts.getNanos()); return retts; } public static Timestamp minusDays(Timestamp ts, int days) { DateTime dateTime = new DateTime(ts.getTime(), DateTimeZone.UTC); DateTime retval = dateTime.minusDays(days); Timestamp retts = new Timestamp(retval.getMillis()); retts.setNanos(ts.getNanos()); return retts; } private static long computeEpicsEpochSecondsOffset() { DateTime Jan11990UTC = new DateTime(1990, 1, 1, 0, 0, 0, 0, DateTimeZone.UTC); long diff = Jan11990UTC.getMillis()/1000; // Per Bob, this should be 631152000 assert(diff == 631152000L); return diff; } /** * Given a start time and an end time, this method breaks this span into a sequence of spans each of which fits within a year. * Used where data is partitioned by year.... * @param start The start time * @param end The end time * @return breakIntoYearlyTimeSpans   */ public static List<TimeSpan> breakIntoYearlyTimeSpans(Timestamp start, Timestamp end) { assert(start.getTime() <= end.getTime()); YearSecondTimestamp startYTS = convertToYearSecondTimestamp(start); YearSecondTimestamp endYTS = convertToYearSecondTimestamp(end); LinkedList<TimeSpan> ret = new LinkedList<TimeSpan>(); if(startYTS.getYear() == endYTS.getYear()) { ret.add(new TimeSpan(start, end)); return ret; } else { ret.add(new TimeSpan(start, getEndOfYear(startYTS.getYear()))); for(int year = startYTS.getYear()+1; year < endYTS.getYear(); year++) { ret.add(new TimeSpan(getStartOfYear(year), getEndOfYear(year))); } ret.add(new TimeSpan(getStartOfYear(endYTS.getYear()), end)); return ret; } } private static String[] TWO_DIGIT_EXPANSIONS = {"00", "01", "02", "03", "04", "05", "06", "07", "08", "09"}; /** * Returns a partition name for the given epoch second based on the partition granularity. * * @param epochSeconds   * @param granularity Partition granularity of the file. * @return PartitionName   */ public static String getPartitionName(long epochSeconds, PartitionGranularity granularity) { DateTime dateTime = new DateTime(epochSeconds*1000, DateTimeZone.UTC); switch(granularity) { case PARTITION_YEAR: return "" + dateTime.getYear(); case PARTITION_MONTH: return "" + dateTime.getYear() + "_" + ( dateTime.getMonthOfYear() < 10 ? TWO_DIGIT_EXPANSIONS[dateTime.getMonthOfYear()] : dateTime.getMonthOfYear()); case PARTITION_DAY: return "" + dateTime.getYear() + "_" + ( dateTime.getMonthOfYear() < 10 ? TWO_DIGIT_EXPANSIONS[dateTime.getMonthOfYear()] : dateTime.getMonthOfYear()) + "_" + ( dateTime.getDayOfMonth() < 10 ? TWO_DIGIT_EXPANSIONS[dateTime.getDayOfMonth()] : dateTime.getDayOfMonth()); case PARTITION_HOUR: return "" + dateTime.getYear() + "_" + ( dateTime.getMonthOfYear() < 10 ? TWO_DIGIT_EXPANSIONS[dateTime.getMonthOfYear()] : dateTime.getMonthOfYear()) + "_" + ( dateTime.getDayOfMonth() < 10 ? TWO_DIGIT_EXPANSIONS[dateTime.getDayOfMonth()] : dateTime.getDayOfMonth()) + "_" + ( dateTime.getHourOfDay() < 10 ? TWO_DIGIT_EXPANSIONS[dateTime.getHourOfDay()] : dateTime.getHourOfDay()); case PARTITION_5MIN: case PARTITION_15MIN: case PARTITION_30MIN: int approxMinutesPerChunk = granularity.getApproxMinutesPerChunk(); int startOfPartition_Min = (dateTime.getMinuteOfHour()/approxMinutesPerChunk)*approxMinutesPerChunk; return "" + dateTime.getYear() + "_" + ( dateTime.getMonthOfYear() < 10 ? TWO_DIGIT_EXPANSIONS[dateTime.getMonthOfYear()] : dateTime.getMonthOfYear()) + "_" + ( dateTime.getDayOfMonth() < 10 ? TWO_DIGIT_EXPANSIONS[dateTime.getDayOfMonth()] : dateTime.getDayOfMonth()) + "_" + ( dateTime.getHourOfDay() < 10 ? TWO_DIGIT_EXPANSIONS[dateTime.getHourOfDay()] : dateTime.getHourOfDay()) + "_" + ( startOfPartition_Min < 10 ? TWO_DIGIT_EXPANSIONS[startOfPartition_Min] : startOfPartition_Min); default: throw new UnsupportedOperationException("Invalid Partition type " + granularity); } } /** * Given an epoch seconds and a granularity, this method gives you the first second in the next partition as epoch seconds. * @param epochSeconds   * @param granularity Partition granularity of the file. * @return NextPartitionFirstSecond   */ public static long getNextPartitionFirstSecond(long epochSeconds, PartitionGranularity granularity) { DateTime dateTime = new DateTime(epochSeconds*1000, DateTimeZone.UTC); DateTime nextPartitionFirstSecond = null; switch(granularity) { case PARTITION_YEAR: nextPartitionFirstSecond = dateTime.plusYears(1).withMonthOfYear(1).withDayOfMonth(1).withHourOfDay(0).withMinuteOfHour(0).withSecondOfMinute(0); return nextPartitionFirstSecond.getMillis()/1000; case PARTITION_MONTH: nextPartitionFirstSecond = dateTime.plusMonths(1).withDayOfMonth(1).withHourOfDay(0).withMinuteOfHour(0).withSecondOfMinute(0); return nextPartitionFirstSecond.getMillis()/1000; case PARTITION_DAY: nextPartitionFirstSecond = dateTime.plusDays(1).withHourOfDay(0).withMinuteOfHour(0).withSecondOfMinute(0); return nextPartitionFirstSecond.getMillis()/1000; case PARTITION_HOUR: nextPartitionFirstSecond = dateTime.plusHours(1).withMinuteOfHour(0).withSecondOfMinute(0); return nextPartitionFirstSecond.getMillis()/1000; case PARTITION_5MIN: case PARTITION_15MIN: case PARTITION_30MIN: int approxMinutesPerChunk = granularity.getApproxMinutesPerChunk(); DateTime nextPartForMin = dateTime.plusMinutes(approxMinutesPerChunk); int startOfPartitionForMin = (nextPartForMin.getMinuteOfHour()/approxMinutesPerChunk)*approxMinutesPerChunk; nextPartitionFirstSecond = nextPartForMin.withMinuteOfHour(startOfPartitionForMin).withSecondOfMinute(0); return nextPartitionFirstSecond.getMillis()/1000; default: throw new UnsupportedOperationException("Invalid Partition type " + granularity); } } /** * Given an epoch seconds and a granularity, this method gives you the last second in the previous partition as epoch seconds. * @param epochSeconds   * @param granularity Partition granularity of the file. * @return PreviousPartitionLastSecond   */ public static long getPreviousPartitionLastSecond(long epochSeconds, PartitionGranularity granularity) { DateTime dateTime = new DateTime(epochSeconds*1000, DateTimeZone.UTC); DateTime previousPartitionLastSecond = null; switch(granularity) { case PARTITION_YEAR: previousPartitionLastSecond = dateTime.minusYears(1).withMonthOfYear(12).withDayOfMonth(31).withHourOfDay(23).withMinuteOfHour(59).withSecondOfMinute(59); return previousPartitionLastSecond.getMillis()/1000; case PARTITION_MONTH: previousPartitionLastSecond = dateTime.withDayOfMonth(1).minusDays(1).withHourOfDay(23).withMinuteOfHour(59).withSecondOfMinute(59); return previousPartitionLastSecond.getMillis()/1000; case PARTITION_DAY: previousPartitionLastSecond = dateTime.minusDays(1).withHourOfDay(23).withMinuteOfHour(59).withSecondOfMinute(59); return previousPartitionLastSecond.getMillis()/1000; case PARTITION_HOUR: previousPartitionLastSecond = dateTime.minusHours(1).withMinuteOfHour(59).withSecondOfMinute(59); return previousPartitionLastSecond.getMillis()/1000; case PARTITION_5MIN: case PARTITION_15MIN: case PARTITION_30MIN: int approxMinutesPerChunk = granularity.getApproxMinutesPerChunk(); int startOfPartition_Min = (dateTime.getMinuteOfHour()/approxMinutesPerChunk)*approxMinutesPerChunk; previousPartitionLastSecond = dateTime.withMinuteOfHour(startOfPartition_Min).withSecondOfMinute(0).minusSeconds(1); return previousPartitionLastSecond.getMillis()/1000; default: throw new UnsupportedOperationException("Invalid Partition type " + granularity); } } /** * Event rate rate limiting uses a tenths of a seconds units to cater to monitor intervals of 0.1 seconds, 0.5 seconds etc.. * This converts a epochSeconds+nanos to time in terms of tenths of a second. * @param epochSeconds   * @param nanos   * @return TenthsOfASecond   * @throws NumberFormatException   */ public static long convertToTenthsOfASecond(long epochSeconds, int nanos) throws NumberFormatException { int tenthsPieceOfNanos = (nanos/(100000000)); if(tenthsPieceOfNanos > 9) { throw new NumberFormatException("Tenths of nanos cannot be greater than 9 but this is " + tenthsPieceOfNanos); } return epochSeconds*10 + tenthsPieceOfNanos; } /** * Whether we are in DST for a particular time in the servers default timezone. * Mostly used by Matlab. * @param ts Timestamp * @return boolean True or False */ public static boolean isDST(Timestamp ts) { return !DateTimeZone.getDefault().isStandardOffset(ts.getTime()); } /** * Break a time span into smaller time spans according to binSize * The first time span has the start time and the end of the first bin. * The next one has the end of the first bin and the start of the second bin. * The last time span has the end as its end. * This is sometimes used to try to speed up retrieval when using post processors over a large time span. * * @param start Timestamp start * @param end Timestamp end * @param binSizeInSeconds   * @return TimeSpan The list of smaller time spans according to binSize */ public static List<TimeSpan> breakIntoIntervals(Timestamp start, Timestamp end, long binSizeInSeconds) { assert(start.getTime() <= end.getTime()); List<TimeSpan> ret = new ArrayList<TimeSpan>(); long startEpochSeconds = convertToEpochSeconds(start); long endEpochSeconds = convertToEpochSeconds(end); long intervals = (endEpochSeconds - startEpochSeconds)/binSizeInSeconds; if(intervals <= 0) { ret.add(new TimeSpan(start, end)); return ret; } else { long currentBinEndEpochSeconds = ((startEpochSeconds/binSizeInSeconds) + 1)*binSizeInSeconds; if(startEpochSeconds != currentBinEndEpochSeconds) ret.add(new TimeSpan(start, TimeUtils.convertFromEpochSeconds(currentBinEndEpochSeconds - 1, 0))); currentBinEndEpochSeconds = currentBinEndEpochSeconds + binSizeInSeconds; while(currentBinEndEpochSeconds < endEpochSeconds) { ret.add(new TimeSpan(TimeUtils.convertFromEpochSeconds(currentBinEndEpochSeconds - binSizeInSeconds, 0), TimeUtils.convertFromEpochSeconds(currentBinEndEpochSeconds - 1, 0))); currentBinEndEpochSeconds = currentBinEndEpochSeconds + binSizeInSeconds; } if(endEpochSeconds != (currentBinEndEpochSeconds - binSizeInSeconds)) ret.add(new TimeSpan(TimeUtils.convertFromEpochSeconds(currentBinEndEpochSeconds - binSizeInSeconds, 0), end)); return ret; } } }