diff --git a/README.md b/README.md index 3116ba9..4213e8e 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,9 @@ This library operates off Period interfaces, which contain the following: ``` type Period interface { - GetStartTime() time.Time - GetEndTime() time.Time - GetIdentifier() string + GetStartTime() time.Time // Inclusive start time + GetEndTime() time.Time // Exclusive end time ("expiration time") + GetIdentifier() string } ``` diff --git a/msp/changeover.go b/msp/changeover.go index 268e3dc..24a735a 100644 --- a/msp/changeover.go +++ b/msp/changeover.go @@ -44,7 +44,7 @@ func GetNextChangeOver(t time.Time, periods ...Period) (ts time.Time, err error) return ts, nil } } - return time.Unix(0, 0), ErrNoNextChangeover + return time.Time{}, ErrNoNextChangeover } func FlattenPeriods(periods ...Period) (ids []string) { diff --git a/msp/changeover_test.go b/msp/changeover_test.go new file mode 100644 index 0000000..25fcf7b --- /dev/null +++ b/msp/changeover_test.go @@ -0,0 +1,304 @@ +package msp + +import ( + "fmt" + "testing" + "time" +) + +func slicesEqual[K comparable](x []K, y []K) bool { + if len(x) != len(y) { + return false + } + for i, w := range x { + if w != y[i] { + return false + } + } + return true +} + +func TestGetChangeOvers(t *testing.T) { + // use a static timestamp to make sure tests don't fail on slower systems or during a process pause + now := time.Now() + testCases := []struct { + ts time.Time + testID string + result []time.Time + periods []Period + }{{testID: "No choices", + ts: now, + result: []time.Time{}, + periods: []Period{}}, + {testID: "Two Choices, shorter is second", + ts: now, + result: []time.Time{now.Add(-5 * time.Minute), now.Add(-2 * time.Minute), now.Add(time.Minute)}, + periods: []Period{TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, one is a year, other a minute", + ts: now, + result: []time.Time{now.Add(-1 * time.Hour * 24 * 365), now.Add(-5 * time.Minute), now.Add(time.Minute)}, + periods: []Period{TimeWindow{StartTime: now.Add(-1 * time.Hour * 24 * 365), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + + {testID: "Two Choices, shorter is first", + ts: now, + result: []time.Time{now.Add(-5 * time.Minute), now.Add(-2 * time.Minute), now.Add(time.Minute)}, + periods: []Period{TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, one in the past", + ts: now, + result: []time.Time{now.Add(-2 * time.Minute), now.Add(-time.Minute), now.Add(time.Minute)}, + periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(-time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, one invalid", + ts: now, + result: []time.Time{now.Add(-2 * time.Minute), now.Add(time.Minute)}, + periods: []Period{TimeWindow{StartTime: now.Add(time.Minute), + EndTime: now.Add(-time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, Identical periods", + ts: now, + result: []time.Time{now.Add(-time.Minute), now.Add(time.Minute)}, + periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "One choice", + ts: now, + result: []time.Time{now.Add(-time.Minute), now.Add(time.Minute)}, + periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}}}} + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s", tc.testID), func(t *testing.T) { + changeovers := GetChangeOvers(tc.periods...) + if !slicesEqual(changeovers, tc.result) { + t.Errorf("Expected %v but got %v", tc.result, changeovers) + } + + }) + } +} +func TestFlattenPeriods(t *testing.T) { + // use a static timestamp to make sure tests don't fail on slower systems or during a process pause + now := time.Now() + testCases := []struct { + ts time.Time + testID string + result []string + err error + periods []Period + }{{testID: "No choices", + ts: now, + result: []string{}, + err: ErrNoValidPeriods, + periods: []Period{}}, + {testID: "Two Choices, shorter is second", + ts: now, + result: []string{"A", "B"}, + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, one is a year, other a minute", + ts: now, + result: []string{"A", "B"}, + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-1 * time.Hour * 24 * 365), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + + {testID: "Two Choices, shorter is first", + ts: now, + result: []string{"B", "A"}, + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, one in the past", + ts: now, + result: []string{"B", "A"}, + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(-time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, one invalid", + ts: now, + result: []string{"B"}, + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(time.Minute), + EndTime: now.Add(-time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, Identical periods", + ts: now, + result: []string{"B"}, + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Triple Nested Periods", + ts: now, + result: []string{"A", "B", "C", "B", "A"}, + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-15 * time.Minute), + EndTime: now.Add(15 * time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(5 * time.Minute), + Identifier: "C"}, + TimeWindow{StartTime: now.Add(-10 * time.Minute), + EndTime: now.Add(10 * time.Minute), + Identifier: "B"}}}, + {testID: "One choice", + ts: now, + result: []string{"A"}, + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}}}} + + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s", tc.testID), func(t *testing.T) { + changeovers := FlattenPeriods(tc.periods...) + if !slicesEqual(changeovers, tc.result) { + t.Errorf("Expected %v but got %v", tc.result, changeovers) + } + + }) + } +} +func TestGetNextChangeOver(t *testing.T) { + // use a static timestamp to make sure tests don't fail on slower systems or during a process pause + now := time.Now() + testCases := []struct { + ts time.Time + testID string + result time.Time + err error + periods []Period + }{{testID: "No choices", + ts: now, + result: time.Time{}, + err: ErrNoNextChangeover, + periods: []Period{}}, + {testID: "Two Choices, shorter is second", + ts: now, + result: now.Add(time.Minute), + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, one is a year, other a minute", + ts: now, + result: now.Add(time.Minute), + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-1 * time.Hour * 24 * 365), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + + {testID: "Two Choices, shorter is first", + ts: now, + result: now.Add(time.Minute), + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-5 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, one in the past", + ts: now, + result: now.Add(time.Minute), + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(-time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, one invalid", + ts: now, + result: now.Add(time.Minute), + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(time.Minute), + EndTime: now.Add(-time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-2 * time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "Two Choices, Identical periods", + ts: now, + result: now.Add(time.Minute), + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}, + TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "B"}}}, + {testID: "One choice", + ts: now, + result: now.Add(time.Minute), + err: nil, + periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute), + EndTime: now.Add(time.Minute), + Identifier: "A"}}}} + for _, tc := range testCases { + t.Run(fmt.Sprintf("%s", tc.testID), func(t *testing.T) { + ts, err := GetNextChangeOver(now, tc.periods...) + if tc.err != err { + t.Errorf("Error %v does not match expected %v", tc.err, err) + } + if ts != tc.result { + t.Errorf("Got %v but expected %v", ts, tc.result) + } + + }) + } +} diff --git a/msp/errors.go b/msp/errors.go index ba5aa9b..3733175 100644 --- a/msp/errors.go +++ b/msp/errors.go @@ -10,5 +10,5 @@ var ( // ErrNoValidPeriods occurs when an empty set of periods is passed or when ll periods are invalid ErrNoValidPeriods = errors.New("error: no valid periods available") // ErrNoNextChangeover occurs when GetNextChangeover is called but there are no changeovers after t - ErrNoNextChangeover = errors.New("error: no valid periods available") + ErrNoNextChangeover = errors.New("error: no valid changeovers available") ) diff --git a/msp/msp.go b/msp/msp.go index 8f9e7d9..93d7b50 100644 --- a/msp/msp.go +++ b/msp/msp.go @@ -61,7 +61,9 @@ func GetDuration(start time.Time, end time.Time) (dur time.Duration, err error) func ValidTimePeriods(ts time.Time, periods ...Period) []Period { var valid []Period for _, p := range periods { - if p.GetStartTime().Before(ts) && p.GetEndTime().After(ts) { + start := p.GetStartTime() + end := p.GetEndTime() + if (start.Before(ts) || start.Equal(ts)) && (end.After(ts)) { valid = append(valid, p) } }