Skip to content

Commit

Permalink
Merge pull request #46 from tjamet/master
Browse files Browse the repository at this point in the history
Add support for office hours
  • Loading branch information
rickar authored Apr 17, 2020
2 parents d3ac514 + cfb0bba commit 7864a4b
Show file tree
Hide file tree
Showing 3 changed files with 221 additions and 1 deletion.
108 changes: 108 additions & 0 deletions cal.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import (
"time"
)

const (
dayStart = 9
dayEnd = 17
)

// IsWeekend reports whether the given date falls on a weekend.
func IsWeekend(date time.Time) bool {
day := date.Weekday()
Expand Down Expand Up @@ -100,6 +105,8 @@ type WorkdayFn func(date time.Time) bool
type Calendar struct {
holidays [13][]Holiday // 0 for offset based holidays, 1-12 for month based
workday [7]bool // flags to indicate a day of the week is a workday
dayStart time.Duration // the time offset at which the workdays starts
dayEnd time.Duration // the time offset at which workdays end
WorkdayFunc WorkdayFn // optional function to override workday flags
Observed ObservedRule
}
Expand All @@ -116,6 +123,8 @@ func NewCalendar() *Calendar {
c.workday[time.Wednesday] = true
c.workday[time.Thursday] = true
c.workday[time.Friday] = true
c.dayStart = time.Duration(dayStart * time.Hour)
c.dayEnd = time.Duration(dayEnd * time.Hour)
return c
}

Expand Down Expand Up @@ -317,6 +326,105 @@ func (c *Calendar) CountWorkdays(start, end time.Time) int64 {
return int64(factor * result)
}

func maxTime(ts ...time.Time) time.Time {
r := time.Time{}
for _, t := range ts {
if t.After(r) {
r = t
}
}
return r
}

func minTime(ts ...time.Time) time.Time {
if len(ts) == 0 {
return time.Time{}
}
r := ts[0]
for _, t := range ts {
if t.Before(r) {
r = t
}
}
return r
}

// DailyWorkedTime returns the total time worked per day
// it makes it easy to compute working times per day
// allowing calls like c.AddWorkHours(time.Now(), 8 * c.DailyWorkedTime())
func (c *Calendar) DailyWorkedTime() time.Duration {
return c.dayEnd - c.dayStart
}

// StartWorkTime returns the time at which work starts in the current day
func (c *Calendar) StartWorkTime(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0,
0, t.Location()).Add(c.dayStart)

}

// EndWorkTime returns the time at which work ends in the current day
func (c *Calendar) EndWorkTime(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0,
0, t.Location()).Add(c.dayEnd)

}

// NextWorkStart determines what will be the next future time work will start
func (c *Calendar) NextWorkStart(t time.Time) time.Time {
start := c.StartWorkTime(t)
for !c.IsWorkday(start) || t.After(start) {
start = start.Add(24 * time.Hour)
}
return start
}

// CountWorkHours counts the actual number of worked hours between 2 different times
func (c *Calendar) CountWorkHours(start, end time.Time) time.Duration {
r := time.Duration(0)
if end.Before(start) {
start, end = end, start
}
current := maxTime(start, c.StartWorkTime(start))
if current.After(c.EndWorkTime(start)) {
current = c.NextWorkStart(start)
}
for current.Before(end) {
lastTimeInDay := minTime(c.EndWorkTime(current), end)
r += lastTimeInDay.Sub(current)
current = c.NextWorkStart(lastTimeInDay)
}
return r
}

// AddWorkHours determines the time in the future where the worked hours will be completed
func (c *Calendar) AddWorkHours(t time.Time, worked time.Duration) time.Time {
wStart := maxTime(t, c.StartWorkTime(t))
for !c.IsWorkday(wStart) {
wStart = c.NextWorkStart(wStart)
}
for worked > 0 {

t = minTime(wStart.Add(worked), c.EndWorkTime(wStart))
worked -= c.CountWorkHours(wStart, t)

wStart = c.NextWorkStart(t)
}
return t
}

// SetWorkingHours configures the calendar to override the default 9-17 working hours
func (c *Calendar) SetWorkingHours(start time.Duration, end time.Duration) {
if start > end {
// This should not really happen, but SetWorkingHours(18*time.Hour, 9*time.Hour) should also mean a 9-18 time range
c.dayStart = end
c.dayEnd = start
} else {
c.dayStart = start
c.dayEnd = end
}
}

// AddSkipNonWorkdays returns start time plus d working duration
func (c *Calendar) AddSkipNonWorkdays(start time.Time, d time.Duration) time.Time {
const day = 24 * time.Hour
Expand Down
112 changes: 112 additions & 0 deletions cal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -797,3 +797,115 @@ func TestCalendar_WorkdaysNrInRangeAustralia(t *testing.T) {
})
}
}

func TestStartWorkTime(t *testing.T) {
c := NewCalendar()
got := c.StartWorkTime(time.Date(2020, 04, 15, 01, 20, 0, 0, time.UTC))
expected := time.Date(2020, 04, 15, dayStart, 0, 0, 0, time.UTC)
if got != expected {
t.Errorf("Calendar.StartWorkTime() = %v, want %v", got, expected)
}
}

func TestEndWorkTime(t *testing.T) {
c := NewCalendar()
got := c.EndWorkTime(time.Date(2020, 04, 15, 01, 20, 0, 0, time.UTC))
expected := time.Date(2020, 04, 15, dayEnd, 0, 0, 0, time.UTC)
if got != expected {
t.Errorf("Calendar.EndWorkTime() = %v, want %v", got, expected)
}
}

func TestNextWorkStart(t *testing.T) {
c := NewCalendar()
got := c.NextWorkStart(time.Date(2020, 04, 15, 01, 20, 0, 0, time.UTC))
expected := time.Date(2020, 04, 15, dayStart, 0, 0, 0, time.UTC)
if got != expected {
t.Errorf("Calendar.NextWorkStart() = %v, want %v", got, expected)
}
got = c.NextWorkStart(time.Date(2020, 04, 15, 10, 20, 0, 0, time.UTC))
expected = time.Date(2020, 04, 16, dayStart, 0, 0, 0, time.UTC)
if got != expected {
t.Errorf("Calendar.NextWorkStart() = %v, want %v", got, expected)
}
got = c.NextWorkStart(time.Date(2020, 04, 15, 21, 20, 0, 0, time.UTC))
expected = time.Date(2020, 04, 16, dayStart, 0, 0, 0, time.UTC)
if got != expected {
t.Errorf("Calendar.NextWorkStart() = %v, want %v", got, expected)
}
got = c.NextWorkStart(time.Date(2020, 04, 18, 21, 20, 0, 0, time.UTC))
expected = time.Date(2020, 04, 20, dayStart, 0, 0, 0, time.UTC)
if got != expected {
t.Errorf("Calendar.NextWorkStart() = %v, want %v", got, expected)
}
}

func TestWorkedHours(t *testing.T) {
c := NewCalendar()

got := c.CountWorkHours(time.Now(), time.Now().Add(7*24*time.Hour))
expected := time.Duration(5 * c.DailyWorkedTime())
if got != expected {
t.Errorf("Calendar.CountWorkHours() = %v, want %v", got, expected)
}

got = c.CountWorkHours(time.Date(2020, 01, 01, 0, 0, 0, 0, time.UTC), time.Date(2019, 01, 01, 0, 0, 0, 0, time.UTC))
expected = time.Duration(261 * c.DailyWorkedTime())
if got != expected {
t.Errorf("Calendar.CountWorkHours() = %v, want %v", got, expected)
}

loc, err := time.LoadLocation("Europe/Madrid")
if err != nil {
t.Errorf("failed to load GMT+1 location: %v", err)
}

got = c.CountWorkHours(time.Date(2020, 4, 15, dayStart+2, 0, 0, 0, time.UTC), time.Date(2020, 4, 15, dayStart+2, 0, 0, 0, loc))
// In april in Spain, there are 2 hours difference with Coordinated Universal Time
expected = time.Duration(2 * time.Hour)
if got != expected {
t.Errorf("Calendar.CountWorkHours() = %v, want %v", got, expected)
}

c.SetWorkingHours(2*time.Hour, 4*time.Hour) // night shift
got = c.CountWorkHours(time.Date(2020, 03, 29, 2, 0, 0, 0, loc), time.Date(2020, 03, 29, 4, 0, 0, 0, loc))
// 2020/03/29 is the daylight saving date in 2020 for Spain
expected = time.Duration(1 * time.Hour)
if got != expected {
t.Errorf("Calendar.CountWorkHours() = %v, want %v", got, expected)
}

}

func TestAddWorkedHours(t *testing.T) {
c := NewCalendar()

got := c.AddWorkHours(time.Date(2020, 04, 15, 0, 0, 0, 0, time.UTC), 5*c.DailyWorkedTime())
expected := time.Date(2020, 04, 21, dayEnd, 0, 0, 0, time.UTC)
if got != expected {
t.Errorf("Calendar.AddWorkHours() = %v, want %v", got, expected)
}

got = c.AddWorkHours(time.Date(2020, 04, dayEnd+1, 3, 0, 0, 0, time.UTC), c.DailyWorkedTime())
expected = time.Date(2020, 04, 20, dayEnd, 0, 0, 0, time.UTC)
if got != expected {
t.Errorf("Calendar.AddWorkHours() = %v, want %v", got, expected)
}

c.SetWorkingHours(9*time.Hour, 18*time.Hour)

got = c.AddWorkHours(time.Date(2020, 04, 15, 3, 0, 0, 0, time.UTC), 10*time.Hour)
expected = time.Date(2020, 04, 16, 10, 0, 0, 0, time.UTC)
if got != expected {
t.Errorf("Calendar.AddWorkHours() = %v, want %v", got, expected)
}
}

func TestDailyWorkedTime(t *testing.T) {
got := NewCalendar().DailyWorkedTime()
expected := 8 * time.Hour
if got != expected {
t.Errorf("Calendar.DailyWorkedTime() = %v, want %v", got, expected)
}

}
2 changes: 1 addition & 1 deletion holiday_defs_sk_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"time"
)

func TestSlovakiaHolidays(t *testing.T) {
func TestSlovakHolidays(t *testing.T) {
c := NewCalendar()
c.Observed = ObservedExact
AddSlovakHolidays(c)
Expand Down

0 comments on commit 7864a4b

Please sign in to comment.