diff --git a/.gitignore b/.gitignore index 951f55934..9b99c9ee0 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ coverage /dayjs.min.js /esm /index.d.ts +locale.json #dev demo.js diff --git a/.travis.yml b/.travis.yml index df613736e..f05c630b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ cache: - ~/.npm - node_modules node_js: + - '14' - '12' - '10' - '8' diff --git a/build/index.js b/build/index.js index 683170df5..4651dd61e 100644 --- a/build/index.js +++ b/build/index.js @@ -8,17 +8,34 @@ const { ncp } = require('ncp') const { promisify } = util const promisifyReadDir = promisify(fs.readdir) +const promisifyReadFile = promisify(fs.readFile) +const promisifyWriteFile = promisify(fs.writeFile) +const localeNameRegex = /\/\/ (.*) \[/ const formatName = n => n.replace(/\.js/, '').replace('-', '_') +const localePath = path.join(__dirname, '../src/locale') + async function build(option) { const bundle = await rollup.rollup(option.input) await bundle.write(option.output) } +async function listLocaleJson(localeArr) { + const localeListArr = [] + await Promise.all(localeArr.map(async (l) => { + const localeData = await promisifyReadFile(path.join(localePath, l), 'utf-8') + localeListArr.push({ + key: l.slice(0, -3), + name: localeData.match(localeNameRegex)[1] + }) + })) + promisifyWriteFile(path.join(__dirname, '../locale.json'), JSON.stringify(localeListArr), 'utf8') +} + (async () => { try { - const locales = await promisifyReadDir(path.join(__dirname, '../src/locale')) + const locales = await promisifyReadDir(localePath) locales.forEach((l) => { build(configFactory({ input: `./src/locale/${l}`, @@ -42,6 +59,9 @@ async function build(option) { })) await promisify(ncp)('./types/', './') + + // list locales + await listLocaleJson(locales) } catch (e) { console.error(e) // eslint-disable-line no-console } diff --git a/src/locale/am.js b/src/locale/am.js new file mode 100644 index 000000000..f8caf04f6 --- /dev/null +++ b/src/locale/am.js @@ -0,0 +1,41 @@ +// Amharic [am] +import dayjs from 'dayjs' + +const locale = { + name: 'am', + weekdays: 'እሑድ_ሰኞ_ማክሰኞ_ረቡዕ_ሐሙስ_አርብ_ቅዳሜ'.split('_'), + weekdaysShort: 'እሑድ_ሰኞ_ማክሰ_ረቡዕ_ሐሙስ_አርብ_ቅዳሜ'.split('_'), + weekdaysMin: 'እሑ_ሰኞ_ማክ_ረቡ_ሐሙ_አር_ቅዳ'.split('_'), + months: 'ጃንዋሪ_ፌብሯሪ_ማርች_ኤፕሪል_ሜይ_ጁን_ጁላይ_ኦገስት_ሴፕቴምበር_ኦክቶበር_ኖቬምበር_ዲሴምበር'.split('_'), + monthsShort: 'ጃንዋ_ፌብሯ_ማርች_ኤፕሪ_ሜይ_ጁን_ጁላይ_ኦገስ_ሴፕቴ_ኦክቶ_ኖቬም_ዲሴም'.split('_'), + weekStart: 1, + yearStart: 4, + relativeTime: { + future: 'በ%s', + past: '%s በፊት', + s: 'ጥቂት ሰከንዶች', + m: 'አንድ ደቂቃ', + mm: '%d ደቂቃዎች', + h: 'አንድ ሰዓት', + hh: '%d ሰዓታት', + d: 'አንድ ቀን', + dd: '%d ቀናት', + M: 'አንድ ወር', + MM: '%d ወራት', + y: 'አንድ ዓመት', + yy: '%d ዓመታት' + }, + formats: { + LT: 'HH:mm', + LTS: 'HH:mm:ss', + L: 'DD/MM/YYYY', + LL: 'MMMM D ፣ YYYY', + LLL: 'MMMM D ፣ YYYY HH:mm', + LLLL: 'dddd ፣ MMMM D ፣ YYYY HH:mm' + }, + ordinal: n => `${n}ኛ` +} + +dayjs.locale(locale, null, true) + +export default locale diff --git a/src/locale/bi.js b/src/locale/bi.js index 7f6cbecba..8dbb0a56d 100644 --- a/src/locale/bi.js +++ b/src/locale/bi.js @@ -1,3 +1,4 @@ +// Bislama [bi] import dayjs from 'dayjs' const locale = { diff --git a/src/locale/en.js b/src/locale/en.js index a9eb16369..71c2f226e 100644 --- a/src/locale/en.js +++ b/src/locale/en.js @@ -1,3 +1,4 @@ +// English [en] // We don't need weekdaysShort, weekdaysMin, monthsShort in en.js locale export default { name: 'en', diff --git a/src/locale/fi.js b/src/locale/fi.js index 0ff83ee1c..d03f4cbd5 100644 --- a/src/locale/fi.js +++ b/src/locale/fi.js @@ -66,13 +66,13 @@ const locale = { LT: 'HH.mm', LTS: 'HH.mm.ss', L: 'DD.MM.YYYY', - LL: 'Do MMMM[ta] YYYY', - LLL: 'Do MMMM[ta] YYYY, [klo] HH.mm', - LLLL: 'dddd, Do MMMM[ta] YYYY, [klo] HH.mm', + LL: 'D. MMMM[ta] YYYY', + LLL: 'D. MMMM[ta] YYYY, [klo] HH.mm', + LLLL: 'dddd, D. MMMM[ta] YYYY, [klo] HH.mm', l: 'D.M.YYYY', - ll: 'Do MMM YYYY', - lll: 'Do MMM YYYY, [klo] HH.mm', - llll: 'ddd, Do MMM YYYY, [klo] HH.mm' + ll: 'D. MMM YYYY', + lll: 'D. MMM YYYY, [klo] HH.mm', + llll: 'ddd, D. MMM YYYY, [klo] HH.mm' } } diff --git a/src/locale/hr.js b/src/locale/hr.js index 113c1631f..204077f2a 100644 --- a/src/locale/hr.js +++ b/src/locale/hr.js @@ -1,13 +1,26 @@ // Croatian [hr] import dayjs from 'dayjs' +const monthFormat = 'siječnja_veljače_ožujka_travnja_svibnja_lipnja_srpnja_kolovoza_rujna_listopada_studenoga_prosinca'.split('_') +const monthStandalone = 'siječanj_veljača_ožujak_travanj_svibanj_lipanj_srpanj_kolovoz_rujan_listopad_studeni_prosinac'.split('_') +const MONTHS_IN_FORMAT = /D[oD]?(\[[^[\]]*\]|\s)+MMMM?/ + +const months = (dayjsInstance, format) => { + if (MONTHS_IN_FORMAT.test(format)) { + return monthFormat[dayjsInstance.month()] + } + return monthStandalone[dayjsInstance.month()] +} +months.s = monthStandalone +months.f = monthFormat + const locale = { name: 'hr', - weekdays: 'Nedjelja_Ponedjeljak_Utorak_Srijeda_Četvrtak_Petak_Subota'.split('_'), - weekdaysShort: 'Ned._Pon._Uto._Sri._Čet._Pet._Sub.'.split('_'), - weekdaysMin: 'Ne_Po_Ut_Sr_Če_Pe_Su'.split('_'), - months: 'Siječanj_Veljača_Ožujak_Travanj_Svibanj_Lipanj_Srpanj_Kolovoz_Rujan_Listopad_Studeni_Prosinac'.split('_'), - monthsShort: 'Sij._Velj._Ožu._Tra._Svi._Lip._Srp._Kol._Ruj._Lis._Stu._Pro.'.split('_'), + weekdays: 'nedjelja_ponedjeljak_utorak_srijeda_četvrtak_petak_subota'.split('_'), + weekdaysShort: 'ned._pon._uto._sri._čet._pet._sub.'.split('_'), + weekdaysMin: 'ne_po_ut_sr_če_pe_su'.split('_'), + months, + monthsShort: 'sij._velj._ožu._tra._svi._lip._srp._kol._ruj._lis._stu._pro.'.split('_'), weekStart: 1, formats: { LT: 'H:mm', diff --git a/src/locale/lt.js b/src/locale/lt.js index 7876d87b0..c846e1177 100644 --- a/src/locale/lt.js +++ b/src/locale/lt.js @@ -1,12 +1,26 @@ // Lithuanian [lt] import dayjs from 'dayjs' +const monthFormat = 'sausio_vasario_kovo_balandžio_gegužės_birželio_liepos_rugpjūčio_rugsėjo_spalio_lapkričio_gruodžio'.split('_') +const monthStandalone = 'sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis'.split('_') +// eslint-disable-next-line no-useless-escape +const MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s)+MMMM?|MMMM?(\[[^\[\]]*\]|\s)+D[oD]?/ + +const months = (dayjsInstance, format) => { + if (MONTHS_IN_FORMAT.test(format)) { + return monthFormat[dayjsInstance.month()] + } + return monthStandalone[dayjsInstance.month()] +} +months.s = monthStandalone +months.f = monthFormat + const locale = { name: 'lt', weekdays: 'sekmadienis_pirmadienis_antradienis_trečiadienis_ketvirtadienis_penktadienis_šeštadienis'.split('_'), weekdaysShort: 'sek_pir_ant_tre_ket_pen_šeš'.split('_'), weekdaysMin: 's_p_a_t_k_pn_š'.split('_'), - months: 'sausis_vasaris_kovas_balandis_gegužė_birželis_liepa_rugpjūtis_rugsėjis_spalis_lapkritis_gruodis'.split('_'), + months, monthsShort: 'sau_vas_kov_bal_geg_bir_lie_rgp_rgs_spa_lap_grd'.split('_'), ordinal: n => `${n}.`, weekStart: 1, diff --git a/src/locale/pl.js b/src/locale/pl.js index c886f584f..2ef1c8273 100644 --- a/src/locale/pl.js +++ b/src/locale/pl.js @@ -23,12 +23,26 @@ function translate(number, withoutSuffix, key) { } } /* eslint-enable */ + +const monthFormat = 'stycznia_lutego_marca_kwietnia_maja_czerwca_lipca_sierpnia_września_października_listopada_grudnia'.split('_') +const monthStandalone = 'styczeń_luty_marzec_kwiecień_maj_czerwiec_lipiec_sierpień_wrzesień_październik_listopad_grudzień'.split('_') +const MONTHS_IN_FORMAT = /D MMMM/ + +const months = (dayjsInstance, format) => { + if (MONTHS_IN_FORMAT.test(format)) { + return monthFormat[dayjsInstance.month()] + } + return monthStandalone[dayjsInstance.month()] +} +months.s = monthStandalone +months.f = monthFormat + const locale = { name: 'pl', - weekdays: 'Niedziela_Poniedziałek_Wtorek_Środa_Czwartek_Piątek_Sobota'.split('_'), - weekdaysShort: 'Ndz_Pon_Wt_Śr_Czw_Pt_Sob'.split('_'), + weekdays: 'niedziela_poniedziałek_wtorek_środa_czwartek_piątek_sobota'.split('_'), + weekdaysShort: 'ndz_pon_wt_śr_czw_pt_sob'.split('_'), weekdaysMin: 'Nd_Pn_Wt_Śr_Cz_Pt_So'.split('_'), - months: 'Styczeń_Luty_Marzec_Kwiecień_Maj_Czerwiec_Lipiec_Sierpień_Wrzesień_Październik_Listopad_Grudzień'.split('_'), + months, monthsShort: 'sty_lut_mar_kwi_maj_cze_lip_sie_wrz_paź_lis_gru'.split('_'), ordinal: n => `${n}.`, weekStart: 1, diff --git a/src/plugin/duration/index.js b/src/plugin/duration/index.js index dad125f11..533cd8d7e 100644 --- a/src/plugin/duration/index.js +++ b/src/plugin/duration/index.js @@ -1,4 +1,4 @@ -import { MILLISECONDS_A_WEEK, MILLISECONDS_A_DAY, MILLISECONDS_A_HOUR, MILLISECONDS_A_MINUTE, MILLISECONDS_A_SECOND } from '../../constant' +import { MILLISECONDS_A_DAY, MILLISECONDS_A_HOUR, MILLISECONDS_A_MINUTE, MILLISECONDS_A_SECOND, MILLISECONDS_A_WEEK } from '../../constant' const MILLISECONDS_A_YEAR = MILLISECONDS_A_DAY * 365 const MILLISECONDS_A_MONTH = MILLISECONDS_A_DAY * 30 @@ -12,6 +12,7 @@ const unitToMS = { hours: MILLISECONDS_A_HOUR, minutes: MILLISECONDS_A_MINUTE, seconds: MILLISECONDS_A_SECOND, + milliseconds: 1, weeks: MILLISECONDS_A_WEEK } diff --git a/src/plugin/timezone/index.js b/src/plugin/timezone/index.js index 0c248c607..6746356bd 100644 --- a/src/plugin/timezone/index.js +++ b/src/plugin/timezone/index.js @@ -10,6 +10,8 @@ const typeToPos = { const ms = 'ms' export default (o, c, d) => { + let defaultTimezone + const localUtcOffset = d().utcOffset() const tzOffset = (timestamp, timezone) => { const date = new Date(timestamp) @@ -33,33 +35,51 @@ export default (o, c, d) => { filled[pos] = parseInt(value, 10) } } - const utcString = `${filled[0]}-${filled[1]}-${filled[2]} ${filled[3]}:${filled[4]}:${filled[5]}:000` + // Workaround for the same behavior in different node version + // https://github.com/nodejs/node/issues/33027 + const hour = filled[3] + const fixedHour = hour === 24 ? 0 : hour + const utcString = `${filled[0]}-${filled[1]}-${filled[2]} ${fixedHour}:${filled[4]}:${filled[5]}:000` const utcTs = d.utc(utcString).valueOf() let asTS = +date const over = asTS % 1000 asTS -= over return (utcTs - asTS) / (60 * 1000) } + + // find the right offset a given local time. The o input is our guess, which determines which + // offset we'll pick in ambiguous cases (e.g. there are two 3 AMs b/c Fallback DST) + // https://github.com/moment/luxon/blob/master/src/datetime.js#L76 const fixOffset = (localTS, o0, tz) => { + // Our UTC time is just a guess because our offset is just a guess let utcGuess = localTS - (o0 * 60 * 1000) + // Test whether the zone matches the offset for this ts const o2 = tzOffset(utcGuess, tz) + // If so, offset didn't change and we're done if (o0 === o2) { return [utcGuess, o0] } + // If not, change the ts by the difference in the offset utcGuess -= (o2 - o0) * 60 * 1000 + // If that gives us the local time we want, we're done const o3 = tzOffset(utcGuess, tz) if (o2 === o3) { return [utcGuess, o2] } + // If it's different, we're in a hole time. + // The offset has changed, but the we don't adjust the time return [localTS - (Math.min(o2, o3) * 60 * 1000), Math.max(o2, o3)] } + const proto = c.prototype - proto.tz = function (timezone) { + + proto.tz = function (timezone = defaultTimezone) { const target = this.toDate().toLocaleString('en-US', { timeZone: timezone }) const diff = Math.round((this.toDate() - new Date(target)) / 1000 / 60) return d(target).utcOffset(localUtcOffset - diff, true).$set(ms, this.$ms) } - d.tz = function (input, timezone) { + + d.tz = function (input, timezone = defaultTimezone) { const previousOffset = tzOffset(+d(), timezone) let localTs if (typeof input !== 'string') { @@ -75,4 +95,8 @@ export default (o, c, d) => { d.tz.guess = function () { return Intl.DateTimeFormat().resolvedOptions().timeZone } + + d.tz.setDefault = function (timezone) { + defaultTimezone = timezone + } } diff --git a/test/locale/hr.test.js b/test/locale/hr.test.js new file mode 100644 index 000000000..8e80d0691 --- /dev/null +++ b/test/locale/hr.test.js @@ -0,0 +1,18 @@ +import moment from 'moment' +import dayjs from '../../src' +import '../../src/locale/hr' + +it('Format month with locale function', () => { + for (let i = 0; i <= 7; i += 1) { + const dayjsUK = dayjs().locale('hr').add(i, 'day') + const momentUK = moment().locale('hr').add(i, 'day') + const testFormat1 = 'DD MMMM YYYY MMM' + const testFormat2 = 'dddd, MMMM D YYYY' + const testFormat3 = 'MMMM' + const testFormat4 = 'MMM' + expect(dayjsUK.format(testFormat1)).toEqual(momentUK.format(testFormat1)) + expect(dayjsUK.format(testFormat2)).toEqual(momentUK.format(testFormat2)) + expect(dayjsUK.format(testFormat3)).toEqual(momentUK.format(testFormat3)) + expect(dayjsUK.format(testFormat4)).toEqual(momentUK.format(testFormat4)) + } +}) diff --git a/test/locale/keys.test.js b/test/locale/keys.test.js index 876d6b338..b3c480e0f 100644 --- a/test/locale/keys.test.js +++ b/test/locale/keys.test.js @@ -4,14 +4,17 @@ import dayjs from '../../src' const localeDir = '../../src/locale' const Locale = [] +const localeNameRegex = /\/\/ (.*) \[/ // load all locales from locale dir fs.readdirSync(path.join(__dirname, localeDir)) .forEach((file) => { + const fPath = path.join(__dirname, localeDir, file) Locale.push({ name: file, // eslint-disable-next-line import/no-dynamic-require, global-require - content: require(path.join(__dirname, localeDir, file)).default + content: require(fPath).default, + file: fs.readFileSync(fPath, 'utf-8') }) }) @@ -31,6 +34,9 @@ Locale.forEach((locale) => { yearStart, meridiem } = locale.content + // comments required + const commentsMatchResult = locale.file.match(localeNameRegex) + expect(commentsMatchResult[1]).not.toBeUndefined() expect(name).toEqual(locale.name.replace('.js', '')) expect(weekdays).toEqual(expect.any(Array)) diff --git a/test/locale/lt.test.js b/test/locale/lt.test.js new file mode 100644 index 000000000..6ff3afdad --- /dev/null +++ b/test/locale/lt.test.js @@ -0,0 +1,18 @@ +import moment from 'moment' +import dayjs from '../../src' +import '../../src/locale/lt' + +it('Format month with locale function', () => { + for (let i = 0; i <= 7; i += 1) { + const dayjsUK = dayjs().locale('lt').add(i, 'day') + const momentUK = moment().locale('lt').add(i, 'day') + const testFormat1 = 'DD MMMM YYYY MMM' + const testFormat2 = 'dddd, MMMM D YYYY' + const testFormat3 = 'MMMM' + const testFormat4 = 'MMM' + expect(dayjsUK.format(testFormat1)).toEqual(momentUK.format(testFormat1)) + expect(dayjsUK.format(testFormat2)).toEqual(momentUK.format(testFormat2)) + expect(dayjsUK.format(testFormat3)).toEqual(momentUK.format(testFormat3)) + expect(dayjsUK.format(testFormat4)).toEqual(momentUK.format(testFormat4)) + } +}) diff --git a/test/locale/pl.test.js b/test/locale/pl.test.js index 6e43450c5..8a68e6a59 100644 --- a/test/locale/pl.test.js +++ b/test/locale/pl.test.js @@ -14,6 +14,21 @@ afterEach(() => { MockDate.reset() }) +it('Format month with locale function', () => { + for (let i = 0; i <= 7; i += 1) { + const dayjsUK = dayjs().locale('pl').add(i, 'day') + const momentUK = moment().locale('pl').add(i, 'day') + const testFormat1 = 'DD MMMM YYYY MMM' + const testFormat2 = 'dddd, MMMM D YYYY' + const testFormat3 = 'MMMM' + const testFormat4 = 'MMM' + expect(dayjsUK.format(testFormat1)).toEqual(momentUK.format(testFormat1)) + expect(dayjsUK.format(testFormat2)).toEqual(momentUK.format(testFormat2)) + expect(dayjsUK.format(testFormat3)).toEqual(momentUK.format(testFormat3)) + expect(dayjsUK.format(testFormat4)).toEqual(momentUK.format(testFormat4)) + } +}) + it('RelativeTime: Time from X', () => { const T = [ [44.4, 'second'], // a few seconds diff --git a/test/plugin/duration.test.js b/test/plugin/duration.test.js index d153da43a..f26c78c72 100644 --- a/test/plugin/duration.test.js +++ b/test/plugin/duration.test.js @@ -18,6 +18,7 @@ afterEach(() => { describe('Creating', () => { it('milliseconds', () => { + expect(dayjs.duration(1, 'ms').toISOString()).toBe('PT0.001S') expect(dayjs.duration(100).toISOString()).toBe('PT0.1S') expect(dayjs.duration(1000).toISOString()).toBe('PT1S') }) diff --git a/test/plugin/timezone.test.js b/test/plugin/timezone.test.js index ca807aa4f..343bb6265 100644 --- a/test/plugin/timezone.test.js +++ b/test/plugin/timezone.test.js @@ -203,3 +203,52 @@ describe('DST, a time that never existed Fall Back', () => { }) }) }) + +describe('set Default', () => { + it('default timezone', () => { + const dateStr = '2014-06-01 12:00' + dayjs.tz.setDefault(NY) + const newYork = dayjs.tz(dateStr) + expect(newYork.format()).toBe('2014-06-01T12:00:00-04:00') + expect(newYork.utcOffset()).toBe(-240) + expect(newYork.valueOf()).toBe(1401638400000) + + expect(dayjs(dateStr).tz().format()).toBe(dayjs(dateStr).tz(NY).format()) + }) + + it('empty timezone means local timezone', () => { + const LOCAL_TZ = dayjs.tz.guess() + const dateStr = '2014-06-01 12:00' + dayjs.tz.setDefault() + expect(dayjs(dateStr).tz().valueOf()).toBe(dayjs(dateStr).tz(LOCAL_TZ).valueOf()) + expect(dayjs.tz(dateStr).valueOf()).toBe(dayjs.tz(dateStr, LOCAL_TZ).valueOf()) + }) + + it('change default timezone', () => { + dayjs.tz.setDefault(NY) + const newYork = dayjs.tz('2014-06-01 12:00') + expect(newYork.utcOffset()).toBe(-240) + + dayjs.tz.setDefault(TOKYO) + const tokyo = dayjs.tz('2014-06-01 12:00') + expect(tokyo.format()).toBe('2014-06-01T12:00:00+09:00') + expect(tokyo.format('Z')).toBe('+09:00') + expect(tokyo.valueOf()).toBe(1401591600000) + }) + + it('override default timezone in proto.tz', () => { + dayjs.tz.setDefault(NY) + const tokyo = dayjs.tz('2014-06-01 12:00', TOKYO) + expect(tokyo.format()).toBe('2014-06-01T12:00:00+09:00') + expect(tokyo.format('Z')).toBe('+09:00') + expect(tokyo.valueOf()).toBe(1401591600000) + }) + + it('override default timezone in d.tz', () => { + dayjs.tz.setDefault(NY) + const tokyo = dayjs.tz('2014-06-01 12:00', TOKYO) + expect(tokyo.format()).toBe('2014-06-01T12:00:00+09:00') + expect(tokyo.format('Z')).toBe('+09:00') + expect(tokyo.valueOf()).toBe(1401591600000) + }) +}) diff --git a/types/plugin/duration.d.ts b/types/plugin/duration.d.ts index 38225c922..edc51b6c9 100644 --- a/types/plugin/duration.d.ts +++ b/types/plugin/duration.d.ts @@ -1,12 +1,12 @@ import { PluginFunc } from 'dayjs' declare const plugin: PluginFunc -export = plugin +export default plugin type DurationInputType = string | number | object type DurationAddType = number | object | Duration -declare class Duration { +export declare class Duration { constructor (input: DurationInputType, unit?: string, locale?: string) clone(): Duration