diff --git a/README.md b/README.md index 16f7764..23b0abc 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,8 @@ Returns a new [*format* function](#_format) for the given string *specifier*. Th Directives marked with an asterisk (*) may be affected by the [locale definition](#localeFormat). 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#interval_count). +The `%` sign indicating a directive may be immediately followed by a sign modifier, `+`. If no sign modifier is specified, the field will be displayed as unsigned. Otherwise, the field will be formatted with a plus sign (`+`) for positive or zero values and a minus sign (`-`) for negative values. + The `%` sign indicating a directive may be immediately followed by a padding modifier: * `0` - zero-padding diff --git a/src/locale.js b/src/locale.js index 6af7a20..77e9522 100644 --- a/src/locale.js +++ b/src/locale.js @@ -145,9 +145,24 @@ export default function(locale) { while (++i < n) { if (specifier.charCodeAt(i) === 37) { string.push(specifier.slice(j, i)); - if ((pad = pads[c = specifier.charAt(++i)]) != null) c = specifier.charAt(++i); - else pad = c === "e" ? " " : "0"; - if (format = formats[c]) c = format(date, pad); + switch (c = specifier.charAt(++i)) { + case "+": { + switch (c = specifier.charAt(++i)) { + case "-": pad = padNoneSigned; break; + case "_": pad = padSpaceSigned; break; + case "0": pad = padZeroSigned; break; + case "e": pad = padSpaceSigned; --i; break; + default: pad = padZeroSigned; --i; break; + } + break; + } + case "-": pad = padNone; break; + case "_": pad = padSpace; break; + case "0": pad = padZero; break; + case "e": pad = padSpace; --i; break; + default: pad = padZero; --i; break; + } + if (format = formats[c = specifier.charAt(++i)]) c = format(date, pad); string.push(c); j = i + 1; } @@ -199,8 +214,10 @@ export default function(locale) { if (j >= m) return -1; c = specifier.charCodeAt(i++); if (c === 37) { - c = specifier.charAt(i++); - parse = parses[c in pads ? specifier.charAt(i++) : c]; + switch (c = specifier.charAt(i++)) { + case "-": case "_": case "0": c = specifier.charAt(i++); break; + } + parse = parses[c]; if (!parse || ((j = parse(d, string, j)) < 0)) return -1; } else if (c != string.charCodeAt(j++)) { return -1; @@ -303,16 +320,37 @@ export default function(locale) { }; }; -var pads = {"-": "", "_": " ", "0": "0"}, - numberRe = /^\s*\d+/, // note: ignores next directive +var numberRe = /^\s*\d+/, // note: ignores next directive + // TODO signedNumberRe = /^\s*[+-]\d+/, percentRe = /^%/, requoteRe = /[\\\^\$\*\+\?\|\[\]\(\)\.\{\}]/g; -function pad(value, fill, width) { - var sign = value < 0 ? "-" : "", - string = (sign ? -value : value) + "", - length = string.length; - return sign + (length < width ? new Array(width - length + 1).join(fill) + string : string); +function padNone(value) { + return Math.abs(value) + ""; +} + +function padZero(value, width) { + var length = (value = Math.abs(value) + "").length; + return length < width ? new Array(width - length + 1).join("0") + value : value; +} + +function padSpace(value, width) { + var length = (value = Math.abs(value) + "").length; + return length < width ? new Array(width - length + 1).join(" ") + value : value; +} + +function padNoneSigned(value) { + return (value < 0 ? "-" : "+") + Math.abs(value); +} + +function padZeroSigned(value, width) { + var sign = value < 0 ? "-" : "+", length = (value = Math.abs(value) + "").length; + return sign + (length < width ? new Array(width - length + 1).join("0") + value : value); +} + +function padSpaceSigned(value, width) { + var sign = value < 0 ? "-" : "+", length = (value = Math.abs(value) + "").length; + return (length < width ? new Array(width - length + 1).join(" ") + sign + value : sign + value); } function requote(s) { @@ -399,115 +437,115 @@ function parseLiteralPercent(d, string, i) { return n ? i + n[0].length : -1; } -function formatDayOfMonth(d, p) { - return pad(d.getDate(), p, 2); +function formatDayOfMonth(d, pad) { + return pad(d.getDate(), 2); } -function formatHour24(d, p) { - return pad(d.getHours(), p, 2); +function formatHour24(d, pad) { + return pad(d.getHours(), 2); } -function formatHour12(d, p) { - return pad(d.getHours() % 12 || 12, p, 2); +function formatHour12(d, pad) { + return pad(d.getHours() % 12 || 12, 2); } -function formatDayOfYear(d, p) { - return pad(1 + day.count(year(d), d), p, 3); +function formatDayOfYear(d, pad) { + return pad(1 + day.count(year(d), d), 3); } -function formatMilliseconds(d, p) { - return pad(d.getMilliseconds(), p, 3); +function formatMilliseconds(d, pad) { + return pad(d.getMilliseconds(), 3); } -function formatMonthNumber(d, p) { - return pad(d.getMonth() + 1, p, 2); +function formatMonthNumber(d, pad) { + return pad(d.getMonth() + 1, 2); } -function formatMinutes(d, p) { - return pad(d.getMinutes(), p, 2); +function formatMinutes(d, pad) { + return pad(d.getMinutes(), 2); } -function formatSeconds(d, p) { - return pad(d.getSeconds(), p, 2); +function formatSeconds(d, pad) { + return pad(d.getSeconds(), 2); } -function formatWeekNumberSunday(d, p) { - return pad(sunday.count(year(d), d), p, 2); +function formatWeekNumberSunday(d, pad) { + return pad(sunday.count(year(d), d), 2); } -function formatWeekdayNumber(d) { - return d.getDay(); +function formatWeekdayNumber(d, pad) { + return pad(d.getDay(), 1); } -function formatWeekNumberMonday(d, p) { - return pad(monday.count(year(d), d), p, 2); +function formatWeekNumberMonday(d, pad) { + return pad(monday.count(year(d), d), 2); } -function formatYear(d, p) { - return pad(d.getFullYear() % 100, p, 2); +function formatYear(d, pad) { + return pad(d.getFullYear() % 100, 2); } -function formatFullYear(d, p) { - return pad(d.getFullYear() % 10000, p, 4); +function formatFullYear(d, pad) { + return pad(d.getFullYear() % 10000, 4); } function formatZone(d) { var z = d.getTimezoneOffset(); return (z > 0 ? "-" : (z *= -1, "+")) - + pad(z / 60 | 0, "0", 2) - + pad(z % 60, "0", 2); + + padZero(z / 60 | 0, 2) + + padZero(z % 60, 2); } -function formatUTCDayOfMonth(d, p) { - return pad(d.getUTCDate(), p, 2); +function formatUTCDayOfMonth(d, pad) { + return pad(d.getUTCDate(), 2); } -function formatUTCHour24(d, p) { - return pad(d.getUTCHours(), p, 2); +function formatUTCHour24(d, pad) { + return pad(d.getUTCHours(), 2); } -function formatUTCHour12(d, p) { - return pad(d.getUTCHours() % 12 || 12, p, 2); +function formatUTCHour12(d, pad) { + return pad(d.getUTCHours() % 12 || 12, 2); } -function formatUTCDayOfYear(d, p) { - return pad(1 + utcDay.count(utcYear(d), d), p, 3); +function formatUTCDayOfYear(d, pad) { + return pad(1 + utcDay.count(utcYear(d), d), 3); } -function formatUTCMilliseconds(d, p) { - return pad(d.getUTCMilliseconds(), p, 3); +function formatUTCMilliseconds(d, pad) { + return pad(d.getUTCMilliseconds(), 3); } -function formatUTCMonthNumber(d, p) { - return pad(d.getUTCMonth() + 1, p, 2); +function formatUTCMonthNumber(d, pad) { + return pad(d.getUTCMonth() + 1, 2); } -function formatUTCMinutes(d, p) { - return pad(d.getUTCMinutes(), p, 2); +function formatUTCMinutes(d, pad) { + return pad(d.getUTCMinutes(), 2); } -function formatUTCSeconds(d, p) { - return pad(d.getUTCSeconds(), p, 2); +function formatUTCSeconds(d, pad) { + return pad(d.getUTCSeconds(), 2); } -function formatUTCWeekNumberSunday(d, p) { - return pad(utcSunday.count(utcYear(d), d), p, 2); +function formatUTCWeekNumberSunday(d, pad) { + return pad(utcSunday.count(utcYear(d), d), 2); } -function formatUTCWeekdayNumber(d) { - return d.getUTCDay(); +function formatUTCWeekdayNumber(d, pad) { + return pad(d.getUTCDay(), 1); } -function formatUTCWeekNumberMonday(d, p) { - return pad(utcMonday.count(utcYear(d), d), p, 2); +function formatUTCWeekNumberMonday(d, pad) { + return pad(utcMonday.count(utcYear(d), d), 2); } -function formatUTCYear(d, p) { - return pad(d.getUTCFullYear() % 100, p, 2); +function formatUTCYear(d, pad) { + return pad(d.getUTCFullYear() % 100, 2); } -function formatUTCFullYear(d, p) { - return pad(d.getUTCFullYear() % 10000, p, 4); +function formatUTCFullYear(d, pad) { + return pad(d.getUTCFullYear() % 10000, 4); } function formatUTCZone() { diff --git a/test/format-parse-test.js b/test/format-parse-test.js index 5e7796e..c7201f1 100644 --- a/test/format-parse-test.js +++ b/test/format-parse-test.js @@ -98,6 +98,17 @@ tape("format(\"%m/%d/%y\").parse(date) parses month, date and two-digit year", f test.end(); }); +// TODO +// tape("format(\"%m/%d/%+y\").parse(date) parses month, date and signed two-digit year", function(test) { +// var p = timeFormat.format("%m/%d/%y").parse; +// test.deepEqual(p("02/03/+69"), date.local(1969, 1, 3)); +// test.deepEqual(p("01/01/+90"), date.local(1990, 0, 1)); +// test.deepEqual(p("02/03/+91"), date.local(1991, 1, 3)); +// test.deepEqual(p("02/03/-68"), date.local(-68, 1, 3)); +// test.equal(p("03/10/10"), null); +// test.end(); +// }); + tape("format(\"%x\").parse(date) parses locale date", function(test) { var p = timeFormat.format("%x").parse; test.deepEqual(p("01/01/1990"), date.local(1990, 0, 1)); diff --git a/test/format-test.js b/test/format-test.js index e4bd305..f5c94eb 100644 --- a/test/format-test.js +++ b/test/format-test.js @@ -210,7 +210,7 @@ tape("format(\"%y\")(date) formats zero-padded two-digit years", function(test) var f = timeFormat.format("%y"); test.equal(f(date.local(+1990, 0, 1)), "90"); test.equal(f(date.local(+2002, 0, 1)), "02"); - test.equal(f(date.local(-0002, 0, 1)), "-02"); + test.equal(f(date.local(-0002, 0, 1)), "02"); // unsigned year! test.end(); }); @@ -220,10 +220,46 @@ tape("format(\"%Y\")(date) formats zero-padded four-digit years", function(test) test.equal(f(date.local( 1990, 0, 1)), "1990"); test.equal(f(date.local( 2002, 0, 1)), "2002"); test.equal(f(date.local(10002, 0, 1)), "0002"); + test.equal(f(date.local( -2, 0, 1)), "0002"); // unsigned year! + test.end(); +}); + +tape("format(\"%+y\")(date) formats zero-padded signed two-digit years", function(test) { + var f = timeFormat.format("%+y"); + test.equal(f(date.local(+1990, 0, 1)), "+90"); + test.equal(f(date.local(+2002, 0, 1)), "+02"); + test.equal(f(date.local(-0002, 0, 1)), "-02"); + test.end(); +}); + +tape("format(\"%+Y\")(date) formats zero-padded signed four-digit years", function(test) { + var f = timeFormat.format("%+Y"); + test.equal(f(date.local( 123, 0, 1)), "+0123"); + test.equal(f(date.local( 1990, 0, 1)), "+1990"); + test.equal(f(date.local( 2002, 0, 1)), "+2002"); + test.equal(f(date.local(10002, 0, 1)), "+0002"); test.equal(f(date.local( -2, 0, 1)), "-0002"); test.end(); }); +tape("format(\"%+_y\")(date) formats space-padded signed two-digit years", function(test) { + var f = timeFormat.format("%+_y"); + test.equal(f(date.local(+1990, 0, 1)), "+90"); + test.equal(f(date.local(+2002, 0, 1)), " +2"); + test.equal(f(date.local(-0002, 0, 1)), " -2"); + test.end(); +}); + +tape("format(\"%+_Y\")(date) formats space-padded signed four-digit years", function(test) { + var f = timeFormat.format("%+_Y"); + test.equal(f(date.local( 123, 0, 1)), " +123"); + test.equal(f(date.local( 1990, 0, 1)), "+1990"); + test.equal(f(date.local( 2002, 0, 1)), "+2002"); + test.equal(f(date.local(10002, 0, 1)), " +2"); + test.equal(f(date.local( -2, 0, 1)), " -2"); + test.end(); +}); + tape("format(\"%Z\")(date) formats time zones", function(test) { var f = timeFormat.format("%Z"); test.equal(f(date.local(1990, 0, 1)), "-0800"); diff --git a/test/utcFormat-test.js b/test/utcFormat-test.js index 6a3b8fb..276678a 100644 --- a/test/utcFormat-test.js +++ b/test/utcFormat-test.js @@ -210,7 +210,7 @@ tape("utcFormat(\"%y\")(date) formats zero-padded two-digit years", function(tes var f = timeFormat.utcFormat("%y"); test.equal(f(date.utc(+1990, 0, 1)), "90"); test.equal(f(date.utc(+2002, 0, 1)), "02"); - test.equal(f(date.utc(-0002, 0, 1)), "-02"); + test.equal(f(date.utc(-0002, 0, 1)), "02"); // unsigned year! test.end(); }); @@ -220,10 +220,46 @@ tape("utcFormat(\"%Y\")(date) formats zero-padded four-digit years", function(te test.equal(f(date.utc( 1990, 0, 1)), "1990"); test.equal(f(date.utc( 2002, 0, 1)), "2002"); test.equal(f(date.utc(10002, 0, 1)), "0002"); + test.equal(f(date.utc( -2, 0, 1)), "0002"); // unsigned year! + test.end(); +}); + +tape("utcFormat(\"%+y\")(date) formats zero-padded signed two-digit years", function(test) { + var f = timeFormat.utcFormat("%+y"); + test.equal(f(date.utc(+1990, 0, 1)), "+90"); + test.equal(f(date.utc(+2002, 0, 1)), "+02"); + test.equal(f(date.utc(-0002, 0, 1)), "-02"); + test.end(); +}); + +tape("utcFormat(\"%+Y\")(date) formats zero-padded signed four-digit years", function(test) { + var f = timeFormat.utcFormat("%+Y"); + test.equal(f(date.utc( 123, 0, 1)), "+0123"); + test.equal(f(date.utc( 1990, 0, 1)), "+1990"); + test.equal(f(date.utc( 2002, 0, 1)), "+2002"); + test.equal(f(date.utc(10002, 0, 1)), "+0002"); test.equal(f(date.utc( -2, 0, 1)), "-0002"); test.end(); }); +tape("utcFormat(\"%+_y\")(date) formats space-padded signed two-digit years", function(test) { + var f = timeFormat.utcFormat("%+_y"); + test.equal(f(date.utc(+1990, 0, 1)), "+90"); + test.equal(f(date.utc(+2002, 0, 1)), " +2"); + test.equal(f(date.utc(-0002, 0, 1)), " -2"); + test.end(); +}); + +tape("utcFormat(\"%+_Y\")(date) formats space-padded signed four-digit years", function(test) { + var f = timeFormat.utcFormat("%+_Y"); + test.equal(f(date.utc( 123, 0, 1)), " +123"); + test.equal(f(date.utc( 1990, 0, 1)), "+1990"); + test.equal(f(date.utc( 2002, 0, 1)), "+2002"); + test.equal(f(date.utc(10002, 0, 1)), " +2"); + test.equal(f(date.utc( -2, 0, 1)), " -2"); + test.end(); +}); + tape("utcFormat(\"%Z\")(date) formats time zones", function(test) { var f = timeFormat.utcFormat("%Z"); test.equal(f(date.utc(1990, 0, 1)), "+0000");