<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: timezones</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/timezones.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2024-11-27T20:45:25+00:00</updated><author><name>Simon Willison</name></author><entry><title>Storing times for human events</title><link href="https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#atom-tag" rel="alternate"/><published>2024-11-27T20:45:25+00:00</published><updated>2024-11-27T20:45:25+00:00</updated><id>https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#atom-tag</id><summary type="html">
    &lt;p&gt;I've worked on &lt;a href="https://en.wikipedia.org/wiki/Lanyrd"&gt;various&lt;/a&gt; event &lt;a href="https://en.wikipedia.org/wiki/Eventbrite"&gt;websites&lt;/a&gt; in the past, and one of the unintuitively difficult problems that inevitably comes up is the best way to store the time that an event is happening. Based on that past experience, here's my current recommendation.&lt;/p&gt;

&lt;p&gt;This is the expanded version of a &lt;a href="https://lobste.rs/s/sorhro/postgresql_timestamp_with_time_zone_s_set#c_xjj8ci"&gt;comment I posted on lobste.rs&lt;/a&gt; a few days ago, which ended up attracting a bunch of attention &lt;a href="https://twitter.com/iavins/status/1861468050748514547"&gt;on Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#the-problem"&gt;The problem&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#the-best-practice-that-isn-t"&gt;The "best practice" that isn't&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#things-that-can-go-wrong"&gt;Things that can go wrong&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#user-error"&gt;User error&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#international-timezone-shenanigans"&gt;International timezone shenanigans&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#microsoft-exchange-and-the-dst-update-of-2007"&gt;Microsoft Exchange and the DST update of 2007&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#my-recommendation-store-the-user-s-intent-time-and-the-location-timezone"&gt;My recommendation: store the user's intent time and the location/timezone&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href="https://simonwillison.net/2024/Nov/27/storing-times-for-human-events/#timezone-uis-suck-generally"&gt;Timezone UIs suck, generally&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id="the-problem"&gt;The problem&lt;/h4&gt;
&lt;p&gt;An event happens on a date, at a time. The precise details of that time are very important: if you tell people to show up to your event at 7pm and it turns out they should have arrived at 6pm they'll miss an hour of the event!&lt;/p&gt;
&lt;p&gt;Some of the worst bugs an events website can have are the ones that result in human beings traveling to a place at a time and finding that the event they came for is not happening at the time they expected.&lt;/p&gt;
&lt;p&gt;So how do you store the time of an event?&lt;/p&gt;
&lt;h4 id="the-best-practice-that-isn-t"&gt;The "best practice" that isn't&lt;/h4&gt;
&lt;p&gt;Any time you talk to database engineers about dates and times you're likely to get the same advice: store everything in UTC. Dates and times are complicated enough that the only unambiguous way to store them is in UTC - no daylight savings or timezones to worry about, it records the exact moment since the dawn of the universe at which the event will take place.&lt;/p&gt;
&lt;p&gt;Then, when you display those times to users, you can convert them to that user's current timezone - neatly available these days using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions"&gt;Intl.DateTimeFormat().resolvedOptions().timeZone&lt;/a&gt; browser API.&lt;/p&gt;
&lt;p&gt;There's a variant of this advice which you're more likely to hear from the PostgreSQL faithful: use &lt;a href="https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-DATETIME-INPUT-TIME-STAMPS"&gt;TIMESTAMP WITH TIME ZONE&lt;/a&gt; or its convenient alias &lt;code&gt;timestamptz&lt;/code&gt;. This stores the exact value in UTC and &lt;em&gt;sounds&lt;/em&gt; like it might store the timezone too... but it doesn't! &lt;a href="https://stackoverflow.com/questions/5876218/difference-between-timestamps-with-without-time-zone-in-postgresql#comment32979814_5876276"&gt;All that's stored&lt;/a&gt; is that UTC value, converted from whatever timezone was active or specified when the value was inserted.&lt;/p&gt;
&lt;p&gt;In either case, we are losing &lt;em&gt;critical&lt;/em&gt; information about when that event is going to happen.&lt;/p&gt;
&lt;h4 id="things-that-can-go-wrong"&gt;Things that can go wrong&lt;/h4&gt;
&lt;p&gt;What's wrong with calculating the exact UTC time the event is starting and storing only that?&lt;/p&gt;
&lt;p&gt;The problem is that we are losing crucial details about the event creator's original intent.&lt;/p&gt;
&lt;p&gt;If I arrange an evening meetup for next year on December 3rd at 6pm, I mean 6pm local time, by whatever definition of local time is active on that particular date.&lt;/p&gt;
&lt;p&gt;There are a number of ways this time can end up misinterpreted:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User error: the user created the event &lt;strong&gt;with an incorrect timezone&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;User error: the user created the event &lt;strong&gt;in the wrong location&lt;/strong&gt;, and later needs to fix it&lt;/li&gt;
&lt;li&gt;International timezone shenanigans: the location in which the event is happening &lt;strong&gt;changes its timezone rules&lt;/strong&gt; at some point between the event being created and the event taking place&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="user-error"&gt;User error&lt;/h4&gt;
&lt;p&gt;By far the most common issue here is &lt;strong&gt;user error&lt;/strong&gt; with respect to how the event was initially created.&lt;/p&gt;
&lt;p&gt;Maybe you asked the user to select the timezone as part of the event creation process. This is not a particularly great question: most users don't particularly care about timezones, or may not understand and respect them to the same extent as professional software developers.&lt;/p&gt;
&lt;p&gt;If they pick the wrong timezone we risk showing the wrong time to anyone else who views their event later on.&lt;/p&gt;
&lt;p&gt;My bigger concern is around location. Imagine a user creates their event in Springfield, Massachusetts... and then a few days later comes back and corrects the location to Springfield, Illinois.&lt;/p&gt;
&lt;p&gt;That means the event is happening in a different timezone. If the user fails to update the time of the event to match the new location, we're going to end up with an incorrect time stored in our database.&lt;/p&gt;
&lt;h4 id="international-timezone-shenanigans"&gt;International timezone shenanigans&lt;/h4&gt;
&lt;p&gt;One of my favourite niche corners of the internet is the &lt;a href="https://lists.iana.org/hyperkitty/list/tz@iana.org/latest"&gt;tz@iana.org mailing list&lt;/a&gt;. This is where the maintainers of the incredible open source &lt;a href="https://en.wikipedia.org/wiki/Tz_database"&gt;tz database&lt;/a&gt; hang out and keep track of global changes to timezone rules.&lt;/p&gt;
&lt;p&gt;It's easy to underestimate how much work this is, and how weird these rule changes can be. Here's a &lt;a href="https://lists.iana.org/hyperkitty/list/tz@iana.org/thread/5KMKN3JXZZHTUHGQWBCJSPTQXXYOPIPP/"&gt;recent email&lt;/a&gt;  proposing a brand new timezone: &lt;code&gt;Antarctica/Concordia&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Goodmorning. I'm writing here to propose a new time zone for an all-year open Antarctic base. The base is a French–Italian research facility that was built 3,233 m (10,607 ft) above sea level at a location called Dome C on the Antarctic Plateau, Antarctica. &lt;a href="https://en.wikipedia.org/wiki/Concordia_Station"&gt;https://en.wikipedia.org/wiki/Concordia_Station&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The timezone is UTC+8 without DST.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That's a pretty easy one. Here's a much more complicated example from March 2023: &lt;a href="https://lists.iana.org/hyperkitty/list/tz@iana.org/thread/EIBJYDJT3XQT5OWCNPIVVVH6U7INA2LW/"&gt;Lebanon DST change internally disputed&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Lebanon is going through many internal disputes surrounding the latest decision to delay DST. Many institutions are refusing to comply with the change and are going to adopt regular DST on Sunday Mar 26th. Those institutions include but are not limited to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;News agencies&lt;/li&gt;
&lt;li&gt;Religious organizations&lt;/li&gt;
&lt;li&gt;Schools, universities, etc...&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The refusal is mainly centered the legality of that decision and, obviously, the technical chaos it will create because of its short notice. Moreover, as some of the below articles mention, this is also causing sectarian strife.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Lebanon ended up with more than one timezone active at the same time, depending on which institution you were talking to!&lt;/p&gt;
&lt;p&gt;It's surprisingly common for countries to make decisions about DST with very little notice. Turkey and Russia and Chile and Morocco are four more examples of countries that can often cause short-term chaos for software developers in this way.&lt;/p&gt;
&lt;p&gt;If you've stored your event start times using UTC this is a &lt;em&gt;big&lt;/em&gt; problem: the new DST rules mean that an already-existing event that starts at 6pm may now start at 5pm or 7pm local time, according to the UTC time you've stored in your database.&lt;/p&gt;
&lt;h4 id="microsoft-exchange-and-the-dst-update-of-2007"&gt;Microsoft Exchange and the DST update of 2007&lt;/h4&gt;
&lt;p&gt;Via &lt;a href="https://lobste.rs/s/shckuc/storing_times_for_human_events#c_ygbfqp"&gt;fanf on Lobsters&lt;/a&gt; I heard about a fascinating example of this problem in action. In 2005 the Bush administration passed the &lt;a href="https://en.wikipedia.org/wiki/Energy_Policy_Act_of_2005#Change_to_daylight_saving_time"&gt;Energy Policy Act of 2005&lt;/a&gt;, one part of which updated the rules for when DST would start across most of the USA.&lt;/p&gt;
&lt;p&gt;This resulted in a bug where Microsoft Exchange and Outlook would display appointment times incorrectly! From &lt;a href="https://learn.microsoft.com/en-us/previous-versions/exchange-server/exchange-80/bb267339(v=exchg.80)?redirectedfrom=MSDN"&gt;Exchange Server and Daylight Saving Time (DST) 2007&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;After installing the DST updates, all old recurring and single instance appointments that occur during the delta period between the DST 2007 rules and the previous DST rules will be one hour later. These appointments will need to be updated so that they will display correctly in Outlook and Outlook Web Access, and for CDO based applications.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Microsoft released a special "Exchange Calendar Update Tool" executable for people to run to fix all of those upcoming calendar events.&lt;/p&gt;
&lt;h4 id="my-recommendation-store-the-user-s-intent-time-and-the-location-timezone"&gt;My recommendation: store the user's intent time and the location/timezone&lt;/h4&gt;
&lt;p&gt;My strong recommendation here is that the most important thing to record is the &lt;strong&gt;original user's intent&lt;/strong&gt;. If they said the event is happening at 6pm, store that! Make sure that when they go to edit their event later they see the same editable time that they entered when they first created it.&lt;/p&gt;
&lt;p&gt;In addition to that, try to get the most accurate possible indication of the timezone in which that event is occurring.&lt;/p&gt;
&lt;p&gt;For most events I would argue that the best version of this is the exact location of the venue itself.&lt;/p&gt;
&lt;p&gt;Users may find timezones confusing, but they hopefully understand the importance of helping their attendees know where exactly the event is taking place.&lt;/p&gt;
&lt;p&gt;If you have the venue location you can &lt;em&gt;almost certainly&lt;/em&gt; derive the timezone from it. I say &lt;em&gt;almost&lt;/em&gt; because, as with anything involving time, there are going to be edge-cases - most critically for venues that are exactly on the line that divides one timezone from another.&lt;/p&gt;
&lt;p&gt;I haven't sat down to design my ideal UI for this, but I can imagine something which makes it &lt;em&gt;abundantly&lt;/em&gt; clear to the user exactly where and when the event is taking place at that crucial local scale.&lt;/p&gt;
&lt;p&gt;Now that we've precisely captured the user's intent and the event location (and through it the exact timezone) we can denormalize: &lt;strong&gt;figure out the UTC time of that event and store that as well&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This UTC version can be used for all sorts of purposes: sorting events by time, figuring out what's happening now/next, displaying the event to other users with its time converted to their local timezone.&lt;/p&gt;
&lt;p&gt;But when the user goes to edit their event, we can show them exactly what they told us originally. When the user edits the location of their event we can maintain that original time, potentially confirming with the user if they want to modify that time based on the new location.&lt;/p&gt;
&lt;p&gt;And if some legislature somewhere on earth makes a surprising change to their DST rules, we can identify all of the events that are affected by that change and update that denormalized UTC time accordingly.&lt;/p&gt;
&lt;h4 id="timezone-uis-suck-generally"&gt;Timezone UIs suck, generally&lt;/h4&gt;
&lt;p&gt;As an aside, here's my least favorite time-related UI on the modern internet, from Google Calendar:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/google-calendar-timezones.jpg" alt="Google Calendar dialog for Event time zone, has a checkbox for Use separate start and end time zones and then a dropdown box with visible options (GMT-11:00) Niue Time, (GMT-11:00) Samoa Standard Time, (GMT-10:00) Cook Islands Standard Time, (GMT-10:00) Hawaii-Aleutian Standard Time, (GMT-10:00) Hawaii-Aleutian Time, (GMT-10:00) Tahiti Time, (GMT-09:30) Marquesas Time, (GMT-09:00) Alaska Time - Anchorage" style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;There isn't even a search option! Good luck finding America/New_York in there, assuming you knew that's what you were looking for in the first place.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/databases"&gt;databases&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/events"&gt;events&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/time"&gt;time&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="databases"/><category term="events"/><category term="time"/><category term="timezones"/></entry><entry><title>California Clock Change</title><link href="https://simonwillison.net/2024/Nov/3/california-clock-change/#atom-tag" rel="alternate"/><published>2024-11-03T05:11:06+00:00</published><updated>2024-11-03T05:11:06+00:00</updated><id>https://simonwillison.net/2024/Nov/3/california-clock-change/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://tools.simonwillison.net/california-clock-change"&gt;California Clock Change&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The clocks go back in California tonight and I finally built my &lt;em&gt;dream&lt;/em&gt; application for helping me remember if I get an hour extra of sleep or not, using a Claude Artifact. Here's &lt;a href="https://gist.github.com/simonw/9510723176f5b44ac1ebc495c95a4bc7"&gt;the transcript&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2024/california-clock-change.jpg" alt="California Clock Change. For Pacific Time (PST/PDT) only. When you go to bed on Saturday, November 2, 2024That's tonight!, you will get an extra hour of sleep! The clocks fall back from 2:00 AM to 1:00 AM on Sunday, November 3, 2024."&gt;&lt;/p&gt;
&lt;p&gt;This is one of my favorite examples yet of the kind of tiny low stakes utilities I'm building with Claude Artifacts because the friction involved in churning out a working application has dropped almost to zero.&lt;/p&gt;
&lt;p&gt;(I added another feature: it now &lt;a href="https://fedi.simonwillison.net/@simon/113419979044849672"&gt;includes a note&lt;/a&gt; of what time my Dog thinks it is if the clocks have recently changed.)


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai"&gt;ai&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/llms"&gt;llms&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/ai-assisted-programming"&gt;ai-assisted-programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/claude-artifacts"&gt;claude-artifacts&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/prompt-to-app"&gt;prompt-to-app&lt;/a&gt;&lt;/p&gt;



</summary><category term="projects"/><category term="timezones"/><category term="ai"/><category term="llms"/><category term="ai-assisted-programming"/><category term="claude-artifacts"/><category term="prompt-to-app"/></entry><entry><title>Australia/Lord_Howe is the weirdest timezone</title><link href="https://simonwillison.net/2024/Oct/31/lord-howe/#atom-tag" rel="alternate"/><published>2024-10-31T22:03:13+00:00</published><updated>2024-10-31T22:03:13+00:00</updated><id>https://simonwillison.net/2024/Oct/31/lord-howe/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://ssoready.com/blog/engineering/truths-programmers-timezones/"&gt;Australia/Lord_Howe is the weirdest timezone&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Lord Howe Island - part of Australia, population 382 - is unique in that the island's standard time zone is UTC+10:30 but is UTC+11 when daylight saving time applies. It's the only time zone where DST represents a 30 minute offset.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://lobste.rs/s/ktjpvq/australia_lord_howe_is_weirdest_timezone"&gt;lobste.rs&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;&lt;/p&gt;



</summary><category term="timezones"/></entry><entry><title>sqlite-jiff</title><link href="https://simonwillison.net/2024/Jul/23/sqlite-jiff/#atom-tag" rel="alternate"/><published>2024-07-23T03:53:52+00:00</published><updated>2024-07-23T03:53:52+00:00</updated><id>https://simonwillison.net/2024/Jul/23/sqlite-jiff/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/asg017/sqlite-jiff"&gt;sqlite-jiff&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I linked to the brand new Jiff datetime library &lt;a href="https://simonwillison.net/2024/Jul/22/jiff/"&gt;yesterday&lt;/a&gt;. Alex Garcia has already used it for an experimental SQLite extension providing a timezone-aware &lt;code&gt;jiff_duration()&lt;/code&gt; function - a useful new capability since SQLite's built in date functions don't handle timezones at all.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select jiff_duration(
  '2024-11-02T01:59:59[America/Los_Angeles]',
  '2024-11-02T02:00:01[America/New_York]',
  'minutes'
) as result; -- returns 179.966
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The implementation is &lt;a href="https://github.com/asg017/sqlite-jiff/blob/e02d625757105a68f5a64954262bd1ef8683212e/src/lib.rs"&gt;65 lines of Rust&lt;/a&gt;.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/agarcia_me/status/1815517168366485619"&gt;@agarcia_me&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/alex-garcia"&gt;alex-garcia&lt;/a&gt;&lt;/p&gt;



</summary><category term="sqlite"/><category term="timezones"/><category term="rust"/><category term="alex-garcia"/></entry><entry><title>Jiff</title><link href="https://simonwillison.net/2024/Jul/22/jiff/#atom-tag" rel="alternate"/><published>2024-07-22T04:48:35+00:00</published><updated>2024-07-22T04:48:35+00:00</updated><id>https://simonwillison.net/2024/Jul/22/jiff/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/BurntSushi/jiff"&gt;Jiff&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Andrew Gallant (aka BurntSushi) implemented &lt;a href="https://github.com/rust-lang/regex"&gt;regex&lt;/a&gt; for Rust and built the fabulous &lt;a href="https://github.com/BurntSushi/ripgrep"&gt;ripgrep&lt;/a&gt;, so it's worth paying attention to their new projects.&lt;/p&gt;
&lt;p&gt;Jiff is a brand new datetime library for Rust which focuses on "providing high level datetime primitives that are difficult to misuse and have reasonable performance". The API design is heavily inspired by the &lt;a href="https://tc39.es/proposal-temporal/docs/index.html"&gt;Temporal&lt;/a&gt; proposal for JavaScript.&lt;/p&gt;
&lt;p&gt;The core type provided by Jiff is &lt;code&gt;Zoned&lt;/code&gt;, best imagine as a 96-bit integer nanosecond time since the Unix each combined with a geographic region timezone and a civil/local calendar date and clock time.&lt;/p&gt;
&lt;p&gt;The &lt;a href="https://docs.rs/jiff/latest/jiff/"&gt;documentation&lt;/a&gt; is comprehensive and a fascinating read if you're interested in API design and timezones.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/burntsushi5/status/1815196645392527519"&gt;@burntsushi5&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/api-design"&gt;api-design&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/rust"&gt;rust&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/andrew-gallant"&gt;andrew-gallant&lt;/a&gt;&lt;/p&gt;



</summary><category term="api-design"/><category term="timezones"/><category term="rust"/><category term="andrew-gallant"/></entry><entry><title>It's Time For A Change: datetime.utcnow() Is Now Deprecated</title><link href="https://simonwillison.net/2023/Nov/18/utcnow-is-now-deprecated/#atom-tag" rel="alternate"/><published>2023-11-18T19:24:33+00:00</published><updated>2023-11-18T19:24:33+00:00</updated><id>https://simonwillison.net/2023/Nov/18/utcnow-is-now-deprecated/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://blog.miguelgrinberg.com/post/it-s-time-for-a-change-datetime-utcnow-is-now-deprecated"&gt;It&amp;#x27;s Time For A Change: datetime.utcnow() Is Now Deprecated&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Miguel Grinberg explains the deprecation of datetime.utcnow() and utcfromtimestamp() in Python 3.12, since they return naive datetime objects which cause all sorts of follow-on problems.&lt;/p&gt;

&lt;p&gt;The replacement idiom is datetime.datetime.now(datetime.timezone.utc)

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/miguelgrinberg/status/1725905759769129349"&gt;@miguelgrinberg&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="timezones"/></entry><entry><title>Benjamin "Zags" Zagorsky: Handling Timezones in Python</title><link href="https://simonwillison.net/2022/May/26/timezones/#atom-tag" rel="alternate"/><published>2022-05-26T03:40:05+00:00</published><updated>2022-05-26T03:40:05+00:00</updated><id>https://simonwillison.net/2022/May/26/timezones/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.youtube.com/watch?v=XZlPXLsSU2U"&gt;Benjamin &amp;quot;Zags&amp;quot; Zagorsky: Handling Timezones in Python&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
The talks from PyCon US have started appearing on YouTube. I found this one really useful for shoring up my Python timezone knowledge: It reminds that if your code calls datetime.now(), datetime.utcnow() or date.today(), you have timezone bugs—you’ve been working with ambiguous representations of instances in time that could span a 26 hour interval from UTC-12 to UTC+14. date.today() represents a 24 hour period and hence is prone to timezone surprises as well. My code has a lot of timezone bugs!


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/pycon"&gt;pycon&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;&lt;/p&gt;



</summary><category term="pycon"/><category term="python"/><category term="timezones"/></entry><entry><title>Temporal: getting started with JavaScript’s new date time API</title><link href="https://simonwillison.net/2021/Jul/7/temporal/#atom-tag" rel="alternate"/><published>2021-07-07T22:29:49+00:00</published><updated>2021-07-07T22:29:49+00:00</updated><id>https://simonwillison.net/2021/Jul/7/temporal/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://2ality.com/2021/06/temporal-api.html"&gt;Temporal: getting started with JavaScript’s new date time API&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Axel Rauschmayer explores the new proposed API for handling dates, times and timezones in JavaScript., which is under development by Ecma TC39 at the moment (and made available as a Polyfill which you are recommended not to run in production since the API is still being figured out). This is a notoriously difficult problem so it’s always interesting to see the latest thinking on how to best address it.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datetime"&gt;datetime&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;&lt;/p&gt;



</summary><category term="datetime"/><category term="javascript"/><category term="timezones"/></entry><entry><title>Big tech warns of 'Japan's millennium bug' ahead of Akihito's abdication</title><link href="https://simonwillison.net/2018/Jul/28/big-tech-warns-japans-millennium-bug-ahead-akihitos-abdication/#atom-tag" rel="alternate"/><published>2018-07-28T14:04:39+00:00</published><updated>2018-07-28T14:04:39+00:00</updated><id>https://simonwillison.net/2018/Jul/28/big-tech-warns-japans-millennium-bug-ahead-akihitos-abdication/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.theguardian.com/technology/2018/jul/25/big-tech-warns-japan-millennium-bug-y2k-emperor-akihito-abdication"&gt;Big tech warns of &amp;#x27;Japan&amp;#x27;s millennium bug&amp;#x27; ahead of Akihito&amp;#x27;s abdication&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Emperor Akihito’s abdication in April 2019 triggers a new era, and the Japanese calendar counts years from the coronation of the current emperor. The era hasn’t changed since 1989 and a great deal of software is unable to handle a change. To make things more complicated... the name of the new era will be announced in late February, but it needs to be represented in unicode as a single new character... and the next version of Unicode (v12) is due out in early March. There may have to be a Unicode 12.1 released shortly afterwards that includes the new codepoint.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://mm.icann.org/pipermail/tz/2018-July/026711.html"&gt;The tz mailing list&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/unicode"&gt;unicode&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/japan"&gt;japan&lt;/a&gt;&lt;/p&gt;



</summary><category term="timezones"/><category term="unicode"/><category term="japan"/></entry><entry><title>Building a location to time zone API with SpatiaLite, OpenStreetMap and Datasette</title><link href="https://simonwillison.net/2017/Dec/12/location-time-zone-api/#atom-tag" rel="alternate"/><published>2017-12-12T15:52:20+00:00</published><updated>2017-12-12T15:52:20+00:00</updated><id>https://simonwillison.net/2017/Dec/12/location-time-zone-api/#atom-tag</id><summary type="html">
    &lt;p&gt;Given a latitude and longitude, how can we tell what time zone that point lies within? Here’s how I built a simple JSON  API to answer that question, using a combination of data from OpenStreetMap, the SpatiaLite extension for SQLite and my &lt;a href="https://github.com/simonw/datasette"&gt;Datasette&lt;/a&gt; API tool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This article is out of date and no longer relevant&lt;/strong&gt;. It has been replaced by a new tutorial: &lt;a href="https://datasette.io/tutorials/spatialite"&gt;Building a location to time zone API with SpatiaLite&lt;/a&gt;, on the official Datasette website.&lt;/p&gt;
&lt;h3&gt;&lt;a id="The_API_4"&gt;&lt;/a&gt;The API&lt;/h3&gt;
&lt;p&gt;You can try the API out here: feed it a latitude and longitude and it will return the corresponding time zone ID: &lt;a href="https://timezones.datasette.io/timezones/by_point"&gt;https://timezones.datasette.io/timezones/by_point&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://timezones.datasette.io/timezones/by_point?longitude=-0.1406632&amp;amp;latitude=50.8246776"&gt;Brighton, England&lt;/a&gt; is in Europe/London (&lt;a href="https://timezones.datasette.io/timezones/by_point.json?longitude=-0.1406632&amp;amp;latitude=50.8246776"&gt;in JSON&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://timezones.datasette.io/timezones/by_point?longitude=-122.4494224&amp;amp;latitude=37.8022071"&gt;San Francisco, USA&lt;/a&gt; is in America/Los_Angeles (&lt;a href="https://timezones.datasette.io/timezones/by_point.json?longitude=-122.4494224&amp;amp;latitude=37.8022071"&gt;in JSON&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://timezones.datasette.io/timezones/by_point?longitude=139.7819661&amp;amp;latitude=35.6631424"&gt;Tokyo, Japan&lt;/a&gt; is Asia/Tokyo (&lt;a href="https://timezones.datasette.io/timezones/by_point.json?longitude=139.7819661&amp;amp;latitude=35.6631424"&gt;in JSON&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;&lt;a id="The_data_14"&gt;&lt;/a&gt;The data&lt;/h3&gt;
&lt;p&gt;I was first introduced to Eric Muller’s &lt;a href="http://efele.net/maps/tz/world/"&gt;tz_world shapefile&lt;/a&gt; by &lt;a href="https://twitter.com/Java_Nick"&gt;Nick Williams&lt;/a&gt; at Eventbrite, who used it to build us an internal time zone lookup API on top of &lt;a href="https://dev.mysql.com/doc/refman/5.7/en/spatial-types.html"&gt;MySQL’s geospatial data types&lt;/a&gt;. Eric’s project is no longer updated and he recommends Evan Siroky’s &lt;a href="https://github.com/evansiroky/timezone-boundary-builder"&gt;timezone-boundary-builder&lt;/a&gt; project as an alternative, which derives time zone shapes from OpenStreetMap and makes the resulting data available under the &lt;a href="https://opendatacommons.org/licenses/odbl/"&gt;Open Database License&lt;/a&gt;. The shapefile itself can be downloaded from &lt;a href="https://github.com/evansiroky/timezone-boundary-builder/releases/tag/2017c"&gt;the GitHub releases page&lt;/a&gt; for the project.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Loading_the_data_into_SpatiaLite_18"&gt;&lt;/a&gt;Loading the data into SpatiaLite&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.gaia-gis.it/fossil/libspatialite/index"&gt;SpatiaLite&lt;/a&gt; is a powerful open source extension for SQLite, which adds a comprehensive &lt;a href="http://www.gaia-gis.it/gaia-sins/spatialite-sql-4.3.0.html"&gt;suite of geospatial functions&lt;/a&gt; - including the ability to ingest shapefiles, convert them into geometries and run point within queries against the resulting shapes.&lt;/p&gt;
&lt;p&gt;The easiest way to get it running on OS X is via &lt;a href="https://brew.sh/"&gt;Homebrew&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ brew install spatialite-tools
$ brew install gdal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Having installed SpatiaLite, we can ingest the shapefile using &lt;code&gt;.loadshp combined_shapefile timezones CP1252 23032&lt;/code&gt; - here’s the full process, from downloading the shapefile to ingesting it into a new SQLite database file called &lt;code&gt;timezones.db&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wget https://github.com/evansiroky/timezone-boundary-builder/releases/download/2017c/timezones.shapefile.zip
$ unzip timezones.shapefile.zip
$ cd dist
$ spatialite timezones.db
SpatiaLite version ..: 4.3.0a   Supported Extensions:
...
Enter SQL statements terminated with a &amp;quot;;&amp;quot;
spatialite&amp;gt; .loadshp combined_shapefile timezones CP1252 23032
========
Loading shapefile at 'combined_shapefile' into SQLite table 'timezones'

BEGIN;
CREATE TABLE &amp;quot;timezones&amp;quot; (
&amp;quot;PK_UID&amp;quot; INTEGER PRIMARY KEY AUTOINCREMENT,
&amp;quot;tzid&amp;quot; TEXT);
SELECT AddGeometryColumn('timezones', 'Geometry', 23032, 'MULTIPOLYGON', 'XY');
COMMIT;

Inserted 414 rows into 'timezones' from SHAPEFILE
========
spatialite&amp;gt; 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s try it out with a query. Here’s the SQL needed to find the time zone for a point in Tokyo:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select tzid
from
    timezones
where
    within(GeomFromText('POINT(139.7819661 35.6631424)'), timezones.Geometry);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let’s run that in SpatiaLite and see what we get:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spatialite&amp;gt; select tzid
   ...&amp;gt; from
   ...&amp;gt;     timezones
   ...&amp;gt; where
   ...&amp;gt;     within(GeomFromText('POINT(139.7819661 35.6631424)'), timezones.Geometry);
Asia/Tokyo
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Looks good so far! How long is it taking though? We can find out by running &lt;code&gt;.timer on&lt;/code&gt; in the spatialite prompt:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spatialite&amp;gt; .timer on
spatialite&amp;gt; select tzid
   ...&amp;gt; from
   ...&amp;gt;     timezones
   ...&amp;gt; where
   ...&amp;gt;     within(GeomFromText('POINT(139.7819661 35.6631424)'), timezones.Geometry);
Asia/Tokyo
CPU Time: user 0.108479 sys 0.064778
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That’s a tenth of a second, or 100ms. Fast, but not brilliant.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Speeding_it_up_with_a_geospatial_index_81"&gt;&lt;/a&gt;Speeding it up with a geospatial index&lt;/h3&gt;
&lt;p&gt;It turns out SpatiaLite includes support for spatial indices, based on SQLite’s &lt;a href="https://sqlite.org/rtree.html"&gt;R*Tree module&lt;/a&gt;. R-Tree indexes can massively accelerate boundary box searches. Our searches are a lot more complex than that, acting as they do against extremely complex polygon shapes - but we can use a boundary box search to dramatically reduce the candidates we need to consider. Let’s create an index against our Geometry column:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SELECT CreateSpatialIndex('timezones', 'geometry');
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To take advantage of this index, we need to expand our original SQL to first filter by geometries where their bounding box contains the point we are searching for. SpatiaLite has created an index table called &lt;code&gt;idx_timezones_Geometry&lt;/code&gt; against which we can run an R-Tree optimized query. Here’s the SQL we will use:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select tzid
from
    timezones
where
    within(GeomFromText('POINT(139.7819661 35.6631424)'), timezones.Geometry)
    and rowid in (
        SELECT pkid FROM idx_timezones_Geometry
        where xmin &amp;lt; 139.7819661
        and xmax &amp;gt; 139.7819661
        and ymin &amp;lt; 35.6631424
        and ymax &amp;gt; 35.6631424
    );
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;How does this affect our performance?&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;spatialite&amp;gt; select tzid
   ...&amp;gt; from
   ...&amp;gt;     timezones
   ...&amp;gt; where
   ...&amp;gt;     within(GeomFromText('POINT(139.7819661 35.6631424)'), timezones.Geometry)
   ...&amp;gt;     and rowid in (
   ...&amp;gt;         SELECT pkid FROM idx_timezones_Geometry
   ...&amp;gt;         where xmin &amp;lt; 139.7819661
   ...&amp;gt;         and xmax &amp;gt; 139.7819661
   ...&amp;gt;         and ymin &amp;lt; 35.6631424
   ...&amp;gt;         and ymax &amp;gt; 35.6631424
   ...&amp;gt;     );
Asia/Tokyo
CPU Time: user 0.001541 sys 0.000111
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;From 100ms down to 1.5ms - nearly a 70x speedup! Not bad at all.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Building_and_publishing_the_API_with_Datasette_121"&gt;&lt;/a&gt;Building and publishing the API with Datasette&lt;/h3&gt;
&lt;p&gt;Now that we have a fast SQL query for finding a time zone for a latitude and longitude we can use Datasette to turn it into a JSON API.&lt;/p&gt;
&lt;p&gt;The simplest way to do that looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette timezones.db \
    --load-extension=/usr/local/lib/mod_spatialite.dylib
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will start Datasette on port 8001 and load the SpatiaLite extension. You can then navigate to &lt;code&gt;localhost:8001/timezones&lt;/code&gt; in your browser and paste in the SQL query… which should look something like this:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://timezones.datasette.io/timezones?sql=select%20tzid%0Afrom%0A%20%20%20%20timezones%0Awhere%0A%20%20%20%20within%28GeomFromText%28%27POINT%28139.7819661%2035.6631424%29%27%29%2C%20timezones.Geometry%29%0A%20%20%20%20and%20rowid%20in%20%28%0A%20%20%20%20%20%20%20%20SELECT%20pkid%20FROM%20idx_timezones_Geometry%0A%20%20%20%20%20%20%20%20where%20xmin%20%3C%20139.7819661%0A%20%20%20%20%20%20%20%20and%20xmax%20%3E%20139.7819661%0A%20%20%20%20%20%20%20%20and%20ymin%20%3C%2035.6631424%0A%20%20%20%20%20%20%20%20and%20ymax%20%3E%2035.6631424%0A%20%20%20%20%29%3B"&gt;https://timezones.datasette.io/timezones?sql=select+tzid%0D%0Afrom%0D%0A&lt;ins&gt;&lt;ins&gt;timezones%0D%0Awhere%0D%0A&lt;/ins&gt;&lt;/ins&gt;within(GeomFromText(‘POINT(139.7819661+35.6631424)’)%2C+timezones.Geometry)%0D%0A&lt;ins&gt;&lt;ins&gt;and+rowid+in+(%0D%0A++++&lt;/ins&gt;&lt;/ins&gt;SELECT+pkid+FROM+idx_timezones_Geometry%0D%0A&lt;ins&gt;&lt;ins&gt;&lt;ins&gt;&lt;ins&gt;where+xmin+&amp;lt;+139.7819661%0D%0A&lt;/ins&gt;&lt;/ins&gt;&lt;/ins&gt;&lt;/ins&gt;and+xmax+&amp;gt;+139.7819661%0D%0A&lt;ins&gt;&lt;ins&gt;&lt;ins&gt;&lt;ins&gt;and+ymin+&amp;lt;+35.6631424%0D%0A&lt;/ins&gt;&lt;/ins&gt;&lt;/ins&gt;&lt;/ins&gt;and+ymax+&amp;gt;+35.6631424%0D%0A++++)%3B&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This works (click the JSON link to get the result as JSON) but it’s a little inconvenient to use: you have to construct a URL with the same latitude and longitude repeated in multiple places.&lt;/p&gt;
&lt;p&gt;We can improve things using Datasette’s support for SQLite named parameters. Here’s that same SQL query using parameters instead of hard-coded latitude and longitude points:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select tzid
from
    timezones
where
    within(GeomFromText('POINT(' || :longitude || ' ' || :latitude || ')'), timezones.Geometry)
    and rowid in (
        SELECT pkid FROM idx_timezones_Geometry
        where xmin &amp;lt; :longitude
        and xmax &amp;gt; :longitude
        and ymin &amp;lt; :latitude
        and ymax &amp;gt; :latitude)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you &lt;a href="https://timezones.datasette.io/timezones?sql=select%20tzid%0Afrom%0A%20%20%20%20timezones%0Awhere%0A%20%20%20%20within%28GeomFromText%28%27POINT%28%27%20%7C%7C%20%3Alongitude%20%7C%7C%20%27%20%27%20%7C%7C%20%3Alatitude%20%7C%7C%20%27%29%27%29%2C%20timezones.Geometry%29%0A%20%20%20%20and%20rowid%20in%20%28%0A%20%20%20%20%20%20%20%20SELECT%20pkid%20FROM%20idx_timezones_Geometry%0A%20%20%20%20%20%20%20%20where%20xmin%20%3C%20%3Alongitude%0A%20%20%20%20%20%20%20%20and%20xmax%20%3E%20%3Alongitude%0A%20%20%20%20%20%20%20%20and%20ymin%20%3C%20%3Alatitude%0A%20%20%20%20%20%20%20%20and%20ymax%20%3E%20%3Alatitude%29"&gt;paste this into Datasette&lt;/a&gt; it will detect the named parameters and turn them into URL querystring parameters hooked up (in thu UI) to HTML form fields.&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2017/datasette-timezone.png" alt="Datasette time zone query showing longitude and latitude form fields" /&gt;&lt;/p&gt;
&lt;p&gt;To save us from having to include the full SQL in the URL every time we call our new API, let’s take advantage of a new feature &lt;a href="https://github.com/simonw/datasette/releases/tag/0.14"&gt;introduced in Datasette 0.14&lt;/a&gt;: &lt;a href="https://datasette.readthedocs.io/en/latest/sql_queries.html#canned-queries"&gt;canned queries&lt;/a&gt;. These are named, pre-packaged queries that can be defined in a &lt;code&gt;metadata.json&lt;/code&gt; file. The file looks like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
    &amp;quot;title&amp;quot;: &amp;quot;OpenStreetMap Time Zone Boundaries&amp;quot;,
    &amp;quot;license&amp;quot;: &amp;quot;ODbL&amp;quot;,
    &amp;quot;license_url&amp;quot;: &amp;quot;http://opendatacommons.org/licenses/odbl/&amp;quot;,
    &amp;quot;source&amp;quot;: &amp;quot;timezone-boundary-builder&amp;quot;,
    &amp;quot;source_url&amp;quot;: &amp;quot;https://github.com/evansiroky/timezone-boundary-builder&amp;quot;,
    &amp;quot;databases&amp;quot;: {
        &amp;quot;timezones&amp;quot;: {
            &amp;quot;queries&amp;quot;: {
                &amp;quot;by_point&amp;quot;: &amp;quot;select tzid\nfrom\n    timezones\nwhere\n    within(GeomFromText('POINT(' || :longitude || ' ' || :latitude || ')'), timezones.Geometry)\n    and rowid in (\n        SELECT pkid FROM idx_timezones_Geometry\n        where xmin &amp;lt; :longitude\n        and xmax &amp;gt; :longitude\n        and ymin &amp;lt; :latitude\n        and ymax &amp;gt; :latitude\n    )&amp;quot;
            }
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The canned query is defined as the &lt;code&gt;by_point&lt;/code&gt; key in the &lt;code&gt;queries&lt;/code&gt; nested object. I’m also adding license and source information here for the project, because it’s good manners.&lt;/p&gt;
&lt;p&gt;We can try this in Datasette on our local machine like so:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette timezones.db -m metadata.json \
    --load-extension=/usr/local/lib/mod_spatialite.dylib 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now visiting &lt;code&gt;localhost:8001/timezones/by_point&lt;/code&gt; will provide the interface for the query - and adding &lt;code&gt;.json&lt;/code&gt; to the URL will turn it into an API.&lt;/p&gt;
&lt;h3&gt;&lt;a id="Vizualizing_time_zones_with_Leaflet_and_GeoJSON_180"&gt;&lt;/a&gt;Vizualizing time zones with Leaflet and GeoJSON&lt;/h3&gt;
&lt;p&gt;If you browse around in Datasette you’ll quickly run into a rather unfortunate problem. The &lt;code&gt;localhost:8001/timezones/timezones&lt;/code&gt; page, which shows the first 100 rows in the table, takes a shockingly long time to load. When it eventually does load you’ll see why: each record includes an enormous binary string containing its the geometry. On my machine just that one page weighs in at 62MB of HTML!&lt;/p&gt;
&lt;p&gt;This is bad: rendering that much HTML in one go can block the event loop and cause the application to become unresponsive. That giant blob of binary data isn’t exactly useful for humans, either.&lt;/p&gt;
&lt;p&gt;We can make some major improvements here using another Datasette 0.14 feature: &lt;a href="https://datasette.readthedocs.io/en/latest/custom_templates.html"&gt;custom templates&lt;/a&gt;. Let’s start with a replacement template that shows just the length of the binary string instead of attempting to render it.&lt;/p&gt;
&lt;p&gt;We’ll do that by over-riding the &lt;code&gt;_rows_and_columns.html&lt;/code&gt; include template, which is used by Datasette on both the table page and the page used to display individual rows. Since we only want to over-ride this template for one particular table we’ll create a file called &lt;code&gt;_rows_and_columns-timezones-timezones.html&lt;/code&gt; - the file name means this will only over-ride the template &lt;code&gt;timezones&lt;/code&gt; table in our &lt;code&gt;timezones&lt;/code&gt; database. Here’s our new template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&amp;lt;table&amp;gt;
    &amp;lt;thead&amp;gt;
        &amp;lt;tr&amp;gt;
            {% for column in display_columns %}
                &amp;lt;th scope=&amp;quot;col&amp;quot;&amp;gt;{{ column }}&amp;lt;/th&amp;gt;
            {% endfor %}
        &amp;lt;/tr&amp;gt;
    &amp;lt;/thead&amp;gt;
    &amp;lt;tbody&amp;gt;
    {% for row in display_rows %}
        &amp;lt;tr&amp;gt;
            {% for cell in row %}
                &amp;lt;td&amp;gt;
                    {% if cell.column == 'Geometry' %}
                        {{ cell.value|length }} bytes
                    {% else %}
                        {{ cell.value }}
                    {% endif %}
                &amp;lt;/td&amp;gt;
            {% endfor %}
        &amp;lt;/tr&amp;gt;
    {% endfor %}
    &amp;lt;/tbody&amp;gt;
&amp;lt;/table&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we put that in a directory called &lt;code&gt;templates/&lt;/code&gt; we can tell Datasette to use it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;datasette timezones.db -m metadata.json \
    --load-extension=/usr/local/lib/mod_spatialite.dylib \
    --template-dir=templates/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Our &lt;code&gt;localhost:8001/timezones/timezones&lt;/code&gt; page now looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2017/datasette-timezone-index.png" alt="Datasette time zone index showing lengths" /&gt;&lt;/p&gt;
&lt;p&gt;But wouldn’t it be neat if we could see the actual shapes of these time zones? It turns out that’s actually pretty easy, using the combination of GeoJSON and the &lt;a href="http://leafletjs.com/"&gt;Leaflet&lt;/a&gt; mapping library.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/GeoJSON"&gt;GeoJSON&lt;/a&gt; is a neat, widely supported standard for encoding geographic information such as polygon shapes as JSON. SpatiaLite ships with built-in GeoJSON support in the form of the &lt;code&gt;AsGeoJSON&lt;/code&gt; SQL function. We can use that function to turn any of our time zone geometries into a GeoJSON string:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;select AsGeoJSON(Geometry) from timezones where tzid = 'Asia/Tokyo';
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you &lt;a href="https://timezones.datasette.io/timezones?sql=select+AsGeoJSON(Geometry)+from+timezones+where+tzid+%3D+%27Asia%2FTokyo%27%3B"&gt;run that with Datasette&lt;/a&gt; you’ll get back a string of GeoJSON. You can paste that into &lt;a href="http://geojson.io"&gt;geojson.io&lt;/a&gt; to instantly visualize it.&lt;/p&gt;
&lt;p&gt;The Leaflet mapping library &lt;a href="http://leafletjs.com/examples/geojson/"&gt;supports GeoJSON out of the box&lt;/a&gt;. We can construct a custom &lt;code&gt;row.html&lt;/code&gt; template for our Datasette that loads Leaflet from &lt;a href="https://unpkg.com"&gt;unpkg.com&lt;/a&gt;, uses &lt;code&gt;fetch()&lt;/code&gt; to execute the &lt;code&gt;AsGeoJSON&lt;/code&gt; query and renders the result in a map on the page. Here’s &lt;a href="https://timezones.datasette.io/timezones/timezones/12"&gt;the result&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://timezones.datasette.io/timezones/timezones/12"&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2017/datasette-geojson.png" alt="Custom Datasette page rendering a GeoJSON map" /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And here’s the &lt;code&gt;row-timezones-timezones.html&lt;/code&gt; template:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{% extends &amp;quot;row.html&amp;quot; %}
{% block extra_head %}
&amp;lt;link rel=&amp;quot;stylesheet&amp;quot; href=&amp;quot;https://unpkg.com/leaflet@1.2.0/dist/leaflet.css&amp;quot; integrity=&amp;quot;sha512-M2wvCLH6DSRazYeZRIm1JnYyh22purTM+FDB5CsyxtQJYeKq83arPe5wgbNmcFXGqiSH2XR8dT/fJISVA1r/zQ==&amp;quot; crossorigin=&amp;quot;&amp;quot;/&amp;gt;
&amp;lt;script src=&amp;quot;https://unpkg.com/leaflet@1.2.0/dist/leaflet.js&amp;quot; integrity=&amp;quot;sha512-lInM/apFSqyy1o6s89K4iQUKg6ppXEgsVxT35HbzUupEVRh2Eu9Wdl4tHj7dZO0s1uvplcYGmt3498TtHq+log==&amp;quot; crossorigin=&amp;quot;&amp;quot;&amp;gt;&amp;lt;/script&amp;gt;
&amp;lt;style&amp;gt;
#map {
  margin-top: 1em;
  width: 100%;
  height: 400px;
}
&amp;lt;/style&amp;gt;
{% endblock %}

{% block content %}
{{ super() }}
&amp;lt;div id=&amp;quot;map&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script&amp;gt;
var pk = location.pathname.split('/').slice(-1)[0];
var tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    detectRetina: true,
    attribution: '&amp;amp;copy; &amp;lt;a href=&amp;quot;https://www.openstreetmap.org/copyright&amp;quot;&amp;gt;OpenStreetMap&amp;lt;/a&amp;gt; contributors, Points &amp;amp;copy 2012 LINZ'
});
var sql = 'select AsGeoJSON(Geometry) from timezones where PK_UID = ' + pk;
fetch('/timezones.json?sql=' + encodeURIComponent(sql)).then(r =&amp;gt; r.json()).then(d =&amp;gt; {
  var map = L.map('map', {layers: [tiles]});
  var geoJSON = JSON.parse(d.rows[0][0]);
  var layer = L.geoJSON(geoJSON)
  layer.addTo(map);
  map.fitBounds(layer.getBounds());
});
&amp;lt;/script&amp;gt;
{% endblock %}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="Publishing_it_to_the_internet_273"&gt;&lt;/a&gt;Publishing it to the internet&lt;/h3&gt;
&lt;p&gt;Normally we would use the &lt;code&gt;datasette publish&lt;/code&gt; command to publish our database to Heroku or Zeit Now, but the SpatiaLite dependency means that won’t work for this case. Instead, we need to construct a custom Dockerfile that builds the SpatiaLite module.&lt;/p&gt;
&lt;p&gt;Since we’re using Docker, we may as well have the Dockerfile download the shapefiles and build the SpatiaLite database for us all in one go. Here’s a Dockerfile that does exactly that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;FROM python:3.6-slim-stretch

RUN apt update
RUN apt install -y python3-dev gcc spatialite-bin libsqlite3-mod-spatialite wget unzip

RUN pip install https://github.com/simonw/datasette/archive/master.zip

# Download and import the shapefiles
RUN wget https://github.com/evansiroky/timezone-boundary-builder/releases/download/2017c/timezones.shapefile.zip \
    &amp;amp;&amp;amp; unzip timezones.shapefile.zip &amp;amp;&amp;amp; \
    cd dist &amp;amp;&amp;amp; \
    echo &amp;quot;.loadshp combined_shapefile timezones CP1252 23032\nSELECT CreateSpatialIndex('timezones', 'geometry');&amp;quot; | spatialite timezones.db &amp;amp;&amp;amp; \
    mv timezones.db .. &amp;amp;&amp;amp; \
    cd .. &amp;amp;&amp;amp; rm -rf dist &amp;amp;&amp;amp; rm timezones.shapefile.zip

ENV SQLITE_EXTENSIONS /usr/lib/x86_64-linux-gnu/mod_spatialite.so

ADD metadata.json metadata.json

ADD templates templates

RUN datasette inspect timezones.db --inspect-file inspect-data.json

EXPOSE 8001

CMD [&amp;quot;datasette&amp;quot;, &amp;quot;serve&amp;quot;, &amp;quot;timezones.db&amp;quot;, &amp;quot;--host&amp;quot;, &amp;quot;0.0.0.0&amp;quot;, &amp;quot;--cors&amp;quot;, &amp;quot;--port&amp;quot;, &amp;quot;8001&amp;quot;, &amp;quot;--inspect-file&amp;quot;, &amp;quot;inspect-data.json&amp;quot;, &amp;quot;-m&amp;quot;, &amp;quot;metadata.json&amp;quot;, &amp;quot;--template-dir&amp;quot;, &amp;quot;templates&amp;quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The full code, including the supporting templates, can be found in &lt;a href="https://github.com/simonw/timezones-api"&gt;simonw/timezones-api&lt;/a&gt; on GitHub.&lt;/p&gt;
&lt;p&gt;If you have Docker installed (&lt;a href="https://www.docker.com/docker-mac"&gt;Docker for Mac&lt;/a&gt; is a one-click install package these days, it’s impressively painless) you can build the container like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker build . -t timezones-api
# Wait for the image to build
docker run -p 8001:8001 timezones-api
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then you can visit &lt;code&gt;http://localhost:8001/&lt;/code&gt; to see your freshly built Datasette in your browser.&lt;/p&gt;
&lt;p&gt;The easiest way to publish it online is using &lt;a href="https://zeit.co/now"&gt;Zeit Now&lt;/a&gt;. Simply run the &lt;code&gt;now&lt;/code&gt; command in the directory containing the Dockerfile and Zeit will upload the entire directory, build the container in the cloud and deploy it with a fresh URL. It’s by far the easiest Docker deployment environment I’ve ever used.&lt;/p&gt;
&lt;p&gt;Now can even deploy directly from a public GitHub repository… so you can deploy your own copy of the API by running the following command:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ now simonw/timezones-api
&amp;gt; Didn't find directory. Searching on GitHub...
&amp;gt; Deploying GitHub repository &amp;quot;simonw/timezones-api&amp;quot; under simonw
&amp;gt; Ready! https://simonw-timezones-api-fbihjcbnog.now.sh (copied to clipboard) [2s]
&amp;gt; Initializing…
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;a id="Canned_queries__SpatiaLite__instant_geospatial_APIs_326"&gt;&lt;/a&gt;Canned queries + SpatiaLite = instant geospatial APIs&lt;/h3&gt;
&lt;p&gt;Hopefully this has helped illustrate the ease with which Datasette, SpatiaLite and canned queries can be used to create and publish geospatial APIs. Thanks to projects like OpenStreetMap the world is full of high quality open geospatial data. Go build something cool with it!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/geospatial"&gt;geospatial&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/openstreetmap"&gt;openstreetmap&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/docker"&gt;docker&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/leaflet"&gt;leaflet&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="geospatial"/><category term="openstreetmap"/><category term="sqlite"/><category term="timezones"/><category term="docker"/><category term="datasette"/><category term="leaflet"/></entry><entry><title>django-timezones</title><link href="https://simonwillison.net/2008/Aug/21/djangotimezones/#atom-tag" rel="alternate"/><published>2008-08-21T23:18:08+00:00</published><updated>2008-08-21T23:18:08+00:00</updated><id>https://simonwillison.net/2008/Aug/21/djangotimezones/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://code.google.com/p/django-timezones/"&gt;django-timezones&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Models, form fields and a template filter for dealing with timezones in Django.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="python"/><category term="timezones"/></entry><entry><title>What's New in Edge Rails: Easier Timezones</title><link href="https://simonwillison.net/2008/Apr/2/timezones/#atom-tag" rel="alternate"/><published>2008-04-02T15:39:32+00:00</published><updated>2008-04-02T15:39:32+00:00</updated><id>https://simonwillison.net/2008/Apr/2/timezones/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://ryandaigle.com/articles/2008/1/25/what-s-new-in-edge-rails-easier-timezones"&gt;What&amp;#x27;s New in Edge Rails: Easier Timezones&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Time zones can be a nightmare to get right—if this works well it’s going to make a lot of people’s lives a whole bunch easier.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://weblog.rubyonrails.com/2008/4/1/a-taste-of-what-s-coming-in-rails-2-1"&gt;A taste of what&amp;#x27;s coming in Rails 2.1&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/rails"&gt;rails&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;&lt;/p&gt;



</summary><category term="rails"/><category term="timezones"/></entry><entry><title>calendar.timegm()</title><link href="https://simonwillison.net/2007/Sep/3/calendar/#atom-tag" rel="alternate"/><published>2007-09-03T01:54:35+00:00</published><updated>2007-09-03T01:54:35+00:00</updated><id>https://simonwillison.net/2007/Sep/3/calendar/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://docs.python.org/lib/module-calendar.html#l2h-719"&gt;calendar.timegm()&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
An “unrelated but handy function” that converts a time.gmtime() in to a corresponding Unix timestamp. I’ve been hand-rolling this one for years; never thought to look in calendar.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="http://intertwingly.net/blog/2007/09/02/Dealing-With-Dates"&gt;Sam Ruby&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/calendar"&gt;calendar&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datetime"&gt;datetime&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sam-ruby"&gt;sam-ruby&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/time"&gt;time&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/unixtimestamp"&gt;unixtimestamp&lt;/a&gt;&lt;/p&gt;



</summary><category term="calendar"/><category term="datetime"/><category term="python"/><category term="sam-ruby"/><category term="time"/><category term="timezones"/><category term="unixtimestamp"/></entry><entry><title>Django vs feedparser on dates</title><link href="https://simonwillison.net/2007/Sep/2/django/#atom-tag" rel="alternate"/><published>2007-09-02T10:17:52+00:00</published><updated>2007-09-02T10:17:52+00:00</updated><id>https://simonwillison.net/2007/Sep/2/django/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://www.deadlybloodyserious.com/2007/09/feedparser-v-django/"&gt;Django vs feedparser on dates&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Some useful tips in the comments. I find Python’s timezone stuff endlessly frustrating: I know it can do what I want, but it always takes me a ridiculously long time to figure out the necessary incantations.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/datetime"&gt;datetime&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/feedparser"&gt;feedparser&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;&lt;/p&gt;



</summary><category term="datetime"/><category term="django"/><category term="feedparser"/><category term="python"/><category term="timezones"/></entry><entry><title>A Zoned Defense</title><link href="https://simonwillison.net/2007/Mar/20/timezone/#atom-tag" rel="alternate"/><published>2007-03-20T09:51:57+00:00</published><updated>2007-03-20T09:51:57+00:00</updated><id>https://simonwillison.net/2007/Mar/20/timezone/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://errtheblog.com/post/1658"&gt;A Zoned Defense&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Using JavaScript’s date.getTimezoneOffset() to detect the user’s timezone and stash it in a cookie.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/timezones"&gt;timezones&lt;/a&gt;&lt;/p&gt;



</summary><category term="javascript"/><category term="timezones"/></entry></feed>