From e4f4b02833c619ebf1c1790209e55c179e754ee3 Mon Sep 17 00:00:00 2001 From: Moshe Jonathan Gordon Radian Date: Sun, 6 Dec 2020 02:36:23 +0200 Subject: [PATCH 1/5] feat: add preparse and postformat features to locales as a plugin --- src/plugin/preParsePostFormat/index.js | 53 ++++++++++++++++++++++++++ src/plugin/relativeTime/index.js | 12 +++++- types/plugin/preParsePostFormat.d.ts | 4 ++ 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 src/plugin/preParsePostFormat/index.js create mode 100644 types/plugin/preParsePostFormat.d.ts diff --git a/src/plugin/preParsePostFormat/index.js b/src/plugin/preParsePostFormat/index.js new file mode 100644 index 000000000..91fbf9ed8 --- /dev/null +++ b/src/plugin/preParsePostFormat/index.js @@ -0,0 +1,53 @@ +import localeData from '../localeData' + +// Plugin template from https://day.js.org/docs/en/plugin/plugin +export default (option, dayjsClass, dayjsFactory) => { + // This plugin depends on other plugins - so I will import them here + // equivalent to dayjsClass.extend(duration) + localeData(option, dayjsClass, dayjsFactory) + + const oldParse = dayjsClass.prototype.parse + dayjsClass.prototype.parse = function (cfg) { + if (typeof cfg.date === 'string') { + const locale = this.$locale() + cfg.date = + locale && locale.preparse ? locale.preparse(cfg.date) : cfg.date + } + // original parse result + return oldParse.bind(this)(cfg) + } + + // // overriding existing API + // // e.g. extend dayjs().format() + const oldFormat = dayjsClass.prototype.format + dayjsClass.prototype.format = function (...args) { + // original format result + const result = oldFormat.call(this, ...args) + // return modified result + const locale = this.$locale() + return locale.postformat ? locale.postformat(result) : result + } + + const oldFromTo = dayjsClass.prototype.fromToBase + + if (oldFromTo) { + dayjsClass.prototype.fromToBase = function ( + input, + withoutSuffix, + instance, + isFrom + ) { + const locale = this.$locale() || instance.$locale() + + // original format result + return oldFromTo.call( + this, + input, + withoutSuffix, + instance, + isFrom, + locale.postformat + ) + } + } +} diff --git a/src/plugin/relativeTime/index.js b/src/plugin/relativeTime/index.js index e11b67a8e..8d61c45b0 100644 --- a/src/plugin/relativeTime/index.js +++ b/src/plugin/relativeTime/index.js @@ -19,7 +19,7 @@ export default (o, c, d) => { yy: '%d years' } d.en.relativeTime = relObj - const fromTo = (input, withoutSuffix, instance, isFrom) => { + proto.fromToBase = (input, withoutSuffix, instance, isFrom, postFormat) => { const loc = instance.$locale().relativeTime || relObj const T = o.thresholds || [ { l: 's', r: 44, d: C.S }, @@ -46,11 +46,14 @@ export default (o, c, d) => { ? d(input).diff(instance, t.d, true) : instance.diff(input, t.d, true) } - const abs = (o.rounding || Math.round)(Math.abs(result)) + let abs = (o.rounding || Math.round)(Math.abs(result)) isFuture = result > 0 if (abs <= t.r || !t.r) { if (abs <= 1 && i > 0) t = T[i - 1] // 1 minutes -> a minute, 0 seconds -> 0 second const format = loc[t.l] + if (postFormat) { + abs = postFormat(`${abs}`) + } if (typeof format === 'string') { out = format.replace('%d', abs) } else { @@ -66,6 +69,11 @@ export default (o, c, d) => { } return pastOrFuture.replace('%s', out) } + + function fromTo(input, withoutSuffix, instance, isFrom) { + return proto.fromToBase(input, withoutSuffix, instance, isFrom) + } + proto.to = function (input, withoutSuffix) { return fromTo(input, withoutSuffix, this, true) } diff --git a/types/plugin/preParsePostFormat.d.ts b/types/plugin/preParsePostFormat.d.ts new file mode 100644 index 000000000..30ec75e5d --- /dev/null +++ b/types/plugin/preParsePostFormat.d.ts @@ -0,0 +1,4 @@ +import { PluginFunc } from 'dayjs' + +declare const plugin: PluginFunc +export = plugin From 0211f07446f935394aef04aac8f3638b02f724b1 Mon Sep 17 00:00:00 2001 From: Moshe Jonathan Gordon Radian Date: Sun, 6 Dec 2020 02:38:41 +0200 Subject: [PATCH 2/5] test: translate moment tests for preparse postformat for the new plugin --- test/plugin/preParsePostFormat.test.js | 167 +++++++++++++++++++++++++ 1 file changed, 167 insertions(+) create mode 100755 test/plugin/preParsePostFormat.test.js diff --git a/test/plugin/preParsePostFormat.test.js b/test/plugin/preParsePostFormat.test.js new file mode 100755 index 000000000..88623496c --- /dev/null +++ b/test/plugin/preParsePostFormat.test.js @@ -0,0 +1,167 @@ +import MockDate from 'mockdate' +// import moment from 'moment' +import dayjs from '../../src' +import preParsePostFormat from '../../src/plugin/preParsePostFormat' +import duration from '../../src/plugin/duration' +import calendar from '../../src/plugin/calendar' +import objectSupport from '../../src/plugin/objectSupport' +import customParseFormat from '../../src/plugin/customParseFormat' +import relativeTime from '../../src/plugin/relativeTime' +import utc from '../../src/plugin/utc' +import arraySupport from '../../src/plugin/arraySupport' +import en from '../../src/locale/en' + +dayjs.extend(utc) +dayjs.extend(customParseFormat) +dayjs.extend(arraySupport) +dayjs.extend(objectSupport) +dayjs.extend(calendar) +dayjs.extend(duration) +dayjs.extend(relativeTime) +dayjs.extend(preParsePostFormat) + +const symbolMap = { + 1: '!', + 2: '@', + 3: '#', + 4: '$', + 5: '%', + 6: '^', + 7: '&', + 8: '*', + 9: '(', + 0: ')' +} +const numberMap = { + '!': '1', + '@': '2', + '#': '3', + $: '4', + '%': '5', + '^': '6', + '&': '7', + '*': '8', + '(': '9', + ')': '0' +} + +const localeCustomizations = { + ...en, + preparse(string) { + if (typeof string !== 'string') { + // console.error('preparse - Expected string, got', { + // string + // }) + throw new Error(`preparse - Expected string, got ${typeof string}`) + } + try { + const res = string.replace(/[!@#$%^&*()]/g, match => numberMap[match]) + // console.log('Called custom preparse', { string, res }) + return res + } catch (error) { + const errorMsg = `Unexpected error during preparse of '${string}' - ${error}` + // console.error(errorMsg) + throw new Error(errorMsg) + } + }, + postformat(string) { + if (typeof string !== 'string') { + // console.error('postformat - Expected string, got', { + // string + // }) + throw new Error(`postformat - Expected string, got ${typeof string}`) + } + try { + const res = string.replace(/\d/g, match => symbolMap[match]) + // console.log('Called custom postformat', { string, res }) + return res + } catch (error) { + const errorMsg = `Unexpected error during postFormat of '${string}' - ${error}` + // console.error(errorMsg) + throw new Error(errorMsg) + } + } +} + +beforeEach(() => { + MockDate.set(new Date()) + dayjs.locale('symbol', localeCustomizations) +}) + +afterEach(() => { + MockDate.reset() + dayjs.locale('symbol', null) +}) + +describe('preparse and postformat', () => { + describe('transform', () => { + const TEST_DATE = '@)!@-)*-@&' + const TEST_NUM = 1346025600 + it('preparse string + format', () => + expect(dayjs.utc(TEST_DATE, 'YYYY-MM-DD').unix()).toBe(TEST_NUM)) + it('preparse ISO8601 string', () => + expect(dayjs.utc(TEST_DATE).unix()).toBe(TEST_NUM)) + it('postformat', () => + expect(dayjs + .unix(TEST_NUM) + .utc() + .format('YYYY-MM-DD')) + .toBe(TEST_DATE)) + }) + + describe('transform from', () => { + dayjs.locale('symbol', localeCustomizations) + const start = dayjs([2007, 1, 28]) + + const t1 = dayjs([2007, 1, 28]).add({ s: 90 }) + it('postformat should work on dayjs.fn.from', () => + expect(start.from(t1, true)).toBe('@ minutes')) + + const t2 = dayjs().add(6, 'd') + it('postformat should work on dayjs.fn.fromNow', () => + expect(t2.fromNow(true)).toBe('^ days')) + + it('postformat should work on dayjs.duration.fn.humanize', () => + expect(dayjs.duration(10, 'h').humanize()).toBe('!) hours')) + }) +}) + +describe('calendar day', () => { + const a = dayjs() + .hour(12) + .minute(0) + .second(0) + + it('today at the same time', () => + expect(dayjs(a).calendar()).toBe('Today at !@:)) PM')) + + it('Now plus 25 min', () => + expect(dayjs(a) + .add({ m: 25 }) + .calendar()) + .toBe('Today at !@:@% PM')) + + it('Now plus 1 hour', () => + expect(dayjs(a) + .add({ h: 1 }) + .calendar()) + .toBe('Today at !:)) PM')) + + it('tomorrow at the same time', () => + expect(dayjs(a) + .add({ d: 1 }) + .calendar()) + .toBe('Tomorrow at !@:)) PM')) + + it('Now minus 1 hour', () => + expect(dayjs(a) + .subtract({ h: 1 }) + .calendar()) + .toBe('Today at !!:)) AM')) + + it('yesterday at the same time', () => + expect(dayjs(a) + .subtract({ d: 1 }) + .calendar()) + .toBe('Yesterday at !@:)) PM')) +}) From e26e802d767eec89aae02c8cecf87f517600a698 Mon Sep 17 00:00:00 2001 From: Moshe Jonathan Gordon Radian Date: Sun, 6 Dec 2020 02:39:46 +0200 Subject: [PATCH 3/5] feat: add preparse postformat to ar locale from moment --- src/locale/ar.js | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/locale/ar.js b/src/locale/ar.js index 3a7bb3851..a7025c442 100644 --- a/src/locale/ar.js +++ b/src/locale/ar.js @@ -2,6 +2,31 @@ import dayjs from 'dayjs' const months = 'يناير_فبراير_مارس_أبريل_مايو_يونيو_يوليو_أغسطس_سبتمبر_أكتوبر_نوفمبر_ديسمبر'.split('_') +const symbolMap = { + 1: '١', + 2: '٢', + 3: '٣', + 4: '٤', + 5: '٥', + 6: '٦', + 7: '٧', + 8: '٨', + 9: '٩', + 0: '٠' +} + +const numberMap = { + '١': '1', + '٢': '2', + '٣': '3', + '٤': '4', + '٥': '5', + '٦': '6', + '٧': '7', + '٨': '8', + '٩': '9', + '٠': '0' +} const locale = { name: 'ar', @@ -26,6 +51,19 @@ const locale = { y: 'عام واحد', yy: '%d أعوام' }, + preparse(string) { + return string + .replace( + /[١٢٣٤٥٦٧٨٩٠]/g, + match => numberMap[match] + ) + .replace(/،/g, ',') + }, + postformat(string) { + return string + .replace(/\d/g, match => symbolMap[match]) + .replace(/,/g, '،') + }, ordinal: n => n, formats: { LT: 'HH:mm', From a5b920060bc7f29efe08403a8c176e4e8ed2c36f Mon Sep 17 00:00:00 2001 From: Moshe Jonathan Gordon Radian Date: Sun, 6 Dec 2020 02:40:57 +0200 Subject: [PATCH 4/5] test: add test for ar locale to ensure preparse and postformat works --- test/locale/ar.test.js | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 test/locale/ar.test.js diff --git a/test/locale/ar.test.js b/test/locale/ar.test.js new file mode 100644 index 000000000..141960943 --- /dev/null +++ b/test/locale/ar.test.js @@ -0,0 +1,50 @@ +import moment from 'moment' +import MockDate from 'mockdate' +import dayjs from '../../src' +import relativeTime from '../../src/plugin/relativeTime' +import preParsePostFormat from '../../src/plugin/preParsePostFormat' +import '../../src/locale/ar' + +dayjs.extend(relativeTime) +dayjs.extend(preParsePostFormat) + +beforeEach(() => { + MockDate.set(new Date()) +}) + +afterEach(() => { + MockDate.reset() +}) + +it('Format Month with locale function', () => { + for (let i = 0; i <= 7; i += 1) { + const dayjsAR = dayjs().locale('ar').add(i, 'day') + const momentAR = moment().locale('ar').add(i, 'day') + const testFormat1 = 'DD MMMM YYYY MMM' + const testFormat2 = 'MMMM' + const testFormat3 = 'MMM' + expect(dayjsAR.format(testFormat1)).toEqual(momentAR.format(testFormat1)) + expect(dayjsAR.format(testFormat2)).toEqual(momentAR.format(testFormat2)) + expect(dayjsAR.format(testFormat3)).toEqual(momentAR.format(testFormat3)) + } +}) + +it('Preparse with locale function', () => { + for (let i = 0; i <= 7; i += 1) { + dayjs.locale('ar') + const momentAR = moment().locale('ar').add(i, 'day') + expect(dayjs(momentAR.format()).format()).toEqual(momentAR.format()) + } +}) + +it('RelativeTime: Time from X gets formatted', () => { + const T = [ + [44.4, 'second', 'منذ ثانية واحدة'] + ] + + T.forEach((t) => { + dayjs.locale('ar') + expect(dayjs().from(dayjs().add(t[0], t[1]))) + .toBe(t[2]) + }) +}) From e565267d925b40af8c7e0663c303f9a397372d03 Mon Sep 17 00:00:00 2001 From: Moshe Jonathan Gordon Radian Date: Sat, 2 Jan 2021 00:44:22 +0200 Subject: [PATCH 5/5] refactor: change preparse-postformat to require importing localedata explicitly --- src/plugin/preParsePostFormat/index.js | 12 +++--------- test/locale/ar.test.js | 2 ++ test/plugin/preParsePostFormat.test.js | 2 ++ 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/plugin/preParsePostFormat/index.js b/src/plugin/preParsePostFormat/index.js index 91fbf9ed8..f8e96d586 100644 --- a/src/plugin/preParsePostFormat/index.js +++ b/src/plugin/preParsePostFormat/index.js @@ -1,11 +1,5 @@ -import localeData from '../localeData' - // Plugin template from https://day.js.org/docs/en/plugin/plugin -export default (option, dayjsClass, dayjsFactory) => { - // This plugin depends on other plugins - so I will import them here - // equivalent to dayjsClass.extend(duration) - localeData(option, dayjsClass, dayjsFactory) - +export default (option, dayjsClass) => { const oldParse = dayjsClass.prototype.parse dayjsClass.prototype.parse = function (cfg) { if (typeof cfg.date === 'string') { @@ -25,7 +19,7 @@ export default (option, dayjsClass, dayjsFactory) => { const result = oldFormat.call(this, ...args) // return modified result const locale = this.$locale() - return locale.postformat ? locale.postformat(result) : result + return locale && locale.postformat ? locale.postformat(result) : result } const oldFromTo = dayjsClass.prototype.fromToBase @@ -46,7 +40,7 @@ export default (option, dayjsClass, dayjsFactory) => { withoutSuffix, instance, isFrom, - locale.postformat + locale && locale.postformat ) } } diff --git a/test/locale/ar.test.js b/test/locale/ar.test.js index 141960943..3f9f60f0c 100644 --- a/test/locale/ar.test.js +++ b/test/locale/ar.test.js @@ -3,8 +3,10 @@ import MockDate from 'mockdate' import dayjs from '../../src' import relativeTime from '../../src/plugin/relativeTime' import preParsePostFormat from '../../src/plugin/preParsePostFormat' +import localeData from '../../src/plugin/localeData' import '../../src/locale/ar' +dayjs.extend(localeData) dayjs.extend(relativeTime) dayjs.extend(preParsePostFormat) diff --git a/test/plugin/preParsePostFormat.test.js b/test/plugin/preParsePostFormat.test.js index 88623496c..685ac8d1d 100755 --- a/test/plugin/preParsePostFormat.test.js +++ b/test/plugin/preParsePostFormat.test.js @@ -2,6 +2,7 @@ import MockDate from 'mockdate' // import moment from 'moment' import dayjs from '../../src' import preParsePostFormat from '../../src/plugin/preParsePostFormat' +import localeData from '../../src/plugin/localeData' import duration from '../../src/plugin/duration' import calendar from '../../src/plugin/calendar' import objectSupport from '../../src/plugin/objectSupport' @@ -12,6 +13,7 @@ import arraySupport from '../../src/plugin/arraySupport' import en from '../../src/locale/en' dayjs.extend(utc) +dayjs.extend(localeData) dayjs.extend(customParseFormat) dayjs.extend(arraySupport) dayjs.extend(objectSupport)