Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Coleman McFarland committed Jul 14, 2019
0 parents commit 60305bb
Show file tree
Hide file tree
Showing 4 changed files with 259 additions and 0 deletions.
5 changes: 5 additions & 0 deletions go.mod
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
7 changes: 7 additions & 0 deletions go.sum
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=
134 changes: 134 additions & 0 deletions lilrange.go
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
}
113 changes: 113 additions & 0 deletions lilrange_test.go
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) })
}

0 comments on commit 60305bb

Please sign in to comment.