1 /* 2 * ***** BEGIN LICENSE BLOCK ***** 3 * Zimbra Collaboration Suite Web Client 4 * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. 5 * 6 * The contents of this file are subject to the Common Public Attribution License Version 1.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: https://www.zimbra.com/license 9 * The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 10 * have been added to cover use of software over a computer network and provide for limited attribution 11 * for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 12 * 13 * Software distributed under the License is distributed on an "AS IS" basis, 14 * WITHOUT WARRANTY OF ANY KIND, either express or implied. 15 * See the License for the specific language governing rights and limitations under the License. 16 * The Original Code is Zimbra Open Source Web Client. 17 * The Initial Developer of the Original Code is Zimbra, Inc. All rights to the Original Code were 18 * transferred by Zimbra, Inc. to Synacor, Inc. on September 14, 2015. 19 * 20 * All portions of the code are Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2016 Synacor, Inc. All Rights Reserved. 21 * ***** END LICENSE BLOCK ***** 22 */ 23 24 /** 25 * This class holds all of the known timezone rules. Each timezone 26 * is represented by an object that has a unique identifier and the 27 * offset to UTC in standard time. If the timezone defines daylight 28 * savings time, then additional information is provided (e.g. when 29 * the DST takes effect and it's offset to UTC). 30 * <p> 31 * Both the standard and daylight information are specified as objects 32 * with the following properties: 33 * <dl> 34 * <dt> offset 35 * <dd> The offset to UTC (in minutes) 36 * <dt> mon 37 * <dd> The transition month of the year (January = 1, ...). 38 * <dt> week 39 * <dd> The transition week of the month (First = 1, ..., Fourth = 4, 40 * Last = -1). 41 * <dt> wkday 42 * <dd> The transition day of the week (Sunday = 1, ...). 43 * <dt> mday 44 * <dd> The transition day of the month (1, ... 31). 45 * <dt> hour 46 * <dd> The transition hour (midnight = 0, noon = 12, ...). 47 * <dt> min 48 * <dd> The transition minute. 49 * <dt> sec 50 * <dt> The transition second (which is usually 0). 51 * </dl> 52 * 53 * <h5>Notes</h5> 54 * <ul> 55 * <li> Timezones with no DST only specify an id and a standard info 56 * object with a single "offset" property. 57 * <li> Timezones with a DST <em>must</em> provide standard and 58 * daylight info objects. 59 * <li> If timezone has DST, then the following properties of the 60 * standard and daylight info objects are <em>required</em>: 61 * offset, month, hour, min, sec. 62 * <li> Transition dates are specified in only one of the following ways: 63 * <ul> 64 * <li> by specifying a specific date of the year (e.g. March 10); 65 * <li> or by specifying the day of a specific week within some 66 * month (e.g. Second Wednesday, Last Saturday, etc). 67 * </ul> 68 * <li> If the transition date is specified as a specific date of the 69 * year, then the following field in the standard and/or daylight 70 * info objects are <em>required</em>: mday. 71 * <li> If the transition date is specified as the day of a specific 72 * week, then the following fields in the standard and/or daylight 73 * info objects are <em>required</em>: week, wkday. 74 * </ul> 75 * 76 * <h5>Examples</h5> 77 * <dl> 78 * <dt> Timezone with no DST 79 * <dd> 80 * <pre> 81 * var timezone = { clientId: "My Timezone", standard: { offset: -480 } }; 82 * </pre> 83 * <dt> America/Los_Angeles, 2007 84 * <dd> 85 * <pre> 86 * var standard = { 87 * offset: -480, 88 * mon: 11, week: 1, wkday: 1, 89 * hour: 2, min: 0, sec: 0 90 * }; 91 * var daylight = { 92 * offset: -420, 93 * mon: 3, week: 2, wkday: 1, 94 * hour: 2, min: 0, sec: 0 95 * }; 96 * var timezone = { clientId: "My Timezone", 97 * standard: standard, daylight: daylight }; 98 * </pre> 99 * <dt> Custom US/Pacific using 11 Mar 2007 and 2 Dec 2007 100 * <dd> 101 * <pre> 102 * var standard = { 103 * offset: -480, 104 * mon: 11, mday: 2, 105 * hour: 2, min: 0, sec: 0 106 * }; 107 * var daylight = { 108 * offset: -420, 109 * mon: 3, mday: 11, 110 * hour: 2, min: 0, sec: 0 111 * }; 112 * var timezone = { clientId: "My Timezone", 113 * standard: standard, daylight: daylight }; 114 * </pre> 115 * </dl> 116 * <p> 117 * <strong>Note:</strong> 118 * Specifying a transition date using a specific date of the year 119 * <em>should</em> be avoided. 120 * 121 * <hr> 122 * 123 * <p> 124 * This class stores mappings between client and server identifiers for 125 * timezones as well as attempting to guess the default timezone. The 126 * application can override this value, through a user preference perhaps, 127 * by setting the <code>DEFAULT</code> property's value. The default 128 * timezone is specified using the client identifier. 129 * 130 * @class 131 */ 132 AjxTimezone = function() {}; 133 134 // 135 // Static methods 136 // 137 138 AjxTimezone.convertTimezone = function(date, fromClientId, toClientId) { 139 if (fromClientId == toClientId) { 140 return date; 141 } 142 var offset1 = AjxTimezone.getOffset(toClientId, date); 143 var offset2 = AjxTimezone.getOffset(fromClientId, date); 144 //returning a new Date object since we might not always want to modify the parameter Date object 145 return new Date(date.getTime() + (offset1 - offset2) * 60 * 1000); 146 }; 147 148 149 AjxTimezone.getTransition = function(onset, year) { 150 var trans = [ year || new Date().getFullYear(), onset.mon, 1 ]; 151 if (onset.mday) { 152 trans[2] = onset.mday; 153 } 154 else if (onset.wkday) { 155 var date = new Date(year, onset.mon - 1, 1, onset.hour, onset.min, onset.sec); 156 157 // last wkday of month 158 if (onset.week == -1) { 159 // NOTE: This creates a date of the *last* day of specified month by 160 // setting the month to *next* month and setting day of month 161 // to zero (i.e. the day *before* the first day). 162 var last = new Date(new Date(date.getTime()).setMonth(onset.mon, 0)); 163 var count = last.getDate(); 164 var wkday = last.getDay() + 1; 165 var adjust = wkday >= onset.wkday ? wkday - onset.wkday : 7 - onset.wkday - wkday; 166 trans[2] = count - adjust; 167 } 168 169 // Nth wkday of month 170 else { 171 var wkday = date.getDay() + 1; 172 var adjust = onset.wkday == wkday ? 1 :0; 173 trans[2] = onset.wkday + 7 * (onset.week - adjust) - wkday + 1; 174 } 175 } 176 return trans; 177 }; 178 179 AjxTimezone.createMDayTransition = function(date, offset) { 180 if (date instanceof Date) { 181 offset = offset != null ? offset : date.getTimezoneOffset(); 182 date = [ 183 date.getFullYear(), date.getMonth() + 1, date.getDate(), 184 date.getHours(), date.getMinutes(), date.getSeconds() 185 ]; 186 } 187 var onset = { offset: offset, trans: date }; 188 return AjxTimezone.addMDayTransition(onset); 189 }; 190 191 AjxTimezone.addMDayTransition = function(onset) { 192 var trans = onset.trans; 193 onset.mon = trans[1]; 194 onset.mday = trans[2]; 195 onset.hour = trans[3]; 196 onset.min = trans[4]; 197 onset.sec = trans[5]; 198 return onset; 199 }; 200 201 AjxTimezone.createWkDayTransition = function (date, offset) { 202 if (date instanceof Date) { 203 offset = offset != null ? offset : date.getTimezoneOffset(); 204 date = [ 205 date.getFullYear(), date.getMonth() + 1, date.getDate(), 206 date.getHours(), date.getMinutes(), date.getSeconds() 207 ]; 208 } 209 var onset = { offset: offset, trans: date }; 210 return AjxTimezone.addWkDayTransition(onset); 211 }; 212 213 AjxTimezone.addWkDayTransition = function(onset) { 214 var trans = onset.trans; 215 var mon = trans[1]; 216 var monDay = trans[2]; 217 var week = Math.floor((monDay - 1) / 7); 218 var date = new Date(trans[0], trans[1] - 1, trans[2], 12, 0, 0); 219 220 // NOTE: This creates a date of the *last* day of specified month by 221 // setting the month to *next* month and setting day of month 222 // to zero (i.e. the day *before* the first day). 223 var count = new Date(new Date(date.getTime()).setMonth(mon - 1, 0)).getDate(); 224 var last = count - monDay < 7; 225 226 // set onset values 227 onset.mon = mon; 228 onset.week = last ? -1 : week + 1; 229 onset.wkday = date.getDay() + 1; 230 onset.hour = trans[3]; 231 onset.min = trans[4]; 232 onset.sec = trans[5]; 233 return onset; 234 }; 235 236 AjxTimezone.createTransitionDate = function(onset) { 237 var date = new Date(AjxTimezoneData.TRANSITION_YEAR, onset.mon - 1, 1, 12, 0, 0); 238 if (onset.mday) { 239 date.setDate(onset.mday); 240 } 241 else if (onset.week == -1) { 242 date.setMonth(date.getMonth() + 1, 0); 243 for (var i = 0; i < 7; i++) { 244 if (date.getDay() + 1 == onset.wkday) { 245 break; 246 } 247 date.setDate(date.getDate() - 1); 248 } 249 } 250 else { 251 for (var i = 0; i < 7; i++) { 252 if (date.getDay() + 1 == onset.wkday) { 253 break; 254 } 255 date.setDate(date.getDate() + 1); 256 } 257 date.setDate(date.getDate() + 7 * (onset.week - 1)); 258 } 259 var trans = [ date.getFullYear(), date.getMonth() + 1, date.getDate() ]; 260 return trans; 261 }; 262 263 AjxTimezone.getZonePreferences = 264 function() { 265 if (AjxTimezone._PREF_ZONE_DISPLAY) { 266 var count = AjxTimezone._PREF_ZONE_DISPLAY.length; 267 var total = AjxTimezone.STANDARD_RULES.length + AjxTimezone.DAYLIGHT_RULES.length; 268 if (count != total) { 269 AjxTimezone._PREF_ZONE_DISPLAY = null; 270 } 271 } 272 273 if (!AjxTimezone._PREF_ZONE_DISPLAY) { 274 AjxTimezone._PREF_ZONE_DISPLAY = []; 275 AjxTimezone.getAbbreviatedZoneChoices(); 276 for (var i = 0; i < AjxTimezone._ABBR_ZONE_OPTIONS.length; i++) { 277 AjxTimezone._PREF_ZONE_DISPLAY.push(AjxTimezone._ABBR_ZONE_OPTIONS[i].displayValue); 278 } 279 } 280 return AjxTimezone._PREF_ZONE_DISPLAY; 281 }; 282 283 AjxTimezone.getZonePreferencesOptions = 284 function() { 285 if (AjxTimezone._PREF_ZONE_OPTIONS) { 286 var count = AjxTimezone._PREF_ZONE_OPTIONS.length; 287 var total = AjxTimezone.STANDARD_RULES.length + AjxTimezone.DAYLIGHT_RULES.length; 288 if (count != total) { 289 AjxTimezone._PREF_ZONE_OPTIONS = null; 290 } 291 } 292 293 if (!AjxTimezone._PREF_ZONE_OPTIONS) { 294 AjxTimezone._PREF_ZONE_OPTIONS = []; 295 AjxTimezone.getAbbreviatedZoneChoices(); 296 for (var i = 0; i < AjxTimezone._ABBR_ZONE_OPTIONS.length; i++) { 297 AjxTimezone._PREF_ZONE_OPTIONS.push(AjxTimezone._ABBR_ZONE_OPTIONS[i].value); //use value is better, serverID is usd by compare operator. 298 } 299 } 300 return AjxTimezone._PREF_ZONE_OPTIONS; 301 }; 302 303 AjxTimezone.getServerId = function(clientId) { 304 return AjxTimezone._CLIENT2SERVER[clientId] || clientId; 305 }; 306 AjxTimezone.getClientId = function(serverId) { 307 return AjxTimezone._SERVER2CLIENT[serverId] || serverId; 308 }; 309 310 AjxTimezone.getShortName = function(clientId) { 311 var rule = AjxTimezone.getRule(clientId); 312 if (rule && rule.shortName) return rule.shortName; 313 var generatedShortName = ["GMT",AjxTimezone._SHORT_NAMES[clientId]].join(""); 314 if(rule) rule.shortName = generatedShortName; 315 return generatedShortName; 316 }; 317 318 AjxTimezone.getMediumName = function(clientId) { 319 var rule = AjxTimezone.getRule(clientId); 320 if (rule && rule.mediumName) return rule.mediumName; 321 var generatedMediumName = AjxMsg[clientId] || ['(',AjxTimezone.getShortName(clientId),') ',clientId].join(""); 322 if(rule) rule.mediumName = generatedMediumName; 323 return generatedMediumName; 324 }; 325 326 AjxTimezone.getLongName = AjxTimezone.getMediumName; 327 328 AjxTimezone.addRule = function(rule) { 329 var serverId = rule.serverId; 330 var clientId = rule.clientId; 331 332 AjxTimezone._CLIENT2SERVER[clientId] = serverId; 333 AjxTimezone._SERVER2CLIENT[serverId] = clientId; 334 AjxTimezone._SHORT_NAMES[clientId] = AjxTimezone._generateShortName(rule.standard.offset); 335 AjxTimezone._CLIENT2RULE[clientId] = rule; 336 337 var array = rule.daylight ? AjxTimezone.DAYLIGHT_RULES : AjxTimezone.STANDARD_RULES; 338 array.push(rule); 339 }; 340 341 AjxTimezone.getRule = function(clientId, tz) { 342 var rule = AjxTimezone._CLIENT2RULE[clientId]; 343 if (!rule) { 344 // try to find the rule treating the clientId as the serverId 345 clientId = AjxTimezone._SERVER2CLIENT[clientId]; 346 rule = AjxTimezone._CLIENT2RULE[clientId]; 347 } 348 if (!rule && tz) { 349 var names = [ "standard", "daylight" ]; 350 var rules = tz.daylight ? AjxTimezone.DAYLIGHT_RULES : AjxTimezone.STANDARD_RULES; 351 for (var i = 0; i < rules.length; i++) { 352 rule = rules[i]; 353 354 var found = true; 355 for (var j = 0; j < names.length; j++) { 356 var name = names[j]; 357 var onset = rule[name]; 358 if (!onset) continue; 359 360 var breakOuter = false; 361 362 for (var p in tz[name]) { 363 if (tz[name][p] != onset[p]) { 364 found = false; 365 breakOuter = true; 366 break; 367 } 368 } 369 370 if(breakOuter){ 371 break; 372 } 373 } 374 if (found) { 375 return rule; 376 } 377 } 378 return null; 379 } 380 381 return rule; 382 }; 383 384 AjxTimezone.getOffset = function(clientId, date) { 385 var rule = AjxTimezone.getRule(clientId || AjxTimezone.DEFAULT); 386 if (rule && rule.daylight) { 387 var year = date.getFullYear(); 388 389 var standard = rule.standard, daylight = rule.daylight; 390 var stdTrans = AjxTimezone.getTransition(standard, year); 391 var dstTrans = AjxTimezone.getTransition(daylight, year); 392 393 var month = date.getMonth()+1, day = date.getDate(); 394 var stdMonth = stdTrans[1], stdDay = stdTrans[2]; 395 var dstMonth = dstTrans[1], dstDay = dstTrans[2]; 396 397 // northern hemisphere 398 var isDST = false; 399 if (dstMonth < stdMonth) { 400 isDST = month > dstMonth && month < stdMonth; 401 isDST = isDST || (month == dstMonth && day >= dstDay); 402 isDST = isDST || (month == stdMonth && day < stdDay); 403 } 404 405 // sorthern hemisphere 406 else { 407 isDST = month < stdMonth || month > dstMonth; 408 isDST = isDST || (month == dstMonth && day >= dstDay); 409 isDST = isDST || (month == stdMonth && day < stdDay); 410 } 411 412 return isDST ? daylight.offset : standard.offset; 413 } 414 return rule ? rule.standard.offset : -(new Date().getTimezoneOffset()); 415 }; 416 417 AjxTimezone.guessMachineTimezone = function() { 418 return AjxTimezone._guessMachineTimezone().clientId; 419 }; 420 421 AjxTimezone.getAbbreviatedZoneChoices = function() { 422 if (AjxTimezone._ABBR_ZONE_OPTIONS) { 423 var count = AjxTimezone._ABBR_ZONE_OPTIONS.length; 424 var total = AjxTimezone.STANDARD_RULES.length + AjxTimezone.DAYLIGHT_RULES.length; 425 if (count != total) { 426 AjxTimezone._ABBR_ZONE_OPTIONS = null; 427 } 428 } 429 if (!AjxTimezone._ABBR_ZONE_OPTIONS) { 430 AjxTimezone._ABBR_ZONE_OPTIONS = []; 431 for (var clientId in AjxTimezone._CLIENT2SERVER) { 432 var rule = AjxTimezone._CLIENT2RULE[clientId]; 433 var serverId = rule.serverId; 434 var option = { 435 displayValue: AjxTimezone.getMediumName(clientId), 436 value: serverId, 437 // these props used by sort comparator 438 standard: rule.standard, 439 serverId: serverId, //In _BY_OFFSET, the attribute name is serverId. 440 clientId: clientId 441 }; 442 AjxTimezone._ABBR_ZONE_OPTIONS.push(option); 443 } 444 AjxTimezone._ABBR_ZONE_OPTIONS.sort(AjxTimezone._BY_OFFSET); 445 } 446 return AjxTimezone._ABBR_ZONE_OPTIONS; 447 }; 448 449 AjxTimezone.getMatchingTimezoneChoices = function() { 450 if (AjxTimezone._MATCHING_ZONE_OPTIONS) { 451 var count = AjxTimezone._MATCHING_ZONE_OPTIONS.length; 452 var total = AjxTimezone.STANDARD_RULES.length + AjxTimezone.DAYLIGHT_RULES.length; 453 if (count != total) { 454 AjxTimezone._MATCHING_ZONE_OPTIONS = null; 455 } 456 } 457 if (!AjxTimezone._MATCHING_ZONE_OPTIONS) { 458 AjxTimezone._MATCHING_ZONE_OPTIONS = []; 459 for (var i in AjxTimezone.MATCHING_RULES) { 460 var rule = AjxTimezone.MATCHING_RULES[i]; 461 var clientId = rule.clientId; 462 var serverId = rule.serverId; 463 if(clientId == AjxTimezone.AUTO_DETECTED) continue; 464 var option = { 465 displayValue: AjxTimezone.getMediumName(clientId), 466 value: serverId, 467 // these props used by sort comparator 468 standard: rule.standard, 469 serverId: serverId, //In _BY_OFFSET, the attribute name is serverId. 470 clientId: clientId 471 }; 472 AjxTimezone._MATCHING_ZONE_OPTIONS.push(option); 473 } 474 AjxTimezone._MATCHING_ZONE_OPTIONS.sort(AjxTimezone._BY_OFFSET); 475 } 476 return AjxTimezone._MATCHING_ZONE_OPTIONS; 477 }; 478 479 AjxTimezone._BY_OFFSET = function(arule, brule) { 480 // sort by offset and then by name 481 var delta = arule.standard.offset - brule.standard.offset; 482 if (delta == 0) { 483 var aname = arule.serverId; 484 var bname = brule.serverId; 485 if (aname < bname) delta = -1; 486 else if (aname > bname) delta = 1; 487 } 488 return delta; 489 }; 490 491 // Constants 492 493 /** 494 * Client identifier for GMT. 495 * <p> 496 * <strong>Note:</strong> 497 * UK observes daylight savings time so this constant should 498 * <em>not</em> be used as the reference point (i.e. UTC) -- 499 * use {@link AjxTimezone.GMT_NO_DST} instead. The name of 500 * this constant is historical. 501 */ 502 AjxTimezone.GMT = "Europe/London"; 503 504 /** 505 * Client identifier for GMT with no daylight savings time. 506 */ 507 AjxTimezone.GMT_NO_DST = "UTC"; 508 509 /** 510 * <strong>Note:</strong> 511 * Do NOT change this value because it is used to reference messages. 512 */ 513 AjxTimezone.AUTO_DETECTED = "Auto-Detected"; 514 515 /** 516 * The default timezone is set by guessing the machine timezone later 517 * in this file. See the static initialization section below for details. 518 */ 519 520 AjxTimezone._CLIENT2SERVER = {}; 521 AjxTimezone._SERVER2CLIENT = {}; 522 AjxTimezone._SHORT_NAMES = {}; 523 AjxTimezone._CLIENT2RULE = {}; 524 525 /** 526 * The data is specified using the server identifiers for historical 527 * reasons. Perhaps in the future we'll use the client (i.e. Java) 528 * identifiers on the server as well. 529 */ 530 AjxTimezone.STANDARD_RULES = []; 531 AjxTimezone.DAYLIGHT_RULES = []; 532 (function() { 533 for (var i = 0; i < AjxTimezoneData.TIMEZONE_RULES.length; i++) { 534 var rule = AjxTimezoneData.TIMEZONE_RULES[i]; 535 var array = rule.daylight ? AjxTimezone.DAYLIGHT_RULES : AjxTimezone.STANDARD_RULES; 536 array.push(rule); 537 } 538 })(); 539 540 /** 541 * One problem with firefox, is if the timezone on the machine changes, 542 * the browser isn't updated. You have to restart firefox for it to get the 543 * new machine timezone. 544 * <p> 545 * <strong>Note:</strong> 546 * It looks like the current versions of FF always reflect the current 547 * timezone w/o needing to restart the browser. 548 * timezonePreference - optional value used to decide timezone rule in case of conflict 549 */ 550 AjxTimezone._guessMachineTimezone = 551 function(timezonePreference) { 552 var dec1 = new Date(AjxTimezoneData.TRANSITION_YEAR, 11, 1, 0, 0, 0); 553 var jun1 = new Date(AjxTimezoneData.TRANSITION_YEAR, 5, 1, 0, 0, 0); 554 var dec1offset = -dec1.getTimezoneOffset(); 555 var jun1offset = -jun1.getTimezoneOffset(); 556 557 AjxTimezone.MATCHING_RULES = []; 558 AjxTimezone.TIMEZONE_CONFLICT = false; 559 var matchingRules = []; 560 var matchingRulesMap = {}; 561 var offsetMatchingRules = []; 562 var daylightMatchingFound = false; 563 564 // if the offset for jun is the same as the offset in december, 565 // then we have a timezone that doesn't deal with daylight savings. 566 if (jun1offset == dec1offset) { 567 var rules = AjxTimezone.STANDARD_RULES; 568 for (var i = 0; i < rules.length ; ++i ) { 569 var rule = rules[i]; 570 if (rule.standard.offset == jun1offset) { 571 if(!matchingRulesMap[rule.serverId]) { 572 matchingRules.push(rule); 573 matchingRulesMap[rule.serverId] = true; 574 } 575 AjxTimezone.MATCHING_RULES.push(rule); 576 } 577 } 578 } 579 580 // we need to find a rule that matches both offsets 581 else { 582 var rules = AjxTimezone.DAYLIGHT_RULES; 583 var dst = Math.max(dec1offset, jun1offset); 584 var std = Math.min(dec1offset, jun1offset); 585 var now = new Date(); 586 var currentOffset = -now.getTimezoneOffset(); 587 for (var i = 0; i < rules.length ; ++i ) { 588 var rule = rules[i]; 589 if (rule.standard.offset == std && rule.daylight.offset == dst) { 590 var strans = rule.standard.trans; 591 var dtrans = rule.daylight.trans; 592 593 var s0 = new Date(strans[0], strans[1]-1, strans[2]-1); 594 var s1 = new Date(strans[0], strans[1]-1, strans[2]+2); 595 var d0 = new Date(dtrans[0], dtrans[1]-1, dtrans[2]-1); 596 var d1 = new Date(dtrans[0], dtrans[1]-1, dtrans[2]+2); 597 if (-s1.getTimezoneOffset() == std && -d1.getTimezoneOffset() == dst && 598 -s0.getTimezoneOffset() == dst && -d0.getTimezoneOffset() == std) { 599 if(!matchingRulesMap[rule.serverId]) { 600 matchingRules.push(rule); 601 matchingRulesMap[rule.serverId] = true; 602 } 603 daylightMatchingFound = true; 604 } 605 } 606 //used for conflict resolution when server rules are wrong 607 if (rule.standard.offset == currentOffset || rule.daylight.offset == currentOffset) { 608 AjxTimezone.MATCHING_RULES.push(rule); 609 } 610 } 611 } 612 613 //when there is a timezone conflict use the preference to find better match 614 if((matchingRules.length > 0) && timezonePreference != null) { 615 var rules = matchingRules; 616 for(var i in rules) { 617 if(rules[i].serverId == timezonePreference) { 618 return rules[i]; 619 } 620 } 621 } 622 623 if(matchingRules.length > 0) { 624 // resolve conflict, if possible 625 if (matchingRules.length > 1) { 626 matchingRules.sort(AjxTimezone.__BY_SCORE); 627 if (matchingRules[0].score != matchingRules[1].score) { 628 matchingRules.length = 1; 629 } 630 } 631 // mark if conflict and return best guess 632 AjxTimezone.TIMEZONE_CONFLICT = (matchingRules.length > 1); 633 return matchingRules[0]; 634 } 635 636 if((AjxTimezone.MATCHING_RULES.length > 0) && timezonePreference != null) { 637 var rules = AjxTimezone.MATCHING_RULES; 638 for(var i in rules) { 639 if(rules[i].serverId == timezonePreference) { 640 return rules[i]; 641 } 642 } 643 } 644 645 // generate default rule 646 return AjxTimezone._generateDefaultRule(); 647 }; 648 649 AjxTimezone.__BY_SCORE = function(a, b) { 650 return b.score - a.score; 651 }; 652 653 // Thanks to Jiho for this new, improved logic for generating the timezone rule. 654 AjxTimezone._generateDefaultRule = function() { 655 var byMonth = 0; 656 var byDate = 1; 657 var byHour = 2; 658 var byMinute = 3; 659 var bySecond = 4; 660 661 // Sweep the range between d1 and d2 looking for DST transitions. 662 // Iterate the range by "by" unit. When a transition is detected, 663 // sweep the range between before/after dates by increasingly 664 // smaller unit, month then date then hour then minute then finally second. 665 function sweepRange(d1, d2, by, rule) { 666 var upperBound = d2.getTime(); 667 var d = new Date(); 668 d.setTime(d1.getTime()); 669 var prevD = new Date(); 670 prevD.setTime(d.getTime()); 671 var prevOffset = d1.getTimezoneOffset() * -1; 672 673 // initialize rule 674 if (!rule) { 675 rule = { 676 clientId: AjxTimezone.AUTO_DETECTED, 677 autoDetected: true 678 }; 679 } 680 681 // perform sweep 682 while (d.getTime() <= upperBound) { 683 // Increment by the right unit. 684 if (by == byMonth) { 685 d.setUTCMonth(d.getUTCMonth() + 1); 686 } 687 else if (by == byDate) { 688 d.setUTCDate(d.getUTCDate() + 1); 689 } 690 else if (by == byHour) { 691 d.setUTCHours(d.getUTCHours() + 1); 692 } 693 else if (by == byMinute) { 694 d.setUTCMinutes(d.getUTCMinutes() + 1); 695 } 696 else if (by == bySecond) { 697 d.setUTCSeconds(d.getUTCSeconds() + 1); 698 } 699 else { 700 return rule; 701 } 702 703 var offset = d.getTimezoneOffset() * -1; 704 if (offset != prevOffset) { 705 if (by < bySecond) { 706 // Drill down. 707 rule = sweepRange(prevD, d, by + 1, rule); 708 } 709 else { 710 // Tricky: 711 // Initialize a Date object whose UTC fields are set to prevD's local fields. 712 // Then add 1 second to get UTC version of onset time. We want to work in UTC 713 // to prevent the date object from experiencing the DST jump when we add 1 second. 714 var trans = new Date(); 715 trans.setUTCFullYear(prevD.getFullYear(), prevD.getMonth(), prevD.getDate()); 716 trans.setUTCHours(prevD.getHours(), prevD.getMinutes(), prevD.getSeconds() + 1); 717 718 var onset = rule[prevOffset < offset ? "daylight" : "standard"] = { 719 offset: offset, 720 trans: [ 721 trans.getUTCFullYear(), trans.getUTCMonth() + 1, trans.getUTCDate(), // yyyy-MM-dd 722 trans.getUTCHours(), trans.getUTCMinutes(), trans.getUTCSeconds() // HH:mm:ss 723 ] 724 }; 725 AjxTimezone.addWkDayTransition(onset); 726 return rule; 727 } 728 } 729 730 prevD.setTime(d.getTime()); 731 prevOffset = offset; 732 } 733 734 return rule; 735 } 736 737 // Find DST transitions between yyyy/07/71 00:00:00 and yyyy+1/06/30 23:59:59. 738 // We can detect transition on/around 12/31 and 01/01. Assume no one will 739 // transition on/around 6/30 and 07/01. 740 var d1 = new Date(); 741 var d2 = new Date(); 742 743 // set sweep start to yesterday 744 var year = d1.getFullYear(); 745 d1.setUTCFullYear(year, d1.getMonth(), d1.getDate() - 1); 746 d1.setUTCHours(0, 0, 0, 0); 747 748 // set sweep end to tomorrow + 1 year 749 d2.setTime(d1.getTime()); 750 d2.setUTCFullYear(year + 1, d1.getMonth(), d1.getDate() + 1); 751 752 // case 1: no onset returned -> TZ doesn't use DST 753 // case 2: two onsets returned -> TZ uses DST 754 // case 3: only one onset returned -> mid-year policy change -> simplify and assume it's non-DST 755 // case 4: three or more onsets returned -> shouldn't happen 756 var rule = sweepRange(d1, d2, byMonth); 757 758 // handle case 1 and 3 759 if (!rule.daylight || !rule.standard) { 760 rule.standard = { offset: d1.getTimezoneOffset() * -1 }; 761 delete rule.daylight; 762 } 763 764 // now that standard offset is determined, set serverId 765 rule.serverId = ["(GMT",AjxTimezone._generateShortName(rule.standard.offset, true),") ",AjxTimezone.AUTO_DETECTED].join(""); 766 767 // bug 33800: guard against inverted daylight/standard onsets 768 if (rule.daylight && rule.daylight.offset < rule.standard.offset) { 769 var onset = rule.daylight; 770 rule.daylight = rule.standard; 771 rule.standard = onset; 772 } 773 774 // add generated rule to proper list 775 //AjxTimezoneData.TIMEZONE_RULES.unshift(rule); 776 //var rules = rule.daylight ? AjxTimezone.DAYLIGHT_RULES : AjxTimezone.STANDARD_RULES; 777 //rules.unshift(rule); 778 779 return rule; 780 }; 781 782 AjxTimezone._generateShortName = function(offset, period) { 783 if (offset == 0) return ""; 784 var sign = offset < 0 ? "-" : "+"; 785 var stdOffset = Math.abs(offset); 786 var hours = Math.floor(stdOffset / 60); 787 var minutes = stdOffset % 60; 788 hours = hours < 10 ? '0' + hours : hours; 789 minutes = minutes < 10 ? '0' + minutes : minutes; 790 return [sign,hours,period?".":"",minutes].join(""); 791 }; 792 793 // Static initialization 794 795 AjxTimezone.DEFAULT_RULE = AjxTimezone._guessMachineTimezone(); 796 797 798 /*** DEBUG *** 799 // This forces the client to create an auto-detected timezone rule, 800 // regardless of whether the actual timezone was detected correctly 801 // from the known list. 802 AjxTimezone.DEFAULT_RULE = AjxTimezone._generateDefaultRule(); 803 /***/ 804 805 (function() { 806 AjxTimezoneData.TIMEZONE_RULES.sort(AjxTimezone._BY_OFFSET); 807 for (var j = 0; j < AjxTimezoneData.TIMEZONE_RULES.length; j++) { 808 var rule = AjxTimezoneData.TIMEZONE_RULES[j]; 809 AjxTimezone.addRule(rule); 810 } 811 })(); 812 813 AjxTimezone.DEFAULT = AjxTimezone.getClientId(AjxTimezone.DEFAULT_RULE.serverId); 814