diff --git a/.eslintrc.json b/.eslintrc.json index 97a4645f..2fa59438 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -38,7 +38,7 @@ ], "import/extensions": [ 2, - "ignorePackages" + "never" ] } -} \ No newline at end of file +} diff --git a/.gitignore b/.gitignore index 7894e392..951f5593 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # IDE .idea +.vscode # npm node_modules diff --git a/.travis.yml b/.travis.yml index 5f28442a..91412f24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ cache: - ~/.npm - node_modules node_js: + - '12' - '10' - '8' - '6' diff --git a/README.md b/README.md index c1394b9c..3443199c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -English | [简体中文](./docs/zh-cn/README.zh-CN.md) | [日本語](./docs/ja/README-ja.md) | [Português Brasileiro](./docs/pt-br/README-pt-br.md) | [한국어](./docs/ko/README-ko.md) | [Español (España)](./docs/es-es/README-es-es.md) +English | [简体中文](./docs/zh-cn/README.zh-CN.md) | [日本語](./docs/ja/README-ja.md) | [Português Brasileiro](./docs/pt-br/README-pt-br.md) | [한국어](./docs/ko/README-ko.md) | [Español (España)](./docs/es-es/README-es-es.md) | [Русский](./docs/ru/README-ru.md)

Day.js

+

Быстрая 2kB альтернатива Moment.js с тем же современным API

+
+

+ Gzip Size + NPM Version + Build Status + Codecov + License +
+ + Sauce Test Status + +

+ +> Day.js - это миниатюрная JavaScript библиотека, которая анализирует, валидирует, манипулирует, и отображает даты и время для современных браузеров, также обладает внушительной совместимостью с Moment.js API. Если вы используете Moment.js, тогда вы уже знаете как пользоваться Day.js. + +```js +dayjs().startOf('month').add(1, 'day').set('year', 2018).format('YYYY-MM-DD HH:mm:ss'); +``` + +* 🕒 Хорошо знакомые API и паттерны (шаблоны) Moment.js +* 💪 Неизменная +* 🔥 Цепная +* 🌐 Поддержка интернационализации (I18n) +* 📦 2kb мини-библиотека +* 👫 Поддержка всеми браузерами + +--- + +## Начало работы + +### Документация + +Вы можете найти больше детальной информации, API, и других документов на веб-сайте [day.js.org](https://day.js.org/). + +### Установка + +```console +npm install dayjs --save +``` + +📚[Инструкция по установке](https://day.js.org/docs/en/installation/installation) + +### API + +API Day.js легко использовать для анализа, проверки, воздействия, и отображения дат и времени. + +```javascript +dayjs('2018-08-08') // parse + +dayjs().format('{YYYY} MM-DDTHH:mm:ss SSS [Z] A') // отображение + +dayjs().set('month', 3).month() // получение значения и его установка + +dayjs().add(1, 'year') // влияние + +dayjs().isBefore(dayjs()) // осведомление +``` + +📚[Ссылка на API](https://day.js.org/docs/en/parse/parse) + +### I18n + +Day.js обладает великолепной поддержкой интернационализации. + +Но ни одна из локализаций не будет включена в вашу сборку до тех пор, пока вы не начнете ее использовать. +```javascript +import 'dayjs/locale/es' // загрузка по требованию + +dayjs.locale('es') // глобальное использование Испанской локали + +dayjs('2018-05-05').locale('zh-cn').format() // использование упрощенной Китайской локали в конкретном случае +``` +📚[Интернационализация](https://day.js.org/docs/en/i18n/i18n) + +### Плагин + +Плагин - это независимый модуль, который может быть добавлен в Day.js с целью расширения функциональных возможностей или добавления новых особенностей. + +```javascript +import advancedFormat from 'dayjs/plugin/advancedFormat' // загрузка по требованию + +dayjs.extend(advancedFormat) // использование плагина + +dayjs().format('Q Do k kk X x') // больше доступных форматов +``` + +📚[Список плагинов](https://day.js.org/docs/en/plugin/plugin) + +## Спонсоры + +Поддержите этот проект, став спонсором. Ваш логотип будет показан здесь с ссылкой на ваш веб-сайт. [[Стать спонсором](https://opencollective.com/dayjs#sponsor)] + + + + + + +## Контрибьюторы + +Этот проект существует благодаря всем людям, кто вносит свой вклад в его развитие. + +Пожалуйста поставьте 💖 звездочку 💖, чтобы поддержать нас. Спасибо. + +Также выражаю благодарность всем нашим спонсорам! 🙏 + + + + + + + +## Лицензия + +Day.js распростроняется под [лицензией MIT](./LICENSE). diff --git a/src/locale/br.js b/src/locale/br.js index 57d7d4f7..bbd242c7 100644 --- a/src/locale/br.js +++ b/src/locale/br.js @@ -3,11 +3,11 @@ import dayjs from 'dayjs' const locale = { name: 'br', - weekdays: "Sul_Lun_Meurzh_Merc'her_Yaou_Gwener_Sadorn".split('_'), - months: "Genver_C'hwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu".split('_'), + weekdays: 'Sul_Lun_Meurzh_Mercʼher_Yaou_Gwener_Sadorn'.split('_'), + months: 'Genver_Cʼhwevrer_Meurzh_Ebrel_Mae_Mezheven_Gouere_Eost_Gwengolo_Here_Du_Kerzu'.split('_'), weekStart: 1, weekdaysShort: 'Sul_Lun_Meu_Mer_Yao_Gwe_Sad'.split('_'), - monthsShort: "Gen_C'hwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker".split('_'), + monthsShort: 'Gen_Cʼhwe_Meu_Ebr_Mae_Eve_Gou_Eos_Gwe_Her_Du_Ker'.split('_'), weekdaysMin: 'Su_Lu_Me_Mer_Ya_Gw_Sa'.split('_'), ordinal: n => n, formats: { @@ -17,10 +17,10 @@ const locale = { LL: 'D [a viz] MMMM YYYY', LLL: 'D [a viz] MMMM YYYY h[e]mm A', LLLL: 'dddd, D [a viz] MMMM YYYY h[e]mm A' - } + }, + meridiem: hour => (hour < 12 ? 'a.m.' : 'g.m.') // a-raok merenn | goude merenn } dayjs.locale(locale, null, true) export default locale - diff --git a/src/locale/en-in.js b/src/locale/en-in.js new file mode 100644 index 00000000..9913857a --- /dev/null +++ b/src/locale/en-in.js @@ -0,0 +1,46 @@ +// English (India) [en-in] +import dayjs from 'dayjs' + +const locale = { + name: 'en-in', + weekdays: 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdaysShort: 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysMin: 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + months: 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + monthsShort: 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + weekStart: 1, + yearStart: 4, + relativeTime: { + future: 'in %s', + past: '%s ago', + s: 'a few seconds', + m: 'a minute', + mm: '%d minutes', + h: 'an hour', + hh: '%d hours', + d: 'a day', + dd: '%d days', + M: 'a month', + MM: '%d months', + y: 'a year', + yy: '%d years' + }, + formats: { + LT: 'HH:mm', + LTS: 'HH:mm:ss', + L: 'DD/MM/YYYY', + LL: 'D MMMM YYYY', + LLL: 'D MMMM YYYY HH:mm', + LLLL: 'dddd, D MMMM YYYY HH:mm' + }, + ordinal: (n) => { + const s = ['th', 'st', 'nd', 'rd'] + const v = n % 100 + return `[${n}${(s[(v - 20) % 10] || s[v] || s[0])}]` + } +} + +dayjs.locale(locale, null, true) + +export default locale + diff --git a/src/locale/en-tt.js b/src/locale/en-tt.js new file mode 100644 index 00000000..99212232 --- /dev/null +++ b/src/locale/en-tt.js @@ -0,0 +1,46 @@ +// English (Trinidad & Tobago) [en-tt] +import dayjs from 'dayjs' + +const locale = { + name: 'en-tt', + weekdays: 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), + weekdaysShort: 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), + weekdaysMin: 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), + months: 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), + monthsShort: 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), + weekStart: 1, + yearStart: 4, + relativeTime: { + future: 'in %s', + past: '%s ago', + s: 'a few seconds', + m: 'a minute', + mm: '%d minutes', + h: 'an hour', + hh: '%d hours', + d: 'a day', + dd: '%d days', + M: 'a month', + MM: '%d months', + y: 'a year', + yy: '%d years' + }, + formats: { + LT: 'HH:mm', + LTS: 'HH:mm:ss', + L: 'DD/MM/YYYY', + LL: 'D MMMM YYYY', + LLL: 'D MMMM YYYY HH:mm', + LLLL: 'dddd, D MMMM YYYY HH:mm' + }, + ordinal: (n) => { + const s = ['th', 'st', 'nd', 'rd'] + const v = n % 100 + return `[${n}${(s[(v - 20) % 10] || s[v] || s[0])}]` + } +} + +dayjs.locale(locale, null, true) + +export default locale + diff --git a/src/locale/fr.js b/src/locale/fr.js index 72290622..cefee134 100644 --- a/src/locale/fr.js +++ b/src/locale/fr.js @@ -7,7 +7,7 @@ const locale = { weekdaysShort: 'dim._lun._mar._mer._jeu._ven._sam.'.split('_'), weekdaysMin: 'di_lu_ma_me_je_ve_sa'.split('_'), months: 'janvier_février_mars_avril_mai_juin_juillet_août_septembre_octobre_novembre_décembre'.split('_'), - monthsShort: 'janv_févr_mars_avril_mai_juin_juil_août_sept_oct_nov_déc'.split('_'), + monthsShort: 'janv._févr._mars_avr._mai_juin_juil._août_sept._oct._nov._déc.'.split('_'), weekStart: 1, formats: { LT: 'HH:mm', diff --git a/src/locale/uk.js b/src/locale/uk.js index 4cfa84d8..27bfacf1 100644 --- a/src/locale/uk.js +++ b/src/locale/uk.js @@ -16,6 +16,8 @@ function relativeTimeWithPlural(number, withoutSuffix, key) { } if (key === 'm') { return withoutSuffix ? 'хвилина' : 'хвилину' + } else if (key === 'h') { + return withoutSuffix ? 'година' : 'годину' } return `${number} ${plural(format[key], +number)}` @@ -35,7 +37,7 @@ const locale = { s: 'декілька секунд', m: relativeTimeWithPlural, mm: relativeTimeWithPlural, - h: 'годину', + h: relativeTimeWithPlural, hh: relativeTimeWithPlural, d: 'день', dd: relativeTimeWithPlural, diff --git a/src/plugin/calendar/index.js b/src/plugin/calendar/index.js index e62e1fa3..d8a8b516 100644 --- a/src/plugin/calendar/index.js +++ b/src/plugin/calendar/index.js @@ -23,7 +23,11 @@ export default (o, c, d) => { diff < 2 ? 'nextDay' : diff < 7 ? 'nextWeek' : sameElse /* eslint-enable no-nested-ternary */ - return this.format(format[retVal] || calendarFormat[retVal]) + const currentFormat = format[retVal] || calendarFormat[retVal] + if (typeof currentFormat === 'function') { + return currentFormat.call(this, d()) + } + return this.format(currentFormat) } } diff --git a/src/plugin/duration/index.js b/src/plugin/duration/index.js new file mode 100644 index 00000000..835456b2 --- /dev/null +++ b/src/plugin/duration/index.js @@ -0,0 +1,174 @@ +import { MILLISECONDS_A_WEEK, MILLISECONDS_A_DAY, MILLISECONDS_A_HOUR, MILLISECONDS_A_MINUTE, MILLISECONDS_A_SECOND } from '../../constant' + +const MILLISECONDS_A_YEAR = MILLISECONDS_A_DAY * 365 +const MILLISECONDS_A_MONTH = MILLISECONDS_A_DAY * 30 + +const durationRegex = /^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/ + +const unitToMS = { + years: MILLISECONDS_A_YEAR, + months: MILLISECONDS_A_MONTH, + days: MILLISECONDS_A_DAY, + hours: MILLISECONDS_A_HOUR, + minutes: MILLISECONDS_A_MINUTE, + seconds: MILLISECONDS_A_SECOND, + weeks: MILLISECONDS_A_WEEK +} + +const isDuration = d => (d instanceof Duration) // eslint-disable-line no-use-before-define + +let $d +let $u + +const wrapper = (input, instance, unit) => + new Duration(input, unit, instance.$l) // eslint-disable-line no-use-before-define + +const prettyUnit = unit => `${$u.p(unit)}s` + +class Duration { + constructor(input, unit, locale) { + this.$d = {} + this.$l = locale || 'en' + if (unit) { + return wrapper(input * unitToMS[prettyUnit(unit)], this) + } + if (typeof input === 'number') { + this.$ms = input + this.parseFromMilliseconds() + return this + } + if (typeof input === 'object') { + Object.keys(input).forEach((k) => { + this.$d[prettyUnit(k)] = input[k] + }) + this.calMilliseconds() + return this + } + if (typeof input === 'string') { + const d = input.match(durationRegex) + if (d) { + [,, + this.$d.years, this.$d.months,, + this.$d.days, this.$d.hours, this.$d.minutes, this.$d.seconds] = d + this.calMilliseconds() + return this + } + } + return this + } + + calMilliseconds() { + this.$ms = Object.keys(this.$d).reduce((total, unit) => ( + total + ((this.$d[unit] || 0) * (unitToMS[unit] || 1)) + ), 0) + } + + parseFromMilliseconds() { + let { $ms } = this + this.$d.years = Math.floor($ms / MILLISECONDS_A_YEAR) + $ms %= MILLISECONDS_A_YEAR + this.$d.months = Math.floor($ms / MILLISECONDS_A_MONTH) + $ms %= MILLISECONDS_A_MONTH + this.$d.days = Math.floor($ms / MILLISECONDS_A_DAY) + $ms %= MILLISECONDS_A_DAY + this.$d.hours = Math.floor($ms / MILLISECONDS_A_HOUR) + $ms %= MILLISECONDS_A_HOUR + this.$d.minutes = Math.floor($ms / MILLISECONDS_A_MINUTE) + $ms %= MILLISECONDS_A_MINUTE + this.$d.seconds = $ms / MILLISECONDS_A_SECOND + } + + toISOString() { + const Y = this.$d.years ? `${this.$d.years}Y` : '' + const M = this.$d.months ? `${this.$d.months}M` : '' + let days = this.$d.days || 0 + if (this.$d.weeks) { + days += this.$d.weeks * 7 + } + const D = days ? `${days}D` : '' + const H = this.$d.hours ? `${this.$d.hours}H` : '' + const m = this.$d.minutes ? `${this.$d.minutes}M` : '' + let seconds = this.$d.seconds || 0 + if (this.$d.milliseconds) { + seconds += this.$d.milliseconds / 1000 + } + const S = seconds ? `${seconds}S` : '' + const T = (H || M || S) ? 'T' : '' + const result = `P${Y}${M}${D}${T}${H}${m}${S}` + return result === 'P' ? 'P0D' : result + } + + toJSON() { + return this.toISOString() + } + + as(unit) { + return this.$ms / (unitToMS[prettyUnit(unit)] || 1) + } + + get(unit) { + let base = this.$ms + const pUnit = prettyUnit(unit) + if (pUnit === 'milliseconds') { + base %= 1000 + } else { + base = Math.floor(base / unitToMS[pUnit]) + } + return base + } + + add(input, unit, isSubtract) { + let another + if (unit) { + another = input * unitToMS[prettyUnit(unit)] + } else if (isDuration(input)) { + another = input.$ms + } else { + another = wrapper(input, this).$ms + } + return wrapper(this.$ms + (another * (isSubtract ? -1 : 1)), this) + } + + subtract(input, unit) { + return this.add(input, unit, true) + } + + locale(l) { + const that = this.clone() + that.$l = l + return that + } + + clone() { + return wrapper(this.$ms, this) + } + + humanize(withSuffix) { + return $d().add(this.$ms, 'ms').locale(this.$l).fromNow(!withSuffix) + } + + milliseconds() { return this.get('milliseconds') } + asMilliseconds() { return this.as('milliseconds') } + seconds() { return this.get('seconds') } + asSeconds() { return this.as('seconds') } + minutes() { return this.get('minutes') } + asMinutes() { return this.as('minutes') } + hours() { return this.get('hours') } + asHours() { return this.as('hours') } + days() { return this.get('days') } + asDays() { return this.as('days') } + weeks() { return this.get('weeks') } + asWeeks() { return this.as('weeks') } + months() { return this.get('months') } + asMonths() { return this.as('months') } + years() { return this.get('years') } + asYears() { return this.as('years') } +} +export default (option, Dayjs, dayjs) => { + $d = dayjs + $u = dayjs().$utils() + dayjs.duration = function (input, unit) { + return wrapper(input, {}, unit) + } + dayjs.isDuration = isDuration +} diff --git a/src/plugin/isToday/index.js b/src/plugin/isToday/index.js new file mode 100644 index 00000000..fe97a8b6 --- /dev/null +++ b/src/plugin/isToday/index.js @@ -0,0 +1,9 @@ +export default (o, c, d) => { + const proto = c.prototype + proto.isToday = function () { + const comparisonTemplate = 'YYYY-MM-DD' + const now = d() + + return this.format(comparisonTemplate) === now.format(comparisonTemplate) + } +} diff --git a/src/plugin/isTomorrow/index.js b/src/plugin/isTomorrow/index.js new file mode 100644 index 00000000..f117766d --- /dev/null +++ b/src/plugin/isTomorrow/index.js @@ -0,0 +1,11 @@ +export default (o, c, d) => { + const proto = c.prototype + proto.isTomorrow = function () { + const comparisonTemplate = 'YYYY-MM-DD' + const tomorrow = d().add(1, 'day') + + return ( + this.format(comparisonTemplate) === tomorrow.format(comparisonTemplate) + ) + } +} diff --git a/src/plugin/isYesterday/index.js b/src/plugin/isYesterday/index.js new file mode 100644 index 00000000..aac904b2 --- /dev/null +++ b/src/plugin/isYesterday/index.js @@ -0,0 +1,11 @@ +export default (o, c, d) => { + const proto = c.prototype + proto.isYesterday = function () { + const comparisonTemplate = 'YYYY-MM-DD' + const yesterday = d().subtract(1, 'day') + + return ( + this.format(comparisonTemplate) === yesterday.format(comparisonTemplate) + ) + } +} diff --git a/src/plugin/relativeTime/index.js b/src/plugin/relativeTime/index.js index 2a68737f..10a8f5de 100644 --- a/src/plugin/relativeTime/index.js +++ b/src/plugin/relativeTime/index.js @@ -1,6 +1,7 @@ import * as C from '../../constant' export default (o, c, d) => { + o = o || {} const proto = c.prototype d.en.relativeTime = { future: 'in %s', @@ -19,7 +20,7 @@ export default (o, c, d) => { } const fromTo = (input, withoutSuffix, instance, isFrom) => { const loc = instance.$locale().relativeTime - const T = [ + const T = o.thresholds || [ { l: 's', r: 44, d: C.S }, { l: 'm', r: 89 }, { l: 'mm', r: 44, d: C.MIN }, @@ -44,10 +45,10 @@ export default (o, c, d) => { ? d(input).diff(instance, t.d, true) : instance.diff(input, t.d, true) } - const abs = Math.round(Math.abs(result)) + const 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 + if (abs <= 1 && i > 0) t = T[i - 1] // 1 minutes -> a minute, 0 seconds -> 0 second const format = loc[t.l] if (typeof format === 'string') { out = format.replace('%d', abs) diff --git a/test/locale/en.test.js b/test/locale/en.test.js new file mode 100644 index 00000000..c7b3de1b --- /dev/null +++ b/test/locale/en.test.js @@ -0,0 +1,24 @@ +import dayjs from '../../src' +import '../../src/locale/en' +import '../../src/locale/en-gb' +import '../../src/locale/en-in' +import '../../src/locale/en-tt' +import localizedFormat from '../../src/plugin/localizedFormat' + +dayjs.extend(localizedFormat) + +const locales = [ + { locale: 'en', expectedDate: '12/25/2019' }, + { locale: 'en-gb', expectedDate: '25/12/2019' }, + { locale: 'en-in', expectedDate: '25/12/2019' }, + { locale: 'en-tt', expectedDate: '25/12/2019' } +] + +describe('English date formats', () => { + locales.forEach((locale) => { + it(`should correctly format date with locale - ${locale.locale}`, () => { + const dayjsWithLocale = dayjs('2019-12-25').locale(locale.locale) + expect(dayjsWithLocale.format('L')).toEqual(locale.expectedDate) + }) + }) +}) diff --git a/test/locale/uk.test.js b/test/locale/uk.test.js index bb734cde..e1a88be2 100644 --- a/test/locale/uk.test.js +++ b/test/locale/uk.test.js @@ -34,3 +34,13 @@ it('RelativeTime: Time from X', () => { .toBe(moment().from(moment().add(t[0], t[1]), true)) }) }) + +it('hour', () => { + const str0 = '2020-03-18 19:15:00' + const str = '2020-03-18 20:15:00' + const result = dayjs(str0).locale('uk').to(str) + + expect(result).toEqual(moment(str0).locale('uk').to(str)) + const result2 = dayjs(str).locale('uk').to(str0, true) + expect(result2).toEqual('година') // different from moment.js +}) diff --git a/test/locale/zh.test.js b/test/locale/zh.test.js index f018eda1..017582a2 100644 --- a/test/locale/zh.test.js +++ b/test/locale/zh.test.js @@ -1,8 +1,8 @@ import dayjs from '../../src' import advancedFormat from '../../src/plugin/advancedFormat' import weekOfYear from '../../src/plugin/weekOfYear' -import '../../src/locale/zh.js' -import '../../src/locale/zh-cn.js' +import '../../src/locale/zh' +import '../../src/locale/zh-cn' dayjs.extend(advancedFormat).extend(weekOfYear) diff --git a/test/plugin/calendar.test.js b/test/plugin/calendar.test.js index 31391ad0..1bdff8c7 100644 --- a/test/plugin/calendar.test.js +++ b/test/plugin/calendar.test.js @@ -81,6 +81,51 @@ it('Custom format', () => { .toEqual(moment(now).calendar(nextDayWithoutFormat, format)) }) +it('Custom callback', () => { + const callbacks = { + sameDay: jest.fn(), + sameElse: jest.fn() + } + const now = '2015-01-15T14:21:22.000Z' + const nextDayWithoutFormat = '2015-01-14T11:23:55.000Z' + expect(dayjs(now).calendar(nextDayWithoutFormat, callbacks)) + .toEqual(moment(now).calendar(nextDayWithoutFormat, callbacks)) +}) + +it('Calls callback', () => { + const callbacks = { + sameDay: jest.fn(), + sameElse: jest.fn() + } + dayjs().calendar(null, callbacks) + expect(callbacks.sameElse).not.toBeCalled() + expect(callbacks.sameDay).toBeCalled() +}) + +it('callback is a function with the scope of the current moment', () => { + const callbacks = { + sameDay: jest.fn() + } + expect(dayjs().calendar(null, callbacks)).toEqual(callbacks.sameDay()) + const callbacks2 = { + sameDay: function cb() { + return this + } + } + const result = dayjs().calendar(null, callbacks2) + expect(result.format).not.toBeUndefined() + expect(dayjs.isDayjs(result)).toBeTruthy() +}) + +it('callback is a function and first argument a moment that depicts now', () => { + const callbacks = { + sameDay: jest.fn() + } + const now = dayjs() + dayjs(now).calendar(now, callbacks) + expect(callbacks.sameDay).toBeCalledWith(now) +}) + it('set global calendar in locale file', () => { const now = '2019-04-03T14:21:22.000Z' zhCn.calendar = { diff --git a/test/plugin/duration.test.js b/test/plugin/duration.test.js new file mode 100644 index 00000000..41be206d --- /dev/null +++ b/test/plugin/duration.test.js @@ -0,0 +1,181 @@ +import MockDate from 'mockdate' +import dayjs from '../../src' +import duration from '../../src/plugin/duration' +import relativeTime from '../../src/plugin/relativeTime' +import '../../src/locale/fr' +import '../../src/locale/es' + +dayjs.extend(relativeTime) +dayjs.extend(duration) + +beforeEach(() => { + MockDate.set(new Date()) +}) + +afterEach(() => { + MockDate.reset() +}) + +describe('Creating', () => { + it('milliseconds', () => { + expect(dayjs.duration(100).toISOString()).toBe('PT0.1S') + expect(dayjs.duration(1000).toISOString()).toBe('PT1S') + }) + it('two argument will bubble up to the next', () => { + expect(dayjs.duration(59, 'seconds').toISOString()).toBe('PT59S') + expect(dayjs.duration(60, 'seconds').toISOString()).toBe('P1M') + expect(dayjs.duration(13213, 'seconds').toISOString()).toBe('PT3H40M13S') + }) + it('object with float', () => { + expect(dayjs.duration({ + seconds: 1, + minutes: 2, + hours: 3, + days: 4, + months: 6, + years: 7 + }).toISOString()).toBe('P7Y6M4DT3H2M1S') + }) + it('object with weeks and float', () => { + expect(dayjs.duration({ + seconds: 1.1, + minutes: 2, + hours: 3, + days: 4, + weeks: 5, + months: 6, + years: 7 + }).toISOString()).toBe('P7Y6M39DT3H2M1.1S') + }) + it('object with millisecond', () => { + expect(dayjs.duration({ + ms: 1 + }).toISOString()).toBe('PT0.001S') + }) +}) + + +describe('Parse ISO string', () => { + it('Full ISO string', () => { + expect(dayjs.duration('P7Y6M4DT3H2M1S').toISOString()).toBe('P7Y6M4DT3H2M1S') + }) + it('Part ISO string', () => { + expect(dayjs.duration('PT2777H46M40S').toISOString()).toBe('PT2777H46M40S') + }) + it('Invalid ISO string', () => { + expect(dayjs.duration('Invalid').toISOString()).toBe('P0D') + }) +}) + +it('Is duration', () => { + expect(dayjs.isDuration(dayjs.duration())).toBe(true) + expect(dayjs.isDuration(dayjs.duration(1))).toBe(true) + expect(dayjs.isDuration(dayjs())).toBe(false) + expect(dayjs.isDuration({})).toBe(false) + expect(dayjs.isDuration()).toBe(false) +}) + +it('toJSON', () => { + expect(JSON.stringify({ + postDuration: dayjs.duration(5, 'minutes') + })).toBe('{"postDuration":"P5M"}') +}) + +describe('Humanize', () => { + it('Humaniz', () => { + expect(dayjs.duration(1, 'minutes').humanize()).toBe('a minute') + expect(dayjs.duration(2, 'minutes').humanize()).toBe('2 minutes') + expect(dayjs.duration(24, 'hours').humanize()).toBe('a day') + expect(dayjs.duration(1, 'minutes').humanize(true)).toBe('in a minute') + expect(dayjs.duration(-1, 'minutes').humanize(true)).toBe('a minute ago') + }) + + it('Locale', () => { + expect(dayjs.duration(1, 'minutes').humanize(true)).toBe('in a minute') + expect(dayjs.duration(1, 'minutes').locale('fr').humanize(true)).toBe('dans une minute') + expect(dayjs.duration(1, 'minutes').locale('es').humanize(true)).toBe('en un minuto') + }) +}) + +describe('Clone', () => { + it('Locale clone', () => { + const d = dayjs.duration(1, 'minutes').locale('fr') + const r = 'dans une minute' + expect(d.humanize(true)).toBe(r) + expect(d.clone().humanize(true)).toBe(r) + }) +}) + +describe('Milliseconds', () => { + expect(dayjs.duration(500).milliseconds()).toBe(500) + expect(dayjs.duration(1500).milliseconds()).toBe(500) + expect(dayjs.duration(15000).milliseconds()).toBe(0) + expect(dayjs.duration(500).asMilliseconds()).toBe(500) + expect(dayjs.duration(1500).asMilliseconds()).toBe(1500) + expect(dayjs.duration(15000).asMilliseconds()).toBe(15000) +}) + +describe('Add', () => { + const a = dayjs.duration(1, 'days') + const b = dayjs.duration(2, 'days') + expect(a.add(b).days()).toBe(3) + expect(a.add(1, 'days').days()).toBe(2) + expect(a.add({ days: 5 }).days()).toBe(6) +}) + +describe('Subtract', () => { + const a = dayjs.duration(3, 'days') + const b = dayjs.duration(2, 'days') + expect(a.subtract(b).days()).toBe(1) +}) + + +describe('Seconds', () => { + expect(dayjs.duration(500).seconds()).toBe(0) + expect(dayjs.duration(1500).seconds()).toBe(1) + expect(dayjs.duration(15000).seconds()).toBe(15) + expect(dayjs.duration(500).asSeconds()).toBe(0.5) + expect(dayjs.duration(1500).asSeconds()).toBe(1.5) + expect(dayjs.duration(15000).asSeconds()).toBe(15) +}) + +describe('Minutes', () => { + expect(dayjs.duration(100000).minutes()).toBe(1) + expect(dayjs.duration(100000).asMinutes().toFixed(2)).toBe('1.67') +}) + +describe('Hours', () => { + expect(dayjs.duration(10000000).hours()).toBe(2) + expect(dayjs.duration(10000000).asHours().toFixed(2)).toBe('2.78') +}) + +describe('Days', () => { + expect(dayjs.duration(100000000).days()).toBe(1) + expect(dayjs.duration(100000000).asDays().toFixed(2)).toBe('1.16') +}) + +describe('Weeks', () => { + expect(dayjs.duration(1000000000).weeks()).toBe(1) + expect(dayjs.duration(1000000000).asWeeks().toFixed(2)).toBe('1.65') +}) + +describe('Month', () => { + expect(dayjs.duration(10000000000).months()).toBe(3) + expect(dayjs.duration({ months: 3 }).asMonths()).toBe(3) +}) + +describe('Years', () => { + expect(dayjs.duration(100000000000).years()).toBe(3) + expect(dayjs.duration(100000000000).asYears().toFixed(2)).toBe('3.17') +}) + +describe('prettyUnit', () => { + const d = dayjs.duration(2, 's') + expect(d.toISOString()).toBe('PT2S') + expect(d.as('Second')).toBe(2) + expect(d.get('s')).toBe(2) + expect(dayjs.duration({ + M: 12, + m: 12 + }).toISOString()).toBe('P12MT12M') +}) diff --git a/test/plugin/isToday.test.js b/test/plugin/isToday.test.js new file mode 100644 index 00000000..0e57a707 --- /dev/null +++ b/test/plugin/isToday.test.js @@ -0,0 +1,18 @@ +import MockDate from 'mockdate' +import dayjs from '../../src' +import isToday from '../../src/plugin/isToday' + +dayjs.extend(isToday) + +beforeEach(() => { + MockDate.set(new Date()) +}) + +afterEach(() => { + MockDate.reset() +}) + +it('is today', () => { + expect(dayjs(new Date()).isToday()).toBeTruthy() + expect(dayjs('2017-01-01').isToday()).toBeFalsy() +}) diff --git a/test/plugin/isTomorrow.test.js b/test/plugin/isTomorrow.test.js new file mode 100644 index 00000000..bd73d77e --- /dev/null +++ b/test/plugin/isTomorrow.test.js @@ -0,0 +1,18 @@ +import MockDate from 'mockdate' +import dayjs from '../../src' +import isTomorrow from '../../src/plugin/isTomorrow' + +dayjs.extend(isTomorrow) + +beforeEach(() => { + MockDate.set(new Date()) +}) + +afterEach(() => { + MockDate.reset() +}) + +it('is tomorrow', () => { + expect(dayjs().add(1, 'day').isTomorrow()).toBeTruthy() + expect(dayjs('2017-01-01').isTomorrow('2019-01-01', '2017-01-01')).toBeFalsy() +}) diff --git a/test/plugin/isYesterday.test.js b/test/plugin/isYesterday.test.js new file mode 100644 index 00000000..ffb9a57e --- /dev/null +++ b/test/plugin/isYesterday.test.js @@ -0,0 +1,18 @@ +import MockDate from 'mockdate' +import dayjs from '../../src' +import isYesterday from '../../src/plugin/isYesterday' + +dayjs.extend(isYesterday) + +beforeEach(() => { + MockDate.set(new Date()) +}) + +afterEach(() => { + MockDate.reset() +}) + +it('is yesterday', () => { + expect(dayjs().subtract(1, 'day').isYesterday()).toBeTruthy() + expect(dayjs('2017-01-01').isYesterday()).toBeFalsy() +}) diff --git a/test/plugin/localeData.test.js b/test/plugin/localeData.test.js index 28d667a0..288c133f 100644 --- a/test/plugin/localeData.test.js +++ b/test/plugin/localeData.test.js @@ -3,6 +3,7 @@ import moment from 'moment' import dayjs from '../../src' import localeData from '../../src/plugin/localeData' import localizedFormat from '../../src/plugin/localizedFormat' +import '../../src/locale/fr' import '../../src/locale/zh-cn' dayjs.extend(localizedFormat) @@ -38,7 +39,7 @@ it('Instance localeData', () => { it('Global localeData', () => { - ['zh-cn', 'en'].forEach((lo) => { + ['zh-cn', 'en', 'fr'].forEach((lo) => { dayjs.locale(lo) moment.locale(lo) const dayjsLocaleData = dayjs.localeData() @@ -54,7 +55,7 @@ it('Global localeData', () => { it('Listing the months and weekdays', () => { - ['zh-cn', 'en'].forEach((lo) => { + ['zh-cn', 'en', 'fr'].forEach((lo) => { dayjs.locale(lo) moment.locale(lo) expect(dayjs.months()).toEqual(moment.months()) diff --git a/test/plugin/relativeTime.test.js b/test/plugin/relativeTime.test.js index 11b1d5d5..e8a26e0a 100644 --- a/test/plugin/relativeTime.test.js +++ b/test/plugin/relativeTime.test.js @@ -1,6 +1,7 @@ import MockDate from 'mockdate' import moment from 'moment' import dayjs from '../../src' +import * as C from '../../src/constant' import relativeTime from '../../src/plugin/relativeTime' import utc from '../../src/plugin/utc' import '../../src/locale/ru' @@ -84,7 +85,7 @@ it('Time to X', () => { expect(dayjs().to(dayjs().subtract(3, 'year'))).toBe(moment().to(moment().subtract(3, 'year'))) }) -it('Locale Fonction', () => { +it('Locale Function', () => { // e.g. in ru locale, m: x minute require additional processing // and provides as a function instead of a string const str0 = '2020-01-06 15:53:00' @@ -115,3 +116,24 @@ it('Time from now with UTC', () => { expect(dutc.fromNow()).toBe(mutc.fromNow()) }) + +it('Custom thresholds and rounding support', () => { + expect(dayjs().subtract(45, 'm').fromNow()).toBe('an hour ago') + dayjs.extend(relativeTime, { + rounding: Math.floor, + thresholds: [ + { l: 's', r: 1 }, + { l: 'm', r: 1 }, + { l: 'mm', r: 59, d: C.MIN }, + { l: 'h', r: 1 }, + { l: 'hh', r: 23, d: C.H }, + { l: 'd', r: 1 }, + { l: 'dd', r: 29, d: C.D }, + { l: 'M', r: 1 }, + { l: 'MM', r: 11, d: C.M }, + { l: 'y' }, + { l: 'yy', d: C.Y } + ] + }) + expect(dayjs().subtract(45, 'm').fromNow()).toBe('45 minutes ago') +}) diff --git a/types/index.d.ts b/types/index.d.ts index a0f38c89..f971394d 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -96,9 +96,9 @@ declare namespace dayjs { locale(preset: string | ILocale, object?: Partial): Dayjs } - export type PluginFunc = (option: any, c: typeof Dayjs, d: typeof dayjs) => void + export type PluginFunc = (option: T, c: typeof Dayjs, d: typeof dayjs) => void - export function extend(plugin: PluginFunc, option?: any): Dayjs + export function extend(plugin: PluginFunc, option?: T): Dayjs export function locale(preset: string | ILocale, object?: Partial, isLocal?: boolean): string diff --git a/types/plugin/duration.d.ts b/types/plugin/duration.d.ts new file mode 100644 index 00000000..38225c92 --- /dev/null +++ b/types/plugin/duration.d.ts @@ -0,0 +1,58 @@ +import { PluginFunc } from 'dayjs' + +declare const plugin: PluginFunc +export = plugin + +type DurationInputType = string | number | object +type DurationAddType = number | object | Duration + +declare class Duration { + constructor (input: DurationInputType, unit?: string, locale?: string) + + clone(): Duration + + humanize(withSuffix: boolean): string + + milliseconds(): number + asMilliseconds(): number + + seconds(): number + asSeconds(): number + + minutes(): number + asMinutes(): number + + hours(): number + asHours(): number + + days(): number + asDays(): number + + weeks(): number + asWeeks(): number + + months(): number + asMonths(): number + + years(): number + asYears(): number + + as(unit: string): number + + get(unit: string): number + + add(input: DurationAddType, unit? : string): Duration + + subtract(input: DurationAddType, unit? : string): Duration + + toJSON(): string + + toISOString(): string + + locale(locale: string): Duration +} + +declare module 'dayjs' { + export function duration(input?: DurationInputType , unit?: string): Duration + export function isDuration(d: any): d is Duration +} diff --git a/types/plugin/isToday.d.ts b/types/plugin/isToday.d.ts new file mode 100644 index 00000000..04ac5818 --- /dev/null +++ b/types/plugin/isToday.d.ts @@ -0,0 +1,10 @@ +import { PluginFunc } from 'dayjs' + +declare const plugin: PluginFunc +export = plugin + +declare module 'dayjs' { + interface Dayjs { + isToday(): boolean + } +} diff --git a/types/plugin/isTomorrow.d.ts b/types/plugin/isTomorrow.d.ts new file mode 100644 index 00000000..08110b6e --- /dev/null +++ b/types/plugin/isTomorrow.d.ts @@ -0,0 +1,10 @@ +import { PluginFunc } from 'dayjs' + +declare const plugin: PluginFunc +export = plugin + +declare module 'dayjs' { + interface Dayjs { + isTomorrow(): boolean + } +} diff --git a/types/plugin/isYesterday.d.ts b/types/plugin/isYesterday.d.ts new file mode 100644 index 00000000..2d8ae9e1 --- /dev/null +++ b/types/plugin/isYesterday.d.ts @@ -0,0 +1,10 @@ +import { PluginFunc } from 'dayjs' + +declare const plugin: PluginFunc +export = plugin + +declare module 'dayjs' { + interface Dayjs { + isYesterday(): boolean + } +} diff --git a/types/plugin/relativeTime.d.ts b/types/plugin/relativeTime.d.ts index d08121ee..444b0c26 100644 --- a/types/plugin/relativeTime.d.ts +++ b/types/plugin/relativeTime.d.ts @@ -1,6 +1,17 @@ import { PluginFunc, ConfigType } from 'dayjs' -declare const plugin: PluginFunc +declare interface RelativeTimeThreshold { + l: string + r?: number + d?: string +} + +declare interface RelativeTimeOptions { + rounding?: (num: number) => number + thresholds?: RelativeTimeThreshold[] +} + +declare const plugin: PluginFunc export = plugin declare module 'dayjs' {