1058 lines
21 KiB
Go
1058 lines
21 KiB
Go
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
|
|
}
|