/*! * Copyright 2013 Raymond Hill * * Project: github.com/gorhill/cronexpr * File: cronexpr_next.go * Version: 1.0 * License: pick the one which suits you : * GPL v3 see * APL v2 see * */ package cronexpr /******************************************************************************/ import ( "sort" "time" ) /******************************************************************************/ var dowNormalizedOffsets = [][]int{ {1, 8, 15, 22, 29}, {2, 9, 16, 23, 30}, {3, 10, 17, 24, 31}, {4, 11, 18, 25}, {5, 12, 19, 26}, {6, 13, 20, 27}, {7, 14, 21, 28}, } /******************************************************************************/ func (expr *Expression) nextYear(t time.Time) time.Time { // Find index at which item in list is greater or equal to // candidate year i := sort.SearchInts(expr.yearList, t.Year()+1) if i == len(expr.yearList) { return time.Time{} } // Year changed, need to recalculate actual days of month expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(expr.yearList[i], expr.monthList[0]) if len(expr.actualDaysOfMonthList) == 0 { return expr.nextMonth(time.Date( expr.yearList[i], time.Month(expr.monthList[0]), 1, expr.hourList[0], expr.minuteList[0], expr.secondList[0], 0, t.Location())) } next := time.Date( expr.yearList[i], time.Month(expr.monthList[0]), expr.actualDaysOfMonthList[0], expr.hourList[0], expr.minuteList[0], expr.secondList[0], 0, time.UTC) return expr.nextTime(t, next) } /******************************************************************************/ func (expr *Expression) nextMonth(t time.Time) time.Time { // Find index at which item in list is greater or equal to // candidate month i := sort.SearchInts(expr.monthList, int(t.Month())+1) if i == len(expr.monthList) { return expr.nextYear(t) } // Month changed, need to recalculate actual days of month expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(t.Year(), expr.monthList[i]) if len(expr.actualDaysOfMonthList) == 0 { return expr.nextMonth(time.Date( t.Year(), time.Month(expr.monthList[i]), 1, expr.hourList[0], expr.minuteList[0], expr.secondList[0], 0, t.Location())) } next := time.Date( t.Year(), time.Month(expr.monthList[i]), expr.actualDaysOfMonthList[0], expr.hourList[0], expr.minuteList[0], expr.secondList[0], 0, time.UTC) return expr.nextTime(t, next) } /******************************************************************************/ func (expr *Expression) nextDayOfMonth(t time.Time) time.Time { // Find index at which item in list is greater or equal to // candidate day of month i := sort.SearchInts(expr.actualDaysOfMonthList, t.Day()+1) if i == len(expr.actualDaysOfMonthList) { return expr.nextMonth(t) } next := time.Date( t.Year(), t.Month(), expr.actualDaysOfMonthList[i], expr.hourList[0], expr.minuteList[0], expr.secondList[0], 0, time.UTC) return expr.nextTime(t, next) } /******************************************************************************/ func (expr *Expression) nextHour(t time.Time) time.Time { // Find index at which item in list is greater or equal to // candidate hour i := sort.SearchInts(expr.hourList, t.Hour()+1) if i == len(expr.hourList) { return expr.nextDayOfMonth(t) } next := time.Date( t.Year(), t.Month(), t.Day(), expr.hourList[i], expr.minuteList[0], expr.secondList[0], 0, time.UTC) return expr.nextTime(t, next) } /******************************************************************************/ func (expr *Expression) nextMinute(t time.Time) time.Time { // Find index at which item in list is greater or equal to // candidate minute i := sort.SearchInts(expr.minuteList, t.Minute()+1) if i == len(expr.minuteList) { return expr.nextHour(t) } next := time.Date( t.Year(), t.Month(), t.Day(), t.Hour(), expr.minuteList[i], expr.secondList[0], 0, time.UTC) return expr.nextTime(t, next) } /******************************************************************************/ func (expr *Expression) nextSecond(t time.Time) time.Time { // nextSecond() assumes all other fields are exactly matched // to the cron expression // Find index at which item in list is greater or equal to // candidate second i := sort.SearchInts(expr.secondList, t.Second()+1) if i == len(expr.secondList) { return expr.nextMinute(t) } next := time.Date( t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), expr.secondList[i], 0, time.UTC) return expr.nextTime(t, next) } func (expr *Expression) roundTime(t time.Time) time.Time { roundingExpr := new(Expression) *roundingExpr = *expr roundingExpr.rounding = true i := sort.SearchInts(expr.hourList, t.Hour()) if i == len(expr.hourList) || expr.hourList[i] != t.Hour() { return roundingExpr.nextHour(t) } i = sort.SearchInts(expr.minuteList, t.Minute()) if i == len(expr.minuteList) || expr.minuteList[i] != t.Minute() { return roundingExpr.nextMinute(t) } i = sort.SearchInts(expr.secondList, t.Second()) if i == len(expr.secondList) || expr.secondList[i] != t.Second() { return roundingExpr.nextSecond(t) } return t } func (expr *Expression) isRounded(t time.Time) bool { i := sort.SearchInts(expr.hourList, t.Hour()) if i == len(expr.hourList) || expr.hourList[i] != t.Hour() { return false } i = sort.SearchInts(expr.minuteList, t.Minute()) if i == len(expr.minuteList) || expr.minuteList[i] != t.Minute() { return false } i = sort.SearchInts(expr.secondList, t.Second()) if i == len(expr.secondList) || expr.secondList[i] != t.Second() { return false } return true } func (expr *Expression) nextTime(prev, next time.Time) time.Time { dstFlags := expr.options.DSTFlags t := prev.Add(noTZDiff(prev, next)) offsetDiff := utcOffset(t) - utcOffset(prev) // a dst leap occurred if offsetDiff > 0 { if dstFlags&DSTLeapUnskip != 0 { return findTimeOfDSTChange(prev, t).Add(1 * time.Second) } return expr.roundTime(t) } // a dst fall occurred if offsetDiff < 0 { twinT := findTwinTime(prev) if !twinT.IsZero() { if dstFlags&DSTFallFireLate != 0 { return twinT } if dstFlags&DSTFallFireEarly != 0 { // skip the twin time return expr.Next(expr.roundTime(twinT)) } } if dstFlags&DSTFallFireEarly != 0 { return t } return expr.roundTime(t) } twinT := findTwinTime(t) if !twinT.IsZero() { if dstFlags&DSTFallFireEarly != 0 { return t } if dstFlags&DSTFallFireLate != 0 { return twinT } } if dstFlags&DSTFallFireLate == 0 && !expr.rounding { twinT = findTwinTime(prev) if !twinT.IsZero() && twinT.Before(prev) && !expr.isRounded(prev) { return expr.Next(t) } } return t } /******************************************************************************/ func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int { actualDaysOfMonthMap := make(map[int]bool) firstDayOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) lastDayOfMonth := firstDayOfMonth.AddDate(0, 1, -1) // As per crontab man page (http://linux.die.net/man/5/crontab#): // "The day of a command's execution can be specified by two // "fields - day of month, and day of week. If both fields are // "restricted (ie, aren't *), the command will be run when // "either field matches the current time" // If both fields are not restricted, all days of the month are a hit if expr.daysOfMonthRestricted == false && expr.daysOfWeekRestricted == false { return genericDefaultList[1 : lastDayOfMonth.Day()+1] } // day-of-month != `*` if expr.daysOfMonthRestricted { // Last day of month if expr.lastDayOfMonth { actualDaysOfMonthMap[lastDayOfMonth.Day()] = true } // Last work day of month if expr.lastWorkdayOfMonth { actualDaysOfMonthMap[workdayOfMonth(lastDayOfMonth, lastDayOfMonth)] = true } // Days of month for v := range expr.daysOfMonth { // Ignore days beyond end of month if v <= lastDayOfMonth.Day() { actualDaysOfMonthMap[v] = true } } // Work days of month // As per Wikipedia: month boundaries are not crossed. for v := range expr.workdaysOfMonth { // Ignore days beyond end of month if v <= lastDayOfMonth.Day() { actualDaysOfMonthMap[workdayOfMonth(firstDayOfMonth.AddDate(0, 0, v-1), lastDayOfMonth)] = true } } } // day-of-week != `*` if expr.daysOfWeekRestricted { // How far first sunday is from first day of month offset := 7 - int(firstDayOfMonth.Weekday()) // days of week // offset : (7 - day_of_week_of_1st_day_of_month) // target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7 for v := range expr.daysOfWeek { w := dowNormalizedOffsets[(offset+v)%7] actualDaysOfMonthMap[w[0]] = true actualDaysOfMonthMap[w[1]] = true actualDaysOfMonthMap[w[2]] = true actualDaysOfMonthMap[w[3]] = true if len(w) > 4 && w[4] <= lastDayOfMonth.Day() { actualDaysOfMonthMap[w[4]] = true } } // days of week of specific week in the month // offset : (7 - day_of_week_of_1st_day_of_month) // target : 1 + (7 * week_of_month) + (offset + day_of_week) % 7 for v := range expr.specificWeekDaysOfWeek { v = 1 + 7*(v/7) + (offset+v)%7 if v <= lastDayOfMonth.Day() { actualDaysOfMonthMap[v] = true } } // Last days of week of the month lastWeekOrigin := firstDayOfMonth.AddDate(0, 1, -7) offset = 7 - int(lastWeekOrigin.Weekday()) for v := range expr.lastWeekDaysOfWeek { v = lastWeekOrigin.Day() + (offset+v)%7 if v <= lastDayOfMonth.Day() { actualDaysOfMonthMap[v] = true } } } return toList(actualDaysOfMonthMap) } func workdayOfMonth(targetDom, lastDom time.Time) int { // If saturday, then friday // If sunday, then monday dom := targetDom.Day() dow := targetDom.Weekday() if dow == time.Saturday { if dom > 1 { dom -= 1 } else { dom += 2 } } else if dow == time.Sunday { if dom < lastDom.Day() { dom += 1 } else { dom -= 2 } } return dom } func utcOffset(t time.Time) int { _, offset := t.Zone() return offset } func noTZ(t time.Time) time.Time { return t.UTC().Add(time.Duration(utcOffset(t)) * time.Second) } func noTZDiff(t1, t2 time.Time) time.Duration { t1 = noTZ(t1) t2 = noTZ(t2) return t2.Sub(t1) } // findTimeOfDSTChange returns the time a second before a DST change occurs, // and returns zero time in case there's no DST change. func findTimeOfDSTChange(t1, t2 time.Time) time.Time { if t1.Location() != t2.Location() || utcOffset(t1) == utcOffset(t2) || t1.Location() == time.UTC { return time.Time{} } // make sure t2 > t1 if t2.Before(t1) { t := t2 t1 = t2 t2 = t } // do a binary search to find the time one second before the dst change len := t2.Unix() - t1.Unix() var a int64 b := len for len > 1 { len = (b - a + 1) / 2 if utcOffset(t1.Add(time.Duration(a+len)*time.Second)) != utcOffset(t1) { b = a + len } else { a = a + len } } return t1.Add(time.Duration(a) * time.Second) } // When a DST fall accurs, a certain interval of time is repeated. Once // in DST time and once in standard time. // findTwinTime tries to find the repated "twin" time if one exists. func findTwinTime(t time.Time) time.Time { offsetDiff := utcOffset(t.Add(12*time.Hour)) - utcOffset(t) // a fall occurs within the next 12 hours if offsetDiff < 0 { border := findTimeOfDSTChange(t, t.Add(12*time.Hour)) t0 := border.Add(time.Duration(offsetDiff) * time.Second) if t0.After(t) { return t } dur := t.Sub(t0) return border.Add(dur) } offsetDiff = utcOffset(t) - utcOffset(t.Add(-12*time.Second)) // a fall occurred in the past 12 hours if offsetDiff < 0 { border := findTimeOfDSTChange(t.Add(-12*time.Hour), t) t0 := border.Add(time.Duration(offsetDiff) * time.Second) if t0.Add(time.Duration(-2*offsetDiff) * time.Second).Before(t) { return t } dur := t.Sub(border) return t0.Add(dur) } return time.Time{} }