Date Time
Utilities for working with Date and time.
Example Usage
import {
formatMillisecondsToHumanReadable,
createTimeRanges,
} from './date-time';
// formatMillisecondsToHumanReadable
formatMillisecondsToHumanReadable(2000);
// ⏬
'2s'
// createTimeRanges
const data = [
{
price: 90,
date: new Date(2022, 2, 10), // 10 march
},
{
price: 100,
date: new Date(2022, 3, 20), // 20 april
}
];
const ranges = createTimeRanges({
firstDay: new Date(2022, 0, 1),
rangeSizeInMonths: 1,
formatName: ({ from, format }) => format(from, "MMM"),
});
const groupedByMonths = ranges.map(range => {
const itemsOfThisRange = data.filter(item => range.matchRange(item.date));
return {
rangeName: range.name,
items: itemsOfThisRange,
};
});
// ⏬
[
{ rangeName: 'Jan', items: [] },
{ rangeName: 'Feb', items: [] },
{ rangeName: 'Mar', items: [{ price: 90, date: new Date(2022, 2, 10) }] },
{ rangeName: 'Apr', items: [{ price: 100, date: new Date(2022, 3, 20) }] },
// ...
{ rangeName: 'Dec', items: [] },
]
Dependencies
registryhttps://shadcn-registry-ts.vercel.app/r/util-array.json
npmpretty-ms
npmdate-fns
Auto Install
npx shadcn@latest add https://shadcn-registry-ts.vercel.app/r/util-date-time.json
Manual Install
date-time.ts
/**
* Source: http://localhost:3000
*/
import prettyMs from 'pretty-ms';
import { addDays, addMonths, formatDate } from "date-fns";
import { createArrayWithLength } from "./array";
/**
* Convert milliseconds to human readable format (string)
*/
export const formatMillisecondsToHumanReadable = (milliseconds: number) => {
return prettyMs(milliseconds, { compact: false });
};
/**
* Create an array of time ranges of months, given a start date and the range size (in months).
* Useful for creating chart data.
* @example
* const ranges = createTimeRanges({
* firstDay: new Date(2024, 0, 1),
* rangeSizeInMonths: 1,
* formatName: (options) => {
* return `${options.format(options.from, "MMM")} ${options.format(options.to, "MMM")}`
* }
* });
* // ⏬
* [
* {
* name: "Jan",
* matchRange: (testDate: Date) => boolean,
* },
* {
* name: "Feb",
* matchRange: (testDate: Date) => boolean,
* },
* // ...
* {
* name: "Dec",
* matchRange: (testDate: Date) => boolean,
* },
* ]
*/
export const createTimeRanges = ({
firstDay,
rangeSizeInMonths,
formatName,
}: {
/** First day of first range */
firstDay: Date,
/** How many month each range span? */
rangeSizeInMonths: number,
/** Format name of the range. */
formatName: (options: {
from: Date,
to: Date,
format: typeof formatDate,
rangeSizeInMonths: number,
}) => string;
}) => {
// utils
/**
* Return a new Date object with time part equal to 00:00:00.000 (h:m:s.ms).
* Used to compare two date only by Day, Month, Year, excluding time from comparision.
*/
const setTimeToMidnight = (d: Date) => new Date(new Date(d).setHours(0, 0, 0, 0));
/** Normalize date to a common type , used for comparision purpose */
const normalizeDate = (date: Date) => setTimeToMidnight(date);
// logic
const rangeCount = Math.ceil(12 / rangeSizeInMonths);
type Range = {
name: string,
matchRange: (testDate: Date) => boolean,
};
const ranges: Range[] = createArrayWithLength(rangeCount).map((_, i) => {
const from = addMonths(firstDay, i * rangeSizeInMonths);
const to = addMonths(addDays(from, -1), rangeSizeInMonths);
const matchRange = (testDate: Date) => {
return (
normalizeDate(testDate).valueOf() >= normalizeDate(from).valueOf()
&&
normalizeDate(testDate).valueOf() <= normalizeDate(to).valueOf()
);
};
const name = formatName({
from,
to,
format: formatDate,
rangeSizeInMonths,
});
return {
name,
matchRange,
};
});
return ranges;
};
Test
date-time.test.ts
import { describe, it, expect } from 'vitest';
import { formatMillisecondsToHumanReadable, createTimeRanges } from './date-time';
describe('date-time - formatMillisecondsToHumanReadable', () => {
it('do it', () => {
// under 1s it should be in ms
expect(formatMillisecondsToHumanReadable(0)).toBe('0ms');
expect(formatMillisecondsToHumanReadable(100)).toBe('100ms');
expect(formatMillisecondsToHumanReadable(200)).toBe('200ms');
expect(formatMillisecondsToHumanReadable(300)).toBe('300ms');
expect(formatMillisecondsToHumanReadable(400)).toBe('400ms');
expect(formatMillisecondsToHumanReadable(500)).toBe('500ms');
expect(formatMillisecondsToHumanReadable(600)).toBe('600ms');
expect(formatMillisecondsToHumanReadable(700)).toBe('700ms');
expect(formatMillisecondsToHumanReadable(800)).toBe('800ms');
expect(formatMillisecondsToHumanReadable(900)).toBe('900ms');
// from 1 to 59s should be in seconds
expect(formatMillisecondsToHumanReadable(1_000)).toBe('1s');
expect(formatMillisecondsToHumanReadable(1_100)).toBe('1.1s');
expect(formatMillisecondsToHumanReadable(1_500)).toBe('1.5s');
expect(formatMillisecondsToHumanReadable(2_000)).toBe('2s');
expect(formatMillisecondsToHumanReadable(10_000)).toBe('10s');
expect(formatMillisecondsToHumanReadable(20_000)).toBe('20s');
expect(formatMillisecondsToHumanReadable(30_000)).toBe('30s');
expect(formatMillisecondsToHumanReadable(40_000)).toBe('40s');
expect(formatMillisecondsToHumanReadable(50_000)).toBe('50s');
expect(formatMillisecondsToHumanReadable(59_000)).toBe('59s');
// from 1m to 1h should be in minutes (and seconds if not zero)
expect(formatMillisecondsToHumanReadable(60_000)).toBe('1m');
expect(formatMillisecondsToHumanReadable(65_000)).toBe('1m 5s');
expect(formatMillisecondsToHumanReadable(90_000)).toBe('1m 30s');
expect(formatMillisecondsToHumanReadable(120_000)).toBe('2m');
expect(formatMillisecondsToHumanReadable(600_000)).toBe('10m');
expect(formatMillisecondsToHumanReadable(1_800_000)).toBe('30m');
expect(formatMillisecondsToHumanReadable(3_600_000 - 1_000)).toBe('59m 59s');
// from 1h to 24h should be in hours
expect(formatMillisecondsToHumanReadable(3_600_000)).toBe('1h');
});
it("strange input", () => {
expect(formatMillisecondsToHumanReadable(-50)).toBe('-50ms');
});
});
describe('date-time - createTimeRanges', () => {
it('do it', () => {
const ranges = createTimeRanges({
firstDay: new Date(2022, 0, 1),
rangeSizeInMonths: 1,
formatName: ({ from, to, rangeSizeInMonths, format }) => rangeSizeInMonths === 1
? format(from, "MMM")
: `${format(from, "MMM")} ${format(to, "MMM")}`
});
expect(ranges.length).toBe(12);
expect(ranges).toMatchObject([
{ name: 'Jan' },
{ name: 'Feb' },
{ name: 'Mar' },
{ name: 'Apr' },
{ name: 'May' },
{ name: 'Jun' },
{ name: 'Jul' },
{ name: 'Aug' },
{ name: 'Sep' },
{ name: 'Oct' },
{ name: 'Nov' },
{ name: 'Dec' },
]);
expect(ranges[0].matchRange(new Date(2022, 0, 1))).toBe(true);
expect(ranges[0].matchRange(new Date(2022, 1, 1))).toBe(false);
});
it('do it', () => {
const ranges = createTimeRanges({
firstDay: new Date(2022, 0, 1),
rangeSizeInMonths: 4,
formatName: ({ from, to, rangeSizeInMonths, format }) => rangeSizeInMonths === 1
? format(from, "MMM")
: `${format(from, "MMM")} ${format(to, "MMM")}`
});
expect(ranges.length).toBe(3);
expect(ranges).toMatchObject([
{ name: 'Jan Apr' },
{ name: 'May Aug' },
{ name: 'Sep Dec' },
]);
expect(ranges[0].matchRange(new Date(2022, 0, 1))).toBe(true);
expect(ranges[0].matchRange(new Date(2022, 10, 1))).toBe(false);
});
it('do it', () => {
const data = [
{
price: 90,
date: new Date(2022, 2, 10), // 10 march
},
{
price: 100,
date: new Date(2022, 3, 20), // 20 april
}
];
const ranges = createTimeRanges({
firstDay: new Date(2022, 0, 1),
rangeSizeInMonths: 1,
formatName: ({ from, format }) => format(from, "MMM"),
});
const groupedByMonths = ranges.map(range => {
const itemsOfThisRange = data.filter(item => range.matchRange(item.date));
return {
rangeName: range.name,
items: itemsOfThisRange,
};
});
groupedByMonths.forEach(group => {
if (group.rangeName === 'Mar') {
expect(group.items.length).toBe(1);
expect(group.items[0].price).toBe(90);
return;
}
if (group.rangeName === 'Apr') {
expect(group.items.length).toBe(1);
expect(group.items[0].price).toBe(100);
return;
}
});
});
});
Command Palette
Search for a command to run...