Clean up useTimeAgo
This commit is contained in:
@@ -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
@@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user