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