src/xkcdgeohash

Search:
Group by:

Geohashing library for Nim

Implementation of the geohashing algorithm from https://xkcd.com/426/

The library provides an object-oriented, functional, and commandline API for calculating geohash coordinates according to the xkcd geohashing algorithem spesification.

Algorithm spec can be seen at: https://geohashing.site/geohashing/The_Algorithm

Copyright (c) 2025 Sebastian H. Lorenzen Licensed under MIT License

Quick Start

import xkcdgeohash
import std/times

# Simple functional API
let result: GeohashResult = xkcdgeohash(68.0, -30.0, now())
echo "Coordinates: ", result.latitude, ", ", result.longitude

# Object-oriented API for repeated calculations
let geohasher: Geohasher = newGeohasher(68, -30)
let coords: GeohashResult = geohasher.hash(now())

# Global geohash calculation
let globalCoords: GeohashResult = xkcdglobalgeohash(now())
echo "Global coordinates: ", globalCoords.latitude, ", ", globalCoords.longitude

Per Default the Library tries to fetch data from the following sources via a http-client:

Object Oriented API:

Ideal when preforming multiple geohash calculations at the same graticule (integer coordinate area). Allows you to reuse setup. It automatically handles Dow Jones data fetching and applies the 30W timezone rule. Dow Jones Provider can be changed, per default HttpDowProvider is used.

# Create a geohasher for the Minneapolis area
let geohasher: Geohasher = newGeohasher(45, -93)

# Calculate coordinates for different dates
let today: GeohashResult = geohasher.hash(now())
let yesterday: GeohashResult = geohasher.hash(now() - 1.days)

# Use custom Dow Jones data source
let customProvider: HttpDowProvider = getDefaultDowProvider()
let customGeohasher: Geohasher = newGeohasher(45, -93, customProvider)

let yeasteryeasterday: GeohashResult = geohasher.hash(now() - 2.days)

# Global geohash with OO API
let globalGeohasher: GlobalGeohasher = newGlobalGeohasher()
let globalResult: GeohashResult = globalGeohasher.hash(now())

Functional API:

Simple, stateless way to calculate geohashes for one-off calculations. It automatically handles Dow Jones data fetching and applies the 30W timezone rule. Dow Jones Provider can be changed, per default HttpDowProvider is used.

# Calculate geohash for specific coordinates and date
let result = xkcdgeohash(45.0, -93.0, dateTime(2008, mMay, 21))

echo "Latitude: ", result.latitude
echo "Longitude: ", result.longitude
echo "Used Dow date: ", result.usedDowDate.format("yyyy-MM-dd")

# Calculate global geohash for a specific date
let globalResult = xkcdglobalgeohash(dateTime(2008, mMay, 21))
echo "Global coordinates: ", globalResult.latitude, ", ", globalResult.longitude

Commandline Use:

XKCD Geohash Calculator

Usage:
    xkcdgeohash --lat=<latitude> --lon=<longitude> [options]
    xkcdgeohash --global [options]
    xkcdgeohash --version
    xkcdgeohash --help

Options:
    --lat=<latitude>         Target latitude
    --lon=<longitude>        Target longitude
    -d, --date=DATE          Target date (YYYY-MM-DD, default: today)
    -g, --global             Calculate global geohash
    -v, --verbose            Show additional information
    -j, --json               Output as JSON
    -f, --format=FORMAT      Output format [default: decimal]
                            (decimal, dms, coordinates)
    --from=DATE              Start date for range
    --to=DATE                End date for range
    --days=N                 Last N days from today
    --source=URL             Dow Jones data source URL
    --data-file=FILE         Local Dow Jones data file
    --url=SERVICE            Generate map URL for service
                            (google, bing, osm, waymarked)
    --zoom=LEVEL             Zoom level for map URLs [default: 15]
    --marker                 Add marker to map URL
    -h, --help               Show this help message
    --version                Show version
    --test                   Toggle use of mockdata when testing

Output Formats:
    decimal                  68.857713, -30.544544 (default)
    dms                      68°51'27.8"N, 30°32'40.4"W
    coordinates              68.857713,-30.544544

URL Services:
    google                   Google Maps
    bing                     Bing Maps
    osm                      OpenStreetMap
    waymarked                Waymarked Trails (hiking/cycling routes)

Examples:
    xkcdgeohash --lat=68.0 --lon=-30.0
    xkcdgeohash --global --date=2008-05-26
    xkcdgeohash --lat=68.0 --lon=-30.0 --url=google --marker
    xkcdgeohash --lat=45.0 --lon=-93.0 --days=7 --url=google --json
    xkcdgeohash --lat=68.0 --lon=-30.0 --verbose --url=osm --zoom=12

30W Timezone Rule

The algorithm implements the 30W timezone rule:

  • West of 30W longitude (Americas): Uses Dow Jones price from same day
  • East of 30W longitude (Europe, Africa, Asia): Uses Dow Jones price from previous day
  • Before 2008-05-27: All coordinates use same day (rule wasn't active yet)
  • Global Hashes: Uses Dow Jones price from previous day, no matter what

See also: https://geohashing.site/geohashing/30W_Time_Zone_Rule#30W_compliance_confusion_matrix

Global Geohashing

Global geohashes provide a single worldwide coordinate for each date, covering the entire globe. Unlike regular geohashes which are constrained to 1x1 degree graticules, global geohashes can land anywhere on Earth.

# Functional API (recommended for most use cases)
let globalCoords = xkcdglobalgeohash(now())
echo "Today's global meetup: ", globalCoords.latitude, ", ", globalCoords.longitude

# Object-oriented API for repeated calculations
let globalGeohasher = newGlobalGeohasher()
let coords1 = globalGeohasher.hash(now())
let coords2 = globalGeohasher.hash(now() - 1.days)

Error Handling

The library defines spesific exceptions types for different error conditions:

  • GeohashError: Base exception type for the library
  • DowDataError: Thrown when Dow Jones data cannot be retrieved. Inherits from GeohashError

Custom Dow Jones Provider (djia)

You can implement you own Dow Jones data source provider by inheriting from the DowJonesProvider strategy interface:

type MyCustomProvider = ref object of DowJonesProvider

Then implement getDowPrice for your custom provider:

method getDowPrice(provider: MyCustomProvider, date: DateTime): float =
    # Custom implementation here
    return 12345.67

A constructor might also be good to have depending on how data found :)

Then use it!

let customProvider: MyCustomProvider = newCustomProvider()
let customGeohasher: Geohasher = newGeohasher(45, -93, customProvider)
let customGlobalGeohasher: GlobalGeohasher = newGlobalGeohasher(customProvider)

See the librarys testing for an implementation of a mock dow jones data provider.

Types

DowDataError = object of GeohashError

Exception thrown when Dow Jones data cannot be retrieved.

This can happen due to network issues, invalid dates, or when all configured data sources fail.

DowJonesProvider = ref object of RootObj

Strategy Interface: Abstract base type for Dow Jones data providers.

Implement this to create custom data sources for Dow Jones prices. The default implementation fetches data from multiple HTTP sources with automatic failover.

Example:

type MyProvider = ref object of DowJonesProvider

method getDowPrice(provider: MyProvider, date: DateTime): float =
    # Custom implementation
    return myGetPrice(date)

Geohasher = object
  graticule*: Graticule      ## Target graticule for calculations
  dowProvider*: DowJonesProvider

Container for geohashing operations with configured data source.

Stores a graticule and Dow Jones provider for efficient repeated calculations within the same coordinate area.

Example:

let geohasher = newGeohasher(45, -93)
let coords1 = geohasher.hash(now())
let coords2 = geohasher.hash(now() - 1.days)

GeohashError = object of CatchableError
Base exception type for all geohashing-related errors.
GeohashResult = object
  latitude*: float           ## Final calculated latitude (Decimal coordinate)
  longitude*: float          ## Final calculated longitude (Decimal coordinate)
  usedDowDate*: DateTime     ## Dow Jones date that was actually used
  usedDate*: DateTime        ## Original target date for the calculation

Result of a geohash calculation containing the final coordinates and metadata about the calculation.

Example:

let result = xkcdgeohash(50.19, 6.83, now())
echo "Coords: ", result.latitude, ", ", result.longitude
echo "Used Dow date: ", result.usedDowDate.format("yyyy-MM-dd")
echo "Target date: ", result.usedDate.format("yyyy-MM-dd")

GlobalGeohasher = object
  dowProvider*: DowJonesProvider
Container for global geohashing operations with configured data source.
Graticule = object
  lat*: int                  ## Latitude: -90 to +90 (-0/+0 excluded) (minute or decimal coordinate)
  lon*: int                  ## Longitude: -179 to +179 (-0/+0 excluded) (minute or decimal coordinate)

Represents a graticule (integer coordinate area) for geohashing.

Note: The ambiguous -0/+0 distinction is excluded for simplicity.

Example:

let skanderborg: Graticule = Graticule(lat: 56, lon: 9)
let minneapolis: Graticule = Graticule(lat: 45, lon: -93)
let berlin: Graticule = Graticule(lat: 52, lon: 13)

HttpDowProvider = ref object of DowJonesProvider
  sources*: seq[string]      ## List of HTTP data source URLs
  currentSourceIndex*: int   ## Index of last successful source

HTTP-based Dow Jones provider with multiple source URLs and failover.

Automatically tries multiple data sources in order until one succeeds. Remembers which source last worked for improved performance.

Procs

proc `$`(geohasher: Geohasher): string {....raises: [], tags: [], forbids: [].}
Convert Geohasher to string representation.
proc `$`(geohashResult: GeohashResult): string {....raises: [], tags: [],
    forbids: [].}
Convert GeohashResult to string representation.
proc `$`(globalGeohasher: GlobalGeohasher): string {....raises: [], tags: [],
    forbids: [].}
Convert GlobalGeohasher to string representation.
proc `$`(graticule: Graticule): string {....raises: [], tags: [], forbids: [].}
Convert Graticule to string representation.
proc `<`(a, b: GeohashResult): bool {....raises: [], tags: [], forbids: [].}
Compare GeohashResult objects for ordering (date first, then coordinates).
proc `<`(a, b: GlobalGeohasher): bool {....raises: [], tags: [], forbids: [].}
Compare GlobalGeohasher objects for ordering (by provider).
proc `<`(a, b: Graticule): bool {....raises: [], tags: [], forbids: [].}
Compare Graticule objects for ordering (latitude first, then longitude).
proc `<=`(a, b: GeohashResult): bool {....raises: [], tags: [], forbids: [].}
Check if GeohashResult a is less than or equal to b.
proc `<=`(a, b: GlobalGeohasher): bool {....raises: [], tags: [], forbids: [].}
Check if GlobalGeohasher a is less than or equal to b.
proc `<=`(a, b: Graticule): bool {....raises: [], tags: [], forbids: [].}
Check if Graticule a is less than or equal to b.
proc `==`(a, b: Geohasher): bool {....raises: [], tags: [], forbids: [].}
Check equality between two Geohasher objects.
proc `==`(a, b: GeohashResult): bool {....raises: [], tags: [], forbids: [].}
Check equality between two GeohashResult objects.
proc `==`(a, b: GlobalGeohasher): bool {....raises: [], tags: [], forbids: [].}
Check equality between two GlobalGeohasher objects.
proc `==`(a, b: Graticule): bool {....raises: [], tags: [], forbids: [].}
Check equality between two Graticule objects.
proc getDefaultDowProvider(): HttpDowProvider {....raises: [], tags: [],
    forbids: [].}

Create a default HTTP-based Dow Jones provider.

Returns an HttpDowProvider configured with the standard geohashing data sources and automatic failover.

Returns: Configured HttpDowProvider ready for use

proc hash(geohasher: Geohasher; date: DateTime): GeohashResult {.
    ...raises: [Exception, ValueError], tags: [RootEffect], forbids: [].}

Calculate geohash coordinates for the specified date.

Performs the complete geohashing algorithm:

  1. Applies 30W timezone rule to determine Dow Jones date
  2. Retrieves Dow Jones opening price
  3. Generates and hashes the date-price string
  4. Converts hash to coordinate offsets
  5. Applies offsets to the graticule

Parameters:

  • geohasher: Configured Geohasher instance
  • date: Target date for coordinate calculation

Returns: GeohashResult with coordinates and metadata

Raises: DowDataError if Dow Jones data cannot be retrieved

Example:

let geohasher = newGeohasher(56, 9)
let result = geohasher.hash(now())

echo "Today's coordinates: ", result.latitude, ", ", result.longitude
echo "Used Dow date: ", result.usedDowDate.format("yyyy-MM-dd")

proc hash(globalGeohasher: GlobalGeohasher; date: DateTime): GeohashResult {.
    ...raises: [Exception, ValueError], tags: [RootEffect], forbids: [].}

Calculate the global geohash coordinates for the specified date.

Parameters:

  • globalGeohasher: Configured GlobalGeohasher instance
  • date: Target date for coordinate calculation

Returns: GeohashResult with coordinates and metadata

Raises: DowDataError if Dow Jones data cannot be retrieved

proc newGeohasher(latitude: int; longitude: int;
                  dowProvider: DowJonesProvider = getDefaultDowProvider()): Geohasher {.
    ...raises: [], tags: [], forbids: [].}

Create a new Geohasher for the specified graticule.

Parameters:

  • latitude: Integer latitude of the target graticule (-90 to +90)
  • longitude: Integer longitude of the target graticule (-179 to +179)
  • dowProvider: Optional custom Dow Jones data provider

Returns: Configured Geohasher ready for coordinate calculations

Example:

# Skanderborg area with default provider
let geohasher = newGeohasher(56, 9)

# Berlin with custom provider
let customProvider = getDefaultDowProvider()
let berlinHasher = newGeohasher(52, 13, customProvider)

proc newGlobalGeohasher(dowProvider: DowJonesProvider = getDefaultDowProvider()): GlobalGeohasher {.
    ...raises: [], tags: [], forbids: [].}

Create a new Geohasher for the specified graticule.

Parameters:

  • dowProvider: Optional custom Dow Jones data provider

Returns: Configured Geohasher ready for coordinate calculations

proc xkcdgeohash(latitude: float; longitude: float; date: DateTime;
                 dowProvider: DowJonesProvider = getDefaultDowProvider()): GeohashResult {.
    ...raises: [Exception, ValueError], tags: [RootEffect], forbids: [].}

Calculate geohash coordinates using the functional API.

This is a convenience function for one-off geohash calculations. It automatically creates a graticule from the provided coordinates and performs the complete geohashing algorithm.

Parameters:

  • latitude: Target latitude (will be truncated to integer for graticule)
  • longitude: Target longitude (will be truncated to integer for graticule)
  • date: Date for coordinate calculation
  • dowProvider: Optional custom Dow Jones data provider

Returns: GeohashResult with calculated coordinates and metadata

Raises: DowDataError if Dow Jones data cannot be retrieved

Example:

# Simple calculation for today
let result = xkcdgeohash(45.5, -93.7, now())

# Specific date with error handling
try:
    let coords = xkcdgeohash(52.0, 13.0, dateTime(2008, mMay, 21))
    echo "Coordinates: ", coords.latitude, ", ", coords.longitude
except DowDataError as e:
    echo "Failed to get data: ", e.msg

proc xkcdglobalgeohash(date: DateTime;
                       dowProvider: DowJonesProvider = getDefaultDowProvider()): GeohashResult {.
    ...raises: [Exception, ValueError], tags: [RootEffect], forbids: [].}

Calculate globaL geohash coordinates using the functional API.

It performs the complete geohashing algorithm and relates them to a point on the globe.

Parameters:

  • date: Date for coordinate calculation
  • dowProvider: Optional custom Dow Jones data provider

Returns: GeohashResult with calculated coordinates and metadata

Raises: DowDataError if Dow Jones data cannot be retrieved

Example:

# Simple calculation for today
let result = xkcdglobalgeohash(now())

# Specific date with error handling
try:
    let coords = xkcdglobalgeohash(dateTime(2008, mMay, 21))
    echo "Coordinates: ", coords.latitude, ", ", coords.longitude
except DowDataError as e:
    echo "Failed to get data: ", e.msg

Methods

method getDowPrice(provider: DowJonesProvider; date: DateTime): float {.base,
    ...raises: [CatchableError], tags: [], forbids: [].}

Retrieve the Dow Jones Industrial Average opening price for a specific date.

Base method - must be implemented by concrete provider types.

Parameters:

  • provider: The data provider instance
  • date: Date for which to retrieve the opening price

Returns: Opening price as a float (typically with 2 decimal places)

Raises:

  • DowDataError: When the price cannot be retrieved
  • CatchableError: Base implementation always raises this