@@ -49,6 +49,85 @@ function isWorldCalendar(calendar) {
4949// of the unix epoch. From calendars.instance().newDate(1970, 1, 1).toJD()
5050var EPOCHJD = 2440587.5 ;
5151
52+ // each calendar needs its own default canonical tick. I would love to use
53+ // 2000-01-01 (or even 0000-01-01) for them all but they don't necessarily
54+ // all support either of those dates. Instead I'll use the most significant
55+ // number they *do* support, biased toward the present day.
56+ var CANONICAL_TICK = {
57+ gregorian : '2000-01-01' ,
58+ coptic : '2000-01-01' ,
59+ discworld : '2000-01-01' ,
60+ ethiopian : '2000-01-01' ,
61+ hebrew : '5000-01-01' ,
62+ islamic : '1000-01-01' ,
63+ julian : '2000-01-01' ,
64+ mayan : '5000-01-01' ,
65+ nanakshahi : '1000-01-01' ,
66+ nepali : '2000-01-01' ,
67+ persian : '1000-01-01' ,
68+ jalali : '1000-01-01' ,
69+ taiwan : '1000-01-01' ,
70+ thai : '2000-01-01' ,
71+ ummalqura : '1400-01-01'
72+ } ;
73+ // Start on a Sunday - for week ticks
74+ // Discworld and Mayan calendars don't have 7-day weeks anyway so don't change them.
75+ // If anyone really cares we can customize the auto tick spacings for these calendars.
76+ var CANONICAL_SUNDAY = {
77+ gregorian : '2000-01-02' ,
78+ coptic : '2000-01-03' ,
79+ discworld : '2000-01-01' ,
80+ ethiopian : '2000-01-05' ,
81+ hebrew : '5000-01-01' ,
82+ islamic : '1000-01-02' ,
83+ julian : '2000-01-03' ,
84+ mayan : '5000-01-01' ,
85+ nanakshahi : '1000-01-05' ,
86+ nepali : '2000-01-05' ,
87+ persian : '1000-01-01' ,
88+ jalali : '1000-01-01' ,
89+ taiwan : '1000-01-04' ,
90+ thai : '2000-01-04' ,
91+ ummalqura : '1400-01-06'
92+ } ;
93+
94+ var DFLTRANGE = {
95+ gregorian : [ '2000-01-01' , '2001-01-01' ] ,
96+ coptic : [ '1700-01-01' , '1701-01-01' ] ,
97+ discworld : [ '1800-01-01' , '1801-01-01' ] ,
98+ ethiopian : [ '2000-01-01' , '2001-01-01' ] ,
99+ hebrew : [ '5700-01-01' , '5701-01-01' ] ,
100+ islamic : [ '1400-01-01' , '1401-01-01' ] ,
101+ julian : [ '2000-01-01' , '2001-01-01' ] ,
102+ mayan : [ '5200-01-01' , '5201-01-01' ] ,
103+ nanakshahi : [ '0500-01-01' , '0501-01-01' ] ,
104+ nepali : [ '2000-01-01' , '2001-01-01' ] ,
105+ persian : [ '1400-01-01' , '1401-01-01' ] ,
106+ jalali : [ '1400-01-01' , '1401-01-01' ] ,
107+ taiwan : [ '0100-01-01' , '0101-01-01' ] ,
108+ thai : [ '2500-01-01' , '2501-01-01' ] ,
109+ ummalqura : [ '1400-01-01' , '1401-01-01' ]
110+ } ;
111+
112+ /*
113+ * dateTick0: get the canonical tick for this calendar
114+ *
115+ * bool sunday is for week ticks, shift it to a Sunday.
116+ */
117+ exports . dateTick0 = function ( calendar , sunday ) {
118+ calendar = ( isWorldCalendar ( calendar ) && calendar ) || 'gregorian' ;
119+ if ( sunday ) return CANONICAL_SUNDAY [ calendar ] ;
120+ return CANONICAL_TICK [ calendar ] ;
121+ } ;
122+
123+ /*
124+ * dfltRange: for each calendar, give a valid default range
125+ */
126+ exports . dfltRange = function ( calendar ) {
127+ calendar = ( isWorldCalendar ( calendar ) && calendar ) || 'gregorian' ;
128+ return DFLTRANGE [ calendar ] ;
129+ } ;
130+
52131// is an object a javascript date?
53132exports . isJSDate = function ( v ) {
54133 return typeof v === 'object' && v !== null && typeof v . getTime === 'function' ;
@@ -87,6 +166,10 @@ var MIN_MS, MAX_MS;
87166 * Note that we follow ISO 8601:2004: there *is* a year 0, which
88167 * is 1BC/BCE, and -1===2BC etc.
89168 *
169+ * World calendars: not all of these *have* agreed extensions to this full range,
170+ * if you have another calendar system but want a date range outside its validity,
171+ * you can use a gregorian date string prefixed with 'G' or 'g'.
172+ *
90173 * Where to cut off 2-digit years between 1900s and 2000s?
91174 * from http://support.microsoft.com/kb/244664:
92175 * 1930-2029 (the most retro of all...)
@@ -120,7 +203,19 @@ exports.dateTime2ms = function(s, calendar) {
120203 // otherwise only accept strings and numbers
121204 if ( typeof s !== 'string' && typeof s !== 'number' ) return BADNUM ;
122205
123- var match = String ( s ) . match ( DATETIME_REGEXP ) ;
206+ s = String ( s ) ;
207+
208+ var isWorld = isWorldCalendar ( calendar ) ;
209+
210+ // to handle out-of-range dates in international calendars, accept
211+ // 'G' as a prefix to force the built-in gregorian calendar.
212+ var s0 = s . charAt ( 0 ) ;
213+ if ( isWorld && ( s0 === 'G' || s0 === 'g' ) ) {
214+ s = s . substr ( 1 ) ;
215+ calendar = '' ;
216+ }
217+
218+ var match = s . match ( DATETIME_REGEXP ) ;
124219 if ( ! match ) return BADNUM ;
125220 var y = match [ 1 ] ,
126221 m = Number ( match [ 3 ] || 1 ) ,
@@ -129,11 +224,14 @@ exports.dateTime2ms = function(s, calendar) {
129224 M = Number ( match [ 9 ] || 0 ) ,
130225 S = Number ( match [ 11 ] || 0 ) ;
131226
132- if ( isWorldCalendar ( calendar ) ) {
227+ if ( isWorld ) {
133228 // disallow 2-digit years for world calendars
134229 if ( y . length === 2 ) return BADNUM ;
135230
136- var cDate = getCal ( calendar ) . newDate ( Number ( y ) , m , d ) ;
231+ var cDate ;
232+ try { cDate = getCal ( calendar ) . newDate ( Number ( y ) , m , d ) ; }
233+ catch ( e ) { return BADNUM ; } // Invalid ... date
234+
137235 if ( ! cDate ) return BADNUM ;
138236
139237 return ( ( cDate . toJD ( ) - EPOCHJD ) * ONEDAY ) +
@@ -192,12 +290,18 @@ exports.ms2DateTime = function(ms, r, calendar) {
192290
193291 var msecTenths = Math . floor ( mod ( ms + 0.05 , 1 ) * 10 ) ,
194292 msRounded = Math . round ( ms - msecTenths / 10 ) ,
195- dateStr , h , m , s , msec10 ;
293+ dateStr , h , m , s , msec10 , d ;
196294
197295 if ( isWorldCalendar ( calendar ) ) {
198296 var dateJD = Math . floor ( msRounded / ONEDAY ) + EPOCHJD ,
199297 timeMs = Math . floor ( mod ( ms , ONEDAY ) ) ;
200- dateStr = getCal ( calendar ) . fromJD ( dateJD ) . formatDate ( 'yyyy-mm-dd' ) ;
298+ try {
299+ dateStr = getCal ( calendar ) . fromJD ( dateJD ) . formatDate ( 'yyyy-mm-dd' ) ;
300+ }
301+ catch ( e ) {
302+ // invalid date in this calendar - fall back to Gyyyy-mm-dd
303+ dateStr = utcFormat ( 'G%Y-%m-%d' ) ( new Date ( msRounded ) ) ;
304+ }
201305
202306 // yyyy does NOT guarantee 4-digit years. YYYY mostly does, but does
203307 // other things for a few calendars, so we can't trust it. Just pad
@@ -217,7 +321,7 @@ exports.ms2DateTime = function(ms, r, calendar) {
217321 msec10 = ( r < FIVEMIN ) ? ( timeMs % ONESEC ) * 10 + msecTenths : 0 ;
218322 }
219323 else {
220- var d = new Date ( msRounded ) ;
324+ d = new Date ( msRounded ) ;
221325
222326 dateStr = utcFormat ( '%Y-%m-%d' ) ( d ) ;
223327
@@ -377,7 +481,12 @@ function modDateFormat(fmt, x, calendar) {
377481 fmt = fmt . replace ( fracMatch , fracSecs ) ;
378482 }
379483 if ( isWorldCalendar ( calendar ) ) {
380- fmt = worldCalFmt ( fmt , x , calendar ) ;
484+ try {
485+ fmt = worldCalFmt ( fmt , x , calendar ) ;
486+ }
487+ catch ( e ) {
488+ return 'Invalid' ;
489+ }
381490 }
382491 return utcFormat ( fmt ) ( d ) ;
383492}
@@ -435,19 +544,22 @@ exports.formatDate = function(x, fmt, tr, calendar) {
435544 if ( fmt ) return modDateFormat ( fmt , x , calendar ) ;
436545
437546 if ( calendar ) {
438- var dateJD = Math . floor ( x + 0.05 / ONEDAY ) + EPOCHJD ,
439- cDate = getCal ( calendar ) . fromJD ( dateJD ) ;
440-
441- if ( tr === 'y' ) dateStr = yearFormatWorld ( cDate ) ;
442- else if ( tr === 'm' ) dateStr = monthFormatWorld ( cDate ) ;
443- else if ( tr === 'd' ) {
444- headStr = yearFormatWorld ( cDate ) ;
445- dateStr = dayFormatWorld ( cDate ) ;
446- }
447- else {
448- headStr = yearMonthDayFormatWorld ( cDate ) ;
449- dateStr = formatTime ( x , tr ) ;
547+ try {
548+ var dateJD = Math . floor ( ( x + 0.05 ) / ONEDAY ) + EPOCHJD ,
549+ cDate = getCal ( calendar ) . fromJD ( dateJD ) ;
550+
551+ if ( tr === 'y' ) dateStr = yearFormatWorld ( cDate ) ;
552+ else if ( tr === 'm' ) dateStr = monthFormatWorld ( cDate ) ;
553+ else if ( tr === 'd' ) {
554+ headStr = yearFormatWorld ( cDate ) ;
555+ dateStr = dayFormatWorld ( cDate ) ;
556+ }
557+ else {
558+ headStr = yearMonthDayFormatWorld ( cDate ) ;
559+ dateStr = formatTime ( x , tr ) ;
560+ }
450561 }
562+ catch ( e ) { return 'Invalid' ; }
451563 }
452564 else {
453565 var d = new Date ( x ) ;
@@ -466,3 +578,122 @@ exports.formatDate = function(x, fmt, tr, calendar) {
466578
467579 return dateStr + ( headStr ? '\n' + headStr : '' ) ;
468580} ;
581+
582+ /*
583+ * incrementMonth: make a new milliseconds value from the given one,
584+ * having changed the month
585+ *
586+ * special case for world calendars: multiples of 12 are treated as years,
587+ * even for calendar systems that don't have (always or ever) 12 months/year
588+ * TODO: perhaps we need a different code for year increments to support this?
589+ *
590+ * ms (number): the initial millisecond value
591+ * dMonth (int): the (signed) number of months to shift
592+ * calendar (string): the calendar system to use
593+ *
594+ * changing month does not (and CANNOT) always preserve day, since
595+ * months have different lengths. The worst example of this is:
596+ * d = new Date(1970,0,31); d.setMonth(1) -> Feb 31 turns into Mar 3
597+ *
598+ * But we want to be able to iterate over the last day of each month,
599+ * regardless of what its number is.
600+ * So shift 3 days forward, THEN set the new month, then unshift:
601+ * 1/31 -> 2/28 (or 29) -> 3/31 -> 4/30 -> ...
602+ *
603+ * Note that odd behavior still exists if you start from the 26th-28th:
604+ * 1/28 -> 2/28 -> 3/31
605+ * but at least you can't shift any dates into the wrong month,
606+ * and ticks on these days incrementing by month would be very unusual
607+ */
608+ var THREEDAYS = 3 * ONEDAY ;
609+ exports . incrementMonth = function ( ms , dMonth , calendar ) {
610+ calendar = isWorldCalendar ( calendar ) && calendar ;
611+
612+ // pull time out and operate on pure dates, then add time back at the end
613+ // this gives maximum precision - not that we *normally* care if we're
614+ // incrementing by month, but better to be safe!
615+ var timeMs = mod ( ms , ONEDAY ) ;
616+ ms = Math . round ( ms - timeMs ) ;
617+
618+ if ( calendar ) {
619+ try {
620+ var dateJD = Math . round ( ms / ONEDAY ) + EPOCHJD ,
621+ calInstance = getCal ( calendar ) ,
622+ cDate = calInstance . fromJD ( dateJD ) ;
623+
624+ if ( dMonth % 12 ) calInstance . add ( cDate , dMonth , 'm' ) ;
625+ else calInstance . add ( cDate , dMonth / 12 , 'y' ) ;
626+
627+ return ( cDate . toJD ( ) - EPOCHJD ) * ONEDAY + timeMs ;
628+ }
629+ catch ( e ) {
630+ logError ( 'invalid ms ' + ms + ' in calendar ' + calendar ) ;
631+ // then keep going in gregorian even though the result will be 'Invalid'
632+ }
633+ }
634+
635+ var y = new Date ( ms + THREEDAYS ) ;
636+ return y . setUTCMonth ( y . getUTCMonth ( ) + dMonth ) + timeMs - THREEDAYS ;
637+ } ;
638+
639+ /*
640+ * findExactDates: what fraction of data is exact days, months, or years?
641+ *
642+ * data: array of millisecond values
643+ * calendar (string) the calendar to test against
644+ */
645+ exports . findExactDates = function ( data , calendar ) {
646+ var exactYears = 0 ,
647+ exactMonths = 0 ,
648+ exactDays = 0 ,
649+ blankCount = 0 ,
650+ d ,
651+ di ;
652+
653+ var calInstance = isWorldCalendar ( calendar ) && getCal ( calendar ) ;
654+
655+ for ( var i = 0 ; i < data . length ; i ++ ) {
656+ di = data [ i ] ;
657+
658+ // not date data at all
659+ if ( ! isNumeric ( di ) ) {
660+ blankCount ++ ;
661+ continue ;
662+ }
663+
664+ // not an exact date
665+ if ( di % ONEDAY ) continue ;
666+
667+ if ( calInstance ) {
668+ try {
669+ d = calInstance . fromJD ( di / ONEDAY + EPOCHJD ) ;
670+ if ( d . day ( ) === 1 ) {
671+ if ( d . month ( ) === 1 ) exactYears ++ ;
672+ else exactMonths ++ ;
673+ }
674+ else exactDays ++ ;
675+ }
676+ catch ( e ) {
677+ // invalid date in this calendar - ignore it here.
678+ }
679+ }
680+ else {
681+ d = new Date ( di ) ;
682+ if ( d . getUTCDate ( ) === 1 ) {
683+ if ( d . getUTCMonth ( ) === 0 ) exactYears ++ ;
684+ else exactMonths ++ ;
685+ }
686+ else exactDays ++ ;
687+ }
688+ }
689+ exactMonths += exactYears ;
690+ exactDays += exactMonths ;
691+
692+ var dataCount = data . length - blankCount ;
693+
694+ return {
695+ exactYears : exactYears / dataCount ,
696+ exactMonths : exactMonths / dataCount ,
697+ exactDays : exactDays / dataCount
698+ } ;
699+ } ;
0 commit comments