Skip to content

Commit

Permalink
feat(useTimeAgo): non-reactive version formatTimeAgo
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Dec 20, 2022
1 parent 8c82e5e commit 9293c1b
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 55 deletions.
13 changes: 12 additions & 1 deletion packages/core/useTimeAgo/index.md
Expand Up @@ -4,7 +4,7 @@ category: Time

# useTimeAgo

Reactive time ago.
Reactive time ago. Automatically update the time ago string when the time changes.

## Usage

Expand All @@ -21,3 +21,14 @@ const timeAgo = useTimeAgo(new Date(2021, 0, 1))
Time Ago: {{ timeAgo }}
</UseTimeAgo>
```

## Non-Reactivity Usage

In case you don't need the reactivity, you can use the `formatTimeAgo` function to get the formatted string instead of a Ref.

```js
import { formatTimeAgo } from '@vueuse/core'

const timeAgo = formatTimeAgo(new Date(2021, 0, 1)) // string
```

2 changes: 1 addition & 1 deletion packages/core/useTimeAgo/index.test.ts
Expand Up @@ -58,7 +58,7 @@ describe('useTimeAgo', () => {
})

test('get undefined when time is invalid', () => {
expect(useTimeAgo('invalid date').value).toBeUndefined()
expect(useTimeAgo('invalid date').value).toBe('')
})

describe('just now', () => {
Expand Down
114 changes: 61 additions & 53 deletions packages/core/useTimeAgo/index.ts
Expand Up @@ -12,27 +12,14 @@ export interface UseTimeAgoMessagesBuiltIn {
justNow: string
past: string | UseTimeAgoFormatter<string>
future: string | UseTimeAgoFormatter<string>
invalid: string
}

export type UseTimeAgoMessages<UnitNames extends string = UseTimeAgoUnitNamesDefault>
= UseTimeAgoMessagesBuiltIn
& Record<UnitNames, string | UseTimeAgoFormatter<number>>

export interface UseTimeAgoOptions<Controls extends boolean, UnitNames extends string = UseTimeAgoUnitNamesDefault> {
/**
* Expose more controls
*
* @default false
*/
controls?: Controls

/**
* Intervals to update, set 0 to disable auto update
*
* @default 30_000
*/
updateInterval?: number

export interface FormatTimeAgoOptions<UnitNames extends string = UseTimeAgoUnitNamesDefault> {
/**
* Maximum unit (of diff in milliseconds) to display the full date instead of relative
*
Expand Down Expand Up @@ -70,7 +57,23 @@ export interface UseTimeAgoOptions<Controls extends boolean, UnitNames extends s
units?: UseTimeAgoUnit<UseTimeAgoUnitNamesDefault>[]
}

interface UseTimeAgoUnit<Unit extends string = UseTimeAgoUnitNamesDefault> {
export interface UseTimeAgoOptions<Controls extends boolean, UnitNames extends string = UseTimeAgoUnitNamesDefault> extends FormatTimeAgoOptions<UnitNames> {
/**
* Expose more controls
*
* @default false
*/
controls?: Controls

/**
* Intervals to update, set 0 to disable auto update
*
* @default 30_000
*/
updateInterval?: number
}

export interface UseTimeAgoUnit<Unit extends string = UseTimeAgoUnitNamesDefault> {
max: number
value: number
name: Unit
Expand Down Expand Up @@ -113,6 +116,7 @@ const DEFAULT_MESSAGES: UseTimeAgoMessages<UseTimeAgoUnitNamesDefault> = {
hour: n => `${n} hour${n > 1 ? 's' : ''}`,
minute: n => `${n} minute${n > 1 ? 's' : ''}`,
second: n => `${n} second${n > 1 ? 's' : ''}`,
invalid: '',
}

const DEFAULT_FORMATTER = (date: Date) => date.toISOString().slice(0, 10)
Expand All @@ -130,42 +134,46 @@ export function useTimeAgo<UnitNames extends string = UseTimeAgoUnitNamesDefault
export function useTimeAgo<UnitNames extends string = UseTimeAgoUnitNamesDefault>(time: MaybeComputedRef<Date | number | string>, options: UseTimeAgoOptions<boolean, UnitNames> = {}) {
const {
controls: exposeControls = false,
max,
updateInterval = 30_000,
} = options

const { now, ...controls } = useNow({ interval: updateInterval, controls: true })
const timeAgo = computed(() => foramtTimeAgo(new Date(resolveUnref(time)), options, unref(now.value)))

if (exposeControls) {
return {
timeAgo,
...controls,
}
}
else {
return timeAgo
}
}

export function foramtTimeAgo<UnitNames extends string = UseTimeAgoUnitNamesDefault>(from: Date, options: FormatTimeAgoOptions<UnitNames> = {}, now: Date | number = Date.now()): string {
const {
max,
messages = DEFAULT_MESSAGES as UseTimeAgoMessages<UnitNames>,
fullDateFormatter = DEFAULT_FORMATTER,
units = DEFAULT_UNITS,
showSecond = false,
rounding = 'round',
} = options

const { abs } = Math
const roundFn = typeof rounding === 'number'
? (n: number) => +n.toFixed(rounding)
: Math[rounding]
const { now, ...controls } = useNow({ interval: updateInterval, controls: true })

function getTimeAgo(from: Date, now: Date) {
const diff = +now - +from
const absDiff = abs(diff)

// less than a minute
if (absDiff < 60000 && !showSecond)
return messages.justNow

if (typeof max === 'number' && absDiff > max)
return fullDateFormatter(new Date(from))
const diff = +now - +from
const absDiff = Math.abs(diff)

if (typeof max === 'string') {
const unitMax = units.find(i => i.name === max)?.max
if (unitMax && absDiff > unitMax)
return fullDateFormatter(new Date(from))
}
function format(diff: number, unit: UseTimeAgoUnit) {
const val = roundFn(Math.abs(diff) / unit.value)
const past = diff > 0

for (const unit of units) {
if (absDiff < unit.max)
return format(diff, unit)
}
const str = applyFormat(unit.name as UnitNames, val, past)
return applyFormat(past ? 'past' : 'future', str, past)
}

function applyFormat(name: UnitNames | keyof UseTimeAgoMessagesBuiltIn, val: number | string, isPast: boolean) {
Expand All @@ -175,23 +183,23 @@ export function useTimeAgo<UnitNames extends string = UseTimeAgoUnitNamesDefault
return formatter.replace('{0}', val.toString())
}

function format(diff: number, unit: UseTimeAgoUnit) {
const val = roundFn(abs(diff) / unit.value)
const past = diff > 0
// less than a minute
if (absDiff < 60000 && !showSecond)
return messages.justNow

const str = applyFormat(unit.name as UnitNames, val, past)
return applyFormat(past ? 'past' : 'future', str, past)
}
if (typeof max === 'number' && absDiff > max)
return fullDateFormatter(new Date(from))

const timeAgo = computed(() => getTimeAgo(new Date(resolveUnref(time)), unref(now.value)))

if (exposeControls) {
return {
timeAgo,
...controls,
}
if (typeof max === 'string') {
const unitMax = units.find(i => i.name === max)?.max
if (unitMax && absDiff > unitMax)
return fullDateFormatter(new Date(from))
}
else {
return timeAgo

for (const unit of units) {
if (absDiff < unit.max)
return format(diff, unit)
}

return messages.invalid
}

0 comments on commit 9293c1b

Please sign in to comment.