From 2dc3c76bb253b399bd0a2ef48822c63b91cdc982 Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Fri, 4 Jan 2019 09:43:46 +0100 Subject: [PATCH 1/4] Format ISO 8601 years (%G) --- src/locale.js | 15 +++++++++++++++ test/format-test.js | 8 ++++++++ test/parse-test.js | 6 ++++-- test/utcFormat-test.js | 8 ++++++++ test/utcParse-test.js | 28 ++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/locale.js b/src/locale.js index 1277298..7ccbc90 100644 --- a/src/locale.js +++ b/src/locale.js @@ -63,6 +63,7 @@ export default function formatLocale(locale) { "d": formatDayOfMonth, "e": formatDayOfMonth, "f": formatMicroseconds, + "G": formatFullYearISO, "H": formatHour24, "I": formatHour12, "j": formatDayOfYear, @@ -96,6 +97,7 @@ export default function formatLocale(locale) { "d": formatUTCDayOfMonth, "e": formatUTCDayOfMonth, "f": formatUTCMicroseconds, + "G": formatUTCFullYearISO, "H": formatUTCHour24, "I": formatUTCHour12, "j": formatUTCDayOfYear, @@ -129,6 +131,7 @@ export default function formatLocale(locale) { "d": parseDayOfMonth, "e": parseDayOfMonth, "f": parseMicroseconds, + "G": parseFullYear, "H": parseHour24, "I": parseHour24, "j": parseDayOfYear, @@ -572,6 +575,12 @@ function formatFullYear(d, p) { return pad(d.getFullYear() % 10000, p, 4); } +function formatFullYearISO(d, p) { + var day = d.getDay(); + d = (day >= 4 || day === 0) ? timeThursday(d) : timeThursday.ceil(d); + return pad(d.getFullYear() % 10000, p, 4); +} + function formatZone(d) { var z = d.getTimezoneOffset(); return (z > 0 ? "-" : (z *= -1, "+")) @@ -646,6 +655,12 @@ function formatUTCFullYear(d, p) { return pad(d.getUTCFullYear() % 10000, p, 4); } +function formatUTCFullYearISO(d, p) { + var day = d.getUTCDay(); + d = (day >= 4 || day === 0) ? utcThursday(d) : utcThursday.ceil(d); + return pad(d.getUTCFullYear() % 10000, p, 4); +} + function formatUTCZone() { return "+0000"; } diff --git a/test/format-test.js b/test/format-test.js index 61f701c..dbb8eeb 100644 --- a/test/format-test.js +++ b/test/format-test.js @@ -110,6 +110,14 @@ tape("timeFormat(\"%e\")(date) formats space-padded dates", function(test) { test.end(); }); +tape("timeFormat(\"%G\")(date) formats zero-padded four-digit ISO 8601 years", function (test) { + var f = timeFormat.timeFormat("%G"); + test.equal(f(date.local(2018, 11, 30, 0)), "2018"); // Sunday + test.equal(f(date.local(2018, 11, 31, 0)), "2019"); // Monday + test.equal(f(date.local(2019, 0, 1, 0)), "2019"); + test.end(); +}); + tape("timeFormat(\"%H\")(date) formats zero-padded hours (24)", function(test) { var f = timeFormat.timeFormat("%H"); test.equal(f(date.local(1990, 0, 1, 0)), "00"); diff --git a/test/parse-test.js b/test/parse-test.js index 1ba9918..c249161 100644 --- a/test/parse-test.js +++ b/test/parse-test.js @@ -72,13 +72,15 @@ tape("timeParse(\"%w %U %Y\")(date) parses numeric weekday (Sunday), week number test.end(); }); -tape("timeParse(\"%w %V %Y\")(date) parses numeric weekday, week number (ISO) and year", function(test) { - var p = timeFormat.timeParse("%w %V %Y"); +tape("timeParse(\"%w %V %G\")(date) parses numeric weekday, week number (ISO) and corresponding year", function(test) { + var p = timeFormat.timeParse("%w %V %G"); test.deepEqual(p("1 01 1990"), date.local(1990, 0, 1)); test.deepEqual(p("0 05 1991"), date.local(1991, 1, 3)); test.deepEqual(p("4 53 1992"), date.local(1992, 11, 31)); test.deepEqual(p("0 52 1994"), date.local(1995, 0, 1)); test.deepEqual(p("0 01 1995"), date.local(1995, 0, 8)); + test.deepEqual(p("1 01 2018"), date.local(2018, 0, 1)); + test.deepEqual(p("1 01 2019"), date.local(2018, 11, 31)); test.end(); }); diff --git a/test/utcFormat-test.js b/test/utcFormat-test.js index 8ea38e2..edd6d6d 100644 --- a/test/utcFormat-test.js +++ b/test/utcFormat-test.js @@ -98,6 +98,14 @@ tape("utcFormat(\"%e\")(date) formats space-padded dates", function(test) { test.end(); }); +tape("utcFormat(\"%G\")(date) formats zero-padded four-digit ISO 8601 years", function (test) { + var f = timeFormat.utcFormat("%G"); + test.equal(f(date.utc(2018, 11, 30, 0)), "2018"); // Sunday + test.equal(f(date.utc(2018, 11, 31, 0)), "2019"); // Monday + test.equal(f(date.utc(2019, 0, 1, 0)), "2019"); + test.end(); +}); + tape("utcFormat(\"%H\")(date) formats zero-padded hours (24)", function(test) { var f = timeFormat.utcFormat("%H"); test.equal(f(date.utc(1990, 0, 1, 0)), "00"); diff --git a/test/utcParse-test.js b/test/utcParse-test.js index c47c6b3..7fab666 100644 --- a/test/utcParse-test.js +++ b/test/utcParse-test.js @@ -150,6 +150,19 @@ tape("utcParse(\"%w %V %Y\")(date) parses numeric weekday, week number (ISO) and test.end(); }); +tape("utcParse(\"%w %V %G\")(date) parses numeric weekday, week number (ISO) and corresponding year", function(test) { + var p = timeFormat.utcParse("%w %V %G"); + test.deepEqual(p("1 01 1990"), date.utc(1990, 0, 1)); + test.deepEqual(p("0 05 1991"), date.utc(1991, 1, 3)); + test.deepEqual(p("4 53 1992"), date.utc(1992, 11, 31)); + test.deepEqual(p("0 52 1994"), date.utc(1995, 0, 1)); + test.deepEqual(p("0 01 1995"), date.utc(1995, 0, 8)); + test.deepEqual(p("1 01 2018"), date.utc(2018, 0, 1)); + test.deepEqual(p("1 01 2019"), date.utc(2018, 11, 31)); + test.equal(p("X 03 2010"), null); + test.end(); +}); + tape("utcParse(\"%V %Y\")(date) week number (ISO) and year", function(test) { var p = timeFormat.utcParse("%V %Y"); test.deepEqual(p("01 1990"), date.utc(1990, 0, 1)); @@ -163,6 +176,21 @@ tape("utcParse(\"%V %Y\")(date) week number (ISO) and year", function(test) { test.end(); }); +tape("utcParse(\"%V %G\")(date) week number (ISO) and corresponding year", function(test) { + var p = timeFormat.utcParse("%V %G"); + test.deepEqual(p("01 1990"), date.utc(1990, 0, 1)); + test.deepEqual(p("05 1991"), date.utc(1991, 0, 28)); + test.deepEqual(p("53 1992"), date.utc(1992, 11, 28)); + test.deepEqual(p("01 1993"), date.utc(1993, 0, 4)); + test.deepEqual(p("01 1995"), date.utc(1995, 0, 2)); + test.deepEqual(p("01 2018"), date.utc(2018, 0, 1)); + test.deepEqual(p("01 2019"), date.utc(2018, 11, 31)); + test.deepEqual(p("00 1995"), null); + test.deepEqual(p("54 1995"), null); + test.deepEqual(p("X 1995"), null); + test.end(); +}); + tape("utcParse(\"%Q\")(date) parses UNIX timestamps", function(test) { var p = timeFormat.utcParse("%Q"); test.deepEqual(p("0"), date.utc(1970, 0, 1)); From e5ff0c93e2d5db81a5b38d0d30bea757dc4011c3 Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Fri, 4 Jan 2019 10:04:45 +0100 Subject: [PATCH 2/4] Format ISO 8601 years (%g) --- src/locale.js | 29 +++++++++++++++++++++++++---- test/format-test.js | 8 ++++++++ test/parse-test.js | 12 ++++++++++++ test/utcFormat-test.js | 8 ++++++++ test/utcParse-test.js | 15 +++++++++++++++ 5 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/locale.js b/src/locale.js index 7ccbc90..6e05a68 100644 --- a/src/locale.js +++ b/src/locale.js @@ -63,6 +63,7 @@ export default function formatLocale(locale) { "d": formatDayOfMonth, "e": formatDayOfMonth, "f": formatMicroseconds, + "g": formatYearISO, "G": formatFullYearISO, "H": formatHour24, "I": formatHour12, @@ -97,6 +98,7 @@ export default function formatLocale(locale) { "d": formatUTCDayOfMonth, "e": formatUTCDayOfMonth, "f": formatUTCMicroseconds, + "g": formatUTCYearISO, "G": formatUTCFullYearISO, "H": formatUTCHour24, "I": formatUTCHour12, @@ -131,6 +133,7 @@ export default function formatLocale(locale) { "d": parseDayOfMonth, "e": parseDayOfMonth, "f": parseMicroseconds, + "g": parseYear, "G": parseFullYear, "H": parseHour24, "I": parseHour24, @@ -553,9 +556,13 @@ function formatWeekNumberSunday(d, p) { return pad(timeSunday.count(timeYear(d) - 1, d), p, 2); } -function formatWeekNumberISO(d, p) { +function dISO(d) { var day = d.getDay(); - d = (day >= 4 || day === 0) ? timeThursday(d) : timeThursday.ceil(d); + return (day >= 4 || day === 0) ? timeThursday(d) : timeThursday.ceil(d); +} + +function formatWeekNumberISO(d, p) { + d = dISO(d); return pad(timeThursday.count(timeYear(d), d) + (timeYear(d).getDay() === 4), p, 2); } @@ -571,6 +578,11 @@ function formatYear(d, p) { return pad(d.getFullYear() % 100, p, 2); } +function formatYearISO(d, p) { + d = dISO(d); + return pad(d.getFullYear() % 100, p, 2); +} + function formatFullYear(d, p) { return pad(d.getFullYear() % 10000, p, 4); } @@ -633,9 +645,13 @@ function formatUTCWeekNumberSunday(d, p) { return pad(utcSunday.count(utcYear(d) - 1, d), p, 2); } -function formatUTCWeekNumberISO(d, p) { +function UTCdISO(d) { var day = d.getUTCDay(); - d = (day >= 4 || day === 0) ? utcThursday(d) : utcThursday.ceil(d); + return (day >= 4 || day === 0) ? utcThursday(d) : utcThursday.ceil(d); +} + +function formatUTCWeekNumberISO(d, p) { + d = UTCdISO(d); return pad(utcThursday.count(utcYear(d), d) + (utcYear(d).getUTCDay() === 4), p, 2); } @@ -651,6 +667,11 @@ function formatUTCYear(d, p) { return pad(d.getUTCFullYear() % 100, p, 2); } +function formatUTCYearISO(d, p) { + d = UTCdISO(d); + return pad(d.getUTCFullYear() % 100, p, 2); +} + function formatUTCFullYear(d, p) { return pad(d.getUTCFullYear() % 10000, p, 4); } diff --git a/test/format-test.js b/test/format-test.js index dbb8eeb..dbbf831 100644 --- a/test/format-test.js +++ b/test/format-test.js @@ -110,6 +110,14 @@ tape("timeFormat(\"%e\")(date) formats space-padded dates", function(test) { test.end(); }); +tape("timeFormat(\"%g\")(date) formats zero-padded two-digit ISO 8601 years", function (test) { + var f = timeFormat.timeFormat("%g"); + test.equal(f(date.local(2018, 11, 30, 0)), "18"); // Sunday + test.equal(f(date.local(2018, 11, 31, 0)), "19"); // Monday + test.equal(f(date.local(2019, 0, 1, 0)), "19"); + test.end(); +}); + tape("timeFormat(\"%G\")(date) formats zero-padded four-digit ISO 8601 years", function (test) { var f = timeFormat.timeFormat("%G"); test.equal(f(date.local(2018, 11, 30, 0)), "2018"); // Sunday diff --git a/test/parse-test.js b/test/parse-test.js index c249161..44ab977 100644 --- a/test/parse-test.js +++ b/test/parse-test.js @@ -84,6 +84,18 @@ tape("timeParse(\"%w %V %G\")(date) parses numeric weekday, week number (ISO) an test.end(); }); +tape("timeParse(\"%w %V %g\")(date) parses numeric weekday, week number (ISO) and corresponding two-digits year", function(test) { + var p = timeFormat.timeParse("%w %V %g"); + test.deepEqual(p("1 01 90"), date.local(1990, 0, 1)); + test.deepEqual(p("0 05 91"), date.local(1991, 1, 3)); + test.deepEqual(p("4 53 92"), date.local(1992, 11, 31)); + test.deepEqual(p("0 52 94"), date.local(1995, 0, 1)); + test.deepEqual(p("0 01 95"), date.local(1995, 0, 8)); + test.deepEqual(p("1 01 18"), date.local(2018, 0, 1)); + test.deepEqual(p("1 01 19"), date.local(2018, 11, 31)); + test.end(); +}); + tape("timeParse(\"%u %U %Y\")(date) parses numeric weekday (Monday), week number (Monday) and year", function(test) { var p = timeFormat.timeParse("%u %W %Y"); test.deepEqual(p("1 00 1990"), date.local(1989, 11, 25)); diff --git a/test/utcFormat-test.js b/test/utcFormat-test.js index edd6d6d..e2fdb6c 100644 --- a/test/utcFormat-test.js +++ b/test/utcFormat-test.js @@ -98,6 +98,14 @@ tape("utcFormat(\"%e\")(date) formats space-padded dates", function(test) { test.end(); }); +tape("timeFormat(\"%g\")(date) formats zero-padded two-digit ISO 8601 years", function (test) { + var f = timeFormat.utcFormat("%g"); + test.equal(f(date.utc(2018, 11, 30, 0)), "18"); // Sunday + test.equal(f(date.utc(2018, 11, 31, 0)), "19"); // Monday + test.equal(f(date.utc(2019, 0, 1, 0)), "19"); + test.end(); +}); + tape("utcFormat(\"%G\")(date) formats zero-padded four-digit ISO 8601 years", function (test) { var f = timeFormat.utcFormat("%G"); test.equal(f(date.utc(2018, 11, 30, 0)), "2018"); // Sunday diff --git a/test/utcParse-test.js b/test/utcParse-test.js index 7fab666..bd18d4e 100644 --- a/test/utcParse-test.js +++ b/test/utcParse-test.js @@ -176,6 +176,21 @@ tape("utcParse(\"%V %Y\")(date) week number (ISO) and year", function(test) { test.end(); }); +tape("utcParse(\"%V %g\")(date) week number (ISO) and corresponding two-digits year", function(test) { + var p = timeFormat.utcParse("%V %g"); + test.deepEqual(p("01 90"), date.utc(1990, 0, 1)); + test.deepEqual(p("05 91"), date.utc(1991, 0, 28)); + test.deepEqual(p("53 92"), date.utc(1992, 11, 28)); + test.deepEqual(p("01 93"), date.utc(1993, 0, 4)); + test.deepEqual(p("01 95"), date.utc(1995, 0, 2)); + test.deepEqual(p("01 18"), date.utc(2018, 0, 1)); + test.deepEqual(p("01 19"), date.utc(2018, 11, 31)); + test.deepEqual(p("00 95"), null); + test.deepEqual(p("54 95"), null); + test.deepEqual(p("X 95"), null); + test.end(); +}); + tape("utcParse(\"%V %G\")(date) week number (ISO) and corresponding year", function(test) { var p = timeFormat.utcParse("%V %G"); test.deepEqual(p("01 1990"), date.utc(1990, 0, 1)); From ba0267e6f1fdf154a813b744973aca88a0121651 Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Fri, 4 Jan 2019 10:17:52 +0100 Subject: [PATCH 3/4] Test %V %g parsing --- test/parse-test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/parse-test.js b/test/parse-test.js index 44ab977..8c4ad67 100644 --- a/test/parse-test.js +++ b/test/parse-test.js @@ -96,6 +96,18 @@ tape("timeParse(\"%w %V %g\")(date) parses numeric weekday, week number (ISO) an test.end(); }); +tape("timeParse(\"%V %g\")(date) parses week number (ISO) and corresponding two-digits year", function(test) { + var p = timeFormat.timeParse("%V %g"); + test.deepEqual(p("01 90"), date.local(1990, 0, 1)); + test.deepEqual(p("05 91"), date.local(1991, 0, 28)); + test.deepEqual(p("53 92"), date.local(1992, 11, 28)); + test.deepEqual(p("52 94"), date.local(1994, 11, 26)); + test.deepEqual(p("01 95"), date.local(1995, 0, 2)); + test.deepEqual(p("01 18"), date.local(2018, 0, 1)); + test.deepEqual(p("01 19"), date.local(2018, 11, 31)); + test.end(); +}); + tape("timeParse(\"%u %U %Y\")(date) parses numeric weekday (Monday), week number (Monday) and year", function(test) { var p = timeFormat.timeParse("%u %W %Y"); test.deepEqual(p("1 00 1990"), date.local(1989, 11, 25)); From 0e7a6b7a7e488741b2e5d7a0f30d941c48100a8b Mon Sep 17 00:00:00 2001 From: David Nowinsky Date: Fri, 4 Jan 2019 10:28:29 +0100 Subject: [PATCH 4/4] Documentation for ISO 8601 years (%G and %g) --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9af08dd..277c930 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ Returns a new formatter for the given string *specifier*. The specifier string m * `%d` - zero-padded day of the month as a decimal number [01,31]. * `%e` - space-padded day of the month as a decimal number [ 1,31]; equivalent to `%_d`. * `%f` - microseconds as a decimal number [000000, 999999]. +* `%g` - ISO 8601 week-based year without century as a decimal number [00,99]. +* `%G` - ISO 8601 week-based year with century as a decimal number. * `%H` - hour (24-hour clock) as a decimal number [00,23]. * `%I` - hour (12-hour clock) as a decimal number [01,12]. * `%j` - day of the year as a decimal number [001,366]. @@ -136,9 +138,9 @@ Directives marked with an asterisk (\*) may be affected by the [locale definitio For `%U`, all days in a new year preceding the first Sunday are considered to be in week 0. For `%W`, all days in a new year preceding the first Monday are considered to be in week 0. Week numbers are computed using [*interval*.count](https://github.com/d3/d3-time/blob/master/README.md#interval_count). For example, 2015-52 and 2016-00 represent Monday, December 28, 2015, while 2015-53 and 2016-01 represent Monday, January 4, 2016. This differs from the [ISO week date](https://en.wikipedia.org/wiki/ISO_week_date) specification (`%V`), which uses a more complicated definition! -For `%V`, per the [strftime man page](http://man7.org/linux/man-pages/man3/strftime.3.html): +For `%V`,`%g` and `%G`, per the [strftime man page](http://man7.org/linux/man-pages/man3/strftime.3.html): -> In this system, weeks start on a Monday, and are numbered from 01, for the first week, up to 52 or 53, for the last week. Week 1 is the first week where four or more days fall within the new year (or, synonymously, week 01 is: the first week of the year that contains a Thursday; or, the week that has 4 January in it). +> In this system, weeks start on a Monday, and are numbered from 01, for the first week, up to 52 or 53, for the last week. Week 1 is the first week where four or more days fall within the new year (or, synonymously, week 01 is: the first week of the year that contains a Thursday; or, the week that has 4 January in it). If the ISO week number belongs to the previous or next year, that year is used instead. The `%` sign indicating a directive may be immediately followed by a padding modifier: