Round a Time to the Nearest 15 Minutes in JavaScript

To round to the nearest quarter hour, call zdt.round({ smallestUnit: 'minute', roundingIncrement: 15 }). Part of Working with ZonedDateTime Objects.

Why this scenario is tricky

Rounding a time to a quarter hour sounds like a division problem, and that framing is exactly what makes hand-rolled versions go wrong. The tempting approach is to convert the time to a number of minutes since midnight, divide by 15, round, and multiply back — but that quietly assumes every day is a clean stack of equal minutes, which is false on daylight-saving days. Compute "minutes since midnight" by subtracting a midnight timestamp and a spring-forward day is 23 hours long, so your slot boundaries drift by an hour for the rest of the day. The arithmetic looks right and produces wrong slots exactly when it matters least visibly.

Temporal removes the division entirely. round understands calendar and clock structure, so asking it to round to a 15-minute increment snaps to real quarter-hour boundaries — :00, :15, :30, :45 — without you ever touching raw minute counts. On a ZonedDateTime it does this while respecting the zone's transitions, so a slot near a DST change lands on the correct wall-clock quarter hour rather than an offset-shifted one. The trickiness, in other words, is entirely in the naive approach; the correct tool makes it a one-liner.

The second subtlety is which way to round. "Nearest" is the default, but scheduling systems frequently want to round up — a booking that arrives at 2:53 should start at the next 3:00 slot, never 2:45, or you would be promising a time that has already partly passed. Being explicit about the rounding mode is what separates a correct booking system from one that occasionally hands out slots in the past.

Rounding time by hand — converting to minutes, dividing, multiplying back — loses sub-minute precision and mishandles the top-of-hour rollover (14:53 should round up to 15:00, crossing the hour). Temporal's round takes an increment and a rounding mode and carries the rollover for you.

Rounding must carry the hourManual minute math drops the rolloverRounding must carry the hourMath.round(min/15)*1514:53 → 15:00 mishandledround({increment:15})carries into the hourround() handles the carry into hours and keeps the zone.

Minimal working solution

The single call round({ smallestUnit: 'minute', roundingIncrement: 15 }) says exactly what you want: treat the minute as the smallest unit and snap to multiples of 15 of it. With the default rounding mode of halfExpand, values at the exact midpoint round away from zero — 2:53 goes to 3:00 because it is past the 2:52:30 midpoint, while 2:52 would go to 2:45. The seconds and sub-second components below the smallest unit are discarded as part of the rounding, so the result is a clean quarter-hour boundary with zeroed lower fields.

Because the operation returns a new immutable ZonedDateTime, the original time is untouched and the rounded value carries the same zone. That means you can round a timestamp for display or slotting without losing the zone information you need to store or re-project it later. If you only have a PlainTime or PlainDateTime rather than a zoned value, round works there too with the same options — the zone-awareness simply does not apply, which is fine when the value is already civil.

import { Temporal } from '@js-temporal/polyfill';
const t = Temporal.ZonedDateTime.from('2024-03-15T14:53:00[America/New_York]');
t.round({ smallestUnit: 'minute', roundingIncrement: 15 }).toString(); // 15:00

Round to 15 minincrement 15 on the minute unitRound to 15 min14:53roundincrement 1515:00

Full production version

Real scheduling wants control over direction, so the production helper takes the rounding mode explicitly. roundingMode: 'ceil' always rounds up to the next slot, which is the right choice for booking start times — you never want to offer a slot that has already begun. 'floor' always rounds down, useful for bucketing an event into the slot it falls within (analytics "which 15-minute window did this happen in"). 'halfExpand' is nearest-with-ties-up, the intuitive default for display. Naming the mode at the call site makes the intent obvious and prevents the subtle bug of using nearest-rounding where up-rounding was required.

The increment is worth parameterizing too, because the same helper serves 15-, 30-, and 60-minute slotting, and even 5-minute increments for finer-grained scheduling. As long as the increment divides evenly into an hour, the boundaries stay aligned to the top of the hour, which is what users expect. When you round a zoned value, the result respects the zone's offset, so a helper used across a DST transition still returns real wall-clock slots; there is no separate code path for transition days because round already accounts for them. That uniformity is the payoff of doing the arithmetic on a zone-aware type instead of on raw minutes.

import { Temporal } from '@js-temporal/polyfill';
// Always round up to the next slot for booking start times.
function ceilToSlot(t: Temporal.ZonedDateTime, minutes = 15) {
  return t.round({ smallestUnit: 'minute', roundingIncrement: minutes, roundingMode: 'ceil' });
}

Ceil to a slotroundingMode ceil for booking slotsCeil to a slottimeincrement nmode ceilnext slot

Verification snippet

Test the three rounding directions against the same input so the difference is unmistakable: 2:53 should round to 3:00 under ceil, to 2:45 under floor, and to 3:00 under halfExpand (since it is past the midpoint). Add a true midpoint like 2:52:30 and assert the tie-breaking behavior of your chosen mode, because ties are where rounding implementations most often disagree. Confirm that seconds below the increment are zeroed in the result, which proves the lower fields were actually cleared rather than carried through.

The assertion that catches the class of bug this page warns about is a DST case. Round a time on a spring-forward or fall-back day and assert the result is a genuine quarter-hour on the wall clock in that zone, not a value shifted by the transition's hour. Run the suite under several TZ settings for zoned inputs and confirm the wall-clock slots stay stable, which demonstrates the rounding is zone-aware rather than secretly operating on a fixed offset.

Round to 15 Minutes assertionsKey cases assert correctlyAssertions that prove the edge case14:53 nearest15:0014:07 nearest14:00ceil 14:0114:15secondsdropped

Common pitfalls

The biggest pitfall is the "minutes since midnight" reimplementation, which breaks on DST days and reinvents logic Temporal already provides correctly. The second is using nearest-rounding where directional rounding is required: a booking system that rounds 2:53 to the nearest slot will sometimes produce 2:45, a start time in the past, which is almost never acceptable — use ceil. The third is forgetting that the default mode is halfExpand, and being surprised when a value just under the midpoint rounds down; if you need a specific behavior, state the mode rather than relying on the default.

A subtler trap is choosing an increment that does not divide evenly into an hour, such as 7 minutes, which produces boundaries that wander relative to the top of the hour and confuse users expecting clean :00/:15/:30/:45 marks. Stick to divisors of 60 for slot scheduling. Finally, if you round a ZonedDateTime but then store only its offset rather than its zone, you lose the ability to re-project the slot correctly after a future rule change — keep the IANA zone with the rounded value.

Round to 15 Minutes pitfallsCommon mistakes and their fixesWrongRightMath.round(min/15)*15no hour carryround increment 15carries correctlyignore roundingModesurprising directionset roundingModeexplicit direction

Frequently Asked Questions

How do I round a time to the nearest 15 minutes?

Call zdt.round({ smallestUnit: 'minute', roundingIncrement: 15 }) on a Temporal.ZonedDateTime (or PlainTime). It rounds to the nearest quarter hour and carries into the hour when needed, so 14:53 becomes 15:00.

How do I always round up to the next slot?

Add roundingMode: 'ceil' to the round options. That makes any time that is not exactly on a boundary move up to the next 15-minute slot, which is what booking and scheduling systems usually want for start times.

How do I round a time to the nearest 15 minutes in JavaScript?

Call round({ smallestUnit: 'minute', roundingIncrement: 15 }) on a Temporal.ZonedDateTime, PlainDateTime, or PlainTime. It snaps to real quarter-hour boundaries (:00, :15, :30, :45) and zeroes the seconds and sub-second fields, returning a new immutable value. On a zoned value it respects the zone's daylight-saving transitions, so the result is a correct wall-clock quarter hour rather than an offset-shifted one.

How do I always round a booking time up to the next slot?

Pass roundingMode: 'ceil' along with the increment, e.g. round({ smallestUnit: 'minute', roundingIncrement: 15, roundingMode: 'ceil' }). Ceil always moves forward to the next quarter hour, so 2:53 becomes 3:00 and a time exactly on a boundary stays put. This prevents the common bug where nearest-rounding hands out a slot start time that has already partly passed.

Why not just divide minutes-since-midnight by 15?

Because computing minutes since midnight by subtracting a midnight timestamp assumes every day has a fixed number of minutes, which is false on daylight-saving days — a spring-forward day is 23 hours long, so your slot boundaries drift by an hour afterward. Temporal.round understands the zone's structure and snaps to real quarter-hour boundaries without any raw minute arithmetic, so it stays correct across transitions.