1 /*
  2  * TimeZone.js - Definition of a time zone class
  3  *
  4  * Copyright © 2012-2015, 2018, JEDLSoft
  5  *
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  *
 10  *     http://www.apache.org/licenses/LICENSE-2.0
 11  *
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  *
 16  * See the License for the specific language governing permissions and
 17  * limitations under the License.
 18  */
 19 
 20 // !data localeinfo zoneinfo
 21 
 22 var ilib = require("../index.js");
 23 var Utils = require("./Utils.js");
 24 var MathUtils = require("./MathUtils.js");
 25 var JSUtils = require("./JSUtils.js");
 26 
 27 var Locale = require("./Locale.js");
 28 var LocaleInfo = require("./LocaleInfo.js");
 29 
 30 var GregRataDie = require("./GregRataDie.js");
 31 var CalendarFactory = require("./CalendarFactory.js");
 32 var IString = require("./IString.js");
 33 var DateFactory = require("./DateFactory.js");
 34 
 35 /**
 36  * @class
 37  * Create a time zone instance.
 38  *
 39  * This class reports and transforms
 40  * information about particular time zones.<p>
 41  *
 42  * The options parameter may contain any of the following properties:
 43  *
 44  * <ul>
 45  * <li><i>id</i> - The id of the requested time zone such as "Europe/London" or
 46  * "America/Los_Angeles". These are taken from the IANA time zone database. (See
 47  * http://www.iana.org/time-zones for more information.) <p>
 48  *
 49  * There is one special
 50  * time zone that is not taken from the IANA database called simply "local". In
 51  * this case, this class will attempt to discover the current time zone and
 52  * daylight savings time settings by calling standard Javascript classes to
 53  * determine the offsets from UTC.
 54  *
 55  * <li><i>locale</i> - The locale for this time zone.
 56  *
 57  * <li><i>offset</i> - Choose the time zone based on the offset from UTC given in
 58  * number of minutes (negative is west, positive is east).
 59  *
 60  * <li><i>onLoad</i> - a callback function to call when the data is fully
 61  * loaded. When the onLoad option is given, this class will attempt to
 62  * load any missing locale data using the ilib loader callback.
 63  * When the data is loaded, the onLoad function is called with the current
 64  * instance as a parameter.
 65  *
 66  * <li><i>sync</i> - tell whether to load any missing locale data synchronously or
 67  * asynchronously. If this option is given as "false", then the "onLoad"
 68  * callback must be given, as the instance returned from this constructor will
 69  * not be usable for a while.
 70  *
 71  * <li><i>loadParams</i> - an object containing parameters to pass to the
 72  * loader callback function when locale data is missing. The parameters are not
 73  * interpretted or modified in any way. They are simply passed along. The object
 74  * may contain any property/value pairs as long as the calling code is in
 75  * agreement with the loader callback function as to what those parameters mean.
 76  * </ul>
 77  *
 78  * There is currently no way in the ECMAscript
 79  * standard to tell which exact time zone is currently in use. Choosing the
 80  * id "locale" or specifying an explicit offset will not give a specific time zone,
 81  * as it is impossible to tell with certainty which zone the offsets
 82  * match.<p>
 83  *
 84  * When the id "local" is given or the offset option is specified, this class will
 85  * have the following behaviours:
 86  * <ul>
 87  * <li>The display name will always be given as the RFC822 style, no matter what
 88  * style is requested
 89  * <li>The id will also be returned as the RFC822 style display name
 90  * <li>When the offset is explicitly given, this class will assume the time zone
 91  * does not support daylight savings time, and the offsets will be calculated
 92  * the same way year round.
 93  * <li>When the offset is explicitly given, the inDaylightSavings() method will
 94  * always return false.
 95  * <li>When the id "local" is given, this class will attempt to determine the
 96  * daylight savings time settings by examining the offset from UTC on Jan 1
 97  * and June 1 of the current year. If they are different, this class assumes
 98  * that the local time zone uses DST. When the offset for a particular date is
 99  * requested, it will use the built-in Javascript support to determine the
100  * offset for that date.
101  * </ul>
102  *
103  * If a more specific time zone is
104  * needed with display names and known start/stop times for DST, use the "id"
105  * property instead to specify the time zone exactly. You can perhaps ask the
106  * user which time zone they prefer so that your app does not need to guess.<p>
107  *
108  * If the id and the offset are both not given, the default time zone for the
109  * locale is retrieved from
110  * the locale info. If the locale is not specified, the default locale for the
111  * library is used.<p>
112  *
113  * Because this class was designed for use in web sites, and the vast majority
114  * of dates and times being formatted are recent date/times, this class is simplified
115  * by not implementing historical time zones. That is, when governments change the
116  * time zone rules for a particular zone, only the latest such rule is implemented
117  * in this class. That means that determining the offset for a date that is prior
118  * to the last change may give the wrong result. Historical time zone calculations
119  * may be implemented in a later version of iLib if there is enough demand for it,
120  * but it would entail a much larger set of time zone data that would have to be
121  * loaded.
122  *
123  *
124  * @constructor
125  * @param {Object} options Options guiding the construction of this time zone instance
126  */
127 var TimeZone = function(options) {
128     this.sync = true;
129     this.locale = new Locale();
130     this.isLocal = false;
131 
132     if (options) {
133         if (options.locale) {
134             this.locale = (typeof(options.locale) === 'string') ? new Locale(options.locale) : options.locale;
135         }
136 
137         if (options.id) {
138             var id = options.id.toString();
139             if (id === 'local') {
140                 this.isLocal = true;
141 
142                 // use standard Javascript Date to figure out the time zone offsets
143                 var now = new Date(),
144                     jan1 = new Date(now.getFullYear(), 0, 1),  // months in std JS Date object are 0-based
145                     jun1 = new Date(now.getFullYear(), 5, 1);
146 
147                 // Javascript's method returns the offset backwards, so we have to
148                 // take the negative to get the correct offset
149                 this.offsetJan1 = -jan1.getTimezoneOffset();
150                 this.offsetJun1 = -jun1.getTimezoneOffset();
151                 // the offset of the standard time for the time zone is always the one that is closest
152                 // to negative infinity of the two, no matter whether you are in the northern or southern
153                 // hemisphere, east or west
154                 this.offset = Math.min(this.offsetJan1, this.offsetJun1);
155             }
156             this.id = id;
157         } else if (options.offset) {
158             this.offset = (typeof(options.offset) === 'string') ? parseInt(options.offset, 10) : options.offset;
159             this.id = this.getDisplayName(undefined, undefined);
160         }
161 
162         if (typeof(options.sync) !== 'undefined') {
163             this.sync = !!options.sync;
164         }
165 
166         this.loadParams = options.loadParams;
167         this.onLoad = options.onLoad;
168     }
169 
170     //console.log("timezone: locale is " + this.locale);
171 
172     if (!this.id) {
173         new LocaleInfo(this.locale, {
174             sync: this.sync,
175             loadParams: this.loadParams,
176             onLoad: ilib.bind(this, function (li) {
177                 this.id = li.getTimeZone() || "Etc/UTC";
178                 this._loadtzdata();
179             })
180         });
181     } else {
182         this._loadtzdata();
183     }
184 
185     //console.log("localeinfo is: " + JSON.stringify(this.locinfo));
186     //console.log("id is: " + JSON.stringify(this.id));
187 };
188 
189 /*
190  * Explanation of the compressed time zone info properties.
191  * {
192  *     "o": "8:0",      // offset from UTC
193  *     "f": "W{c}T",    // standard abbreviation. For time zones that observe DST, the {c} replacement is replaced with the
194  *                      // letter in the e.c or s.c properties below
195  *     "e": {           // info about the end of DST
196  *         "j": 78322.5 // Julian day when the transition happens. Either specify the "j" property or all of the "m", "r", and
197  *                      // "t" properties, but not both sets.
198  *         "m": 3,      // month that it ends
199  *         "r": "l0",   // rule for the day it ends "l" = "last", numbers are Sun=0 through Sat=6. Other syntax is "0>7".
200  *                      // This means the 0-day (Sun) after the 7th of the month. Other possible operators are <, >, <=, >=
201  *         "t": "2:0",  // time of day that the DST turns off, hours:minutes
202  *         "c": "S"     // character to replace into the abbreviation for standard time
203  *     },
204  *     "s": {           // info about the start of DST
205  *         "j": 78189.5 // Julian day when the transition happens. Either specify the "j" property or all of the "m", "r", and
206  *                      // "t" properties, but not both sets.
207  *         "m": 10,     // month that it starts
208  *         "r": "l0",   // rule for the day it starts "l" = "last", numbers are Sun=0 through Sat=6. Other syntax is "0>7".
209  *                      // This means the 0-day (Sun) after the 7th of the month. Other possible operators are <, >, <=, >=
210  *         "t": "2:0",  // time of day that the DST turns on, hours:minutes
211  *         "v": "1:0",  // amount of time saved in hours:minutes
212  *         "c": "D"     // character to replace into the abbreviation for daylight time
213  *     },
214  *     "c": "AU",       // ISO code for the country that contains this time zone
215  *     "n": "W. Australia {c} Time"
216  *                      // long English name of the zone. The {c} replacement is for the word "Standard" or "Daylight" as appropriate
217  * }
218  */
219 TimeZone.prototype._loadtzdata = function () {
220     var zoneName = this.id.replace(/-/g, "m").replace(/\+/g, "p");
221     // console.log("id is: " + JSON.stringify(this.id));
222     // console.log("zoneinfo is: " + JSON.stringify(ilib.data.zoneinfo[zoneName]));
223     if (!ilib.data.zoneinfo[zoneName] && typeof(this.offset) === 'undefined') {
224         Utils.loadData({
225             object: "TimeZone",
226             nonlocale: true,    // locale independent
227             name: "zoneinfo/" + this.id + ".json",
228             sync: this.sync,
229             loadParams: this.loadParams,
230             callback: ilib.bind(this, function (tzdata) {
231                 if (tzdata && !JSUtils.isEmpty(tzdata)) {
232                     ilib.data.zoneinfo[zoneName] = tzdata;
233                 }
234                 this._initZone(zoneName);
235             })
236         });
237     } else {
238         this._initZone(zoneName);
239     }
240 };
241 
242 TimeZone.prototype._initZone = function(zoneName) {
243     /**
244      * @private
245      * @type {{o:string,f:string,e:Object.<{m:number,r:string,t:string,z:string}>,s:Object.<{m:number,r:string,t:string,z:string,v:string,c:string}>,c:string,n:string}}
246      */
247     this.zone = ilib.data.zoneinfo[zoneName];
248     if (!this.zone && typeof(this.offset) === 'undefined') {
249         this.id = "Etc/UTC";
250         this.zone = ilib.data.zoneinfo[this.id];
251     }
252 
253     this._calcDSTSavings();
254 
255     if (typeof(this.offset) === 'undefined' && this.zone.o) {
256         var offsetParts = this._offsetStringToObj(this.zone.o);
257         /**
258          * @private
259          * @type {number} raw offset from UTC without DST, in minutes
260          */
261         this.offset = (Math.abs(offsetParts.h || 0) * 60 + (offsetParts.m || 0)) * MathUtils.signum(offsetParts.h || 0);
262     }
263 
264     if (this.onLoad && typeof(this.onLoad) === 'function') {
265         this.onLoad(this);
266     }
267 };
268 
269 /** @private */
270 TimeZone._marshallIds = function (country, sync, callback) {
271     var tz, ids = [];
272 
273     if (!country) {
274         // local is a special zone meaning "the local time zone according to the JS engine we are running upon"
275         ids.push("local");
276         for (tz in ilib.data.timezones) {
277             if (ilib.data.timezones[tz]) {
278                 ids.push(ilib.data.timezones[tz]);
279             }
280         }
281         if (typeof(callback) === 'function') {
282             callback(ids);
283         }
284     } else {
285         if (!ilib.data.zoneinfo.zonetab) {
286             Utils.loadData({
287                 object: "TimeZone",
288                 nonlocale: true,    // locale independent
289                 name: "zoneinfo/zonetab.json",
290                 sync: sync,
291                 callback: ilib.bind(this, function (tzdata) {
292                     if (tzdata) {
293                         ilib.data.zoneinfo.zonetab = tzdata;
294                     }
295 
296                     ids = ilib.data.zoneinfo.zonetab[country];
297 
298                     if (typeof(callback) === 'function') {
299                         callback(ids);
300                     }
301                 })
302             });
303         } else {
304             ids = ilib.data.zoneinfo.zonetab[country];
305             if (typeof(callback) === 'function') {
306                 callback(ids);
307             }
308         }
309     }
310 
311     return ids;
312 };
313 
314 /**
315  * Return an array of available zone ids that the constructor knows about.
316  * The country parameter is optional. If it is not given, all time zones will
317  * be returned. If it specifies a country code, then only time zones for that
318  * country will be returned.
319  *
320  * @param {string|undefined} country country code for which time zones are being sought
321  * @param {boolean} sync whether to find the available ids synchronously (true) or asynchronously (false)
322  * @param {function(Array.<string>)} onLoad callback function to call when the data is finished loading
323  * @return {Array.<string>} an array of zone id strings
324  */
325 TimeZone.getAvailableIds = function (country, sync, onLoad) {
326     var tz, ids = [];
327 
328     if (typeof(sync) !== 'boolean') {
329         sync = true;
330     }
331 
332     if (ilib.data.timezones.length === 0) {
333         if (typeof(ilib._load) !== 'undefined' && typeof(ilib._load.listAvailableFiles) === 'function') {
334             ilib._load.listAvailableFiles(sync, function(hash) {
335                 for (var dir in hash) {
336                     var files = hash[dir];
337                     if (ilib.isArray(files)) {
338                         files.forEach(function (filename) {
339                             if (filename && filename.match(/^zoneinfo/)) {
340                                 ilib.data.timezones.push(filename.replace(/^zoneinfo\//, "").replace(/\.json$/, ""));
341                             }
342                         });
343                     }
344                 }
345                 ids = TimeZone._marshallIds(country, sync, onLoad);
346             });
347         } else {
348             for (tz in ilib.data.zoneinfo) {
349                 if (ilib.data.zoneinfo[tz]) {
350                     ilib.data.timezones.push(tz);
351                 }
352             }
353             ids = TimeZone._marshallIds(country, sync, onLoad);
354         }
355     } else {
356         ids = TimeZone._marshallIds(country, sync, onLoad);
357     }
358 
359     return ids;
360 };
361 
362 /**
363  * Return the id used to uniquely identify this time zone.
364  * @return {string} a unique id for this time zone
365  */
366 TimeZone.prototype.getId = function () {
367     return this.id.toString();
368 };
369 
370 /**
371  * Return the abbreviation that is used for the current time zone on the given date.
372  * The date may be in DST or during standard time, and many zone names have different
373  * abbreviations depending on whether or not the date is falls within DST.<p>
374  *
375  * There are two styles that are supported:
376  *
377  * <ol>
378  * <li>standard - returns the 3 to 5 letter abbreviation of the time zone name such
379  * as "CET" for "Central European Time" or "PDT" for "Pacific Daylight Time"
380  * <li>rfc822 - returns an RFC 822 style time zone specifier, which specifies more
381  * explicitly what the offset is from UTC
382  * <li>long - returns the long name of the zone in English
383  * </ol>
384  *
385  * @param {IDate|Object|JulianDay|Date|string|number=} date a date to determine if it is in daylight time or standard time
386  * @param {string=} style one of "standard" or "rfc822". Default if not specified is "standard"
387  * @return {string} the name of the time zone, abbreviated according to the style
388  */
389 TimeZone.prototype.getDisplayName = function (date, style) {
390     var temp;
391     style = (this.isLocal || typeof(this.zone) === 'undefined') ? "rfc822" : (style || "standard");
392     switch (style) {
393         default:
394         case 'standard':
395             if (this.zone.f && this.zone.f !== "zzz") {
396                 if (this.zone.f.indexOf("{c}") !== -1) {
397                     var letter = "";
398                     letter = this.inDaylightTime(date) ? this.zone.s && this.zone.s.c : this.zone.e && this.zone.e.c;
399                     temp = new IString(this.zone.f);
400                     return temp.format({c: letter || ""});
401                 }
402                 return this.zone.f;
403             }
404             temp = "GMT" + this.zone.o;
405             if (this.inDaylightTime(date)) {
406                 temp += "+" + this.zone.s.v;
407             }
408             return temp;
409 
410         case 'rfc822':
411             var offset = this.getOffset(date), // includes the DST if applicable
412                 ret = "UTC",
413                 hour = offset.h || 0,
414                 minute = offset.m || 0;
415 
416             if (hour !== 0) {
417                 ret += (hour > 0) ? "+" : "-";
418                 if (Math.abs(hour) < 10) {
419                     ret += "0";
420                 }
421                 ret += (hour < 0) ? -hour : hour;
422                 if (minute < 10) {
423                     ret += "0";
424                 }
425                 ret += minute;
426             }
427             return ret;
428 
429         case 'long':
430             if (this.zone.n) {
431                 if (this.zone.n.indexOf("{c}") !== -1) {
432                     var str = this.inDaylightTime(date) ? "Daylight" : "Standard";
433                     temp = new IString(this.zone.n);
434                     return temp.format({c: str || ""});
435                 }
436                 return this.zone.n;
437             }
438             temp = "GMT" + this.zone.o;
439             if (this.inDaylightTime(date)) {
440                 temp += "+" + this.zone.s.v;
441             }
442             return temp;
443     }
444 };
445 
446 /**
447  * Convert the offset string to an object with an h, m, and possibly s property
448  * to indicate the hours, minutes, and seconds.
449  *
450  * @private
451  * @param {string} str the offset string to convert to an object
452  * @return {Object.<{h:number,m:number,s:number}>} an object giving the offset for the zone at
453  * the given date/time, in hours, minutes, and seconds
454  */
455 TimeZone.prototype._offsetStringToObj = function (str) {
456     var offsetParts = (typeof(str) === 'string') ? str.split(":") : [],
457         ret = {h:0},
458         temp;
459 
460     if (offsetParts.length > 0) {
461         ret.h = parseInt(offsetParts[0], 10);
462         if (offsetParts.length > 1) {
463             temp = parseInt(offsetParts[1], 10);
464             if (temp) {
465                 ret.m = temp;
466             }
467             if (offsetParts.length > 2) {
468                 temp = parseInt(offsetParts[2], 10);
469                 if (temp) {
470                     ret.s = temp;
471                 }
472             }
473         }
474     }
475 
476     return ret;
477 };
478 
479 /**
480  * Returns the offset of this time zone from UTC at the given date/time. If daylight saving
481  * time is in effect at the given date/time, this method will return the offset value
482  * adjusted by the amount of daylight saving.
483  * @param {IDate|Object|JulianDay|Date|string|number=} date the date for which the offset is needed
484  * @return {Object.<{h:number,m:number}>} an object giving the offset for the zone at
485  * the given date/time, in hours, minutes, and seconds
486  */
487 TimeZone.prototype.getOffset = function (date) {
488     if (!date) {
489         return this.getRawOffset();
490     }
491     var offset = this.getOffsetMillis(date)/60000;
492 
493     var hours = MathUtils.down(offset/60),
494         minutes = Math.abs(offset) - Math.abs(hours)*60;
495 
496     var ret = {
497         h: hours
498     };
499     if (minutes !== 0) {
500         ret.m = minutes;
501     }
502     return ret;
503 };
504 
505 /**
506  * Returns the offset of this time zone from UTC at the given date/time expressed in
507  * milliseconds. If daylight saving
508  * time is in effect at the given date/time, this method will return the offset value
509  * adjusted by the amount of daylight saving. Negative numbers indicate offsets west
510  * of UTC and conversely, positive numbers indicate offset east of UTC.
511  *
512  * @param {IDate|Object|JulianDay|Date|string|number=} date the date for which the offset is needed, or null for the
513  * present date
514  * @return {number} the number of milliseconds of offset from UTC that the given date is
515  */
516 TimeZone.prototype.getOffsetMillis = function (date) {
517     var ret;
518 
519     // check if the dst property is defined -- the intrinsic JS Date object doesn't work so
520     // well if we are in the overlap time at the end of DST
521     if (this.isLocal && typeof(date.dst) === 'undefined') {
522         var d = (!date) ? new Date() : new Date(date.getTimeExtended());
523         return -d.getTimezoneOffset() * 60000;
524     }
525 
526     ret = this.offset;
527 
528     if (date && this.inDaylightTime(date)) {
529         ret += this.dstSavings;
530     }
531 
532     return ret * 60000;
533 };
534 
535 /**
536  * Return the offset in milliseconds when the date has an RD number in wall
537  * time rather than in UTC time.
538  * @protected
539  * @param {IDate|Object|JulianDay|Date|string|number} date the date to check in wall time
540  * @returns {number} the number of milliseconds of offset from UTC that the given date is
541  */
542 TimeZone.prototype._getOffsetMillisWallTime = function (date) {
543     var ret;
544 
545     ret = this.offset;
546 
547     if (date && this.inDaylightTime(date, true)) {
548         ret += this.dstSavings;
549     }
550 
551     return ret * 60000;
552 };
553 
554 /**
555  * Returns the offset of this time zone from UTC at the given date/time. If daylight saving
556  * time is in effect at the given date/time, this method will return the offset value
557  * adjusted by the amount of daylight saving.
558  * @param {IDate|Object|JulianDay|Date|string|number=} date the date for which the offset is needed
559  * @return {string} the offset for the zone at the given date/time as a string in the
560  * format "h:m:s"
561  */
562 TimeZone.prototype.getOffsetStr = function (date) {
563     var offset = this.getOffset(date),
564         ret;
565 
566     ret = offset.h;
567     if (typeof(offset.m) !== 'undefined') {
568         ret += ":" + offset.m;
569         if (typeof(offset.s) !== 'undefined') {
570             ret += ":" + offset.s;
571         }
572     } else {
573         ret += ":0";
574     }
575 
576     return ret;
577 };
578 
579 /**
580  * Gets the offset from UTC for this time zone.
581  * @return {Object.<{h:number,m:number,s:number}>} an object giving the offset from
582  * UTC for this time zone, in hours, minutes, and seconds
583  */
584 TimeZone.prototype.getRawOffset = function () {
585     var hours = MathUtils.down(this.offset/60),
586         minutes = Math.abs(this.offset) - Math.abs(hours)*60;
587 
588     var ret = {
589         h: hours
590     };
591     if (minutes != 0) {
592         ret.m = minutes;
593     }
594     return ret;
595 };
596 
597 /**
598  * Gets the offset from UTC for this time zone expressed in milliseconds. Negative numbers
599  * indicate zones west of UTC, and positive numbers indicate zones east of UTC.
600  *
601  * @return {number} an number giving the offset from
602  * UTC for this time zone in milliseconds
603  */
604 TimeZone.prototype.getRawOffsetMillis = function () {
605     return this.offset * 60000;
606 };
607 
608 /**
609  * Gets the offset from UTC for this time zone without DST savings.
610  * @return {string} the offset from UTC for this time zone, in the format "h:m:s"
611  */
612 TimeZone.prototype.getRawOffsetStr = function () {
613     var off = this.getRawOffset();
614     return off.h + ":" + (off.m || "0");
615 };
616 
617 /**
618  * Return the amount of time in hours:minutes that the clock is advanced during
619  * daylight savings time.
620  * @return {Object.<{h:number,m:number,s:number}>} the amount of time that the
621  * clock advances for DST in hours, minutes, and seconds
622  */
623 TimeZone.prototype.getDSTSavings = function () {
624     if (this.isLocal) {
625         // take the absolute because the difference in the offsets may be positive or
626         // negative, depending on the hemisphere
627         var savings = Math.abs(this.offsetJan1 - this.offsetJun1);
628         var hours = MathUtils.down(savings/60),
629             minutes = savings - hours*60;
630         return {
631             h: hours,
632             m: minutes
633         };
634     } else if (this.zone && this.zone.s) {
635         return this._offsetStringToObj(this.zone.s.v);    // this.zone.start.savings
636     }
637     return {h:0};
638 };
639 
640 /**
641  * Return the amount of time in hours:minutes that the clock is advanced during
642  * daylight savings time.
643  * @return {string} the amount of time that the clock advances for DST in the
644  * format "h:m:s"
645  */
646 TimeZone.prototype.getDSTSavingsStr = function () {
647     if (this.isLocal) {
648         var savings = this.getDSTSavings();
649         return savings.h + ":" + savings.m;
650     } else if (typeof(this.offset) !== 'undefined' && this.zone && this.zone.s) {
651         return this.zone.s.v;    // this.zone.start.savings
652     }
653     return "0:0";
654 };
655 
656 /**
657  * return the rd of the start of DST transition for the given year
658  * @protected
659  * @param {Object} rule set of rules
660  * @param {number} year year to check
661  * @return {number} the rd of the start of DST for the year
662  */
663 TimeZone.prototype._calcRuleStart = function (rule, year) {
664     var type = "=",
665         weekday = 0,
666         day,
667         refDay,
668         cal,
669         hour = 0,
670         minute = 0,
671         second = 0,
672         time,
673         i;
674 
675     if (typeof(rule.j) !== 'undefined') {
676         refDay = new GregRataDie({
677             julianday: rule.j
678         });
679     } else {
680         if (rule.r.charAt(0) === 'l' || rule.r.charAt(0) === 'f') {
681             cal = CalendarFactory({type: "gregorian"}); // can be synchronous
682             type = rule.r.charAt(0);
683             weekday = parseInt(rule.r.substring(1), 10);
684             day = (type === 'l') ? cal.getMonLength(rule.m, year) : 1;
685             //console.log("_calcRuleStart: Calculating the " +
686             //        (rule.r.charAt(0) == 'f' ? "first " : "last ") + weekday +
687             //        " of month " + rule.m);
688         } else {
689             i = rule.r.indexOf('<');
690             if (i === -1) {
691                 i = rule.r.indexOf('>');
692             }
693 
694             if (i !== -1) {
695                 type = rule.r.charAt(i);
696                 weekday = parseInt(rule.r.substring(0, i), 10);
697                 day = parseInt(rule.r.substring(i+1), 10);
698                 //console.log("_calcRuleStart: Calculating the " + weekday +
699                 //        type + day + " of month " + rule.m);
700             } else {
701                 day = parseInt(rule.r, 10);
702                 //console.log("_calcRuleStart: Calculating the " + day + " of month " + rule.m);
703             }
704         }
705 
706         if (rule.t) {
707             time = rule.t.split(":");
708             hour = parseInt(time[0], 10);
709             if (time.length > 1) {
710                 minute = parseInt(time[1], 10);
711                 if (time.length > 2) {
712                     second = parseInt(time[2], 10);
713                 }
714             }
715         }
716         //console.log("calculating rd of " + year + "/" + rule.m + "/" + day);
717         refDay = new GregRataDie({
718             year: year,
719             month: rule.m,
720             day: day,
721             hour: hour,
722             minute: minute,
723             second: second
724         });
725     }
726     //console.log("refDay is " + JSON.stringify(refDay));
727     var d = refDay.getRataDie();
728 
729     switch (type) {
730         case 'l':
731         case '<':
732             //console.log("returning " + refDay.onOrBefore(rd, weekday));
733             d = refDay.onOrBefore(weekday);
734             break;
735         case 'f':
736         case '>':
737             //console.log("returning " + refDay.onOrAfterRd(rd, weekday));
738             d = refDay.onOrAfter(weekday);
739             break;
740     }
741     return d;
742 };
743 
744 /**
745  * @private
746  */
747 TimeZone.prototype._calcDSTSavings = function () {
748     var saveParts = this.getDSTSavings();
749 
750     /**
751      * @private
752      * @type {number} savings in minutes when DST is in effect
753      */
754     this.dstSavings = (Math.abs(saveParts.h || 0) * 60 + (saveParts.m || 0)) * MathUtils.signum(saveParts.h || 0);
755 };
756 
757 /**
758  * @private
759  */
760 TimeZone.prototype._getDSTStartRule = function (year) {
761     // TODO: update this when historic/future zones are supported
762     return this.zone.s;
763 };
764 
765 /**
766  * @private
767  */
768 TimeZone.prototype._getDSTEndRule = function (year) {
769     // TODO: update this when historic/future zones are supported
770     return this.zone.e;
771 };
772 
773 /**
774  * Returns whether or not the given date is in daylight saving time for the current
775  * zone. Note that daylight savings time is observed for the summer. Because
776  * the seasons are reversed, daylight savings time in the southern hemisphere usually
777  * runs from the end of the year through New Years into the first few months of the
778  * next year. This method will correctly calculate the start and end of DST for any
779  * location.
780  *
781  * @param {IDate|Object|JulianDay|Date|string|number=} date a date for which the info about daylight time is being sought,
782  * or undefined to tell whether we are currently in daylight savings time
783  * @param {boolean=} wallTime if true, then the given date is in wall time. If false or
784  * undefined, it is in the usual UTC time.
785  * @return {boolean} true if the given date is in DST for the current zone, and false
786  * otherwise.
787  */
788 TimeZone.prototype.inDaylightTime = function (date, wallTime) {
789     var rd, startRd, endRd, year;
790     if (date) {
791         // need an IDate instance, so convert as necessary
792         date = DateFactory._dateToIlib(date, this.id, this.locale);
793     }
794     if (this.isLocal) {
795         // check if the dst property is defined -- the intrinsic JS Date object doesn't work so
796         // well if we are in the overlap time at the end of DST, so we have to work around that
797         // problem by adding in the savings ourselves
798         var offset = this.offset * 60000;
799         if (typeof(date.dst) !== 'undefined' && !date.dst) {
800             offset += this.dstSavings * 60000;
801         }
802 
803         var d = new Date(date ? date.getTimeExtended() - offset: undefined);
804         // the DST offset is always the one that is closest to positive infinity, no matter
805         // if you are in the northern or southern hemisphere, east or west
806         var dst = Math.max(this.offsetJan1, this.offsetJun1);
807         return (-d.getTimezoneOffset() === dst);
808     }
809 
810     if (!date || !date.cal || date.cal.type !== "gregorian") {
811         // convert to Gregorian so that we can tell if it is in DST or not
812         var time = date && typeof(date.getTimeExtended) === 'function' ? date.getTimeExtended() : undefined;
813         rd = new GregRataDie({unixtime: time}).getRataDie();
814         year = new Date(time).getUTCFullYear();
815     } else {
816         rd = date.rd.getRataDie();
817         year = date.year;
818     }
819     // rd should be a Gregorian RD number now, in UTC
820 
821     // if we aren't using daylight time in this zone for the given year, then we are
822     // not in daylight time
823     if (!this.useDaylightTime(year)) {
824         return false;
825     }
826 
827     // these calculate the start/end in local wall time
828     var startrule = this._getDSTStartRule(year);
829     var endrule = this._getDSTEndRule(year);
830     startRd = this._calcRuleStart(startrule, year);
831     endRd = this._calcRuleStart(endrule, year);
832 
833     if (wallTime) {
834         // rd is in wall time, so we have to make sure to skip the missing time
835         // at the start of DST when standard time ends and daylight time begins
836         startRd += this.dstSavings/1440;
837     } else {
838         // rd is in UTC, so we have to convert the start/end to UTC time so
839         // that they can be compared directly to the UTC rd number of the date
840 
841         // when DST starts, time is standard time already, so we only have
842         // to subtract the offset to get to UTC and not worry about the DST savings
843         startRd -= this.offset/1440;
844 
845         // when DST ends, time is in daylight time already, so we have to
846         // subtract the DST savings to get back to standard time, then the
847         // offset to get to UTC
848         endRd -= (this.offset + this.dstSavings)/1440;
849     }
850 
851     // In the northern hemisphere, the start comes first some time in spring (Feb-Apr),
852     // then the end some time in the fall (Sept-Nov). In the southern
853     // hemisphere, it is the other way around because the seasons are reversed. Standard
854     // time is still in the winter, but the winter months are May-Aug, and daylight
855     // savings time usually starts Aug-Oct of one year and runs through Mar-May of the
856     // next year.
857     if (rd < endRd && endRd - rd <= this.dstSavings/1440 && typeof(date.dst) === 'boolean') {
858         // take care of the magic overlap time at the end of DST
859         return date.dst;
860     }
861     if (startRd < endRd) {
862         // northern hemisphere
863         return (rd >= startRd && rd < endRd) ? true : false;
864     }
865     // southern hemisphere
866     return (rd >= startRd || rd < endRd) ? true : false;
867 };
868 
869 /**
870  * Returns true if this time zone switches to daylight savings time at some point
871  * in the year, and false otherwise.
872  * @param {number} year Whether or not the time zone uses daylight time in the given year. If
873  * this parameter is not given, the current year is assumed.
874  * @return {boolean} true if the time zone uses daylight savings time
875  */
876 TimeZone.prototype.useDaylightTime = function (year) {
877 
878     // this zone uses daylight savings time iff there is a rule defining when to start
879     // and when to stop the DST
880     return (this.isLocal && this.offsetJan1 !== this.offsetJun1) ||
881         (typeof(this.zone) !== 'undefined' &&
882         typeof(this.zone.s) !== 'undefined' &&
883         typeof(this.zone.e) !== 'undefined');
884 };
885 
886 /**
887  * Returns the ISO 3166 code of the country for which this time zone is defined.
888  * @return {string} the ISO 3166 code of the country for this zone
889  */
890 TimeZone.prototype.getCountry = function () {
891     return this.zone.c;
892 };
893 
894 module.exports = TimeZone;
895