/* * The MIT License (MIT) * * Copyright (c) 2015 Lachlan Dowding * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package permafrost.tundra.time; import com.wm.data.IData; import permafrost.tundra.data.IDataMap; import java.text.DecimalFormat; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.SortedSet; import java.util.TimeZone; import java.util.TreeSet; import java.util.regex.Pattern; /** * A collection of convenience methods for working with time zones. */ public final class TimeZoneHelper { /** * A sorted set of all time zone IDs known to the JVM. */ protected static final SortedSet<String> ZONES = new TreeSet<String>(Arrays.asList(TimeZone.getAvailableIDs())); /** * Regular expression pattern for matching a time zone offset specified as HH:mm (hours and minutes). */ protected static final Pattern OFFSET_HHMM_PATTERN = Pattern.compile("([\\+-])?(\\d?\\d):(\\d\\d)"); /** * Regular expression pattern for matching a time zone offset specified as an XML duration string. */ protected static final Pattern OFFSET_XML_PATTERN = Pattern.compile("-?P(\\d+|T\\d+).+"); /** * Regular expression pattern for matching a time zone offset specified in milliseconds. */ protected static final Pattern OFFSET_RAW_PATTERN = Pattern.compile("[\\+-]?\\d+"); /** * The UTC time zone. */ public static final TimeZone UTC_TIME_ZONE = TimeZone.getTimeZone("UTC"); /** * The default time zone used by Tundra. */ public static final TimeZone DEFAULT_TIME_ZONE = UTC_TIME_ZONE; /** * Disallow instantiation of this class. */ private TimeZoneHelper() {} /** * Returns the time zone associated with the given ID. * * @param id A time zone ID. * @return The time zone associated with the given ID. */ public static TimeZone get(String id) { if (id == null) return null; TimeZone timezone = null; if (id.equals("$default") || id.equalsIgnoreCase("local") || id.equalsIgnoreCase("self")) { timezone = self(); } else { if (id.equals("Z")) { timezone = TimeZone.getTimeZone("UTC"); } else { java.util.regex.Matcher matcher = OFFSET_HHMM_PATTERN.matcher(id); if (matcher.matches()) { String sign = matcher.group(1); String hours = matcher.group(2); String minutes = matcher.group(3); int offset = Integer.parseInt(hours) * 60 * 60 * 1000 + Integer.parseInt(minutes) * 60 * 1000; if (sign != null && sign.equals("-")) offset = offset * -1; timezone = get(offset); } else { matcher = OFFSET_XML_PATTERN.matcher(id); if (matcher.matches()) { try { timezone = get(Integer.parseInt(DurationHelper.format(id, "xml", "milliseconds"))); } catch (NumberFormatException ex) { // ignore } } else { matcher = OFFSET_RAW_PATTERN.matcher(id); if (matcher.matches()) { // try parsing the id as a raw millisecond offset try { timezone = get(Integer.parseInt(id)); } catch (NumberFormatException ex) { // ignore } } else if (ZONES.contains(id)) { timezone = TimeZone.getTimeZone(id); } } } } } if (timezone == null) throw new IllegalArgumentException("Unknown time zone specified: '" + id + "'"); return timezone; } /** * Returns the first matching time zone ID for the given raw millisecond time zone offset. * * @param offset A time zone offset in milliseconds. * @return The ID of the first matching time zone with the given offset. */ protected static TimeZone get(int offset) { DecimalFormat decimalFormat = new DecimalFormat("00"); String sign = offset < 0 ? "-" : "+"; int hours = Math.abs(offset / (1000 * 60 * 60)); int minutes = Math.abs(offset / (1000 * 60)) - (hours * 60); String timezoneID = "GMT" + sign + decimalFormat.format(hours) + ":" + decimalFormat.format(minutes); return TimeZone.getTimeZone(timezoneID); } /** * @return The JVM's default time zone. */ public static TimeZone self() { return TimeZone.getDefault(); } /** * @return All time zones known to the JVM. */ public static TimeZone[] list() { String[] id = TimeZone.getAvailableIDs(); TimeZone[] zones = new TimeZone[id.length]; for (int i = 0; i < id.length; i++) { zones[i] = get(id[i]); } return zones; } /** * Returns the given Calendar object converted to the default time zone. * * @param calendar The Calendar object to be normalized. * @return The given Calendar object converted to the default time zone. */ public static Calendar normalize(Calendar calendar) { return convert(calendar, DEFAULT_TIME_ZONE); } /** * Converts the given calendar to the given time zone. * * @param input The calendar to be coverted to another time zone. * @param timezone The time zone ID identifying the time zone the calendar will be converted to. * @return A new calendar representing the same instant in time as the given calendar but in the given time. */ public static Calendar convert(Calendar input, String timezone) { return convert(input, get(timezone)); } /** * Converts the given calendar to the given time zone. * * @param input The calendar to be converted to another time zone. * @param timezone The time zone the calendar will be converted to. * @return A new calendar representing the same instant in time as the given calendar but in the given time. */ public static Calendar convert(Calendar input, TimeZone timezone) { if (input == null || timezone == null || timezone.equals(input.getTimeZone())) return input; Calendar output = Calendar.getInstance(timezone); output.setTimeInMillis(input.getTimeInMillis()); return output; } /** * Replaces the time zone on the given calendar with the given time zone. * * @param input The calendar to replace the time zone on. * @param timezone A time zone ID identifying the time zone the calendar will be forced into. * @return A new calendar that has been forced into a new time zone. */ public static Calendar replace(Calendar input, String timezone) { return replace(input, get(timezone)); } /** * Replaces the time zone on the given calendar with the given time zone. * * @param input The calendar to replace the time zone on. * @param timezone The new time zone the calendar will be forced into. * @return A new calendar that has been forced into a new time zone. */ public static Calendar replace(Calendar input, TimeZone timezone) { if (input == null || timezone == null || timezone.equals(input.getTimeZone())) return input; long instant = input.getTimeInMillis(); TimeZone currentZone = input.getTimeZone(); int currentOffset = currentZone.getOffset(instant); int desiredOffset = timezone.getOffset(instant); // reset instant to UTC time then force it to input timezone instant = instant + currentOffset - desiredOffset; // convert to output zone Calendar output = Calendar.getInstance(timezone); output.setTimeInMillis(instant); return output; } /** * Returns an IData representation of the given TimeZone object, using the given datetime to resolve whether * daylight savings is active. * * @param timezone The TimeZone object to be converted to an IData representation. * @param instant The datetime used to resolve the status of daylight savings. * @return An IData representation of the given TimeZone object. */ public static IData toIData(TimeZone timezone, Calendar instant) { if (timezone == null) return null; if (instant == null) instant = Calendar.getInstance(); Date dateInstant = instant.getTime(); IDataMap output = new IDataMap(); boolean dstActive = timezone.inDaylightTime(dateInstant); output.put("id", timezone.getID()); output.put("name", timezone.getDisplayName(dstActive, TimeZone.SHORT)); output.put("description", timezone.getDisplayName(dstActive, TimeZone.LONG)); output.put("utc.offset", DurationHelper.format(timezone.getOffset(dateInstant.getTime()), DurationPattern.XML)); output.put("dst.used?", "" + timezone.useDaylightTime()); output.put("dst.active?", "" + dstActive); output.put("dst.offset", DurationHelper.format(timezone.getDSTSavings(), DurationPattern.XML)); return output; } /** * Returns an IData representation of the given TimeZone object, using the given datetime string to resolve whether * daylight savings is active. * * @param timezone The TimeZone object to be converted to an IData representation. * @param datetime The datetime string used to resolve the status of daylight savings. * @param pattern The pattern to use to parse the given datetime string. * @return An IData representation of the given TimeZone object. */ public static IData toIData(TimeZone timezone, String datetime, String pattern) { return toIData(timezone, DateTimeHelper.parse(datetime, pattern)); } /** * Returns an IData representation of the given TimeZone objects, using the given datetime to resolve whether * daylight savings is active. * * @param timezones A list of TimeZone objects to be converted to an IData representation. * @param instant The datetime used to resolve the status of daylight savings. * @return An IData[] representation of the given TimeZone objects. */ public static IData[] toIDataArray(TimeZone[] timezones, Calendar instant) { if (timezones == null) return null; IData[] output = new IData[timezones.length]; for (int i = 0; i < timezones.length; i++) { output[i] = toIData(timezones[i], instant); } return output; } /** * Returns an IData representation of the given TimeZone objects, using the given datetime string to resolve whether * daylight savings is active. * * @param timezones A list of TimeZone objects to be converted to an IData representation. * @param datetime The datetime string used to resolve the status of daylight savings. * @param pattern The pattern to use to parse the given datetime string. * @return An IData[] representation of the given TimeZone objects. */ public static IData[] toIDataArray(TimeZone[] timezones, String datetime, String pattern) { return toIDataArray(timezones, DateTimeHelper.parse(datetime, pattern)); } }