Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/overlapping timespan check #36

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 14 additions & 5 deletions internal/pkg/values/layer.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import (
)

var (
errForceUpAndDownTime = errors.New("error: both forceUptime and forceDowntime are defined")
errUpAndDownTime = errors.New("error: both uptime and downtime are defined")
errTimeAndPeriod = errors.New("error: both a time and a period is defined")
errInvalidDownscaleReplicas = errors.New("error: downscale replicas value is invalid")
errValueNotSet = errors.New("error: no layer implements this value")
errForceUpAndDownTime = errors.New("error: both forceUptime and forceDowntime are defined")
errUpAndDownTime = errors.New("error: both uptime and downtime are defined")
errTimeAndPeriod = errors.New("error: both a time and a period is defined")
errInvalidDownscaleReplicas = errors.New("error: downscale replicas value is invalid")
errValueNotSet = errors.New("error: no layer implements this value")
errUpAndDownscaleOverlapping = errors.New("error: up- and downscale periods are overlapping")
)

const Undefined = -1 // Undefined represents an undefined integer value
Expand Down Expand Up @@ -82,6 +83,14 @@ func (l Layer) checkForIncompatibleFields() error {
(l.UpscalePeriod != nil || l.DownscalePeriod != nil) {
return errTimeAndPeriod
}
// up- and downscale periods overlapping
for _, upscale := range l.UpscalePeriod {
for _, downscale := range l.DownscalePeriod {
if doTimespansOverlap(upscale, downscale) {
return errUpAndDownscaleOverlapping
}
}
}
return nil
}

Expand Down
28 changes: 28 additions & 0 deletions internal/pkg/values/layer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,34 @@ func TestLayer_checkForIncompatibleFields(t *testing.T) {
},
wantErr: true,
},
{
name: "down- and upscale periods overlapping",
layer: Layer{
DownscalePeriod: timeSpans{absoluteTimeSpan{
from: time.Now(),
to: time.Now().Add(1 * time.Hour),
}},
UpscalePeriod: timeSpans{absoluteTimeSpan{ // overlapping
from: time.Now(),
to: time.Now().Add(1 * time.Hour),
}},
},
wantErr: true,
},
{
name: "down- and upscale do not overlap",
layer: Layer{
DownscalePeriod: timeSpans{absoluteTimeSpan{
from: time.Now(),
to: time.Now().Add(time.Hour),
}},
UpscalePeriod: timeSpans{absoluteTimeSpan{
from: time.Now().Add(2 * time.Hour),
to: time.Now().Add(3 * time.Hour),
}},
},
wantErr: false,
},
{
name: "valid",
layer: Layer{
Expand Down
113 changes: 112 additions & 1 deletion internal/pkg/values/timespan.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,48 @@ func (t relativeTimeSpan) isTimeOfDayInRange(timeOfDay time.Time) bool {
return (t.timeFrom.Before(timeOfDay) || t.timeFrom.Equal(timeOfDay)) && t.timeTo.After(timeOfDay)
}

// isTimeInSpan check if the time is in the span
// isTimeInSpan checks if the time is in the span
func (t relativeTimeSpan) isTimeInSpan(targetTime time.Time) bool {
targetTime = targetTime.In(t.timezone)
timeOfDay := getTimeOfDay(targetTime)
weekday := targetTime.Weekday()
return t.isTimeOfDayInRange(timeOfDay) && t.isWeekdayInRange(weekday)
}

// inLocation returns an array of relative timespans matching the timespan converted to the given location
func (t relativeTimeSpan) inLocation(timezone *time.Location) []relativeTimeSpan {
var result []relativeTimeSpan
sameDays := relativeTimeSpan{
timezone: timezone,
weekdayFrom: t.weekdayFrom,
weekdayTo: t.weekdayTo,
timeFrom: t.timeFrom.In(timezone),
timeTo: t.timeTo.In(timezone),
}
result = append(result, sameDays)
if isTimeFromSkippedToPreviousDay(sameDays.timeFrom) {
daysBefore := relativeTimeSpan{
timezone: timezone,
timeFrom: sameDays.timeFrom.Add(24 * time.Hour),
timeTo: sameDays.timeTo.Add(24 * time.Hour),
weekdayFrom: getWeekdayBefore(sameDays.weekdayFrom),
weekdayTo: getWeekdayBefore(sameDays.weekdayTo),
}
result = append(result, daysBefore)
}
if isTimeToSkippedToNextDay(sameDays.timeTo) {
daysAfter := relativeTimeSpan{
timezone: timezone,
timeFrom: sameDays.timeFrom.Add(-24 * time.Hour),
timeTo: sameDays.timeTo.Add(-24 * time.Hour),
weekdayFrom: getWeekdayAfter(sameDays.weekdayFrom),
weekdayTo: getWeekdayAfter(sameDays.weekdayTo),
}
result = append(result, daysAfter)
}
return result
}

type absoluteTimeSpan struct {
from time.Time
to time.Time
Expand Down Expand Up @@ -230,3 +264,80 @@ func getTimeOfDay(targetTime time.Time) time.Time {
targetTime.Location(),
)
}

// doTimespansOverlap checks if the given timespans overlap with each other
func doTimespansOverlap(span1, span2 TimeSpan) bool {
switch s1 := span1.(type) {
case absoluteTimeSpan:
if s2, ok := span2.(absoluteTimeSpan); ok {
return absAndAbsOverlap(s1, s2)
}
return relAndAbsOverlap(span2.(relativeTimeSpan), s1)
case relativeTimeSpan:
if s2, ok := span2.(absoluteTimeSpan); ok {
return relAndAbsOverlap(s1, s2)
}
return relAndRelOverlap(s1, span2.(relativeTimeSpan))
}
return false // this shouldn't ever be reached
}

// relAndRelOverlap checks if two relative timespans overlap
func relAndRelOverlap(rel1 relativeTimeSpan, rel2 relativeTimeSpan) bool {
_, tzOffset1 := rel1.timeFrom.Zone()
_, tzOffset2 := rel2.timeFrom.Zone()
if tzOffset1 == tzOffset2 { // optimized path for timespans with same timezone offset
return relAndRelSameTimezoneOverlap(rel1, rel2)
}

// slow path for timespans with different timezones
rel2List := rel2.inLocation(rel1.timezone)
for _, rel2 := range rel2List {
if relAndRelSameTimezoneOverlap(rel1, rel2) {
return true
}
}
return false
}

// relAndRelSameTimezoneOverlap checks if two relative timespans overlap, without converting them to the same timezone before
func relAndRelSameTimezoneOverlap(rel1 relativeTimeSpan, rel2 relativeTimeSpan) bool {
jonathan-mayer marked this conversation as resolved.
Show resolved Hide resolved
if rel1.timeFrom.After(rel1.timeTo) && rel2.timeFrom.After(rel2.timeTo) { // if both timespans are reversed, they both overlap anytime
return true
}
overlappingWeekdays := rel1.isWeekdayInRange(rel2.weekdayFrom) ||
rel1.isWeekdayInRange(rel2.weekdayTo) ||
rel2.isWeekdayInRange(rel1.weekdayFrom) ||
rel2.isWeekdayInRange(rel1.weekdayTo)
if !overlappingWeekdays {
return false
}
overlappingTimeOfDays := rel1.isTimeOfDayInRange(rel2.timeFrom) ||
rel1.isTimeOfDayInRange(asExclusiveTimestamp(rel2.timeTo)) ||
rel2.isTimeOfDayInRange(rel1.timeFrom) ||
rel2.isTimeOfDayInRange(asExclusiveTimestamp(rel1.timeTo))
return overlappingTimeOfDays
}

// relAndAbsOverlap checks if a relative and an aboslute timespan overlap
func relAndAbsOverlap(rel relativeTimeSpan, abs absoluteTimeSpan) bool {
abs.from = abs.from.In(rel.timezone)
abs.to = abs.to.In(rel.timezone)
if abs.to.Sub(abs.from) >= 7*24*time.Hour {
return true
}
if rel.isTimeInSpan(abs.from) || rel.isTimeInSpan(asExclusiveTimestamp(abs.to)) {
return true
}
if rel.weekdayFrom > rel.weekdayTo { // check if weekdays are in reverse
return abs.from.Weekday() > rel.weekdayFrom || abs.from.Weekday() < rel.weekdayTo ||
jonathan-mayer marked this conversation as resolved.
Show resolved Hide resolved
abs.to.Weekday() > rel.weekdayFrom || abs.to.Weekday() < rel.weekdayTo
}
return rel.weekdayFrom > abs.from.Weekday() && rel.weekdayFrom < abs.to.Weekday() ||
rel.weekdayTo > abs.from.Weekday() && rel.weekdayTo < abs.to.Weekday()
}

// absAndAbsOverlap checks if two absolute timespans overlap
func absAndAbsOverlap(abs1 absoluteTimeSpan, abs2 absoluteTimeSpan) bool {
return abs1.from.Before(abs2.to) && abs1.to.After(abs2.from)
}
Loading