Skip to content

Commit 54a7e8b

Browse files
authored
Merge pull request #1252 from plotly/calendar-fixes
Calendar fixes
2 parents c64613c + 6dc0ef8 commit 54a7e8b

File tree

5 files changed

+179
-17
lines changed

5 files changed

+179
-17
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
"tinycolor2": "^1.3.0",
9191
"topojson-client": "^2.1.0",
9292
"webgl-context": "^2.2.0",
93-
"world-calendars": "^1.0.0"
93+
"world-calendars": "^1.0.3"
9494
},
9595
"devDependencies": {
9696
"brfs": "^1.4.3",

src/components/calendars/index.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ var DFLTRANGE = {
106106
var UNKNOWN = '##';
107107
var d3ToWorldCalendars = {
108108
'd': {'0': 'dd', '-': 'd'}, // 2-digit or unpadded day of month
109+
'e': {'0': 'd', '-': 'd'}, // alternate, always unpadded day of month
109110
'a': {'0': 'D', '-': 'D'}, // short weekday name
110111
'A': {'0': 'DD', '-': 'DD'}, // full weekday name
111112
'j': {'0': 'oo', '-': 'o'}, // 3-digit or unpadded day of the year
@@ -119,20 +120,20 @@ var d3ToWorldCalendars = {
119120
'w': UNKNOWN, // day of the week [0(sunday),6]
120121
// combined format, we replace the date part with the world-calendar version
121122
// and the %X stays there for d3 to handle with time parts
122-
'%c': {'0': 'D M m %X yyyy', '-': 'D M m %X yyyy'},
123-
'%x': {'0': 'mm/dd/yyyy', '-': 'mm/dd/yyyy'}
123+
'c': {'0': 'D M d %X yyyy', '-': 'D M d %X yyyy'},
124+
'x': {'0': 'mm/dd/yyyy', '-': 'mm/dd/yyyy'}
124125
};
125126

126127
function worldCalFmt(fmt, x, calendar) {
127-
var dateJD = Math.floor(x + 0.05 / ONEDAY) + EPOCHJD,
128+
var dateJD = Math.floor((x + 0.05) / ONEDAY) + EPOCHJD,
128129
cDate = getCal(calendar).fromJD(dateJD),
129130
i = 0,
130131
modifier, directive, directiveLen, directiveObj, replacementPart;
131132
while((i = fmt.indexOf('%', i)) !== -1) {
132133
modifier = fmt.charAt(i + 1);
133134
if(modifier === '0' || modifier === '-' || modifier === '_') {
134135
directiveLen = 3;
135-
directive = fmt.charAt(i + 1);
136+
directive = fmt.charAt(i + 2);
136137
if(modifier === '_') modifier = '-';
137138
}
138139
else {

src/lib/dates.js

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -366,16 +366,19 @@ exports.cleanDate = function(v, dflt, calendar) {
366366
* d3's vocabulary:
367367
* %{n}f where n is the max number of digits of fractional seconds
368368
*/
369-
var fracMatch = /%(\d?)f/g;
369+
var fracMatch = /%\d?f/g;
370370
function modDateFormat(fmt, x, calendar) {
371-
var fm = fmt.match(fracMatch),
372-
d = new Date(x);
373-
if(fm) {
374-
var digits = Math.min(+fm[1] || 6, 6),
375-
fracSecs = String((x / 1000 % 1) + 2.0000005)
376-
.substr(2, digits).replace(/0+$/, '') || '0';
377-
fmt = fmt.replace(fracMatch, fracSecs);
378-
}
371+
372+
fmt = fmt.replace(fracMatch, function(match) {
373+
var digits = Math.min(+(match.charAt(1)) || 6, 6),
374+
fracSecs = ((x / 1000 % 1) + 2)
375+
.toFixed(digits)
376+
.substr(2).replace(/0+$/, '') || '0';
377+
return fracSecs;
378+
});
379+
380+
var d = new Date(Math.floor(x + 0.05));
381+
379382
if(isWorldCalendar(calendar)) {
380383
try {
381384
fmt = Registry.getComponentMethod('calendars', 'worldCalFmt')(fmt, x, calendar);
@@ -393,15 +396,39 @@ function modDateFormat(fmt, x, calendar) {
393396
* tr: tickround ('M', 'S', or # digits)
394397
* only supports UTC times (where every day is 24 hours and 0 is at midnight)
395398
*/
399+
var MAXSECONDS = [59, 59.9, 59.99, 59.999, 59.9999];
396400
function formatTime(x, tr) {
397-
var timePart = mod(x, ONEDAY);
401+
var timePart = mod(x + 0.05, ONEDAY);
398402

399403
var timeStr = lpad(Math.floor(timePart / ONEHOUR), 2) + ':' +
400404
lpad(mod(Math.floor(timePart / ONEMIN), 60), 2);
401405

402406
if(tr !== 'M') {
403407
if(!isNumeric(tr)) tr = 0; // should only be 'S'
404-
timeStr += ':' + String(100 + d3.round(mod(x / ONESEC, 60), tr)).substr(1);
408+
409+
/*
410+
* this is a weird one - and shouldn't come up unless people
411+
* monkey with tick0 in weird ways, but we need to do something!
412+
* IN PARTICULAR we had better not display garbage (see below)
413+
* for numbers we always round to the nearest increment of the
414+
* precision we're showing, and this seems like the right way to
415+
* handle seconds and milliseconds, as they have a decimal point
416+
* and people will interpret that to mean rounding like numbers.
417+
* but for larger increments we floor the value: it's always
418+
* 2013 until the ball drops on the new year. We could argue about
419+
* which field it is where we start rounding (should 12:08:59
420+
* round to 12:09 if we're stopping at minutes?) but for now I'll
421+
* say we round seconds but floor everything else. BUT that means
422+
* we need to never round up to 60 seconds, ie 23:59:60
423+
*/
424+
var sec = Math.min(mod(x / ONESEC, 60), MAXSECONDS[tr]);
425+
426+
var secStr = (100 + sec).toFixed(tr).substr(1);
427+
if(tr > 0) {
428+
secStr = secStr.replace(/0+$/, '').replace(/[\.]$/, '');
429+
}
430+
431+
timeStr += ':' + secStr;
405432
}
406433
return timeStr;
407434
}
@@ -459,7 +486,7 @@ exports.formatDate = function(x, fmt, tr, calendar) {
459486
catch(e) { return 'Invalid'; }
460487
}
461488
else {
462-
var d = new Date(x);
489+
var d = new Date(Math.floor(x + 0.05));
463490

464491
if(tr === 'y') dateStr = yearFormat(d);
465492
else if(tr === 'm') dateStr = monthFormat(d);
-193 Bytes
Loading

test/jasmine/tests/lib_date_test.js

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,4 +468,138 @@ describe('dates', function() {
468468
});
469469
});
470470
});
471+
472+
describe('formatDate', function() {
473+
function assertFormatRounds(ms, calendar, results) {
474+
['y', 'm', 'd', 'M', 'S', 1, 2, 3, 4].forEach(function(tr, i) {
475+
expect(Lib.formatDate(ms, '', tr, calendar))
476+
.toBe(results[i], calendar);
477+
});
478+
}
479+
480+
it('should pick a format based on tickround if no format is provided', function() {
481+
var ms = Lib.dateTime2ms('2012-08-13 06:19:34.5678');
482+
assertFormatRounds(ms, 'gregorian', [
483+
'2012',
484+
'Aug 2012',
485+
'Aug 13\n2012',
486+
'06:19\nAug 13, 2012',
487+
'06:19:35\nAug 13, 2012',
488+
'06:19:34.6\nAug 13, 2012',
489+
'06:19:34.57\nAug 13, 2012',
490+
'06:19:34.568\nAug 13, 2012',
491+
'06:19:34.5678\nAug 13, 2012'
492+
]);
493+
494+
// and for world calendars - in coptic this is 1728-12-07 (month=Meso)
495+
assertFormatRounds(ms, 'coptic', [
496+
'1728',
497+
'Meso 1728',
498+
'Meso 7\n1728',
499+
'06:19\nMeso 7, 1728',
500+
'06:19:35\nMeso 7, 1728',
501+
'06:19:34.6\nMeso 7, 1728',
502+
'06:19:34.57\nMeso 7, 1728',
503+
'06:19:34.568\nMeso 7, 1728',
504+
'06:19:34.5678\nMeso 7, 1728'
505+
]);
506+
});
507+
508+
it('should accept custom formats using d3 specs even for world cals', function() {
509+
var ms = Lib.dateTime2ms('2012-08-13 06:19:34.5678');
510+
[
511+
// some common formats (plotly workspace options)
512+
['%Y-%m-%d', '2012-08-13', '1728-12-07'],
513+
['%H:%M:%S', '06:19:34', '06:19:34'],
514+
['%Y-%m-%e %H:%M:%S', '2012-08-13 06:19:34', '1728-12-7 06:19:34'],
515+
['%A, %b %e', 'Monday, Aug 13', 'Pesnau, Meso 7'],
516+
517+
// test padding behavior
518+
// world doesn't support space-padded (yet?)
519+
['%Y-%_m-%_d', '2012- 8-13', '1728-12-7'],
520+
['%Y-%-m-%-d', '2012-8-13', '1728-12-7'],
521+
522+
// and some strange ones to cover all fields
523+
['%a%j!%-j', 'Mon226!226', 'Pes337!337'],
524+
[
525+
'%W or un or space padded-> %-W,%_W',
526+
'33 or un or space padded-> 33,33',
527+
'48 or un or space padded-> 48,48'
528+
],
529+
[
530+
'%B \'%y WOY:%U DOW:%w',
531+
'August \'12 WOY:32 DOW:1',
532+
'Mesori \'28 WOY:## DOW:##' // world-cals doesn't support U or w
533+
],
534+
[
535+
'%c && %x && .%2f .%f', // %<n>f is our addition
536+
'Mon Aug 13 06:19:34 2012 && 08/13/2012 && .57 .5678',
537+
'Pes Meso 7 06:19:34 1728 && 12/07/1728 && .57 .5678'
538+
]
539+
540+
].forEach(function(v) {
541+
var fmt = v[0],
542+
expectedGregorian = v[1],
543+
expectedCoptic = v[2];
544+
545+
// tickround is irrelevant here...
546+
expect(Lib.formatDate(ms, fmt, 'y'))
547+
.toBe(expectedGregorian, fmt);
548+
expect(Lib.formatDate(ms, fmt, 4, 'gregorian'))
549+
.toBe(expectedGregorian, fmt);
550+
expect(Lib.formatDate(ms, fmt, 'y', 'coptic'))
551+
.toBe(expectedCoptic, fmt);
552+
});
553+
});
554+
555+
it('should not round up to 60 seconds', function() {
556+
// see note in dates.js -> formatTime about this rounding
557+
assertFormatRounds(-0.1, 'gregorian', [
558+
'1969',
559+
'Dec 1969',
560+
'Dec 31\n1969',
561+
'23:59\nDec 31, 1969',
562+
'23:59:59\nDec 31, 1969',
563+
'23:59:59.9\nDec 31, 1969',
564+
'23:59:59.99\nDec 31, 1969',
565+
'23:59:59.999\nDec 31, 1969',
566+
'23:59:59.9999\nDec 31, 1969'
567+
]);
568+
569+
// in coptic this is Koi 22, 1686
570+
assertFormatRounds(-0.1, 'coptic', [
571+
'1686',
572+
'Koi 1686',
573+
'Koi 22\n1686',
574+
'23:59\nKoi 22, 1686',
575+
'23:59:59\nKoi 22, 1686',
576+
'23:59:59.9\nKoi 22, 1686',
577+
'23:59:59.99\nKoi 22, 1686',
578+
'23:59:59.999\nKoi 22, 1686',
579+
'23:59:59.9999\nKoi 22, 1686'
580+
]);
581+
582+
// and using the custom format machinery
583+
expect(Lib.formatDate(-0.1, '%Y-%m-%d %H:%M:%S.%f'))
584+
.toBe('1969-12-31 23:59:59.9999');
585+
expect(Lib.formatDate(-0.1, '%Y-%m-%d %H:%M:%S.%f', null, 'coptic'))
586+
.toBe('1686-04-22 23:59:59.9999');
587+
588+
});
589+
590+
it('should remove extra fractional second zeros', function() {
591+
expect(Lib.formatDate(0.1, '', 4)).toBe('00:00:00.0001\nJan 1, 1970');
592+
expect(Lib.formatDate(0.1, '', 3)).toBe('00:00:00\nJan 1, 1970');
593+
expect(Lib.formatDate(0.1, '', 0)).toBe('00:00:00\nJan 1, 1970');
594+
expect(Lib.formatDate(0.1, '', 'S')).toBe('00:00:00\nJan 1, 1970');
595+
expect(Lib.formatDate(0.1, '', 3, 'coptic'))
596+
.toBe('00:00:00\nKoi 23, 1686');
597+
598+
// because the decimal point is explicitly part of the format
599+
// string here, we can't remove it OR the very first zero after it.
600+
expect(Lib.formatDate(0.1, '%S.%f')).toBe('00.0001');
601+
expect(Lib.formatDate(0.1, '%S.%3f')).toBe('00.0');
602+
});
603+
604+
});
471605
});

0 commit comments

Comments
 (0)