init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user