/*
* Copyright (C) 2006 Google Inc.
*
* Licensed 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.
* All Rights Reserved.
*/
package com.google.ical.util;
import com.google.ical.values.DateTimeValue;
import com.google.ical.values.DateTimeValueImpl;
import com.google.ical.values.DateValue;
import com.google.ical.values.DateValueImpl;
import com.google.ical.values.TimeValue;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility methods for working with times and dates.
*
* @author Neal Gafter
*/
public class TimeUtils {
private static TimeZone ZULU = new SimpleTimeZone(0, "Etc/GMT");
public static TimeZone utcTimezone() {
return ZULU;
}
/**
* Get a "time_t" in millis given a number of seconds since
* Dershowitz/Reingold epoch relative to a given timezone.
* @param epochSecs Number of seconds since Dershowitz/Reingold
* epoch, relatve to zone.
* @param zone Timezone against which epochSecs applies
* @return Number of milliseconds since 00:00:00 Jan 1, 1970 GMT
*/
private static long timetMillisFromEpochSecs(long epochSecs,
TimeZone zone) {
DateTimeValue date = timeFromSecsSinceEpoch(epochSecs);
Calendar cal = new GregorianCalendar(zone);
cal.clear(); // clear millis
cal.setTimeZone(zone);
cal.set(date.year(), date.month() - 1, date.day(),
date.hour(), date.minute(), date.second());
return cal.getTimeInMillis();
}
private static DateTimeValue convert(DateTimeValue time,
TimeZone zone,
int sense) {
if (zone == null ||
zone.hasSameRules(ZULU) ||
time.year() == 0) {
return time;
}
long timetMillis = 0;
if (sense > 0) {
// time is in UTC
timetMillis = timetMillisFromEpochSecs(secsSinceEpoch(time), ZULU);
} else {
// time is in local time; since zone.getOffset() expects millis
// in UTC, need to convert before we can get the offset (ironic)
timetMillis = timetMillisFromEpochSecs(secsSinceEpoch(time), zone);
}
int millisecondOffset = zone.getOffset(timetMillis);
int millisecondRound = millisecondOffset < 0 ? -500 : 500;
int secondOffset = (millisecondOffset + millisecondRound) / 1000;
return addSeconds(time, sense * secondOffset);
}
public static DateValue fromUtc(DateValue date, TimeZone zone) {
return (date instanceof DateTimeValue)
? fromUtc((DateTimeValue) date, zone)
: date;
}
public static DateTimeValue fromUtc(DateTimeValue date, TimeZone zone) {
return convert(date, zone, +1);
}
public static DateValue toUtc(DateValue date, TimeZone zone) {
return (date instanceof TimeValue)
? convert((DateTimeValue) date, zone, -1)
: date;
}
private static DateTimeValue addSeconds(DateTimeValue dtime, int seconds) {
return new DTBuilder(dtime.year(), dtime.month(),
dtime.day(), dtime.hour(),
dtime.minute(),
dtime.second() + seconds).toDateTime();
}
public static DateValue add(DateValue d, DateValue dur) {
DTBuilder db = new DTBuilder(d);
db.year += dur.year();
db.month += dur.month();
db.day += dur.day();
if (dur instanceof TimeValue) {
TimeValue tdur = (TimeValue) dur;
db.hour += tdur.hour();
db.minute += tdur.minute();
db.second += tdur.second();
return db.toDateTime();
} else if (d instanceof TimeValue) {
return db.toDateTime();
}
return db.toDate();
}
/**
* the number of days between two dates.
*
* @param dv1 non null.
* @param dv2 non null.
* @return a number of days.
*/
public static int daysBetween(DateValue dv1, DateValue dv2) {
return fixedFromGregorian(dv1) - fixedFromGregorian(dv2);
}
public static int daysBetween(
int y1, int m1, int d1,
int y2, int m2, int d2) {
return fixedFromGregorian(y1, m1, d1) - fixedFromGregorian(y2, m2, d2);
}
private static int fixedFromGregorian(DateValue date) {
return fixedFromGregorian(date.year(), date.month(), date.day());
}
/**
* the number of days since the <em>epoch</em>,
* which is the imaginary beginning of year zero in a hypothetical
* backward extension of the Gregorian calendar through time.
* See "Calendrical Calculations" by Reingold and Dershowitz.
*/
public static int fixedFromGregorian(int year, int month, int day) {
int yearM1 = year - 1;
return 365 * yearM1 +
yearM1/4 -
yearM1/100 +
yearM1/400 +
(367*month - 362)/12 +
(month <= 2 ? 0 :
isLeapYear(year) ? -1 :
-2) +
day;
}
public static boolean isLeapYear(int year) {
return (year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0));
}
/** count of days inthe given year */
public static int yearLength(int year) {
return isLeapYear(year) ? 366 : 365;
}
/** count of days in the given month (one indexed) of the given year. */
public static int monthLength(int year, int month) {
switch (month) {
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31;
case 4:
case 6:
case 9:
case 11:
return 30;
case 2:
return isLeapYear(year) ? 29 : 28;
default:
throw new AssertionError(month);
}
}
private static int[] MONTH_START_TO_DOY = new int[12];
static {
assert !isLeapYear(1970);
for (int m = 1; m < 12; ++m) {
MONTH_START_TO_DOY[m] = MONTH_START_TO_DOY[m - 1] + monthLength(1970, m);
}
assert 365 == MONTH_START_TO_DOY[11] + monthLength(1970, 12) :
"" + (MONTH_START_TO_DOY[11] + monthLength(1970, 12));
}
/** the day of the year in [0-365] of the given date. */
public static int dayOfYear(int year, int month, int date) {
int leapAdjust = month > 2 && isLeapYear(year) ? 1 : 0;
return MONTH_START_TO_DOY[month - 1] + leapAdjust + date - 1;
}
/**
* Compute the gregorian time from the number of seconds since the
* Proleptic Gregorian Epoch.
* See "Calendrical Calculations", Reingold and Dershowitz.
*/
public static DateTimeValue timeFromSecsSinceEpoch(long secsSinceEpoch) {
// TODO: should we handle -ve years?
int secsInDay = (int) (secsSinceEpoch % SECS_PER_DAY);
int daysSinceEpoch = (int) (secsSinceEpoch / SECS_PER_DAY);
int approx = (int) ((daysSinceEpoch + 10) * 400L / 146097);
int year = (daysSinceEpoch >= fixedFromGregorian(approx+1, 1, 1))
? approx+1 : approx;
int jan1 = fixedFromGregorian(year, 1, 1);
int priorDays = daysSinceEpoch - jan1;
int march1 = fixedFromGregorian(year, 3, 1);
int correction = (daysSinceEpoch < march1) ? 0 :
isLeapYear(year) ? 1 : 2;
int month = (12 * (priorDays + correction) + 373) / 367;
int month1 = fixedFromGregorian(year, month, 1);
int day = daysSinceEpoch - month1 + 1;
int second = secsInDay % 60;
int minutesInDay = secsInDay / 60;
int minute = minutesInDay % 60;
int hour = minutesInDay / 60;
if (!(hour >= 0 && hour < 24)) throw new AssertionError(
"Input was: " + secsSinceEpoch + "to make hour: " + hour);
DateTimeValue result =
new DateTimeValueImpl(year, month, day, hour, minute, second);
// assert result.equals(normalize(result));
// assert secsSinceEpoch(result) == secsSinceEpoch;
return result;
}
private static final long SECS_PER_DAY = 60L * 60 * 24;
/**
* Compute the number of seconds from the Proleptic Gregorian epoch
* to the given time.
*/
public static long secsSinceEpoch(DateValue date) {
long result = fixedFromGregorian(date) *
SECS_PER_DAY;
if (date instanceof TimeValue) {
TimeValue time = (TimeValue) date;
result +=
time.second() +
60 * (time.minute() +
60 * time.hour());
}
return result;
}
public static DateTimeValue dayStart(DateValue dv) {
return new DateTimeValueImpl(dv.year(), dv.month(), dv.day(), 0, 0, 0);
}
/**
* a DateValue with the same year, month, and day as the given instance that
* is not a TimeValue.
*/
public static DateValue toDateValue(DateValue dv) {
return (!(dv instanceof TimeValue) ? dv
: new DateValueImpl(dv.year(), dv.month(), dv.day()));
}
private static final TimeZone BOGUS_TIMEZONE =
TimeZone.getTimeZone("noSuchTimeZone");
private static final Pattern UTC_TZID =
Pattern.compile("^GMT([+-]0(:00)?)?$|UTC|Zulu|Etc\\/GMT|Greenwich.*",
Pattern.CASE_INSENSITIVE);
/**
* returns the timezone with the given name or null if no such timezone.
* calendar/common/ICalUtil uses this function
*/
public static TimeZone timeZoneForName(String tzString) {
// This is a horrible hack since there is no easier way to get a timezone
// only if the string is recognized as a timezone.
// The TimeZone.getTimeZone javadoc says the following:
// Returns:
// the specified TimeZone, or the GMT zone if the given ID cannot be
// understood.
TimeZone tz = TimeZone.getTimeZone(tzString);
if (tz.hasSameRules(BOGUS_TIMEZONE)) {
// see if the user really was asking for GMT because if
// TimeZone.getTimeZone can't recognize tzString, then that is what it
// will return.
Matcher m = UTC_TZID.matcher(tzString);
if (m.matches()) {
return TimeUtils.utcTimezone();
}
// unrecognizable timezone
return null;
}
return tz;
}
private TimeUtils() {
// uninstantiable
}
}