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