/* -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil -*- * * The contents of this file are subject to the Mozilla Public License * Version 1.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.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See * the License for the specific language governing rights and limitations * under the License. * * The Original Code is the Grendel mail/news client. * * The Initial Developer of the Original Code is Netscape Communications * Corporation. Portions created by Netscape are Copyright (C) 1997 * Netscape Communications Corporation. All Rights Reserved. * * Created: Jamie Zawinski , 3 Aug 1995. * Ported from C on 19 Aug 1997. * * Contributors: Edwin Woudt */ package calypso.util; import java.util.Date; import java.util.TimeZone; import calypso.util.ByteBuf; /** Parses a date out of a string of bytes. Call it like this:

Note that it operates on bytes, not chars, since network dates are always ASCII.

Why would you want to use this instead of java.text.DateFormat.parse()? Because this algorithm has been tested in the field against real-world message headers for several years (the C code hadn't changed substantively since Netscape 2.0.) (There had been DST-related problems, but the tokenizer/parser was always sound.) @see Date @see Calendar @see java.text.DateFormat */ public class NetworkDate { private NetworkDate() { } static private final int UNKNOWN = 0; // random unknown token static private final int SUN = 101; // days of the week static private final int MON = 102; static private final int TUE = 103; static private final int WED = 104; static private final int THU = 105; static private final int FRI = 107; static private final int SAT = 108; static private final int JAN = 201; // months static private final int FEB = 202; static private final int MAR = 203; static private final int APR = 204; static private final int MAY = 205; static private final int JUN = 206; static private final int JUL = 207; static private final int AUG = 208; static private final int SEP = 209; static private final int OCT = 210; static private final int NOV = 211; static private final int DEC = 212; static private final int PST = 301; // a smattering of timezones static private final int PDT = 302; static private final int MST = 303; static private final int MDT = 304; static private final int CST = 305; static private final int CDT = 306; static private final int EST = 307; static private final int EDT = 308; static private final int AST = 309; static private final int NST = 310; static private final int GMT = 311; static private final int BST = 312; static private final int MET = 313; static private final int EET = 314; static private final int JST = 315; /** This parses a time/date string into a Time object. If it can't be parsed, -1 is returned.

Many formats are handled, including:

But note that 6/5/95 is ambiguous, since it's not obvious which is the day and which is the month. (6/13/95 is not ambiguous, however.) @param buf The bytes to parse. This assumes the input to be a sequence of 8-bit ASCII characters. High-bit characters are handled; 16-bit Unicode characters are not. @param default_to_gmt If the input string doesn't contain a description of the timezone, then this argument determines whether the string is interpreted relative to the local time zone (false) or to GMT (true). The correct value to pass in for this argument depends on what standard specified the time string which you are parsing; for RFC822 dates, this argument should be true. @return Microseconds since Jan 1, 1900, GMT. You can pass this to new Date(long). Returns -1 if the string is unparsable (no other negative value will ever be returned; dates before the Epoch are not handled.) */ public static long parseLong(ByteBuf buf, boolean default_to_gmt) { return parseLong(buf.toBytes(), 0, buf.length(), default_to_gmt); } /** The same, but takes a byte array and a region within it, instead of a ByteBuf. */ public static long parseLong(byte string[], int start, int end, boolean default_to_gmt) { int dotw = UNKNOWN; int month = UNKNOWN; int zone = UNKNOWN; int zone_offset = -1; int date = -1; int year = -1; int hour = -1; int min = -1; int sec = -1; int i = start; while (i < end) { byte c = string[i]; byte c2 = ((i+1 < end) ? string[i+1] : 0); byte c3 = ((i+2 < end) ? string[i+2] : 0); switch (c) { case (byte)'a': case (byte)'A': if (month == UNKNOWN && (c2 == 'p' || c2 == 'P') && (c3 == 'r' || c3 == 'R')) month = APR; else if (zone == UNKNOWN && (c2 == 's' || c2 == 's') && (c3 == 't' || c3 == 'T')) zone = AST; else if (month == UNKNOWN && (c2 == 'u' || c2 == 'U') && (c3 == 'g' || c3 == 'G')) month = AUG; break; case (byte)'b': case (byte)'B': if (zone == UNKNOWN && (c2 == 's' || c2 == 'S') && (c3 == 't' || c3 == 'T')) zone = BST; break; case (byte)'c': case (byte)'C': if (zone == UNKNOWN && (c2 == 'd' || c2 == 'D') && (c3 == 't' || c3 == 'T')) zone = CDT; else if (zone == UNKNOWN && (c2 == 's' || c2 == 'S') && (c3 == 't' || c3 == 'T')) zone = CST; break; case (byte)'d': case (byte)'D': if (month == UNKNOWN && (c2 == 'e' || c2 == 'E') && (c3 == 'c' || c3 == 'C')) month = DEC; break; case (byte)'e': case (byte)'E': if (zone == UNKNOWN && (c2 == 'd' || c2 == 'D') && (c3 == 't' || c3 == 'T')) zone = EDT; else if (zone == UNKNOWN && (c2 == 'e' || c2 == 'E') && (c3 == 't' || c3 == 'T')) zone = EET; else if (zone == UNKNOWN && (c2 == 's' || c2 == 'S') && (c3 == 't' || c3 == 'T')) zone = EST; break; case (byte)'f': case (byte)'F': if (month == UNKNOWN && (c2 == 'e' || c2 == 'E') && (c3 == 'b' || c3 == 'B')) month = FEB; else if (dotw == UNKNOWN && (c2 == 'r' || c2 == 'R') && (c3 == 'i' || c3 == 'I')) dotw = FRI; break; case (byte)'g': case (byte)'G': if (zone == UNKNOWN && (c2 == 'm' || c2 == 'M') && (c3 == 't' || c3 == 'T')) zone = GMT; break; case (byte)'j': case (byte)'J': if (month == UNKNOWN && (c2 == 'a' || c2 == 'A') && (c3 == 'n' || c3 == 'N')) month = JAN; else if (zone == UNKNOWN && (c2 == 's' || c2 == 'S') && (c3 == 't' || c3 == 'T')) zone = JST; else if (month == UNKNOWN && (c2 == 'u' || c2 == 'U') && (c3 == 'l' || c3 == 'L')) month = JUL; else if (month == UNKNOWN && (c2 == 'u' || c2 == 'U') && (c3 == 'n' || c3 == 'N')) month = JUN; break; case (byte)'m': case (byte)'M': if (month == UNKNOWN && (c2 == 'a' || c2 == 'A') && (c3 == 'r' || c3 == 'R')) month = MAR; else if (month == UNKNOWN && (c2 == 'a' || c2 == 'A') && (c3 == 'y' || c3 == 'Y')) month = MAY; else if (zone == UNKNOWN && (c2 == 'd' || c2 == 'D') && (c3 == 't' || c3 == 'T')) zone = MDT; else if (zone == UNKNOWN && (c2 == 'e' || c2 == 'E') && (c3 == 't' || c3 == 'T')) zone = MET; else if (dotw == UNKNOWN && (c2 == 'o' || c2 == 'O') && (c3 == 'n' || c3 == 'N')) dotw = MON; else if (zone == UNKNOWN && (c2 == 's' || c2 == 'S') && (c3 == 't' || c3 == 'T')) zone = MST; break; case (byte)'n': case (byte)'N': if (month == UNKNOWN && (c2 == 'o' || c2 == 'O') && (c3 == 'v' || c3 == 'V')) month = NOV; else if (zone == UNKNOWN && (c2 == 's' || c2 == 'S') && (c3 == 't' || c3 == 'T')) zone = NST; break; case (byte)'o': case (byte)'O': if (month == UNKNOWN && (c2 == 'c' || c2 == 'C') && (c3 == 't' || c3 == 'T')) month = OCT; break; case (byte)'p': case (byte)'P': if (zone == UNKNOWN && (c2 == 'd' || c2 == 'D') && (c3 == 't' || c3 == 'T')) zone = PDT; else if (zone == UNKNOWN && (c2 == 's' || c2 == 'S') && (c3 == 't' || c3 == 'T')) zone = PST; break; case (byte)'s': case (byte)'S': if (dotw == UNKNOWN && (c2 == 'a' || c2 == 'A') && (c3 == 't' || c3 == 'T')) dotw = SAT; else if (month == UNKNOWN && (c2 == 'e' || c2 == 'E') && (c3 == 'p' || c3 == 'P')) month = SEP; else if (dotw == UNKNOWN && (c2 == 'u' || c2 == 'U') && (c3 == 'n' || c3 == 'N')) dotw = SUN; break; case (byte)'t': case (byte)'T': if (dotw == UNKNOWN && (c2 == 'h' || c2 == 'H') && (c3 == 'u' || c3 == 'U')) dotw = THU; else if (dotw == UNKNOWN && (c2 == 'u' || c2 == 'U') && (c3 == 'e' || c3 == 'E')) dotw = TUE; break; case (byte)'u': case (byte)'U': if (zone == UNKNOWN && (c2 == 't' || c2 == 'T') && !(c3 >= 'A' && c3 <= 'Z') && !(c3 >= 'a' && c3 <= 'z')) // UT is the same as GMT but UTx is not. zone = GMT; break; case (byte)'w': case (byte)'W': if (dotw == UNKNOWN && (c2 == 'e' || c2 == 'E') && (c3 == 'd' || c3 == 'D')) dotw = WED; break; // parsing timezone offsets case (byte)'+': case (byte)'-': { if (zone_offset >= 0) { // already got one... i++; break; } if (zone != UNKNOWN && zone != GMT) { // GMT+0300 is legal, but PST+0300 is not. i++; break; } int sign = ((c == '+') ? 1 : -1); i++; /* move over sign */ int token_start = i; int token_end; for (token_end = token_start; token_end < end; token_end++) { c = string[token_end]; if (c < '0' || c > '9') break; } if ((token_end - token_start) == 4) // GMT+0000: offset in HHMM zone_offset = (((((string[token_start]-'0') * 10) + (string[token_start+1]-'0')) * 60) + (((string[token_start+2]-'0') * 10) + (string[token_start+3]-'0'))); else if ((token_end - token_start) == 2) // GMT+00: offset in hours zone_offset = (((string[token_start]-'0') * 10) + (string[token_start+1]-'0')) * 60; else if ((token_end - token_start) == 1) // GMT+0: offset in hours zone_offset = (string[token_start]-'0') * 60; else /* three digits, or more than 4: unrecognised. */ break; zone_offset *= sign; zone = GMT; break; } // parsing numeric tokens case (byte)'0': case (byte)'1': case (byte)'2': case (byte)'3': case (byte)'4': case (byte)'5': case (byte)'6': case (byte)'7': case (byte)'8': case (byte)'9': { int tmp_hour = -1; int tmp_min = -1; int tmp_sec = -1; int token_start = i; int token_end; // move token_end to the first non-digit, or end of string. for (token_end = token_start; token_end < end; token_end++) { c = string[token_end]; if (c < '0' || c > '9') break; } if (token_end < end && c == ':') { // If there's a colon, this is an hour/minute specification. // If we've already set hour or min, ignore this one. if (hour > 0 && min > 0) break; // We have seen "[0-9]+:", so this is probably HH:MM[:SS] if ((token_end - token_start) == 2) // two digits then a colon -- it is the hour. tmp_hour = ((string[token_start]-'0') * 10 + (string[token_start+1]-'0')); else if ((token_end - token_start) == 1) // one digit then a colon -- it is the hour. tmp_hour = (string[token_start]-'0'); else break; // Got the hour; move over the colon, and parse the minutes. // move token_start just past the colon. token_start = token_end+1; if (token_start >= end) break; // move token_end to the first non-digit, or end of string. for(token_end = token_start; token_end < end; token_end++) { c = string[token_end]; if (c < '0' || c > '9') break; } if ((token_end - token_start) == 2) // two digits followed by a colon or EOS -- it is the minutes. tmp_min = ((string[token_start]-'0') * 10 + (string[token_start+1]-'0')); else if ((token_end - token_start) == 1) // one digit followed by a colon or EOS -- it is the minutes. tmp_min = (string[token_start]-'0'); else break; // Got the hour and minutes; move over the colon, and try to // parse the seconds. token_start = token_end; if (token_end < end && string[token_end] == ':') { // move token_start just past the colon. token_start++; // move token_end to the first non-digit, or end of string. for(token_end = token_start; token_end < end; token_end++) { c = string[token_end]; if (c < '0' || c > '9') break; } if ((token_end - token_start) == 2) // two digits followed by EOT -- it is the seconds. tmp_sec = ((string[token_start]-'0') * 10 + (string[token_start+1]-'0')); else if ((token_end - token_start) == 1) // one digit followed by EOT -- it is the seconds. tmp_sec = (string[token_start]-'0'); } // If we made it here, we've parsed hour and min, // and possibly sec, so it worked as a unit. // skip over whitespace and see if there's an AM or PM // directly following the time. if (token_end < end && tmp_hour <= 12) { for(; token_start < end; token_start++) { c = string[token_start]; if (c < '0' || c > '9') break; } while (token_start < end && string[token_start] <= ' ') token_start++; if (token_start+1 < end) { if ((string[token_start] == 'p' || string[token_start] == 'P') && (string[token_start+1] == 'm' || string[token_start+1] == 'M')) // 10:05pm == 22:05, and 12:05pm == 12:05 tmp_hour = (tmp_hour == 12 ? 12 : tmp_hour + 12); else if (tmp_hour == 12 && (string[token_start] == 'a' || string[token_start] == 'A') && (string[token_start+1] == 'm' || string[token_start+1] == 'M')) // 12:05am == 00:05 tmp_hour = 0; } } // if we made it all the way to here, we can keep these values. // (We might have bugged out after parsing hour and before // parsing min, and it would be bad to take one and not the // other.) hour = tmp_hour; min = tmp_min; sec = tmp_sec; i = token_end-1; break; } else if (token_end+1 < end && (c == '/' || c == '-') && (string[token_end+1] >= '0' && string[token_end+1] <= '9')) { // Perhaps this is 6/16/95, 16/6/95, 6-16-95, or 16-6-95 // or even 95-06-05... // #### But it doesn't handle 1995-06-22. // int n1, n2, n3; if (month != UNKNOWN) // if we saw a month name, this can't be. break; token_start = i; // get the first one or two digits n1 = (string[token_start] - '0'); token_start++; if (token_start+1 < end && string[token_start] >= '0' && string[token_start] <= '9') { n1 = (n1*10) + (string[token_start] - '0'); token_start++; } // demand that the next char be / or -. if (token_start >= end-1 || (string[token_start] != '/' && string[token_start] != '-')) break; token_start++; // demand that the next char be a digit. if (string[token_start] < '0' || string[token_start] > '9') break; // get the second group of one or two digits n2 = (string[token_start] - '0'); token_start++; if (token_start+1 < end && string[token_start] >= '0' && string[token_start] <= '9') { n2 = (n2*10) + (string[token_start] - '0'); token_start++; } // demand that the next char be / or -. if (token_start >= end-1 || (string[token_start] != '/' && string[token_start] != '-')) break; token_start++; // demand that the next char be a digit. if (string[token_start] < '0' || string[token_start] > '9') break; // get the third group of one, two, or four digits n3 = (string[token_start] - '0'); token_start++; if (token_start+1 <= end && string[token_start] >= '0' && string[token_start] <= '9') { // digit two n3 = (n3*10) + (string[token_start] - '0'); token_start++; // digit three if (token_start+1 < end && string[token_start] >= '0' && string[token_start] <= '9') { // if we have digit three but not digit four, give up on it: // there are no three-digit-numbers in this context. if (token_start+2 > end || (string[token_start+1] < '0' && string[token_start+1] > '9')) break; n3 = (n3*10) + (string[token_start++] - '0'); n3 = (n3*10) + (string[token_start++] - '0'); } } // if the digits are followed by an alphanumeric, give up on it. if (token_start < end && ((string[token_start] >= '0' && string[token_start] <= '9') || (string[token_start] >= 'a' && string[token_start] <= 'z') || (string[token_start] >= 'A' && string[token_start] <= 'Z'))) break; // At this point, we've parsed three 1-2 digit numbers, with / or - // between them. Now decide what the hell they are (DD/MM/YY or // MM/DD/YY or YY/MM/DD.) if (n1 > 70) { // must be YY/MM/DD if (n2 > 12) break; if (n3 > 31) break; year = n1; if (year < 1900) year += 1900; month = (n2 + JAN - 1); date = n3; i = token_start; break; } if (n3 < 70 || // before epoch - can't represent it. (n1 > 12 && n2 > 12)) { // illegal i = token_start; break; } if (n3 < 1900) n3 += 1900; if (n1 > 12) { // must be DD/MM/YY date = n1; month = (n2 + JAN - 1); year = n3; } else { // assume MM/DD/YY // #### In the ambiguous case, should we consult the locale to // find out the local default? month = (n1 + JAN - 1); date = n2; year = n3; } i = token_start; } else if (token_end+1 < end && (string[token_end] >= 'A' || string[token_end] <= 'Z') && (string[token_end+1] >= 'a' && string[token_end+1] <= 'z')) { // Digits followed by non-punctuation - what's that? } else if ((token_end - token_start) == 4) { // four digits in a row must be a year. if (year < 0) year = ((string[i]-'0') * 1000 + (string[i+1]-'0') * 100 + (string[i+2]-'0') * 10 + (string[i+3]-'0')); } else if ((token_end - token_start) == 2) { // two digits in a row might be a date, or a year int n = ((string[i]-'0') * 10 + (string[i+1]-'0')); // If we don't have a date (day of the month) and we see a number // less than 32, then assume that is the date. // // Otherwise, if we have a date and not a year, assume this is the // year. If it is less than 70, then assume it refers to the 21st // century. If it is two digits (>= 70), assume it refers to the // 20th century. Otherwise, assume it refers to an unambiguous year. // // The world will surely end soon. // if (date < 0 && n < 32) date = n; else if (year < 0) { if (n < 70) year = 2000 + n; else if (n < 100) year = 1900 + n; else year = n; } } else if ((token_end - token_start) == 1) { // one digit all alone -- it must be a date. if (date < 0) date = string[i]-'0'; } else { // else, three or more than four digits - what's that? break; } } } // closes switch // Skip to the end of this token, whether we parsed it or not. // Tokens are delimited by whitespace, or ,;-/ // But explicitly not :+-. for (; i < end; i++) { c = string[i]; if (c <= ' ' || c == ',' || c == ';' || c == '-' || c == '+' || c == '/' || c == '(' || c == ')' || c == '[' || c == ']') break; } boolean done = false; while (!done) { done = true; // skip over uninteresting chars before the next token. for (; i < end; i++) { c = string[i]; if (! (c <= ' ' || c == ',' || c == ';' || c == '/' || c == '(' || c == ')' || c == '[' || c == ']')) break; } // "-" is ignored at the beginning of a token if we have not yet // parsed a year, or if the character after the dash is not a digit. if (i < end && c == '-' && (year < 0 || string[i+1] < '0' || string[i+1] > '9')) { i++; done = false; } } } // done parsing string -- turn symbolic zones into numeric offsets. if (zone != UNKNOWN && zone_offset == -1) { switch (zone) { case PST: zone_offset = -8 * 60; break; case PDT: zone_offset = -7 * 60; break; case MST: zone_offset = -7 * 60; break; case MDT: zone_offset = -6 * 60; break; case CST: zone_offset = -6 * 60; break; case CDT: zone_offset = -5 * 60; break; case EST: zone_offset = -5 * 60; break; case EDT: zone_offset = -4 * 60; break; case AST: zone_offset = -4 * 60; break; case NST: zone_offset = -3 * 60 - 30; break; case GMT: zone_offset = 0 * 60; break; case BST: zone_offset = 1 * 60; break; case MET: zone_offset = 1 * 60; break; case EET: zone_offset = 2 * 60; break; case JST: zone_offset = 9 * 60; break; } } // If we didn't find a year, month, or day-of-the-month, we can't // possibly parse this, so just give up. (In fact, in the Unix C version // of this code, I'd seen mktime() do something random -- it always // returned "Tue Feb 5 06:28:16 2036", which is no doubt a // numerologically significant date... // if (month == UNKNOWN || date == -1 || year == -1) return -1; if (zone_offset == -1) { if (default_to_gmt) zone_offset = 0; else zone_offset = localZoneOffset(); } return UTC((year - 1900), (month - JAN), date, (hour == -1 ? 0 : hour), (min == -1 ? 0 : min) - zone_offset, (sec == -1 ? 0 : sec)); } /** The same, but assumes GMT is the default timezone. */ public static long parseLong(ByteBuf string) { return parseLong(string, true); } /** Like parseLong(), but returns a new Date object instead. */ public static Date parseDate(ByteBuf string, boolean default_to_gmt) { long date = parseLong(string, default_to_gmt); if (date == -1) return null; return new Date(date); } /** The same, but assumes GMT is the default timezone. */ public static Date parseDate(ByteBuf string) { return parseDate(string, true); } /** Composes the given date into a number suitable for passing to new Date(n). This is the number of milliseconds past the Epoch. */ public static long UTC(int year, int month, int date, int hour, int min, int sec) { long day = (date + monthOffset[month] + ((((year & 3) != 0) || ((year % 100 == 0) && ((year + 300) % 400 != 0)) || (month < 2)) ? -1 : 0) /* convert day-of-month to 0 based range, * except following February in a leap year, * in which case we skip the conversion to * account for the extra day in February */ + ((year - 70) * 365L) // days per year + ((year - 69) / 4) // plus leap days - ((year - 1) / 100) // no leap on century years + ((year + 299) / 400)); // except %400 years return (1000 * ((sec + (60 * (min + (60 * hour)))) + (60 * 60 * 24) * day)); } static private final short monthOffset[] = { 0, // 31 January 31, // 28 February 59, // 31 March 90, // 30 April 120, // 31 May 151, // 30 June 181, // 31 July 212, // 31 August 243, // 30 September 273, // 31 October 304, // 30 November 334 // 31 December // 365 }; static private int localZoneOffset() { TimeZone z = TimeZone.getDefault(); int off = z.getRawOffset() / (1000 * 60); if (z.inDaylightTime(new Date())) // Consing!! off += 60; return off; } /* private static void debug_print(int dotw, int month, int zone, int offset, int date, int year, int hour, int min, int sec) { System.out.println("dotw = " + debug_token_to_string(dotw) + " (" + dotw + ")"); System.out.println("date = " + date); System.out.println("month = " + debug_token_to_string(month) + " (" + month + ")"); System.out.println("year = " + year); System.out.println("hour = " + hour); System.out.println("min = " + min); System.out.println("sec = " + sec); System.out.println("zone = " + debug_token_to_string(zone) + " (" + zone + ")"); System.out.println("off = " + offset); System.out.println(""); } private static String debug_token_to_string(int t) { switch (t) { case UNKNOWN: return "UNKNOWN"; case SUN: return "SUN"; case MON: return "MON"; case TUE: return "TUE"; case WED: return "WED"; case THU: return "THU"; case FRI: return "FRI"; case SAT: return "SAT"; case JAN: return "JAN"; case FEB: return "FEB"; case MAR: return "MAR"; case APR: return "APR"; case MAY: return "MAY"; case JUN: return "JUN"; case JUL: return "JUL"; case AUG: return "AUG"; case SEP: return "SEP"; case OCT: return "OCT"; case NOV: return "NOV"; case DEC: return "DEC"; case PST: return "PST"; case PDT: return "PDT"; case MST: return "MST"; case MDT: return "MDT"; case CST: return "CST"; case CDT: return "CDT"; case EST: return "EST"; case EDT: return "EDT"; case AST: return "AST"; case NST: return "NST"; case GMT: return "GMT"; case BST: return "BST"; case MET: return "MET"; case EET: return "EET"; case JST: return "JST"; default: return "???"; } } */ } /* We only handle a few time zones, but in case someone is feeling ambitious, here are a bunch of other timezone names and numbers. It's generally frowned upon to put timezone names in date headers instead of GMT offsets, so the small set of zone names that we already handle is probably sufficient. --------------------------------------------------------------------- I lifted this list of time zone abbreviations out of a UNIX computers setup file (specifically, from an AT&T StarServer running UNIX System V Release 4, in the /usr/lib/local/TZ directory). The list is by no means complete or comprehensive, as much of it comes out of scripts designed to adjust the time on computers when Daylight Savings Time (DST) rolls around. Also, I would consider it at least a little suspect. First, because it was compiled by Americans, and you know how us Americans are with geography :). Second, the data looks to be a little old, circa 1991 (note the reference to the "Soviet Union"). The first column is an approximate name for the time zone described, the second column gives the time relative to GMT, the third column takes a stab at listing the country that the time zone is in, and the final column gives one or more abbreviations that apply to that time zone (note that abbreviations that end with "DST" or with "S" as the middle letter indicate Daylight Savings Time is in effect). I've also tried to roughly divide the listings into geographical groupings. Hope this helps, Raymond McCauley Texas A&M University scooter@tamu.edu ================= Europe Great Britain 0:00 GB-Eire GMT, BST Western European nations +0:00 W-Eur WET, WET DST Iceland +0:00 - WET Middle European nations +1:00 M-Eur MET, MET DST Poland +1:00 W-Eur MET, MET DST Eastern European nations +2:00 E-Eur EET, EET DST Turkey +3:00 Turkey EET, EET DST Warsaw Pact/Soviet Union +3:00 M-Eur ???? North America Canada/Newfoundland -3:30 Canada NST, NDT Canada/Atlantic -4:00 Canada AST, ADT Canada/Eastern -5:00 Canada EST, EDT Canada/Central -6:00 Canada CST, CDT Canada/East-Saskatchewan -6:00 Canada CST Canada/Mountain -7:00 Canada MST, MDT Canada/Pacific -8:00 Canada PST, PDT Canada/Yukon -9:00 Canada YST, YDT US/Eastern -5:00 US EST, EDT US/Central -6:00 US CST, CDT US/Mountain -7:00 US MST, MDT US/Pacific -8:00 US PST, PDT US/Yukon -9:00 US YST, YDT US/Hawaii -10:00 US HST, PST, PDT, PPET Mexico/BajaNorte -8:00 Mexico PST, PDT Mexico/BajaSur -7:00 Mexico MST Mexico/General -6:00 Mexico CST South America Brazil/East -3:00 Brazil EST, EDT Brazil/West -4:00 Brazil WST, WDT Brazil/Acre -5:00 Brazil AST, ADT Brazil/DeNoronha -2:00 Brazil FST, FDT Chile/Continental -4:00 Chile CST, CDT Chile/EasterIsland -6:00 Chile EST, EDT Asia People's Repub. of China +8:00 PRC CST, CDT (Yes, they really have only one time zone.) Republic of Korea +9:00 ROK KST, KDT Japan +9:00 Japan JST Singapore +8:00 Singapore SST Hongkong +8:00 U.K. HKT ROC +8:00 - CST Middle East Israel +3:00 Israel IST, IDT (This was the only listing I found) Australia Australia/Tasmania +10:00 Oz EST Australia/Queensland +10:00 Oz EST Australia/North +9:30 Oz CST Australia/West +8:00 Oz WST Australia/South +9:30 Oz CST Hour TZN DZN Zone Example 0 GMT Greenwich Mean Time GMT0 0 UTC Universal Coordinated Time UTC0 2 FST FDT Fernando De Noronha Std FST2FDT 3 BST Brazil Standard Time BST3 3 EST EDT Eastern Standard (Brazil) EST3EDT 3 GST Greenland Standard Time GST3 3:30 NST NDT Newfoundland Standard Time NST3:30NDT 4 AST ADT Atlantic Standard Time AST4ADT 4 WST WDT Western Standard (Brazil) WST4WDT 5 EST EDT Eastern Standard Time EST5EDT 5 CST CDT Chile Standard Time CST5CDT Hour TZN DZN Zone Example 5 AST ADT Acre Standard Time AST5ADT 5 CST CDT Cuba Standard Time CST5CDT 6 CST CDT Central Standard Time CST6CDT 6 EST EDT Easter Island Standard EST6EDT 7 MST MDT Mountain Standard Time MST7MDT 8 PST PDT Pacific Standard Time PST8PDT 9 AKS AKD Alaska Standard Time AKS9AKD 9 YST YDT Yukon Standard Time YST9YST 10 HST HDT Hawaii Standard Time HST10HDT 11 SST Somoa Standard Time SST11 -12 NZS NZD New Zealand Standard Time NZS-12NZD -10 GST Guam Standard Time GST-10 -10 EAS EAD Eastern Australian Standard EAS-10EAD -9:30 CAS CAD Central Australian Standard CAS-9:30CAD -9 JST Japan Standard Time JST-9 -9 KST KDT Korean Standard Time KST-9KDT -8 CCT China Coast Time CCT-8 -8 HKT Hong Kong Time HKT-8 -8 SST Singapore Standard Time SST-8 -8 WAS WAD Western Australian Standard WAS-8WAD -7:30 JT Java Standard Time JST-7:30 -7 NST North Sumatra Time NST-7 -5:30 IST Indian Standard Time IST-5:30 -3:30 IST IDT Iran Standard Time IST-3:30IDT -3 MSK MSD Moscow Winter Time MSK-3MSD -2 EET Eastern Europe Time EET-2 -2 IST IDT Israel Standard Time IST-2IDT -1 MEZ MES Middle European Time MEZ-1MES -1 SWT SST Swedish Winter Time SWT-1SST -1 FWT FST French Winter Time FWT-1FST -1 CET CES Central European Time CET-1CES -1 WAT West African Time WAT-1 */