13 Commits

Author SHA1 Message Date
8b431b5d2f chore: bump Go 1.18→1.26, fix typos, clean up tests
- Update go.mod to Go 1.26
- Fix help text typos ('menut' → 'menu', 'most-significant' → 'most-specific')
- Fix error comment typo ('ll' → 'all')
- Remove unnecessary fmt.Sprintf in test names
- Remove unused fmt imports from test files
- Add compile-time interface check for TimeWindow
2026-02-22 21:01:23 +00:00
433d28a87b add sponsor button 2022-10-22 20:47:21 -07:00
2e68bb06af Use a struct instead of string slices 2022-10-14 22:04:08 -07:00
f989049717 Add gap test 2022-10-14 20:50:12 -07:00
Ethan Holz
6adb6be6b4 test: Added more comprehensive tests 2022-10-14 16:33:43 -05:00
Ethan Holz
dd79d7a54b feat: Added help and option to specify date. Changed so that calendar displays even if MSP does not. 2022-10-14 16:33:42 -05:00
9317c4b137 Merge pull request #1 from ethanholz/generate-timeline
Added functionality to generate timeline and preliminary support for help function
2022-10-14 13:11:04 -07:00
Ethan Holz
a2c1864a77 fix: Removed logging from main.go 2022-10-14 14:19:43 -05:00
Ethan Holz
8612f90d46 feat: Completed functionality for generating a period timeline 2022-10-14 14:17:50 -05:00
Ethan Holz
9698d90308 feat: Initial changes to handline generating a timeline 2022-10-13 17:02:55 -05:00
88ce350a61 updates to changeover and tests 2022-04-03 22:33:50 -07:00
0f88864200 add changes to changeover code based on unit tests 2022-04-03 19:10:55 -07:00
71bb0676c0 changeover features in progress 2022-04-03 18:39:46 -07:00
12 changed files with 1034 additions and 73 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
# These are supported funding model platforms
github: taigrr # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

View File

@@ -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
}
```

2
go.mod
View File

@@ -1,3 +1,3 @@
module github.com/taigrr/most-specific-period
go 1.17
go 1.26

33
main.go
View File

@@ -20,9 +20,11 @@ type Period struct {
func (p Period) GetEndTime() time.Time {
return p.EndTime
}
func (p Period) GetStartTime() time.Time {
return p.StartTime
}
func (p Period) GetIdentifier() string {
return p.Identifier
}
@@ -33,13 +35,35 @@ func init() {
flag.PrintDefaults()
}
}
func warnMessage() {
fmt.Print("Please type your date formats as follows, hit return between each field (RFC 3339), and hit Control+D to signal you are complete: \nIdentifier: id\nStartTime: 2019-10-12T07:20:50.52Z\nEndTime: 2019-10-12T07:20:50.52Z\n")
}
func helpMessage() {
fmt.Print("\nmost-specific-period [-h][-d]\n\nGenerates a timeline of periods and will provide a most specific period if available.\n\n-h\tShows this help menu\n-d\tProvide an RFC 3339 time to provide an alternate point for calculating MSP.")
}
func main() {
var start time.Time
help := flag.Bool("h", false, "displays help command")
userDate := flag.String("d", "", "use a custom date to calculate MSP")
flag.Parse()
if *help {
helpMessage()
os.Exit(0)
}
if userDate != nil && *userDate != "" {
t, err := time.Parse(time.RFC3339, *userDate)
if err != nil {
fmt.Println("Please enter the date using the YYYY-MM-DDT00:00:00.00Z")
os.Exit(1)
}
start = t
} else {
start = time.Now()
}
terminal := false
fi, _ := os.Stdin.Stat()
if (fi.Mode() & os.ModeCharDevice) == 0 {
@@ -98,9 +122,14 @@ func main() {
count++
}
m, err := msp.MostSpecificPeriod(time.Now(), periods...)
vals := msp.GenerateTimeline(periods...)
fmt.Print("\nTimeline of changeovers:\n")
for _, val := range vals {
fmt.Println(val)
}
m, err := msp.MostSpecificPeriod(start, periods...)
if err != nil {
fmt.Printf("Error: %v\n", err)
fmt.Printf("No significant period found\n")
os.Exit(1)
}
if terminal {

60
msp/changeover.go Normal file
View 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
}

480
msp/changeover_test.go Normal file
View File

@@ -0,0 +1,480 @@
package msp
import (
"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(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(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(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)
}
})
}
}

View File

@@ -5,8 +5,10 @@ import (
)
var (
// ErrEndAfterStart occurs when a period given has an end time after its start time
// ErrEndAfterStart occurs when a period's start time is after its 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 all 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")
)

View File

@@ -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)
}
}

View File

@@ -1,28 +1,11 @@
package msp
import (
"fmt"
"testing"
"time"
)
type TimeWindow struct {
StartTime time.Time
EndTime time.Time
Identifier string
}
func (p TimeWindow) GetIdentifier() string {
return p.Identifier
}
func (p TimeWindow) GetEndTime() time.Time {
return p.EndTime
}
func (p TimeWindow) GetStartTime() time.Time {
return p.StartTime
}
//(periods ...Period) (id string, err error) {
// (periods ...Period) (id string, err error) {
func TestMostSpecificPeriod(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()
@@ -32,81 +15,137 @@ func TestMostSpecificPeriod(t *testing.T) {
result string
err error
periods []Period
}{{testID: "No choices",
ts: now,
result: "",
err: ErrNoValidPeriods,
periods: []Period{}},
{testID: "Two Choices, shorter is second",
}{
{
testID: "No choices",
ts: now,
result: "",
err: ErrNoValidPeriods,
periods: []Period{},
},
{
testID: "Two Choices, shorter is second",
ts: now,
result: "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),
periods: []Period{
TimeWindow{
StartTime: now.Add(-5 * time.Minute),
EndTime: now.Add(time.Minute),
Identifier: "B"}}},
{testID: "Two Choices, one is a year, other a 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: "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),
periods: []Period{
TimeWindow{
StartTime: now.Add(-1 * time.Hour * 24 * 365),
EndTime: now.Add(time.Minute),
Identifier: "B"}}},
Identifier: "A",
},
TimeWindow{
StartTime: now.Add(-5 * time.Minute),
EndTime: now.Add(time.Minute),
Identifier: "B",
},
},
},
{testID: "Two Choices, shorter is first",
{
testID: "Two Choices, shorter is first",
ts: now,
result: "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),
periods: []Period{
TimeWindow{
StartTime: now.Add(-2 * time.Minute),
EndTime: now.Add(time.Minute),
Identifier: "B"}}},
{testID: "Two Choices, one in the past",
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: "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),
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",
Identifier: "B",
},
},
},
{
testID: "Two Choices, one invalid",
ts: now,
result: "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),
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",
Identifier: "B",
},
},
},
{
testID: "Two Choices, Identical periods",
ts: now,
result: "B",
err: nil,
periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute),
EndTime: now.Add(time.Minute),
Identifier: "A"},
TimeWindow{StartTime: now.Add(-time.Minute),
periods: []Period{
TimeWindow{
StartTime: now.Add(-time.Minute),
EndTime: now.Add(time.Minute),
Identifier: "B"}}},
{testID: "One choice",
Identifier: "A",
},
TimeWindow{
StartTime: now.Add(-time.Minute),
EndTime: now.Add(time.Minute),
Identifier: "B",
},
},
},
{
testID: "One choice",
ts: now,
result: "A",
err: nil,
periods: []Period{TimeWindow{StartTime: now.Add(-time.Minute),
periods: []Period{TimeWindow{
StartTime: now.Add(-time.Minute),
EndTime: now.Add(time.Minute),
Identifier: "A"}}}}
Identifier: "A",
}},
},
}
for _, tc := range testCases {
t.Run(fmt.Sprintf("%s", tc.testID), func(t *testing.T) {
t.Run(tc.testID, func(t *testing.T) {
id, err := MostSpecificPeriod(tc.ts, tc.periods...)
if id != tc.result {
t.Errorf("ID '%s' does not match expected '%s'", id, tc.result)
@@ -114,7 +153,6 @@ func TestMostSpecificPeriod(t *testing.T) {
if err != tc.err {
t.Errorf("Error '%v' does not match expected '%v'", err, tc.err)
}
})
}
}

57
msp/timeline.go Normal file
View File

@@ -0,0 +1,57 @@
package msp
import (
"fmt"
"time"
)
type TimeWindow struct {
StartTime time.Time
EndTime time.Time
Identifier string
}
func (p TimeWindow) GetIdentifier() string {
return p.Identifier
}
func (p TimeWindow) GetEndTime() time.Time {
return p.EndTime
}
func (p TimeWindow) GetStartTime() time.Time {
return p.StartTime
}
func (t TimeWindow) String() string {
return fmt.Sprintf("%s\t%s\t%s",
t.GetIdentifier(),
t.GetStartTime(),
t.GetEndTime())
}
// Outputs a formatted timeline of periods
func GenerateTimeline(periods ...Period) (out []Period) {
if len(periods) == 0 {
return out
}
periodsByID := make(map[string]Period)
ids := FlattenPeriods(periods...)
for _, val := range periods {
id := val.GetIdentifier()
periodsByID[id] = val
}
start := periodsByID[ids[0]].GetStartTime()
for _, val := range ids {
next, err := GetNextChangeOver(start, periods...)
if err == nil {
if next.Equal(periodsByID[val].GetStartTime()) {
start = periodsByID[val].GetStartTime()
next = periodsByID[val].GetEndTime()
}
out = append(out, TimeWindow{StartTime: start, EndTime: next, Identifier: val})
start = next
}
}
return out
}

278
msp/timeline_test.go Normal file
View File

@@ -0,0 +1,278 @@
package msp
import (
"fmt"
"testing"
"time"
)
// (periods ...Period) (id string, err error) {
func TestGenerateTime(t *testing.T) {
now := time.Now()
testCases := []struct {
ts time.Time
testID string
result []string
periods []Period
}{
{
testID: "No choices",
ts: now,
result: []string{},
periods: []Period{},
},
{
testID: "Two Choices, shorter is second",
ts: now,
result: []string{
fmt.Sprintf("A\t%s\t%s\n", now.Add(-5*time.Minute), now.Add(-2*time.Minute)),
fmt.Sprintf("B\t%s\t%s\n", 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: []string{
fmt.Sprintf("A\t%s\t%s\n", now.Add(-1*time.Hour*24*365), now.Add(-5*time.Minute)),
fmt.Sprintf("B\t%s\t%s\n", 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: []string{
fmt.Sprintf("B\t%s\t%s\n", now.Add(-5*time.Minute), now.Add(-2*time.Minute)),
fmt.Sprintf("A\t%s\t%s\n", 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: []string{
fmt.Sprintf("B\t%s\t%s\n", now.Add(-2*time.Minute), now.Add(-time.Minute)),
fmt.Sprintf("A\t%s\t%s\n", 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: []string{
fmt.Sprintf("B\t%s\t%s\n", 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: []string{
fmt.Sprintf("B\t%s\t%s\n", 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: []string{
fmt.Sprintf("A\t%s\t%s\n", now.Add(-time.Minute), now.Add(time.Minute)),
},
periods: []Period{TimeWindow{
StartTime: now.Add(-time.Minute),
EndTime: now.Add(time.Minute),
Identifier: "A",
}},
},
{
testID: "not in current point in time",
ts: now,
result: []string{
fmt.Sprintf("A\t%s\t%s\n", now.Add(-time.Hour*24*30), now.Add(-time.Hour*24*29)),
fmt.Sprintf("B\t%s\t%s\n", now.Add(time.Hour*24*90), now.Add(time.Hour*24*120)),
},
periods: []Period{
TimeWindow{
StartTime: now.Add(-time.Hour * 24 * 30),
EndTime: now.Add(-time.Hour * 24 * 29),
Identifier: "A",
},
TimeWindow{
StartTime: now.Add(time.Hour * 24 * 90),
EndTime: now.Add(time.Hour * 24 * 120),
Identifier: "B",
},
},
},
{
testID: "three overlapping periods",
ts: now,
result: []string{
fmt.Sprintf("C\t%s\t%s\n", now.Add(-time.Hour*24*31), now.Add(-time.Hour*24*30)),
fmt.Sprintf("A\t%s\t%s\n", now.Add(-time.Hour*24*30), now.Add(-time.Hour*24*29)),
fmt.Sprintf("C\t%s\t%s\n", now.Add(-time.Hour*24*29), now.Add(time.Hour*24*90)),
fmt.Sprintf("B\t%s\t%s\n", now.Add(time.Hour*24*90), now.Add(time.Hour*24*120)),
fmt.Sprintf("C\t%s\t%s\n", now.Add(time.Hour*24*120), now.Add(time.Hour*24*140)),
},
periods: []Period{
TimeWindow{
StartTime: now.Add(-time.Hour * 24 * 30),
EndTime: now.Add(-time.Hour * 24 * 29),
Identifier: "A",
},
TimeWindow{
StartTime: now.Add(time.Hour * 24 * 90),
EndTime: now.Add(time.Hour * 24 * 120),
Identifier: "B",
},
TimeWindow{
StartTime: now.Add(-time.Hour * 24 * 31),
EndTime: now.Add(time.Hour * 24 * 140),
Identifier: "C",
},
},
},
{
testID: "multiple overlapping periods",
ts: now,
result: []string{
fmt.Sprintf("D\t%s\t%s\n", now.Add(-time.Hour*24*150), now.Add(-time.Hour*24*65)),
fmt.Sprintf("E\t%s\t%s\n", now.Add(-time.Hour*24*65), now.Add(-time.Hour*24*31)),
fmt.Sprintf("C\t%s\t%s\n", now.Add(-time.Hour*24*31), now.Add(-time.Hour*24*30)),
fmt.Sprintf("A\t%s\t%s\n", now.Add(-time.Hour*24*30), now.Add(-time.Hour*24*29)),
fmt.Sprintf("C\t%s\t%s\n", now.Add(-time.Hour*24*29), now.Add(time.Hour*24*90)),
fmt.Sprintf("B\t%s\t%s\n", now.Add(time.Hour*24*90), now.Add(time.Hour*24*120)),
fmt.Sprintf("C\t%s\t%s\n", now.Add(time.Hour*24*120), now.Add(time.Hour*24*140)),
fmt.Sprintf("E\t%s\t%s\n", now.Add(time.Hour*24*140), now.Add(time.Hour*24*175)),
},
periods: []Period{
TimeWindow{
StartTime: now.Add(-time.Hour * 24 * 30),
EndTime: now.Add(-time.Hour * 24 * 29),
Identifier: "A",
},
TimeWindow{
StartTime: now.Add(time.Hour * 24 * 90),
EndTime: now.Add(time.Hour * 24 * 120),
Identifier: "B",
},
TimeWindow{
StartTime: now.Add(-time.Hour * 24 * 31),
EndTime: now.Add(time.Hour * 24 * 140),
Identifier: "C",
},
TimeWindow{
StartTime: now.Add(-time.Hour * 24 * 150),
EndTime: now.Add(time.Hour * 24 * 175),
Identifier: "D",
},
TimeWindow{
StartTime: now.Add(-time.Hour * 24 * 65),
EndTime: now.Add(time.Hour * 24 * 175),
Identifier: "E",
},
},
},
{
testID: "periods with a gap in the middle",
ts: now,
result: []string{
fmt.Sprintf("A\t%s\t%s\n", now.Add(-time.Minute*10), now.Add(-time.Minute*5)),
fmt.Sprintf("B\t%s\t%s\n", now.Add(time.Minute*5), now.Add(time.Minute*10)),
},
periods: []Period{
TimeWindow{
StartTime: now.Add(-time.Minute * 10),
EndTime: now.Add(-time.Minute * 5),
Identifier: "A",
},
TimeWindow{
StartTime: now.Add(time.Minute * 5),
EndTime: now.Add(time.Minute * 10),
Identifier: "B",
},
},
},
}
for _, tc := range testCases {
t.Run(tc.testID, func(t *testing.T) {
timeline := GenerateTimeline(tc.periods...)
if len(timeline) != len(tc.result) {
t.Fatalf("Time line had %d results, expected %d", len(timeline), len(tc.result))
}
for idx, period := range timeline {
if period.(TimeWindow).String()+"\n" != tc.result[idx] {
t.Errorf("Expected:\t%s\nHad:\t%s", period, tc.result[idx])
}
}
})
}
}

View File

@@ -2,6 +2,9 @@ package msp
import "time"
// Compile-time interface check.
var _ Period = TimeWindow{}
type Period interface {
GetStartTime() time.Time
GetEndTime() time.Time