/* * 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.Locale; import java.util.Map; 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_DEFAULT_LOWER_LIMIT = "1996"; private static String YEAR_LOWER_LIMIT; 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"; // This variable holds the seconds since Epoch (January 1, 1970 00:00:00 GMT) // to the start of the selected YEAR_LOWER_LIMIT. private final static int SSE_YEAR_LOWER_LIMIT; private final static String[] months = new String[12]; static { // Set up the starting year. YEAR_LOWER_LIMIT = System.getProperty("wayback.timestamp.startyear", YEAR_DEFAULT_LOWER_LIMIT); Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); cal.set(Integer.parseInt(YEAR_LOWER_LIMIT), 0, 1, 0, 0, 0); SSE_YEAR_LOWER_LIMIT = (int)(cal.getTimeInMillis() / 1000); // Set up the array of shorthanded month names. Map <String, Integer> month = cal.getDisplayNames(Calendar.MONTH, Calendar.SHORT, Locale.getDefault()); for (String s:month.keySet()) { months[month.get(s)] = s; } } 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 final 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 final 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 String getDaysInMonthBound(int year, int month) { Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT")); cal.set(Calendar.YEAR, year); cal.set(Calendar.MONTH, month); cal.set(Calendar.DAY_OF_MONTH, 1); return new Integer(cal.getActualMaximum(Calendar.DAY_OF_MONTH)).toString(); } /** * @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((long)SSE_YEAR_LOWER_LIMIT * 1000); } } private static String padDigits(String input, String min, String max, String missing) { if(input == null) { input = ""; } StringBuilder finalDigits = new StringBuilder(); //String finalDigits = ""; for(int i = 0; i < missing.length(); i++) { if(input.length() <= i) { finalDigits.append(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.append(inc); } } return finalDigits.toString(); } 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) { StringBuilder boundTimestamp = new StringBuilder(); if (input == null) { input = ""; } // MAKE SURE THE YEAR IS WITHIN LEGAL BOUNDARIES: boundTimestamp.append(boundDigits(input.substring(0,4), YEAR_LOWER_LIMIT, String.valueOf(Calendar.getInstance(TimeZone.getTimeZone("GMT")).get(Calendar.YEAR) ))); // MAKE SURE THE MONTH IS WITHIN LEGAL BOUNDARIES: boundTimestamp.append(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.append(boundDigits(input.substring(6,8), DAY_LOWER_LIMIT,maxDayOfMonth)); // MAKE SURE THE HOUR IS WITHIN LEGAL BOUNDARIES: boundTimestamp.append(boundDigits(input.substring(8,10), HOUR_LOWER_LIMIT,HOUR_UPPER_LIMIT)); // MAKE SURE THE MINUTE IS WITHIN LEGAL BOUNDARIES: boundTimestamp.append(boundDigits(input.substring(10,12), MINUTE_LOWER_LIMIT,MINUTE_UPPER_LIMIT)); // MAKE SURE THE SECOND IS WITHIN LEGAL BOUNDARIES: boundTimestamp.append(boundDigits(input.substring(12,14), SECOND_LOWER_LIMIT,SECOND_UPPER_LIMIT)); return boundTimestamp.toString(); } /** * 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_YEAR_LOWER_LIMIT); } /** * Set the mimimum year for the service. * @param year The four digit year to set the lower limit of years handled by the server. */ public void setStartYear(int year) { YEAR_LOWER_LIMIT = Integer.toString(year); } /** * @return The four digit start year of the interval. */ public static int getStartYear() { return Integer.parseInt(YEAR_LOWER_LIMIT); } /** * @return The four digit end year of the interval. */ public static int getEndYear() { return Calendar.getInstance(TimeZone.getTimeZone("GMT")).get(Calendar.YEAR); } }