/* * This file is part of the Wayback archival access software * (http://archive-access.sourceforge.net/projects/wayback/). * * Licensed to the Internet Archive (IA) by one or more individual * contributors. * * The IA licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.archive.wayback.util; import java.text.ParseException; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import java.util.SimpleTimeZone; import java.util.TimeZone; import org.archive.util.ArchiveUtils; /** * Represents a moment in time as a 14-digit string, and interally as a Date. * * @author Brad Tofel * @version $Date$, $Revision$ */ public class Timestamp { private final static String LOWER_TIMESTAMP_LIMIT = "10000000000000"; private final static String UPPER_TIMESTAMP_LIMIT = "29991939295959"; private final static String YEAR_LOWER_LIMIT = "1996"; private final static String YEAR_UPPER_LIMIT = String.valueOf(Calendar.getInstance(TimeZone.getTimeZone("GMT")).get(Calendar.YEAR)); private final static String MONTH_LOWER_LIMIT = "01"; private final static String MONTH_UPPER_LIMIT = "12"; private final static String DAY_LOWER_LIMIT = "01"; private final static String HOUR_UPPER_LIMIT = "23"; private final static String HOUR_LOWER_LIMIT = "00"; private final static String MINUTE_UPPER_LIMIT = "59"; private final static String MINUTE_LOWER_LIMIT = "00"; private final static String SECOND_UPPER_LIMIT = "59"; private final static String SECOND_LOWER_LIMIT = "00"; private final static int SSE_1996 = 820454400; private final static String[] months = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; private final static String DAYS_IN_MONTH[][]; private final static int DIM_START_YEAR = 1972; private final static int DIM_END_YEAR = 2032; static { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); cal.clear(); int years = DIM_END_YEAR - DIM_START_YEAR; DAYS_IN_MONTH = new String[years][12]; for(int y = 0; y < years; y++) { for(int m = 0; m < 12; m++) { cal.set(Calendar.YEAR,DIM_START_YEAR + y); cal.set(Calendar.MONTH,m); cal.set(Calendar.DAY_OF_MONTH,1); int calV = cal.getActualMaximum(Calendar.DAY_OF_MONTH); String maxDayOfMonth = String.valueOf(calV); if(maxDayOfMonth.length() == 1) { maxDayOfMonth = "0" + maxDayOfMonth; } DAYS_IN_MONTH[y][m] = maxDayOfMonth; } } } private static String getDaysInMonthBound(int year, int month) { return DAYS_IN_MONTH[year - DIM_START_YEAR][month]; } private String dateStr = null; private Date date = null; /** * Constructor */ public Timestamp() { } /** * Construct and initialize structure from a 14-digit String timestamp. If * the argument is too short, or specifies an invalid timestamp, cleanup * will be attempted to create the earliest legal timestamp given the input. * @param dateStr from which to set date */ public Timestamp(final String dateStr) { setDate(dateStrToDate(dateStr)); } /** * Construct and initialize structure from an integer number of seconds * since the epoch. * @param sse SecondsSinceEpoch */ public Timestamp(final int sse) { setSse(sse); } /** * Construct and initialize structure from an Date * @param date from which date should be set */ public Timestamp(final Date date) { setDate(date); } /** * set internal structure using Date argument * @param date from which date should be set */ public void setDate(final Date date) { this.date = (Date) date.clone(); dateStr = ArchiveUtils.get14DigitDate(date); } /** * @return Date for this Timestamp */ public Date getDate() { return date; } /** * set internal structure using seconds since the epoch integer argument * @param sse SecondsSinceEpoch */ public void setSse(final int sse) { setDate(new Date(((long)sse) * 1000)); } /** * initialize interal data structures for this Timestamp from the 14-digit * argument. Will clean up timestamp as needed to yield the ealiest * possible timestamp given the possible partial or wrong argument. * * @param dateStr containing the timestamp */ public void setDateStr(String dateStr) { setDate(dateStrToDate(dateStr)); } /** * @return the 14-digit String representation of this Timestamp. */ public String getDateStr() { return dateStr; } /** * @return the integer number of seconds since epoch represented by this * Timestamp. */ public int sse() { return Math.round(date.getTime() / 1000); } /** * function that calculates integer seconds between this records * timeStamp and the arguments timeStamp. result is the absolute number of * seconds difference. * * @param otherTimeStamp to compare * @return int absolute seconds between the argument and this records * timestamp. */ public int absDistanceFromTimestamp(final Timestamp otherTimeStamp) { return Math.abs(distanceFromTimestamp(otherTimeStamp)); } /** * function that calculates integer seconds between this records * timeStamp and the arguments timeStamp. result is negative if this records * timeStamp is less than the argument, positive if it is greater, and 0 if * the same. * * @param otherTimeStamp to compare * @return int milliseconds */ public int distanceFromTimestamp(final Timestamp otherTimeStamp) { return otherTimeStamp.sse() - sse(); } /** * @return the year portion(first 4 digits) of this Timestamp */ public String getYear() { return this.dateStr.substring(0, 4); } /** * @return the month portion(digits 5-6) of this Timestamp */ public String getMonth() { return this.dateStr.substring(4, 6); } /** * @return the day portion(digits 7-8) of this Timestamp */ public String getDay() { return this.dateStr.substring(6, 8); } /** * @return user friendly String representation of the date of this * Timestamp. eg: "Jan 13, 1999" */ public String prettyDate() { String year = dateStr.substring(0, 4); String month = dateStr.substring(4, 6); String day = dateStr.substring(6, 8); int monthInt = Integer.parseInt(month) - 1; String prettyMonth = "UNK"; if ((monthInt >= 0) && (monthInt < months.length)) { prettyMonth = months[monthInt]; } return prettyMonth + " " + day + ", " + year; } /** * @return user friendly String representation of the Time of this * Timestamp. */ public String prettyTime() { return dateStr.substring(8, 10) + ":" + dateStr.substring(10, 12) + ":" + dateStr.substring(12, 14); } /** * @return user friendly String representation of the Date and Time of this * Timestamp. */ public String prettyDateTime() { return prettyDate() + " " + prettyTime(); } /* * * ALL STATIC METHOD BELOW HERE: * ============================= * */ private static Calendar getCalendar() { String[] ids = TimeZone.getAvailableIDs(0); if (ids.length < 1) { return null; } TimeZone gmt = new SimpleTimeZone(0, ids[0]); return new GregorianCalendar(gmt); } /** * @param dateStr up to 14 digit String representing date * @return a GMT Calendar object, set to the date represented */ public static Calendar dateStrToCalendar(final String dateStr) { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); String paddedDateStr = padStartDateStr(dateStr); int iYear = Integer.parseInt(paddedDateStr.substring(0,4)); int iMonth = Integer.parseInt(paddedDateStr.substring(4,6)); int iDay = Integer.parseInt(paddedDateStr.substring(6,8)); int iHour = Integer.parseInt(paddedDateStr.substring(8,10)); int iMinute = Integer.parseInt(paddedDateStr.substring(10,12)); int iSecond = Integer.parseInt(paddedDateStr.substring(12,14)); cal.set(Calendar.YEAR,iYear); cal.set(Calendar.MONTH,iMonth - 1); cal.set(Calendar.DAY_OF_MONTH,iDay); cal.set(Calendar.HOUR_OF_DAY,iHour); cal.set(Calendar.MINUTE,iMinute); cal.set(Calendar.SECOND,iSecond); cal.set(Calendar.MILLISECOND,0); return cal; } /** * cleanup the dateStr argument assuming earliest values, and return a * GMT calendar set to the time described by the dateStr. * * @param dateStr from which to create Calendar * @return Calendar */ public static Date dateStrToDate(final String dateStr) { String paddedDateStr = padStartDateStr(dateStr); try { return ArchiveUtils.parse14DigitDate(paddedDateStr); } catch (ParseException e) { e.printStackTrace(); // TODO: This is certainly not the right thing, but padStartDateStr // should ensure we *never* get here.. return new Date(SSE_1996); } } private static String padDigits(String input, String min, String max, String missing) { if(input == null) { input = ""; } String finalDigits = ""; for(int i = 0; i < missing.length(); i++) { if(input.length() <= i) { finalDigits = finalDigits + missing.charAt(i); } else { char inc = input.charAt(i); char maxc = max.charAt(i); char minc = min.charAt(i); if(inc > maxc) { inc = maxc; } else if (inc < minc) { inc = minc; } finalDigits = finalDigits + inc; } } return finalDigits; } private static String boundDigits(final String test, final String min, final String max) { if(test.compareTo(min) < 0) { return min; } else if(test.compareTo(max) > 0) { return max; } return test; } // check each of YEAR, MONTH, DAY, HOUR, MINUTE, SECOND to make sure they // are not too large or too small, factoring in the month, leap years, etc. // BUGBUG: Leap second bug here.. How long till someone notices? private static String boundTimestamp(String input) { String boundTimestamp = ""; if(input == null) { input = ""; } // MAKE SURE THE YEAR IS WITHIN LEGAL BOUNDARIES: boundTimestamp = boundDigits(input.substring(0,4), YEAR_LOWER_LIMIT,YEAR_UPPER_LIMIT); // MAKE SURE THE MONTH IS WITHIN LEGAL BOUNDARIES: boundTimestamp += boundDigits(input.substring(4,6), MONTH_LOWER_LIMIT,MONTH_UPPER_LIMIT); // NOW DEPENDING ON THE YEAR + MONTH, MAKE SURE THE DAY OF MONTH IS // WITHIN LEGAL BOUNDARIES: int iYear = Integer.parseInt(boundTimestamp.substring(0,4)); int iMonth = Integer.parseInt(boundTimestamp.substring(4,6)); String maxDayOfMonth = getDaysInMonthBound(iYear, iMonth-1); boundTimestamp += boundDigits(input.substring(6,8), DAY_LOWER_LIMIT,maxDayOfMonth); // MAKE SURE THE HOUR IS WITHIN LEGAL BOUNDARIES: boundTimestamp += boundDigits(input.substring(8,10), HOUR_LOWER_LIMIT,HOUR_UPPER_LIMIT); // MAKE SURE THE MINUTE IS WITHIN LEGAL BOUNDARIES: boundTimestamp += boundDigits(input.substring(10,12), MINUTE_LOWER_LIMIT,MINUTE_UPPER_LIMIT); // MAKE SURE THE SECOND IS WITHIN LEGAL BOUNDARIES: boundTimestamp += boundDigits(input.substring(12,14), SECOND_LOWER_LIMIT,SECOND_UPPER_LIMIT); return boundTimestamp; } /** * clean up timestamp argument assuming latest possible values for missing * or bogus digits. * @param timestamp String * @return String */ public static String padEndDateStr(String timestamp) { return boundTimestamp(padDigits(timestamp,LOWER_TIMESTAMP_LIMIT, UPPER_TIMESTAMP_LIMIT,UPPER_TIMESTAMP_LIMIT)); } /** * clean up timestamp argument assuming earliest possible values for missing * or bogus digits. * @param timestamp String * @return String */ public static String padStartDateStr(String timestamp) { return boundTimestamp(padDigits(timestamp,LOWER_TIMESTAMP_LIMIT, UPPER_TIMESTAMP_LIMIT,LOWER_TIMESTAMP_LIMIT)); } /** * @param dateStr containing timestamp * @return Timestamp object representing the earliest date represented by * the (possibly) partial digit-string argument. */ public static Timestamp parseBefore(final String dateStr) { return new Timestamp(padStartDateStr(dateStr)); } /** * @param dateStr containing timestamp * @return Timestamp object representing the latest date represented by the * (possibly) partial digit-string argument. */ public static Timestamp parseAfter(final String dateStr) { return new Timestamp(padEndDateStr(dateStr)); } /** * @param sse SecondsSinceEpoch * @return Timestamp object representing the seconds since epoch argument. */ public static Timestamp fromSse(final int sse) { //String dateStr = ArchiveUtils.get14DigitDate(sse * 1000); return new Timestamp(sse); } /** * @return Timestamp object representing the current date. */ public static Timestamp currentTimestamp() { return new Timestamp(new Date()); } /** * @return Timestamp object representing the latest possible date. */ public static Timestamp latestTimestamp() { return currentTimestamp(); } /** * @return Timestamp object representing the earliest possible date. */ public static Timestamp earliestTimestamp() { return new Timestamp(SSE_1996); } }