Clean up useTimeAgo

This commit is contained in:
Eric Bailey
2024-08-29 16:42:39 -05:00
parent dd19a1a323
commit 0ac13b17ac
2 changed files with 289 additions and 86 deletions
+136 -25
View File
@@ -1,102 +1,213 @@
import {describe, expect, it} from '@jest/globals' import {describe, expect, it} from '@jest/globals'
import {MessageDescriptor} from '@lingui/core'
import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns' import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns'
import {dateDiff} from '../useTimeAgo' import {dateDiff} from '../useTimeAgo'
const lingui: any = (obj: MessageDescriptor) => obj.message
const base = new Date('2024-06-17T00:00:00Z') const base = new Date('2024-06-17T00:00:00Z')
describe('dateDiff', () => { describe('dateDiff', () => {
it(`works with numbers`, () => { it(`works with numbers`, () => {
expect(dateDiff(subDays(base, 3), Number(base), {lingui})).toEqual('3d') const earlier = subDays(base, 3)
expect(dateDiff(earlier, Number(base))).toEqual({
value: 3,
unit: 'day',
earlier,
later: base,
})
}) })
it(`works with strings`, () => { it(`works with strings`, () => {
expect(dateDiff(subDays(base, 3), base.toString(), {lingui})).toEqual('3d') const earlier = subDays(base, 3)
expect(dateDiff(earlier, base.toString())).toEqual({
value: 3,
unit: 'day',
earlier,
later: base,
})
}) })
it(`works with dates`, () => { it(`works with dates`, () => {
expect(dateDiff(subDays(base, 3), base, {lingui})).toEqual('3d') const earlier = subDays(base, 3)
expect(dateDiff(earlier, base)).toEqual({
value: 3,
unit: 'day',
earlier,
later: base,
})
}) })
it(`equal values return now`, () => { it(`equal values return now`, () => {
expect(dateDiff(base, base, {lingui})).toEqual('now') expect(dateDiff(base, base)).toEqual({
value: 0,
unit: 'now',
earlier: base,
later: base,
})
}) })
it(`future dates return now`, () => { it(`future dates return now`, () => {
expect(dateDiff(addDays(base, 3), base, {lingui})).toEqual('now') const earlier = addDays(base, 3)
expect(dateDiff(earlier, base)).toEqual({
value: 0,
unit: 'now',
earlier,
later: base,
})
}) })
it(`values < 5 seconds ago return now`, () => { it(`values < 5 seconds ago return now`, () => {
const then = subSeconds(base, 4) const then = subSeconds(base, 4)
expect(dateDiff(then, base, {lingui})).toEqual('now') expect(dateDiff(then, base)).toEqual({
value: 0,
unit: 'now',
earlier: then,
later: base,
})
}) })
it(`values >= 5 seconds ago return seconds`, () => { it(`values >= 5 seconds ago return seconds`, () => {
const then = subSeconds(base, 5) const then = subSeconds(base, 5)
expect(dateDiff(then, base, {lingui})).toEqual('5s') expect(dateDiff(then, base)).toEqual({
value: 5,
unit: 'second',
earlier: then,
later: base,
})
}) })
it(`values < 1 min return seconds`, () => { it(`values < 1 min return seconds`, () => {
const then = subSeconds(base, 59) const then = subSeconds(base, 59)
expect(dateDiff(then, base, {lingui})).toEqual('59s') expect(dateDiff(then, base)).toEqual({
value: 59,
unit: 'second',
earlier: then,
later: base,
})
}) })
it(`values >= 1 min return minutes`, () => { it(`values >= 1 min return minutes`, () => {
const then = subSeconds(base, 60) const then = subSeconds(base, 60)
expect(dateDiff(then, base, {lingui})).toEqual('1m') expect(dateDiff(then, base)).toEqual({
value: 1,
unit: 'minute',
earlier: then,
later: base,
})
}) })
it(`minutes round down`, () => { it(`minutes round down`, () => {
const then = subSeconds(base, 119) const then = subSeconds(base, 119)
expect(dateDiff(then, base, {lingui})).toEqual('1m') expect(dateDiff(then, base)).toEqual({
value: 1,
unit: 'minute',
earlier: then,
later: base,
})
}) })
it(`values < 1 hour return minutes`, () => { it(`values < 1 hour return minutes`, () => {
const then = subMinutes(base, 59) const then = subMinutes(base, 59)
expect(dateDiff(then, base, {lingui})).toEqual('59m') expect(dateDiff(then, base)).toEqual({
value: 59,
unit: 'minute',
earlier: then,
later: base,
})
}) })
it(`values >= 1 hour return hours`, () => { it(`values >= 1 hour return hours`, () => {
const then = subMinutes(base, 60) const then = subMinutes(base, 60)
expect(dateDiff(then, base, {lingui})).toEqual('1h') expect(dateDiff(then, base)).toEqual({
value: 1,
unit: 'hour',
earlier: then,
later: base,
})
}) })
it(`hours round down`, () => { it(`hours round down`, () => {
const then = subMinutes(base, 119) const then = subMinutes(base, 119)
expect(dateDiff(then, base, {lingui})).toEqual('1h') expect(dateDiff(then, base)).toEqual({
value: 1,
unit: 'hour',
earlier: then,
later: base,
})
}) })
it(`values < 1 day return hours`, () => { it(`values < 1 day return hours`, () => {
const then = subHours(base, 23) const then = subHours(base, 23)
expect(dateDiff(then, base, {lingui})).toEqual('23h') expect(dateDiff(then, base)).toEqual({
value: 23,
unit: 'hour',
earlier: then,
later: base,
})
}) })
it(`values >= 1 day return days`, () => { it(`values >= 1 day return days`, () => {
const then = subHours(base, 24) const then = subHours(base, 24)
expect(dateDiff(then, base, {lingui})).toEqual('1d') expect(dateDiff(then, base)).toEqual({
value: 1,
unit: 'day',
earlier: then,
later: base,
})
}) })
it(`days round down`, () => { it(`days round down`, () => {
const then = subHours(base, 47) const then = subHours(base, 47)
expect(dateDiff(then, base, {lingui})).toEqual('1d') expect(dateDiff(then, base)).toEqual({
value: 1,
unit: 'day',
earlier: then,
later: base,
})
}) })
it(`values < 30 days return days`, () => { it(`values < 30 days return days`, () => {
const then = subDays(base, 29) const then = subDays(base, 29)
expect(dateDiff(then, base, {lingui})).toEqual('29d') expect(dateDiff(then, base)).toEqual({
value: 29,
unit: 'day',
earlier: then,
later: base,
})
}) })
it(`values >= 30 days return months`, () => { it(`values >= 30 days return months`, () => {
const then = subDays(base, 30) const then = subDays(base, 30)
expect(dateDiff(then, base, {lingui})).toEqual('1mo') expect(dateDiff(then, base)).toEqual({
value: 1,
unit: 'month',
earlier: then,
later: base,
})
}) })
it(`months round down`, () => { it(`months round down`, () => {
const then = subDays(base, 59) const then = subDays(base, 59)
expect(dateDiff(then, base, {lingui})).toEqual('1mo') expect(dateDiff(then, base)).toEqual({
value: 1,
unit: 'month',
earlier: then,
later: base,
})
}) })
it(`values are rounded by increments of 30`, () => { it(`values are rounded by increments of 30`, () => {
const then = subDays(base, 61) const then = subDays(base, 61)
expect(dateDiff(then, base, {lingui})).toEqual('2mo') expect(dateDiff(then, base)).toEqual({
value: 2,
unit: 'month',
earlier: then,
later: base,
})
}) })
it(`values < 360 days return months`, () => { it(`values < 360 days return months`, () => {
const then = subDays(base, 359) const then = subDays(base, 359)
expect(dateDiff(then, base, {lingui})).toEqual('11mo') expect(dateDiff(then, base)).toEqual({
value: 11,
unit: 'month',
earlier: then,
later: base,
})
}) })
it(`values >= 360 days return the earlier value`, () => { it(`values >= 360 days return the earlier value`, () => {
const then = subDays(base, 360) const then = subDays(base, 360)
expect(dateDiff(then, base, {lingui})).toEqual(then.toLocaleDateString()) expect(dateDiff(then, base)).toEqual({
value: 12,
unit: 'month',
earlier: then,
later: base,
})
}) })
}) })
+153 -61
View File
@@ -1,25 +1,16 @@
import {useCallback} from 'react' import {useCallback} from 'react'
import {msg, plural} from '@lingui/macro' import {I18n} from '@lingui/core'
import {I18nContext, useLingui} from '@lingui/react' import {defineMessage, msg, plural} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {differenceInSeconds} from 'date-fns' import {differenceInSeconds} from 'date-fns'
export type TimeAgoOptions = { export type DateDiffFormat = 'long' | 'short'
lingui: I18nContext['_']
format?: 'long' | 'short'
}
export function useGetTimeAgo() { type DateDiff = {
const {_} = useLingui() value: number
return useCallback( unit: 'now' | 'second' | 'minute' | 'hour' | 'day' | 'month'
( earlier: Date
earlier: number | string | Date, later: Date
later: number | string | Date,
options?: Omit<TimeAgoOptions, 'lingui'>,
) => {
return dateDiff(earlier, later, {lingui: _, format: options?.format})
},
[_],
)
} }
const NOW = 5 const NOW = 5
@@ -28,59 +19,160 @@ const HOUR = MINUTE * 60
const DAY = HOUR * 24 const DAY = HOUR * 24
const MONTH_30 = DAY * 30 const MONTH_30 = DAY * 30
export function useGetTimeAgo() {
const {i18n} = useLingui()
return useCallback(
(
earlier: number | string | Date,
later: number | string | Date,
options?: {format: DateDiffFormat},
) => {
const diff = dateDiff(earlier, later)
return formatDateDiff({diff, i18n, format: options?.format})
},
[i18n],
)
}
/** /**
* Returns the difference between `earlier` and `later` dates, formatted as a * Returns the difference between `earlier` and `later` dates, based on
* natural language string. * opinionated rules.
*
* - All month are considered exactly 30 days.
* - Dates assume `earlier` <= `later`, and will otherwise return 'now'.
* - All values round down
*/
export function dateDiff(
earlier: number | string | Date,
later: number | string | Date,
): DateDiff {
let diff = {
value: 0,
unit: 'now' as DateDiff['unit'],
}
const e = new Date(earlier)
const l = new Date(later)
const diffSeconds = differenceInSeconds(l, e)
if (diffSeconds < NOW) {
diff = {
value: 0,
unit: 'now' as DateDiff['unit'],
}
} else if (diffSeconds < MINUTE) {
diff = {
value: diffSeconds,
unit: 'second' as DateDiff['unit'],
}
} else if (diffSeconds < HOUR) {
const value = Math.floor(diffSeconds / MINUTE)
diff = {
value,
unit: 'minute' as DateDiff['unit'],
}
} else if (diffSeconds < DAY) {
const value = Math.floor(diffSeconds / HOUR)
diff = {
value,
unit: 'hour' as DateDiff['unit'],
}
} else if (diffSeconds < MONTH_30) {
const value = Math.floor(diffSeconds / DAY)
diff = {
value,
unit: 'day' as DateDiff['unit'],
}
} else {
const value = Math.floor(diffSeconds / MONTH_30)
diff = {
value,
unit: 'month' as DateDiff['unit'],
}
}
return {
...diff,
earlier: e,
later: l,
}
}
/**
* Accepts a `DateDiff` and teturns the difference between `earlier` and
* `later` dates, formatted as a natural language string.
* *
* - All month are considered exactly 30 days. * - All month are considered exactly 30 days.
* - Dates assume `earlier` <= `later`, and will otherwise return 'now'. * - Dates assume `earlier` <= `later`, and will otherwise return 'now'.
* - Differences >= 360 days are returned as the "M/D/YYYY" string * - Differences >= 360 days are returned as the "M/D/YYYY" string
* - All values round down * - All values round down
*/ */
export function dateDiff( export function formatDateDiff({
earlier: number | string | Date, diff,
later: number | string | Date, format = 'short',
options: TimeAgoOptions, i18n,
): string { }: {
const _ = options.lingui diff: DateDiff
const format = options?.format || 'short' format?: DateDiffFormat
i18n: I18n
}): string {
const long = format === 'long' const long = format === 'long'
const diffSeconds = differenceInSeconds(new Date(later), new Date(earlier))
if (diffSeconds < NOW) { switch (diff.unit) {
return _(msg`now`) case 'now': {
} else if (diffSeconds < MINUTE) { return i18n._(msg`now`)
return `${diffSeconds}${ }
long ? ` ${plural(diffSeconds, {one: 'second', other: 'seconds'})}` : 's' case 'second': {
}` return long
} else if (diffSeconds < HOUR) { ? i18n._(plural(diff.value, {one: '# second', other: '# seconds'}))
const diff = Math.floor(diffSeconds / MINUTE) : i18n._(
return `${diff}${ defineMessage({
long ? ` ${plural(diff, {one: 'minute', other: 'minutes'})}` : 'm' message: `${diff.value}s`,
}` comment: `How many seconds have passed, displayed in a narrow form`,
} else if (diffSeconds < DAY) { }),
const diff = Math.floor(diffSeconds / HOUR) )
return `${diff}${ }
long ? ` ${plural(diff, {one: 'hour', other: 'hours'})}` : 'h' case 'minute': {
}` return long
} else if (diffSeconds < MONTH_30) { ? i18n._(plural(diff.value, {one: '# minute', other: '# minutes'}))
const diff = Math.floor(diffSeconds / DAY) : i18n._(
return `${diff}${ defineMessage({
long ? ` ${plural(diff, {one: 'day', other: 'days'})}` : 'd' message: `${diff.value}m`,
}` comment: `How many minutes have passed, displayed in a narrow form`,
} else { }),
const diff = Math.floor(diffSeconds / MONTH_30) )
if (diff < 12) { }
return `${diff}${ case 'hour': {
long ? ` ${plural(diff, {one: 'month', other: 'months'})}` : 'mo' return long
}` ? i18n._(plural(diff.value, {one: '# hour', other: '# hours'}))
} else { : i18n._(
const str = new Date(earlier).toLocaleDateString() defineMessage({
message: `${diff.value}h`,
if (long) { comment: `How many hours have passed, displayed in a narrow form`,
return _(msg`on ${str}`) }),
)
}
case 'day': {
return long
? i18n._(plural(diff.value, {one: '# day', other: '# days'}))
: i18n._(
defineMessage({
message: `${diff.value}d`,
comment: `How many days have passed, displayed in a narrow form`,
}),
)
}
case 'month': {
if (diff.value < 12) {
return long
? i18n._(plural(diff.value, {one: '# month', other: '# months'}))
: i18n._(
defineMessage({
message: `${diff.value}mo`,
comment: `How many months have passed, displayed in a narrow form`,
}),
)
} }
return str return i18n.date(new Date(diff.earlier))
} }
} }
} }