Javascript Date & Time Utilities
by Alexis Hope,
JavaScript’s built-in Date API is famously awkward. For most production work, Day.js is the right answer — it’s a 2kb drop-in that gives you immutable date objects, a chainable API, and a plugin ecosystem that covers almost every edge case. But if you’re keeping dependencies lean, or you just want to understand what’s happening under the hood, native Date gets you further than most people realise.
What follows is a comprehensive reference for the patterns you’ll reach for repeatedly.
Creating Dates
// Now
const now = new Date();
// From a string (ISO 8601 is safest across environments)
const d = new Date('2024-03-15T09:30:00');
// From timestamp (milliseconds since Unix epoch)
const fromMs = new Date(1710494400000);
// From parts: year, month (0-indexed!), day, hours, minutes, seconds
const explicit = new Date(2024, 2, 15, 9, 30, 0); // March 15 2024
// Current timestamp as a number
const ts = Date.now(); // faster than +new Date()
Month is 0-indexed (
0= January,11= December). This is the single most common source of off-by-one bugs with nativeDate.
Formatting Dates
JavaScript has no built-in date formatting — you compose it yourself, or use Intl.DateTimeFormat.
Manual formatting
function padTwo(n) {
return String(n).padStart(2, '0');
}
function formatDate(date) {
const y = date.getFullYear();
const m = padTwo(date.getMonth() + 1);
const d = padTwo(date.getDate());
return `${y}-${m}-${d}`; // "2024-03-15"
}
function formatTime(date) {
const h = padTwo(date.getHours());
const m = padTwo(date.getMinutes());
const s = padTwo(date.getSeconds());
return `${h}:${m}:${s}`; // "09:30:00"
}
function formatDateTime(date) {
return `${formatDate(date)} ${formatTime(date)}`;
}
Intl.DateTimeFormat (locale-aware)
const date = new Date('2024-03-15');
// Short locale string
date.toLocaleDateString('en-AU'); // "15/03/2024"
date.toLocaleDateString('en-US'); // "3/15/2024"
// Custom format via options
const fmt = new Intl.DateTimeFormat('en-GB', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
});
fmt.format(date); // "Friday, 15 March 2024"
// Time only
new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}).format(new Date()); // "09:30:00"
Human-Readable Duration
The original function this article was born from. Expanded here to handle the full range of durations.
function pluralize(count, word, suffix = 's') {
return count === 1 ? word : word + suffix;
}
// Format a Date object as hours and minutes
function timeNice(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
return `${hours} ${pluralize(hours, 'hour')} ${minutes} ${pluralize(minutes, 'minute')}`;
}
// Format a duration in milliseconds as a human string
function durationNice(ms) {
const totalSeconds = Math.floor(ms / 1000);
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = totalSeconds % 60;
const parts = [];
if (days) parts.push(`${days} ${pluralize(days, 'day')}`);
if (hours) parts.push(`${hours} ${pluralize(hours, 'hour')}`);
if (minutes) parts.push(`${minutes} ${pluralize(minutes, 'minute')}`);
if (seconds) parts.push(`${seconds} ${pluralize(seconds, 'second')}`);
return parts.length ? parts.join(', ') : '0 seconds';
}
durationNice(3661000); // "1 hour, 1 minute, 1 second"
durationNice(90000); // "1 minute, 30 seconds"
Relative Time (“time ago”)
function timeAgo(date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
const intervals = [
{ label: 'year', seconds: 31536000 },
{ label: 'month', seconds: 2592000 },
{ label: 'week', seconds: 604800 },
{ label: 'day', seconds: 86400 },
{ label: 'hour', seconds: 3600 },
{ label: 'minute', seconds: 60 },
];
for (const { label, seconds: s } of intervals) {
const count = Math.floor(seconds / s);
if (count >= 1) return `${count} ${pluralize(count, label)} ago`;
}
return 'just now';
}
timeAgo(new Date(Date.now() - 3700000)); // "1 hour ago"
timeAgo(new Date(Date.now() - 172800000)); // "2 days ago"
Intl.RelativeTimeFormatis now well-supported and handles locale automatically if you prefer not to roll your own.
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' });
rtf.format(-1, 'day'); // "yesterday"
rtf.format(-7, 'day'); // "7 days ago"
rtf.format(1, 'month'); // "next month"
Date Arithmetic
// Add days
function addDays(date, n) {
const d = new Date(date);
d.setDate(d.getDate() + n);
return d;
}
// Add months (handles month-end edge cases better than adding days)
function addMonths(date, n) {
const d = new Date(date);
d.setMonth(d.getMonth() + n);
return d;
}
// Difference in days between two dates
function diffDays(a, b) {
const ms = Math.abs(b.getTime() - a.getTime());
return Math.floor(ms / 86400000);
}
// Difference in calendar months
function diffMonths(a, b) {
return (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
}
Start and End of Period
Useful for building date range queries and chart axes.
function startOfDay(date) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
return d;
}
function endOfDay(date) {
const d = new Date(date);
d.setHours(23, 59, 59, 999);
return d;
}
function startOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth(), 1);
}
function endOfMonth(date) {
return new Date(date.getFullYear(), date.getMonth() + 1, 0, 23, 59, 59, 999);
}
function startOfYear(date) {
return new Date(date.getFullYear(), 0, 1);
}
Comparisons and Validation
function isBefore(a, b) {
return a.getTime() < b.getTime();
}
function isAfter(a, b) {
return a.getTime() > b.getTime();
}
function isSameDay(a, b) {
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
function isWithinRange(date, start, end) {
const t = date.getTime();
return t >= start.getTime() && t <= end.getTime();
}
function isValidDate(date) {
return date instanceof Date && !isNaN(date.getTime());
}
Timezone Utilities
Native Date always works in the local timezone of the runtime. For timezone-aware work in production you want a library, but these cover common ground.
// Convert a date to a specific IANA timezone string
function toTimezone(date, tz) {
return date.toLocaleString('en-GB', { timeZone: tz });
}
toTimezone(new Date(), 'America/New_York');
toTimezone(new Date(), 'Australia/Sydney');
// Get the UTC offset in hours for the local environment
function utcOffsetHours() {
return -new Date().getTimezoneOffset() / 60;
}
// ISO string without milliseconds
function toISOShort(date) {
return date.toISOString().slice(0, 19) + 'Z';
}
When to Reach for Day.js Instead
All of the above is manageable, but it adds up. Day.js covers everything here and more in 2kb:
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
dayjs().format('YYYY-MM-DD'); // "2024-03-15"
dayjs().from(dayjs('2020-01-01')); // "4 years ago"
dayjs().add(7, 'day').toDate();
dayjs().startOf('month').toISOString();
The API is nearly identical to the old Moment.js, the bundle is tiny, and the plugin system handles timezones, locales, and duration formatting without pulling in the whole world. If you’re building anything beyond a handful of these utilities, use it.