├── .github └── workflows │ └── codecov.yml ├── .gitignore ├── LICENSE ├── README.md ├── base.go ├── conv.go ├── date.go ├── dateparse.go ├── dateparse_test.go ├── datetime.go ├── duration.go ├── duration_test.go ├── go.mod ├── month.go ├── time.go └── week.go /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Codecov coverage 3 | 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-20.04 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-go@v2 12 | with: 13 | go-version: '1.14' 14 | - name: Run coverage 15 | run: go test -race -coverprofile=coverage.txt -covermode=atomic 16 | - name: Upload coverage to Codecov 17 | run: bash <(curl -s https://codecov.io/bash) 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Codecov coveragei build](https://github.com/tada-team/dateparse/actions/workflows/codecov.yml/badge.svg)](https://github.com/tada-team/dateparse/actions/workflows/codecov.yml) [![codecov](https://codecov.io/gh/tada-team/dateparse/branch/master/graph/badge.svg)](https://codecov.io/gh/tada-team/dateparse) 2 | # dateparse 3 | 4 | Лёгкий способ превратить пользовательский ввод даты во что-то машинопонятное. 5 | 6 | Пользователи такие затейники, чего только не вводят, но мы пытаемся всё понять и простить: 7 | 8 | ```go 9 | package main 10 | 11 | import ( 12 | "time" 13 | "github.com/tada-team/dateparse" 14 | ) 15 | 16 | func main() { 17 | date, message := dateparse.Parse("в следующий понедельник утром посмотреть код", nil) 18 | if date.IsZero() { 19 | panic("invalid date") 20 | } 21 | print("at:", date) 22 | print("do:", message) 23 | 24 | loc, err := time.LoadLocation("Europe/Moscow") 25 | if err != nil { 26 | panic(err) 27 | } 28 | date, _ = dateparse.Parse("завтра", &dateparse.Opts{ 29 | TodayEndHour: 20, 30 | Now: time.Now().In(loc), 31 | }) 32 | print(date) 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /base.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | var ( 4 | dayDD = `(30|31|0[1-9]|[1-2]\d|[1-9])` 5 | monthMM = `(1[012]|0[1-9])` 6 | yearYY = `(\d\d)` 7 | yearYYYY = `(20\d\dк?г?)` 8 | hourHH = `(0[0-9]|1[0-9]|2[0-3]|[0-9])` 9 | minuteMM = `(0[0-9]|[0-5][0-9])` 10 | ) 11 | 12 | var ( 13 | morning = `утром|утро|утра|morning|a.m` 14 | evening = `вечером|вечер|вечера|evening|p.m` 15 | midnight = "полночь|ночью|midnight" 16 | noon = "днем|полдень|noon|midday" 17 | ) 18 | 19 | var ( 20 | today = `сегодня|today` 21 | tomorrow = `завтра|tomorrow` 22 | afterTomorrow = `послезавтра|after tomorrow|aftertomorrow` 23 | afterAfterTomorrow = `послепослезавтра|after after tomorrow|afteraftertomorrow` 24 | yesterday = `вчера|yesterday` 25 | ) 26 | -------------------------------------------------------------------------------- /conv.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | func forceInt(s string) int { return int(forceInt64(s)) } 9 | 10 | func forceInt64(s string) int64 { 11 | s = strings.TrimSpace(s) 12 | s = strings.TrimLeft(s, "0") 13 | if s == "" { 14 | return 0 15 | } 16 | val, err := strconv.ParseInt(s, 10, 64) 17 | if err != nil { 18 | return 0 19 | } 20 | return val 21 | } 22 | 23 | func forceFloat64(s string) float64 { 24 | s = strings.TrimSpace(s) 25 | s = strings.TrimLeft(s, "0") 26 | if s == "" { 27 | return 0 28 | } 29 | val, err := strconv.ParseFloat(s, 64) 30 | if err != nil { 31 | return 0 32 | } 33 | return val 34 | } 35 | 36 | func normalizeStrings(ss []string) []string { 37 | if len(ss) <= 1 { 38 | return ss 39 | } 40 | res := make([]string, 0, len(ss)) 41 | for _, bit := range ss { 42 | bit = strings.TrimSpace(bit) 43 | if bit != "" { 44 | res = append(res, bit) 45 | } 46 | } 47 | return res 48 | } 49 | -------------------------------------------------------------------------------- /date.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | var ( 11 | datePrefix = `(в|во|in|on|ровно|the|at)` 12 | dateSuffix = `(-ого|-го|-ва|-его|th|числа|date|\\|/|года|years|[.])` 13 | daySuffix = `(-ого|-го|-ва|-его|th|числа|date|\\)` 14 | ) 15 | 16 | var ( 17 | baseDurRegex = regexp.MustCompile(fmt.Sprintf(`(%s)[" "/]`, duration)) 18 | baseDurOnlyRegex = regexp.MustCompile(fmt.Sprintf(`(%s)$`, duration)) 19 | baseDurTimeRegex = regexp.MustCompile(fmt.Sprintf(`(\d\d?\d?)[" "](%s)`, durationTime)) 20 | baseWeekOnlyRegex = regexp.MustCompile(fmt.Sprintf(`^(%s|%s)$`, weeks, shortWeeks)) 21 | baseWeekPrefixOnlyRegex = regexp.MustCompile(fmt.Sprintf(`^%s[" "](%s|%s)$`, datePrefix, weeks, shortWeeks)) 22 | baseWeekPrefixRegex = regexp.MustCompile(fmt.Sprintf(`^%s[" "](%s|%s)[" "]`, datePrefix, weeks, shortWeeks)) 23 | baseWeekRegex = regexp.MustCompile(fmt.Sprintf(`^(%s|%s)[" "]`, weeks, shortWeeks)) 24 | weekDurSuffixRegex = regexp.MustCompile(fmt.Sprintf(`%s[" "](%s)[" "]%s`, datePrefix, weeks, durationSuffix)) 25 | durSuffixWeekRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[" "]%s[" "](%s)`, datePrefix, durationSuffix, datePrefix, weeks)) 26 | durPrefixWeekRegex = regexp.MustCompile(fmt.Sprintf(`%s[" "]%s[" "](%s)[" "]?%s?`, datePrefix, durPrefix, weeks, durationSuffix)) 27 | ) 28 | 29 | var ( 30 | ddRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[" "]?%s`, datePrefix, dayDD, daySuffix)) 31 | ddmmRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[/.]%s\s?%s?`, datePrefix, dayDD, monthMM, dateSuffix)) 32 | ddMonthRegex = regexp.MustCompile(fmt.Sprintf(`%s%s?[" "](%s)`, dayDD, dateSuffix, months)) 33 | ddmmyyyyRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[/.]%s[/.]%s\s?%s?`, datePrefix, dayDD, monthMM, yearYYYY, dateSuffix)) 34 | ddMonthyyyyRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[" "/.](%s)[" "/.]%s\s?%s?`, datePrefix, dayDD, months, yearYYYY, dateSuffix)) 35 | ddmmyyRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[/.]%s[/.]%s\s?%s?`, datePrefix, dayDD, monthMM, yearYY, dateSuffix)) 36 | ddMonthyyRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[" "/.](%s)[" "/.]%s\s?%s`, datePrefix, dayDD, months, yearYY, dateSuffix)) 37 | isoyyyymmddRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[/.-]?%s[/.-]?%s\s?`, datePrefix, yearYYYY, monthMM, dayDD)) 38 | isoyymmddRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[/.-]?%s[/.-]?%s`, datePrefix, yearYY, monthMM, dayDD)) 39 | ) 40 | 41 | var ( 42 | durTimeRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[" "](\d\d?\d?)[" "]?(%s)?`, datePrefix, durPrefix, durationTime)) 43 | durRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[" "](%s)?\s?(%s)`, datePrefix, durPrefix, wordNumbers, durationWds)) 44 | wdsRegex = regexp.MustCompile(fmt.Sprintf(`(%s)\b[" "/]?%s?`, durationWds, durationSuffix)) 45 | wdsSuffuxRegex = regexp.MustCompile(fmt.Sprintf(`(%s)[" "/]%s[" "]%s`, durationWds, datePrefix, durationSuffix)) 46 | wdsTimeRegex = regexp.MustCompile(fmt.Sprintf(`%s[" "](\d\d)[" "](%s)`, datePrefix, hours)) 47 | ) 48 | 49 | var ( 50 | mmddyyyyRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[/.]%s[/.]%s\s?%s?`, datePrefix, monthMM, dayDD, yearYYYY, dateSuffix)) 51 | mmddyyRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[/.]%s[/.]%s\s?%s?`, datePrefix, monthMM, dayDD, yearYY, dateSuffix)) 52 | mmddRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[/.]%s\s?%s?`, datePrefix, monthMM, dayDD, dateSuffix)) 53 | ) 54 | 55 | func parseDate(s string, opts Opts) (t time.Time, st string) { 56 | switch { 57 | case baseDurOnlyRegex.MatchString(s): 58 | return calculateWordsDate(baseDurOnlyRegex.FindStringSubmatch(s), opts) 59 | case baseWeekPrefixOnlyRegex.MatchString(s): 60 | return calculateWeekDuration(baseWeekPrefixOnlyRegex.FindStringSubmatch(s), opts, 2) 61 | case baseWeekOnlyRegex.MatchString(s): 62 | return calculateWeekDuration(baseWeekOnlyRegex.FindStringSubmatch(s), opts, 1) 63 | case wdsSuffuxRegex.MatchString(s): 64 | return calculateWordsDate(wdsSuffuxRegex.FindStringSubmatch(s), opts) 65 | case baseDurRegex.MatchString(s): 66 | return calculateWordsDate(baseDurRegex.FindStringSubmatch(s), opts) 67 | case weekDurSuffixRegex.MatchString(s): 68 | return calculateWeekDuration(weekDurSuffixRegex.FindStringSubmatch(s), opts, 2) 69 | case baseWeekPrefixRegex.MatchString(s): 70 | return calculateWeekDuration(baseWeekPrefixRegex.FindStringSubmatch(s), opts, 2) 71 | case baseWeekRegex.MatchString(s): 72 | return calculateWeekDuration(baseWeekRegex.FindStringSubmatch(s), opts, 1) 73 | case durTimeRegex.MatchString(s): 74 | return calculateDuration(durTimeRegex.FindStringSubmatch(s), opts, 2) 75 | case durRegex.MatchString(s): 76 | return calculateDuration(durRegex.FindStringSubmatch(s), opts, 2) 77 | case durPrefixWeekRegex.MatchString(s): 78 | return calculateWeekDuration(durPrefixWeekRegex.FindStringSubmatch(s), opts, 3) 79 | case durSuffixWeekRegex.MatchString(s): 80 | return calculateWeekDuration(durSuffixWeekRegex.FindStringSubmatch(s), opts, -3) 81 | //case rareyyyymmdd.MatchString(s): 82 | // return calculateFullDate(rareyyyymmdd.FindStringSubmatch(s), opts, 2, 3, 4) 83 | //case rareyymmdd.MatchString(s): 84 | // return calculateFullDate(rareyymmdd.FindStringSubmatch(s), opts, 2, 3, 4) 85 | case ddMonthyyyyRegex.MatchString(s): 86 | return calculateFullDate(ddMonthyyyyRegex.FindStringSubmatch(s), opts, 4, 3, 2) 87 | case ddMonthyyRegex.MatchString(s): 88 | return calculateFullDate(ddMonthyyRegex.FindStringSubmatch(s), opts, 4, 3, 2) 89 | case ddmmyyyyRegex.MatchString(s): 90 | return calculateFullDate(ddmmyyyyRegex.FindStringSubmatch(s), opts, 4, 3, 2) 91 | case mmddyyyyRegex.MatchString(s): 92 | return calculateFullDate(mmddyyyyRegex.FindStringSubmatch(s), opts, 4, 2, 3) 93 | case ddmmyyRegex.MatchString(s): 94 | return calculateFullDate(ddmmyyRegex.FindStringSubmatch(s), opts, 4, 3, 2) 95 | case mmddyyRegex.MatchString(s): 96 | return calculateFullDate(mmddyyRegex.FindStringSubmatch(s), opts, 4, 2, 3) 97 | case isoyyyymmddRegex.MatchString(s): 98 | return calculateFullDate(isoyyyymmddRegex.FindStringSubmatch(s), opts, 2, 3, 4) 99 | case isoyymmddRegex.MatchString(s): 100 | return calculateFullDate(isoyymmddRegex.FindStringSubmatch(s), opts, 2, 3, 4) 101 | case ddMonthRegex.MatchString(s): 102 | return calculateDate(ddMonthRegex.FindStringSubmatch(s), opts, 3, 1) 103 | case ddmmRegex.MatchString(s): 104 | return calculateDate(ddmmRegex.FindStringSubmatch(s), opts, 3, 2) 105 | case mmddRegex.MatchString(s): 106 | return calculateDate(mmddRegex.FindStringSubmatch(s), opts, 2, 3) 107 | case wdsTimeRegex.MatchString(s): 108 | m := wdsTimeRegex.FindStringSubmatch(s) 109 | date := getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), forceInt(m[2]), 0, 0, opts) 110 | if date.Before(opts.Now) { 111 | date = date.Add(24 * time.Hour) 112 | } 113 | return date, m[0] 114 | case baseDurTimeRegex.MatchString(s): 115 | return calculateDuration(baseDurTimeRegex.FindStringSubmatch(s), opts, 1) 116 | case wdsRegex.MatchString(s): 117 | return calculateWordsDate(wdsRegex.FindStringSubmatch(s), opts) 118 | case ddRegex.MatchString(s): 119 | m := ddRegex.FindStringSubmatch(s) 120 | day := forceInt(m[2]) 121 | return getDate(opts.Now.Year(), opts.Now.Month(), day, opts.TodayEndHour, 0, 0, opts), m[0] 122 | } 123 | return opts.Now, st 124 | } 125 | 126 | func getDate(year int, month time.Month, day int, hour int, minute int, second int, opts Opts) time.Time { 127 | return time.Date(year, month, day, hour, minute, second, 0, opts.Now.Location()) 128 | } 129 | 130 | func calculateDate(m []string, opts Opts, monthPosition int, dayPosition int) (time.Time, string) { 131 | month := opts.Now.Month() 132 | if mth := parseMonth(m[monthPosition]); mth != 0 { 133 | month = mth 134 | } else { 135 | month = time.Month(forceInt(m[monthPosition])) 136 | } 137 | 138 | year := opts.Now.Year() 139 | if month < opts.Now.Month() { 140 | year++ 141 | } 142 | 143 | day := forceInt(m[dayPosition]) 144 | return getDate(year, month, day, opts.TodayEndHour, 0, 0, opts), m[0] 145 | } 146 | 147 | func calculateFullDate(m []string, opts Opts, yearPosition int, monthPosition int, dayPosition int) (time.Time, string) { 148 | year := opts.Now.Year() 149 | if len(m[yearPosition]) == 2 { 150 | year = forceInt("20" + m[yearPosition][:2]) 151 | } else { 152 | year = forceInt(m[yearPosition][:4]) 153 | } 154 | date, _ := calculateDate(m, opts, monthPosition, dayPosition) 155 | if date.Month() < opts.Now.Month() && year == opts.Now.Year() { 156 | year += 1 157 | } 158 | return getDate(year, date.Month(), date.Day(), opts.TodayEndHour, 0, 0, opts), m[0] 159 | } 160 | 161 | func calculateWordsDate(m []string, opts Opts) (time.Time, string) { 162 | m = normalizeStrings(m) 163 | date := getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), opts.Now.Hour(), opts.Now.Minute(), 0, opts) 164 | str := strings.Replace(m[0], "/", "", 1) 165 | switch { 166 | case strings.Contains(today, str) || strings.Contains(today, m[1]): 167 | date = getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), opts.TodayEndHour, 0, 0, opts) 168 | case strings.Contains(tomorrow, str) || strings.Contains(tomorrow, m[1]): 169 | date = getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day()+1, opts.TodayEndHour, 0, 0, opts) 170 | case strings.Contains(afterTomorrow, m[0]): 171 | date = getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day()+2, opts.TodayEndHour, 0, 0, opts) 172 | case strings.Contains(afterAfterTomorrow, m[0]): 173 | date = getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day()+3, opts.TodayEndHour, 0, 0, opts) 174 | case strings.Contains(yesterday, m[0]): 175 | date = getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day()+365, opts.TodayEndHour, 0, 0, opts) 176 | } 177 | if len(m) > 2 { 178 | switch { 179 | case strings.Contains(morning, m[2]): 180 | date = getDate(date.Year(), date.Month(), date.Day(), 10, 0, 0, opts) 181 | case strings.Contains(noon, m[2]): 182 | date = getDate(date.Year(), date.Month(), date.Day(), 12, 0, 0, opts) 183 | case strings.Contains(evening, m[2]): 184 | date = getDate(date.Year(), date.Month(), date.Day(), 18, 0, 0, opts) 185 | case strings.Contains(midnight, m[2]): 186 | if date.Day() == opts.Now.Day() { 187 | date = date.Add(24 * time.Hour) 188 | } 189 | date = getDate(date.Year(), date.Month(), date.Day(), 0, 0, 0, opts) 190 | } 191 | } 192 | if len(m) > 3 { 193 | switch { 194 | case strings.Contains(noon, m[3]): 195 | date = getDate(date.Year(), date.Month(), date.Day(), 12, 0, 0, opts) 196 | case strings.Contains(midnight, m[3]): 197 | if date.Day() == opts.Now.Day() { 198 | date = date.Add(24 * time.Hour) 199 | } 200 | date = getDate(date.Year(), date.Month(), date.Day(), 0, 0, 0, opts) 201 | } 202 | } 203 | 204 | return date, m[0] 205 | } 206 | -------------------------------------------------------------------------------- /dateparse.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | type Opts struct { 9 | TodayEndHour int 10 | Now time.Time 11 | } 12 | 13 | func Parse(s string, opts *Opts) (time.Time, string) { 14 | if opts == nil { 15 | opts = new(Opts) 16 | } 17 | if opts.TodayEndHour == 0 { 18 | opts.TodayEndHour = 18 19 | } 20 | date, msg := dateTimeParse(strings.TrimSpace(strings.ToLower(s)), *opts) 21 | return date.Round(time.Second), msg 22 | } 23 | -------------------------------------------------------------------------------- /dateparse_test.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | type parserStruct struct { 9 | date time.Time 10 | message string 11 | } 12 | 13 | func TestDateparse(t *testing.T) { 14 | loc, err := time.LoadLocation("Europe/Moscow") 15 | if err != nil { 16 | t.Fatal("load location fail:", err) 17 | } 18 | dt := time.Date(2020, 10, 10, 12, 1, 0, 0, loc) // saturday 19 | for k, want := range map[string]parserStruct{ 20 | "через час тест": { 21 | dt.Add(1 * time.Hour), 22 | "тест", 23 | }, 24 | "в 15 часов": { 25 | time.Date(dt.Year(), dt.Month(), dt.Day(), 15, 0, 0, 0, dt.Location()), 26 | "", 27 | }, 28 | "в 11 часов": { 29 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 11, 0, 0, 0, dt.Location()), 30 | "", 31 | }, 32 | "at 11 hours": { 33 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 11, 0, 0, 0, dt.Location()), 34 | "", 35 | }, 36 | "в 11 комментарий": { 37 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 11, 0, 0, 0, dt.Location()), 38 | "комментарий", 39 | }, 40 | "сегодня/13:00 почитать": { 41 | time.Date(dt.Year(), dt.Month(), dt.Day(), 13, 0, 0, 0, dt.Location()), 42 | "почитать", 43 | }, 44 | "завтра/13:00": { 45 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 13, 0, 0, 0, dt.Location()), 46 | "", 47 | }, 48 | "сегодня в полночь": { 49 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 0, 0, 0, 0, dt.Location()), 50 | "", 51 | }, 52 | "послезавтра днем": { 53 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 12, 0, 0, 0, dt.Location()), 54 | "", 55 | }, 56 | "at midnight": { 57 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 0, 0, 0, 0, dt.Location()), 58 | "", 59 | }, 60 | "tomorrow at midnight eat": { 61 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 0, 0, 0, 0, dt.Location()), 62 | "eat", 63 | }, 64 | "завтра в полночь": { 65 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 0, 0, 0, 0, dt.Location()), 66 | "", 67 | }, 68 | "послезавтра ночью": { 69 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 0, 0, 0, 0, dt.Location()), 70 | "", 71 | }, 72 | "сегодня в 22:00 тестируем": { 73 | time.Date(dt.Year(), dt.Month(), dt.Day(), 22, 0, 0, 0, dt.Location()), 74 | "тестируем", 75 | }, 76 | "сегодня в 18:00 умыться": { 77 | time.Date(dt.Year(), dt.Month(), dt.Day(), 18, 0, 0, 0, dt.Location()), 78 | "умыться", 79 | }, 80 | "сегодня в 18": { 81 | time.Date(dt.Year(), dt.Month(), dt.Day(), 18, 0, 0, 0, dt.Location()), 82 | "", 83 | }, 84 | "ровно через год": { 85 | dt.Add(24 * 365 * time.Hour), 86 | "", 87 | }, 88 | "пнуть женю в 16:00": { 89 | time.Date(dt.Year(), dt.Month(), dt.Day(), 16, 0, 0, 0, dt.Location()), 90 | "пнуть женю", 91 | }, 92 | "в 11 утра": { 93 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 11, 0, 0, 0, dt.Location()), 94 | "", 95 | }, 96 | " в 11 вечера покодить": { 97 | time.Date(dt.Year(), dt.Month(), dt.Day(), 23, 0, 0, 0, dt.Location()), 98 | "покодить", 99 | }, 100 | "в 22:00 спать": { 101 | time.Date(dt.Year(), dt.Month(), dt.Day(), 22, 0, 0, 0, dt.Location()), 102 | "спать", 103 | }, 104 | "в 18:00 декомпозировать": { 105 | time.Date(dt.Year(), dt.Month(), dt.Day(), 18, 0, 0, 0, dt.Location()), 106 | "декомпозировать", 107 | }, 108 | "05.09.2019 в 12:00": { 109 | time.Date(2019, 9, 5, 12, 0, 0, 0, dt.Location()), 110 | "", 111 | }, 112 | "05.09.2019/13:00": { 113 | time.Date(2019, 9, 5, 13, 0, 0, 0, dt.Location()), 114 | "", 115 | }, 116 | "05/26/2021": { 117 | time.Date(2021, 5, 26, 18, 0, 0, 0, dt.Location()), 118 | "", 119 | }, 120 | "05/26/22": { 121 | time.Date(2022, 5, 26, 18, 0, 0, 0, dt.Location()), 122 | "", 123 | }, 124 | "05/26": { 125 | time.Date(dt.Year()+1, 5, 26, 18, 0, 0, 0, dt.Location()), 126 | "", 127 | }, 128 | "26/05/22": { 129 | time.Date(2022, 5, 26, 18, 0, 0, 0, dt.Location()), 130 | "", 131 | }, 132 | "09.07 в 12:00": { 133 | time.Date(dt.Year()+1, 7, 9, 12, 0, 0, 0, dt.Location()), 134 | "", 135 | }, 136 | "1 августа": { 137 | time.Date(dt.Year()+1, 8, 1, 18, 0, 0, 0, dt.Location()), 138 | "", 139 | }, 140 | "23 июня в 9 утра": { 141 | time.Date(dt.Year()+1, 6, 23, 9, 0, 0, 0, dt.Location()), 142 | "", 143 | }, 144 | "1 июля": { 145 | time.Date(dt.Year()+1, 7, 1, 18, 0, 0, 0, dt.Location()), 146 | "", 147 | }, 148 | "31 сентября": { 149 | time.Date(dt.Year()+1, 9, 31, 18, 0, 0, 0, dt.Location()), 150 | "", 151 | }, 152 | "15 ноября выпить молока": { 153 | time.Date(dt.Year(), 11, 15, 18, 0, 0, 0, dt.Location()), 154 | "выпить молока", 155 | }, 156 | "15 октября": { 157 | time.Date(dt.Year(), 10, 15, 18, 0, 0, 0, dt.Location()), 158 | "", 159 | }, 160 | "12/13": { 161 | time.Date(dt.Year(), 12, 13, 18, 0, 0, 0, dt.Location()), 162 | "", 163 | }, 164 | "12:13 проверить что-то": { 165 | time.Date(dt.Year(), dt.Month(), dt.Day(), 12, 13, 0, 0, dt.Location()), 166 | "проверить что-то", 167 | }, 168 | "16.16": { 169 | time.Date(dt.Year(), dt.Month(), dt.Day(), 16, 16, 0, 0, dt.Location()), 170 | "", 171 | }, 172 | "30th morning chil": { 173 | time.Date(dt.Year(), dt.Month(), 30, 10, 0, 0, 0, dt.Location()), 174 | "chil", 175 | }, 176 | "30-го чил": { 177 | time.Date(dt.Year(), dt.Month(), 30, 18, 0, 0, 0, dt.Location()), 178 | "чил", 179 | }, 180 | "18 июня в 14:00 на море": { 181 | time.Date(dt.Year()+1, 6, 18, 14, 0, 0, 0, dt.Location()), 182 | "на море", 183 | }, 184 | "18.03.2019 12:00": { 185 | time.Date(2019, 3, 18, 12, 0, 0, 0, dt.Location()), 186 | "", 187 | }, 188 | "20 марта 2020 года": { 189 | time.Date(2021, 3, 20, 18, 0, 0, 0, dt.Location()), 190 | "", 191 | }, 192 | "20 марта 2021 года купить дошик": { 193 | time.Date(2021, 3, 20, 18, 0, 0, 0, dt.Location()), 194 | "купить дошик", 195 | }, 196 | "20 марта 21 года": { 197 | time.Date(2021, 3, 20, 18, 0, 0, 0, dt.Location()), 198 | "", 199 | }, 200 | "20 мая": { 201 | time.Date(dt.Year()+1, 5, 20, 18, 0, 0, 0, dt.Location()), 202 | "", 203 | }, 204 | "31 мая": { 205 | time.Date(dt.Year()+1, 5, 31, 18, 0, 0, 0, dt.Location()), 206 | "", 207 | }, 208 | "20.11.2019к 9:45 посетить врача": { 209 | time.Date(2019, 11, 20, 9, 45, 0, 0, dt.Location()), 210 | "посетить врача", 211 | }, 212 | "21.09 14:00 чильнуть": { 213 | time.Date(dt.Year()+1, 9, 21, 14, 0, 0, 0, dt.Location()), 214 | "чильнуть", 215 | }, 216 | "23 14:00 поужинать в кафе": { 217 | time.Date(dt.Year(), dt.Month(), dt.Day(), 14, 0, 0, 0, dt.Location()), 218 | "23 поужинать в кафе", 219 | }, 220 | "23-ого в 14:00 срезы": { 221 | time.Date(dt.Year(), dt.Month(), 23, 14, 0, 0, 0, dt.Location()), 222 | "срезы", 223 | }, 224 | "23 числа в 14:00 др начальника": { 225 | time.Date(dt.Year(), dt.Month(), 23, 14, 0, 0, 0, dt.Location()), 226 | "др начальника", 227 | }, 228 | "23/12 в 14 тренировка": { 229 | time.Date(dt.Year(), 12, 23, 14, 0, 0, 0, dt.Location()), 230 | "тренировка", 231 | }, 232 | "23/12 14:00 тренировка 2": { 233 | time.Date(dt.Year(), 12, 23, 14, 0, 0, 0, dt.Location()), 234 | "тренировка 2", 235 | }, 236 | "25.04 в 14 проснуться": { 237 | time.Date(dt.Year()+1, 4, 25, 14, 0, 0, 0, dt.Location()), 238 | "проснуться", 239 | }, 240 | "26.09. 12:00 улыбнуться": { 241 | time.Date(dt.Year()+1, 9, 26, 12, 0, 0, 0, dt.Location()), 242 | "улыбнуться", 243 | }, 244 | "31.05.2019 спектакль": { 245 | time.Date(2019, 5, 31, 18, 0, 0, 0, dt.Location()), 246 | "спектакль", 247 | }, 248 | "7:00 завтрак": { 249 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 7, 0, 0, 0, dt.Location()), 250 | "завтрак", 251 | }, 252 | "в 10 выпить кофан": { 253 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 10, 0, 0, 0, dt.Location()), 254 | "выпить кофан", 255 | }, 256 | "в 16:59 позвонить": { 257 | time.Date(dt.Year(), dt.Month(), dt.Day(), 16, 59, 0, 0, dt.Location()), 258 | "позвонить", 259 | }, 260 | "в 1:30": { 261 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 1, 30, 0, 0, dt.Location()), 262 | "", 263 | }, 264 | "в понедельник убраться": { 265 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 18, 0, 0, 0, dt.Location()), 266 | "убраться", 267 | }, 268 | "в следующий понедельник": { 269 | time.Date(dt.Year(), dt.Month(), dt.Day()+9, 18, 0, 0, 0, dt.Location()), 270 | "", 271 | }, 272 | "в следующий понедельник утром посмотреть код": { 273 | time.Date(dt.Year(), dt.Month(), dt.Day()+9, 10, 0, 0, 0, dt.Location()), 274 | "посмотреть код", 275 | }, 276 | "в субботу утром": { 277 | time.Date(dt.Year(), dt.Month(), dt.Day()+7, 10, 0, 0, 0, dt.Location()), 278 | "", 279 | }, 280 | "утром": { 281 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 10, 0, 0, 0, dt.Location()), 282 | "", 283 | }, 284 | "вечером": { 285 | time.Date(dt.Year(), dt.Month(), dt.Day(), 18, 0, 0, 0, dt.Location()), 286 | "", 287 | }, 288 | "понедельник через два дома офис": { 289 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 18, 0, 0, 0, dt.Location()), 290 | "через два дома офис", 291 | }, 292 | "утром в субботу почистить зубы": { 293 | time.Date(dt.Year(), dt.Month(), dt.Day()+7, 10, 0, 0, 0, dt.Location()), 294 | "почистить зубы", 295 | }, 296 | "в полночь в понедельник помыть голову": { 297 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 0, 0, 0, 0, dt.Location()), 298 | "помыть голову", 299 | }, 300 | "в субботу": { 301 | time.Date(dt.Year(), dt.Month(), dt.Day(), 18, 0, 0, 0, dt.Location()), 302 | "", 303 | }, 304 | "on monday test": { 305 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 18, 0, 0, 0, dt.Location()), 306 | "test", 307 | }, 308 | "on monday at 5 p.m": { 309 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 17, 0, 0, 0, dt.Location()), 310 | "", 311 | }, 312 | "в пятницу в 5 утра": { 313 | time.Date(dt.Year(), dt.Month(), dt.Day()+6, 5, 0, 0, 0, dt.Location()), 314 | "", 315 | }, 316 | "в пятницу в 5 вечера на волгу": { 317 | time.Date(dt.Year(), dt.Month(), dt.Day()+6, 17, 0, 0, 0, dt.Location()), 318 | "на волгу", 319 | }, 320 | "в среду": { 321 | time.Date(dt.Year(), dt.Month(), dt.Day()+4, 18, 0, 0, 0, dt.Location()), 322 | "", 323 | }, 324 | "в пн": { 325 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 18, 0, 0, 0, dt.Location()), 326 | "", 327 | }, 328 | "пн": { 329 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 18, 0, 0, 0, dt.Location()), 330 | "", 331 | }, 332 | "вт": { 333 | time.Date(dt.Year(), dt.Month(), dt.Day()+3, 18, 0, 0, 0, dt.Location()), 334 | "", 335 | }, 336 | "ср": { 337 | time.Date(dt.Year(), dt.Month(), dt.Day()+4, 18, 0, 0, 0, dt.Location()), 338 | "", 339 | }, 340 | "чт": { 341 | time.Date(dt.Year(), dt.Month(), dt.Day()+5, 18, 0, 0, 0, dt.Location()), 342 | "", 343 | }, 344 | "пт": { 345 | time.Date(dt.Year(), dt.Month(), dt.Day()+6, 18, 0, 0, 0, dt.Location()), 346 | "", 347 | }, 348 | "сб": { 349 | time.Date(dt.Year(), dt.Month(), dt.Day(), 18, 0, 0, 0, dt.Location()), 350 | "", 351 | }, 352 | "вс": { 353 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 18, 0, 0, 0, dt.Location()), 354 | "", 355 | }, 356 | "вс тестируем приложение": { 357 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 18, 0, 0, 0, dt.Location()), 358 | "тестируем приложение", 359 | }, 360 | "в полдень покушать": { 361 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 12, 0, 0, 0, dt.Location()), 362 | "покушать", 363 | }, 364 | "четверговый митап завтра": { 365 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 18, 0, 0, 0, dt.Location()), 366 | "четверговый митап", 367 | }, 368 | "что завтра совещание": { 369 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 18, 0, 0, 0, dt.Location()), 370 | "что совещание", 371 | }, 372 | "в полночь": { 373 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 0, 0, 0, 0, dt.Location()), 374 | "", 375 | }, 376 | "среду": { 377 | time.Date(dt.Year(), dt.Month(), dt.Day()+4, 18, 0, 0, 0, dt.Location()), 378 | "", 379 | }, 380 | "в среду в 12:30": { 381 | time.Date(dt.Year(), dt.Month(), dt.Day()+4, 12, 30, 0, 0, dt.Location()), 382 | "", 383 | }, 384 | "в среду утром": { 385 | time.Date(dt.Year(), dt.Month(), dt.Day()+4, 10, 0, 0, 0, dt.Location()), 386 | "", 387 | }, 388 | "во вторник": { 389 | time.Date(dt.Year(), dt.Month(), dt.Day()+3, 18, 0, 0, 0, dt.Location()), 390 | "", 391 | }, 392 | "вчера": { 393 | time.Date(dt.Year(), dt.Month(), dt.Day()+365, 18, 0, 0, 0, dt.Location()), 394 | "", 395 | }, 396 | "завтра в 12": { 397 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 12, 0, 0, 0, dt.Location()), 398 | "", 399 | }, 400 | "завтра воскресный праздник": { 401 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 18, 0, 0, 0, dt.Location()), 402 | "воскресный праздник", 403 | }, 404 | "09.12": { 405 | time.Date(dt.Year(), 12, 9, 18, 0, 0, 0, dt.Location()), 406 | "", 407 | }, 408 | "09/12": { 409 | time.Date(dt.Year(), 12, 9, 18, 0, 0, 0, dt.Location()), 410 | "", 411 | }, 412 | "09/12/2050 продать": { 413 | time.Date(2050, 12, 9, 18, 0, 0, 0, dt.Location()), 414 | "продать", 415 | }, 416 | "2020-04-25": { 417 | time.Date(2021, 4, 25, 18, 0, 0, 0, dt.Location()), 418 | "", 419 | }, 420 | "2020-0809": { 421 | time.Date(2021, 8, 9, 18, 0, 0, 0, dt.Location()), 422 | "", 423 | }, 424 | "20-0809": { 425 | time.Date(2021, 8, 9, 18, 0, 0, 0, dt.Location()), 426 | "", 427 | }, 428 | "2020-0809 потеряли дефис": { 429 | time.Date(2021, 8, 9, 18, 0, 0, 0, dt.Location()), 430 | "потеряли дефис", 431 | }, 432 | "2020-04-25 это Iso формат": { 433 | time.Date(2021, 4, 25, 18, 0, 0, 0, dt.Location()), 434 | "это iso формат", 435 | }, 436 | "20-04-25": { 437 | time.Date(2021, 4, 25, 18, 0, 0, 0, dt.Location()), 438 | "", 439 | }, 440 | "200425": { 441 | time.Date(2021, 4, 25, 18, 0, 0, 0, dt.Location()), 442 | "", 443 | }, 444 | "20200425": { 445 | time.Date(2021, 4, 25, 18, 0, 0, 0, dt.Location()), 446 | "", 447 | }, 448 | "20200425 потеряли все": { 449 | time.Date(2021, 4, 25, 18, 0, 0, 0, dt.Location()), 450 | "потеряли все", 451 | }, 452 | "20-04-25 покормить собаку": { 453 | time.Date(2021, 4, 25, 18, 0, 0, 0, dt.Location()), 454 | "покормить собаку", 455 | }, 456 | "10/02/50-го покушать": { 457 | time.Date(2050, 2, 10, 18, 0, 0, 0, dt.Location()), 458 | "покушать", 459 | }, 460 | "сегодня": { 461 | time.Date(dt.Year(), dt.Month(), dt.Day(), 18, 0, 0, 0, dt.Location()), 462 | "", 463 | }, 464 | "сегодня сохранить фото": { 465 | time.Date(dt.Year(), dt.Month(), dt.Day(), 18, 0, 0, 0, dt.Location()), 466 | "сохранить фото", 467 | }, 468 | "30 минут кофе": { 469 | dt.Add(30 * time.Minute), 470 | "кофе", 471 | }, 472 | "завтра": { 473 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 18, 0, 0, 0, dt.Location()), 474 | "", 475 | }, 476 | "завтра через неделю поход": { 477 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 18, 0, 0, 0, dt.Location()), 478 | "через неделю поход", 479 | }, 480 | "завтра в среду важное совещание": { 481 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 18, 0, 0, 0, dt.Location()), 482 | "в среду важное совещание", 483 | }, 484 | "послезавтра помыть голову": { 485 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 18, 0, 0, 0, dt.Location()), 486 | "помыть голову", 487 | }, 488 | "послепослезавтра": { 489 | time.Date(dt.Year(), dt.Month(), dt.Day()+3, 18, 0, 0, 0, dt.Location()), 490 | "", 491 | }, 492 | "after tomorrow": { 493 | time.Date(dt.Year(), dt.Month(), dt.Day()+2, 18, 0, 0, 0, dt.Location()), 494 | "", 495 | }, 496 | "через 1 минуту моргнуть": { 497 | dt.Add(1 * time.Minute), 498 | "моргнуть", 499 | }, 500 | "in 1 min": { 501 | dt.Add(1 * time.Minute), 502 | "", 503 | }, 504 | "через 3 недели": { 505 | dt.Add(3 * 7 * 24 * time.Hour), 506 | "", 507 | }, 508 | "через 3 неделя": { 509 | dt.Add(3 * 7 * 24 * time.Hour), 510 | "", 511 | }, 512 | "1 неделя": { 513 | dt.Add(7 * 24 * time.Hour), 514 | "", 515 | }, 516 | "1 week wake up": { 517 | dt.Add(7 * 24 * time.Hour), 518 | "wake up", 519 | }, 520 | "через 10 недель": { 521 | dt.Add(10 * 7 * 24 * time.Hour), 522 | "", 523 | }, 524 | "через 30": { 525 | dt.Add(30 * time.Minute), 526 | "", 527 | }, 528 | "через 30 секунд куку": { 529 | dt.Add(30 * time.Second), 530 | "куку", 531 | }, 532 | "через 6 часов": { 533 | dt.Add(6 * time.Hour), 534 | "", 535 | }, 536 | "через 2 часа": { 537 | dt.Add(2 * time.Hour), 538 | "", 539 | }, 540 | "через 7 дней": { 541 | dt.Add(7 * 24 * time.Hour), 542 | "", 543 | }, 544 | "через 2 дня": { 545 | dt.Add(2 * 24 * time.Hour), 546 | "", 547 | }, 548 | "через 100 дней отдохнуть": { 549 | dt.Add(100 * 24 * time.Hour), 550 | "отдохнуть", 551 | }, 552 | "через год": { 553 | dt.Add(365 * 24 * time.Hour), 554 | "", 555 | }, 556 | "через месяц": { 557 | dt.Add(31 * 24 * time.Hour), 558 | "", 559 | }, 560 | "через минуту": { 561 | dt.Add(1 * time.Minute), 562 | "", 563 | }, 564 | "через минуту [username](http://ya.ru)": { 565 | dt.Add(1 * time.Minute), 566 | "[username](http://ya.ru)", 567 | }, 568 | "через 2 минуты": { 569 | dt.Add(2 * time.Minute), 570 | "", 571 | }, 572 | "через неделю выходной": { 573 | dt.Add(7 * 24 * time.Hour), 574 | "выходной", 575 | }, 576 | "через два часа": { 577 | dt.Add(2 * time.Hour), 578 | "", 579 | }, 580 | "через три дня сходить в кино": { 581 | dt.Add(3 * 24 * time.Hour), 582 | "сходить в кино", 583 | }, 584 | "в следующую субботу": { 585 | time.Date(dt.Year(), dt.Month(), dt.Day()+7, 18, 0, 0, 0, dt.Location()), 586 | "", 587 | }, 588 | "в следующую субботу праздник": { 589 | time.Date(dt.Year(), dt.Month(), dt.Day()+7, 18, 0, 0, 0, dt.Location()), 590 | "праздник", 591 | }, 592 | "с утра тест": { 593 | time.Date(dt.Year(), dt.Month(), dt.Day()+1, 10, 0, 0, 0, dt.Location()), 594 | "тест", 595 | }, 596 | "через три минуты тест": { 597 | dt.Add(time.Minute * 3), 598 | "тест", 599 | }, 600 | "через пол часа тест": { 601 | dt.Add(time.Minute * 30), 602 | "тест", 603 | }, 604 | "через четверть минуты тест": { 605 | dt.Add(time.Second * 15), 606 | "тест", 607 | }, 608 | "через полчаса тест": { 609 | dt.Add(time.Minute * 30), 610 | "тест", 611 | }, 612 | // FIXME: 613 | //"в субботу в 11 утра": { 614 | // time.Date(dt.Year(), dt.Month(), dt.Day()+7, 11, 0, 0, 0, dt.Location()), 615 | // "", 616 | //}, 617 | //"12 13": { 618 | // time.Date(dt.Year(), dt.Month(), dt.Day(), 12, 13, 0, 0, dt.Location()), 619 | // "", 620 | //}, 621 | //"20.11/13:00": { 622 | // time.Date(dt.Year(), 11, 20, 13, 0, 0, 0, dt.Location()), 623 | // "", 624 | //}, 625 | //"завтра про полдень в": { 626 | // dt.Add(24 * time.Hour), 627 | // "", 628 | //}, 629 | //"09 00": { 630 | // time.Date(dt.Year(), dt.Month(), dt.Day()+1, 9, 0, 0, 0, dt.Location()), 631 | // "", 632 | //}, 633 | //"15": { 634 | // time.Date(dt.Year(), dt.Month(), dt.Day(), 15, 0, 0, 0, dt.Location()), 635 | // "", 636 | //}, 637 | //"2": { 638 | // time.Date(dt.Year(), dt.Month(), dt.Day()+1, 2, 0, 0, 0, dt.Location()), 639 | // "", 640 | //}, 641 | //"9 14": { 642 | // time.Date(dt.Year(), dt.Month(), dt.Day()+1, 9, 14, 0, 0, dt.Location()), 643 | // "", 644 | //}, 645 | } { 646 | t.Run(k, func(t *testing.T) { 647 | got, msg := Parse(k, &Opts{Now: dt}) 648 | if got.IsZero() || !got.Equal(want.date) || msg != want.message { 649 | t.Errorf("dateparse error on '%s': got '%s' (comment: '%s') want '%s' (comment: '%s')", k, got, msg, want.date, want.message) 650 | } 651 | }) 652 | } 653 | } 654 | 655 | // BenchmarkParse-12 15781 76097 ns/op 494 B/op 14 allocs/op 656 | // ==> 657 | // BenchmarkParse-12 16088 75671 ns/op 439 B/op 8 allocs/op 658 | func BenchmarkParse(b *testing.B) { 659 | b.ReportAllocs() 660 | for i := 0; i < b.N; i++ { 661 | Parse("сегодня в 18", nil) 662 | } 663 | } 664 | -------------------------------------------------------------------------------- /datetime.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "math/rand" 5 | "regexp" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | var dateTimeRegex, _ = joinRegexp([]*regexp.Regexp{baseDurOnlyRegex, baseWeekOnlyRegex, baseWeekPrefixRegex, baseWeekPrefixOnlyRegex, 11 | baseDurRegex, baseWeekRegex, baseTimeOrientationRegex, durTimeRegex, baseDurTimeRegex, durRegex, wdsSuffuxRegex, wdsRegex, 12 | ddRegex, ddmmRegex, ddMonthRegex, ddmmyyyyRegex, mmddyyyyRegex, mmddRegex, ddMonthyyyyRegex, ddmmyyRegex, mmddyyRegex, 13 | ddMonthyyRegex, durPrefixWeekRegex, weekDurSuffixRegex, durSuffixWeekRegex, hhmmRegex, hhRegex, isoyyyymmddRegex, isoyymmddRegex, wdsTimeRegex}, "|") 14 | 15 | func dateTimeParse(s string, opts Opts) (t time.Time, msg string) { 16 | if dateTimeRegex.MatchString(s) { 17 | 18 | marker := getMarker() 19 | s = strings.ReplaceAll(s, "://", marker) 20 | 21 | date, replacingDate := parseDate(s, opts) 22 | s = strings.Replace(s, strings.TrimSpace(replacingDate), "", 1) 23 | timeP, replacingTime := parseTime(s, opts) 24 | if (timeP.Before(opts.Now) || timeP == opts.Now) && date == opts.Now { 25 | date = date.Add(24 * time.Hour) 26 | } 27 | 28 | hour := timeP.Hour() 29 | minute := timeP.Minute() 30 | second := timeP.Second() 31 | 32 | replacingTime = strings.TrimSpace(replacingTime) 33 | if len(replacingTime) == 0 { 34 | hour = date.Hour() 35 | minute = date.Minute() 36 | second = date.Second() 37 | } 38 | 39 | s = strings.Replace(s, replacingTime, "", 1) 40 | s = strings.ReplaceAll(s, marker, "://") 41 | 42 | return getDate(date.Year(), date.Month(), date.Day(), hour, minute, second, opts), strings.TrimSpace(s) 43 | } 44 | return 45 | } 46 | 47 | func joinRegexp(regexps []*regexp.Regexp, sep string) (*regexp.Regexp, error) { 48 | var b strings.Builder 49 | for i, re := range regexps { 50 | if i > 0 { 51 | b.WriteString(sep) 52 | } 53 | b.WriteString(re.String()) 54 | } 55 | return regexp.Compile(b.String()) 56 | } 57 | 58 | func getMarker() string { 59 | b := make([]byte, 20) 60 | rand.Read(b) 61 | return string(b) 62 | } 63 | -------------------------------------------------------------------------------- /duration.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | var ( 9 | seconds = `сек|секунд|sec|seconds` 10 | minutes = `мин|минут|минуту|минуты|min` 11 | hours = `часов|hours|hour|часа|час` 12 | days = `дней|дня|days` 13 | weeksWords = `недель|неделю|недели|неделя|weeks|week` 14 | monthsWords = `месяцев|месяца|month` 15 | years = `лет|года|год|years` 16 | durationTimeWords = strings.Join([]string{seconds, minutes, hours, days, weeksWords, monthsWords, years}, "|") 17 | ) 18 | 19 | var ( 20 | durPrefix = `(через|in|следующ[и]?[й]?[у]?[ю]?|следующий|следующую|next)` 21 | duration = strings.Join([]string{today, tomorrow, afterTomorrow, afterAfterTomorrow, yesterday}, "|") 22 | durationTime = `сек[у]?[н]?[д]?[а]?[у]?|мин[у]?[т]?[у]?[а]?[ы]?|min[u]?[t]?[e]?[s]?|час[о]?[в]?[а]?|hour[s]?|дн[е]?[й]?[я]?|day[s]?|недел[ь]?[я]?[и]?[ю]?|week[s]?|год|year[s]?|месяц[а]?[е]?[в]?|month[s]?` 23 | durationWds = strings.Join([]string{duration, durationTime}, "|") 24 | durationSuffix = `(утр[а]?[о]?[м]?|morning|вечер[а]?[о]?[м]?|evening|\\|/|днем|полдень|полночь|midday|noon|midnight|ночью)` 25 | ) 26 | 27 | var ( 28 | quarter = `quarter|четверть` 29 | half = `half|пол` 30 | one = `one|один` 31 | two = `two|два` 32 | three = `three|три` 33 | four = `four|четыре` 34 | five = `five|пять` 35 | six = `six|шесть` 36 | seven = `seven|семь` 37 | eight = `eight|восемь` 38 | nine = `nine|девять` 39 | ten = `ten|десять` 40 | wordNumbers = strings.Join([]string{quarter, half, one, two, three, four, five, six, seven, eight, nine, ten}, "|") 41 | ) 42 | 43 | func checkWordNumber(s string) float64 { 44 | if v := forceFloat64(s); v != 0 { 45 | return v 46 | } 47 | switch { 48 | case strings.Contains(quarter, s): 49 | return 0.25 50 | case strings.Contains(half, s): 51 | return 0.5 52 | case strings.Contains(one, s): 53 | return 1 54 | case strings.Contains(two, s): 55 | return 2 56 | case strings.Contains(three, s): 57 | return 3 58 | case strings.Contains(four, s): 59 | return 4 60 | case strings.Contains(five, s): 61 | return 5 62 | case strings.Contains(six, s): 63 | return 6 64 | case strings.Contains(seven, s): 65 | return 7 66 | case strings.Contains(eight, s): 67 | return 8 68 | case strings.Contains(nine, s): 69 | return 9 70 | case strings.Contains(ten, s): 71 | return 10 72 | } 73 | return 0 74 | } 75 | 76 | func calculateDuration(m []string, opts Opts, k int) (time.Time, string) { 77 | dur := durationParse(m[k:], opts) 78 | if dur > 0 { 79 | return opts.Now.Add(dur).In(opts.Now.Location()), m[0] 80 | } 81 | return opts.Now, m[0] 82 | } 83 | 84 | func durationParse(bits []string, opts Opts) (dur time.Duration) { 85 | if strings.Contains(durPrefix, bits[0]) { 86 | return durationParse(normalizeStrings(bits[1:]), opts) 87 | } 88 | 89 | switch len(bits) { 90 | case 1: 91 | word := bits[0] 92 | switch { 93 | case strings.Contains(durationTimeWords, word): 94 | return durationParse([]string{"1", word}, opts) 95 | } 96 | if forceInt64(word) > 0 { 97 | return durationParse([]string{word, "минут"}, opts) 98 | } 99 | case 2: 100 | v := checkWordNumber(bits[0]) 101 | if v == 0 { 102 | return 103 | } 104 | word := strings.TrimSpace(bits[1]) 105 | if v < 1 && v >= 0 { 106 | div := time.Duration(1 / v) 107 | switch { 108 | case strings.Contains(seconds, word): 109 | return time.Second / div 110 | case strings.Contains(minutes, word): 111 | return time.Minute / div 112 | case strings.Contains(hours, word): 113 | return time.Hour / div 114 | case strings.Contains(days, word): 115 | return time.Hour * 12 116 | case strings.Contains(weeksWords, word): 117 | return time.Hour * 12 * 7 118 | case strings.Contains(monthsWords, word): 119 | return time.Hour * 12 * 31 // XXX: 120 | case strings.Contains(years, word): 121 | return time.Hour * 12 * 365 // XXX: 122 | default: 123 | return durationParse(bits[:1], opts) 124 | } 125 | } 126 | 127 | switch { 128 | case strings.Contains(seconds, word): 129 | return time.Duration(v) * time.Second 130 | case strings.Contains(minutes, word): 131 | return time.Duration(v) * time.Minute 132 | case strings.Contains(hours, word): 133 | return time.Duration(v) * time.Hour 134 | case strings.Contains(days, word): 135 | return time.Duration(v) * time.Hour * 24 136 | case strings.Contains(weeksWords, word): 137 | return time.Duration(v) * time.Hour * 24 * 7 138 | case strings.Contains(monthsWords, word): 139 | return time.Duration(v) * time.Hour * 24 * 31 // XXX: 140 | case strings.Contains(years, word): 141 | return time.Duration(v) * time.Hour * 24 * 365 // XXX: 142 | default: 143 | return durationParse(bits[:1], opts) 144 | } 145 | } 146 | return 147 | } 148 | -------------------------------------------------------------------------------- /duration_test.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCheckWordNumber(t *testing.T) { 8 | for _, tt := range []struct { 9 | input string 10 | output float64 11 | }{ 12 | { 13 | "пол", 14 | 0.5, 15 | }, 16 | { 17 | "четверть", 18 | 0.25, 19 | }, 20 | } { 21 | result := checkWordNumber(tt.input) 22 | if result != tt.output { 23 | t.Errorf("%v != %v", result, tt.output) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/tada-team/dateparse 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /month.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | var ( 9 | january = `января|янв|january|jan` 10 | february = `февраля|фев|february|feb` 11 | march = `марта|мар|march|mar` 12 | april = `апреля|апрель|апр|april|apr` 13 | may = `мая|май|may` 14 | june = `июня|июнь|june|jun` 15 | july = `июля|июль|july|jul` 16 | august = `августа|август|august|aug` 17 | september = `сентября|сентябрь|сент|september|sep` 18 | october = `октября|октябрь|october|oct` 19 | november = `ноября|ноябрь|november|nov` 20 | december = `декабря|дек|december|dec` 21 | months = strings.Join([]string{january, february, march, april, may, june, july, august, september, october, november, december}, "|") 22 | ) 23 | 24 | func parseMonth(monthStr string) time.Month { 25 | switch { 26 | case strings.Contains(january, monthStr): 27 | return 1 28 | case strings.Contains(february, monthStr): 29 | return 2 30 | case strings.Contains(march, monthStr): 31 | return 3 32 | case strings.Contains(april, monthStr): 33 | return 4 34 | case strings.Contains(may, monthStr): 35 | return 5 36 | case strings.Contains(june, monthStr): 37 | return 6 38 | case strings.Contains(july, monthStr): 39 | return 7 40 | case strings.Contains(august, monthStr): 41 | return 8 42 | case strings.Contains(september, monthStr): 43 | return 9 44 | case strings.Contains(october, monthStr): 45 | return 10 46 | case strings.Contains(november, monthStr): 47 | return 11 48 | case strings.Contains(december, monthStr): 49 | return 12 50 | } 51 | return 0 52 | } 53 | -------------------------------------------------------------------------------- /time.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | var ( 11 | timePrefix = `(с|в|к|by|at)` 12 | timeSuffix = `(утра|вечера|мин[у]?[т]?|a[.]?m|p[.]?m)` 13 | ) 14 | 15 | var ( 16 | hhmmRegex = regexp.MustCompile(fmt.Sprintf(`%s?[" "]?%s[.:]%s`, timePrefix, hourHH, minuteMM)) 17 | hhRegex = regexp.MustCompile(fmt.Sprintf(`%s[" "]%s\s?%s?`, timePrefix, hourHH, timeSuffix)) 18 | baseTimeOrientationRegex = regexp.MustCompile(fmt.Sprintf(`%s?%s?[" "]?%s`, timePrefix, datePrefix, durationSuffix)) 19 | ) 20 | 21 | func parseTime(s string, opts Opts) (t time.Time, st string) { 22 | switch { 23 | case hhmmRegex.MatchString(s): 24 | return calculateTime(hhmmRegex.FindStringSubmatch(s), opts) 25 | case hhRegex.MatchString(s): 26 | return calculateTime(hhRegex.FindStringSubmatch(s), opts) 27 | case baseTimeOrientationRegex.MatchString(s): 28 | return calculateTime(baseTimeOrientationRegex.FindStringSubmatch(s), opts) 29 | } 30 | return opts.Now, st 31 | } 32 | 33 | func calculateTime(t []string, opts Opts) (time.Time, string) { 34 | m := normalizeStrings(t[1:]) 35 | switch len(m) { 36 | case 4: 37 | hour := forceInt(m[2]) 38 | minute := forceInt(m[3]) 39 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), hour, minute, 0, opts), t[0] 40 | case 3: 41 | hour := forceInt(m[1]) 42 | switch { 43 | case strings.Contains(morning, m[2]): 44 | if hour > 12 { 45 | hour -= 12 46 | } 47 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), hour, 0, 0, opts), t[0] 48 | case strings.Contains(evening, m[2]): 49 | if hour < 12 { 50 | hour += 12 51 | } 52 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), hour, 0, 0, opts), t[0] 53 | } 54 | minute := forceInt(m[2]) 55 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), hour, minute, 0, opts), t[0] 56 | case 2: 57 | switch { 58 | case strings.Contains(morning, m[1]): 59 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), 10, 0, 0, opts), t[0] 60 | case strings.Contains(noon, m[1]): 61 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), 12, 0, 0, opts), t[0] 62 | case strings.Contains(timePrefix, m[0]): 63 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), forceInt(m[1]), 0, 0, opts), t[0] 64 | } 65 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), forceInt(m[0]), forceInt(m[1]), 0, opts), t[0] 66 | case 1: 67 | switch { 68 | case strings.Contains(morning, m[0]): 69 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), 10, 0, 0, opts), t[0] 70 | case strings.Contains(evening, m[0]): 71 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), 18, 0, 0, opts), t[0] 72 | case strings.Contains(noon, m[0]): 73 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), 12, 0, 0, opts), t[0] 74 | case strings.Contains(midnight, m[0]): 75 | return getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), 0, 0, 0, opts), t[0] 76 | 77 | } 78 | } 79 | return opts.Now, t[0] 80 | } 81 | -------------------------------------------------------------------------------- /week.go: -------------------------------------------------------------------------------- 1 | package dateparse 2 | 3 | import ( 4 | "strings" 5 | "time" 6 | ) 7 | 8 | var ( 9 | sunday = `воскресенье|sunday` 10 | monday = `понедельник|monday` 11 | tuesday = `вторник|tuesday` 12 | wednesday = `среду|среда|wednesday` 13 | thursday = `четверг|thursday` 14 | friday = `пятницу|пятница|friday` 15 | saturday = `субботу|суббота|saturday` 16 | weeks = strings.Join([]string{ 17 | sunday, 18 | monday, 19 | tuesday, 20 | wednesday, 21 | thursday, 22 | friday, 23 | saturday, 24 | }, "|") 25 | ) 26 | 27 | var ( 28 | shortSunday = `вс|воскр|sun` 29 | shortMonday = `пн|пнд|понед|mon` 30 | shortTuesday = `вт|thu` 31 | shortWednesday = `ср|wed` 32 | shortThursday = `чт|thu` 33 | shortFriday = `пт|fri` 34 | shortSaturday = `сб|sat` 35 | shortWeeks = strings.Join([]string{ 36 | shortSunday, 37 | shortMonday, 38 | shortTuesday, 39 | shortWednesday, 40 | shortThursday, 41 | shortFriday, 42 | shortSaturday, 43 | }, "|") 44 | ) 45 | 46 | func parseWeekDay(s string, opts Opts) time.Time { 47 | date := getDate(opts.Now.Year(), opts.Now.Month(), opts.Now.Day(), opts.TodayEndHour, 0, 0, opts) 48 | if weakDay := parseWeekDays(s); weakDay < 7 { 49 | v := weakDay - int(date.Weekday()) 50 | if v < 0 { 51 | date = date.Add(time.Duration(v)*24*time.Hour + 7*24*time.Hour) 52 | } else { 53 | date = date.Add(time.Duration(v) * 24 * time.Hour) 54 | } 55 | } 56 | return date 57 | } 58 | 59 | func parseWeekDays(s string) int { 60 | switch { 61 | case strings.Contains(strings.Join([]string{sunday, shortSunday}, "|"), s): 62 | return 0 63 | case strings.Contains(strings.Join([]string{monday, shortMonday}, "|"), s): 64 | return 1 65 | case strings.Contains(strings.Join([]string{tuesday, shortTuesday}, "|"), s): 66 | return 2 67 | case strings.Contains(strings.Join([]string{wednesday, shortWednesday}, "|"), s): 68 | return 3 69 | case strings.Contains(strings.Join([]string{thursday, shortThursday}, "|"), s): 70 | return 4 71 | case strings.Contains(strings.Join([]string{friday, shortFriday}, "|"), s): 72 | return 5 73 | case strings.Contains(strings.Join([]string{saturday, shortSaturday}, "|"), s): 74 | return 6 75 | } 76 | return 7 77 | } 78 | 79 | func calculateWeekDuration(m []string, opts Opts, weekPosition int) (time.Time, string) { 80 | timePosition := weekPosition + 1 81 | if weekPosition < 0 { 82 | weekPosition = len(m) - 1 83 | timePosition = weekPosition - 2 84 | } 85 | date := parseWeekDay(m[weekPosition], opts) 86 | switch { 87 | case strings.Contains(durPrefix, m[weekPosition-1]): 88 | date = date.Add(24 * 7 * time.Hour) 89 | } 90 | if len(m) > 3 { 91 | switch { 92 | case strings.Contains(morning, m[timePosition]) && m[timePosition] != "": 93 | if date.Weekday() == opts.Now.Weekday() && opts.Now.Hour() > 10 { 94 | date = date.Add(24 * 7 * time.Hour) 95 | } 96 | return getDate(date.Year(), date.Month(), date.Day(), 10, 0, 0, opts), m[0] 97 | case strings.Contains(evening, m[timePosition]) && m[timePosition] != "": 98 | return date, m[0] 99 | case strings.Contains(noon, m[timePosition]) && m[timePosition] != "": 100 | return getDate(date.Year(), date.Month(), date.Day(), 12, 0, 0, opts), m[0] 101 | case strings.Contains(midnight, m[timePosition]) && m[timePosition] != "": 102 | return getDate(date.Year(), date.Month(), date.Day(), 0, 0, 0, opts), m[0] 103 | } 104 | } 105 | if date.Before(opts.Now) { 106 | date = date.Add(7 * 24 * time.Hour) 107 | } 108 | return date, m[0] 109 | } 110 | --------------------------------------------------------------------------------