-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add initial date formatting functions (#1279)
* feat: add initial date formatting functions * add export * add japanese capitalization test * cleanup dateRelativeToNow * update function names to be more generic
- Loading branch information
1 parent
c4afed8
commit f8340e9
Showing
5 changed files
with
262 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
import { | ||
format, | ||
formatDuration, | ||
intervalToDuration, | ||
formatDistance, | ||
isToday, | ||
isYesterday, | ||
isThisWeek, | ||
isThisYear, | ||
} from 'date-fns'; | ||
import { capitalizeFirstLetter } from './utils'; | ||
|
||
// Base functions just wrap core date-fns functions, but this allows us to do checks and set default options. | ||
|
||
function _baseFormat (date, formatString) { | ||
_checkLocaleSet(); | ||
return format(date, formatString, { | ||
locale: global.locale, | ||
}); | ||
} | ||
|
||
function _baseFormatDuration (duration, formatString) { | ||
_checkLocaleSet(); | ||
return formatDuration(duration, { | ||
locale: global.locale, | ||
format: formatString, | ||
}); | ||
} | ||
|
||
function _baseFormatDistance (date, baseDate) { | ||
_checkLocaleSet(); | ||
return formatDistance(date, baseDate, { | ||
locale: global.locale, | ||
}); | ||
} | ||
|
||
function _isLocaleSet () { | ||
return global.locale !== undefined; | ||
} | ||
|
||
function _checkLocaleSet () { | ||
if (!_isLocaleSet()) { | ||
throw new Error('Locale not set, please call setDateLocale(locale) and pass ' + | ||
'in a datefns locale object as the locale param before calling this function'); | ||
} | ||
} | ||
|
||
/** | ||
* Sets the locale for date-fns. This should be called before any date-fns functions are called. | ||
* @param {Locale} locale A date-fns locale object | ||
*/ | ||
export function setDateLocale (locale) { | ||
global.locale = locale; | ||
} | ||
|
||
/** | ||
* This formats a date to the Dialtone standard medium date format as shown here: | ||
* https://dialpad.design/guides/writing-guidelines/#formats-by-length | ||
* @param {Date} date A javascript date object | ||
* @returns {string} A string in the format of 'September 2, 2022' | ||
*/ | ||
export function getDateMedium (date) { | ||
return _baseFormat(date, 'MMMM d, y'); | ||
} | ||
|
||
/** | ||
* Converts a call duration in total number of seconds to a human readable string | ||
* such as 'less than a minute' or '4 hours 34 minutes'. | ||
* @param {number} durationInSeconds The duration of the call in seconds | ||
* @returns {string} A human readable string representing the duration of the call | ||
*/ | ||
export function durationInHHMM (durationInSeconds) { | ||
if (durationInSeconds < 60) { | ||
// returns 'less than a minute', we're doing it like this instead of returning a string | ||
// so datefns handles i18n. | ||
return _baseFormatDistance(0, 29 * 1000); | ||
} | ||
const duration = intervalToDuration({ | ||
start: 0, | ||
end: durationInSeconds * 1000, | ||
}); | ||
return _baseFormatDuration(duration, ['hours', 'minutes']); | ||
} | ||
|
||
/** | ||
* gets the human readable name of the day relative to the current time. For example, if you pass in -1 it will | ||
* say "Yesterday" if you pass in 0 it will say "Today", if you pass in 1 it will say "Tomorrow". | ||
* @param {number} days The number of days relative to the current time | ||
* @returns {string} A human readable string representing the distance between the date and now | ||
*/ | ||
function _getRelativeDaysText (days) { | ||
const rtl = new Intl.RelativeTimeFormat(global.locale.code, { numeric: 'auto' }); | ||
return capitalizeFirstLetter(rtl.formatToParts(days, 'day')[0].value, global.locale.code); | ||
} | ||
|
||
/** | ||
* Returns the distance between the passed in date and now in a human readable format, typically used | ||
* when showing a history of items in a log such as a feed list. | ||
* | ||
* datefns does not support 'today' and 'yesterday' without showing time so we use Intl for these cases. | ||
* | ||
* examples below to explain | ||
* the different potential formats: | ||
* | ||
* If current day: | ||
* Today | ||
* | ||
* If previous day: | ||
* Yesterday | ||
* | ||
* Older than yesterday, but in the same calendar week: | ||
* Monday | ||
* | ||
* Older than the most recent calendar week, but in the same year: | ||
* Monday, October 14 | ||
* | ||
* older than a calendar year: | ||
* October 14, 2022 | ||
* | ||
* | ||
* @param {Date} date The timestamp of the item's date | ||
* @returns {string} A human readable string representing the distance between the date and now | ||
*/ | ||
export function relativeDate (date) { | ||
if (isToday(date)) { | ||
return _getRelativeDaysText(0); | ||
} else if (isYesterday(date)) { | ||
return _getRelativeDaysText(-1); | ||
} else if (isThisWeek(date)) { | ||
return _baseFormat(date, 'EEEE'); | ||
} else if (isThisYear(date)) { | ||
return _baseFormat(date, 'EEEE, MMMM d'); | ||
} else { | ||
return _baseFormat(date, 'MMMM d, y'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { beforeEach, vi } from 'vitest'; | ||
import { es, enUS } from 'date-fns/locale'; | ||
import { | ||
setDateLocale, | ||
getDateMedium, | ||
durationInHHMM, | ||
relativeDate, | ||
} from './dates'; | ||
|
||
const testInputDate = new Date(2022, 8, 2); | ||
|
||
describe('Date function tests', () => { | ||
beforeEach(() => { | ||
vi.useFakeTimers(); | ||
}); | ||
|
||
afterEach(() => { | ||
vi.useRealTimers(); | ||
}); | ||
|
||
describe('When locale is not set', () => { | ||
it('getDateMedium throws an error', () => { | ||
global.locale = undefined; | ||
expect(() => getDateMedium(testInputDate)).toThrow(); | ||
}); | ||
}); | ||
|
||
describe('When locale is set to enUS', () => { | ||
beforeEach(() => { | ||
setDateLocale(enUS); | ||
}); | ||
|
||
it('getDateMedium returns the expected date', () => { | ||
expect(getDateMedium(testInputDate)).toBe('September 2, 2022'); | ||
}); | ||
|
||
it.each([ | ||
[new Date(2023, 9, 24, 10, 0, 0), null, 'Today'], | ||
[new Date(2023, 9, 23, 10, 0, 0), null, 'Yesterday'], | ||
[new Date(2023, 9, 22, 10, 0, 0), null, 'Sunday'], | ||
[new Date(2023, 9, 14, 10, 0, 0), null, 'Saturday, October 14'], | ||
[new Date(2022, 9, 14, 10, 0, 0), null, 'October 14, 2022'], | ||
[new Date(2022, 11, 31, 23, 59, 59), new Date(2023, 0, 1, 0, 0, 0), 'Yesterday'], | ||
])('When %s is passed in to relativeDate, it returns %s', (inputDate, currentTime, expected) => { | ||
if (currentTime === null) { | ||
// Set current time to Oct 24 2023 10:30:00 AM by default. | ||
currentTime = new Date(2023, 9, 24, 10, 30, 0); | ||
} | ||
vi.setSystemTime(currentTime); | ||
|
||
expect(relativeDate(inputDate)).toBe(expected); | ||
}); | ||
|
||
it.each([ | ||
[59, 'less than a minute'], | ||
[29, 'less than a minute'], | ||
[0, 'less than a minute'], | ||
[60, '1 minute'], | ||
[55 * 60, '55 minutes'], | ||
[(4 * 60 * 60) + (34 * 60), '4 hours 34 minutes'], | ||
])('When %d is passed in to durationInHHMM, it returns %s', (inputSeconds, expected) => { | ||
expect(durationInHHMM(inputSeconds)).toBe(expected); | ||
}); | ||
}); | ||
|
||
/** | ||
* Most functions can just be tested using just the enUS locale, but we should test a few language based | ||
* formats here just to make sure that i18n is working as expected. | ||
*/ | ||
|
||
describe('When locale is set to es', () => { | ||
beforeEach(() => { | ||
setDateLocale(es); | ||
}); | ||
|
||
it('getDateMedium returns the expected date', () => { | ||
expect(getDateMedium(testInputDate)).toBe('septiembre 2, 2022'); | ||
}); | ||
|
||
it.each([ | ||
[new Date(2023, 9, 24, 10, 0, 0), null, 'Hoy'], | ||
[new Date(2023, 9, 14, 10, 0, 0), null, 'sábado, octubre 14'], | ||
])('When %s is passed in to relativeDate, it returns %s', (inputDate, currentTime, expected) => { | ||
if (currentTime === null) { | ||
// Set current time to Oct 24 2023 10:30:00 AM by default. | ||
currentTime = new Date(2023, 9, 24, 10, 30, 0); | ||
} | ||
vi.setSystemTime(currentTime); | ||
|
||
expect(relativeDate(inputDate)).toBe(expected); | ||
}); | ||
|
||
it.each([ | ||
[59, 'menos de un minuto'], | ||
[(4 * 60 * 60) + (34 * 60), '4 horas 34 minutos'], | ||
])('When %d is passed in to durationInHHMM, it returns %s', (inputSeconds, expected) => { | ||
expect(durationInHHMM(inputSeconds)).toBe(expected); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -88,3 +88,5 @@ export { | |
filterFormattedMessages, | ||
getValidationState, | ||
} from './common/utils'; | ||
|
||
export * from './common/dates'; |