Answers · Where the data lives
Why is the date off by one day after I save it and read it back?
In shortYou saved a calendar date as if it were a precise moment, so showing it in another time zone can move it to the day before or after.
You saved a calendar date as if it were a precise moment, so reading it back in another zone moves it to the day before or after. A date such as 14 March is a reading on a calendar, not an instant that happens everywhere at once. An instant is one moment for everybody: 2024-03-14T00:00:00Z is the same moment in London, New York and Tokyo. A local reading is what a particular clock says at that moment, and it can be 13 March in New York while it is already 14 March in London. If the stored value has no zone, or if a date-only value is turned into midnight in some zone, the conversion to a local reading is where the day moves.
The arithmetic that turns an instant into a day is a division. With as the count of seconds from the origin and as the seconds already elapsed within the current day, the whole days since the origin are . The 86400 is 60 times 60 times 24, which is the seconds in one day. That division gives a day in the origin's zone, not in the viewer's. To get the viewer's day you must add their offset first, and the offset is not a constant: it changes with daylight saving and with rules a government has changed. A stored number of hours freezes one answer; a stored zone name lets the answer be looked up for the instant in question.
The midnight you did not store
Take a birthday, 14 March 2024, which is a local date with no time. If the code stores it as the instant 2024-03-14T00:00:00Z, that instant is midnight at the origin. A viewer at UTC-5 subtracts five hours from 00:00 and gets 19:00 on 13 March, so the birthday shows a day early. A viewer at UTC+10 adds ten hours and gets 10:00 on 14 March, so the same stored instant shows the right day. The arithmetic is not a bug in the file: 00:00 minus five hours crosses the previous midnight, which is exactly what an offset does near the start of a day. The same thing happens in reverse if a local midnight is written without an offset: a reader that assumes UTC and a reader that assumes UTC-5 are looking at two different instants, and one of them can land on the other date.
| stored instant | viewer's offset | local date shown |
|---|---|---|
| 2024-03-14T00:00:00Z | UTC-5 | 2024-03-13 19:00 |
| 2024-03-14T00:00:00Z | UTC | 2024-03-14 00:00 |
| 2024-03-14T00:00:00Z | UTC+10 | 2024-03-14 10:00 |
The natural guess is that the save corrupted the value, or that the database is one day behind. That guess fails because there is no single date stored in the first place. A count of seconds, or an instant ending in Z, names one moment, and every viewer is entitled to a different local date for it. If the value is a date-only string with no time and no zone, the file has stored all it can store, and the date is still ambiguous the moment somebody converts it. The repair is not to add a day in the display code. That hides the problem for one offset and makes it wrong for another. The repair is to decide whether the value means an instant or a calendar date, and then to store that thing.
Where the shift disappears
A full instant with an offset is stable: 2024-03-14T00:00:00-05:00 is 05:00 UTC, and a correct reader that keeps the offset will render it as 14 March in UTC-5 and as 15:00 on 14 March in UTC+10, with no date change for this example. A plain calendar date is also stable if it is never given a time: store 2024-03-14 as three fields, or as a string that is explicitly a date, and do not parse it into an instant at midnight. The shift returns when a zone is looked up once and stored as a fixed number of hours, because offsets change with daylight saving and with government rules. Store the zone name, such as Europe/Paris, and resolve the offset at the moment of display. For a day's sales report, compute the day once in the business's zone and store that day, rather than recomputing it on each viewer's machine.
Two rules cover almost every case. If the value is a moment when something happened, store the instant, send it with its offset, and convert only for display. If the value is a day on a calendar, such as a birthday or a closing date, store the day and never convert it.
Worth keeping
- An instant is one moment everywhere; a calendar date is a reading on a particular clock.
- Storing a date of birth at midnight in a zone makes it a different date for viewers far enough east or west.
- A full instant with its offset, or a plain date with no time, is stable; a date without a zone is not.
Where this goes next
This question is one step of Bytes, Text and File Formats, a course that works through its subject from the beginning. The page that teaches this part of it is Dates, Times, and the Day It Moved, and it reads the whole way through whether or not you have an account.
What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.
Read that pageThe whole courseOther questions people ask about this
- Is UTF-8 the same thing as Unicode, or what's the difference?No: Unicode assigns a number to each character, while UTF-8 is a way of writing those numbers as bytes, using one to four bytes per character.
- Why does my CSV file fall apart when one of the fields has a comma in it?Because the same comma both separates fields and sits inside the value, so a reader splitting on commas finds one field too many and every later column shifts.
- Why do programmers write numbers in hexadecimal instead of just normal numbers?Programmers use hexadecimal because each digit stands for exactly four bits, so one byte is always written as two digits and the columns line up.
- Why doesn't 0.1 plus 0.2 come out as 0.3?A tenth cannot be stored exactly in binary, so the two stored values are already slightly wrong before they are added.
- How do I make a step safe to run again when I'm not sure whether it already ran?Change the operation so that sending it twice leaves the same stored data as sending it once, instead of relying on remembering not to send it again.
- Why does my count come out doubled after the job restarts?Your count doubled because the restart replayed events the saved counter already held: the state and input position were not saved as one snapshot.