skip to content

A Dart booking app counts nights as checkOut.difference(checkIn).inDays on local-midnight DateTimes and is off by one around a daylight-saving change; why, and how do you fix it?

level: seniorimportance: should knowfreq 38%

answer

  1. difference measures elapsed time
  2. a local day can last 23 hours
  3. inDays keeps whole days only
  4. add(Duration(days: 1)) means 24 hours
  5. calendar dates as DateTime.utc

basics

~20 s

difference() measures elapsed time, and a local day containing a clock change lasts 23 or 25 hours, so two local midnights can be 47 hours apart and inDays truncates to 1. Count calendar days on DateTime.utc dates instead.

solid answer

~40 s

`DateTime.difference` returns a `Duration` of real elapsed time between two instants; it knows nothing about calendar days. When clocks spring forward, the local day is 23 hours long, so from local midnight on 28 March to local midnight on 30 March in a zone that changes on the 29th is 47 hours, and `Duration.inDays` returns whole days only, giving 1. The same problem hits `add(const Duration(days: n))`, which adds exactly n times 24 hours and can land at 23:00 the previous evening or 01:00. The fix is to treat stay dates as calendar dates: build `DateTime.utc(year, month, day)` for each and take the difference of those, since UTC has no daylight saving, and move by days with the constructor, `DateTime(y, m, d + n)`, which normalises overflow instead of adding hours.

code

dart · 20 lines
dart
int nightsBuggy(DateTime checkIn, DateTime checkOut) =>
    checkOut.difference(checkIn).inDays;

int nights(DateTime checkIn, DateTime checkOut) {
  final from = DateTime.utc(checkIn.year, checkIn.month, checkIn.day);
  final to = DateTime.utc(checkOut.year, checkOut.month, checkOut.day);
  return to.difference(from).inDays;
}

DateTime addNights(DateTime day, int n) =>
    DateTime.utc(day.year, day.month, day.day + n); // calendar days

void main() {
  // Local midnights around 29 March 2026, when EU clocks go forward.
  final checkIn = DateTime(2026, 3, 28);
  final checkOut = DateTime(2026, 3, 30);
  print(nightsBuggy(checkIn, checkOut)); // 1 on a device in such a zone
  print(nights(checkIn, checkOut)); // 2 on every device
  print(addNights(DateTime.utc(2026, 2, 27), 2)); // 2026-03-01 00:00:00.000Z
}

go deeper

for a junior

Remember that difference() returns elapsed time as a Duration and inDays drops the leftover hours, so a 47-hour gap reads as 1 day.

for a middle

Explain 23- and 25-hour local days, why add(Duration(days: n)) is n times 24 hours, and how DateTime.utc dates avoid both.

for a senior

Diagnose the zone- and season-dependent report, fix it by modelling stay dates as calendar dates, and add tests pinned to both clock changes.

for a principal

Push the model boundary into the API: dates as dates, instants as UTC, and zones resolved in one place, so no screen does time arithmetic ad hoc.

## The bug in one sentence `checkOut.difference(checkIn).inDays` measures **elapsed hours** and truncates them to **whole days**, while a hotel stay is counted in **calendar dates**. The two disagree whenever a daylight-saving change falls inside the stay and the values are in local time. ## Why local days are not always 24 hours In a zone with daylight saving, the local clock jumps once or twice a year: - **Spring forward:** one local day has only 23 hours. - **Fall back:** one local day has 25 hours. A `DateTime` created with `DateTime(2026, 3, 28)` is local midnight, a real instant. The next local midnight is 23 hours later if the clocks change that night. The `DateTime` documentation states it directly: the difference between two midnights in local time may be less than 24 hours times the number of days between them if there is a daylight-saving change in between. ## What each API actually does | Call | What it computes | |---|---| | `a.difference(b)` | a `Duration` of elapsed time, negative if `b` is later | | `duration.inDays` | whole days, truncated toward zero | | `dt.add(const Duration(days: 1))` | exactly 24 hours later | | `DateTime(y, m, d + 1)` | the next calendar day at the same wall-clock time, overflow normalised | | `DateTime.utc(y, m, d)` | a date in a zone that has no daylight saving | So for a two-night stay from 28 to 30 March 2026, in a zone whose clocks go forward on the 29th, the local midnights are **47 hours** apart and `inDays` returns **1**. In autumn the span becomes 49 hours and `inDays` still returns the right count, which is why the bug appears only for spring stays and escapes testing. `add` has the mirror problem: the documentation notes that adding `Duration(days: 50)` adds 50 times 24 hours, so if the offset differs at the result, the time of day changes and the result may not even land on the calendar date 50 days later. Starting from local midnight, adding one "day" across a spring change gives 01:00 on the following day, and across an autumn change, 23:00 on the same day. ## The fix 1. **Model stay dates as dates, not instants.** Store the check-in and check-out as year, month and day, or as `DateTime.utc(y, m, d)` values. 2. **Count nights in UTC.** `DateTime.utc(checkOut.year, checkOut.month, checkOut.day).difference(DateTime.utc(checkIn.year, checkIn.month, checkIn.day)).inDays` is exact, because UTC days are always 24 hours. 3. **Step through dates with the constructor.** `DateTime(y, m, d + i)` or `DateTime.utc(y, m, d + i)` lets the constructor normalise day 32 into the next month, instead of adding hours. 4. **Convert to an instant only when you need one**, for example check-in at 15:00 in the property's zone, and do that with a real time-zone source. ## Why UTC dates are safe `DateTime.utc` values never shift for daylight saving, so midnight to midnight is always 24 hours, `difference(...).inDays` equals the number of calendar days, and `add(const Duration(days: n))` lands on the same wall-clock time. The dart.dev library tour gives the same advice: shifting a `DateTime` by days with a `Duration` is problematic because of clock shifts, so use UTC dates if you must shift days. ## Diagnosing the report Bug reports for this defect look random: "a customer in Berlin was charged one night less", "only for bookings made in March", "cannot reproduce on my machine". The pattern to spot is that the result depends on the **device's zone** and on **the dates chosen**, not on the code path. Reproduce by constructing the exact local midnights on a machine set to an affected zone, or by reasoning from the offsets: if `checkIn.timeZoneOffset` and `checkOut.timeZoneOffset` differ, a clock change sits between them. ## Testing it - Unit tests that use `DateTime(...)` run in the test machine's zone, which may be UTC on CI and hide the bug. - Test the night counter with `DateTime.utc` inputs and with explicit dates around both yearly clock changes. - Check a stay across a month end and a leap day, such as 28 February to 1 March, which the constructor's overflow handling gets right. ## What a senior answer adds Say that the bug depends on the device's zone, so it shows up only for some users and only in some weeks, which makes it look random in bug reports. Then separate the three concepts a booking system mixes: **calendar dates** for the stay, **instants** for events such as payment, and **zones** for turning one into the other.

  • Why does the bug not appear in autumn?
    In autumn the local day has 25 hours, so two local midnights two days apart are 49 hours apart and `inDays` truncates to 2, the right answer. Only the 23-hour spring day pushes the elapsed time below a whole number of days, which is why the defect slips past tests run at other times of year.
  • Does moving everything to DateTime.now().toUtc() fix it?
    Not by itself. Converting a local midnight with `toUtc()` keeps the same instant, so the 47-hour gap remains. The fix is to build new UTC values from the calendar fields, `DateTime.utc(d.year, d.month, d.day)`, so each date becomes a UTC midnight.
  • How do you iterate over each night of a stay?
    Loop an index from 0 to the night count and build `DateTime.utc(start.year, start.month, start.day + i)` for each. The constructor normalises overflow, so day 32 becomes the first of the next month, and UTC values never drift in wall-clock time.

saying these in an interview costs you the question

  • difference() counts calendar days between the two dates
  • add(Duration(days: 1)) always lands on the next calendar day at the same time
  • inDays rounds 47 hours up to 2 days
  • Calling toUtc() on the local midnights removes the problem
  • The bug is in Dart's time-zone database and needs an SDK upgrade