Topic 4.3
Dates and Time Zones
In one line
A timestamp is one exact instant, but the calendar day it falls on depends on whose clock you read. Parse dates with a stated format, format them on a stated clock, and never let a chart silently shift a ride to the wrong day.
Think of it like this
A phone call at 00:30 in London is still the evening before in New York. It is one call, but two people write two different dates in their diaries. A ride in the data is the call, the time zone is whose diary you write it in.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- instant
- One exact moment in time, the same for everyone on Earth.
- UTC
- Coordinated Universal Time: the reference clock with offset zero. Written with a Z at the end of a timestamp.
- time zone
- A region's rule for turning an instant into a wall-clock time, including its summer-time changes. Named like
Atlantic/Azores. - offset
- How far a clock is from UTC, such as −1 hour.
- ISO 8601
- The standard date text:
2025-06-01or2025-06-01T00:00:09Z. Biggest unit first. - summer time (DST)
- The yearly shift of clocks by one hour. It makes one local day 23 hours long and another 25.
Step by step
01Ask which day a trip belongs to
The first example loads trips-10k.csv (10,000 trips, all in June 2025, times in UTC) and asks five clocks which day the same trips fall on. For UTC and for Ardenvale the answer is identical: 0 trips move to another day, because Ardenvale is on UTC+0 in summer.
For Los Angeles, the first trip (00:00:09 UTC on 1 June) is on 31 May, and 2,856 of the 10,000 trips are on a different date than their UTC date. For Auckland, 5,057 are. The rides did not change, only whose diary they are written in.
02Find the days that are not 24 hours
The second example walks through every real hour of 2025, writes down which Ardenvale date each hour belongs to, and counts. Two dates stand out: 30 March has 23 hours, and 26 October has 25.
The first and last dates are cut by our UTC range, so the code leaves them out. The same code with America/New_York finds 9 March and 2 November instead, and with Asia/Kolkata finds nothing, because that zone has no summer time.
03Pick the clock once
Name the dashboard's clock in one place (TIME_ZONE = "Atlantic/Azores" in config.js) and write on the chart which day it uses.
04Parse and format with a stated clock
The third example parses a European date (13/03/2025) with d3.utcParse, shows what the browser's own guess gives (Invalid Date), then prints one instant on three clocks, and sorts dates three ways.
Try it yourself
- 1
Try another clock
In the first example replace
Pacific/AucklandwithAsia/Kolkata. The last row becomesAsia/Kolkata first trip on 2025-06-01 | trips on 1 June: 267 | trips on another day than UTC: 2266: that zone is 5 hours 30 minutes ahead, and the offset need not be a whole hour. - 2
Another city's short day
In the second example change the zone to
America/New_York(and the example's2025-03-30to2025-03-09). It finds 9 March with 23 hours and 2 November with 25: every zone with summer time has its own two days. - 3
Break the date text
In the third example, parse
"2025-02-30"withutcParse("%Y-%m-%d"). It gives2025-03-02: the parser rolls the day over like the 32 March case. A month of 13 (2025-13-01) rolls over too, to2026-01-01, and a day of 00 gives the last day of the month before. Only text that does not fit the pattern, such as2025-03-12followed by a space, givesnull.
Code & diagrams
A chart that groups by the wrong clock moves thousands of rides to the wrong bar and prints no error. Europe/London shows 386 moved trips because London is one hour ahead of UTC in summer.
Preview
Console
Nothing logged yet.Expected console output
First trip: T07790 starts at 2025-06-01T00:00:09Z
UTC first trip on 2025-06-01 | trips on 1 June: 330 | trips on another day than UTC: 0
Atlantic/Azores first trip on 2025-06-01 | trips on 1 June: 330 | trips on another day than UTC: 0
Europe/London first trip on 2025-06-01 | trips on 1 June: 317 | trips on another day than UTC: 386
America/Los_Angeles first trip on 2025-05-31 | trips on 1 June: 313 | trips on another day than UTC: 2856
Pacific/Auckland first trip on 2025-06-01 | trips on 1 June: 179 | trips on another day than UTC: 5057This reads the browser's own time-zone rules, so it needs no data file. The edge case: dividing by 24 is wrong on exactly these two days.
Preview
Console
Nothing logged yet.Expected console output
Full local days counted: 364
Days that are not 24 hours long: [ [ '2025-03-30', 23 ], [ '2025-10-26', 25 ] ]
2,118 rides on 2025-03-30 is 92.1 per hour, not 88.3Two silent failures: the impossible date 2025-03-32 becomes 1 April, and the default sort puts 30 March first.
Console
(nothing was printed)Expected console output
utcParse('13/03/2025'): 2025-03-13T00:00:00.000Z
utcParse('2025-03-32'): 2025-04-01T00:00:00.000Z
utcParse('2025-03-12 '): null
new Date('13/03/2025'): Invalid Date
utcFormat: Wednesday 18 June 2025, 08:15
Ardenvale: Wednesday, 18 June 2025 at 08:15
Auckland: Wednesday, 18 June 2025 at 20:15
default sort() : 2025-03-30 2025-03-04 2025-03-12
d3.ascending : 2025-03-04 2025-03-12 2025-03-30
ISO text sorts too: 2025-03-04 2025-03-12 2025-03-30
but this text does not: 04/03/2025 13/03/2025 30/01/2025Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Read a European date with the browser's guess
In the third example add new Date("13/03/2025").toISOString();.
Myth vs fact
Myth
A Date has a time zone.
Fact
A Date is one instant, in milliseconds. The zone only appears when you print it or ask for its day. getDate() uses the viewer's clock, getUTCDate() the UTC clock.
Myth
Every day has 24 hours.
Fact
In zones with summer time, two days a year have 23 and 25 hours. Ardenvale's were 30 March and 26 October 2025.
Myth
UTC is a time zone I can ignore on a dashboard.
Fact
If rides are stored in UTC and the team reads local days, grouping on the UTC date moves rides to other days. In Ardenvale's summer the two clocks agree, which hides the bug until winter.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
new Date("2025-03-12")is UTC midnight butnew Date("2025-03-12T00:00")is local midnight, a quirk of the standard.d3.autoTypeadds a workaround so both read alike in browsers, but the safe rule is to parse with an explicit format. - ▸
Intl.DateTimeFormatobjects are expensive to create, cheap to use. Create one per zone and reuse it, asdayIn(zone)does. Formatting 10,000 trips with one formatter is fast; creating a formatter per row is not.
Remember this
- 1
The problem this solves: Lumen's rides are stored with a UTC time, but the operations team thinks in Ardenvale's local days. If a chart groups rides by the UTC date, a ride at 23:30 local time can land on tomorrow's bar and every daily total is slightly wrong, with no error anywhere.
- 2
An instant and a clock reading are different things.
2025-06-01T00:00:09Znames one instant. The trailingZmeans UTC ("zero offset"). A JavaScriptDatestores only that instant, as milliseconds since 1 January 1970 UTC. It has no zone of its own: the zone appears only when you turn it into text or ask for its day, hours and so on. - 3
Ardenvale's clock. Ardenvale uses the zone
Atlantic/Azores: UTC−1 in winter and UTC+0 in summer, because clocks go forward on the last Sunday of March and back on the last Sunday of October. In 2025 that was 30 March and 26 October (the third example below finds both days by counting real hours). All June 2025 trips fall in summer time, where local time equals UTC, so for June the UTC day and the Ardenvale day match for all 10,000 trips. For a winter file they would not. - 4
Parsing.
new Date("2025-03-12")reads a date-only text as UTC midnight, butnew Date("2025-03-12T00:00")(a date and time with no zone) as the viewer's local time.new Date("13/03/2025")isInvalid Date. Do not let the browser guess:d3.utcParse("%d/%m/%Y")states the format and the clock, and returnsnull(not anInvalid Date) for text that does not fit.d3.timeParseis the same but reads the text as the viewer's local time. Careful: the parser accepts any two-digit day or month and rolls it over, as the third example shows for2025-03-32(it becomes 1 April). It returnsnullonly when the text does not fit the pattern. Check the result, or compare the parsed pieces with the text (the cleaner in Topic 4.4 does). - 5
Formatting.
Intl.DateTimeFormatis built into the browser and takes atimeZone:new Intl.DateTimeFormat("en-CA", { timeZone: "Atlantic/Azores", year: "numeric", month: "2-digit", day: "2-digit" })writes the local date as2025-06-01. (Theen-CAlocale is used because it writes year-month-day.)d3.utcFormat("%Y-%m-%d")writes the UTC clock, andd3.timeFormatwrites the viewer's clock. Which clock you pick is a decision, not a detail. - 6
Days are not always 24 hours. On the day clocks go forward, the local day has 23 hours, and on the day they go back it has 25. A chart of hourly rides for 30 March 2025 has no 01:00 on Ardenvale's clock. A "rides per hour" figure computed as total ÷ 24 is wrong on those two days: 2,118 rides on 30 March is 92.1 per hour, not 88.3.
- 7
Sorting.
Dateobjects subtract to numbers, but the defaultsort()turns them into text first and sorts the text, which puts30 Marbefore04 Maras the first line of the third example shows. Usesort((a, b) => a - b)ord3.ascending. ISO text (2025-03-04) sorts correctly as text because the biggest unit comes first.13/03/2025does not.
Explain it without notes
Why can a trip at 00:30 UTC belong to the previous day for some viewers?
How do you get the local day of an instant for Ardenvale without a library?
Why is dividing a daily total by 24 wrong on 30 March 2025?
Practice
Write localDay(iso, zone) returning YYYY-MM-DD. Use it to count how many of trips-10k.csv's trips start on 15 June on the Pacific/Auckland clock.
Write hoursInLocalDay(dateText, zone) that returns 23, 24 or 25 for a YYYY-MM-DD date by counting the hours.
Trade-offs
- ↔
Grouping by the local day matches what the team means, but needs a named zone and a formatter. Grouping by the UTC day is simple and wrong for them in winter. Decide once, write it on the chart.
Done when you can
I can explain why a Date is an instant and not a clock reading.
I can get the local day of a UTC timestamp for a named zone.
I can name Ardenvale's two odd-length days in 2025.
I parse dates with a stated format and handle
null.I sort dates with a comparator.