/* * Funambol is a mobile platform developed by Funambol, Inc. * Copyright (C) 2009 Funambol, Inc. * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by * the Free Software Foundation with the addition of the following permission * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED * WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * 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 Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA. * * You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite * 305, Redwood City, CA 94063, USA, or at email address info@funambol.com. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License * version 3, these Appropriate Legal Notices must retain the display of the * "Powered by Funambol" logo. If the display of the logo is not reasonably * feasible for technical reasons, the Appropriate Legal Notices must display * the words "Powered by Funambol". */ package com.funambol.common.pim.vcalendar; import java.util.Vector; import java.util.Calendar; import java.util.Date; import java.util.TimeZone; import com.funambol.util.Log; import com.funambol.util.StringUtil; /** * A set of Calendar utility methods */ public class CalendarUtils { private static final String TAG_LOG = "CalendarUtils"; public static final long SECOND_FACTOR = 1000; public static final long MINUTE_FACTOR = 60*SECOND_FACTOR; public static final long HOUR_FACTOR = 60*MINUTE_FACTOR; public static final long DAY_FACTOR = 24*HOUR_FACTOR; public final static String PATTERN_YYYYMMDD = "yyyyMMdd"; public final static int PATTERN_YYYYMMDD_LENGTH = 8; public final static String PATTERN_YYYY_MM_DD = "yyyy-MM-dd"; public final static int PATTERN_YYYY_MM_DD_LENGTH = 10; public final static String PATTERN_UTC = "yyyyMMdd'T'HHmmss'Z'"; public final static int PATTERN_UTC_LENGTH = 16; // UTC WOZ = UTC without 'Z' public final static String PATTERN_UTC_WOZ = "yyyyMMdd'T'HHmmss"; public final static int PATTERN_UTC_WOZ_LENGTH = 15; public static final long UNDEFINED_TIME = -1; private static TimeZone utc = TimeZone.getTimeZone("UTC"); /** * Get the local time from DATE/DATE-TIME value, starting from the specified * timezone id. If the tzid is not specified (is null) set the timezone to * UTC only if the time value is in UTC format (e.g. ends with 'Z'). If the * timezone id is specified, try to get the corresponding timezone, if not * successfull get the device default timezone. * @param value * @param tzid * @return */ public static long getLocalDateTime(String value, String tzid) { TimeZone tz = TimeZone.getTimeZone("UTC"); if(tzid == null) { tzid = "UTC"; if(value.length() == PATTERN_UTC_WOZ_LENGTH) { // This is a local time tz = TimeZone.getDefault(); tzid = ""; } } else { // If the datetime value contains the Z char it means that the time // is expressed in UTC. In this case we force to UTC timezone if(value.length() == PATTERN_UTC_LENGTH) { // This is a utc time tz = TimeZone.getTimeZone("UTC"); } else { tz = TimeZone.getTimeZone(tzid); if(!tz.getID().equals(tzid)) { tz = TimeZone.getDefault(); } } } long utcDate = parseDateTime(value, tz).getTime().getTime(); return utcDate; } /** * Shift the time from UTC to the local timezone * @param time * @return */ public static long adjustTimeToDefaultTimezone(long time) { return time+getDefaultTimeZoneOffset(); } /** * Shift the time from the local timezone do UTC * @param time * @return */ public static long adjustTimeFromDefaultTimezone(long time) { return time-getDefaultTimeZoneOffset(); } /** * Get time (a {@code long} value that holds the number of milliseconds * since midnight GMT, January 1, 1970) from date in "yyyy-MM-dd" String * format * @param field Date in "yyyy-MM-dd" String format * @return time at 00:00:00 from date */ public static Calendar parseDate(String field, TimeZone tz) { int day = 0; int month = 0; int year = 0; Calendar date = null; if (field.length() == PATTERN_YYYY_MM_DD_LENGTH) { year = Integer.parseInt(field.substring(0, 4)); month = Integer.parseInt(field.substring(5, 7)); day = Integer.parseInt(field.substring(8, 10)); } else { year = Integer.parseInt(field.substring(0, 4)); month = Integer.parseInt(field.substring(4, 6)); day = Integer.parseInt(field.substring(6, 8)); } date = Calendar.getInstance(tz); date.set(Calendar.DAY_OF_MONTH, day); date.set(Calendar.MONTH, month - 1); if (year < 1970) { Log.error(TAG_LOG, "[CalendarUtils.parseDateTime] Date cannot be represented in UTC: " + field); } date.set(Calendar.YEAR, year); date.set(Calendar.HOUR_OF_DAY, 0); date.set(Calendar.MINUTE, 0); date.set(Calendar.SECOND, 0); date.set(Calendar.MILLISECOND, 0); return date; } /** * Get time from date in "yyyyMMddTHHmmssZ" or "yyyyMMdd" or "yyyyMMddTHHmmss" * * @param data The data to parse * @param tz The timezone * * @return The Calendar object set to the specific date-time */ public static Calendar parseDateTime(String data, TimeZone tz) { int day = 0; int month = 0; int year = 0; int hour = 0; int minute = 0; int second = 0; Calendar date = null; if (data.length() <= PATTERN_YYYY_MM_DD_LENGTH) { return parseDate(data, tz); } year = Integer.parseInt(data.substring(0, 4)); if (year < 1970) { Log.error(TAG_LOG, "[CalendarUtils.parseDateTime] Date cannot be represented in UTC: " + data); } month = Integer.parseInt(data.substring(4, 6)); day = Integer.parseInt(data.substring(6, 8)); if (data.charAt(8)=='T') { hour = Integer.parseInt(data.substring(9, 11)); minute = Integer.parseInt(data.substring(11, 13)); second = Integer.parseInt(data.substring(13, 15)); } date = Calendar.getInstance(tz); date.set(Calendar.DAY_OF_MONTH, day); date.set(Calendar.MONTH, month - 1); date.set(Calendar.YEAR, year); date.set(Calendar.HOUR_OF_DAY, hour); date.set(Calendar.MINUTE, minute); date.set(Calendar.SECOND, second); date.set(Calendar.MILLISECOND, 0); return date; } /** * Get time from date in "yyyyMMddTHHmmssZ" or "yyyyMMdd" or "yyyyMMddTHHmmss" * format. * * @param data The data to parse * @param tz The timezone offset String in the ISO 8601 format. * @param daylights The Vector containing a list of daylights. * * @return The Calendar object set to the specific date-time */ public static Calendar parseDateTime(String data, String tz, Vector daylights) { Calendar calendar = null; if(data.length() == PATTERN_UTC_LENGTH) { // "yyyyMMddTHHmmssZ" calendar = parseDateTime(data, utc); } else if(data.length() == PATTERN_UTC_WOZ_LENGTH) { // "yyyyMMddTHHmmss" if(tz == null) { // Set default local timezone if not specified calendar = parseDateTime(data, TimeZone.getDefault()); return calendar; } else { calendar = parseDateTime(data, utc); } String daylightOffset = getDaylightSavingOffset(calendar, daylights); if(daylightOffset != null) { tz = daylightOffset; } Date date = calendar.getTime(); long newTime = date.getTime() - getTimezoneOffset(tz); date.setTime(newTime); calendar.setTime(date); } else if(data.length() <= PATTERN_YYYY_MM_DD_LENGTH) { // "yyyyMMdd" or "yyyy-MM-dd" calendar = parseDate(data, utc); } return calendar; } private static String getDaylightSavingOffset(Calendar datetime, Vector daylights) { for(int i=0; i<daylights.size(); i++) { String dlinfo = (String)daylights.elementAt(i); String[] dlinfos = StringUtil.split(dlinfo, ";"); if("FALSE".equals(dlinfos[0])) { continue; } Calendar daylightStart = parseDateTime(dlinfos[2], utc); Calendar daylightEnd = parseDateTime(dlinfos[3], utc); if(datetime.after(daylightStart) && datetime.before(daylightEnd)) { // We found a corresponfing daylight saving info return dlinfos[1]; } } return null; } protected static long getTimezoneOffset(String tzinfo) { if(tzinfo == null) { return 0; } String unsigedInfo = tzinfo; int sign = 1; if (tzinfo.startsWith("+")) { unsigedInfo = tzinfo.substring(1); } else if (tzinfo.startsWith("-")) { unsigedInfo = tzinfo.substring(1); sign = -1; } return getUnsignedTimezoneOffset(unsigedInfo)*sign; } protected static long getUnsignedTimezoneOffset(String tzinfo) { int minutes = 0, hours = 0; if(tzinfo.length() > 2) { hours = Integer.parseInt(tzinfo.substring(0, 2)); if(tzinfo.indexOf(":") != -1) { minutes = Integer.parseInt(tzinfo.substring(3)); } else { minutes = Integer.parseInt(tzinfo.substring(2)); } } else { hours = Integer.parseInt(tzinfo); } return hours*HOUR_FACTOR + minutes*MINUTE_FACTOR; } /** * Format a DateTime String from the specified time in milliseconds, and the * specific timezone offset and daylight. If it's an allday time format as * "yyyyMMdd", format as "yyyyMMddTHHmmss(Z)" otherwise. * @param milliseconds * @param allday * @param tz * @param daylight * @return */ public static String formatDateTime(long milliseconds, boolean allday, String tz, Vector daylight) { String result = ""; // Get a calendar instance in UTC time Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")); calendar.setTime(new Date(milliseconds)); String daylightOffset = null; if (daylight != null) { daylightOffset = getDaylightSavingOffset(calendar, daylight); } if(daylightOffset != null) { tz = daylightOffset; } if(tz != null) { milliseconds += getTimezoneOffset(tz); calendar.setTime(new Date(milliseconds)); } // format date "yyyyMMdd" int day = calendar.get(Calendar.DAY_OF_MONTH); int month = calendar.get(Calendar.MONTH) + 1; int year = calendar.get(Calendar.YEAR); result += getFullInt(year, 4); //yyyy result += getFullInt(month, 2); //MM result += getFullInt(day, 2); //dd // add time as "THHmmss(Z)" int hour = calendar.get(Calendar.HOUR_OF_DAY); int minute = calendar.get(Calendar.MINUTE); int second = calendar.get(Calendar.SECOND); result += "T"; result += getFullInt(hour, 2); //HH result += getFullInt(minute, 2); //mm result += getFullInt(second, 2); //ss if(tz == null) { result += "Z"; } return result; } /** * Format a DateTime String from the specified time in milliseconds, and the * specific timezone id. If it's an allday time format as "yyyyMMdd", format * as "yyyyMMddTHHmmss(Z)" otherwise. * @param milliseconds * @param allday * @param tzid * @return */ public static String formatDateTime(long milliseconds, boolean allday, String tzid) { String result = ""; if(tzid == null) { tzid = "GMT"; } // Get a calendar instance using the device default timezone Calendar date = Calendar.getInstance(TimeZone.getDefault()); date.setTime(new Date(milliseconds)); TimeZone tz = TimeZone.getTimeZone(tzid); if(!tz.getID().equals(tzid)) { // If the returned timezone is UTC (e.g. the tzid is unknown) update // the timezone id tzid = "GMT"; } date.setTimeZone(tz); // format as "yyyyMMdd" int day = date.get(Calendar.DAY_OF_MONTH); int month = date.get(Calendar.MONTH) + 1; int year = date.get(Calendar.YEAR); result += getFullInt(year, 4); //yyyy result += getFullInt(month, 2); //MM result += getFullInt(day, 2); //dd if(!allday) { // add time as "yyyyMMddTHHmmss(Z)" int hour = date.get(Calendar.HOUR_OF_DAY); int minute = date.get(Calendar.MINUTE); int second = date.get(Calendar.SECOND); result += "T"; result += getFullInt(hour, 2); //HH result += getFullInt(minute, 2); //mm result += getFullInt(second, 2); //ss if(tzid.equals("GMT") || tzid.equals("UTC")) { result += "Z"; } } return result; } /** * Fill a number String with '0' chars * @param value * @param digits * @return */ public static String getFullInt(int value, int digits) { value = Math.abs(value); String result = Integer.toString(value); if(value < 10 && digits > 1) result = "0" + result; if(value < 100 && digits > 2) result = "0" + result; if(value < 1000 && digits > 3) result = "0" + result; return result; } /** * Get the offset between GMT and the local timezone * @return the offset */ public static long getDefaultTimeZoneOffset() { long offset = 0; TimeZone zn = TimeZone.getDefault(); Calendar local = Calendar.getInstance(); local.setTime(new Date(System.currentTimeMillis())); // the offset to add to GMT to get local time, modified in case of // daylight savings int time = (int)(local.get(Calendar.HOUR_OF_DAY)*HOUR_FACTOR + local.get(Calendar.MINUTE)*MINUTE_FACTOR + local.get(Calendar.SECOND)*SECOND_FACTOR); offset = zn.getOffset(1, // era AD local.get(Calendar.YEAR), local.get(Calendar.MONTH), local.get(Calendar.DAY_OF_MONTH), local.get(Calendar.DAY_OF_WEEK), time); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "getDefaultTimeZoneOffset Offset: " + offset); } return offset; } /** * Get the offset between GMT and the specified timezone * @return the offset */ public static long getTimeZoneOffset(TimeZone zn) { long offset = 0; Calendar local = Calendar.getInstance(); local.setTime(new Date(System.currentTimeMillis())); // the offset to add to GMT to get local time, modified in case of // daylight savings int time = (int)(local.get(Calendar.HOUR_OF_DAY)*HOUR_FACTOR + local.get(Calendar.MINUTE)*MINUTE_FACTOR + local.get(Calendar.SECOND)*SECOND_FACTOR); offset = zn.getOffset(1, // era AD local.get(Calendar.YEAR), local.get(Calendar.MONTH), local.get(Calendar.DAY_OF_MONTH), local.get(Calendar.DAY_OF_WEEK), time); if (Log.isLoggable(Log.TRACE)) { Log.trace(TAG_LOG, "getTimeZoneOffset Offset: " + offset); } return offset; } }