github.com/rigado/snapd@v2.42.5-go-mod+incompatible/timeutil/schedule.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package timeutil
    21  
    22  import (
    23  	"bytes"
    24  	"fmt"
    25  	"math/rand"
    26  	"regexp"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  )
    31  
    32  // Match 0:00-24:00, where 24:00 means the later end of the day.
    33  var validTime = regexp.MustCompile(`^([0-9]|0[0-9]|1[0-9]|2[0-3]):([0-5][0-9])$|^24:00$`)
    34  
    35  // Clock represents a hour:minute time within a day.
    36  type Clock struct {
    37  	Hour   int
    38  	Minute int
    39  }
    40  
    41  func (t Clock) String() string {
    42  	return fmt.Sprintf("%02d:%02d", t.Hour, t.Minute)
    43  }
    44  
    45  // Sub returns the duration t - other.
    46  func (t Clock) Sub(other Clock) time.Duration {
    47  	t1 := time.Duration(t.Hour)*time.Hour + time.Duration(t.Minute)*time.Minute
    48  	t2 := time.Duration(other.Hour)*time.Hour + time.Duration(other.Minute)*time.Minute
    49  	dur := t1 - t2
    50  	if dur < 0 {
    51  		dur = -(dur + 24*time.Hour)
    52  	}
    53  	return dur
    54  }
    55  
    56  // Add adds given duration to t and returns a new Clock
    57  func (t Clock) Add(dur time.Duration) Clock {
    58  	t1 := time.Duration(t.Hour)*time.Hour + time.Duration(t.Minute)*time.Minute
    59  	t2 := t1 + dur
    60  	nt := Clock{
    61  		Hour:   int(t2.Hours()) % 24,
    62  		Minute: int(t2.Minutes()) % 60,
    63  	}
    64  	return nt
    65  }
    66  
    67  // Time generates a time.Time with hour and minute set from t, while year, month
    68  // and day are taken from base
    69  func (t Clock) Time(base time.Time) time.Time {
    70  	return time.Date(base.Year(), base.Month(), base.Day(),
    71  		t.Hour, t.Minute, 0, 0, base.Location())
    72  }
    73  
    74  // ParseClock parses a string that contains hour:minute and returns
    75  // a Clock type or an error
    76  func ParseClock(s string) (t Clock, err error) {
    77  	m := validTime.FindStringSubmatch(s)
    78  	if len(m) == 0 {
    79  		return t, fmt.Errorf("cannot parse %q", s)
    80  	}
    81  
    82  	if m[0] == "24:00" {
    83  		t.Hour = 24
    84  		return t, nil
    85  	}
    86  
    87  	t.Hour, err = strconv.Atoi(m[1])
    88  	if err != nil {
    89  		return t, fmt.Errorf("cannot parse %q: %s", m[1], err)
    90  	}
    91  	t.Minute, err = strconv.Atoi(m[2])
    92  	if err != nil {
    93  		return t, fmt.Errorf("cannot parse %q: %s", m[2], err)
    94  	}
    95  	return t, nil
    96  }
    97  
    98  const (
    99  	EveryWeek uint = 0
   100  	LastWeek  uint = 5
   101  )
   102  
   103  // Week represents a weekday such as Monday, Tuesday, with optional
   104  // week-in-the-month position, eg. the first Monday of the month
   105  type Week struct {
   106  	Weekday time.Weekday
   107  	// Pos defines which week inside the month the Day refers to, where zero
   108  	// means every week, 1 means first occurrence of the weekday, and 5
   109  	// means last occurrence (which might be the fourth or the fifth).
   110  	Pos uint
   111  }
   112  
   113  func (w Week) String() string {
   114  	// Wednesday -> wed
   115  	day := strings.ToLower(w.Weekday.String()[0:3])
   116  	if w.Pos == EveryWeek {
   117  		return day
   118  	}
   119  	return day + strconv.Itoa(int(w.Pos))
   120  }
   121  
   122  // WeekSpan represents a span of weekdays between Start and End days. WeekSpan
   123  // may wrap around the week, eg. fri-mon is a span from Friday to Monday
   124  type WeekSpan struct {
   125  	Start Week
   126  	End   Week
   127  }
   128  
   129  func (ws WeekSpan) String() string {
   130  	if ws.End != ws.Start {
   131  		return ws.Start.String() + "-" + ws.End.String()
   132  	}
   133  	return ws.Start.String()
   134  }
   135  
   136  func findNthWeekDay(t time.Time, weekday time.Weekday, nthInMonth uint) time.Time {
   137  	// move to the beginning of the month
   138  	t = t.AddDate(0, 0, -t.Day()+1)
   139  
   140  	var nth uint
   141  	for {
   142  		if t.Weekday() == weekday {
   143  			nth++
   144  			if nth == nthInMonth {
   145  				break
   146  			}
   147  		}
   148  		t = t.Add(24 * time.Hour)
   149  	}
   150  	return t
   151  }
   152  
   153  // Match checks if t is within the day span represented by ws.
   154  func (ws WeekSpan) Match(t time.Time) bool {
   155  	start, end := ws.Start, ws.End
   156  	wdStart, wdEnd := start.Weekday, end.Weekday
   157  
   158  	if start.Pos != EveryWeek {
   159  		if start.Pos == LastWeek {
   160  			// last week of the month
   161  			if !isLastWeekdayInMonth(t) {
   162  				return false
   163  			}
   164  		} else {
   165  			startDay := findNthWeekDay(t, start.Weekday, start.Pos)
   166  			endDay := findNthWeekDay(t, end.Weekday, end.Pos)
   167  
   168  			if t.Day() < startDay.Day() || t.Day() > endDay.Day() {
   169  				return false
   170  			}
   171  			return true
   172  		}
   173  	}
   174  
   175  	if wdStart <= wdEnd {
   176  		// single day (mon) or start < end (eg. mon-fri)
   177  		return t.Weekday() >= wdStart && t.Weekday() <= wdEnd
   178  	}
   179  	// wraps around the week end, eg. fri-mon
   180  	return t.Weekday() >= wdStart || t.Weekday() <= wdEnd
   181  }
   182  
   183  // ClockSpan represents a time span within 24h, potentially crossing days. For
   184  // example, 23:00-1:00 represents a span from 11pm to 1am.
   185  type ClockSpan struct {
   186  	Start Clock
   187  	End   Clock
   188  	// Split defines the number of subspans this span will be divided into.
   189  	Split uint
   190  	// Spread defines whether the events are randomly spread inside the span
   191  	// or subspans.
   192  	Spread bool
   193  }
   194  
   195  func (ts ClockSpan) String() string {
   196  	sep := "-"
   197  	if ts.Spread {
   198  		sep = "~"
   199  	}
   200  	if ts.End != ts.Start {
   201  		s := ts.Start.String() + sep + ts.End.String()
   202  		if ts.Split > 0 {
   203  			s += "/" + strconv.Itoa(int(ts.Split))
   204  		}
   205  		return s
   206  	}
   207  	return ts.Start.String()
   208  }
   209  
   210  // Window generates a ScheduleWindow which has the start date same as t. The
   211  // window's start and end time are set according to Start and End, with the end
   212  // time possibly crossing into the next day.
   213  func (ts ClockSpan) Window(t time.Time) ScheduleWindow {
   214  	start := ts.Start.Time(t)
   215  	end := ts.End.Time(t)
   216  
   217  	// 23:00-1:00
   218  	if end.Before(start) {
   219  		end = end.Add(24 * time.Hour)
   220  	}
   221  	return ScheduleWindow{
   222  		Start:  start,
   223  		End:    end,
   224  		Spread: ts.Spread,
   225  	}
   226  }
   227  
   228  // ClockSpans returns a slice of ClockSpans generated from ts by splitting the
   229  // time between ts.Start and ts.End into ts.Split equal spans.
   230  func (ts ClockSpan) ClockSpans() []ClockSpan {
   231  	if ts.Split == 0 || ts.Split == 1 || ts.End == ts.Start {
   232  		return []ClockSpan{ts}
   233  	}
   234  
   235  	span := ts.End.Sub(ts.Start)
   236  	if span < 0 {
   237  		span = -span
   238  	}
   239  	step := span / time.Duration(ts.Split)
   240  
   241  	spans := make([]ClockSpan, ts.Split)
   242  	for i := uint(0); i < ts.Split; i++ {
   243  		start := ts.Start.Add(time.Duration(i) * step)
   244  		spans[i] = ClockSpan{
   245  			Start:  start,
   246  			End:    start.Add(step),
   247  			Split:  0, // no more subspans
   248  			Spread: ts.Spread,
   249  		}
   250  	}
   251  	return spans
   252  }
   253  
   254  // Schedule represents a single schedule
   255  type Schedule struct {
   256  	WeekSpans  []WeekSpan
   257  	ClockSpans []ClockSpan
   258  }
   259  
   260  func (sched *Schedule) String() string {
   261  	var buf bytes.Buffer
   262  
   263  	for i, span := range sched.WeekSpans {
   264  		if i > 0 {
   265  			buf.WriteByte(',')
   266  		}
   267  		buf.WriteString(span.String())
   268  	}
   269  
   270  	if len(sched.WeekSpans) > 0 && len(sched.ClockSpans) > 0 {
   271  		buf.WriteByte(',')
   272  	}
   273  
   274  	for i, span := range sched.ClockSpans {
   275  		if i > 0 {
   276  			buf.WriteByte(',')
   277  		}
   278  		buf.WriteString(span.String())
   279  	}
   280  	return buf.String()
   281  }
   282  
   283  func (sched *Schedule) flattenedClockSpans() []ClockSpan {
   284  	baseTimes := sched.ClockSpans
   285  	if len(baseTimes) == 0 {
   286  		baseTimes = []ClockSpan{{}}
   287  	}
   288  
   289  	times := make([]ClockSpan, 0, len(baseTimes))
   290  	for _, ts := range baseTimes {
   291  		times = append(times, ts.ClockSpans()...)
   292  	}
   293  	return times
   294  }
   295  
   296  // isLastWeekdayInMonth returns true if t.Weekday() is the last weekday
   297  // occurring this t.Month(), eg. check is Feb 25 2017 is the last Saturday of
   298  // February.
   299  func isLastWeekdayInMonth(t time.Time) bool {
   300  	// try a week from now, if it's still the same month then t.Weekday() is
   301  	// not last
   302  	return t.Month() != t.Add(7*24*time.Hour).Month()
   303  }
   304  
   305  // ScheduleWindow represents a time window between Start and End times when the
   306  // scheduled event can happen.
   307  type ScheduleWindow struct {
   308  	Start time.Time
   309  	End   time.Time
   310  	// Spread defines whether the event shall be randomly placed between
   311  	// Start and End times
   312  	Spread bool
   313  }
   314  
   315  // Includes returns whether t is inside the window.
   316  func (s ScheduleWindow) Includes(t time.Time) bool {
   317  	return !(t.Before(s.Start) || t.After(s.End))
   318  }
   319  
   320  // IsZero returns whether s is uninitialized.
   321  func (s ScheduleWindow) IsZero() bool {
   322  	return s.Start.IsZero() || s.End.IsZero()
   323  }
   324  
   325  // Next returns the earliest window after last according to the schedule.
   326  func (sched *Schedule) Next(last time.Time) ScheduleWindow {
   327  	now := timeNow()
   328  
   329  	tspans := sched.flattenedClockSpans()
   330  
   331  	for t := last; ; t = t.Add(24 * time.Hour) {
   332  		// try to find a matching schedule by moving in 24h jumps, check
   333  		// if the event needs to happen on a specific day in a specific
   334  		// week, next pick the earliest event time
   335  
   336  		var window ScheduleWindow
   337  
   338  		if len(sched.WeekSpans) > 0 {
   339  			// if there's a week schedule, check if we hit that
   340  			// first
   341  			var weekMatch bool
   342  			for _, week := range sched.WeekSpans {
   343  				if week.Match(t) {
   344  					weekMatch = true
   345  					break
   346  				}
   347  			}
   348  
   349  			if !weekMatch {
   350  				continue
   351  			}
   352  		}
   353  
   354  		for _, tspan := range tspans {
   355  			// consider all time spans for this particular date and
   356  			// find the earliest possible one that is not before
   357  			// 'now', and does not include the 'last' time
   358  			newWindow := tspan.Window(t)
   359  
   360  			if newWindow.End.Before(now) {
   361  				// the time span ends before 'now', try another
   362  				// one
   363  				continue
   364  			}
   365  
   366  			if newWindow.Includes(last) {
   367  				// same interval as last update, move forward
   368  				continue
   369  			}
   370  
   371  			if window.IsZero() || newWindow.Start.Before(window.Start) {
   372  				// this candidate comes before current
   373  				// candidate, so use it
   374  				window = newWindow
   375  			}
   376  		}
   377  		if window.End.Before(now) {
   378  			// no suitable time span was found this day so try the
   379  			// next day
   380  			continue
   381  		}
   382  		return window
   383  	}
   384  
   385  }
   386  
   387  func randDur(a, b time.Time) time.Duration {
   388  	dur := b.Sub(a)
   389  	if dur > 5*time.Minute {
   390  		// doing it this way we still spread really small windows about
   391  		dur -= 5 * time.Minute
   392  	}
   393  
   394  	if dur <= 0 {
   395  		// avoid panic'ing (even if things are probably messed up)
   396  		return 0
   397  	}
   398  
   399  	return time.Duration(rand.Int63n(int64(dur)))
   400  }
   401  
   402  var (
   403  	timeNow = time.Now
   404  )
   405  
   406  func init() {
   407  	rand.Seed(time.Now().UnixNano())
   408  }
   409  
   410  // Next returns the earliest event after last according to the provided
   411  // schedule but no later than maxDuration since last.
   412  func Next(schedule []*Schedule, last time.Time, maxDuration time.Duration) time.Duration {
   413  	now := timeNow()
   414  
   415  	window := ScheduleWindow{
   416  		Start: last.Add(maxDuration),
   417  		End:   last.Add(maxDuration).Add(1 * time.Hour),
   418  	}
   419  
   420  	for _, sched := range schedule {
   421  		next := sched.Next(last)
   422  		if next.Start.Before(window.Start) {
   423  			window = next
   424  		}
   425  	}
   426  	if window.Start.Before(now) {
   427  		return 0
   428  	}
   429  
   430  	when := window.Start.Sub(now)
   431  	if window.Spread {
   432  		when += randDur(window.Start, window.End)
   433  	}
   434  
   435  	return when
   436  
   437  }
   438  
   439  var weekdayMap = map[string]time.Weekday{
   440  	"sun": time.Sunday,
   441  	"mon": time.Monday,
   442  	"tue": time.Tuesday,
   443  	"wed": time.Wednesday,
   444  	"thu": time.Thursday,
   445  	"fri": time.Friday,
   446  	"sat": time.Saturday,
   447  }
   448  
   449  // parseClockRange parses a string like "9:00-11:00" and returns the start and
   450  // end times.
   451  func parseClockRange(s string) (start, end Clock, err error) {
   452  	l := strings.SplitN(s, "-", 2)
   453  	if len(l) != 2 {
   454  		return start, end, fmt.Errorf("cannot parse %q: not a valid interval", s)
   455  	}
   456  
   457  	start, err = ParseClock(l[0])
   458  	if err != nil {
   459  		return start, end, fmt.Errorf("cannot parse %q: not a valid time", l[0])
   460  	}
   461  	end, err = ParseClock(l[1])
   462  	if err != nil {
   463  		return start, end, fmt.Errorf("cannot parse %q: not a valid time", l[1])
   464  	}
   465  
   466  	return start, end, nil
   467  }
   468  
   469  // ParseLegacySchedule takes an obsolete schedule string in the form of:
   470  //
   471  // 9:00-15:00 (every day between 9am and 3pm)
   472  // 9:00-15:00/21:00-22:00 (every day between 9am,5pm and 9pm,10pm)
   473  //
   474  // and returns a list of Schedule types or an error
   475  func ParseLegacySchedule(scheduleSpec string) ([]*Schedule, error) {
   476  	var schedule []*Schedule
   477  
   478  	for _, s := range strings.Split(scheduleSpec, "/") {
   479  		start, end, err := parseClockRange(s)
   480  		if err != nil {
   481  			return nil, err
   482  		}
   483  		schedule = append(schedule, &Schedule{
   484  			ClockSpans: []ClockSpan{{
   485  				Start:  start,
   486  				End:    end,
   487  				Spread: true,
   488  			}},
   489  		})
   490  	}
   491  
   492  	return schedule, nil
   493  }
   494  
   495  // ParseSchedule parses a schedule in V2 format. The format is described as:
   496  //
   497  //     eventlist = eventset *( ",," eventset )
   498  //     eventset = wdaylist / timelist / wdaylist "," timelist
   499  //
   500  //     wdaylist = wdayset *( "," wdayset )
   501  //     wdayset = wday / wdayspan
   502  //     wday =  ( "sun" / "mon" / "tue" / "wed" / "thu" / "fri" / "sat" ) [ DIGIT ]
   503  //     wdayspan = wday "-" wday
   504  //
   505  //     timelist = timeset *( "," timeset )
   506  //     timeset = time / timespan
   507  //     time = 2DIGIT ":" 2DIGIT
   508  //     timespan = time ( "-" / "~" ) time [ "/" ( time / count ) ]
   509  //     count = 1*DIGIT
   510  //
   511  // Examples:
   512  // mon,10:00,,fri,15:00 (Monday at 10:00, Friday at 15:00)
   513  // mon,fri,10:00,15:00 (Monday at 10:00 and 15:00, Friday at 10:00 and 15:00)
   514  // mon-wed,fri,9:00-11:00/2 (Monday to Wednesday and on Friday, twice between
   515  //                           9:00 and 11:00)
   516  // mon,9:00~11:00,,wed,22:00~23:00 (Monday, sometime between 9:00 and 11:00, and
   517  //                                  on Wednesday, sometime between 22:00 and 23:00)
   518  // mon,wed  (Monday and on Wednesday)
   519  // mon,,wed (same as above)
   520  //
   521  // Returns a slice of schedules or an error if parsing failed
   522  func ParseSchedule(scheduleSpec string) ([]*Schedule, error) {
   523  	var schedule []*Schedule
   524  
   525  	for _, s := range strings.Split(scheduleSpec, ",,") {
   526  		// cut the schedule in event sets
   527  		//     eventlist = eventset *( ",," eventset )
   528  		sched, err := parseEventSet(s)
   529  		if err != nil {
   530  			return nil, err
   531  		}
   532  		schedule = append(schedule, sched)
   533  	}
   534  	return schedule, nil
   535  }
   536  
   537  // parseWeekSpan parses a weekly span such as "mon-tue" or "mon2-tue3".
   538  func parseWeekSpan(s string) (span WeekSpan, err error) {
   539  	var parsed WeekSpan
   540  
   541  	split := strings.Split(s, spanToken)
   542  	if len(split) > 2 {
   543  		return span, fmt.Errorf("cannot parse %q: invalid week span", s)
   544  	}
   545  
   546  	parsed.Start, err = parseWeekday(split[0])
   547  	if err != nil {
   548  		return span, fmt.Errorf("cannot parse %q: %q is not a valid weekday", s, split[0])
   549  	}
   550  
   551  	if len(split) == 2 {
   552  		parsed.End, err = parseWeekday(split[1])
   553  		if err != nil {
   554  			return span, fmt.Errorf("cannot parse %q: %q is not a valid weekday", s, split[1])
   555  		}
   556  	} else {
   557  		parsed.End = parsed.Start
   558  	}
   559  
   560  	if parsed.End.Pos < parsed.Start.Pos {
   561  		// eg. mon4-mon1
   562  		return span, fmt.Errorf("cannot parse %q: unsupported schedule", s)
   563  	}
   564  
   565  	if (parsed.Start.Pos != EveryWeek) != (parsed.End.Pos != EveryWeek) {
   566  		return span, fmt.Errorf("cannot parse %q: week number must be present for both weekdays or neither", s)
   567  	}
   568  
   569  	return parsed, nil
   570  }
   571  
   572  // parseClockSpan parses a time specification which can either be `<hh>:<mm>` or
   573  // `<hh>:<mm>[-~]<hh>:<mm>[/count]`. Alternatively the span can be one of
   574  // special tokens `-`, `~` (followed by an optional [/count]) that indicate a
   575  // whole day span, or a whole day span with spread respectively.
   576  func parseClockSpan(s string) (span ClockSpan, err error) {
   577  	var rest string
   578  
   579  	// timespan = time ( "-" / "~" ) time [ "/" ( time / count ) ]
   580  
   581  	span.Split, rest, err = parseCount(s)
   582  	if err != nil {
   583  		return ClockSpan{}, fmt.Errorf("cannot parse %q: not a valid interval", s)
   584  	}
   585  
   586  	if strings.Contains(rest, spreadToken) {
   587  		// timespan uses "~" to indicate that the actual event
   588  		// time is to be spread.
   589  		span.Spread = true
   590  		rest = strings.Replace(rest, spreadToken, spanToken, 1)
   591  	}
   592  
   593  	if rest == "-" {
   594  		// whole day span
   595  		span.Start = Clock{0, 0}
   596  		span.End = Clock{24, 0}
   597  	} else if strings.Contains(rest, spanToken) {
   598  		span.Start, span.End, err = parseClockRange(rest)
   599  	} else {
   600  		span.Start, err = ParseClock(rest)
   601  		span.End = span.Start
   602  	}
   603  
   604  	if err != nil {
   605  		return ClockSpan{}, fmt.Errorf("cannot parse %q: not a valid time", s)
   606  	}
   607  
   608  	return span, nil
   609  }
   610  
   611  // parseWeekday parses a single weekday (eg. wed, mon5),
   612  func parseWeekday(s string) (week Week, err error) {
   613  	l := len(s)
   614  	if l != 3 && l != 4 {
   615  		return week, fmt.Errorf("cannot parse %q: invalid format", s)
   616  	}
   617  
   618  	var day = s
   619  	var pos uint
   620  	if l == 4 {
   621  		day = s[0:3]
   622  		v, err := strconv.ParseUint(s[3:], 10, 32)
   623  		if err != nil || v < 1 || v > 5 {
   624  			return week, fmt.Errorf("cannot parse %q: invalid week number", s)
   625  		}
   626  		pos = uint(v)
   627  	}
   628  
   629  	weekday, ok := weekdayMap[day]
   630  	if !ok {
   631  		return week, fmt.Errorf("cannot parse %q: invalid weekday", s)
   632  	}
   633  
   634  	return Week{weekday, pos}, nil
   635  }
   636  
   637  // parseCount will parse the string containing a count token and return the
   638  // count count and the rest of the string with count information removed, or an error.
   639  func parseCount(s string) (count uint, rest string, err error) {
   640  	if !strings.Contains(s, countToken) {
   641  		return 0, s, nil
   642  	}
   643  
   644  	// timespan = time ( "-" / "~" ) time [ "/" ( time / count ) ]
   645  	split := strings.Split(s, countToken)
   646  	if len(split) != 2 {
   647  		return 0, "", fmt.Errorf("cannot parse %q: invalid event count", s)
   648  	}
   649  
   650  	rest = split[0]
   651  	countStr := split[1]
   652  	c, err := strconv.ParseUint(countStr, 10, 32)
   653  	if err != nil || c == 0 {
   654  		return 0, "", fmt.Errorf("cannot parse %q: invalid event interval", s)
   655  	}
   656  	return uint(c), rest, nil
   657  }
   658  
   659  const (
   660  	spanToken   = "-"
   661  	spreadToken = "~"
   662  	countToken  = "/"
   663  )
   664  
   665  // Parse event set into a Schedule
   666  func parseEventSet(s string) (*Schedule, error) {
   667  	var fragments []string
   668  	// split eventset into fragments
   669  	//     eventset = wdaylist / timelist / wdaylist "," timelist
   670  	// or wdaysets
   671  	//     wdaylist = wdayset *( "," wdayset )
   672  	// or timesets
   673  	//     timelist = timeset *( "," timeset )
   674  	//
   675  	// NOTE: the syntax is ambiguous in the sense the type of a 'set' is now
   676  	// explicitly indicated, fragments with : inside are expected to be
   677  	// timesets
   678  
   679  	if els := strings.Split(s, ","); len(els) > 1 {
   680  		fragments = els
   681  	} else {
   682  		fragments = []string{s}
   683  	}
   684  
   685  	var schedule Schedule
   686  	// indicates that any further fragment must be timesets
   687  	var expectTime bool
   688  
   689  	for _, fragment := range fragments {
   690  		if len(fragment) == 0 {
   691  			return nil, fmt.Errorf("cannot parse %q: not a valid fragment", s)
   692  		}
   693  
   694  		if strings.Contains(fragment, ":") {
   695  			// must be a clock span
   696  			span, err := parseClockSpan(fragment)
   697  			if err != nil {
   698  				return nil, err
   699  			}
   700  			schedule.ClockSpans = append(schedule.ClockSpans, span)
   701  
   702  			expectTime = true
   703  
   704  		} else if !expectTime {
   705  			// we're not expecting timeset , so this must be a wdayset
   706  			span, err := parseWeekSpan(fragment)
   707  			if err != nil {
   708  				return nil, err
   709  			}
   710  			schedule.WeekSpans = append(schedule.WeekSpans, span)
   711  		} else {
   712  			// not a timeset
   713  			return nil, fmt.Errorf("cannot parse %q: invalid schedule fragment", fragment)
   714  		}
   715  	}
   716  
   717  	return &schedule, nil
   718  }
   719  
   720  // Includes checks whether given time t falls inside the time range covered by
   721  // the schedule. A single time schedule eg. '10:00' is treated as spanning the
   722  // time [10:00, 10:01)
   723  func (sched *Schedule) Includes(t time.Time) bool {
   724  	if len(sched.WeekSpans) > 0 {
   725  		var weekMatch bool
   726  		for _, week := range sched.WeekSpans {
   727  			if week.Match(t) {
   728  				weekMatch = true
   729  				break
   730  			}
   731  		}
   732  		if !weekMatch {
   733  			return false
   734  		}
   735  	}
   736  
   737  	for _, tspan := range sched.flattenedClockSpans() {
   738  		window := tspan.Window(t)
   739  		if window.End.Equal(window.Start) {
   740  			// schedule granularity is a minute, a schedule '10:00'
   741  			// in fact is: [10:00, 10:01)
   742  			window.End = window.End.Add(time.Minute)
   743  		}
   744  		// Includes() does the [start,end] check, but we really what
   745  		// [start,end)
   746  		if window.Includes(t) && t.Before(window.End) {
   747  			return true
   748  		}
   749  	}
   750  	return false
   751  }
   752  
   753  // Includes checks whether given time t falls inside the time range covered by
   754  // a schedule.
   755  func Includes(schedule []*Schedule, t time.Time) bool {
   756  	for _, sched := range schedule {
   757  		if sched.Includes(t) {
   758  			return true
   759  		}
   760  	}
   761  	return false
   762  }