2020-10-13 00:04:16 +00:00
package timeinterval
2020-09-22 07:13:10 +00:00
import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"
)
// TimeInterval describes intervals of time. ContainsTime will tell you if a golang time is contained
// within the interval.
type TimeInterval struct {
2020-10-06 10:07:08 +00:00
Times [ ] TimeRange ` yaml:"times,omitempty" `
Weekdays [ ] WeekdayRange ` yaml:"weekdays,flow,omitempty" `
DaysOfMonth [ ] DayOfMonthRange ` yaml:"days_of_month,flow,omitempty" `
Months [ ] MonthRange ` yaml:"months,flow,omitempty" `
Years [ ] YearRange ` yaml:"years,flow,omitempty" `
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
/ * TimeRange represents a range of minutes within a 1440 minute day , exclusive of the End minute . A day consists of 1440 minutes .
For example , 5 : 00 PM to End of the day would Begin at 1020 and End at 1440. * /
type TimeRange struct {
StartMinute int
EndMinute int
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
// InclusiveRange is used to hold the Beginning and End values of many time interval components
type InclusiveRange struct {
Begin int
End int
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
// A WeekdayRange is an inclusive range between [0, 6] where 0 = Sunday
type WeekdayRange struct {
InclusiveRange
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
// A DayOfMonthRange is an inclusive range that may have negative Beginning/End values that represent distance from the End of the month Beginning at -1
type DayOfMonthRange struct {
InclusiveRange
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
// A MonthRange is an inclusive range between [1, 12] where 1 = January
type MonthRange struct {
InclusiveRange
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
// A YearRange is a positive inclusive range
type YearRange struct {
InclusiveRange
2020-09-22 07:13:10 +00:00
}
type yamlTimeRange struct {
StartTime string ` yaml:"start_time" `
EndTime string ` yaml:"end_time" `
}
2020-10-06 10:07:08 +00:00
// A range with a Beginning and End that can be represented as strings
2020-09-22 07:13:10 +00:00
type stringableRange interface {
setBegin ( int )
setEnd ( int )
// Try to map a member of the range into an integer.
memberFromString ( string ) ( int , error )
}
2020-10-06 10:07:08 +00:00
func ( ir * InclusiveRange ) setBegin ( n int ) {
ir . Begin = n
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
func ( ir * InclusiveRange ) setEnd ( n int ) {
ir . End = n
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
func ( ir * InclusiveRange ) memberFromString ( in string ) ( out int , err error ) {
2020-09-22 07:13:10 +00:00
out , err = strconv . Atoi ( in )
if err != nil {
return - 1 , err
}
return out , nil
}
2020-10-06 10:07:08 +00:00
func ( r * WeekdayRange ) memberFromString ( in string ) ( out int , err error ) {
2020-09-22 07:13:10 +00:00
out , ok := daysOfWeek [ in ]
if ! ok {
return - 1 , fmt . Errorf ( "%s is not a valid weekday" , in )
}
return out , nil
}
2020-10-06 10:07:08 +00:00
func ( r * MonthRange ) memberFromString ( in string ) ( out int , err error ) {
2020-09-22 07:13:10 +00:00
out , ok := months [ in ]
if ! ok {
2020-10-06 10:07:08 +00:00
out , err = strconv . Atoi ( in )
if err != nil {
return - 1 , fmt . Errorf ( "%s is not a valid month" , in )
}
2020-09-22 07:13:10 +00:00
}
return out , nil
}
var daysOfWeek = map [ string ] int {
"sunday" : 0 ,
"monday" : 1 ,
"tuesday" : 2 ,
"wednesday" : 3 ,
"thursday" : 4 ,
"friday" : 5 ,
"saturday" : 6 ,
}
2020-10-06 10:07:08 +00:00
var daysOfWeekInv = map [ int ] string {
0 : "sunday" ,
1 : "monday" ,
2 : "tuesday" ,
3 : "wednesday" ,
4 : "thursday" ,
5 : "friday" ,
6 : "saturday" ,
}
2020-09-22 07:13:10 +00:00
var months = map [ string ] int {
"january" : 1 ,
"february" : 2 ,
"march" : 3 ,
"april" : 4 ,
"may" : 5 ,
"june" : 6 ,
"july" : 7 ,
"august" : 8 ,
"september" : 9 ,
"october" : 10 ,
"november" : 11 ,
"december" : 12 ,
}
2020-10-06 10:07:08 +00:00
var monthsInv = map [ int ] string {
1 : "january" ,
2 : "february" ,
3 : "march" ,
4 : "april" ,
5 : "may" ,
6 : "june" ,
7 : "july" ,
8 : "august" ,
9 : "september" ,
10 : "october" ,
11 : "november" ,
12 : "december" ,
}
// UnmarshalYAML implements the Unmarshaller interface for WeekdayRange.
func ( r * WeekdayRange ) UnmarshalYAML ( unmarshal func ( interface { } ) error ) error {
2020-09-22 07:13:10 +00:00
var str string
if err := unmarshal ( & str ) ; err != nil {
return err
}
err := stringableRangeFromString ( str , r )
2020-10-06 10:07:08 +00:00
if r . Begin > r . End {
return errors . New ( "Start day cannot be before End day" )
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
if r . Begin < 0 || r . Begin > 6 {
2020-09-22 07:13:10 +00:00
return fmt . Errorf ( "%s is not a valid day of the week: out of range" , str )
}
2020-10-06 10:07:08 +00:00
if r . End < 0 || r . End > 6 {
2020-09-22 07:13:10 +00:00
return fmt . Errorf ( "%s is not a valid day of the week: out of range" , str )
}
return err
}
2020-10-06 10:07:08 +00:00
// MarshalYAML implements the yaml.Marshaler interface for WeekdayRange
func ( r WeekdayRange ) MarshalYAML ( ) ( interface { } , error ) {
beginStr , ok := daysOfWeekInv [ r . Begin ]
if ! ok {
return nil , fmt . Errorf ( "Unable to convert %d into weekday string" , r . Begin )
}
if r . Begin == r . End {
return interface { } ( beginStr ) , nil
}
endStr , ok := daysOfWeekInv [ r . End ]
if ! ok {
return nil , fmt . Errorf ( "Unable to convert %d into weekday string" , r . End )
}
rangeStr := fmt . Sprintf ( "%s:%s" , beginStr , endStr )
return interface { } ( rangeStr ) , nil
}
// UnmarshalYAML implements the Unmarshaller interface for DayOfMonthRange.
func ( r * DayOfMonthRange ) UnmarshalYAML ( unmarshal func ( interface { } ) error ) error {
2020-09-22 07:13:10 +00:00
var str string
if err := unmarshal ( & str ) ; err != nil {
return err
}
err := stringableRangeFromString ( str , r )
2020-10-06 10:07:08 +00:00
if r . Begin == 0 || r . Begin < - 31 || r . Begin > 31 {
return fmt . Errorf ( "%d is not a valid day of the month: out of range" , r . Begin )
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
if r . End == 0 || r . End < - 31 || r . End > 31 {
return fmt . Errorf ( "%d is not a valid day of the month: out of range" , r . End )
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
// Check Beginning <= End accounting for negatives day of month indices
trueBegin := r . Begin
trueEnd := r . End
if r . Begin < 0 {
trueBegin = 30 + r . Begin
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
if r . End < 0 {
trueEnd = 30 + r . End
2020-09-22 07:13:10 +00:00
}
if trueBegin > trueEnd {
2020-10-06 10:07:08 +00:00
return errors . New ( "Start day cannot be before End day" )
2020-09-22 07:13:10 +00:00
}
return err
}
2020-10-06 10:07:08 +00:00
// UnmarshalYAML implements the Unmarshaller interface for MonthRange.
func ( r * MonthRange ) UnmarshalYAML ( unmarshal func ( interface { } ) error ) error {
2020-09-22 07:13:10 +00:00
var str string
if err := unmarshal ( & str ) ; err != nil {
return err
}
err := stringableRangeFromString ( str , r )
2020-10-06 10:07:08 +00:00
if r . Begin > r . End {
return errors . New ( "Start month cannot be before End month" )
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
if r . Begin < 1 || r . Begin > 12 {
2020-09-22 07:13:10 +00:00
return fmt . Errorf ( "%s is not a valid month: out of range" , str )
}
2020-10-06 10:07:08 +00:00
if r . End < 1 || r . End > 12 {
2020-09-22 07:13:10 +00:00
return fmt . Errorf ( "%s is not a valid month: out of range" , str )
}
return err
}
2020-10-06 10:07:08 +00:00
// MarshalYAML implements the yaml.Marshaler interface for DayOfMonthRange
func ( r MonthRange ) MarshalYAML ( ) ( interface { } , error ) {
beginStr , ok := monthsInv [ r . Begin ]
if ! ok {
return nil , fmt . Errorf ( "Unable to convert %d into month" , r . Begin )
}
if r . Begin == r . End {
return interface { } ( beginStr ) , nil
}
endStr , ok := monthsInv [ r . End ]
if ! ok {
return nil , fmt . Errorf ( "Unable to convert %d into month" , r . End )
}
rangeStr := fmt . Sprintf ( "%s:%s" , beginStr , endStr )
return interface { } ( rangeStr ) , nil
}
// UnmarshalYAML implements the Unmarshaller interface for YearRange.
func ( r * YearRange ) UnmarshalYAML ( unmarshal func ( interface { } ) error ) error {
2020-09-22 07:13:10 +00:00
var str string
if err := unmarshal ( & str ) ; err != nil {
return err
}
err := stringableRangeFromString ( str , r )
2020-10-06 10:07:08 +00:00
if r . Begin > r . End {
return errors . New ( "Start day cannot be before End day" )
2020-09-22 07:13:10 +00:00
}
return err
}
2020-10-06 10:07:08 +00:00
// UnmarshalYAML implements the Unmarshaller interface for TimeRanges.
func ( tr * TimeRange ) UnmarshalYAML ( unmarshal func ( interface { } ) error ) error {
2020-09-22 07:13:10 +00:00
var y yamlTimeRange
if err := unmarshal ( & y ) ; err != nil {
return err
}
if y . EndTime == "" || y . StartTime == "" {
2020-10-06 10:07:08 +00:00
return errors . New ( "Both start and End times must be provided" )
2020-09-22 07:13:10 +00:00
}
start , err := parseTime ( y . StartTime )
if err != nil {
return nil
}
2020-10-06 10:07:08 +00:00
End , err := parseTime ( y . EndTime )
2020-09-22 07:13:10 +00:00
if err != nil {
return err
}
if start < 0 {
return errors . New ( "Start time out of range" )
}
2020-10-06 10:07:08 +00:00
if End > 1440 {
2020-09-22 07:13:10 +00:00
return errors . New ( "End time out of range" )
}
2020-10-06 10:07:08 +00:00
if start >= End {
return errors . New ( "Start time cannot be equal or greater than End time" )
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
tr . StartMinute , tr . EndMinute = start , End
2020-09-22 07:13:10 +00:00
return nil
}
2020-10-06 10:07:08 +00:00
//MarshalYAML implements the yaml.Marshaler interface for TimeRange
func ( tr TimeRange ) MarshalYAML ( ) ( out interface { } , err error ) {
startHr := tr . StartMinute / 60
endHr := tr . EndMinute / 60
startMin := tr . StartMinute % 60
endMin := tr . EndMinute % 60
startStr := fmt . Sprintf ( "%02d:%02d" , startHr , startMin )
endStr := fmt . Sprintf ( "%02d:%02d" , endHr , endMin )
yTr := yamlTimeRange { startStr , endStr }
return interface { } ( yTr ) , err
}
//MarshalYAML implements the yaml.Marshaler interface for InclusiveRange
func ( ir InclusiveRange ) MarshalYAML ( ) ( interface { } , error ) {
if ir . Begin == ir . End {
return strconv . Itoa ( ir . Begin ) , nil
}
out := fmt . Sprintf ( "%d:%d" , ir . Begin , ir . End )
return interface { } ( out ) , nil
}
2020-09-22 07:13:10 +00:00
// TimeLayout specifies the layout to be used in time.Parse() calls for time intervals
const TimeLayout = "15:04"
var validTime string = "^((([01][0-9])|(2[0-3])):[0-5][0-9])$|(^24:00$)"
var validTimeRE * regexp . Regexp = regexp . MustCompile ( validTime )
// Given a time, determines the number of days in the month that time occurs in.
func daysInMonth ( t time . Time ) int {
monthStart := time . Date ( t . Year ( ) , t . Month ( ) , 1 , 0 , 0 , 0 , 0 , t . Location ( ) )
monthEnd := monthStart . AddDate ( 0 , 1 , 0 )
diff := monthEnd . Sub ( monthStart )
return int ( diff . Hours ( ) / 24 )
}
func clamp ( n , min , max int ) int {
if n <= min {
return min
}
if n >= max {
return max
}
return n
}
// ContainsTime returns true if the TimeInterval contains the given time, otherwise returns false
func ( tp TimeInterval ) ContainsTime ( t time . Time ) bool {
if tp . Times != nil {
in := false
for _ , validMinutes := range tp . Times {
2020-10-06 10:07:08 +00:00
if ( t . Hour ( ) * 60 + t . Minute ( ) ) >= validMinutes . StartMinute && ( t . Hour ( ) * 60 + t . Minute ( ) ) < validMinutes . EndMinute {
2020-09-22 07:13:10 +00:00
in = true
break
}
}
if ! in {
return false
}
}
if tp . DaysOfMonth != nil {
in := false
for _ , validDates := range tp . DaysOfMonth {
2020-10-13 00:28:10 +00:00
var begin , end int
2020-09-22 07:13:10 +00:00
daysInMonth := daysInMonth ( t )
2020-10-06 10:07:08 +00:00
if validDates . Begin < 0 {
2020-10-13 00:28:10 +00:00
begin = daysInMonth + validDates . Begin + 1
2020-09-22 07:13:10 +00:00
} else {
2020-10-13 00:28:10 +00:00
begin = validDates . Begin
2020-09-22 07:13:10 +00:00
}
2020-10-06 10:07:08 +00:00
if validDates . End < 0 {
2020-10-13 00:28:10 +00:00
end = daysInMonth + validDates . End + 1
2020-09-22 07:13:10 +00:00
} else {
2020-10-13 00:28:10 +00:00
end = validDates . End
2020-09-22 07:13:10 +00:00
}
// Clamp to the boundaries of the month to prevent crossing into other months
2020-10-13 00:28:10 +00:00
begin = clamp ( begin , - 1 * daysInMonth , daysInMonth )
end = clamp ( end , - 1 * daysInMonth , daysInMonth )
if t . Day ( ) >= begin && t . Day ( ) <= end {
2020-09-22 07:13:10 +00:00
in = true
break
}
}
if ! in {
return false
}
}
if tp . Months != nil {
in := false
for _ , validMonths := range tp . Months {
2020-10-06 10:07:08 +00:00
if t . Month ( ) >= time . Month ( validMonths . Begin ) && t . Month ( ) <= time . Month ( validMonths . End ) {
2020-09-22 07:13:10 +00:00
in = true
break
}
}
if ! in {
return false
}
}
if tp . Weekdays != nil {
in := false
for _ , validDays := range tp . Weekdays {
2020-10-06 10:07:08 +00:00
if t . Weekday ( ) >= time . Weekday ( validDays . Begin ) && t . Weekday ( ) <= time . Weekday ( validDays . End ) {
2020-09-22 07:13:10 +00:00
in = true
break
}
}
if ! in {
return false
}
}
if tp . Years != nil {
in := false
for _ , validYears := range tp . Years {
2020-10-06 10:07:08 +00:00
if t . Year ( ) >= validYears . Begin && t . Year ( ) <= validYears . End {
2020-09-22 07:13:10 +00:00
in = true
break
}
}
if ! in {
return false
}
}
return true
}
2020-10-06 10:07:08 +00:00
// Converts a string of the form "HH:MM" into a TimeRange
2020-09-22 07:13:10 +00:00
func parseTime ( in string ) ( mins int , err error ) {
if ! validTimeRE . MatchString ( in ) {
return 0 , fmt . Errorf ( "Couldn't parse timestamp %s, invalid format" , in )
}
timestampComponents := strings . Split ( in , ":" )
if len ( timestampComponents ) != 2 {
return 0 , fmt . Errorf ( "Invalid timestamp format: %s" , in )
}
timeStampHours , err := strconv . Atoi ( timestampComponents [ 0 ] )
if err != nil {
return 0 , err
}
timeStampMinutes , err := strconv . Atoi ( timestampComponents [ 1 ] )
if err != nil {
return 0 , err
}
if timeStampHours < 0 || timeStampHours > 24 || timeStampMinutes < 0 || timeStampMinutes > 60 {
return 0 , fmt . Errorf ( "Timestamp %s out of range" , in )
}
// Timestamps are stored as minutes elapsed in the day, so multiply hours by 60
mins = timeStampHours * 60 + timeStampMinutes
return mins , nil
}
2020-10-06 10:07:08 +00:00
// Converts a range that can be represented as strings (e.g. monday:wednesday) into an equivalent integer-represented range
2020-09-22 07:13:10 +00:00
func stringableRangeFromString ( in string , r stringableRange ) ( err error ) {
in = strings . ToLower ( in )
if strings . ContainsRune ( in , ':' ) {
components := strings . Split ( in , ":" )
if len ( components ) != 2 {
return fmt . Errorf ( "Coudn't parse range %s, invalid format" , in )
}
start , err := r . memberFromString ( components [ 0 ] )
if err != nil {
return err
}
2020-10-06 10:07:08 +00:00
End , err := r . memberFromString ( components [ 1 ] )
2020-09-22 07:13:10 +00:00
if err != nil {
return err
}
r . setBegin ( start )
2020-10-06 10:07:08 +00:00
r . setEnd ( End )
2020-09-22 07:13:10 +00:00
return nil
}
val , err := r . memberFromString ( in )
if err != nil {
return err
}
r . setBegin ( val )
r . setEnd ( val )
return nil
}