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

src/duration.ts

Lines changed: 50 additions & 83 deletions
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

src/relative-time-element.ts

Lines changed: 11 additions & 2 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy