Turning cities into time

My adventures in cities and dates and time

But first, back in time…

Goal

  • User visits https://datetime.link
  • User searches for a city
  • Each city is assigned a human-readable ID
  • Look up time in that city

A few hours of coding, right?

Giphy
Giphy

Getting a list of cities

  • Downloadable database instead of a city API
    • Self-contained, no other costs except for hosting
    • Must map to timezone information
    • Sufficient city names, not just timezone names
Giphy
Giphy

I found GeoNames!

  • Has timezones! In tz format too!
  • Has alternate names!
  • License: Creative Commons Attribution 4.0

(Do you know of any alternatives?)

GeoNames

  • User-editable database of points of interest
  • Provides a dump of information in tab-seperated files
  • Each place is organised by a hierarchy
    • country
    • admin1 (administrative level 1)
    • admin2/city
    • admin3
    • admin4
    • admin5
  • Provides alternative names and timezone
  • Guide: http://download.geonames.org/export/dump/readme.txt
Density of GeoNames information. Source: Wikimedia
Density of GeoNames information. Source: Wikimedia

GeoNames city data snippet

screenshot of data snipped

tz database

  • An ICANN-supported database of timezones
  • Shipped with most Linux distributions
  • Supported by the Go time package
  • Format: Continent/City
    • Asia/Singapore
    • America/New_York

Assigning an identifier

  • I wanted each city to be referenced directly in a URL while remaining human-readable
    • https://datetime.link/now/Singapore,London
  • But there are cities with the same name but different region or country
    • Paradise, California, US
    • Paradise, Nevada, US
  • Join city, admin1 and country. Ignore admin1 when same as country name
    • Singapore-SG, Slough-England-GB, Albany-New_York-US
  • In the future, add option to pick the more populous city
https://media.giphy.com/media/kDq2MhBUOKEow/giphy.gif
Giphy

Relaxed searches

  • I wanted to keep alternatenames for cities, admin1 and country so I could later implement fuzzy search
  • Because alternatenames sometimes have very similar or duplicates, I filtered some out
  • Example: SIN,Sin-ka-po,Singapore,Singapore City,Singapour,Singapur,Sin gapura,Sinkapoure,Sîn-kâ-po,Tumasik,cinkappur,prathes singkhpor,shingaporu,sigapura,sing-gapo l,sing-gapoleu,singapura,singkh por,sngapwr,snghafwrt,syngpwr,xin jia po,xing jia po,Σιγκαπού ρη,Сингапур,Сінгапур,סינגפור,ﺲﻨﻏﺎﻓﻭﺭﺓ,ﺲﻧگﺍپﻭﺭ,सिंगापुर,सिंगापूर,ਸਿੰਗਾਪੁਰ,சிங்கப்பூர்,ประเทศสิงคโปร์,สิงค์โปร,ປະເທດ ສງກະໂປ,ປະເທດສິງກະໂປ,စငကာပနငင,စင်ကာပူနိုင်ငံ,សងហបរ,សិង្ហបុរី,シンガポール,新加坡,星架坡,싱가포르,싱가폴
  • Search is something I haven’t implemented, but would have been really easy if I instead threw all the data into a database. But no, I want to challenge myself. That’s the fun part of side projects!

Processing the GeoNames data

func readCities(f string, countries map[string]data.Country, admin1s map[string]data.Admin1) (map[string]*data.City, error) {
    file, err := os.Open(f)
    ...
    r := csv.NewReader(file)
    r.Comma = '\t'
    r.Comment = '#'
    m := make(map[string]*data.City)
    for {
        ...
        name := record[1]
        ref := normalizeName(record[2])
        alternateNames, err := limitNames(name, splitNames(record[3]))
        ...
        admin1Code := record[10]
        countryRef := record[8]
        population, err := strconv.ParseUint(record[14], 10, 64)
        ...
        timezone := record[17]
        ...
        country := countries[countryRef]
        admin1 := admin1s[countryRef+"."+admin1Code]
        eref := extendRef(ref, admin1.Ref, country.Ref)
        ...
        c := &**data.City{
            Ref:            ref,
            Name:           name,
            AlternateNames: alternateNames,
            Timezone:       timezone,
            Population:     population,
            Admin1:         admin1,
            Country:        country,
        }**
        ...
    }
}

func main() {
    admin1s, err := readAdmin1Divisions("../third-party/admin1CodesASCII.txt")
    ...
    countries, err := readCountries("../third-party/countryInfo.txt")
    ...
    cities, err := readCities("../third-party/cities15000.txt", countries, admin1s)
    ...
    b, err := json.Marshal(cities)
    ...
    err = ioutil.WriteFile("../data/cities.json", b, 0644)
}

Result

Now I have an ASCII-based key for every city, and their alternate names and timezone!

{
  ...
  "Albany-Georgia-US": {
    "n": "Albany",
    "an": [
      "City of Opportunity",
      "albany  jarjya",
      "Олбани",
      "ao er ba ni",
      "olbeoni",
      "orubani",
      "albani"
    ],
    "t": "America/New_York",
    "p": 74843,
    "a1": {
      "n": "Georgia"
    },
    "c": {
      "r": "US",
      "n": "United States"
    }
  },
  ...
  "Singapore-SG": {
    "n": "Singapore",
    "an": [
      "Σιγκαπούρη",
      "prathes singkhpor",
      "Сингапур",
      "Singapore City",
      "sing-gapoleu",
      "xing jia po",
      "Sîn-kâ-po",
      "Sinkapoure",
      "singkh por",
      "shingaporu",
      "Sin-ka-po"
    ],
    "t": "Asia/Singapore",
    "p": 3547809,
    "a1": {
      "n": ""
    },
    "c": {
      "r": "SG",
      "n": "Singapore"
    }
  },
  ...
}
Giphy
Giphy

Resolving timezones

  • How do I get from a tz database timezone (“Asia/Singapore”) into a time?
  • Go support tz database timezones natively!
    ...
    // Parse time portion
    var t time.Time
    timeString := "2022-07-27T00:00Z"
    ...
    for _, f := range timeFormats {
        t, err = **time.Parse(f, timeString)**
        if err == nil {
            break
        }
    }
    ...
    loc, err := **time.LoadLocation("Asia/Singapore")**
    ...
    timeInSingapore := t.In(loc) // So easy!
    log.Printf("%s", timeInSingapore)

How Go resolves timezones

Go time package LoadLocation
Go time package LoadLocation
  • Can’t use system timezone database on Windows (https://github.com/golang/go/issues/38453)
    • Solution: import _ “time/tzdata” or go build -tags timetzdata
  • In JavaScript, most browsers ship with an embedded IANA timezone database. However, the specification does not require implementations to do so.
  • In Dart, the standard library does not support timezones other than local and UTC, because there’s no guarantee that the system Dart code is running in has a timezone database. There are Dart packages that do so though.

Thank you!

  • GeekcampSG 2022 is happening late October! Follow our socials for updates
  • I'm hiring a Principal Software Engineer for Happily Ever After (https://hea.care)