From 90b7ddf4cc5759325f4dc023193c808f83a8ce3f Mon Sep 17 00:00:00 2001 From: Rick Arnold Date: Wed, 17 Nov 2021 21:53:49 -0500 Subject: [PATCH] cal: add optional IsHoliday caching Added Cacheable flag to Calendar. When enabled, IsHoliday checks are cached and execute ~50x faster for repeated calls. Fixes #81 --- v2/cal.go | 57 +++++++++++++++++++++++++++++++++++++++++++++++--- v2/cal_test.go | 26 +++++++++++++++++------ 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/v2/cal.go b/v2/cal.go index 1851185..bd7293e 100644 --- a/v2/cal.go +++ b/v2/cal.go @@ -8,14 +8,38 @@ import "time" // require a full time.Time value. var DefaultLoc = time.Local +// CacheMaxSize is the maximum number of items that can be stored in the cache +var CacheMaxSize = 365 * 3 + +// CacheEvictSize is the number of items to evict from cache when it is full +var CacheEvictSize = 30 + // Calendar represents a basic yearly calendar with a list of holidays. type Calendar struct { Name string // calendar short name Description string // calendar description Locations []*time.Location // locations where the calendar applies Holidays []*Holiday // applicable holidays for this calendar + Cacheable bool // indicates that holiday calcs can be cached (don't change holiday defs while enabled) + + isHolCache map[holCacheKey]*holCacheEntry // cached results for IsHoliday +} + +type holCacheKey struct { + year int + month time.Month + day int } +type holCacheEntry struct { + act bool + obs bool + hol *Holiday +} + +// shared entry to avoid repeated allocations for "false" entries +var holFalseEntry *holCacheEntry = &holCacheEntry{act: false, obs: false, hol: nil} + // IsApplicable reports whether the calendar is applicable for the given // location. // @@ -50,12 +74,17 @@ func (c *Calendar) IsHoliday(date time.Time) (actual, observed bool, h *Holiday) } year, month, day := date.Date() - for _, hol := range c.Holidays { - if hol.Month != 0 && hol.Month != month { - continue + if c.Cacheable { + if c.isHolCache == nil { + c.isHolCache = make(map[holCacheKey]*holCacheEntry) } + if v, ok := c.isHolCache[holCacheKey{year: year, month: month, day: day}]; ok { + return v.act, v.obs, v.hol + } + } + for _, hol := range c.Holidays { act, obs := hol.Calc(year) actMatch := !act.IsZero() @@ -69,9 +98,31 @@ func (c *Calendar) IsHoliday(date time.Time) (actual, observed bool, h *Holiday) obsMatch = obsMonth == month && obsDay == day } if actMatch || obsMatch { + if c.Cacheable { + c.evict() + c.isHolCache[holCacheKey{year: year, month: month, day: day}] = + &holCacheEntry{act: actMatch, obs: obsMatch, hol: hol} + } return actMatch, obsMatch, hol } } + if c.Cacheable { + c.evict() + c.isHolCache[holCacheKey{year: year, month: month, day: day}] = holFalseEntry + } return false, false, nil } + +func (c *Calendar) evict() { + if len(c.isHolCache) >= CacheMaxSize { + n := 0 + for k := range c.isHolCache { + delete(c.isHolCache, k) + n++ + if n >= CacheEvictSize { + break + } + } + } +} diff --git a/v2/cal_test.go b/v2/cal_test.go index c8973b5..b05995a 100644 --- a/v2/cal_test.go +++ b/v2/cal_test.go @@ -54,26 +54,40 @@ func TestIsHoliday(t *testing.T) { Func: CalcDayOfMonth, } + cachedCalendar := &Calendar{ + Holidays: []*Holiday{hol}, + Cacheable: true, + } + CacheMaxSize = 2 + CacheEvictSize = 1 + tests := []struct { - c Calendar + c *Calendar date time.Time wantAct bool wantObs bool wantHol *Holiday }{ - {Calendar{}, time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC), false, false, nil}, - {Calendar{Holidays: []*Holiday{hol}, + {&Calendar{}, time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC), false, false, nil}, + {&Calendar{Holidays: []*Holiday{hol}, Locations: []*time.Location{zone1, zone2}}, time.Date(2015, 7, 4, 12, 30, 0, 0, time.UTC), false, false, nil}, - {Calendar{Holidays: []*Holiday{hol}, + {&Calendar{Holidays: []*Holiday{hol}, Locations: []*time.Location{zone1, zone2}}, time.Date(2015, 7, 4, 12, 30, 0, 0, zone1), true, false, hol}, - {Calendar{Holidays: []*Holiday{hol}, + {&Calendar{Holidays: []*Holiday{hol}, Locations: []*time.Location{zone1, zone2}}, time.Date(2015, 7, 3, 12, 30, 0, 0, zone2), false, true, hol}, - {Calendar{Holidays: []*Holiday{hol}, + {&Calendar{Holidays: []*Holiday{hol}, Locations: []*time.Location{zone1, zone2}}, time.Date(2015, 8, 4, 12, 30, 0, 0, zone2), false, false, nil}, + + {cachedCalendar, time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC), false, false, nil}, + {cachedCalendar, time.Date(2000, 1, 1, 12, 30, 0, 0, time.UTC), false, false, nil}, + {cachedCalendar, time.Date(2015, 7, 4, 12, 30, 0, 0, time.UTC), true, false, hol}, + {cachedCalendar, time.Date(2015, 7, 4, 12, 30, 0, 0, time.UTC), true, false, hol}, + {cachedCalendar, time.Date(2015, 7, 3, 12, 30, 0, 0, time.UTC), false, true, hol}, + {cachedCalendar, time.Date(2015, 7, 3, 12, 30, 0, 0, time.UTC), false, true, hol}, } for i, test := range tests {