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:
- http://carabiner.peeron.com/xkcd/map/data/
- http://geo.crox.net/djia/
- http://www1.geo.crox.net/djia/
- http://www2.geo.crox.net/djia/
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 `<`(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: 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: 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 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:
- Applies 30W timezone rule to determine Dow Jones date
- Retrieves Dow Jones opening price
- Generates and hashes the date-price string
- Converts hash to coordinate offsets
- 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