package main import ( "fmt" "iter" "math/bits" "slices" "strconv" "strings" "time" "unicode" ) type Frequency string const ( Yearly Frequency = "yearly" Monthly Frequency = "monthly" Weekly Frequency = "weekly" Daily Frequency = "daily" Once Frequency = "once" ) // Day of the week with week of the month encoded. // 4 bits 4-7: week of the month 0b00000000_01111111 // 7 bits 8-16: day of the week 0b00001111_00000000 type Day uint16 const ( Sunday Day = 1 << iota Monday Day = 1 << iota Tuesday Day = 1 << iota Wednesday Day = 1 << iota Thursday Day = 1 << iota Friday Day = 1 << iota Saturday Day = 1 << iota ) func NewDay(d Day, ofMonth int) Day { return Day(ofMonth&15<<8 | int(d&127)) } func (d *Day) Set(day Day, weekOfMonth int) { *d = Day(weekOfMonth&15<<8 | int(day&127)) } func (d Day) Day() Day { return Day(int(d) & 127) } func (d Day) WeekOfMonth() int { weekOfMonth := int(d>>8) & 15 if weekOfMonth&0b1000 > 0 { return -((^weekOfMonth & 15) + 1) } return weekOfMonth } func (d Day) DayOfWeek() int { return bits.TrailingZeros16(uint16(d & 127)) } func (d Day) String() string { var day string switch d.Day() { case Monday: day = "Mon" case Tuesday: day = "Tue" case Wednesday: day = "Wed" case Thursday: day = "Thu" case Friday: day = "Fri" case Saturday: day = "Sat" case Sunday: day = "Sun" } if weekOfMonth := d.WeekOfMonth(); weekOfMonth != 0 { return fmt.Sprintf("%-d%s", weekOfMonth, day) } return day } func (d Day) Less(a Day) bool { return d < a } func (d *Day) Parse(text string) (int, error) { if len(text) == 0 { return 0, fmt.Errorf("invalid day") } var weekOfMonth int var day string i := 0 for _, c := range text[i:] { if unicode.IsDigit(c) || c == '-' { i++ continue } break } weekOfMonth, _ = strconv.Atoi(text[:i]) di := 0 for _, c := range text[i:] { if unicode.IsLetter(c) { di++ continue } break } day = text[i : i+di] i += di switch strings.ToLower(day) { case "mon", "monday": d.Set(Monday, weekOfMonth) case "tue", "tuesday": d.Set(Tuesday, weekOfMonth) case "wed", "wednesday": d.Set(Wednesday, weekOfMonth) case "thu", "thursday", "thur", "thr": d.Set(Thursday, weekOfMonth) case "fri", "friday": d.Set(Friday, weekOfMonth) case "sat", "saturday": d.Set(Saturday, weekOfMonth) case "sun", "sunday": d.Set(Sunday, weekOfMonth) default: return 0, fmt.Errorf("invalid day") } return i, nil } // Month of the year with day of the day of month encoded. // 5 bits : day of the month 0b00000000_00000000_00011111 // 22 bits : month of the year 0b00001111_11111111_00000000 type Month int32 const ( January Month = 1 << (iota + 8) // 0b0000_00000001_00000000 February Month = 1 << (iota + 8) // 0b0000_00000010_00000000 March Month = 1 << (iota + 8) // 0b0000_00000100_00000000 April Month = 1 << (iota + 8) // 0b0000_00001000_00000000 May Month = 1 << (iota + 8) // 0b0000_00010000_00000000 June Month = 1 << (iota + 8) // 0b0000_00100000_00000000 July Month = 1 << (iota + 8) // 0b0000_01000000_00000000 August Month = 1 << (iota + 8) // 0b0000_10000000_00000000 September Month = 1 << (iota + 8) // 0b0001_00000000_00000000 October Month = 1 << (iota + 8) // 0b0010_00000000_00000000 November Month = 1 << (iota + 8) // 0b0100_00000000_00000000 December Month = 1 << (iota + 8) // 0b1000_00000000_00000000 // day of month -31-31 0b1-0b0011_1111 ) func NewMonth(m Month, dayOfMonth int) Month { return Month(int(m)&^63 | dayOfMonth&63) } func (m Month) Month() Month { return Month(int(m) & ^int(255)) } func (m Month) DayOfMonth() int { dayOfMonth := int(m) & 0b11_1111 if dayOfMonth&0b0010_0000 > 0 { return -((^dayOfMonth & 0b1_1111) + 1) } return dayOfMonth } func (m Month) MonthOfYear() time.Month { return time.Month(bits.TrailingZeros32(uint32(m>>8)) + 1) } func (m Month) String() string { var month string switch m.Month() { case January: month = "Jan" case February: month = "Feb" case March: month = "Mar" case April: month = "Apr" case May: month = "May" case June: month = "Jun" case July: month = "Jul" case August: month = "Aug" case September: month = "Sep" case October: month = "Oct" case November: month = "Nov" case December: month = "Dec" } if dayOfMonth := m.DayOfMonth(); dayOfMonth != 0 { return fmt.Sprintf("%s%-d", month, dayOfMonth) } return month } func (m Month) Less(a Month) bool { return m < a } func (w *Month) Parse(text string) (int, error) { if len(text) == 0 { return 0, fmt.Errorf("invalid month") } mon, day := "", 0 i := 0 for _, c := range text { if !unicode.IsLetter(c) { break } i++ } mon = text[:i] if i < len(text) { negative := false if text[i] == '-' { i++ negative = true } di := 0 for _, c := range text[i:] { if !unicode.IsDigit(c) { break } di++ } day, _ = strconv.Atoi(text[i : i+di]) i += di if negative { day = -day } } switch strings.ToLower(mon) { case "jan", "january": *w = NewMonth(January, day) case "feb", "february": *w = NewMonth(February, day) case "mar", "march": *w = NewMonth(March, day) case "apr", "april": *w = NewMonth(April, day) case "may": *w = NewMonth(May, day) case "jun", "june": *w = NewMonth(June, day) case "jul", "july": *w = NewMonth(July, day) case "aug", "august": *w = NewMonth(August, day) case "sep", "sept", "september": *w = NewMonth(September, day) case "oct", "october": *w = NewMonth(October, day) case "nov", "november": *w = NewMonth(November, day) case "dec", "december": *w = NewMonth(December, day) default: return i, fmt.Errorf("unknown month %q", mon) } return i, nil } type Slate struct { Date Date Frequency Frequency Until time.Time Interval int Count int } func (s *Slate) Parse(text string) (int, error) { i, err := s.Date.Parse(text) if err != nil { return 0, err } return i, nil } // String func (s Slate) String() string { return s.Date.String() } func (s Slate) IsZero() bool { return s.Date.IsZero() && s.Frequency == "" && s.Until.IsZero() && s.Interval == 0 && s.Count == 0 } func (s Slate) Project(start time.Time) iter.Seq[time.Time] { if start.IsZero() { start = time.Now() } if !s.Until.IsZero() && s.Until.Before(start) { return slices.Values[[]time.Time](nil) } var increment time.Duration switch s.Frequency { case Once: increment = 0 case Daily: increment = time.Hour * 24 case Weekly: increment = time.Hour * 24 * 7 case Monthly: increment = time.Hour * 24 * 30 case Yearly: increment = time.Hour * 24 * 365 } if s.Interval > 0 { increment *= time.Duration(s.Interval) } return func(yield func(time.Time) bool) { for i := 0; i < s.Count; { for d := range s.Date.Project(start) { if !s.Until.IsZero() && s.Until.Before(d) { return } if !yield(d) { return } i++ } start = start.Add(increment) } } } type Week int8 func NewWeek(ofYear int) Week { return Week(ofYear & 63) } func (w Week) WeekOfYear() int { return int(w) } func (w Week) String() string { return fmt.Sprintf("w%02d", int8(w)) } func (w Week) Less(a Week) bool { return w < a } func (w *Week) Parse(text string) (int, error) { if len(text) == 0 { return 0, fmt.Errorf("invalid week") } if !strings.HasPrefix(strings.ToLower(text), "w") { return 0, fmt.Errorf("invalid week") } i := 1 for _, c := range text[1:] { if unicode.IsDigit(c) || c == '-' { i++ continue } break } wk, err := strconv.Atoi(text[1:i]) if err != nil { return 0, err } *w = Week(wk) return i, nil } type Date struct { Year int Month int Day int MonthOfYear []Month WeekOfYear []Week DayOfWeek []Day } func (d Date) String() string { v, _ := d.MarshalText() return string(v) } func (d Date) MarshalText() ([]byte, error) { hasYear := d.Year != 0 hasMonth := d.Month != 0 hasDay := d.Day != 0 onlyYear := hasYear && !hasMonth && !hasDay yearAndMonth := hasYear && hasMonth && !hasDay yearMonthDay := hasYear && hasMonth && hasDay hasMonthOfYear := len(d.MonthOfYear) > 0 hasWeekOfYear := len(d.WeekOfYear) > 0 hasDayOfWeek := len(d.DayOfWeek) > 0 // onlyYear onlyWeekOfYear := hasWeekOfYear && !hasDayOfWeek && !hasMonthOfYear onlyMonthOfYear := hasMonthOfYear && !hasWeekOfYear && !hasDayOfWeek weekOfYearAndDayOfWeek := hasWeekOfYear && !hasMonthOfYear && hasDayOfWeek monthOfYearAndDayOfWeek := hasMonthOfYear && !hasWeekOfYear && hasDayOfWeek // yearAndMonth onlyDayOfWeek := hasDayOfWeek && !hasWeekOfYear && !hasMonthOfYear switch { case onlyYear && !hasMonthOfYear && !hasWeekOfYear && !hasDayOfWeek: return []byte(fmt.Sprintf("%04d", d.Year)), nil case onlyYear && onlyWeekOfYear: s := apply(d.WeekOfYear, func(w Week) string { return w.String() }) return []byte(fmt.Sprintf("%04d %s", d.Year, strings.Join(s, ", "))), nil case onlyYear && weekOfYearAndDayOfWeek: z := zip(d.WeekOfYear, d.DayOfWeek) s := apply(z, func(w pair[Week, Day]) string { return fmt.Sprintf("%s %s", w.A.String(), w.B.Day().String()) }) return []byte(fmt.Sprintf("%04d %s", d.Year, strings.Join(s, ", "))), nil case onlyYear && monthOfYearAndDayOfWeek: z := zip(d.MonthOfYear, d.DayOfWeek) s := apply(z, func(w pair[Month, Day]) string { return fmt.Sprintf("%s %s", w.A.Month().String(), w.B.String()) }) return []byte(fmt.Sprintf("%04d %s", d.Year, strings.Join(s, ", "))), nil case onlyYear && onlyMonthOfYear: s := apply(d.MonthOfYear, func(w Month) string { return w.String() }) return []byte(fmt.Sprintf("%04d %s", d.Year, strings.Join(s, ", "))), nil case yearAndMonth && !hasDayOfWeek: return []byte(fmt.Sprintf("%04d-%02d", d.Year, d.Month)), nil case yearAndMonth && onlyDayOfWeek: s := apply(d.DayOfWeek, func(w Day) string { return w.String() }) return []byte(fmt.Sprintf("%04d-%02d %s", d.Year, d.Month, strings.Join(s, ", "))), nil case yearMonthDay: return []byte(fmt.Sprintf("%04d-%02d-%02d", d.Year, d.Month, d.Day)), nil } return nil, nil } func (d Date) IsZero() bool { return d.Year == 0 && d.Month == 0 && d.Day == 0 && len(d.MonthOfYear) == 0 && len(d.WeekOfYear) == 0 && len(d.DayOfWeek) == 0 } // ## Date formats ## // YYYY - year // 2024 - year 2024 // 2026 - year 2026 // YYYY moy Ndow[, ...] - year moy dow // 2024 Jan 1Mon, Feb -2Tue - first monday of january 2024 and second from last tuesday of february 2024 // YYYY moyNN[, ...] - year moyNN // 2024 Jan1, Feb-2 - first day of january 2024 and second last day of february 2024 // YYYY 'w'NN[, ...] - year woy // 2024 w01, w05 - first week of 2024 and fifth week of 2024 // YYYY 'w'NN dow[, ...] - year woy dow // 2024 w01 Mon - first monday of 2024 // 2024 w02 Mon, w05 Tue - first monday on second week of 2024 and first tuesday on fifth week of 2024 // YYYY-MM - month // 2024-01 - month of january 2024 // YYYY-MM Ndow[, ...] month day of week // 2024-01 1Mon, 2Tue // YYYY-MM-DD day // 2024-01-01 - first day of january 2024 func (d *Date) Parse(text string) (int, error) { i := 0 ds, text, ok := strings.Cut(text, " ") i += len(ds) if ok { i++ } if t, err := time.Parse("2006-01-02", ds); err == nil { d.Year = t.Year() d.Month = int(t.Month()) d.Day = t.Day() return i, nil } if t, err := time.Parse("2006-01", ds); err == nil { d.Year = t.Year() d.Month = int(t.Month()) var di int di, d.DayOfWeek, err = repeatDayOfWeek(text) if err == nil { i += di return i, nil } return i, nil } if t, err := time.Parse("2006", ds); err == nil { d.Year = t.Year() var di int di, d.WeekOfYear, d.DayOfWeek, err = repeatWeekOfYearAndDayOfWeek(text) if err == nil { i += di return i, nil } di, d.MonthOfYear, d.DayOfWeek, err = repeatMonthOfYearAndDayOfWeek(text) if err == nil { i += di return i, nil } if di, d.WeekOfYear, err = repeatWeekOfYear(text); err == nil { i += di return i, nil } if di, d.MonthOfYear, err = repeatMonth(text); err == nil { i += di return i, nil } if di, d.DayOfWeek, err = repeatDayOfWeek(text); err == nil { i += di return i, nil } return i, nil } return 0, fmt.Errorf("invalid date") } func (d Date) Project(start time.Time) iter.Seq[time.Time] { hasYear := d.Year != 0 hasMonth := d.Month != 0 hasDay := d.Day != 0 onlyYear := hasYear && !hasMonth && !hasDay yearAndMonth := hasYear && hasMonth && !hasDay yearMonthDay := hasYear && hasMonth && hasDay hasMonthOfYear := len(d.MonthOfYear) > 0 hasWeekOfYear := len(d.WeekOfYear) > 0 hasDayOfWeek := len(d.DayOfWeek) > 0 // onlyYear onlyWeekOfYear := hasWeekOfYear && !hasDayOfWeek && !hasMonthOfYear onlyMonthOfYear := hasMonthOfYear && !hasWeekOfYear && !hasDayOfWeek weekOfYearAndDayOfWeek := hasWeekOfYear && !hasMonthOfYear && hasDayOfWeek monthOfYearAndDayOfWeek := hasMonthOfYear && !hasWeekOfYear && hasDayOfWeek // yearAndMonth onlyDayOfWeek := hasDayOfWeek && !hasWeekOfYear && !hasMonthOfYear switch { case onlyYear && !hasMonthOfYear && !hasWeekOfYear && !hasDayOfWeek: year := max(d.Year, start.Year()) month := time.Month(max(1, d.Month)) day := max(1, d.Day) dt := time.Date(year, month, day, 0, 0, 0, 0, start.Location()) dt = endOfYear(dt) return func(yield func(time.Time) bool) { yield(dt) return } case onlyYear && onlyWeekOfYear: year := max(start.Year(), d.Year) return func(yield func(time.Time) bool) { for _, w := range d.WeekOfYear { if !yield(dayOfWeekNumber(year, w.WeekOfYear(), 6)) { return } } } case onlyYear && weekOfYearAndDayOfWeek: year := max(start.Year(), d.Year) return func(yield func(time.Time) bool) { for i, w := range d.WeekOfYear { dt := dayOfWeekNumber(year, w.WeekOfYear(), int(d.DayOfWeek[i].DayOfWeek())) if start.After(dt) { continue } if !yield(dt) { return } } } case onlyYear && monthOfYearAndDayOfWeek: year := max(start.Year(), d.Year) return func(yield func(time.Time) bool) { for i, m := range d.MonthOfYear { month := m.MonthOfYear() dt := time.Date(year, month, 1, 0, 0, 0, 0, start.Location()) dt = monthWithNWeek(d.DayOfWeek[i], dt) if start.After(dt) { continue } if !yield(dt) { return } } } case onlyYear && onlyMonthOfYear: year := max(start.Year(), d.Year) return func(yield func(time.Time) bool) { for _, m := range d.MonthOfYear { month := time.Month(m.MonthOfYear()) var dt time.Time if day := m.DayOfMonth(); day == 0 { dt = time.Date(year, month, 1, 0, 0, 0, 0, start.Location()) dt = endOfMonth(dt) } else { dt = time.Date(year, month, day, 0, 0, 0, 0, start.Location()) } if start.After(dt) { continue } if !yield(dt) { return } } } case yearAndMonth && !hasDayOfWeek: year := d.Year month := max(start.Month(), time.Month(d.Month)) if start.Year() > d.Year { year = start.Year() month = start.Month() } return func(yield func(time.Time) bool) { dt := time.Date(year, month, 1, 0, 0, 0, 0, start.Location()) dt = endOfMonth(dt) if !yield(dt) { return } } case yearAndMonth && onlyDayOfWeek: year := max(start.Year(), d.Year) month := max(start.Month(), time.Month(d.Month)) day := max(start.Day(), d.Day) if start.Year() > d.Year { year = start.Year() month = start.Month() day = start.Day() } return func(yield func(time.Time) bool) { for _, d := range d.DayOfWeek { dt := time.Date(year, month, day, 0, 0, 0, 0, start.Location()) dt = beginningOfWeek(dt).AddDate(0,0, d.DayOfWeek()) if start.After(dt) { continue } if !yield(dt) { return } } } case yearMonthDay: year := max(start.Year(), d.Year) month := max(start.Month(), time.Month(d.Month)) day := max(start.Day(), d.Day) d := time.Date(year, month, day, 0, 0, 0, 0, start.Location()) return func(yield func(time.Time) bool) { yield(d) return } } return slices.Values[[]time.Time](nil) } func (d Date) Less(a Date) bool { if d.Year < a.Year { return true } if d.Year > a.Year { return false } if d.Month != 0 && d.Month < a.Month { return true } if d.Month != 0 && d.Month > a.Month { return false } if d.Day != 0 && d.Day < a.Day { return true } if d.Day != 0 && d.Day > a.Day { return false } return true } func apply[T, F any](arr []T, fn func(T) F) []F { res := make([]F, len(arr)) for i, v := range arr { res[i] = fn(v) } return res } type pair[A, B any] struct { A A B B } func zip[A, B any](a []A, b []B) []pair[A, B] { l := min(len(a), len(b)) res := make([]pair[A, B], l) for i := range a { res[i] = struct { A A B B }{a[i], b[i]} } return res } func repeatDayOfWeek(text string) (int, []Day, error) { var i int var lis []Day next := true for next { next = false var dow Day di, err := dow.Parse(text) if err != nil { return 0, nil, err } lis = append(lis, dow) text = text[di:] i += di for di, c := range text { if c == ',' { next = true continue } if c == ' ' { continue } text = text[di:] i += di break } } return i, lis, nil } func repeatMonth(text string) (int, []Month, error) { var i int var lis []Month next := true for next { next = false var month Month di, err := month.Parse(text) if err != nil { return 0, nil, err } lis = append(lis, month) text = text[di:] i += di for di, c := range text { if c == ',' { next = true continue } if c == ' ' { continue } text = text[di:] i += di break } } return i, lis, nil } func repeatWeekOfYear(text string) (int, []Week, error) { var i int var lis []Week next := true for next { next = false var dow Week di, err := dow.Parse(text) if err != nil { return 0, nil, err } lis = append(lis, dow) text = text[di:] i += di for di, c := range text { if c == ',' { next = true continue } if c == ' ' { continue } text = text[di:] i += di break } } return i, lis, nil } func repeatWeekOfYearAndDayOfWeek(text string) (int, []Week, []Day, error) { var i int var weeks []Week var days []Day next := true for next { next = false var dow Week di, err := dow.Parse(text) if err != nil { return 0, nil, nil, err } weeks = append(weeks, dow) text = text[di:] i += di if len(text) == 0 || text[0] != ' ' { return 0, nil, nil, fmt.Errorf("invalid date") } text = text[1:] i++ var day Day di, err = day.Parse(text) if err != nil { return 0, nil, nil, err } days = append(days, day) text = text[di:] i += di for di, c := range text { if c == ',' { next = true continue } if c == ' ' { continue } text = text[di:] i += di break } } return i, weeks, days, nil } func repeatMonthOfYearAndDayOfWeek(text string) (int, []Month, []Day, error) { var i int var months []Month var days []Day next := true for next { next = false var dow Month di, err := dow.Parse(text) if err != nil { return 0, nil, nil, err } months = append(months, dow) text = text[di:] i += di if len(text) == 0 || text[0] != ' ' { return 0, nil, nil, fmt.Errorf("invalid date") } text = text[1:] i++ var day Day di, err = day.Parse(text) if err != nil { return 0, nil, nil, err } days = append(days, day) text = text[di:] i += di for di, c := range text { if c == ',' { next = true continue } if c == ' ' { continue } text = text[di:] i += di break } } return i, months, days, nil } func beginningOfMonth(date time.Time) time.Time { year, month, _ := date.Date() return time.Date(year, month, 1, 0,0,0,0, date.Location()) } func endOfMonth(date time.Time) time.Time { year, month, _ := date.Date() date = time.Date(year, month, 1, 0,0,0,0, date.Location()) return date.AddDate(0, 1, -date.Day()) } func beginningOfYear(date time.Time) time.Time { year, _, _ := date.Date() return time.Date(year, 1, 1, 0,0,0,0, date.Location()) } func beginningOfWeek(date time.Time) time.Time { year, month, day := date.Date() return time.Date(year, month, day-int(date.Weekday()), 0,0,0,0, date.Location()) } func endOfYear(date time.Time) time.Time { return date.AddDate(1, int(-date.Month()+1), -date.Day()) } func dayOfWeekNumber(year int, week int, day int) time.Time { // start of the year date := time.Date(year, 1, 1, 0, 0, 0, 0, time.UTC) // clamp to max 53 week = min(53, week) // reverse week starts from the end of year if week < 0 { eoy := endOfYear(date) _, isoWeek := eoy.ISOWeek() if isoWeek == 1 { isoWeek = 53 } week = isoWeek + week + 1 } // clamp to min 1 week = max(1, week) // move to the Nth week date = date.AddDate(0, 0, (7 * (week - 1))) // move to day date = date.AddDate(0, 0, -int(date.Weekday())+day) return date } func monthWithNWeek(d Day, dt time.Time) time.Time { dow := d.DayOfWeek() if wom := d.WeekOfMonth(); wom >= 0 { if wom == 0 { wom = 1 } day := 7 - int(dt.Weekday()) + dow + 7*(wom-1) dt = dt.AddDate(0, 0, day) } else { dt = endOfMonth(dt) day := 7 - int(dt.Weekday()) wk := 7 - dow shift := 7 * (-wom - 1) day = wk - day + shift dt = dt.AddDate(0, 0, -day) } return dt }