Skip to content

Commit

Permalink
fix: add Duration plugin (#858)
Browse files Browse the repository at this point in the history
  • Loading branch information
1 parent 8c37690 commit 1e33e4b
Show file tree
Hide file tree
Showing 4 changed files with 414 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

# IDE
.idea
.vscode

# npm
node_modules
Expand Down
174 changes: 174 additions & 0 deletions src/plugin/duration/index.js
Original file line number Diff line number Diff line change
@@ -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
}
181 changes: 181 additions & 0 deletions test/plugin/duration.test.js
Original file line number Diff line number Diff line change
@@ -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')
})
Loading

0 comments on commit 1e33e4b

Please sign in to comment.