mirror of
https://github.com/taigrr/most-specific-period.git
synced 2026-04-02 03:38:41 -07:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
88ce350a61
|
|||
|
0f88864200
|
|||
|
71bb0676c0
|
|||
|
01897a6ae8
|
15
README.md
15
README.md
@@ -8,7 +8,14 @@ An MSP, or Most Specific Period is defined as follows:
|
||||
- Given two valid periods, each exactly 30 days long, but offset by a week, the second (newer) period is given precedence.
|
||||
- Given two valid periods with exact same start and end time, the period with the lowest-ranking lexicographic identifier is returned (i.e. period B has precedence over period A, as ‘B’ comes after ‘A’ lexicographically.) This is because the default behavior is to choose the newer period, and named periods are often in lexicographical order (increasing numbers, letters, etc.)
|
||||
|
||||
This library operates off Period structs, which contain the following:
|
||||
- StartTime (time.Time)
|
||||
- EndTime (time.Time)
|
||||
- Identifier (string)
|
||||
This library operates off Period interfaces, which contain the following:
|
||||
|
||||
```
|
||||
type Period interface {
|
||||
GetStartTime() time.Time // Inclusive start time
|
||||
GetEndTime() time.Time // Exclusive end time ("expiration time")
|
||||
GetIdentifier() string
|
||||
}
|
||||
```
|
||||
|
||||
An example program is available to observe the operation and usage of this library.
|
||||
|
||||
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")
|
||||
// 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 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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user