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', diff --git a/src/plugin/preParsePostFormat/index.js b/src/plugin/preParsePostFormat/index.js new file mode 100644 index 000000000..f8e96d586 --- /dev/null +++ b/src/plugin/preParsePostFormat/index.js @@ -0,0 +1,47 @@ +// Plugin template from https://day.js.org/docs/en/plugin/plugin +export default (option, dayjsClass) => { + 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 && 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 && 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/test/locale/ar.test.js b/test/locale/ar.test.js new file mode 100644 index 000000000..3f9f60f0c --- /dev/null +++ b/test/locale/ar.test.js @@ -0,0 +1,52 @@ +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 localeData from '../../src/plugin/localeData' +import '../../src/locale/ar' + +dayjs.extend(localeData) +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]) + }) +}) diff --git a/test/plugin/preParsePostFormat.test.js b/test/plugin/preParsePostFormat.test.js new file mode 100755 index 000000000..685ac8d1d --- /dev/null +++ b/test/plugin/preParsePostFormat.test.js @@ -0,0 +1,169 @@ +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' +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(localeData) +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')) +}) 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