|
package main
import (
"fmt"
"strconv"
"strings"
"time"
)
func main() {
dates := []string{
"today",
"tomorrow",
"tomorrow at 3am",
"in 3 days",
"in a month",
"in 3 months",
"next week",
"next month",
"in two weeks",
"in 3 weeks",
"2016-09-28",
"3pm",
"in 4 days at 10 pm",
}
for _, d := range dates {
fmt.Printf("'%s': ", d)
t, err := parseTime(d)
if err != nil {
fmt.Printf("%s\n", err)
} else {
fmt.Printf("%s\n", t)
}
}
}
var timeLayouts = []string{
time.RFC3339,
"2006-01-02",
}
func parseTime(s string) (time.Time, error) {
now := time.Now().Round(time.Second)
return parseTimeRelative(s, now)
}
func parseTimeRelative(s string, now time.Time) (time.Time, error) {
var t time.Time
parts := strings.Fields(s)
// more indicates the parts index where there might be "more"
// parsable data
more := 0
if len(parts) == 0 {
return t, fmt.Errorf("empty date spec")
}
switch parts[0] {
case "today":
if len(parts) >= 1 {
t = now
more = 1
}
case "tomorrow":
if len(parts) >= 1 {
t = now.AddDate(0, 0, 1)
more = 1
}
case "in":
if len(parts) >= 3 {
n, err := parseNumber(parts[1])
if err != nil {
return t, err
}
modifier, err := parseModifier(parts[2])
if err != nil {
return t, err
}
t = modifier(n, now)
more = 3
}
case "next":
if len(parts) >= 2 {
modifier, err := parseModifier(parts[1])
if err != nil {
return t, err
}
t = modifier(1, now)
more = 2
}
}
if len(parts) == more {
return t, nil
} else if len(parts) == more+2 && parts[more] == "at" {
h, err := parseHours(parts[more+1])
if err != nil {
return t, err
}
t = truncateHours(t)
return t.Add(h), nil
} else if len(parts) == more+3 && parts[more] == "at" &&
(parts[more+2] == "am" || parts[more+2] == "pm") {
h, err := parseHours(parts[more+1] + parts[more+2])
if err != nil {
return t, err
}
t = truncateHours(t)
return t.Add(h), nil
}
l := len(parts)
if (l == 1 || l == 2) && (strings.HasSuffix(parts[l-1], "am") || strings.HasSuffix(parts[l-1], "pm")) {
s = parts[0]
if l == 2 {
s += parts[1]
}
h, err := parseHours(s)
if err != nil {
return t, err
}
t = truncateHours(now)
return t.Add(h), nil
}
for _, layout := range timeLayouts {
t, err := time.Parse(layout, s)
if err == nil {
return t, nil
}
}
return t, fmt.Errorf("unknown date spec '%s' (unexpected)", s)
}
func parseNumber(n string) (int, error) {
switch n {
case "a", "an", "one":
return 1, nil
case "two":
return 2, nil
case "three":
return 3, nil
case "four":
return 4, nil
case "five":
return 5, nil
case "six":
return 6, nil
case "seven":
return 7, nil
case "eight":
return 8, nil
case "nine":
return 9, nil
case "ten":
return 10, nil
default:
return strconv.Atoi(n)
}
}
func parseModifier(m string) (func(int, time.Time) time.Time, error) {
switch m {
case "second", "seconds":
return durationModifier(time.Second), nil
case "minute", "minutes":
return durationModifier(time.Minute), nil
case "hour", "hours":
return durationModifier(time.Hour), nil
case "day", "days":
return dateModifier(0, 0, 1), nil
case "week", "weeks":
return dateModifier(0, 0, 7), nil
case "month", "months":
return dateModifier(0, 1, 0), nil
case "year", "years":
return dateModifier(1, 0, 0), nil
default:
return nil, fmt.Errorf("unknown modifier '%s'", m)
}
}
func durationModifier(d time.Duration) func(int, time.Time) time.Time {
return func(n int, t time.Time) time.Time {
return t.Add(time.Duration(n) * d)
}
}
func dateModifier(years, months, days int) func(int, time.Time) time.Time {
return func(n int, t time.Time) time.Time {
return t.AddDate(n*years, n*months, n*days)
}
}
func parseHours(s string) (time.Duration, error) {
var d time.Duration
isPm := false
switch {
case strings.HasSuffix(s, "am"):
s = s[0 : len(s)-2]
case strings.HasSuffix(s, "pm"):
isPm = true
s = s[0 : len(s)-2]
default:
return d, fmt.Errorf("unknown hours '%s'", s)
}
n, err := strconv.Atoi(s)
if err != nil {
return d, err
}
if n < 1 || n > 12 {
return d, fmt.Errorf("invalid hour: %d (must be between 1 and 12)", n)
}
if isPm {
n += 12
}
return time.Duration(n) * time.Hour, nil
}
func truncateHours(t time.Time) time.Time {
return t.Truncate(time.Hour).Add(-time.Duration(t.Hour()) * time.Hour)
}
|