mirror of https://git.sr.ht/~rjarry/aerc
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
472 lines
9.2 KiB
Go
472 lines
9.2 KiB
Go
package parse
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.sr.ht/~rjarry/aerc/lib/log"
|
|
)
|
|
|
|
const dateFmt = "2006-01-02"
|
|
|
|
// ParseDateRange parses a date range into a start and end date. Dates are
|
|
// expected to be in the YYYY-MM-DD format.
|
|
//
|
|
// Start and end dates are connected by the range operator ".." where end date
|
|
// is not included in the date range.
|
|
//
|
|
// ParseDateRange can also parse open-ended ranges, i.e. start.. or ..end are
|
|
// allowed.
|
|
//
|
|
// Relative date terms (such as "1 week 1 day" or "1w 1d") can be used, too.
|
|
func DateRange(s string) (start, end time.Time, err error) {
|
|
s = cleanInput(s)
|
|
s = ensureRangeOp(s)
|
|
i := strings.Index(s, "..")
|
|
switch {
|
|
case i < 0:
|
|
// single date
|
|
start, err = translate(s)
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to parse date: %w", err)
|
|
return
|
|
}
|
|
end = start.AddDate(0, 0, 1)
|
|
|
|
case i == 0:
|
|
// end date only
|
|
if len(s) < 2 {
|
|
err = fmt.Errorf("no date found")
|
|
return
|
|
}
|
|
end, err = translate(s[2:])
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to parse date: %w", err)
|
|
return
|
|
}
|
|
|
|
case i > 0:
|
|
// start date first
|
|
start, err = translate(s[:i])
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to parse date: %w", err)
|
|
return
|
|
}
|
|
if len(s[i:]) <= 2 {
|
|
return
|
|
}
|
|
// and end dates if available
|
|
end, err = translate(s[(i + 2):])
|
|
if err != nil {
|
|
err = fmt.Errorf("failed to parse date: %w", err)
|
|
return
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
type dictFunc = func(bool) time.Time
|
|
|
|
// dict is a dictionary to translate words to dates. Map key must be at least 3
|
|
// characters for matching purposes.
|
|
var dict map[string]dictFunc = map[string]dictFunc{
|
|
"today": func(_ bool) time.Time {
|
|
return time.Now()
|
|
},
|
|
"yesterday": func(_ bool) time.Time {
|
|
return time.Now().AddDate(0, 0, -1)
|
|
},
|
|
"week": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -7
|
|
}
|
|
return time.Now().AddDate(0, 0,
|
|
daydiff(time.Monday)+diff)
|
|
},
|
|
"month": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(0, diff, -t.Day()+1)
|
|
},
|
|
"year": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff, 0, -t.YearDay()+1)
|
|
},
|
|
"monday": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -7
|
|
}
|
|
return time.Now().AddDate(0, 0,
|
|
daydiff(time.Monday)+diff)
|
|
},
|
|
"tuesday": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -7
|
|
}
|
|
return time.Now().AddDate(0, 0,
|
|
daydiff(time.Tuesday)+diff)
|
|
},
|
|
"wednesday": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -7
|
|
}
|
|
return time.Now().AddDate(0, 0,
|
|
daydiff(time.Wednesday)+diff)
|
|
},
|
|
"thursday": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -7
|
|
}
|
|
return time.Now().AddDate(0, 0,
|
|
daydiff(time.Thursday)+diff)
|
|
},
|
|
"friday": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -7
|
|
}
|
|
return time.Now().AddDate(0, 0,
|
|
daydiff(time.Friday)+diff)
|
|
},
|
|
"saturday": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -7
|
|
}
|
|
return time.Now().AddDate(0, 0,
|
|
daydiff(time.Saturday)+diff)
|
|
},
|
|
"sunday": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -7
|
|
}
|
|
return time.Now().AddDate(0, 0,
|
|
daydiff(time.Sunday)+diff)
|
|
},
|
|
"january": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.January), -t.Day()+1)
|
|
},
|
|
"february": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.February), -t.Day()+1)
|
|
},
|
|
"march": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.March), -t.Day()+1)
|
|
},
|
|
"april": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.April), -t.Day()+1)
|
|
},
|
|
"may": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.May), -t.Day()+1)
|
|
},
|
|
"june": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.June), -t.Day()+1)
|
|
},
|
|
"july": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.July), -t.Day()+1)
|
|
},
|
|
"august": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.August), -t.Day()+1)
|
|
},
|
|
"september": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.September), -t.Day()+1)
|
|
},
|
|
"october": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.October), -t.Day()+1)
|
|
},
|
|
"november": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.November), -t.Day()+1)
|
|
},
|
|
"december": func(this bool) time.Time {
|
|
diff := 0
|
|
if !this {
|
|
diff = -1
|
|
}
|
|
t := time.Now()
|
|
return t.AddDate(diff,
|
|
monthdiff(time.December), -t.Day()+1)
|
|
},
|
|
}
|
|
|
|
func daydiff(d time.Weekday) int {
|
|
daydiff := d - time.Now().Weekday()
|
|
if daydiff > 0 {
|
|
return int(daydiff) - 7
|
|
}
|
|
return int(daydiff)
|
|
}
|
|
|
|
func monthdiff(d time.Month) int {
|
|
monthdiff := d - time.Now().Month()
|
|
if monthdiff > 0 {
|
|
return int(monthdiff) - 12
|
|
}
|
|
return int(monthdiff)
|
|
}
|
|
|
|
// translate translates regular time words into date strings
|
|
func translate(s string) (time.Time, error) {
|
|
if s == "" {
|
|
return time.Now(), fmt.Errorf("empty string")
|
|
}
|
|
log.Tracef("input: %s", s)
|
|
s0 := s
|
|
|
|
// if next characters is integer, then parse a relative date
|
|
if '0' <= s[0] && s[0] <= '9' && hasUnit(s) {
|
|
relDate, err := RelativeDate(s)
|
|
if err != nil {
|
|
log.Errorf("could not parse relative date from '%s': %v",
|
|
s0, err)
|
|
} else {
|
|
log.Tracef("relative date: translated to %v from %s",
|
|
relDate, s0)
|
|
return bod(relDate.Apply(time.Now())), nil
|
|
}
|
|
}
|
|
|
|
// consult dictionary for terms translation
|
|
s, this, hasPrefix := handlePrefix(s)
|
|
for term, dateFn := range dict {
|
|
if term == "month" && !hasPrefix {
|
|
continue
|
|
}
|
|
if strings.Contains(term, s) {
|
|
log.Tracef("dictionary: translated to %s from %s",
|
|
term, s0)
|
|
return bod(dateFn(this)), nil
|
|
}
|
|
}
|
|
|
|
// this is a regular date, parse it in the normal format
|
|
log.Infof("parse: translates %s to regular format", s0)
|
|
return time.Parse(dateFmt, s)
|
|
}
|
|
|
|
// bod returns the begin of the day
|
|
func bod(t time.Time) time.Time {
|
|
y, m, d := t.Date()
|
|
return time.Date(y, m, d, 0, 0, 0, 0, t.Location())
|
|
}
|
|
|
|
func handlePrefix(s string) (string, bool, bool) {
|
|
var hasPrefix bool
|
|
this := true
|
|
if strings.HasPrefix(s, "this") {
|
|
hasPrefix = true
|
|
s = strings.TrimPrefix(s, "this")
|
|
}
|
|
if strings.HasPrefix(s, "last") {
|
|
hasPrefix = true
|
|
this = false
|
|
s = strings.TrimPrefix(s, "last")
|
|
}
|
|
return s, this, hasPrefix
|
|
}
|
|
|
|
func cleanInput(s string) string {
|
|
s = strings.ToLower(s)
|
|
s = strings.ReplaceAll(s, " ", "")
|
|
s = strings.ReplaceAll(s, "_", "")
|
|
return s
|
|
}
|
|
|
|
// RelDate is the relative date in the past, e.g. yesterday would be
|
|
// represented as RelDate{0,0,1}.
|
|
type RelDate struct {
|
|
Year uint
|
|
Month uint
|
|
Day uint
|
|
}
|
|
|
|
func (d RelDate) Apply(t time.Time) time.Time {
|
|
return t.AddDate(-int(d.Year), -int(d.Month), -int(d.Day))
|
|
}
|
|
|
|
// ParseRelativeDate parses a string of relative terms into a DateAdd.
|
|
//
|
|
// Syntax: N (year|month|week|day) ..
|
|
//
|
|
// The following are valid inputs:
|
|
// 5weeks1day
|
|
// 5w1d
|
|
//
|
|
// Adapted from the Go stdlib in src/time/format.go
|
|
func RelativeDate(s string) (RelDate, error) {
|
|
s0 := s
|
|
s = cleanInput(s)
|
|
var da RelDate
|
|
for s != "" {
|
|
var n uint
|
|
|
|
var err error
|
|
|
|
// expect an integer
|
|
if !('0' <= s[0] && s[0] <= '9') {
|
|
return da, fmt.Errorf("not a valid relative term: %s",
|
|
s0)
|
|
}
|
|
|
|
// consume integer
|
|
n, s, err = leadingInt(s)
|
|
if err != nil {
|
|
return da, fmt.Errorf("cannot read integer in %s",
|
|
s0)
|
|
}
|
|
|
|
// consume the units
|
|
i := 0
|
|
for ; i < len(s); i++ {
|
|
c := s[i]
|
|
if '0' <= c && c <= '9' {
|
|
break
|
|
}
|
|
}
|
|
if i == 0 {
|
|
return da, fmt.Errorf("missing unit in %s", s0)
|
|
}
|
|
|
|
u := s[:i]
|
|
s = s[i:]
|
|
switch u[0] {
|
|
case 'y':
|
|
da.Year += n
|
|
case 'm':
|
|
da.Month += n
|
|
case 'w':
|
|
da.Day += 7 * n
|
|
case 'd':
|
|
da.Day += n
|
|
default:
|
|
return da, fmt.Errorf("unknown unit %s in %s", u, s0)
|
|
}
|
|
|
|
}
|
|
|
|
return da, nil
|
|
}
|
|
|
|
func hasUnit(s string) (has bool) {
|
|
for _, u := range "ymwd" {
|
|
if strings.Contains(s, string(u)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// leadingInt parses and returns the leading integer in s.
|
|
//
|
|
// Adapted from the Go stdlib in src/time/format.go
|
|
func leadingInt(s string) (x uint, rem string, err error) {
|
|
i := 0
|
|
for ; i < len(s); i++ {
|
|
c := s[i]
|
|
if c < '0' || c > '9' {
|
|
break
|
|
}
|
|
x = x*10 + uint(c) - '0'
|
|
}
|
|
return x, s[i:], nil
|
|
}
|
|
|
|
func ensureRangeOp(s string) string {
|
|
if strings.Contains(s, "..") {
|
|
return s
|
|
}
|
|
s0 := s
|
|
for _, m := range []string{"this", "last"} {
|
|
for _, u := range []string{"year", "month", "week"} {
|
|
term := m + u
|
|
if strings.Contains(s, term) {
|
|
if m == "last" {
|
|
return s0 + "..this" + u
|
|
} else {
|
|
return s0 + ".."
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return s0
|
|
}
|