I'm looking for a way to cache data using a map (or equivalent) but instead of a comparable key (like int
, string
, etc) it would be lower and upper time boundaries. I would then be able to perform a map lookup of a specific timestamp.
In SQL terms this is what I would want (note the exclusive upper bound). This can be GIST-indexed and it essentially becomes like a hash.
SELECT col_x FROM table_y WHERE NOW() <@ TSTZRANGE(start_date, end_date, '[)');
My absolutely basic solution right now consists of creating one map entry per minute-truncated timestamp, which works well but obviously generates a lot of highly redundant entries.
Alternatively I could use the lower boundary only as a key, but I'm not sure how to lookup the map for the "first match greater or equal to the key" (that's obviously contrary to the comparable requirement). Is this where slices would be more appropriate?
By the way I'm using int64
timestamps instead of time.Time
.
Look, i can suggest you a practical solution using an interval tree implementation. In my opinion, this is much more efficient than creating entries per minute, here's the code:
type TimeRange struct {
Start int64
End int64
}
type IntervalNode struct {
Range TimeRange
Value interface{}
Max int64
Left *IntervalNode
Right *IntervalNode
}
type IntervalMap struct {
root *IntervalNode
}
func NewIntervalMap() *IntervalMap {
return &IntervalMap{}
}
func (im *IntervalMap) Insert(start, end int64, value interface{}) {
if im.root == nil {
im.root = &IntervalNode{
Range: TimeRange{Start: start, End: end},
Value: value,
Max: end,
}
return
}
im.root = im.insert(im.root, TimeRange{Start: start, End: end}, value)
}
func (im *IntervalMap) insert(node *IntervalNode, r TimeRange, value interface{}) *IntervalNode {
if node == nil {
return &IntervalNode{Range: r, Value: value, Max: r.End}
}
if r.Start < node.Range.Start {
node.Left = im.insert(node.Left, r, value)
} else {
node.Right = im.insert(node.Right, r, value)
}
if node.Max < r.End {
node.Max = r.End
}
return node
}
func (im *IntervalMap) Find(timestamp int64) interface{} {
if im.root == nil {
return nil
}
return im.search(im.root, timestamp)
}
func (im *IntervalMap) search(node *IntervalNode, timestamp int64) interface{} {
if node == nil {
return nil
}
// Check if current node contains the timestamp
if timestamp >= node.Range.Start && timestamp < node.Range.End {
return node.Value
}
// Search left subtree if it could contain our timestamp
if node.Left != nil && timestamp < node.Range.Start {
return im.search(node.Left, timestamp)
}
// Search right subtree
return im.search(node.Right, timestamp)
}
How we can use it?
timeMap := NewIntervalMap()
// Insert some ranges
timeMap.Insert(
time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC).Unix(),
time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC).Unix(),
"value1",
)
// Look up a timestamp
now := time.Now().Unix()
value := timeMap.Find(now)
It is beneficial because you'll get O(log n) lookup time, no redundant entries, it handles overlapping ranges correctly and it's memory efficient