Skip to content

Commit 936397b

Browse files
committed
Reimplement duration rounding.
1 parent a80c1bf commit 936397b

File tree

4 files changed

+214
-348
lines changed

4 files changed

+214
-348
lines changed

Diff for: src/duration.ts

+50-83
Original file line numberDiff line numberDiff line change
@@ -119,101 +119,68 @@ export function elapsedTime(date: Date, precision: Unit = 'second', now = Date.n
119119
)
120120
}
121121

122+
const durationRoundingThresholds = [
123+
Infinity, // Year
124+
11, // Month
125+
28, // Day
126+
21, // Hour
127+
55, // Minute
128+
55, // Second
129+
900, // Millisecond
130+
]
131+
122132
interface RoundingOpts {
123133
relativeTo: Date | number
124134
}
125135

126136
export function roundToSingleUnit(duration: Duration, {relativeTo = Date.now()}: Partial<RoundingOpts> = {}): Duration {
127-
relativeTo = new Date(relativeTo)
137+
return roundBalancedToSingleUnit(
138+
// TODO: Remove the positive sign in `+relativeTo` after integrating the new `elapsedTime` implementation.
139+
elapsedTime(applyDuration(new Date(relativeTo), duration), 'millisecond', +relativeTo),
140+
)
141+
}
142+
143+
export function roundBalancedToSingleUnit(duration: Duration): Duration {
128144
if (duration.blank) return duration
129145
const sign = duration.sign
130-
let years = Math.abs(duration.years)
131-
let months = Math.abs(duration.months)
132-
let weeks = Math.abs(duration.weeks)
133-
let days = Math.abs(duration.days)
134-
let hours = Math.abs(duration.hours)
135-
let minutes = Math.abs(duration.minutes)
136-
let seconds = Math.abs(duration.seconds)
137-
let milliseconds = Math.abs(duration.milliseconds)
138-
139-
if (milliseconds >= 900) seconds += Math.round(milliseconds / 1000)
140-
if (seconds || minutes || hours || days || weeks || months || years) {
141-
milliseconds = 0
146+
const values = [
147+
Math.abs(duration.years),
148+
Math.abs(duration.months),
149+
Math.abs(duration.days),
150+
Math.abs(duration.hours),
151+
Math.abs(duration.minutes),
152+
Math.abs(duration.seconds),
153+
Math.abs(duration.milliseconds),
154+
]
155+
let biggestUnitIndex = values.findIndex(v => v > 0)
156+
const roundedLowerUnit =
157+
biggestUnitIndex < values.length - 1 &&
158+
values[biggestUnitIndex + 1] >= durationRoundingThresholds[biggestUnitIndex + 1]
159+
if (roundedLowerUnit) {
160+
values[biggestUnitIndex] += 1
142161
}
143-
144-
if (seconds >= 55) minutes += Math.round(seconds / 60)
145-
if (minutes || hours || days || weeks || months || years) seconds = 0
146-
147-
if (minutes >= 55) hours += Math.round(minutes / 60)
148-
if (hours || days || weeks || months || years) minutes = 0
149-
150-
if (days && hours >= 12) days += Math.round(hours / 24)
151-
if (!days && hours >= 21) days += Math.round(hours / 24)
152-
if (days || weeks || months || years) hours = 0
153-
154-
// Resolve calendar dates
155-
const currentYear = relativeTo.getFullYear()
156-
const currentMonth = relativeTo.getMonth()
157-
const currentDate = relativeTo.getDate()
158-
if (days >= 27 || years + months + days) {
159-
const newMonthDate = new Date(relativeTo)
160-
newMonthDate.setDate(1)
161-
newMonthDate.setMonth(currentMonth + months * sign + 1)
162-
newMonthDate.setDate(0)
163-
const monthDateCorrection = Math.max(0, currentDate - newMonthDate.getDate())
164-
165-
const newDate = new Date(relativeTo)
166-
newDate.setFullYear(currentYear + years * sign)
167-
newDate.setDate(currentDate - monthDateCorrection)
168-
newDate.setMonth(currentMonth + months * sign)
169-
newDate.setDate(currentDate - monthDateCorrection + days * sign)
170-
const yearDiff = newDate.getFullYear() - relativeTo.getFullYear()
171-
const monthDiff = newDate.getMonth() - relativeTo.getMonth()
172-
const daysDiff = Math.abs(Math.round((Number(newDate) - Number(relativeTo)) / 86400000)) + monthDateCorrection
173-
const monthsDiff = Math.abs(yearDiff * 12 + monthDiff)
174-
if (daysDiff < 27) {
175-
if (days >= 6) {
176-
weeks += Math.round(days / 7)
177-
days = 0
178-
} else {
179-
days = daysDiff
180-
}
181-
months = years = 0
182-
} else if (monthsDiff <= 11) {
183-
months = monthsDiff
184-
years = 0
185-
} else {
186-
months = 0
187-
years = yearDiff * sign
188-
}
189-
if (months || years) days = 0
162+
if (values[biggestUnitIndex] >= durationRoundingThresholds[biggestUnitIndex]) {
163+
--biggestUnitIndex
164+
values[biggestUnitIndex] = 1
190165
}
191-
if (years) months = 0
192-
193-
if (weeks >= 4) months += Math.round(weeks / 4)
194-
if (months || years) weeks = 0
195-
if (days && weeks && !months && !years) {
196-
weeks += Math.round(days / 7)
197-
days = 0
166+
for (let i = biggestUnitIndex + 1; i < values.length; ++i) {
167+
values[i] = 0
198168
}
199-
200-
return new Duration(
201-
years * sign,
202-
months * sign,
203-
weeks * sign,
204-
days * sign,
205-
hours * sign,
206-
minutes * sign,
207-
seconds * sign,
208-
milliseconds * sign,
209-
)
169+
if (biggestUnitIndex === 2 && values[2] >= 6) {
170+
const weeks = Math.max(1, Math.floor((values[2] + (roundedLowerUnit ? 0 : 1)) / 7))
171+
if (weeks < 4) {
172+
return new Duration(0, 0, weeks * sign)
173+
}
174+
values[biggestUnitIndex] = 0
175+
--biggestUnitIndex
176+
values[biggestUnitIndex] = 1
177+
}
178+
values[biggestUnitIndex] *= sign
179+
values.splice(2, 0, 0)
180+
return new Duration(...values)
210181
}
211182

212-
export function getRelativeTimeUnit(
213-
duration: Duration,
214-
opts?: Partial<RoundingOpts>,
215-
): [number, Intl.RelativeTimeFormatUnit] {
216-
const rounded = roundToSingleUnit(duration, opts)
183+
export function getRoundedRelativeTimeUnit(rounded: Duration): [number, Intl.RelativeTimeFormatUnit] {
217184
if (rounded.blank) return [0, 'second']
218185
for (const unit of unitNames) {
219186
if (unit === 'millisecond') continue

Diff for: src/relative-time-element.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
1-
import {Duration, elapsedTime, getRelativeTimeUnit, isDuration, roundToSingleUnit, Unit, unitNames} from './duration.js'
1+
import {
2+
Duration,
3+
Unit,
4+
elapsedTime,
5+
getRoundedRelativeTimeUnit,
6+
isDuration,
7+
roundToSingleUnit,
8+
unitNames,
9+
} from './duration.js'
210
const HTMLElement = globalThis.HTMLElement || (null as unknown as typeof window['HTMLElement'])
311

412
export type DeprecatedFormat = 'auto' | 'micro' | 'elapsed'
@@ -157,6 +165,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor
157165
const tense = this.tense
158166
let empty = emptyDuration
159167
if (format === 'micro') {
168+
// TODO: Switch to `roundBalancedToSingleUnit` after integrating the new `elapsedTime` implementation.
160169
duration = roundToSingleUnit(duration)
161170
empty = microEmptyDuration
162171
if ((this.tense === 'past' && duration.sign !== -1) || (this.tense === 'future' && duration.sign !== 1)) {
@@ -180,7 +189,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor
180189
const tense = this.tense
181190
if (tense === 'future' && duration.sign !== 1) duration = emptyDuration
182191
if (tense === 'past' && duration.sign !== -1) duration = emptyDuration
183-
const [int, unit] = getRelativeTimeUnit(duration)
192+
const [int, unit] = getRoundedRelativeTimeUnit(roundToSingleUnit(duration))
184193
if (unit === 'second' && int < 10) {
185194
return relativeFormat.format(0, this.precision === 'millisecond' ? 'second' : this.precision)
186195
}

0 commit comments

Comments
 (0)