Skip to content

Commit

Permalink
Add new implementation of relative time.
Browse files Browse the repository at this point in the history
  • Loading branch information
leduyquang753 committed Dec 31, 2024
1 parent a80c1bf commit 361ef2d
Show file tree
Hide file tree
Showing 4 changed files with 337 additions and 10 deletions.
66 changes: 66 additions & 0 deletions src/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,72 @@ export function elapsedTime(date: Date, precision: Unit = 'second', now = Date.n
)
}

const durationRoundingThresholds = [
Infinity, // Year
11, // Month
28, // Day
21, // Hour
55, // Minute
55, // Second
900, // Millisecond
]

export function relativeTime(
date: Date,
precision: Unit = 'second',
nowTimestamp: Date | number = Date.now(),
): [number, Intl.RelativeTimeFormatUnit] {
let precisionIndex = unitNames.indexOf(precision)
if (precisionIndex === -1) {
precisionIndex = unitNames.length
}
const now = new Date(nowTimestamp)
const sign = Math.sign(date.getTime() - now.getTime())
const dateWithoutTime = new Date(date)
dateWithoutTime.setHours(0)
dateWithoutTime.setMinutes(0)
dateWithoutTime.setSeconds(0)
dateWithoutTime.setMilliseconds(0)
const nowWithoutTime = new Date(now)
nowWithoutTime.setHours(0)
nowWithoutTime.setMinutes(0)
nowWithoutTime.setSeconds(0)
nowWithoutTime.setMilliseconds(0)
if (
precisionIndex >= 4 && // At least hour.
(dateWithoutTime.getTime() === nowWithoutTime.getTime() ||
Math.abs(date.getTime() - now.getTime()) < 1000 * 60 * 60 * 12)
) {
const difference = Math.round(((date.getTime() - now.getTime()) / 1000) * sign)
let hours = Math.floor(difference / 3600)
let minutes = Math.floor((difference % 3600) / 60)
const seconds = Math.floor(difference % 60)
if (hours === 0) {
if (seconds >= durationRoundingThresholds[5]) minutes += 1
if (minutes >= durationRoundingThresholds[4]) return [sign, 'hour']
if (precision === 'hour') return [0, 'hour']
if (minutes === 0 && precisionIndex >= 6) return [seconds * sign, 'second']
return [minutes * sign, 'minute']
} else {
if (hours < 23 && minutes >= durationRoundingThresholds[4]) hours += 1
return [hours * sign, 'hour']
}
}
const days = Math.round(((dateWithoutTime.getTime() - nowWithoutTime.getTime()) / (1000 * 60 * 60 * 24)) * sign)
const months = date.getFullYear() * 12 + date.getMonth() - (now.getFullYear() * 12 + now.getMonth())
if (
precisionIndex >= 2 && // At least week.
(months === 0 || days <= 26)
) {
if (precision === 'week' || days >= 6) return [Math.floor((days + 1) / 7) * sign, 'week']
return [days * sign, 'day']
}
if (precision !== 'year' && Math.abs(months) < 12) {
return [months, 'month']
}
return [date.getFullYear() - now.getFullYear(), 'year']
}

interface RoundingOpts {
relativeTo: Date | number
}
Expand Down
13 changes: 7 additions & 6 deletions src/relative-time-element.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Duration, elapsedTime, getRelativeTimeUnit, isDuration, roundToSingleUnit, Unit, unitNames} from './duration.js'
import {Duration, Unit, elapsedTime, isDuration, relativeTime, roundToSingleUnit, unitNames} from './duration.js'
const HTMLElement = globalThis.HTMLElement || (null as unknown as typeof window['HTMLElement'])

export type DeprecatedFormat = 'auto' | 'micro' | 'elapsed'
Expand Down Expand Up @@ -172,15 +172,16 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor
return duration.abs().toLocaleString(locale, {style})
}

#getRelativeFormat(duration: Duration): string {
#getRelativeFormat(date: Date): string {
const relativeFormat = new Intl.RelativeTimeFormat(this.#lang, {
numeric: 'auto',
style: this.formatStyle,
})
let [int, unit] = relativeTime(date, this.precision)
const tense = this.tense
if (tense === 'future' && duration.sign !== 1) duration = emptyDuration
if (tense === 'past' && duration.sign !== -1) duration = emptyDuration
const [int, unit] = getRelativeTimeUnit(duration)
if ((tense === 'future' && int < 0) || (tense === 'past' && int > 0)) {
;[int, unit] = [0, 'second']
}
if (unit === 'second' && int < 10) {
return relativeFormat.format(0, this.precision === 'millisecond' ? 'second' : this.precision)
}
Expand Down Expand Up @@ -453,7 +454,7 @@ export class RelativeTimeElement extends HTMLElement implements Intl.DateTimeFor
if (format === 'duration') {
newText = this.#getDurationFormat(duration)
} else if (format === 'relative') {
newText = this.#getRelativeFormat(duration)
newText = this.#getRelativeFormat(date)
} else {
newText = this.#getDateTimeFormat(date)
}
Expand Down
262 changes: 261 additions & 1 deletion test/duration.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import {assert} from '@open-wc/testing'
import {applyDuration, Duration, elapsedTime, getRelativeTimeUnit, roundToSingleUnit} from '../src/duration.ts'
import {
Duration,
applyDuration,
elapsedTime,
getRelativeTimeUnit,
relativeTime,
roundToSingleUnit,
} from '../src/duration.ts'
import {Temporal} from '@js-temporal/polyfill'

suite('duration', function () {
Expand Down Expand Up @@ -228,6 +235,259 @@ suite('duration', function () {
}
})

suite('relativeTime', function () {
const relativeTests = [
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:00',
expected: [0, 'second'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:01',
expected: [1, 'second'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T11:59:59',
expected: [-1, 'second'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:58',
expected: [1, 'minute'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T11:59:02',
expected: [-1, 'minute'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:05:00',
expected: [5, 'minute'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T11:55:00',
expected: [-5, 'minute'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:58:00',
expected: [1, 'hour'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T11:02:00',
expected: [-1, 'hour'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:54:55',
expected: [1, 'hour'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T11:05:05',
expected: [-1, 'hour'],
},
{
now: '2024-10-15T00:00:00',
date: '2024-10-15T23:59:59',
expected: [23, 'hour'],
},
{
now: '2024-10-15T23:59:59',
date: '2024-10-15T00:00:00',
expected: [-23, 'hour'],
},
{
now: '2024-10-15T18:00:00',
date: '2024-10-16T00:00:00',
expected: [6, 'hour'],
},
{
now: '2024-10-15T00:00:00',
date: '2024-10-14T18:00:00',
expected: [-6, 'hour'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-16T00:00:00',
expected: [1, 'day'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-14T23:00:00',
expected: [-1, 'day'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-25T12:00:00',
expected: [1, 'week'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-05T12:00:00',
expected: [-1, 'week'],
},
{
now: '2024-10-01T12:00:00',
date: '2024-10-21T12:00:00',
expected: [3, 'week'],
},
{
now: '2024-10-21T12:00:00',
date: '2024-10-01T12:00:00',
expected: [-3, 'week'],
},
{
now: '2024-10-05T12:00:00',
date: '2024-11-01T12:00:00',
expected: [1, 'month'],
},
{
now: '2024-10-01T12:00:00',
date: '2024-09-04T12:00:00',
expected: [-1, 'month'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-12-15T12:00:00',
expected: [2, 'month'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-08-15T12:00:00',
expected: [-2, 'month'],
},
{
now: '2024-10-15T12:00:00',
date: '2025-01-15T12:00:00',
expected: [3, 'month'],
},
{
now: '2025-01-15T12:00:00',
date: '2024-10-15T12:00:00',
expected: [-3, 'month'],
},
{
now: '2024-10-15T12:00:00Z',
date: '2025-09-15T12:00:00Z',
expected: [11, 'month'],
},
{
now: '2024-10-15T12:00:00Z',
date: '2023-11-15T12:00:00Z',
expected: [-11, 'month'],
},
{
now: '2024-10-15T12:00:00Z',
date: '2025-10-15T12:00:00Z',
expected: [1, 'year'],
},
{
now: '2024-10-15T12:00:00Z',
date: '2023-10-15T12:00:00Z',
expected: [-1, 'year'],
},
{
now: '2024-10-15T12:00:00Z',
date: '2029-01-15T12:00:00Z',
expected: [5, 'year'],
},
{
now: '2024-01-15T12:00:00Z',
date: '2019-10-15T12:00:00Z',
expected: [-5, 'year'],
},

{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:00',
precision: 'minute',
expected: [0, 'minute'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:00',
precision: 'hour',
expected: [0, 'hour'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:00',
precision: 'day',
expected: [0, 'day'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:00',
precision: 'week',
expected: [0, 'week'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:00',
precision: 'month',
expected: [0, 'month'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:00',
precision: 'year',
expected: [0, 'year'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:00:50',
precision: 'minute',
expected: [0, 'minute'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T12:50:00',
precision: 'hour',
expected: [0, 'hour'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-15T22:00:00',
precision: 'day',
expected: [0, 'day'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-20T12:00:00',
precision: 'week',
expected: [0, 'week'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-10-31T12:00:00',
precision: 'month',
expected: [0, 'month'],
},
{
now: '2024-10-15T12:00:00',
date: '2024-12-15T12:00:00',
precision: 'year',
expected: [0, 'year'],
},
]
for (const {
now,
date,
precision = 'second',
expected: [val, unit],
} of relativeTests) {
test(`relativeTime(${date}, ${precision}, ${now}) === [${val}, ${unit}]`, () => {
assert.deepEqual(relativeTime(new Date(date), precision, new Date(now)), [val, unit])
})
}
})

suite('roundToSingleUnit', function () {
const roundTests = new Set([
['PT20S', 'PT20S'],
Expand Down
Loading

0 comments on commit 361ef2d

Please sign in to comment.