mirror of
https://github.com/taigrr/most-specific-period.git
synced 2026-04-02 03:38:41 -07:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
88ce350a61
|
|||
|
0f88864200
|
|||
|
71bb0676c0
|
@@ -12,9 +12,9 @@ This library operates off Period interfaces, which contain the following:
|
|||||||
|
|
||||||
```
|
```
|
||||||
type Period interface {
|
type Period interface {
|
||||||
GetStartTime() time.Time
|
GetStartTime() time.Time // Inclusive start time
|
||||||
GetEndTime() time.Time
|
GetEndTime() time.Time // Exclusive end time ("expiration time")
|
||||||
GetIdentifier() string
|
GetIdentifier() string
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,3 +1,3 @@
|
|||||||
module github.com/taigrr/most-specific-period
|
module github.com/taigrr/most-specific-period
|
||||||
|
|
||||||
go 1.17
|
go 1.18
|
||||||
|
|||||||
60
msp/changeover.go
Normal file
60
msp/changeover.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package msp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetChangeOvers(periods ...Period) (changeovers []time.Time) {
|
||||||
|
timeStamps := []time.Time{}
|
||||||
|
for _, x := range periods {
|
||||||
|
timeStamps = append(timeStamps, x.GetEndTime())
|
||||||
|
timeStamps = append(timeStamps, x.GetStartTime())
|
||||||
|
}
|
||||||
|
if len(timeStamps) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sort.Slice(timeStamps, func(i, j int) bool {
|
||||||
|
return timeStamps[i].Before(timeStamps[j])
|
||||||
|
})
|
||||||
|
// timeStamps is sorted, so this will always result in an unused time
|
||||||
|
// struct, as it's before the first
|
||||||
|
previousTs := timeStamps[0].Add(-10 * time.Nanosecond)
|
||||||
|
for _, ts := range timeStamps {
|
||||||
|
if ts.Equal(previousTs) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
previousTs = ts
|
||||||
|
before := ts.Add(-1 * time.Nanosecond)
|
||||||
|
after := ts.Add(1 * time.Nanosecond)
|
||||||
|
from, _ := MostSpecificPeriod(before, periods...)
|
||||||
|
to, _ := MostSpecificPeriod(after, periods...)
|
||||||
|
if from == to {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
changeovers = append(changeovers, ts)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetNextChangeOver(t time.Time, periods ...Period) (ts time.Time, err error) {
|
||||||
|
changeOvers := GetChangeOvers(periods...)
|
||||||
|
for _, ts := range changeOvers {
|
||||||
|
if ts.After(t) {
|
||||||
|
return ts, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return time.Time{}, ErrNoNextChangeover
|
||||||
|
}
|
||||||
|
|
||||||
|
func FlattenPeriods(periods ...Period) (ids []string) {
|
||||||
|
changeovers := GetChangeOvers(periods...)
|
||||||
|
for _, c := range changeovers {
|
||||||
|
id, err := MostSpecificPeriod(c, periods...)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
304
msp/changeover_test.go
Normal file
304
msp/changeover_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,4 +9,6 @@ var (
|
|||||||
ErrEndAfterStart = errors.New("error: start time is after end time")
|
ErrEndAfterStart = errors.New("error: start time is after end time")
|
||||||
// ErrNoValidPeriods occurs when an empty set of periods is passed or when ll periods are invalid
|
// ErrNoValidPeriods occurs when an empty set of periods is passed or when ll periods are invalid
|
||||||
ErrNoValidPeriods = errors.New("error: no valid periods available")
|
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 changeovers available")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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 {
|
func ValidTimePeriods(ts time.Time, periods ...Period) []Period {
|
||||||
var valid []Period
|
var valid []Period
|
||||||
for _, p := range periods {
|
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)
|
valid = append(valid, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user