little-date: opinionated date range formatting that just works

August 20, 2024

|repo-review

by Florian Narr

little-date: opinionated date range formatting that just works

little-date is a small TypeScript library that formats date ranges into compact, readable strings. Pass two dates, get back "Jan 1 - 12" instead of "1/1/2023, 00:00:00 AM - 1/12/2023, 23:59:59 PM".

Why I starred it

Date range display is one of those problems that looks trivial until you're actually building a dashboard. You've got analytics filters, calendar UIs, booking systems — and every team reinvents the same wheel with the same bugs: redundant year suffixes, awkward AM/PM formatting, inconsistent handling of same-day vs multi-day ranges.

What caught my eye here is that it's an opinionated library from Vercel, built during a hackathon by Timo Lins. Small scope, clear contract, well-tested. The kind of utility that should be a package but usually ends up as a utils/dates.ts file that slowly rots.

How it works

The whole library is ~130 lines in src/format-date-range.ts. It leans on date-fns for the heavy lifting (comparisons, formatting) and implements a decision tree that walks down through increasingly specific cases.

The structure is a flat cascade of if blocks, each handling one scenario and returning early:

// Check if the range is the entire year
if (
  isSameMinute(startOfYear(from), from) &&
  isSameMinute(endOfYear(to), to)
) {
  return `${format(from, "yyyy")}`;
}

// Check if the range is an entire quarter
if (
  isSameMinute(startOfQuarter(from), from) &&
  isSameMinute(endOfQuarter(to), to) &&
  getQuarter(from) === getQuarter(to)
) {
  return `Q${getQuarter(from)} ${format(from, "yyyy")}`;
}

The ordering matters here. Special semantic ranges (full year, quarter, full month) are checked first, before falling through to generic day/time ranges. If your range is exactly Jan 1 00:00 to Dec 31 23:59, you get "2023", not "Jan 1 - Dec 31". That's the opinionated part — the library recognizes that a full year range means a year.

The time-suffix logic is worth reading. Rather than always appending time, it checks whether the start/end timestamps differ from startOfDay and endOfDay by more than a minute:

const startTimeSuffix =
  includeTime && !isSameMinute(startOfDay(from), from)
    ? `, ${formatTime(from)}`
    : "";

This is how "Jan 1 - 12" works cleanly even when your timestamps are 00:00:00.000 and 23:59:59.999 — the library recognizes these as boundary times and omits them.

The createFormatTime helper runs toLocaleTimeString and then strips leading zeros and normalizes AM/PM. That's where locale-aware 24-hour formatting falls out naturally — pass "en-GB" and you get "0:11 - 14:00" without any special-casing.

One thing I liked: the tests are pinned to a fixed today date (2023-11-15T12:00:00.000Z) and the test runner forces TZ=UTC. That's the right call for anything touching dates — non-deterministic timezone behavior in test suites is a common source of flaky CI. The coverage is solid: 14 cases covering same-month, cross-month, cross-year, today, full year, quarter, full months, custom separator, and timezone.

Using it

import { formatDateRange } from 'little-date';

// Same month
formatDateRange(
  new Date('2024-01-01T00:00:00.000Z'),
  new Date('2024-01-12T23:59:59.999Z')
);
// "Jan 1 - 12"

// Cross-year
formatDateRange(
  new Date('2022-01-01T00:00:00.000Z'),
  new Date('2023-01-20T23:59:59.999Z')
);
// "Jan 1 '22 - Jan 20 '23"

// Full quarter
formatDateRange(
  new Date('2024-01-01T00:00:00.000Z'),
  new Date('2024-03-31T23:59:59.999Z')
);
// "Q1 2024"

// With time
formatDateRange(
  new Date('2024-01-01T12:11:00.000Z'),
  new Date('2024-01-01T14:30:00.000Z'),
  { locale: 'en-US' }
);
// "Jan 1, 12:11pm - 2:30pm"

Options are minimal: locale, includeTime, separator, timezone, and today (for testing). That last one is a nice touch — it makes the "is this today?" check testable without mocking Date.now.

Rough edges

The docs mention customization upfront but the answer is essentially "fork it" — the README recommends copying src/format-date-range.ts into your own codebase if you need anything beyond the provided options. That's an honest position given the single-file architecture, but it does mean you're on your own for keeping up with any upstream fixes.

The library depends on date-fns v2, which is on the heavier side for what's happening here. You're pulling in all the comparison and formatting utilities from date-fns when the actual logic is just a handful of isSameMinute calls. date-fns is tree-shakeable, so in practice this isn't a real problem for bundled output, but it's worth noting the single runtime dependency.

Commit activity has been quiet since version 1.2.1 (which added timezone support). The core feature set is small, so there may not be much left to add — but if you hit an edge case, don't expect a quick response.

Bottom line

Drop-in solution if you're building any dashboard, analytics filter, or calendar UI and you're tired of writing the same date range formatting logic. The source is short enough to read in 10 minutes and the decision tree is clean enough to copy if you need to customize it.

vercel/little-date on GitHub
vercel/little-date