Skip to content

Commit

Permalink
cal: add optional IsHoliday caching
Browse files Browse the repository at this point in the history
Added Cacheable flag to Calendar.
When enabled, IsHoliday checks are cached and execute ~50x faster
for repeated calls.

Fixes #81
  • Loading branch information
rickar committed Nov 18, 2021
1 parent 48dad1c commit 90b7ddf
Show file tree
Hide file tree
Showing 2 changed files with 74 additions and 9 deletions.
57 changes: 54 additions & 3 deletions v2/cal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
//
Expand Down Expand Up @@ -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()
Expand All @@ -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
}
}
}
}
26 changes: 20 additions & 6 deletions v2/cal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down

0 comments on commit 90b7ddf

Please sign in to comment.