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