/src/sys/java/fan/sys/TimeZone.java
Java | 793 lines | 512 code | 94 blank | 187 comment | 106 complexity | 2601f78bc35df171478c449623a2dfe2 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
- //
- // Copyright (c) 2007, Brian Frank and Andy Frank
- // Licensed under the Academic Free License version 3.0
- //
- // History:
- // 28 Sep 07 Brian Frank Creation
- // 21 Jul 09 Brian Frank Upgrade to more compressed format
- //
- package fan.sys;
- import java.io.BufferedInputStream;
- import java.io.BufferedReader;
- import java.io.DataInputStream;
- import java.io.File;
- import java.io.FileInputStream;
- import java.io.InputStream;
- import java.io.InputStreamReader;
- import java.io.IOException;
- import java.io.RandomAccessFile;
- import java.util.Arrays;
- import java.util.HashMap;
- import java.util.Iterator;
- import java.util.Map.Entry;
- /**
- * TimeZone
- */
- public final class TimeZone
- extends FanObj
- {
- //////////////////////////////////////////////////////////////////////////
- // Constructors
- //////////////////////////////////////////////////////////////////////////
- public static List listNames()
- {
- return new List(Sys.StrType, indexNames).ro();
- }
- public static List listFullNames()
- {
- List list = new List(Sys.StrType);
- for (int i=0; i<indexNames.length; ++i)
- {
- String prefix = prefixes[indexPrefixes[i] & 0xff];
- String name = indexNames[i];
- if (prefix.length() != 0) name = prefix + "/" + name;
- list.add(name);
- }
- return list.ro();
- }
- public static TimeZone fromStr(String name) { return fromStr(name, true); }
- public static TimeZone fromStr(String name, boolean checked)
- {
- // check cache first
- TimeZone tz;
- synchronized (cache)
- {
- tz = (TimeZone)cache.get(name);
- if (tz != null) return tz;
- }
- // try to load from database
- try
- {
- tz = loadTimeZone(name);
- }
- catch (Exception e)
- {
- e.printStackTrace();
- throw IOErr.make("Cannot load from timezone database: " + name);
- }
- // if not found, check aliases
- if (tz == null)
- {
- if (aliases == null) loadAliases();
- String alias = (String)aliases.get(name);
- if (alias != null)
- {
- tz = fromStr(alias); // better be found
- synchronized (cache)
- {
- TimeZone dup = (TimeZone)cache.get(name);
- if (dup != null) return dup;
- cache.put(name, tz);
- return tz;
- }
- }
- }
- // if found, then cache and return
- if (tz != null)
- {
- synchronized (cache)
- {
- TimeZone dup = (TimeZone)cache.get(name);
- if (dup != null) return dup;
- cache.put(tz.name, tz);
- cache.put(tz.fullName, tz);
- return tz;
- }
- }
- // not found
- if (checked) throw ParseErr.make("TimeZone not found: " + name);
- return null;
- }
- public static TimeZone defVal()
- {
- return utc;
- }
- public static TimeZone utc()
- {
- return utc;
- }
- public static TimeZone rel()
- {
- return rel;
- }
- public static TimeZone cur()
- {
- return cur;
- }
- /** Get generic GMT offset where offset is in seconds */
- static TimeZone fromGmtOffset(int offset)
- {
- if (offset == 0) return TimeZone.utc();
- StringBuffer s = new StringBuffer();
- if (offset < 0) { offset = -offset; s.append("GMT+"); } else { s.append("GMT-"); }
- int hour = offset / 3600;
- /* we don't have standard GMT timezones to support fractional
- hours like nutjobs that use timezones such as 3:30, so just
- round to nearest GMT hour
- int min = (offset % 3600)/60;
- if (min != 0) throw new RuntimeException("Cannot convert fractional hour to GMT timezone: " + hour + ":" + min);
- */
- s.append(hour);
- return TimeZone.fromStr(s.toString());
- }
- //////////////////////////////////////////////////////////////////////////
- // Obj
- //////////////////////////////////////////////////////////////////////////
- public String toStr() { return name; }
- public Type typeof() { return Sys.TimeZoneType; }
- public Object trap(String name, List args)
- {
- // private undocumented access
- if (name.equals("rules")) return rules();
- return super.trap(name, args);
- }
- //////////////////////////////////////////////////////////////////////////
- // Methods
- //////////////////////////////////////////////////////////////////////////
- public String name()
- {
- return name;
- }
- public String fullName()
- {
- return fullName;
- }
- public Duration offset(long year)
- {
- return Duration.make(rule((int)year).offset * Duration.nsPerSec);
- }
- public Duration dstOffset(long year)
- {
- Rule r = rule((int)year);
- if (r.dstOffset == 0) return null;
- return Duration.make(r.dstOffset * Duration.nsPerSec);
- }
- public String stdAbbr(long year)
- {
- return rule((int)year).stdAbbr;
- }
- public String dstAbbr(long year)
- {
- return rule((int)year).dstAbbr;
- }
- public String abbr(int year, boolean inDST)
- {
- return inDST ? rule(year).dstAbbr : rule(year).stdAbbr;
- }
- final Rule rule(int year)
- {
- // most hits should be in latest rule
- Rule rule = rules[0];
- if (year >= rule.startYear) return rule;
- // check historical time zones
- for (int i=1; i<rules.length; ++i)
- if (year >= (rule = rules[i]).startYear) return rule;
- // return oldest rule
- return rules[rules.length-1];
- }
- private List rules()
- {
- List list = new List(Sys.ObjType);
- for (int i=0; i<rules.length; i++)
- {
- Rule r = rules[i];
- Map map = new Map(Sys.StrType, Sys.ObjType);
- map.set("startYear", Long.valueOf(r.startYear));
- map.set("offset", Long.valueOf(r.offset));
- map.set("stdAbbr", r.stdAbbr);
- map.set("dstOffset", Long.valueOf(r.dstOffset));
- if (r.dstOffset != 0)
- {
- map.set("dstAbbr", r.dstAbbr);
- map.set("dstStart", dstTimeToMap(r.dstStart));
- map.set("dstEnd", dstTimeToMap(r.dstEnd));
- }
- list.add(map);
- }
- return list;
- }
- private Map dstTimeToMap(DstTime t)
- {
- Map map = new Map(Sys.StrType, Sys.ObjType);
- map.set("mon", Long.valueOf(t.mon));
- map.set("onMode", Long.valueOf(t.onMode));
- map.set("onWeekday", Long.valueOf(t.onWeekday));
- map.set("onDay", Long.valueOf(t.onDay));
- map.set("atTime", Long.valueOf(t.atTime));
- map.set("atMode", Long.valueOf(t.atMode));
- return map;
- }
- //////////////////////////////////////////////////////////////////////////
- // Aliases
- //////////////////////////////////////////////////////////////////////////
- private static void loadAliases()
- {
- HashMap map = new HashMap();
- BufferedReader in = null;
- try
- {
- // read as props file without dependency on pulling in
- // lot of sys types that might use DateTime
- String sep = java.io.File.separator;
- InputStream fin = Sys.isJarDist ?
- TimeZone.class.getClassLoader().getResourceAsStream("res/sys/timezone-aliases.props") :
- new FileInputStream(Sys.homeDir + sep + "etc" + sep + "sys" + sep + "timezone-aliases.props");
- in = new BufferedReader(new InputStreamReader(fin, "UTF-8"));
- // parse props file line by line
- String line;
- while ((line = in.readLine()) != null)
- {
- // skip empty lines and // comment lines
- line = line.trim();
- if (line.length() == 0 || line.startsWith("//")) continue;
- // split key=val
- int eq = line.indexOf('=');
- if (eq < 0) { System.out.println("ERROR: timezone-aliases.props line: " + line); continue; }
- String key = line.substring(0, eq).trim();
- String val = line.substring(eq+1).trim();
- // map by fullName
- map.put(key, val);
- // map by simple name
- int slash = key.lastIndexOf('/');
- if (slash > 0) map.put(key.substring(slash+1), val);
- }
- }
- catch (Throwable e)
- {
- System.out.println("ERROR: Cannot read timezone-aliases.props");
- e.printStackTrace();
- }
- finally
- {
- try { if (in != null) in.close(); } catch (Exception e) {}
- }
- // save to field and force memory barrier sync
- TimeZone.aliases = map;
- synchronized(TimeZone.aliases) {}
- }
- //////////////////////////////////////////////////////////////////////////
- // Database
- //////////////////////////////////////////////////////////////////////////
- //
- // TimeZone database format:
- //
- // ftz
- // {
- // u8 magic ("fantz 02")
- // utf summary
- // u1 numPrefixes
- // utf[] prefixes
- // u2 numIndexItems
- // indexItems[] // sorted by name
- // {
- // u1 prefix id
- // utf name
- // u4 fileOffset
- // }
- // timeZones[]
- // {
- // u2 numRules
- // rules[]
- // {
- // u2 startYear
- // i4 utcOffset (seconds)
- // utf stdAbbr
- // i4 dstOffset (seconds); if zero rest is skipped
- // [
- // utf dstAbbr
- // dstTime dstStart
- // dstTime dstEnd
- // ]
- // }
- // }
- //
- // dstTime
- // {
- // u1 month
- // u1 onMode 'd', 'l', '>', '<' (date, last, >=, and <=)
- // u1 onWeekday (0-6)
- // u1 onDay
- // i4 atTime (seconds)
- // u1 atMode 'w' , 's', 'u' (wall, standad, universal)
- // }
- //
- // The timezone database is generated by the "/adm/buildtz.fan"
- // script. Refer to the "zic.8.txt" source file the code
- // distribution of the Olson database to further describe
- // model of the original data.
- //
- /**
- * Load the name and file offset list from file into memory.
- */
- static void loadIndex()
- throws IOException
- {
- DataInputStream in = openDb();
- try
- {
- // check magic "fantz 02"
- long magic = in.readLong();
- if (magic != 0x66616e747a203032L)
- throw new java.io.IOException("Invalid magic 0x" + Long.toHexString(magic));
- String summary = in.readUTF();
- // load prefixes
- int numPrefixes = in.readUnsignedByte();
- prefixes = new String[numPrefixes];
- for (int i=0; i<numPrefixes; ++i)
- prefixes[i] = in.readUTF();
- // load the zones and verify in sort order
- int num = in.readUnsignedShort();
- indexPrefixes = new byte[num];
- indexNames = new String[num];
- indexOffsets = new int[num];
- for (int i=0; i<num; ++i)
- {
- indexPrefixes[i] = (byte)in.read();
- indexNames[i] = in.readUTF();
- indexOffsets[i] = in.readInt();
- if (i != 0 && indexNames[i-1].compareTo(indexNames[i]) >= 0)
- throw new java.io.IOException("Index not sorted");
- }
- }
- finally
- {
- in.close();
- }
- }
- /**
- * Find the specified name in the index and load a time zone
- * definition. If the name is not found then return null.
- */
- static TimeZone loadTimeZone(String x)
- throws IOException
- {
- String name = x;
- int slash = x.lastIndexOf('/');
- if (slash > 0) name = name.substring(slash+1);
- // find index, which maps the file offset
- int ix = Arrays.binarySearch(indexNames, name);
- if (ix < 0) return null;
- // map full name
- String fullName = name;
- String prefix = prefixes[indexPrefixes[ix] & 0xff];
- if (prefix.length() != 0) fullName = prefix + "/" + name;
- if (slash > 0 && !x.equals(fullName)) return null;
- // create time zone instance
- TimeZone tz = new TimeZone();
- tz.name = name;
- tz.fullName = fullName;
- // read time zone definition from database file
- DataInputStream in = openDb();
- try
- {
- in.skip(indexOffsets[ix]);
- int numRules = in.readUnsignedShort();
- tz.rules = new Rule[numRules];
- for (int i=0; i<numRules; ++i)
- {
- Rule r = tz.rules[i] = new Rule();
- r.startYear = in.readUnsignedShort();
- r.offset = in.readInt();
- r.stdAbbr = in.readUTF();
- r.dstOffset = in.readInt();
- if (r.dstOffset == 0) continue;
- r.dstAbbr = in.readUTF();
- r.dstStart = loadDstTime(in);
- r.dstEnd = loadDstTime(in);
- if (i != 0 && tz.rules[i-1].startYear <= r.startYear)
- throw new java.io.IOException("TimeZone rules not sorted: " + name);
- }
- }
- finally
- {
- in.close();
- }
- return tz;
- }
- static DstTime loadDstTime(DataInputStream in)
- throws IOException
- {
- DstTime t = new DstTime();
- t.mon = in.readByte();
- t.onMode = in.readByte();
- t.onWeekday = in.readByte();
- t.onDay = in.readByte();
- t.atTime = in.readInt();
- t.atMode = in.readByte();
- return t;
- }
- static DataInputStream openDb()
- throws IOException
- {
- if (Sys.isJarDist)
- return new DataInputStream(new BufferedInputStream(
- TimeZone.class.getClassLoader().getResourceAsStream("etc/sys/timezones.ftz")));
- else
- return new DataInputStream(new BufferedInputStream(
- new FileInputStream(dbFile)));
- }
- //////////////////////////////////////////////////////////////////////////
- // Java
- //////////////////////////////////////////////////////////////////////////
- // This is mostly for testing right.
- java.util.TimeZone java()
- {
- return java.util.TimeZone.getTimeZone(name);
- }
- //////////////////////////////////////////////////////////////////////////
- // DST Calculations
- //////////////////////////////////////////////////////////////////////////
- /**
- * Compute the daylight savings time offset (in seconds)
- * for the specified parameters:
- * - Rule: the rule for a given year
- * - mon: month 0-11
- * - day: day 1-31
- * - weekday: 0-6
- * - time: seconds since midnight
- */
- static int dstOffset(Rule rule, int year, int mon, int day, int time)
- {
- DstTime start = rule.dstStart;
- DstTime end = rule.dstEnd;
- if (start == null) return 0;
- int s = compare(rule, start, year, mon, day, time);
- int e = compare(rule, end, year, mon, day, time);
- // if end month comes earlier than start month,
- // then this is dst in southern hemisphere
- if (end.mon < start.mon)
- {
- if (e > 0 || s <= 0) return rule.dstOffset;
- }
- else
- {
- if (s <= 0 && e > 0) return rule.dstOffset;
- }
- return 0;
- }
- /**
- * Compare the specified time to the dst start/end time.
- * Return -1 if x < specified time and +1 if x > specified time.
- */
- static int compare(Rule rule, DstTime x, int year, int mon, int day, int time)
- {
- int c = compareMonth(x, mon);
- if (c != 0) return c;
- c = compareOnDay(rule, x, year, mon, day);
- if (c != 0) return c;
- return compareAtTime(rule, x, time);
- }
- /**
- * Return if given date is the DstTime transition date
- */
- static boolean isDstDate(Rule rule, DstTime x, int year, int mon, int day)
- {
- return compareMonth(x, mon) == 0 &&
- compareOnDay(rule, x, year, mon, day) == 0;
- }
- /**
- * Compare month
- */
- static int compareMonth(DstTime x, int mon)
- {
- if (x.mon < mon) return -1;
- if (x.mon > mon) return +1;
- return 0;
- }
- /**
- * Compare on day.
- * 'd' 5 the fifth of the month
- * 'l' lastSun the last Sunday in the month
- * 'l' lastMon the last Monday in the month
- * '>' Sun>=8 first Sunday on or after the eighth
- * '<' Sun<=25 last Sunday on or before the 25th (not used)
- */
- static int compareOnDay(Rule rule, DstTime x, int year, int mon, int day)
- {
- // universal atTime might push us into the previous day
- if (x.atMode == 'u' && rule.offset + x.atTime < 0)
- ++day;
- switch (x.onMode)
- {
- case 'd':
- if (x.onDay < day) return -1;
- if (x.onDay > day) return +1;
- return 0;
- case 'l':
- int last = DateTime.weekdayInMonth(year, mon, x.onWeekday, -1);
- if (last < day) return -1;
- if (last > day) return +1;
- return 0;
- case '>':
- int start = DateTime.weekdayInMonth(year, mon, x.onWeekday, 1);
- while (start < x.onDay) start += 7;
- if (start < day) return -1;
- if (start > day) return +1;
- return 0;
- default:
- throw new IllegalStateException(""+(char)x.onMode);
- }
- }
- /**
- * Compare at time.
- */
- static int compareAtTime(Rule rule, DstTime x, int time)
- {
- int atTime = x.atTime;
- // if universal time, then we need to move atTime back to
- // local time (we might cross into the previous day)
- if (x.atMode == 'u')
- {
- if (rule.offset + x.atTime < 0)
- atTime = 24*60*60 + rule.offset + x.atTime;
- else
- atTime += rule.offset;
- }
- if (atTime < time) return -1;
- if (atTime > time) return +1;
- return 0;
- }
- //////////////////////////////////////////////////////////////////////////
- // Rule
- //////////////////////////////////////////////////////////////////////////
- static class Rule
- {
- /**
- * Return if this rule is wall time based. We take a shortcut
- * and assume that both start/end both use the same atMode - right
- * now (2007) it appears this is a valid assumption with the
- * single obscure exception being "Pacific/Tongatapu"
- */
- boolean isWallTime() { return dstStart.atMode == 'w'; }
- int startYear; // year rule took effect
- int offset; // UTC offset in seconds
- String stdAbbr; // standard time abbreviation
- int dstOffset; // seconds
- String dstAbbr; // daylight time abbreviation
- DstTime dstStart; // starting time
- DstTime dstEnd; // end time
- }
- static class DstTime
- {
- byte mon; // month (0-11)
- byte onMode; // 'd', 'l', '>', '<' (date, last, >=, and <=)
- byte onWeekday; // weekday (0-6)
- byte onDay; // weekday (0-6)
- int atTime; // seconds
- byte atMode; // 'w' , 's', 'u' (wall, standard, universal)
- public String toString()
- {
- return "DstTime mon=" + mon + " onMode=" + (char)onMode +
- " onWeekday=" + onWeekday + " onDay=" + onDay +
- " atTime=" + atTime + " " + (char)atMode;
- }
- }
- //////////////////////////////////////////////////////////////////////////
- // Database Index
- //////////////////////////////////////////////////////////////////////////
- static File dbFile = new File(Sys.homeDir, "etc" + java.io.File.separator + "sys" + java.io.File.separator + "timezones.ftz");
- static String[] prefixes = new String[0];
- static byte[] indexPrefixes = new byte[0];
- static String[] indexNames = new String[0];
- static int[] indexOffsets = new int[0];
- static HashMap aliases;
- static HashMap cache = new HashMap(); // String -> TimeZone
- static TimeZone utc;
- static TimeZone rel;
- static TimeZone cur;
- //////////////////////////////////////////////////////////////////////////
- // Initialization
- //////////////////////////////////////////////////////////////////////////
- static
- {
- try
- {
- loadIndex();
- }
- catch (Throwable e)
- {
- System.out.println("ERROR: Cannot load timezone database");
- e.printStackTrace();
- }
- try
- {
- utc = fromStr("Etc/UTC");
- }
- catch (Throwable e)
- {
- System.out.println("ERROR: Cannot init UTC timezone");
- e.printStackTrace();
- utc = loadFallback("Etc/UTC", "UTC");
- }
- try
- {
- rel = fromStr("Etc/Rel");
- }
- catch (Throwable e)
- {
- System.out.println("ERROR: Cannot init Rel timezone");
- e.printStackTrace();
- rel = loadFallback("Etc/Rel", "Rel");
- }
- try
- {
- // first check system property, otherwise try to use Java timezone
- String sysProp = Sys.sysConfig("timezone");
- if (sysProp != null)
- {
- cur = fromStr(sysProp);
- }
- else
- {
- cur = fromJava(java.util.TimeZone.getDefault().getID());
- }
- }
- catch (Throwable e)
- {
- System.out.println("ERROR: Cannot init current timezone");
- e.printStackTrace();
- cur = utc;
- }
- }
- private static TimeZone loadFallback(String fullName, String name)
- {
- TimeZone tz = new TimeZone();
- tz.name = name;
- tz.fullName = fullName;
- tz.rules = new Rule[] { new Rule() };
- return tz;
- }
- /**
- * Convert a Java timezone name to a Olson timezone identifier
- * as used by Fan. For Windows and OS X, Java reports the timezone
- * correctly. However Unix variants all seems to do things a bit
- * differently and do it wrong. Instead of reporting a real timezone,
- * many Unix variants report a GMT offset (which doesn't map to a
- * political region's DST rules).
- */
- public static TimeZone fromJava(String javatz)
- {
- // handle various UTC (we haven't seen all these, but just to be safe)
- if (javatz.equals("GMT0")) return utc;
- if (javatz.equals("GMT+00:00")) return utc;
- if (javatz.equals("GMT-00:00")) return utc;
- // Solarsis and many Linux distros seem to use
- // use GMT+/-hh:mm which we map to Etc
- if (javatz.startsWith("GMT"))
- {
- if (javatz.endsWith(":00")) javatz = javatz.substring(0, javatz.length()-3);
- if (javatz.startsWith("GMT-0")) javatz = "GMT-" + javatz.substring(5);
- if (javatz.startsWith("GMT+0")) javatz = "GMT+" + javatz.substring(5);
- return fromStr(javatz);
- }
- // we've had reports that Ubuntu uses timezone names
- // deprecated back in 1993, but handle some of the common ones
- if (javatz.equals("US/Eastern")) return fromStr("New_York");
- if (javatz.equals("US/Central")) return fromStr("Chicago");
- if (javatz.equals("US/Mountain")) return fromStr("Denver");
- if (javatz.equals("US/Pacific")) return fromStr("Los_Angeles");
- if (javatz.equals("US/Arizona")) return fromStr("Phoenix");
- // assume we have an actual timezone or throw exception
- return fromStr(javatz);
- }
- //////////////////////////////////////////////////////////////////////////
- // Fields
- //////////////////////////////////////////////////////////////////////////
- private String name; // time zone identifer
- private String fullName; // identifer in zoneinfo database
- private Rule[] rules; // reverse sorted by year
- }