gogo-map

Golang map equivalent to postgres tstzrange and contains operator?


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.


Solution

  • 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