diff --git a/lib/constants.js b/lib/constants.js index ae1f026..94664ab 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -8,6 +8,7 @@ export const u_SEC = 2 ** 6; export const u_DSEC = 2 ** 7; // decisecond export const u_CSEC = 2 ** 8; // centisecond export const u_MSEC = 2 ** 9; // millisecond +export const u_QUARTER = 2 ** 10; // Excel date boundaries export const MIN_S_DATE = 0; diff --git a/lib/index.js b/lib/index.js index fb9d096..2792273 100644 --- a/lib/index.js +++ b/lib/index.js @@ -117,6 +117,9 @@ function prepareFormatterData (pattern, shouldThrow = false) { * When the formatter encounters `*` it normally emits nothing instead of the * `*` and the next character (like Excel TEXT function does). Setting this * to a character will make the formatter emit that followed by the next one. + * @param {boolean} [options.q=false] + * Enables using the "q" for quarters. This does not work in spreadsheets so + * it is off by default. * @returns {string} A formatted value */ export function format (pattern, value, options = {}) { diff --git a/lib/options.js b/lib/options.js index 7e84f70..4088a27 100644 --- a/lib/options.js +++ b/lib/options.js @@ -9,6 +9,8 @@ export const defaultOptions = { dateSpanLarge: true, // Simulate the Lotus 1-2-3 leap year bug leap1900: true, + // Allow the use of "q" to mean quarters + q: false, // Emit regular vs. non-breaking spaces nbsp: false, // Robust/throw mode diff --git a/lib/parseFormatSection.js b/lib/parseFormatSection.js index 1d175a6..0e01067 100644 --- a/lib/parseFormatSection.js +++ b/lib/parseFormatSection.js @@ -1,7 +1,7 @@ /* eslint-disable padded-blocks */ import { resolveLocale } from './locale.js'; import { - u_YEAR, u_MONTH, u_DAY, u_HOUR, u_MIN, u_SEC, u_DSEC, u_CSEC, u_MSEC, + u_YEAR, u_QUARTER, u_MONTH, u_DAY, u_HOUR, u_MIN, u_SEC, u_DSEC, u_CSEC, u_MSEC, EPOCH_1900, EPOCH_1317, TOKEN_AMPM, TOKEN_BREAK, TOKEN_CALENDAR, TOKEN_CHAR, TOKEN_COLOR, TOKEN_COMMA, TOKEN_CONDITION, TOKEN_DATETIME, TOKEN_DBNUM, TOKEN_DIGIT, TOKEN_DURATION, TOKEN_ERROR, TOKEN_ESCAPED, TOKEN_EXP, @@ -236,34 +236,33 @@ export function parseFormatSection (inputTokens) { const bit = { type: '', size: 0, date: 1 }; const value = token.value.toLowerCase(); // deal with in tokenizer? const startsWith = value[0]; - if (value === 'y' || value === 'yy') { + if (startsWith === 'y' || startsWith === 'e') { bit.size = u_YEAR; - bit.type = 'year-short'; - } - else if (startsWith === 'y' || startsWith === 'e') { - bit.size = u_YEAR; - bit.type = 'year'; - } - else if (value === 'b' || value === 'bb') { - bit.size = u_YEAR; - bit.type = 'b-year-short'; + bit.type = value === 'y' || value === 'yy' + ? 'year-short' + : 'year'; } else if (startsWith === 'b') { bit.size = u_YEAR; - bit.type = 'b-year'; + bit.type = value === 'b' || value === 'bb' + ? 'b-year-short' + : 'b-year'; + } + else if (startsWith === 'q') { + bit.size = u_QUARTER; + bit.type = value === 'q' ? 'quarter-short' : 'quarter'; + bit.value = token.value; } else if (value === 'd' || value === 'dd') { bit.size = u_DAY; bit.type = 'day'; bit.pad = /dd/.test(value); } - else if (value === 'ddd' || value === 'aaa') { - bit.size = u_DAY; - bit.type = 'weekday-short'; - } else if (startsWith === 'd' || startsWith === 'a') { bit.size = u_DAY; - bit.type = 'weekday'; + bit.type = value === 'ddd' || value === 'aaa' + ? 'weekday-short' + : 'weekday'; } else if (startsWith === 'h') { bit.size = u_HOUR; diff --git a/lib/runPart.js b/lib/runPart.js index 07885f2..f228ed4 100644 --- a/lib/runPart.js +++ b/lib/runPart.js @@ -413,6 +413,14 @@ export function runPart (value, part, opts, l10n_) { const y = year % 100; ret.push(y < 10 ? '0' : '', y); } + else if (tokenType === 'quarter') { + const q = String(1 + ~~((month - 1) / 3)).padStart(2, '0'); + ret.push(opts.q ? q : tok.value); + } + else if (tokenType === 'quarter-short') { + const q = String(1 + ~~((month - 1) / 3)); + ret.push(opts.q ? q : tok.value); + } else if (tokenType === 'month') { ret.push((tok.pad && month < 10 ? '0' : ''), month); } diff --git a/lib/tokenize.js b/lib/tokenize.js index 647372a..5e4ba51 100644 --- a/lib/tokenize.js +++ b/lib/tokenize.js @@ -28,7 +28,7 @@ const tokenHandlers = [ [ TOKEN_DIGIT, /^[1-9]/, 0 ], [ TOKEN_CALENDAR, /^(?:B[12])/i, 0 ], [ TOKEN_ERROR, /^B$/, 0 ], // pattern must not end in a "B" - [ TOKEN_DATETIME, /^(?:[hH]+|[mM]+|[sS]+|[yY]+|[bB]+|[dD]+|[gG]+|[aA]{3,}|e+)/, 0 ], + [ TOKEN_DATETIME, /^(?:[hH]+|[mM]+|[sS]+|[yY]+|[bB]+|[dD]+|[gG]+|[qQ]+|[aA]{3,}|e+)/, 0 ], [ TOKEN_DURATION, /^(?:\[(h+|m+|s+)\])/i, 1 ], [ TOKEN_CONDITION, /^\[(<[=>]?|>=?|=)\s*(-?[.\d]+)\]/, [ 1, 2 ] ], [ TOKEN_DBNUM, /^\[(DBNum[0-4]?\d)\]/i, 1 ], diff --git a/test/date-parsing-test.js b/test/date-parsing-test.js index c83821b..959327d 100644 --- a/test/date-parsing-test.js +++ b/test/date-parsing-test.js @@ -93,6 +93,38 @@ test('Date specifiers:', t => { t.end(); }); +test('Date specifiers: optional quarters', t => { + // default operation + t.format('yyyy\\Qq', date, '1909Qq'); + t.format('yyyy\\Qqq', date, '1909Qqq'); + t.format('yyyy\\Qqqq', date, '1909Qqqq'); + t.format('yyyy\\Qqqqq', date, '1909Qqqqq'); + t.format('yyyy\\Qqqqqq', date, '1909Qqqqqq'); + // opting into quarters + t.format('yyyy\\Qq', date, '1909Q1', { q: true }); + t.format('yyyy\\Qqq', date, '1909Q01', { q: true }); + t.format('yyyy\\Qqqq', date, '1909Q01', { q: true }); + t.format('yyyy\\Qqqqq', date, '1909Q01', { q: true }); + t.format('yyyy\\Qqqqqq', date, '1909Q01', { q: true }); + t.format('yyyy"Q"q', date, '1909Q1', { q: true }); + t.format('yyyy-"Q"Q', date, '1909-Q1', { q: true }); + t.format('yyyy-"Q"Q', date, '1909-Q1', { q: true }); + // correct quarters + t.format('yyyy"Q"q', 31413, '1986Q1', { q: true }); + t.format('yyyy"Q"q', 31444, '1986Q1', { q: true }); + t.format('yyyy"Q"q', 31472, '1986Q1', { q: true }); + t.format('yyyy"Q"q', 31503, '1986Q2', { q: true }); + t.format('yyyy"Q"q', 31533, '1986Q2', { q: true }); + t.format('yyyy"Q"q', 31564, '1986Q2', { q: true }); + t.format('yyyy"Q"q', 31594, '1986Q3', { q: true }); + t.format('yyyy"Q"q', 31625, '1986Q3', { q: true }); + t.format('yyyy"Q"q', 31656, '1986Q3', { q: true }); + t.format('yyyy"Q"q', 31686, '1986Q4', { q: true }); + t.format('yyyy"Q"q', 31717, '1986Q4', { q: true }); + t.format('yyyy"Q"q', 31747, '1986Q4', { q: true }); + t.end(); +}); + test('Date specifiers: month vs. minute', t => { t.format('h', date, '3'); t.format('m', date, '1');