wangtengyu
2018-12-07 f459412e0dac4ed94106da043b4c6f8576bfe496
commit | author | age
19351a 1 /*global window */
B 2 /**
3  * @license countdown.js v2.5.3 http://countdownjs.org
4  * Copyright (c)2006-2014 Stephen M. McKamey.
5  * Licensed under The MIT License.
6  */
7 /*jshint bitwise:false */
8
9 /**
10  * @public
11  * @type {Object|null}
12  */
13 var module;
14
15 /**
16  * API entry
17  * @public
18  * @param {function(Object)|Date|number} start the starting date
19  * @param {function(Object)|Date|number} end the ending date
20  * @param {number} units the units to populate
21  * @return {Object|number}
22  */
23 var countdown = (
24
25 /**
26  * @param {Object} module CommonJS Module
27  */
28 function(module) {
29     /*jshint smarttabs:true */
30
31     'use strict';
32
33     /**
34      * @private
35      * @const
36      * @type {number}
37      */
38     var MILLISECONDS    = 0x001;
39
40     /**
41      * @private
42      * @const
43      * @type {number}
44      */
45     var SECONDS            = 0x002;
46
47     /**
48      * @private
49      * @const
50      * @type {number}
51      */
52     var MINUTES            = 0x004;
53
54     /**
55      * @private
56      * @const
57      * @type {number}
58      */
59     var HOURS            = 0x008;
60
61     /**
62      * @private
63      * @const
64      * @type {number}
65      */
66     var DAYS            = 0x010;
67
68     /**
69      * @private
70      * @const
71      * @type {number}
72      */
73     var WEEKS            = 0x020;
74
75     /**
76      * @private
77      * @const
78      * @type {number}
79      */
80     var MONTHS            = 0x040;
81
82     /**
83      * @private
84      * @const
85      * @type {number}
86      */
87     var YEARS            = 0x080;
88
89     /**
90      * @private
91      * @const
92      * @type {number}
93      */
94     var DECADES            = 0x100;
95
96     /**
97      * @private
98      * @const
99      * @type {number}
100      */
101     var CENTURIES        = 0x200;
102
103     /**
104      * @private
105      * @const
106      * @type {number}
107      */
108     var MILLENNIA        = 0x400;
109
110     /**
111      * @private
112      * @const
113      * @type {number}
114      */
115     var DEFAULTS        = YEARS|MONTHS|DAYS|HOURS|MINUTES|SECONDS;
116
117     /**
118      * @private
119      * @const
120      * @type {number}
121      */
122     var MILLISECONDS_PER_SECOND = 1000;
123
124     /**
125      * @private
126      * @const
127      * @type {number}
128      */
129     var SECONDS_PER_MINUTE = 60;
130
131     /**
132      * @private
133      * @const
134      * @type {number}
135      */
136     var MINUTES_PER_HOUR = 60;
137
138     /**
139      * @private
140      * @const
141      * @type {number}
142      */
143     var HOURS_PER_DAY = 24;
144
145     /**
146      * @private
147      * @const
148      * @type {number}
149      */
150     var MILLISECONDS_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
151
152     /**
153      * @private
154      * @const
155      * @type {number}
156      */
157     var DAYS_PER_WEEK = 7;
158
159     /**
160      * @private
161      * @const
162      * @type {number}
163      */
164     var MONTHS_PER_YEAR = 12;
165
166     /**
167      * @private
168      * @const
169      * @type {number}
170      */
171     var YEARS_PER_DECADE = 10;
172
173     /**
174      * @private
175      * @const
176      * @type {number}
177      */
178     var DECADES_PER_CENTURY = 10;
179
180     /**
181      * @private
182      * @const
183      * @type {number}
184      */
185     var CENTURIES_PER_MILLENNIUM = 10;
186
187     /**
188      * @private
189      * @param {number} x number
190      * @return {number}
191      */
192     var ceil = Math.ceil;
193
194     /**
195      * @private
196      * @param {number} x number
197      * @return {number}
198      */
199     var floor = Math.floor;
200
201     /**
202      * @private
203      * @param {Date} ref reference date
204      * @param {number} shift number of months to shift
205      * @return {number} number of days shifted
206      */
207     function borrowMonths(ref, shift) {
208         var prevTime = ref.getTime();
209
210         // increment month by shift
211         ref.setMonth( ref.getMonth() + shift );
212
213         // this is the trickiest since months vary in length
214         return Math.round( (ref.getTime() - prevTime) / MILLISECONDS_PER_DAY );
215     }
216
217     /**
218      * @private
219      * @param {Date} ref reference date
220      * @return {number} number of days
221      */
222     function daysPerMonth(ref) {
223         var a = ref.getTime();
224
225         // increment month by 1
226         var b = new Date(a);
227         b.setMonth( ref.getMonth() + 1 );
228
229         // this is the trickiest since months vary in length
230         return Math.round( (b.getTime() - a) / MILLISECONDS_PER_DAY );
231     }
232
233     /**
234      * @private
235      * @param {Date} ref reference date
236      * @return {number} number of days
237      */
238     function daysPerYear(ref) {
239         var a = ref.getTime();
240
241         // increment year by 1
242         var b = new Date(a);
243         b.setFullYear( ref.getFullYear() + 1 );
244
245         // this is the trickiest since years (periodically) vary in length
246         return Math.round( (b.getTime() - a) / MILLISECONDS_PER_DAY );
247     }
248
249     /**
250      * Applies the Timespan to the given date.
251      * 
252      * @private
253      * @param {Timespan} ts
254      * @param {Date=} date
255      * @return {Date}
256      */
257     function addToDate(ts, date) {
258         date = (date instanceof Date) || ((date !== null) && isFinite(date)) ? new Date(+date) : new Date();
259         if (!ts) {
260             return date;
261         }
262
263         // if there is a value field, use it directly
264         var value = +ts.value || 0;
265         if (value) {
266             date.setTime(date.getTime() + value);
267             return date;
268         }
269
270         value = +ts.milliseconds || 0;
271         if (value) {
272             date.setMilliseconds(date.getMilliseconds() + value);
273         }
274
275         value = +ts.seconds || 0;
276         if (value) {
277             date.setSeconds(date.getSeconds() + value);
278         }
279
280         value = +ts.minutes || 0;
281         if (value) {
282             date.setMinutes(date.getMinutes() + value);
283         }
284
285         value = +ts.hours || 0;
286         if (value) {
287             date.setHours(date.getHours() + value);
288         }
289
290         value = +ts.weeks || 0;
291         if (value) {
292             value *= DAYS_PER_WEEK;
293         }
294
295         value += +ts.days || 0;
296         if (value) {
297             date.setDate(date.getDate() + value);
298         }
299
300         value = +ts.months || 0;
301         if (value) {
302             date.setMonth(date.getMonth() + value);
303         }
304
305         value = +ts.millennia || 0;
306         if (value) {
307             value *= CENTURIES_PER_MILLENNIUM;
308         }
309
310         value += +ts.centuries || 0;
311         if (value) {
312             value *= DECADES_PER_CENTURY;
313         }
314
315         value += +ts.decades || 0;
316         if (value) {
317             value *= YEARS_PER_DECADE;
318         }
319
320         value += +ts.years || 0;
321         if (value) {
322             date.setFullYear(date.getFullYear() + value);
323         }
324
325         return date;
326     }
327
328     /**
329      * @private
330      * @const
331      * @type {number}
332      */
333     var LABEL_MILLISECONDS    = 0;
334
335     /**
336      * @private
337      * @const
338      * @type {number}
339      */
340     var LABEL_SECONDS        = 1;
341
342     /**
343      * @private
344      * @const
345      * @type {number}
346      */
347     var LABEL_MINUTES        = 2;
348
349     /**
350      * @private
351      * @const
352      * @type {number}
353      */
354     var LABEL_HOURS            = 3;
355
356     /**
357      * @private
358      * @const
359      * @type {number}
360      */
361     var LABEL_DAYS            = 4;
362
363     /**
364      * @private
365      * @const
366      * @type {number}
367      */
368     var LABEL_WEEKS            = 5;
369
370     /**
371      * @private
372      * @const
373      * @type {number}
374      */
375     var LABEL_MONTHS        = 6;
376
377     /**
378      * @private
379      * @const
380      * @type {number}
381      */
382     var LABEL_YEARS            = 7;
383
384     /**
385      * @private
386      * @const
387      * @type {number}
388      */
389     var LABEL_DECADES        = 8;
390
391     /**
392      * @private
393      * @const
394      * @type {number}
395      */
396     var LABEL_CENTURIES        = 9;
397
398     /**
399      * @private
400      * @const
401      * @type {number}
402      */
403     var LABEL_MILLENNIA        = 10;
404
405     /**
406      * @private
407      * @type {Array}
408      */
409     var LABELS_SINGLUAR;
410
411     /**
412      * @private
413      * @type {Array}
414      */
415     var LABELS_PLURAL;
416
417     /**
418      * @private
419      * @type {string}
420      */
421     var LABEL_LAST;
422
423     /**
424      * @private
425      * @type {string}
426      */
427     var LABEL_DELIM;
428
429     /**
430      * @private
431      * @type {string}
432      */
433     var LABEL_NOW;
434
435     /**
436      * Formats a number as a string
437      * 
438      * @private
439      * @param {number} value
440      * @return {string}
441      */
442     var formatNumber;
443
444     /**
445      * @private
446      * @param {number} value
447      * @param {number} unit unit index into label list
448      * @return {string}
449      */
450     function plurality(value, unit) {
451         return formatNumber(value)+((value === 1) ? LABELS_SINGLUAR[unit] : LABELS_PLURAL[unit]);
452     }
453
454     /**
455      * Formats the entries with singular or plural labels
456      * 
457      * @private
458      * @param {Timespan} ts
459      * @return {Array}
460      */
461     var formatList;
462
463     /**
464      * Timespan representation of a duration of time
465      * 
466      * @private
467      * @this {Timespan}
468      * @constructor
469      */
470     function Timespan() {}
471
472     /**
473      * Formats the Timespan as a sentence
474      * 
475      * @param {string=} emptyLabel the string to use when no values returned
476      * @return {string}
477      */
478     Timespan.prototype.toString = function(emptyLabel) {
479         var label = formatList(this);
480
481         var count = label.length;
482         if (!count) {
483             return emptyLabel ? ''+emptyLabel : LABEL_NOW;
484         }
485         if (count === 1) {
486             return label[0];
487         }
488
489         var last = LABEL_LAST+label.pop();
490         return label.join(LABEL_DELIM)+last;
491     };
492
493     /**
494      * Formats the Timespan as a sentence in HTML
495      * 
496      * @param {string=} tag HTML tag name to wrap each value
497      * @param {string=} emptyLabel the string to use when no values returned
498      * @return {string}
499      */
500     Timespan.prototype.toHTML = function(tag, emptyLabel) {
501         tag = tag || 'span';
502         var label = formatList(this);
503
504         var count = label.length;
505         if (!count) {
506             emptyLabel = emptyLabel || LABEL_NOW;
507             return emptyLabel ? '<'+tag+'>'+emptyLabel+'</'+tag+'>' : emptyLabel;
508         }
509         for (var i=0; i<count; i++) {
510             // wrap each unit in tag
511             label[i] = '<'+tag+'>'+label[i]+'</'+tag+'>';
512         }
513         if (count === 1) {
514             return label[0];
515         }
516
517         var last = LABEL_LAST+label.pop();
518         return label.join(LABEL_DELIM)+last;
519     };
520
521     /**
522      * Applies the Timespan to the given date
523      * 
524      * @param {Date=} date the date to which the timespan is added.
525      * @return {Date}
526      */
527     Timespan.prototype.addTo = function(date) {
528         return addToDate(this, date);
529     };
530
531     /**
532      * Formats the entries as English labels
533      * 
534      * @private
535      * @param {Timespan} ts
536      * @return {Array}
537      */
538     formatList = function(ts) {
539         var list = [];
540
541         var value = ts.millennia;
542         if (value) {
543             list.push(plurality(value, LABEL_MILLENNIA));
544         }
545
546         value = ts.centuries;
547         if (value) {
548             list.push(plurality(value, LABEL_CENTURIES));
549         }
550
551         value = ts.decades;
552         if (value) {
553             list.push(plurality(value, LABEL_DECADES));
554         }
555
556         value = ts.years;
557         if (value) {
558             list.push(plurality(value, LABEL_YEARS));
559         }
560
561         value = ts.months;
562         if (value) {
563             list.push(plurality(value, LABEL_MONTHS));
564         }
565
566         value = ts.weeks;
567         if (value) {
568             list.push(plurality(value, LABEL_WEEKS));
569         }
570
571         value = ts.days;
572         if (value) {
573             list.push(plurality(value, LABEL_DAYS));
574         }
575
576         value = ts.hours;
577         if (value) {
578             list.push(plurality(value, LABEL_HOURS));
579         }
580
581         value = ts.minutes;
582         if (value) {
583             list.push(plurality(value, LABEL_MINUTES));
584         }
585
586         value = ts.seconds;
587         if (value) {
588             list.push(plurality(value, LABEL_SECONDS));
589         }
590
591         value = ts.milliseconds;
592         if (value) {
593             list.push(plurality(value, LABEL_MILLISECONDS));
594         }
595
596         return list;
597     };
598
599     /**
600      * Borrow any underflow units, carry any overflow units
601      * 
602      * @private
603      * @param {Timespan} ts
604      * @param {string} toUnit
605      */
606     function rippleRounded(ts, toUnit) {
607         switch (toUnit) {
608             case 'seconds':
609                 if (ts.seconds !== SECONDS_PER_MINUTE || isNaN(ts.minutes)) {
610                     return;
611                 }
612                 // ripple seconds up to minutes
613                 ts.minutes++;
614                 ts.seconds = 0;
615
616                 /* falls through */
617             case 'minutes':
618                 if (ts.minutes !== MINUTES_PER_HOUR || isNaN(ts.hours)) {
619                     return;
620                 }
621                 // ripple minutes up to hours
622                 ts.hours++;
623                 ts.minutes = 0;
624
625                 /* falls through */
626             case 'hours':
627                 if (ts.hours !== HOURS_PER_DAY || isNaN(ts.days)) {
628                     return;
629                 }
630                 // ripple hours up to days
631                 ts.days++;
632                 ts.hours = 0;
633
634                 /* falls through */
635             case 'days':
636                 if (ts.days !== DAYS_PER_WEEK || isNaN(ts.weeks)) {
637                     return;
638                 }
639                 // ripple days up to weeks
640                 ts.weeks++;
641                 ts.days = 0;
642
643                 /* falls through */
644             case 'weeks':
645                 if (ts.weeks !== daysPerMonth(ts.refMonth)/DAYS_PER_WEEK || isNaN(ts.months)) {
646                     return;
647                 }
648                 // ripple weeks up to months
649                 ts.months++;
650                 ts.weeks = 0;
651
652                 /* falls through */
653             case 'months':
654                 if (ts.months !== MONTHS_PER_YEAR || isNaN(ts.years)) {
655                     return;
656                 }
657                 // ripple months up to years
658                 ts.years++;
659                 ts.months = 0;
660
661                 /* falls through */
662             case 'years':
663                 if (ts.years !== YEARS_PER_DECADE || isNaN(ts.decades)) {
664                     return;
665                 }
666                 // ripple years up to decades
667                 ts.decades++;
668                 ts.years = 0;
669
670                 /* falls through */
671             case 'decades':
672                 if (ts.decades !== DECADES_PER_CENTURY || isNaN(ts.centuries)) {
673                     return;
674                 }
675                 // ripple decades up to centuries
676                 ts.centuries++;
677                 ts.decades = 0;
678
679                 /* falls through */
680             case 'centuries':
681                 if (ts.centuries !== CENTURIES_PER_MILLENNIUM || isNaN(ts.millennia)) {
682                     return;
683                 }
684                 // ripple centuries up to millennia
685                 ts.millennia++;
686                 ts.centuries = 0;
687                 /* falls through */
688             }
689     }
690
691     /**
692      * Ripple up partial units one place
693      * 
694      * @private
695      * @param {Timespan} ts timespan
696      * @param {number} frac accumulated fractional value
697      * @param {string} fromUnit source unit name
698      * @param {string} toUnit target unit name
699      * @param {number} conversion multiplier between units
700      * @param {number} digits max number of decimal digits to output
701      * @return {number} new fractional value
702      */
703     function fraction(ts, frac, fromUnit, toUnit, conversion, digits) {
704         if (ts[fromUnit] >= 0) {
705             frac += ts[fromUnit];
706             delete ts[fromUnit];
707         }
708
709         frac /= conversion;
710         if (frac + 1 <= 1) {
711             // drop if below machine epsilon
712             return 0;
713         }
714
715         if (ts[toUnit] >= 0) {
716             // ensure does not have more than specified number of digits
717             ts[toUnit] = +(ts[toUnit] + frac).toFixed(digits);
718             rippleRounded(ts, toUnit);
719             return 0;
720         }
721
722         return frac;
723     }
724
725     /**
726      * Ripple up partial units to next existing
727      * 
728      * @private
729      * @param {Timespan} ts
730      * @param {number} digits max number of decimal digits to output
731      */
732     function fractional(ts, digits) {
733         var frac = fraction(ts, 0, 'milliseconds', 'seconds', MILLISECONDS_PER_SECOND, digits);
734         if (!frac) { return; }
735
736         frac = fraction(ts, frac, 'seconds', 'minutes', SECONDS_PER_MINUTE, digits);
737         if (!frac) { return; }
738
739         frac = fraction(ts, frac, 'minutes', 'hours', MINUTES_PER_HOUR, digits);
740         if (!frac) { return; }
741
742         frac = fraction(ts, frac, 'hours', 'days', HOURS_PER_DAY, digits);
743         if (!frac) { return; }
744
745         frac = fraction(ts, frac, 'days', 'weeks', DAYS_PER_WEEK, digits);
746         if (!frac) { return; }
747
748         frac = fraction(ts, frac, 'weeks', 'months', daysPerMonth(ts.refMonth)/DAYS_PER_WEEK, digits);
749         if (!frac) { return; }
750
751         frac = fraction(ts, frac, 'months', 'years', daysPerYear(ts.refMonth)/daysPerMonth(ts.refMonth), digits);
752         if (!frac) { return; }
753
754         frac = fraction(ts, frac, 'years', 'decades', YEARS_PER_DECADE, digits);
755         if (!frac) { return; }
756
757         frac = fraction(ts, frac, 'decades', 'centuries', DECADES_PER_CENTURY, digits);
758         if (!frac) { return; }
759
760         frac = fraction(ts, frac, 'centuries', 'millennia', CENTURIES_PER_MILLENNIUM, digits);
761
762         // should never reach this with remaining fractional value
763         if (frac) { throw new Error('Fractional unit overflow'); }
764     }
765
766     /**
767      * Borrow any underflow units, carry any overflow units
768      * 
769      * @private
770      * @param {Timespan} ts
771      */
772     function ripple(ts) {
773         var x;
774
775         if (ts.milliseconds < 0) {
776             // ripple seconds down to milliseconds
777             x = ceil(-ts.milliseconds / MILLISECONDS_PER_SECOND);
778             ts.seconds -= x;
779             ts.milliseconds += x * MILLISECONDS_PER_SECOND;
780
781         } else if (ts.milliseconds >= MILLISECONDS_PER_SECOND) {
782             // ripple milliseconds up to seconds
783             ts.seconds += floor(ts.milliseconds / MILLISECONDS_PER_SECOND);
784             ts.milliseconds %= MILLISECONDS_PER_SECOND;
785         }
786
787         if (ts.seconds < 0) {
788             // ripple minutes down to seconds
789             x = ceil(-ts.seconds / SECONDS_PER_MINUTE);
790             ts.minutes -= x;
791             ts.seconds += x * SECONDS_PER_MINUTE;
792
793         } else if (ts.seconds >= SECONDS_PER_MINUTE) {
794             // ripple seconds up to minutes
795             ts.minutes += floor(ts.seconds / SECONDS_PER_MINUTE);
796             ts.seconds %= SECONDS_PER_MINUTE;
797         }
798
799         if (ts.minutes < 0) {
800             // ripple hours down to minutes
801             x = ceil(-ts.minutes / MINUTES_PER_HOUR);
802             ts.hours -= x;
803             ts.minutes += x * MINUTES_PER_HOUR;
804
805         } else if (ts.minutes >= MINUTES_PER_HOUR) {
806             // ripple minutes up to hours
807             ts.hours += floor(ts.minutes / MINUTES_PER_HOUR);
808             ts.minutes %= MINUTES_PER_HOUR;
809         }
810
811         if (ts.hours < 0) {
812             // ripple days down to hours
813             x = ceil(-ts.hours / HOURS_PER_DAY);
814             ts.days -= x;
815             ts.hours += x * HOURS_PER_DAY;
816
817         } else if (ts.hours >= HOURS_PER_DAY) {
818             // ripple hours up to days
819             ts.days += floor(ts.hours / HOURS_PER_DAY);
820             ts.hours %= HOURS_PER_DAY;
821         }
822
823         while (ts.days < 0) {
824             // NOTE: never actually seen this loop more than once
825
826             // ripple months down to days
827             ts.months--;
828             ts.days += borrowMonths(ts.refMonth, 1);
829         }
830
831         // weeks is always zero here
832
833         if (ts.days >= DAYS_PER_WEEK) {
834             // ripple days up to weeks
835             ts.weeks += floor(ts.days / DAYS_PER_WEEK);
836             ts.days %= DAYS_PER_WEEK;
837         }
838
839         if (ts.months < 0) {
840             // ripple years down to months
841             x = ceil(-ts.months / MONTHS_PER_YEAR);
842             ts.years -= x;
843             ts.months += x * MONTHS_PER_YEAR;
844
845         } else if (ts.months >= MONTHS_PER_YEAR) {
846             // ripple months up to years
847             ts.years += floor(ts.months / MONTHS_PER_YEAR);
848             ts.months %= MONTHS_PER_YEAR;
849         }
850
851         // years is always non-negative here
852         // decades, centuries and millennia are always zero here
853
854         if (ts.years >= YEARS_PER_DECADE) {
855             // ripple years up to decades
856             ts.decades += floor(ts.years / YEARS_PER_DECADE);
857             ts.years %= YEARS_PER_DECADE;
858
859             if (ts.decades >= DECADES_PER_CENTURY) {
860                 // ripple decades up to centuries
861                 ts.centuries += floor(ts.decades / DECADES_PER_CENTURY);
862                 ts.decades %= DECADES_PER_CENTURY;
863
864                 if (ts.centuries >= CENTURIES_PER_MILLENNIUM) {
865                     // ripple centuries up to millennia
866                     ts.millennia += floor(ts.centuries / CENTURIES_PER_MILLENNIUM);
867                     ts.centuries %= CENTURIES_PER_MILLENNIUM;
868                 }
869             }
870         }
871     }
872
873     /**
874      * Remove any units not requested
875      * 
876      * @private
877      * @param {Timespan} ts
878      * @param {number} units the units to populate
879      * @param {number} max number of labels to output
880      * @param {number} digits max number of decimal digits to output
881      */
882     function pruneUnits(ts, units, max, digits) {
883         var count = 0;
884
885         // Calc from largest unit to smallest to prevent underflow
886         if (!(units & MILLENNIA) || (count >= max)) {
887             // ripple millennia down to centuries
888             ts.centuries += ts.millennia * CENTURIES_PER_MILLENNIUM;
889             delete ts.millennia;
890
891         } else if (ts.millennia) {
892             count++;
893         }
894
895         if (!(units & CENTURIES) || (count >= max)) {
896             // ripple centuries down to decades
897             ts.decades += ts.centuries * DECADES_PER_CENTURY;
898             delete ts.centuries;
899
900         } else if (ts.centuries) {
901             count++;
902         }
903
904         if (!(units & DECADES) || (count >= max)) {
905             // ripple decades down to years
906             ts.years += ts.decades * YEARS_PER_DECADE;
907             delete ts.decades;
908
909         } else if (ts.decades) {
910             count++;
911         }
912
913         if (!(units & YEARS) || (count >= max)) {
914             // ripple years down to months
915             ts.months += ts.years * MONTHS_PER_YEAR;
916             delete ts.years;
917
918         } else if (ts.years) {
919             count++;
920         }
921
922         if (!(units & MONTHS) || (count >= max)) {
923             // ripple months down to days
924             if (ts.months) {
925                 ts.days += borrowMonths(ts.refMonth, ts.months);
926             }
927             delete ts.months;
928
929             if (ts.days >= DAYS_PER_WEEK) {
930                 // ripple day overflow back up to weeks
931                 ts.weeks += floor(ts.days / DAYS_PER_WEEK);
932                 ts.days %= DAYS_PER_WEEK;
933             }
934
935         } else if (ts.months) {
936             count++;
937         }
938
939         if (!(units & WEEKS) || (count >= max)) {
940             // ripple weeks down to days
941             ts.days += ts.weeks * DAYS_PER_WEEK;
942             delete ts.weeks;
943
944         } else if (ts.weeks) {
945             count++;
946         }
947
948         if (!(units & DAYS) || (count >= max)) {
949             //ripple days down to hours
950             ts.hours += ts.days * HOURS_PER_DAY;
951             delete ts.days;
952
953         } else if (ts.days) {
954             count++;
955         }
956
957         if (!(units & HOURS) || (count >= max)) {
958             // ripple hours down to minutes
959             ts.minutes += ts.hours * MINUTES_PER_HOUR;
960             delete ts.hours;
961
962         } else if (ts.hours) {
963             count++;
964         }
965
966         if (!(units & MINUTES) || (count >= max)) {
967             // ripple minutes down to seconds
968             ts.seconds += ts.minutes * SECONDS_PER_MINUTE;
969             delete ts.minutes;
970
971         } else if (ts.minutes) {
972             count++;
973         }
974
975         if (!(units & SECONDS) || (count >= max)) {
976             // ripple seconds down to milliseconds
977             ts.milliseconds += ts.seconds * MILLISECONDS_PER_SECOND;
978             delete ts.seconds;
979
980         } else if (ts.seconds) {
981             count++;
982         }
983
984         // nothing to ripple milliseconds down to
985         // so ripple back up to smallest existing unit as a fractional value
986         if (!(units & MILLISECONDS) || (count >= max)) {
987             fractional(ts, digits);
988         }
989     }
990
991     /**
992      * Populates the Timespan object
993      * 
994      * @private
995      * @param {Timespan} ts
996      * @param {?Date} start the starting date
997      * @param {?Date} end the ending date
998      * @param {number} units the units to populate
999      * @param {number} max number of labels to output
1000      * @param {number} digits max number of decimal digits to output
1001      */
1002     function populate(ts, start, end, units, max, digits) {
1003         var now = new Date();
1004
1005         ts.start = start = start || now;
1006         ts.end = end = end || now;
1007         ts.units = units;
1008
1009         ts.value = end.getTime() - start.getTime();
1010         if (ts.value < 0) {
1011             // swap if reversed
1012             var tmp = end;
1013             end = start;
1014             start = tmp;
1015         }
1016
1017         // reference month for determining days in month
1018         ts.refMonth = new Date(start.getFullYear(), start.getMonth(), 15, 12, 0, 0);
1019         try {
1020             // reset to initial deltas
1021             ts.millennia = 0;
1022             ts.centuries = 0;
1023             ts.decades = 0;
1024             ts.years = end.getFullYear() - start.getFullYear();
1025             ts.months = end.getMonth() - start.getMonth();
1026             ts.weeks = 0;
1027             ts.days = end.getDate() - start.getDate();
1028             ts.hours = end.getHours() - start.getHours();
1029             ts.minutes = end.getMinutes() - start.getMinutes();
1030             ts.seconds = end.getSeconds() - start.getSeconds();
1031             ts.milliseconds = end.getMilliseconds() - start.getMilliseconds();
1032
1033             ripple(ts);
1034             pruneUnits(ts, units, max, digits);
1035
1036         } finally {
1037             delete ts.refMonth;
1038         }
1039
1040         return ts;
1041     }
1042
1043     /**
1044      * Determine an appropriate refresh rate based upon units
1045      * 
1046      * @private
1047      * @param {number} units the units to populate
1048      * @return {number} milliseconds to delay
1049      */
1050     function getDelay(units) {
1051         if (units & MILLISECONDS) {
1052             // refresh very quickly
1053             return MILLISECONDS_PER_SECOND / 30; //30Hz
1054         }
1055
1056         if (units & SECONDS) {
1057             // refresh every second
1058             return MILLISECONDS_PER_SECOND; //1Hz
1059         }
1060
1061         if (units & MINUTES) {
1062             // refresh every minute
1063             return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE;
1064         }
1065
1066         if (units & HOURS) {
1067             // refresh hourly
1068             return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR;
1069         }
1070         
1071         if (units & DAYS) {
1072             // refresh daily
1073             return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY;
1074         }
1075
1076         // refresh the rest weekly
1077         return MILLISECONDS_PER_SECOND * SECONDS_PER_MINUTE * MINUTES_PER_HOUR * HOURS_PER_DAY * DAYS_PER_WEEK;
1078     }
1079
1080     /**
1081      * API entry point
1082      * 
1083      * @public
1084      * @param {Date|number|Timespan|null|function(Timespan,number)} start the starting date
1085      * @param {Date|number|Timespan|null|function(Timespan,number)} end the ending date
1086      * @param {number=} units the units to populate
1087      * @param {number=} max number of labels to output
1088      * @param {number=} digits max number of decimal digits to output
1089      * @return {Timespan|number}
1090      */
1091     function countdown(start, end, units, max, digits) {
1092         var callback;
1093
1094         // ensure some units or use defaults
1095         units = +units || DEFAULTS;
1096         // max must be positive
1097         max = (max > 0) ? max : NaN;
1098         // clamp digits to an integer between [0, 20]
1099         digits = (digits > 0) ? (digits < 20) ? Math.round(digits) : 20 : 0;
1100
1101         // ensure start date
1102         var startTS = null;
1103         if ('function' === typeof start) {
1104             callback = start;
1105             start = null;
1106
1107         } else if (!(start instanceof Date)) {
1108             if ((start !== null) && isFinite(start)) {
1109                 start = new Date(+start);
1110             } else {
1111                 if ('object' === typeof startTS) {
1112                     startTS = /** @type{Timespan} */(start);
1113                 }
1114                 start = null;
1115             }
1116         }
1117
1118         // ensure end date
1119         var endTS = null;
1120         if ('function' === typeof end) {
1121             callback = end;
1122             end = null;
1123
1124         } else if (!(end instanceof Date)) {
1125             if ((end !== null) && isFinite(end)) {
1126                 end = new Date(+end);
1127             } else {
1128                 if ('object' === typeof end) {
1129                     endTS = /** @type{Timespan} */(end);
1130                 }
1131                 end = null;
1132             }
1133         }
1134
1135         // must wait to interpret timespans until after resolving dates
1136         if (startTS) {
1137             start = addToDate(startTS, end);
1138         }
1139         if (endTS) {
1140             end = addToDate(endTS, start);
1141         }
1142
1143         if (!start && !end) {
1144             // used for unit testing
1145             return new Timespan();
1146         }
1147
1148         if (!callback) {
1149             return populate(new Timespan(), /** @type{Date} */(start), /** @type{Date} */(end), /** @type{number} */(units), /** @type{number} */(max), /** @type{number} */(digits));
1150         }
1151
1152         // base delay off units
1153         var delay = getDelay(units),
1154             timerId,
1155             fn = function() {
1156                 callback(
1157                     populate(new Timespan(), /** @type{Date} */(start), /** @type{Date} */(end), /** @type{number} */(units), /** @type{number} */(max), /** @type{number} */(digits)),
1158                     timerId
1159                 );
1160             };
1161
1162         fn();
1163         return (timerId = setInterval(fn, delay));
1164     }
1165
1166     /**
1167      * @public
1168      * @const
1169      * @type {number}
1170      */
1171     countdown.MILLISECONDS = MILLISECONDS;
1172
1173     /**
1174      * @public
1175      * @const
1176      * @type {number}
1177      */
1178     countdown.SECONDS = SECONDS;
1179
1180     /**
1181      * @public
1182      * @const
1183      * @type {number}
1184      */
1185     countdown.MINUTES = MINUTES;
1186
1187     /**
1188      * @public
1189      * @const
1190      * @type {number}
1191      */
1192     countdown.HOURS = HOURS;
1193
1194     /**
1195      * @public
1196      * @const
1197      * @type {number}
1198      */
1199     countdown.DAYS = DAYS;
1200
1201     /**
1202      * @public
1203      * @const
1204      * @type {number}
1205      */
1206     countdown.WEEKS = WEEKS;
1207
1208     /**
1209      * @public
1210      * @const
1211      * @type {number}
1212      */
1213     countdown.MONTHS = MONTHS;
1214
1215     /**
1216      * @public
1217      * @const
1218      * @type {number}
1219      */
1220     countdown.YEARS = YEARS;
1221
1222     /**
1223      * @public
1224      * @const
1225      * @type {number}
1226      */
1227     countdown.DECADES = DECADES;
1228
1229     /**
1230      * @public
1231      * @const
1232      * @type {number}
1233      */
1234     countdown.CENTURIES = CENTURIES;
1235
1236     /**
1237      * @public
1238      * @const
1239      * @type {number}
1240      */
1241     countdown.MILLENNIA = MILLENNIA;
1242
1243     /**
1244      * @public
1245      * @const
1246      * @type {number}
1247      */
1248     countdown.DEFAULTS = DEFAULTS;
1249
1250     /**
1251      * @public
1252      * @const
1253      * @type {number}
1254      */
1255     countdown.ALL = MILLENNIA|CENTURIES|DECADES|YEARS|MONTHS|WEEKS|DAYS|HOURS|MINUTES|SECONDS|MILLISECONDS;
1256
1257     /**
1258      * Override the unit labels
1259      * @public
1260      * @param {string|Array=} singular a pipe ('|') delimited list of singular unit name overrides
1261      * @param {string|Array=} plural a pipe ('|') delimited list of plural unit name overrides
1262      * @param {string=} last a delimiter before the last unit (default: ' and ')
1263      * @param {string=} delim a delimiter to use between all other units (default: ', ')
1264      * @param {string=} empty a label to use when all units are zero (default: '')
1265      * @param {function(number):string=} formatter a function which formats numbers as a string
1266      */
1267     countdown.setLabels = function(singular, plural, last, delim, empty, formatter) {
1268         singular = singular || [];
1269         if (singular.split) {
1270             singular = singular.split('|');
1271         }
1272         plural = plural || [];
1273         if (plural.split) {
1274             plural = plural.split('|');
1275         }
1276
1277         for (var i=LABEL_MILLISECONDS; i<=LABEL_MILLENNIA; i++) {
1278             // override any specified units
1279             LABELS_SINGLUAR[i] = singular[i] || LABELS_SINGLUAR[i];
1280             LABELS_PLURAL[i] = plural[i] || LABELS_PLURAL[i];
1281         }
1282
1283         LABEL_LAST = ('string' === typeof last) ? last : LABEL_LAST;
1284         LABEL_DELIM = ('string' === typeof delim) ? delim : LABEL_DELIM;
1285         LABEL_NOW = ('string' === typeof empty) ? empty : LABEL_NOW;
1286         formatNumber = ('function' === typeof formatter) ? formatter : formatNumber;
1287     };
1288
1289     /**
1290      * Revert to the default unit labels
1291      * @public
1292      */
1293     var resetLabels = countdown.resetLabels = function() {
1294         LABELS_SINGLUAR = ' millisecond| second| minute| hour| day| week| month| year| decade| century| millennium'.split('|');
1295         LABELS_PLURAL = ' milliseconds| seconds| minutes| hours| days| weeks| months| years| decades| centuries| millennia'.split('|');
1296         LABEL_LAST = ' and ';
1297         LABEL_DELIM = ', ';
1298         LABEL_NOW = '';
1299         formatNumber = function(value) { return value; };
1300     };
1301
1302     resetLabels();
1303
1304     if (module && module.exports) {
1305         module.exports = countdown;
1306
1307     } else if (typeof window.define === 'function' && typeof window.define.amd !== 'undefined') {
1308         window.define('countdown', [], function() {
1309             return countdown;
1310         });
1311     }
1312
1313     return countdown;
1314
1315 })(module);