1package sheet23import (4 "strings"56 "src.rest/cat/012/internal/numfmt"7 "src.rest/cat/012/internal/value"8)910// Date rules ("Date is", "is before", "is after") take a date or a11// period as Sheets and Excel name them: today, tomorrow, yesterday, the12// past week, month or year, and this, last or next week, month or year.13// Weeks run Sunday to Saturday, as Excel's this week does. A period is14// a range of days: a date is in it, before its first day or after its15// last.1617// Periods lists the words a date rule takes, in the order the rules18// editor's hint gives them.19func Periods() []string {20 return []string{"today", "tomorrow", "yesterday", "past week", "past month", "past year",21 "this week", "last week", "next week", "this month", "last month", "next month",22 "this year", "last year", "next year"}23}2425// datePeriod reads a date rule's value: a date as typed, a number of26// days, or a period, as the first and last days it covers.27func datePeriod(arg string) (lo, hi float64, ok bool) {28 today := float64(int64(numfmt.SerialOf(value.Now())))29 word := strings.Join(strings.Fields(strings.ToLower(arg)), " ")30 word = strings.TrimPrefix(strings.TrimPrefix(word, "in the "), "the ")31 switch word {32 case "today":33 return today, today, true34 case "tomorrow":35 return today + 1, today + 1, true36 case "yesterday":37 return today - 1, today - 1, true38 case "past week", "last 7 days":39 return today - 6, today, true40 case "past month":41 return monthsBefore(today, 1), today, true42 case "past year":43 return monthsBefore(today, 12), today, true44 }45 if which, unit, found := strings.Cut(word, " "); found {46 step, known := map[string]int{"this": 0, "last": -1, "next": 1}[which]47 if lo, hi, ok := periodOf(today, unit, step); known && ok {48 return lo, hi, true49 }50 }51 n, _, ok := ParseValue(strings.TrimSpace(arg))52 n = float64(int64(n))53 return n, n, ok54}5556// periodOf is the week, month or year step periods away from the one57// holding day.58func periodOf(day float64, unit string, step int) (lo, hi float64, ok bool) {59 y, m, _ := numfmt.Civil(int64(day))60 switch unit {61 case "week":62 lo = day - float64(numfmt.Weekday(int64(day))) + float64(7*step)63 return lo, lo + 6, true64 case "month":65 y, m = numfmt.AddMonths(y, m, step)66 return numfmt.DateSerial(y, m, 1), numfmt.DateSerial(y, m, numfmt.DaysIn(y, m)), true67 case "year":68 return numfmt.DateSerial(y+step, 1, 1), numfmt.DateSerial(y+step, 12, 31), true69 }70 return 0, 0, false71}7273// monthsBefore is the day n months before day, on the last day of that74// month when it's shorter.75func monthsBefore(day float64, n int) float64 {76 y, m, d := numfmt.Civil(int64(day))77 y, m = numfmt.AddMonths(y, m, -n)78 return numfmt.DateSerial(y, m, min(d, numfmt.DaysIn(y, m)))79}8081// IsPeriod reports whether a date rule's value names a period (or a day82// relative to today) rather than a date.83func IsPeriod(arg string) bool {84 _, _, ok := datePeriod(arg)85 _, _, isDate := ParseValue(strings.TrimSpace(arg))86 return ok && !isDate87}