A high-performance, zero-allocation Go parser and interval-math evaluator for OpenStreetMap opening_hours specifications.
-
$O(1)$ Hardware-Accelerated Bitmask Table: EvaluatesIsOpenin ~4.5 nanoseconds (>220 Million ops/sec) via embedded[158]uint64scalar bit tests. -
Zero-Allocation Interval Math: Zero heap allocations on query paths (
IsOpen,TimeToOpen,TimeToOpenForDuration,WhenTime,NextDur,NextDate). - Thread-Safe & Lock-Free Interning: Automatic lock-free interning and caching of parsed expressions.
-
Overnight Shifts: Full support for shifts spanning midnight (e.g.
Mo 22:00-04:00,Su 22:00-04:00). -
Overrides & Exclusions: Handles
off/closedrules overriding previous rules (e.g.Mo-Su 00:00-24:00; Tu 12:00-13:00 off). -
Duration Availability: Find wait times for contiguous tasks of duration
$D$ (GetTimeToOpenForDuration/When). -
Standard JSON Support: Native
json.Marshalerandjson.Unmarshalerimplementations. -
Reflection-free JSON Decode: A dependency-free
DecodeJSON([]byte)entry point that decodes a JSON string in ~26 ns with zero allocations (*OpeningHoursis the shared interned instance) — no runtime reflection, no third-party modules.
The Go implementation leverages specialized low-level and compiler-aware optimizations to achieve sub-10ns evaluations and zero GC overhead:
-
Dual State Representation:
-
Disjoint Interval Table (
[]TimeWindow): Minute intervals within the week[0, 10080)stored as{Start, End int}. Intervals are sorted and disjoint. -
Hardware Bitmask Table (
[158]uint64): A 10,080-bit packed bitmask where bit$i$ represents minute$i$ of the week. Point-in-time checks (IsOpen) execute in$O(1)$ via scalar bit tests:(bitmask[weekMin >> 6] & (1 << (weekMin & 63))) != 0.
-
Disjoint Interval Table (
-
$O(\log N)$ Binary Search with Small-Window Unrolling:- Interval-based lookups (
GetTimeToOpen,When,NextDur) utilize binary search over the sortedwindowsarray. For short schedules ($N \le 3$ ), branches are unrolled directly. - Includes compiler Bounds Check Elimination (BCE) hints (
_ = windows[n-1]) to eliminate slice bounds checking inside hot search loops.
- Interval-based lookups (
-
Two-Tier Lock-Free Caching Hierarchy:
-
L1 Atomic Slot: An
atomic.Pointer[parseSlot]holds the most recently accessed expression. Consecutive lookups on the same expression bypass lock contention and hash table lookups completely. -
L2 Concurrent Intern Pool: Backed by
sync.RWMutexandmap[string]*OpeningHoursfor deduplicating parsed expressions globally.
-
L1 Atomic Slot: An
-
Reflection-Free JSON Fast Path:
-
DecodeJSON([]byte)directly scans quotes, validates escapes, and resolves directly to the interned*OpeningHoursinstance viastringEqualsBytesandunsafe.StringData, eliminating runtime reflection and allocations.
-
-
Zero-Allocation Stack Parsing:
- Parsing uses stack-allocated buffers (
[8]openingRule,[4]timeRange) and ASCII byte scanning, completely avoiding heap allocations during rule tokenization.
- Parsing uses stack-allocated buffers (
go get github.com/chneau/openhours/v2package main
import (
"fmt"
"time"
"github.com/chneau/openhours/v2"
)
func main() {
// 1. Parse an OSM opening_hours string
oh := openhours.Parse("Mo-Fr 08:00-12:00, 13:00-17:00; Sa 08:00-12:00")
monday10am := time.Date(2026, 5, 18, 10, 0, 0, 0, time.UTC)
// 2. Fast point-in-time check (4.5 ns/op)
isOpen := oh.IsOpen(monday10am) // true
fmt.Println("Is open:", isOpen)
// 3. Current shift end
if shiftEnd := oh.GetCurrentShiftEnd(monday10am); shiftEnd != nil {
fmt.Println("Current shift ends at:", *shiftEnd) // 2026-05-18 12:00:00 UTC
}
// 4. Time to next open
tuesdayLunch := time.Date(2026, 5, 19, 12, 30, 0, 0, time.UTC)
timeToOpen := oh.GetTimeToOpen(tuesdayLunch) // 30m (opens at 13:00)
fmt.Println("Opens in:", *timeToOpen)
// 5. Find when a 3-hour job can be serviced
waitFor3h := oh.GetTimeToOpenForDuration(tuesdayLunch, 3*time.Hour)
whenCanStart := oh.When(tuesdayLunch, 3*time.Hour) // 2026-05-19 13:00:00 UTC
fmt.Println("Wait for 3h slot:", *waitFor3h, "Starts at:", *whenCanStart)
// 6. Next state transitions
isOpenNow, durationRemaining := oh.NextDur(monday10am)
_, nextTransitionDate := oh.NextDate(monday10am) // 2026-05-18 12:00:00 UTC
fmt.Println("Open now:", isOpenNow, "Remaining:", durationRemaining, "Next date:", nextTransitionDate)
}OpeningHours serializes to a JSON string of the expression. Use DecodeJSON for a dependency-free, reflection-free decode that returns the shared interned *OpeningHours (~26 ns, zero allocations):
oh, err := openhours.DecodeJSON([]byte(`"Mo-Fr 08:00-12:00, 13:00-17:00"`))
if err != nil {
log.Fatal(err)
}
fmt.Println(oh.IsOpen(monday10am))| # | Workload | Calls | Latency / Op | Throughput |
|---|---|---|---|---|
| 1 | IsOpen (Pure call) |
1,000,000 | 4.5 ns | 220,000,000 ops/sec |
| 2 | TimeToOpen (Zero-alloc) |
10,000 | 9.5 ns | 105,000,000 ops/sec |
| 3 | Parse (Interned / Cached) |
1,000 | 14.5 ns | 69,000,000 ops/sec |
| 4 | When |
10,000 | 23.0 ns | 43,000,000 ops/sec |
| 5 | NextDur |
10,000 | 25.5 ns | 39,000,000 ops/sec |
| 6 | TimeToOpenForDuration (Zero-alloc) |
10,000 | 27.8 ns | 36,000,000 ops/sec |
| 7 | GetTimeToOpen |
10,000 | 35.3 ns | 28,000,000 ops/sec |
| 8 | NextDate |
10,000 | 39.8 ns | 25,000,000 ops/sec |
| 9 | GetTimeToOpenForDuration |
10,000 | 47.5 ns | 21,000,000 ops/sec |
| 10 | Parse (Uncached) |
1,000 | 345.5 ns | 2,900,000 ops/sec |
# Run all unit tests
go test ./...
# Run tests with race detector and verbose output
go test -v -race ./...
# Run tests with statement coverage report
go test -cover ./...
# Run benchmarks with memory allocation metrics
go test -benchmem -bench=. ./...
# Run linter
golangci-lint run ./...MIT License. Copyright (c) 2026 chneau.