From ba5ff3f41c61acc2c3668ae2bbcc48dd168b1759 Mon Sep 17 00:00:00 2001 From: harttle Date: Sat, 14 Dec 2019 18:33:10 +0000 Subject: [PATCH] feat: full syntax for strftime, close #177 - strftime syntax: - new conversions: %n, %t - fixed conversions: %N - flags: _ ^ - 0 # : - modifiers: E, O (ignored) --- src/template/filter/filter.ts | 4 +- src/util/strftime.ts | 292 +++++++++++++-------------- src/util/underscore.ts | 19 +- test/integration/liquid/fs-option.ts | 2 +- test/integration/liquid/liquid.ts | 4 +- test/unit/util/strftime.ts | 291 +++++++++++++++++--------- test/unit/util/underscore.ts | 13 ++ 7 files changed, 370 insertions(+), 255 deletions(-) diff --git a/src/template/filter/filter.ts b/src/template/filter/filter.ts index c574de3746..8ec070068f 100644 --- a/src/template/filter/filter.ts +++ b/src/template/filter/filter.ts @@ -1,6 +1,6 @@ import { Expression } from '../../render/expression' import { Context } from '../../context/context' -import { isArray } from '../../util/underscore' +import { isArray, identify } from '../../util/underscore' import { FilterImplOptions } from './filter-impl-options' type KeyValuePair = [string?, string?] @@ -18,7 +18,7 @@ export class Filter { if (!impl && strictFilters) throw new TypeError(`undefined filter: ${name}`) this.name = name - this.impl = impl || (x => x) + this.impl = impl || identify this.args = args } public * render (value: any, context: Context) { diff --git a/src/util/strftime.ts b/src/util/strftime.ts index cd604520af..895184dab2 100644 --- a/src/util/strftime.ts +++ b/src/util/strftime.ts @@ -1,5 +1,6 @@ -import { padStart } from './underscore' +import { changeCase, padStart, padEnd } from './underscore' +const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/ const monthNames = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' @@ -15,170 +16,155 @@ const suffixes = { 3: 'rd', 'default': 'th' } +interface FormatOptions { + flags: object; + width?: string; + modifier?: string; +} function abbr (str: string) { return str.slice(0, 3) } // prototype extensions -const _date = { - daysInMonth: function (d: Date) { - const feb = _date.isLeapYear(d) ? 29 : 28 - return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] - }, - - getDayOfYear: function (d: Date) { - let num = 0 - for (let i = 0; i < d.getMonth(); ++i) { - num += _date.daysInMonth(d)[i] - } - return num + d.getDate() - }, - - getWeekOfYear: function (d: Date, startDay: number) { - // Skip to startDay of this week - const now = this.getDayOfYear(d) + (startDay - d.getDay()) - // Find the first startDay of the year - const jan1 = new Date(d.getFullYear(), 0, 1) - const then = (7 - jan1.getDay() + startDay) - return padStart(String(Math.floor((now - then) / 7) + 1), 2, '0') - }, - - isLeapYear: function (d: Date) { - const year = d.getFullYear() - return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year))) - }, - - getSuffix: function (d: Date) { - const str = d.getDate().toString() - const index = parseInt(str.slice(-1)) - return suffixes[index] || suffixes['default'] - }, - - century: function (d: Date) { - return parseInt(d.getFullYear().toString().substring(0, 2), 10) +function daysInMonth (d: Date) { + const feb = isLeapYear(d) ? 29 : 28 + return [31, feb, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] +} +function getDayOfYear (d: Date) { + let num = 0 + for (let i = 0; i < d.getMonth(); ++i) { + num += daysInMonth(d)[i] } + return num + d.getDate() +} +function getWeekOfYear (d: Date, startDay: number) { + // Skip to startDay of this week + const now = getDayOfYear(d) + (startDay - d.getDay()) + // Find the first startDay of the year + const jan1 = new Date(d.getFullYear(), 0, 1) + const then = (7 - jan1.getDay() + startDay) + return String(Math.floor((now - then) / 7) + 1) +} +function isLeapYear (d: Date) { + const year = d.getFullYear() + return !!((year & 3) === 0 && (year % 100 || (year % 400 === 0 && year))) +} +function getSuffix (d: Date) { + const str = d.getDate().toString() + const index = parseInt(str.slice(-1)) + return suffixes[index] || suffixes['default'] +} +function century (d: Date) { + return parseInt(d.getFullYear().toString().substring(0, 2), 10) +} + +// default to 0 +const padWidths = { + d: 2, + e: 2, + H: 2, + I: 2, + j: 3, + k: 2, + l: 2, + L: 3, + m: 2, + M: 2, + S: 2, + U: 2, + W: 2 } +// default to '0' +const padChars = { + a: ' ', + A: ' ', + b: ' ', + B: ' ', + c: ' ', + e: ' ', + k: ' ', + l: ' ', + p: ' ', + P: ' ' +} const formatCodes = { - a: function (d: Date) { - return dayNamesShort[d.getDay()] - }, - A: function (d: Date) { - return dayNames[d.getDay()] - }, - b: function (d: Date) { - return monthNamesShort[d.getMonth()] - }, - B: function (d: Date) { - return monthNames[d.getMonth()] - }, - c: function (d: Date) { - return d.toLocaleString() - }, - C: function (d: Date) { - return _date.century(d) - }, - d: function (d: Date) { - return padStart(d.getDate(), 2, '0') - }, - e: function (d: Date) { - return padStart(d.getDate(), 2) - }, - H: function (d: Date) { - return padStart(d.getHours(), 2, '0') - }, - I: function (d: Date) { - return padStart(String(d.getHours() % 12 || 12), 2, '0') - }, - j: function (d: Date) { - return padStart(_date.getDayOfYear(d), 3, '0') - }, - k: function (d: Date) { - return padStart(d.getHours(), 2) - }, - l: function (d: Date) { - return padStart(String(d.getHours() % 12 || 12), 2) - }, - L: function (d: Date) { - return padStart(d.getMilliseconds(), 3, '0') - }, - m: function (d: Date) { - return padStart(d.getMonth() + 1, 2, '0') - }, - M: function (d: Date) { - return padStart(d.getMinutes(), 2, '0') - }, - p: function (d: Date) { - return (d.getHours() < 12 ? 'AM' : 'PM') - }, - P: function (d: Date) { - return (d.getHours() < 12 ? 'am' : 'pm') - }, - q: function (d: Date) { - return _date.getSuffix(d) - }, - s: function (d: Date) { - return Math.round(d.valueOf() / 1000) - }, - S: function (d: Date) { - return padStart(d.getSeconds(), 2, '0') - }, - u: function (d: Date) { - return d.getDay() || 7 - }, - U: function (d: Date) { - return _date.getWeekOfYear(d, 0) - }, - w: function (d: Date) { - return d.getDay() - }, - W: function (d: Date) { - return _date.getWeekOfYear(d, 1) - }, - x: function (d: Date) { - return d.toLocaleDateString() - }, - X: function (d: Date) { - return d.toLocaleTimeString() - }, - y: function (d: Date) { - return d.getFullYear().toString().substring(2, 4) - }, - Y: function (d: Date) { - return d.getFullYear() - }, - z: function (d: Date) { - const tz = d.getTimezoneOffset() / 60 * 100 - return (tz > 0 ? '-' : '+') + padStart(String(Math.abs(tz)), 4, '0') - }, - '%': function () { - return '%' - } + a: (d: Date) => dayNamesShort[d.getDay()], + A: (d: Date) => dayNames[d.getDay()], + b: (d: Date) => monthNamesShort[d.getMonth()], + B: (d: Date) => monthNames[d.getMonth()], + c: (d: Date) => d.toLocaleString(), + C: (d: Date) => century(d), + d: (d: Date) => d.getDate(), + e: (d: Date) => d.getDate(), + H: (d: Date) => d.getHours(), + I: (d: Date) => String(d.getHours() % 12 || 12), + j: (d: Date) => getDayOfYear(d), + k: (d: Date) => d.getHours(), + l: (d: Date) => String(d.getHours() % 12 || 12), + L: (d: Date) => d.getMilliseconds(), + m: (d: Date) => d.getMonth() + 1, + M: (d: Date) => d.getMinutes(), + N: (d: Date, opts: FormatOptions) => { + const width = Number(opts.width) || 9 + const str = String(d.getMilliseconds()).substr(0, width) + return padEnd(str, width, '0') + }, + p: (d: Date) => (d.getHours() < 12 ? 'AM' : 'PM'), + P: (d: Date) => (d.getHours() < 12 ? 'am' : 'pm'), + q: (d: Date) => getSuffix(d), + s: (d: Date) => Math.round(d.valueOf() / 1000), + S: (d: Date) => d.getSeconds(), + u: (d: Date) => d.getDay() || 7, + U: (d: Date) => getWeekOfYear(d, 0), + w: (d: Date) => d.getDay(), + W: (d: Date) => getWeekOfYear(d, 1), + x: (d: Date) => d.toLocaleDateString(), + X: (d: Date) => d.toLocaleTimeString(), + y: (d: Date) => d.getFullYear().toString().substring(2, 4), + Y: (d: Date) => d.getFullYear(), + z: (d: Date, opts: FormatOptions) => { + const offset = d.getTimezoneOffset() + const nOffset = Math.abs(offset) + const h = Math.floor(nOffset / 60) + const m = nOffset % 60 + return (offset > 0 ? '-' : '+') + + padStart(h, 2, '0') + + (opts.flags[':'] ? ':' : '') + + padStart(m, 2, '0') + }, + 't': () => '\t', + 'n': () => '\n', + '%': () => '%' }; -(formatCodes as any).h = formatCodes.b; -(formatCodes as any).N = formatCodes.L +(formatCodes as any).h = formatCodes.b -export default function (d: Date, format: string) { +export default function (d: Date, formatStr: string) { let output = '' - let remaining = format - - while (true) { - const r = /%./g - const results = r.exec(remaining) - - // No more format codes. Add the remaining text and return - if (!results) { - return output + remaining - } - - // Add the preceding text - output += remaining.slice(0, r.lastIndex - 2) - remaining = remaining.slice(r.lastIndex) - - // Add the format code - const ch = results[0].charAt(1) - const func = formatCodes[ch] - output += func ? func(d) : '%' + ch + let remaining = formatStr + let match + while ((match = rFormat.exec(remaining))) { + output += remaining.slice(0, match.index) + remaining = remaining.slice(match.index + match[0].length) + output += format(d, match) } + return output + remaining +} + +function format (d: Date, match: RegExpExecArray) { + const [input, flagStr = '', width, modifier, conversion] = match + const convert = formatCodes[conversion] + if (!convert) return input + const flags = {} + for (const flag of flagStr) flags[flag] = true + let ret = String(convert(d, { flags, width, modifier })) + let padChar = padChars[conversion] || '0' + let padWidth = width || padWidths[conversion] || 0 + if (flags['^']) ret = ret.toUpperCase() + else if (flags['#']) ret = changeCase(ret) + if (flags['_']) padChar = ' ' + else if (flags['0']) padChar = '0' + if (flags['-']) padWidth = 0 + return padStart(ret, padWidth, padChar) } diff --git a/src/util/underscore.ts b/src/util/underscore.ts index 90335df42d..9bbc0ec106 100644 --- a/src/util/underscore.ts +++ b/src/util/underscore.ts @@ -101,8 +101,25 @@ export function range (start: number, stop: number, step = 1) { } export function padStart (str: any, length: number, ch = ' ') { + return pad(str, length, ch, (str, ch) => ch + str) +} + +export function padEnd (str: any, length: number, ch = ' ') { + return pad(str, length, ch, (str, ch) => str + ch) +} + +export function pad (str: any, length: number, ch: string, add: (str: string, ch: string) => string) { str = String(str) let n = length - str.length - while (n-- > 0) str = ch + str + while (n-- > 0) str = add(str, ch) return str } + +export function identify (val: T): T { + return val +} + +export function changeCase (str: string): string { + const hasLowerCase = [...str].some(ch => ch >= 'a' && ch <= 'z') + return hasLowerCase ? str.toUpperCase() : str.toLowerCase() +} diff --git a/test/integration/liquid/fs-option.ts b/test/integration/liquid/fs-option.ts index 3a226dabb7..2ab424de3b 100644 --- a/test/integration/liquid/fs-option.ts +++ b/test/integration/liquid/fs-option.ts @@ -12,7 +12,7 @@ describe('LiquidOptions#fs', function () { engine = new Liquid({ root: '/root/', fs - }) + } as any) }) it('should be used to read templates', function () { return engine.renderFile('files/foo') diff --git a/test/integration/liquid/liquid.ts b/test/integration/liquid/liquid.ts index 21b8231ce3..f5bb3e0adb 100644 --- a/test/integration/liquid/liquid.ts +++ b/test/integration/liquid/liquid.ts @@ -18,8 +18,8 @@ describe('Liquid', function () { }) it('should call plugin with Liquid', async function () { const engine = new Liquid() - engine.plugin(function (Liquid) { - this.registerFilter('t', x => isFalsy(x)) + engine.plugin(function () { + this.registerFilter('t', isFalsy) }) const html = await engine.parseAndRender('{{false|t}}') expect(html).to.equal('true') diff --git a/test/unit/util/strftime.ts b/test/unit/util/strftime.ts index b37ef4111f..f0a79482c9 100644 --- a/test/unit/util/strftime.ts +++ b/test/unit/util/strftime.ts @@ -7,114 +7,213 @@ describe('util/strftime', function () { const now = new Date('2016-01-04 13:15:23') const then = new Date('2016-03-06 03:05:03') - it('should format detailed datetime', function () { - expect(t(now, '%Y-%m-%d %H:%M:%S')).to.equal('2016-01-04 13:15:23') - }) - it('should format %A as Monday', function () { - expect(t(now, '%A')).to.equal('Monday') - }) - it('should format %B as month name', function () { - expect(t(now, '%B')).to.equal('January') - }) - it('should format %C as century', function () { - expect(t(now, '%C')).to.equal('20') - }) - it('should format %c as local string', function () { - expect(t(now, '%c')).to.equal(now.toLocaleString()) - }) - it('should format %e as space padded date', function () { - expect(t(now, '%e')).to.equal(' 4') - }) - it('should format %I as 0 padded hour12', function () { - expect(t(now, '%I')).to.equal('01') - }) - it('should format %I as 12 for 00:00', function () { - const date = new Date('2016-01-01 00:00:00') - expect(t(date, '%I')).to.equal('12') - }) - describe('%j', function () { - it('should format %j as day of year', function () { - expect(t(then, '%j')).to.equal('066') + describe('Date (Year, Month, Day)', () => { + it('should format %C as century', function () { + expect(t(now, '%C')).to.equal('20') }) - it('should take count of leap years', function () { - const date = new Date('2001-03-01') - expect(t(date, '%j')).to.equal('060') + it('should format %B as month name', function () { + expect(t(now, '%B')).to.equal('January') }) - it('should take count of leap years', function () { - const date = new Date('2000-03-01') - expect(t(date, '%j')).to.equal('061') + it('should format %e as space padded date', function () { + expect(t(now, '%e')).to.equal(' 4') + }) + it('should format %y as 2-digit year', function () { + expect(t(now, '%y')).to.equal('16') + }) + describe('%j', function () { + it('should format %j as day of year', function () { + expect(t(then, '%j')).to.equal('066') + }) + it('should take count of leap years', function () { + const date = new Date('2001-03-01') + expect(t(date, '%j')).to.equal('060') + }) + it('should take count of leap years', function () { + const date = new Date('2000-03-01') + expect(t(date, '%j')).to.equal('061') + }) + }) + it('should format %q as date suffix', function () { + const st = new Date('2016-03-01T03:05:03.000Z') + const nd = new Date('2016-03-02T03:05:03.000Z') + const rd = new Date('2016-03-03T03:05:03.000Z') + expect(t(st, '%q')).to.equal('st') + expect(t(nd, '%q')).to.equal('nd') + expect(t(rd, '%q')).to.equal('rd') + expect(t(now, '%q')).to.equal('th') }) }) - it('should format %k as space padded hour', function () { - expect(t(then, '%k')).to.equal(' 3') - }) - it('should format %l as space padded hour12', function () { - expect(t(now, '%l')).to.equal(' 1') - }) - it('should format %l as 12 for 00:00', function () { - const date = new Date('2016-01-01 00:00:00') - expect(t(date, '%l')).to.equal('12') - }) - it('should format %L as 0 padded millisecond', function () { - expect(t(then, '%L')).to.equal('000') - }) - it('should format %p as upper cased am/pm', function () { - expect(t(now, '%p')).to.equal('PM') - expect(t(then, '%p')).to.equal('AM') - }) - it('should format %P as lower cased am/pm', function () { - expect(t(now, '%P')).to.equal('pm') - expect(t(then, '%P')).to.equal('am') - }) - it('should format %q as date suffix', function () { - const st = new Date('2016-03-01T03:05:03.000Z') - const nd = new Date('2016-03-02T03:05:03.000Z') - const rd = new Date('2016-03-03T03:05:03.000Z') - expect(t(st, '%q')).to.equal('st') - expect(t(nd, '%q')).to.equal('nd') - expect(t(rd, '%q')).to.equal('rd') - expect(t(now, '%q')).to.equal('th') - }) - it('should format %s as UNIX seconds', function () { - expect(t(now, '%s')).to.be.match(/\d+/) - }) - it('should format %u as day of week(1-7)', function () { - expect(t(now, '%u')).to.be.equal('1') - expect(t(then, '%u')).to.be.equal('7') - }) - it('should format %U as week of year, starts with 0', function () { - expect(t(now, '%U')).to.equal('01') + + describe('Time (Hour, Minute, Second, Subsecond)', function () { + it('should format %I as 0 padded hour12', function () { + expect(t(now, '%I')).to.equal('01') + }) + it('should format %I as 12 for 00:00', function () { + const date = new Date('2016-01-01 00:00:00') + expect(t(date, '%I')).to.equal('12') + }) + it('should format %k as space padded hour', function () { + expect(t(then, '%k')).to.equal(' 3') + }) + it('should format %l as space padded hour12', function () { + expect(t(now, '%l')).to.equal(' 1') + }) + it('should format %l as 12 for 00:00', function () { + const date = new Date('2016-01-01 00:00:00') + expect(t(date, '%l')).to.equal('12') + }) + it('should format %L as 0 padded millisecond', function () { + expect(t(then, '%L')).to.equal('000') + }) + it('should format %N as fractional seconds digits', function () { + const time = new Date('2019-12-15 01:21:00.129') + expect(t(time, '%N')).to.equal('129000000') + expect(t(time, '%2N')).to.equal('12') + expect(t(time, '%10N')).to.equal('1290000000') + expect(t(time, '%0N')).to.equal('129000000') + }) + it('should format %p as upper cased am/pm', function () { + expect(t(now, '%p')).to.equal('PM') + expect(t(then, '%p')).to.equal('AM') + }) + it('should format %P as lower cased am/pm', function () { + expect(t(now, '%P')).to.equal('pm') + expect(t(now, '%^8P')).to.equal(' PM') + expect(t(then, '%P')).to.equal('am') + }) }) - it('should format %w as day of month(0-7)', function () { - expect(t(now, '%w')).to.be.equal('1') - expect(t(then, '%w')).to.be.equal('0') + + describe('Weekday', function () { + it('should format %A as Monday', function () { + expect(t(now, '%A')).to.equal('Monday') + expect(t(now, '%^A')).to.equal('MONDAY') + expect(t(now, '%#A')).to.equal('MONDAY') + }) + it('should format %a as Mon', function () { + expect(t(now, '%a')).to.equal('Mon') + expect(t(now, '%^a')).to.equal('MON') + }) + it('should format %u as day of week(1-7)', function () { + expect(t(now, '%u')).to.be.equal('1') + expect(t(then, '%u')).to.be.equal('7') + }) + it('should format %w as day of week(0-7)', function () { + expect(t(now, '%w')).to.be.equal('1') + expect(t(then, '%w')).to.be.equal('0') + }) }) - it('should format %W as week of year, starts with 1', function () { - expect(t(now, '%W')).to.be.equal('01') + + describe('Seconds since the Unix Epoch', () => { + it('should format %s as UNIX seconds', function () { + expect(t(now, '%s')).to.be.match(/\d+/) + }) }) - it('should format %x as local date string', function () { - expect(t(now, '%x')).to.equal(now.toLocaleDateString()) + + describe('Week number', () => { + it('should format %U as week of year, starts with 0', function () { + expect(t(now, '%U')).to.equal('01') + }) + it('should format %W as week of year, starts with 1', function () { + expect(t(now, '%W')).to.be.equal('01') + }) }) - it('should format %X as local time string', function () { - expect(t(now, '%X')).to.equal(now.toLocaleTimeString()) + + describe('Time zone', () => { + it('should format %z as time zone', function () { + const now = new Date('2016-01-04 13:15:23') + now.getTimezoneOffset = () => -480 // suppose we're in +8:00 + expect(t(now, '%z')).to.equal('+0800') + }) + it('should format %z as negative time zone', function () { + const date = new Date('2016-01-04T13:15:23.000Z') + date.getTimezoneOffset = () => 480 // suppose we're in -8:00 + expect(t(date, '%z')).to.equal('-0800') + }) }) - it('should format %y as 2-digit year', function () { - expect(t(now, '%y')).to.equal('16') + + describe('combination', () => { + it('should format %x as local date string', function () { + expect(t(now, '%x')).to.equal(now.toLocaleDateString()) + }) + it('should format %X as local time string', function () { + expect(t(now, '%X')).to.equal(now.toLocaleTimeString()) + }) + it('should format detailed datetime', function () { + expect(t(now, '%Y-%m-%d %H:%M:%S')).to.equal('2016-01-04 13:15:23') + }) + + it('should format %c as local string', function () { + expect(t(now, '%c')).to.equal(now.toLocaleString()) + }) }) - it('should format %z as time zone', function () { - const now = new Date('2016-01-04 13:15:23') - now.getTimezoneOffset = () => -480 // suppose we're in +8:00 - expect(t(now, '%z')).to.equal('+0800') + + describe('literal strings', () => { + it('should escape %% as %', function () { + expect(t(now, '%%')).to.equal('%') + }) + it('should escape %n as \\n', function () { + expect(t(now, '%n')).to.equal('\n') + }) + it('should escape %t as \\t', function () { + expect(t(now, '%t')).to.equal('\t') + }) + it('should retain un-recognized formaters', function () { + expect(t(now, '%o')).to.equal('%o') + }) }) - it('should format %z as negative time zone', function () { - const date = new Date('2016-01-04T13:15:23.000Z') - date.getTimezoneOffset = () => 480 // suppose we're in -8:00 - expect(t(date, '%z')).to.equal('-0800') + + describe('width field', () => { + it('should support width field', () => { + expect(t(now, '%8Y')).to.equal('00002016') + }) + it('should ignore invalid width', () => { + expect(t(then, '%1Y')).to.equal('2016') + expect(t(then, '%1H')).to.equal('3') + }) + it('should have higher priority than H', () => { + expect(t(then, '%0H')).to.equal('03') + }) }) - it('should escape %% as %', function () { - expect(t(now, '%%')).to.equal('%') + describe('modifier field', () => { + it('should ignore E modifier', () => { + expect(t(now, '%EY')).to.equal('2016') + }) + it('should ignore O modifier', () => { + expect(t(now, '%OY')).to.equal('2016') + }) + it('should support modifier with width field', () => { + expect(t(now, '%8EY')).to.equal('00002016') + }) }) - it('should retain un-recognized formaters', function () { - expect(t(now, '%o')).to.equal('%o') + describe('flags field', () => { + it('should support - flag', () => { + expect(t(now, '%-m')).to.equal('1') + }) + it('should support _ flag', () => { + expect(t(now, '%_m')).to.equal(' 1') + }) + it('should support 0 flag', () => { + expect(t(now, '%0m')).to.equal('01') + }) + it('should support ^ flag', () => { + expect(t(now, '%^B')).to.equal('JANUARY') + }) + it('should respect to specific conversion', () => { + expect(t(now, '%^P')).to.equal('PM') + expect(t(now, '%P')).to.equal('pm') + }) + it('should support # flag', () => { + expect(t(now, '%#B')).to.equal('JANUARY') + expect(t(now, '%#P')).to.equal('PM') + }) + it('should support : flag', () => { + const date = new Date('2016-01-04T13:15:23.000Z') + date.getTimezoneOffset = () => -480 // suppose we're in +8:00 + expect(t(date, '%:z')).to.equal('+08:00') + expect(t(date, '%z')).to.equal('+0800') + }) + it('should support multiple flags', () => { + expect(t(now, '%^08P')).to.equal('000000PM') + }) }) }) diff --git a/test/unit/util/underscore.ts b/test/unit/util/underscore.ts index 70e092de7e..2bc4a06fd7 100644 --- a/test/unit/util/underscore.ts +++ b/test/unit/util/underscore.ts @@ -95,4 +95,17 @@ describe('util/underscore', function () { expect(_.isObject(2)).to.be.false }) }) + describe('.padEnd()', function () { + it('should default ch to " "', () => { + expect(_.padEnd('foo', 5)).to.equal('foo ') + }) + }) + describe('.changeCase()', function () { + it('should to upper case if there is one lowercase', () => { + expect(_.changeCase('fooA')).to.equal('FOOA') + }) + it('should to lower case if all upper case', () => { + expect(_.changeCase('FOOA')).to.equal('fooa') + }) + }) })