diff --git a/src/relative-time.js b/src/relative-time.js index 35c712a..3147563 100644 --- a/src/relative-time.js +++ b/src/relative-time.js @@ -1,6 +1,6 @@ /* @flow strict */ -import {strftime, makeFormatter, isDayFirst, isThisYear, isYearSeparator} from './utils' +import {strftime, makeFormatter, makeRelativeFormatter, isDayFirst, isThisYear, isYearSeparator} from './utils' export default class RelativeTime { date: Date @@ -62,31 +62,29 @@ export default class RelativeTime { const month = Math.round(day / 30) const year = Math.round(month / 12) if (ms < 0) { - return 'just now' + return formatRelativeTime(0, 'second') } else if (sec < 10) { - return 'just now' + return formatRelativeTime(0, 'second') } else if (sec < 45) { - return `${sec} seconds ago` + return formatRelativeTime(-sec, 'second') } else if (sec < 90) { - return 'a minute ago' + return formatRelativeTime(-min, 'minute') } else if (min < 45) { - return `${min} minutes ago` + return formatRelativeTime(-min, 'minute') } else if (min < 90) { - return 'an hour ago' + return formatRelativeTime(-hr, 'hour') } else if (hr < 24) { - return `${hr} hours ago` + return formatRelativeTime(-hr, 'hour') } else if (hr < 36) { - return 'a day ago' + return formatRelativeTime(-day, 'day') } else if (day < 30) { - return `${day} days ago` + return formatRelativeTime(-day, 'day') } else if (day < 45) { - return 'a month ago' - } else if (month < 12) { - return `${month} months ago` + return formatRelativeTime(-month, 'month') } else if (month < 18) { - return 'a year ago' + return formatRelativeTime(-year, 'year') } else { - return `${year} years ago` + return formatRelativeTime(-year, 'year') } } @@ -124,29 +122,29 @@ export default class RelativeTime { const month = Math.round(day / 30) const year = Math.round(month / 12) if (month >= 18) { - return `${year} years from now` + return formatRelativeTime(year, 'year') } else if (month >= 12) { - return 'a year from now' + return formatRelativeTime(year, 'year') } else if (day >= 45) { - return `${month} months from now` + return formatRelativeTime(month, 'month') } else if (day >= 30) { - return 'a month from now' + return formatRelativeTime(month, 'month') } else if (hr >= 36) { - return `${day} days from now` + return formatRelativeTime(day, 'day') } else if (hr >= 24) { - return 'a day from now' + return formatRelativeTime(day, 'day') } else if (min >= 90) { - return `${hr} hours from now` + return formatRelativeTime(hr, 'hour') } else if (min >= 45) { - return 'an hour from now' + return formatRelativeTime(hr, 'hour') } else if (sec >= 90) { - return `${min} minutes from now` + return formatRelativeTime(min, 'minute') } else if (sec >= 45) { - return 'a minute from now' + return formatRelativeTime(min, 'minute') } else if (sec >= 10) { - return `${sec} seconds from now` + return formatRelativeTime(sec, 'second') } else { - return 'just now' + return formatRelativeTime(0, 'second') } } @@ -189,4 +187,92 @@ export default class RelativeTime { } } +function formatRelativeTime(value: number, unit: string): string { + const formatter = relativeFormatter() + if (formatter) { + return formatter.format(value, unit) + } else { + return formatEnRelativeTime(value, unit) + } +} + +// Simplified "en" RelativeTimeFormat.format function +// +// Values should roughly match +// new Intl.RelativeTimeFormat('en', {numeric: 'auto'}).format(value, unit) +// +function formatEnRelativeTime(value: number, unit: string): string { + if (value === 0) { + switch (unit) { + case 'year': + case 'quarter': + case 'month': + case 'week': + return `this ${unit}` + case 'day': + return 'today' + case 'hour': + case 'minute': + return `in 0 ${unit}s` + case 'second': + return 'now' + } + } else if (value === 1) { + switch (unit) { + case 'year': + case 'quarter': + case 'month': + case 'week': + return `next ${unit}` + case 'day': + return 'tomorrow' + case 'hour': + case 'minute': + case 'second': + return `in 1 ${unit}` + } + } else if (value === -1) { + switch (unit) { + case 'year': + case 'quarter': + case 'month': + case 'week': + return `last ${unit}` + case 'day': + return 'yesterday' + case 'hour': + case 'minute': + case 'second': + return `1 ${unit} ago` + } + } else if (value > 1) { + switch (unit) { + case 'year': + case 'quarter': + case 'month': + case 'week': + case 'day': + case 'hour': + case 'minute': + case 'second': + return `in ${value} ${unit}s` + } + } else if (value < -1) { + switch (unit) { + case 'year': + case 'quarter': + case 'month': + case 'week': + case 'day': + case 'hour': + case 'minute': + case 'second': + return `${-value} ${unit}s ago` + } + } + + throw new RangeError(`Invalid unit argument for format() '${unit}'`) +} + const timeFormatter = makeFormatter({hour: 'numeric', minute: '2-digit'}) +const relativeFormatter = makeRelativeFormatter({numeric: 'auto'}) diff --git a/src/utils.js b/src/utils.js index b97bb20..8e49bf3 100644 --- a/src/utils.js +++ b/src/utils.js @@ -164,3 +164,23 @@ export function isThisYear(date: Date) { const now = new Date() return now.getUTCFullYear() === date.getUTCFullYear() } + +// eslint-disable-next-line flowtype/no-weak-types +export function makeRelativeFormatter(options: any): () => ?any { + let format + return function() { + if (format) return format + if ('Intl' in window && 'RelativeTimeFormat' in window.Intl) { + try { + // eslint-disable-next-line flowtype/no-flow-fix-me-comments + // $FlowFixMe: missing RelativeTimeFormat type + format = new Intl.RelativeTimeFormat(undefined, options) + return format + } catch (e) { + if (!(e instanceof RangeError)) { + throw e + } + } + } + } +} diff --git a/test/relative-time.js b/test/relative-time.js index d9eeb43..57f8835 100644 --- a/test/relative-time.js +++ b/test/relative-time.js @@ -10,14 +10,14 @@ suite('relative-time', function() { const now = new Date(Date.now() + 3 * 60 * 60 * 24 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, '3 days from now') + assert.equal(time.textContent, 'in 3 days') }) - test('rewrites from now past datetime to a day ago', function() { + test('rewrites from now past datetime to yesterday', function() { const now = new Date(Date.now() - 1 * 60 * 60 * 24 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'a day ago') + assert.equal(time.textContent, 'yesterday') }) test('rewrites from now past datetime to hours ago', function() { @@ -31,14 +31,14 @@ suite('relative-time', function() { const now = new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, '3 hours from now') + assert.equal(time.textContent, 'in 3 hours') }) test('rewrites from now past datetime to an hour ago', function() { const now = new Date(Date.now() - 1 * 60 * 60 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'an hour ago') + assert.equal(time.textContent, '1 hour ago') }) test('rewrites from now past datetime to minutes ago', function() { @@ -52,49 +52,49 @@ suite('relative-time', function() { const now = new Date(Date.now() + 3 * 60 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, '3 minutes from now') + assert.equal(time.textContent, 'in 3 minutes') }) test('rewrites from now past datetime to a minute ago', function() { const now = new Date(Date.now() - 1 * 60 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'a minute ago') + assert.equal(time.textContent, '1 minute ago') }) - test('rewrites a few seconds ago to just now', function() { + test('rewrites a few seconds ago to now', function() { const now = new Date().toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'just now') + assert.equal(time.textContent, 'now') }) - test('rewrites a few seconds from now to just now', function() { + test('rewrites a few seconds from now to now', function() { const now = new Date().toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'just now') + assert.equal(time.textContent, 'now') }) - test('displays future times as just now', function() { + test('displays future times as now', function() { const now = new Date(Date.now() + 3 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'just now') + assert.equal(time.textContent, 'now') }) - test('displays a day ago', function() { + test('displays yesterday', function() { const now = new Date(Date.now() - 60 * 60 * 24 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'a day ago') + assert.equal(time.textContent, 'yesterday') }) test('displays a day from now', function() { const now = new Date(Date.now() + 60 * 60 * 24 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'a day from now') + assert.equal(time.textContent, 'tomorrow') }) test('displays 2 days ago', function() { @@ -108,7 +108,7 @@ suite('relative-time', function() { const now = new Date(Date.now() + 2 * 60 * 60 * 24 * 1000).toISOString() const time = document.createElement('relative-time') time.setAttribute('datetime', now) - assert.equal(time.textContent, '2 days from now') + assert.equal(time.textContent, 'in 2 days') }) test('switches to dates after 30 past days', function() { @@ -144,10 +144,10 @@ suite('relative-time', function() { const now = new Date().toISOString() time.setAttribute('datetime', now) - assert.equal(time.textContent, 'just now') + assert.equal(time.textContent, 'now') time.removeAttribute('datetime') - assert.equal(time.textContent, 'just now') + assert.equal(time.textContent, 'now') }) test('sets relative contents when parsed element is upgraded', function() { @@ -157,6 +157,6 @@ suite('relative-time', function() { if ('CustomElements' in window) { window.CustomElements.upgradeSubtree(root) } - assert.equal(root.children[0].textContent, 'just now') + assert.equal(root.children[0].textContent, 'now') }) }) diff --git a/test/time-ago.js b/test/time-ago.js index abd7cd1..d1cc054 100644 --- a/test/time-ago.js +++ b/test/time-ago.js @@ -13,18 +13,18 @@ suite('time-ago', function() { assert.equal(time.textContent, '3 minutes ago') }) - test('rewrites a few seconds ago to just now', function() { + test('rewrites a few seconds ago to now', function() { const now = new Date().toISOString() const time = document.createElement('time-ago') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'just now') + assert.equal(time.textContent, 'now') }) - test('displays future times as just now', function() { + test('displays future times as now', function() { const now = new Date(Date.now() + 3 * 1000).toISOString() const time = document.createElement('time-ago') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'just now') + assert.equal(time.textContent, 'now') }) test('sets relative contents when parsed element is upgraded', function() { @@ -34,7 +34,7 @@ suite('time-ago', function() { if ('CustomElements' in window) { window.CustomElements.upgradeSubtree(root) } - assert.equal(root.children[0].textContent, 'just now') + assert.equal(root.children[0].textContent, 'now') }) test('micro formats years', function() { diff --git a/test/time-until.js b/test/time-until.js index 97c1b0e..423e8d8 100644 --- a/test/time-until.js +++ b/test/time-until.js @@ -3,28 +3,28 @@ suite('time-until', function() { const now = new Date(Date.now() + 10 * 365 * 24 * 60 * 60 * 1000).toISOString() const time = document.createElement('time-until') time.setAttribute('datetime', now) - assert.equal(time.textContent, '10 years from now') + assert.equal(time.textContent, 'in 10 years') }) test('rewrites from now future datetime to minutes ago', function() { const now = new Date(Date.now() + 3 * 60 * 1000).toISOString() const time = document.createElement('time-until') time.setAttribute('datetime', now) - assert.equal(time.textContent, '3 minutes from now') + assert.equal(time.textContent, 'in 3 minutes') }) - test('rewrites a few seconds from now to just now', function() { + test('rewrites a few seconds from now to now', function() { const now = new Date().toISOString() const time = document.createElement('time-until') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'just now') + assert.equal(time.textContent, 'now') }) - test('displays past times as just now', function() { + test('displays past times as now', function() { const now = new Date(Date.now() + 3 * 1000).toISOString() const time = document.createElement('time-until') time.setAttribute('datetime', now) - assert.equal(time.textContent, 'just now') + assert.equal(time.textContent, 'now') }) test('sets relative contents when parsed element is upgraded', function() { @@ -34,7 +34,7 @@ suite('time-until', function() { if ('CustomElements' in window) { window.CustomElements.upgradeSubtree(root) } - assert.equal(root.children[0].textContent, 'just now') + assert.equal(root.children[0].textContent, 'now') }) test('micro formats years', function() {