All files / lib date.ts

90.9% Statements 10/11
100% Branches 2/2
100% Functions 5/5
90% Lines 9/10

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118                              72x                                                                 78x 339x 339x 339x         64x                                                           3x             10x 10x                                              
// TODO(#1522): remove when Intl.DurationFormat is Baseline Widely Available (in Sep 2027)
import { DurationFormat } from '@formatjs/intl-durationformat';
import { intervalToDuration, isValid, parse } from 'date-fns';
 
/**
 * Returns a parsed date or undefined if a parse error occurs or the date is invalid
 * @param maybeDatestring a date string in the following formats:
 * - YYYY-MM-DD
 * - YYYY-MM-DDTHH:mm:ss
 * - YYYY-MM-DDTHH:mm:ssZ
 * - YYYY-MM-DDTHH:mm:ss±HH:mm
 *
 * We don't accept any other [valid, but insane ISO datestring](https://bsky.app/profile/gwen.works/post/3ljvdiur2lc2s)
 */
export function parseISOSafe(maybeDatestring: string) {
	return tryParse(
		maybeDatestring,
		'yyyy-MM-dd',
		"yyyy-MM-dd'T'HH:mm:ss",
		"yyyy-MM-dd'T'HH:mm:ss.SSS",
		"yyyy-MM-dd'T'HH:mm:ssXXX",
		"yyyy-MM-dd'T'HH:mm:ss.SSSXXX"
	);
}
 
if (import.meta.vitest) {
	const { test, expect, describe } = import.meta.vitest;
 
	describe('parseISOSafe', () => {
		test('works on sane ISO 8601 datestrings', () => {
			expect(parseISOSafe('2023-10-01')).toBeInstanceOf(Date);
			expect(parseISOSafe('2023-10-01T12:00:00')).toBeInstanceOf(Date);
			expect(parseISOSafe('2023-10-01T12:00:00Z')).toBeInstanceOf(Date);
			expect(parseISOSafe('2023-10-01T12:00:00+02:00')).toBeInstanceOf(Date);
			expect(parseISOSafe('2025-04-25T12:38:36.000Z')).toBeInstanceOf(Date);
		});
		test('does not parse "61"', () => {
			// Crazy right??
			expect(parseISOSafe('61')).toBeUndefined();
		});
	});
}
 
/**
 * Returns a parsed date or undefined if a parse error occurs or the date is invalid,
 * trying the given formats in order
 */
function tryParse(maybeDatestring: string, ...formats: string[]): Date | undefined {
	for (const format of formats) {
		try {
			const date = parse(maybeDatestring, format, new Date());
			if (isValid(date)) return date;
		} catch {
			continue;
		}
	}
	return undefined;
}
 
if (import.meta.vitest) {
	const { test, expect, describe } = import.meta.vitest;
 
	describe('tryParse', () => {
		test('works on valid datestrings', () => {
			expect(tryParse('2023-10-01', 'yyyy-MM-dd')).toBeInstanceOf(Date);
			expect(tryParse('2023-10-01T12:00:00', "yyyy-MM-dd'T'HH:mm:ss")).toBeInstanceOf(Date);
			expect(tryParse('2023-10-01T12:00:00Z', "yyyy-MM-dd'T'HH:mm:ssXXX")).toBeInstanceOf(
				Date
			);
		});
		test('returns undefined for Invalid Date datestrings', () => {
			expect(tryParse('2019-05-09T08:25:22+0000')).toBeUndefined();
		});
		test('returns undefined for malformed datestrings', () => {
			expect(tryParse('2023_10-01', 'yyyy-MM-dd')).toBeUndefined();
			expect(tryParse('chicken jockey')).toBeUndefined();
		});
	});
}
 
/**
 * Formats a date as a distance to now, but in a short format (e.g. "5m" instead of "5 minutes ago")
 * Uses Intl.DurationFormat#formatToParts under the hood
 * @returns array of non-whitespace-only parts. In practice, this is a alternating array of numbers and unit strings, in descending order of magnitude (e.g. ["1", "d", "5", "hr"] for "1 day and 5 hours ago"). Useful if you have not much space and wanna cut it to e.g. only "1d" instead of "1d 5hr".
 */
export function formatDistanceToNowShortParts(locale: string, date: Date | number): string[] {
	return new DurationFormat(locale, { style: 'narrow' })
		.formatToParts(
			intervalToDuration({
				start: Date.now(),
				end: date,
			})
		)
		.map((part) => part.value)
		.filter((value) => value.trim());
}
 
if (import.meta.vitest) {
	const { test, expect, describe, vi } = import.meta.vitest;
 
	describe('formatDistanceToNowShortParts', () => {
		test('formats distance to now in short parts', () => {
			const now = Date.now();
			vi.useFakeTimers().setSystemTime(now);
			expect(
				formatDistanceToNowShortParts('en-US', new Date(now - 1000 * 60 * 60 * 24))
			).toEqual(['-', '1', 'd']);
			expect(formatDistanceToNowShortParts('en-US', new Date(now + 1000 * 60 * 5))).toEqual([
				'5',
				'm',
			]);
			expect(
				formatDistanceToNowShortParts('en-US', new Date(now + 1000 * 60 * 60 * 26))
			).toEqual(['1', 'd', '2', 'h']);
		});
	});
}