-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Coleman McFarland
committed
Jul 14, 2019
0 parents
commit 60305bb
Showing
4 changed files
with
259 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
module github.com/dontlaugh/lilrange | ||
|
||
go 1.12 | ||
|
||
require github.com/stretchr/testify v1.3.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= | ||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | ||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | ||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | ||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= | ||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= | ||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
package lilrange | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"strconv" | ||
"strings" | ||
"time" | ||
) | ||
|
||
type Range struct { | ||
Start time.Time | ||
End time.Time | ||
Duration time.Duration | ||
repr string | ||
} | ||
|
||
func (t Range) Within(instant time.Time) bool { | ||
return instant.After(t.Start) && instant.Before(t.End) | ||
} | ||
|
||
func (r Range) Next() Range { | ||
return Range{ | ||
r.Start.Add(24 * time.Hour), | ||
r.End.Add(24 * time.Hour), | ||
r.Duration, | ||
r.repr, | ||
} | ||
} | ||
|
||
func Parse(s string) (*Range, error) { | ||
var tr Range | ||
splitted := strings.Split(s, "-") | ||
if len(splitted) != 2 { | ||
return nil, fmt.Errorf("invalid time range value: %s", s) | ||
} | ||
startHr, startMin, err := extractAndValidate(splitted[0]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
endHr, endMin, err := extractAndValidate(splitted[1]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
// 0. determine duration | ||
dur, _ := CalculateDurationMinutes(startHr, startMin, endHr, endMin) | ||
// 1. determine if "today's start" has happened | ||
now := time.Now() | ||
nowYear, nowMonth, nowDate := now.Date() | ||
todaysStart := time.Date(nowYear, nowMonth, nowDate, startHr, startMin, 0, 0, time.UTC) | ||
endTime := todaysStart.Add(time.Minute * time.Duration(dur)) | ||
|
||
if now.After(endTime) { | ||
// today's range has ended. Our default behavior is to return the next | ||
// range, e.g. tomorrow's | ||
tr.Start = todaysStart.Add(time.Hour * 24) | ||
tr.End = endTime.Add(time.Hour * 24) | ||
} else { | ||
// Today's range has not ended. We are either a) inside this range, or | ||
// b) it is in the future and has not started. Either way, assign the | ||
// computed times and let the caller decide what to do. | ||
tr.Start = todaysStart | ||
tr.End = endTime | ||
} | ||
tr.Duration = time.Minute * time.Duration(dur) | ||
tr.repr = s | ||
return &tr, nil | ||
} | ||
|
||
func CalculateDurationMinutes(startHr, startMin, endHr, endMin int) (int, bool) { | ||
if !between(startHr, 0, 23) || !between(endHr, 0, 23) || !between(startMin, 0, 59) || !between(endMin, 0, 59) { | ||
panic("invalid values for hours and/or minutes") | ||
} | ||
var crossesMidnight bool | ||
if startHr > endHr { | ||
crossesMidnight = true | ||
} | ||
var duration int | ||
if crossesMidnight { | ||
minUntilMidnight := ((24 - startHr) * 60) - startMin | ||
minAfterMidnight := (endHr * 60) + endMin | ||
duration = minUntilMidnight + minAfterMidnight | ||
} else { | ||
duration = ((endHr - startHr) * 60) - startMin + endMin | ||
} | ||
return duration, crossesMidnight | ||
} | ||
|
||
func validRune(r rune) bool { | ||
valid := []rune{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'} | ||
for _, x := range valid { | ||
if x == r { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
func extractAndValidate(s string) (int, int, error) { | ||
runes := []rune(s) | ||
if len(runes) != 4 { | ||
return -1, -1, fmt.Errorf("invalid time component: %s", s) | ||
} | ||
for pos, char := range s { | ||
// TODO make this a comparison to range of ASCII bytes rather than another loop | ||
if !validRune(char) { | ||
return -1, -1, fmt.Errorf("invalid char at position %v: %c [%v]", | ||
pos, char, char) | ||
|
||
} | ||
} | ||
hr, min := string(runes[0:2]), string(runes[2:4]) | ||
// We've asserted that our chars are ASCII 0-9, so Atoi should never fail. | ||
hrInt, err := strconv.Atoi(hr) | ||
if err != nil { | ||
panic("how is this possible") | ||
} | ||
minInt, err := strconv.Atoi(min) | ||
if err != nil { | ||
panic("universe is broken, sorry") | ||
} | ||
if !between(hrInt, 0, 23) { | ||
return -1, -1, errors.New("hour value must be between 0 and 23") | ||
} | ||
if !between(minInt, 0, 59) { | ||
return -1, -1, errors.New("minute value must be between 0 and 59") | ||
} | ||
return hrInt, minInt, nil | ||
} | ||
|
||
// x is between this and that | ||
func between(x, this, that int) bool { | ||
return x >= this && x <= that | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
package lilrange | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestParse(t *testing.T) { | ||
type testCase struct { | ||
input string | ||
expected *Range | ||
} | ||
var invalidInputs []testCase = []testCase{ | ||
// invalid inputs, so we expect nil | ||
{"0300", nil}, | ||
{"0300-000", nil}, | ||
{"300-000", nil}, | ||
{"-000", nil}, | ||
{"", nil}, | ||
{"-", nil}, | ||
{"-0300", nil}, | ||
{"-0", nil}, | ||
{"0030-0", nil}, | ||
} | ||
|
||
for _, c := range invalidInputs { | ||
actual, _ := Parse(c.input) | ||
assert.EqualValues(t, c.expected, actual) | ||
} | ||
|
||
type durationTest struct { | ||
input string | ||
expected time.Duration | ||
} | ||
var durationTests []durationTest = []durationTest{ | ||
// valid inputs, and we assert on the durations we compute | ||
{"0100-0200", 1 * time.Hour}, | ||
{"0000-0002", 2 * time.Minute}, | ||
{"2359-0000", 1 * time.Minute}, | ||
{"2359-0001", 2 * time.Minute}, | ||
{"2259-0001", 62 * time.Minute}, | ||
} | ||
|
||
for _, dt := range durationTests { | ||
actual, err := Parse(dt.input) | ||
if err != nil { | ||
t.Errorf("duration test fail: %v", err) | ||
} | ||
assert.EqualValues(t, dt.expected, actual.Duration) | ||
} | ||
} | ||
|
||
func TestExtractAndValidate(t *testing.T) { | ||
type testCase struct { | ||
input string | ||
expected []int | ||
} | ||
var cases = []testCase{ | ||
// invalid cases | ||
{"0", []int{-1, -1}}, | ||
{"223", []int{-1, -1}}, | ||
{"2460", []int{-1, -1}}, | ||
{"2460", []int{-1, -1}}, | ||
{"0260", []int{-1, -1}}, | ||
{"9901", []int{-1, -1}}, | ||
{"12.1", []int{-1, -1}}, | ||
{"12:1", []int{-1, -1}}, | ||
{"1:21", []int{-1, -1}}, | ||
{"1:21", []int{-1, -1}}, | ||
{"1/21", []int{-1, -1}}, | ||
// valid cases | ||
{"2359", []int{23, 59}}, | ||
{"0000", []int{0, 0}}, | ||
{"0001", []int{0, 1}}, | ||
{"1111", []int{11, 11}}, | ||
{"0223", []int{2, 23}}, | ||
} | ||
|
||
for _, c := range cases { | ||
hr, min, _ := extractAndValidate(c.input) | ||
assert.EqualValues(t, c.expected, []int{hr, min}) | ||
} | ||
} | ||
|
||
func TestCalculateDurationMinutes(t *testing.T) { | ||
|
||
type testCase struct { | ||
// inputs: startHr, startMin, endHr, endMin | ||
inputs []int | ||
expected int | ||
} | ||
var cases = []testCase{ | ||
{[]int{0, 0, 1, 0}, 60}, | ||
{[]int{1, 0, 2, 0}, 60}, | ||
{[]int{23, 0, 1, 0}, 120}, | ||
{[]int{23, 15, 1, 0}, 105}, | ||
{[]int{23, 15, 1, 10}, 115}, | ||
} | ||
|
||
for _, c := range cases { | ||
dur, _ := CalculateDurationMinutes(c.inputs[0], c.inputs[1], | ||
c.inputs[2], c.inputs[3]) | ||
assert.EqualValues(t, c.expected, dur) | ||
} | ||
// Assert that invalid inputs panic. | ||
assert.Panics(t, func() { CalculateDurationMinutes(-1, 0, 0, 0) }) | ||
assert.Panics(t, func() { CalculateDurationMinutes(25, 0, 0, 0) }) | ||
assert.Panics(t, func() { CalculateDurationMinutes(0, 0, 25, 0) }) | ||
assert.Panics(t, func() { CalculateDurationMinutes(0, 69, 0, 0) }) | ||
assert.Panics(t, func() { CalculateDurationMinutes(0, 0, 0, 69) }) | ||
} |