Skip to content

Commit 3f5c085

Browse files
authored
fix: Correct handling negative duration (#1317)
1 parent 5c785d5 commit 3f5c085

File tree

2 files changed

+118
-25
lines changed

2 files changed

+118
-25
lines changed

src/plugin/duration/index.js

+79-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { MILLISECONDS_A_DAY, MILLISECONDS_A_HOUR, MILLISECONDS_A_MINUTE, MILLISECONDS_A_SECOND, MILLISECONDS_A_WEEK, REGEX_FORMAT } from '../../constant'
1+
import {
2+
MILLISECONDS_A_DAY,
3+
MILLISECONDS_A_HOUR,
4+
MILLISECONDS_A_MINUTE,
5+
MILLISECONDS_A_SECOND,
6+
MILLISECONDS_A_WEEK,
7+
REGEX_FORMAT
8+
} from '../../constant'
29

310
const MILLISECONDS_A_YEAR = MILLISECONDS_A_DAY * 365
411
const MILLISECONDS_A_MONTH = MILLISECONDS_A_DAY * 30
@@ -16,7 +23,7 @@ const unitToMS = {
1623
weeks: MILLISECONDS_A_WEEK
1724
}
1825

19-
const isDuration = d => (d instanceof Duration) // eslint-disable-line no-use-before-define
26+
const isDuration = d => d instanceof Duration // eslint-disable-line no-use-before-define
2027

2128
let $d
2229
let $u
@@ -25,6 +32,30 @@ const wrapper = (input, instance, unit) =>
2532
new Duration(input, unit, instance.$l) // eslint-disable-line no-use-before-define
2633

2734
const prettyUnit = unit => `${$u.p(unit)}s`
35+
const isNegative = number => number < 0
36+
const roundNumber = number =>
37+
(isNegative(number) ? Math.ceil(number) : Math.floor(number))
38+
const absolute = number => Math.abs(number)
39+
const getNumberUnitFormat = (number, unit) => {
40+
if (!number) {
41+
return {
42+
negative: false,
43+
format: ''
44+
}
45+
}
46+
47+
if (isNegative(number)) {
48+
return {
49+
negative: true,
50+
format: `${absolute(number)}${unit}`
51+
}
52+
}
53+
54+
return {
55+
negative: false,
56+
format: `${number}${unit}`
57+
}
58+
}
2859

2960
class Duration {
3061
constructor(input, unit, locale) {
@@ -49,8 +80,14 @@ class Duration {
4980
const d = input.match(durationRegex)
5081
if (d) {
5182
[,,
52-
this.$d.years, this.$d.months, this.$d.weeks,
53-
this.$d.days, this.$d.hours, this.$d.minutes, this.$d.seconds] = d
83+
this.$d.years,
84+
this.$d.months,
85+
this.$d.weeks,
86+
this.$d.days,
87+
this.$d.hours,
88+
this.$d.minutes,
89+
this.$d.seconds
90+
] = d
5491
this.calMilliseconds()
5592
return this
5693
}
@@ -66,39 +103,54 @@ class Duration {
66103

67104
parseFromMilliseconds() {
68105
let { $ms } = this
69-
this.$d.years = Math.floor($ms / MILLISECONDS_A_YEAR)
106+
this.$d.years = roundNumber($ms / MILLISECONDS_A_YEAR)
70107
$ms %= MILLISECONDS_A_YEAR
71-
this.$d.months = Math.floor($ms / MILLISECONDS_A_MONTH)
108+
this.$d.months = roundNumber($ms / MILLISECONDS_A_MONTH)
72109
$ms %= MILLISECONDS_A_MONTH
73-
this.$d.days = Math.floor($ms / MILLISECONDS_A_DAY)
110+
this.$d.days = roundNumber($ms / MILLISECONDS_A_DAY)
74111
$ms %= MILLISECONDS_A_DAY
75-
this.$d.hours = Math.floor($ms / MILLISECONDS_A_HOUR)
112+
this.$d.hours = roundNumber($ms / MILLISECONDS_A_HOUR)
76113
$ms %= MILLISECONDS_A_HOUR
77-
this.$d.minutes = Math.floor($ms / MILLISECONDS_A_MINUTE)
114+
this.$d.minutes = roundNumber($ms / MILLISECONDS_A_MINUTE)
78115
$ms %= MILLISECONDS_A_MINUTE
79-
this.$d.seconds = Math.floor($ms / MILLISECONDS_A_SECOND)
116+
this.$d.seconds = roundNumber($ms / MILLISECONDS_A_SECOND)
80117
$ms %= MILLISECONDS_A_SECOND
81118
this.$d.milliseconds = $ms
82119
}
83120

84121
toISOString() {
85-
const Y = this.$d.years ? `${this.$d.years}Y` : ''
86-
const M = this.$d.months ? `${this.$d.months}M` : ''
122+
const Y = getNumberUnitFormat(this.$d.years, 'Y')
123+
const M = getNumberUnitFormat(this.$d.months, 'M')
124+
87125
let days = +this.$d.days || 0
88126
if (this.$d.weeks) {
89127
days += this.$d.weeks * 7
90128
}
91-
const D = days ? `${days}D` : ''
92-
const H = this.$d.hours ? `${this.$d.hours}H` : ''
93-
const m = this.$d.minutes ? `${this.$d.minutes}M` : ''
129+
130+
const D = getNumberUnitFormat(days, 'D')
131+
const H = getNumberUnitFormat(this.$d.hours, 'H')
132+
const m = getNumberUnitFormat(this.$d.minutes, 'M')
133+
94134
let seconds = this.$d.seconds || 0
95135
if (this.$d.milliseconds) {
96136
seconds += this.$d.milliseconds / 1000
97137
}
98-
const S = seconds ? `${seconds}S` : ''
99-
const T = (H || m || S) ? 'T' : ''
100-
const result = `P${Y}${M}${D}${T}${H}${m}${S}`
101-
return result === 'P' ? 'P0D' : result
138+
139+
const S = getNumberUnitFormat(seconds, 'S')
140+
141+
const negativeMode =
142+
Y.negative ||
143+
M.negative ||
144+
D.negative ||
145+
H.negative ||
146+
m.negative ||
147+
S.negative
148+
149+
const T = H.format || m.format || S.format ? 'T' : ''
150+
const P = negativeMode ? '-' : ''
151+
152+
const result = `${P}P${Y.format}${M.format}${D.format}${T}${H.format}${m.format}${S.format}`
153+
return result === 'P' || result === '-P' ? 'P0D' : result
102154
}
103155

104156
toJSON() {
@@ -136,11 +188,11 @@ class Duration {
136188
if (pUnit === 'milliseconds') {
137189
base %= 1000
138190
} else if (pUnit === 'weeks') {
139-
base = Math.floor(base / unitToMS[pUnit])
191+
base = roundNumber(base / unitToMS[pUnit])
140192
} else {
141193
base = this.$d[pUnit]
142194
}
143-
return base
195+
return base === 0 ? 0 : base // a === 0 will be true on both 0 and -0
144196
}
145197

146198
add(input, unit, isSubtract) {
@@ -152,6 +204,7 @@ class Duration {
152204
} else {
153205
another = wrapper(input, this).$ms
154206
}
207+
155208
return wrapper(this.$ms + (another * (isSubtract ? -1 : 1)), this)
156209
}
157210

@@ -170,7 +223,10 @@ class Duration {
170223
}
171224

172225
humanize(withSuffix) {
173-
return $d().add(this.$ms, 'ms').locale(this.$l).fromNow(!withSuffix)
226+
return $d()
227+
.add(this.$ms, 'ms')
228+
.locale(this.$l)
229+
.fromNow(!withSuffix)
174230
}
175231

176232
milliseconds() { return this.get('milliseconds') }
@@ -190,6 +246,7 @@ class Duration {
190246
years() { return this.get('years') }
191247
asYears() { return this.as('years') }
192248
}
249+
193250
export default (option, Dayjs, dayjs) => {
194251
$d = dayjs
195252
$u = dayjs().$utils()

test/plugin/duration.test.js

+39-3
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ describe('Creating', () => {
2727
expect(dayjs.duration(60, 'seconds').toISOString()).toBe('PT1M')
2828
expect(dayjs.duration(13213, 'seconds').toISOString()).toBe('PT3H40M13S')
2929
})
30+
it('two argument will bubble up to the next (negative number)', () => {
31+
expect(dayjs.duration(-59, 'seconds').toISOString()).toBe('-PT59S')
32+
expect(dayjs.duration(-60, 'seconds').toISOString()).toBe('-PT1M')
33+
expect(dayjs.duration(-13213, 'seconds').toISOString()).toBe('-PT3H40M13S')
34+
})
3035
it('object with float', () => {
3136
expect(dayjs.duration({
3237
seconds: 1,
@@ -53,9 +58,13 @@ describe('Creating', () => {
5358
ms: 1
5459
}).toISOString()).toBe('PT0.001S')
5560
})
61+
it('object with negative millisecond', () => {
62+
expect(dayjs.duration({
63+
ms: -1
64+
}).toISOString()).toBe('-PT0.001S')
65+
})
5666
})
5767

58-
5968
describe('Parse ISO string', () => {
6069
it('Full ISO string', () => {
6170
expect(dayjs.duration('P7Y6M4DT3H2M1S').toISOString()).toBe('P7Y6M4DT3H2M1S')
@@ -131,6 +140,26 @@ describe('Milliseconds', () => {
131140
expect(dayjs.duration(15000).asMilliseconds()).toBe(15000)
132141
})
133142

143+
describe('Milliseconds', () => {
144+
describe('Positive number', () => {
145+
expect(dayjs.duration(500).milliseconds()).toBe(500)
146+
expect(dayjs.duration(1500).milliseconds()).toBe(500)
147+
expect(dayjs.duration(15000).milliseconds()).toBe(0)
148+
expect(dayjs.duration(500).asMilliseconds()).toBe(500)
149+
expect(dayjs.duration(1500).asMilliseconds()).toBe(1500)
150+
expect(dayjs.duration(15000).asMilliseconds()).toBe(15000)
151+
})
152+
153+
describe('Negative number', () => {
154+
expect(dayjs.duration(-500).milliseconds()).toBe(-500)
155+
expect(dayjs.duration(-1500).milliseconds()).toBe(-500)
156+
expect(dayjs.duration(-15000).milliseconds()).toBe(0)
157+
expect(dayjs.duration(-500).asMilliseconds()).toBe(-500)
158+
expect(dayjs.duration(-1500).asMilliseconds()).toBe(-1500)
159+
expect(dayjs.duration(-15000).asMilliseconds()).toBe(-15000)
160+
})
161+
})
162+
134163
describe('Add', () => {
135164
const a = dayjs.duration(1, 'days')
136165
const b = dayjs.duration(2, 'days')
@@ -179,8 +208,15 @@ describe('Hours', () => {
179208
})
180209

181210
describe('Days', () => {
182-
expect(dayjs.duration(100000000).days()).toBe(1)
183-
expect(dayjs.duration(100000000).asDays().toFixed(2)).toBe('1.16')
211+
it('positive number', () => {
212+
expect(dayjs.duration(100000000).days()).toBe(1)
213+
expect(dayjs.duration(100000000).asDays().toFixed(2)).toBe('1.16')
214+
})
215+
216+
it('negative number', () => {
217+
expect(dayjs.duration(-1).days()).toBe(0)
218+
expect(dayjs.duration(-86399999).asDays()).toBeCloseTo(-0.999999, 4)
219+
})
184220
})
185221

186222
describe('Weeks', () => {

0 commit comments

Comments
 (0)