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