Telegram Webhookによるボット操作機能を追加(課題の追加・完了・一覧・スヌーズ対応)
This commit is contained in:
169
internal/service/date_parser.go
Normal file
169
internal/service/date_parser.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
reTimeHHMM = regexp.MustCompile(`^(\d{1,2}):(\d{2})$`)
|
||||
reTimeHHJMM = regexp.MustCompile(`^(\d{1,2})時(\d{1,2})分$`)
|
||||
reTimeHHJ = regexp.MustCompile(`^(\d{1,2})時$`)
|
||||
reDateMD = regexp.MustCompile(`^(\d{1,2})/(\d{1,2})$`)
|
||||
reDateMJDJ = regexp.MustCompile(`^(\d{1,2})月(\d{1,2})日$`)
|
||||
)
|
||||
|
||||
var weekdayNames = map[string]time.Weekday{
|
||||
"月": time.Monday,
|
||||
"火": time.Tuesday,
|
||||
"水": time.Wednesday,
|
||||
"木": time.Thursday,
|
||||
"金": time.Friday,
|
||||
"土": time.Saturday,
|
||||
"日": time.Sunday,
|
||||
}
|
||||
|
||||
// ParseAddCommand parses the arguments after /add.
|
||||
// Format: <title> <date> [time] [#subject] [!priority]
|
||||
// Returns title, dueDate, subject, priority (Go string: "high"/"medium"/"low"), error.
|
||||
// On format error, err.Error() == "usage" means show usage hint.
|
||||
func ParseAddCommand(text string) (title string, dueDate time.Time, subject string, priority string, err error) {
|
||||
priority = "medium"
|
||||
tokens := strings.Fields(text)
|
||||
if len(tokens) == 0 {
|
||||
err = fmt.Errorf("usage")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract #subject and !priority modifiers
|
||||
var core []string
|
||||
for _, tok := range tokens {
|
||||
switch {
|
||||
case strings.HasPrefix(tok, "#") && len(tok) > 1:
|
||||
subject = tok[1:]
|
||||
case strings.HasPrefix(tok, "!") && len(tok) > 1:
|
||||
priority = parsePriorityJP(tok[1:])
|
||||
default:
|
||||
core = append(core, tok)
|
||||
}
|
||||
}
|
||||
|
||||
if len(core) < 2 {
|
||||
err = fmt.Errorf("usage")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract optional time token from end
|
||||
hour, minute := 23, 59
|
||||
if h, m, ok := parseTimeToken(core[len(core)-1]); ok {
|
||||
hour, minute = h, m
|
||||
core = core[:len(core)-1]
|
||||
}
|
||||
|
||||
if len(core) < 2 {
|
||||
err = fmt.Errorf("usage")
|
||||
return
|
||||
}
|
||||
|
||||
// Extract date token from end
|
||||
dateTok := core[len(core)-1]
|
||||
parsedDate, ok := parseDateToken(dateTok, hour, minute)
|
||||
if !ok {
|
||||
err = fmt.Errorf("日付が読み取れませんでした: %q\n使える形式: 今日/明日/明後日/月〜日/MM/DD/M月D日", dateTok)
|
||||
return
|
||||
}
|
||||
dueDate = parsedDate
|
||||
core = core[:len(core)-1]
|
||||
|
||||
if len(core) == 0 {
|
||||
err = fmt.Errorf("usage")
|
||||
return
|
||||
}
|
||||
title = strings.Join(core, " ")
|
||||
return
|
||||
}
|
||||
|
||||
func parsePriorityJP(s string) string {
|
||||
switch s {
|
||||
case "高":
|
||||
return "high"
|
||||
case "低":
|
||||
return "low"
|
||||
default:
|
||||
return "medium"
|
||||
}
|
||||
}
|
||||
|
||||
func parseTimeToken(s string) (hour, minute int, ok bool) {
|
||||
if m := reTimeHHMM.FindStringSubmatch(s); m != nil {
|
||||
h, _ := strconv.Atoi(m[1])
|
||||
min, _ := strconv.Atoi(m[2])
|
||||
if h <= 23 && min <= 59 {
|
||||
return h, min, true
|
||||
}
|
||||
}
|
||||
if m := reTimeHHJMM.FindStringSubmatch(s); m != nil {
|
||||
h, _ := strconv.Atoi(m[1])
|
||||
min, _ := strconv.Atoi(m[2])
|
||||
if h <= 23 && min <= 59 {
|
||||
return h, min, true
|
||||
}
|
||||
}
|
||||
if m := reTimeHHJ.FindStringSubmatch(s); m != nil {
|
||||
h, _ := strconv.Atoi(m[1])
|
||||
if h <= 23 {
|
||||
return h, 0, true
|
||||
}
|
||||
}
|
||||
return 0, 0, false
|
||||
}
|
||||
|
||||
func parseDateToken(s string, hour, minute int) (time.Time, bool) {
|
||||
now := time.Now()
|
||||
loc := now.Location()
|
||||
|
||||
switch s {
|
||||
case "今日":
|
||||
return time.Date(now.Year(), now.Month(), now.Day(), hour, minute, 0, 0, loc), true
|
||||
case "明日":
|
||||
d := now.AddDate(0, 0, 1)
|
||||
return time.Date(d.Year(), d.Month(), d.Day(), hour, minute, 0, 0, loc), true
|
||||
case "明後日":
|
||||
d := now.AddDate(0, 0, 2)
|
||||
return time.Date(d.Year(), d.Month(), d.Day(), hour, minute, 0, 0, loc), true
|
||||
}
|
||||
|
||||
if wd, ok := weekdayNames[s]; ok {
|
||||
d := nextWeekdayFrom(now, wd)
|
||||
return time.Date(d.Year(), d.Month(), d.Day(), hour, minute, 0, 0, loc), true
|
||||
}
|
||||
|
||||
var month, day int
|
||||
if m := reDateMD.FindStringSubmatch(s); m != nil {
|
||||
month, _ = strconv.Atoi(m[1])
|
||||
day, _ = strconv.Atoi(m[2])
|
||||
} else if m := reDateMJDJ.FindStringSubmatch(s); m != nil {
|
||||
month, _ = strconv.Atoi(m[1])
|
||||
day, _ = strconv.Atoi(m[2])
|
||||
} else {
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
if month < 1 || month > 12 || day < 1 || day > 31 {
|
||||
return time.Time{}, false
|
||||
}
|
||||
t := time.Date(now.Year(), time.Month(month), day, hour, minute, 0, 0, loc)
|
||||
if t.Before(now) {
|
||||
t = t.AddDate(1, 0, 0)
|
||||
}
|
||||
return t, true
|
||||
}
|
||||
|
||||
// nextWeekdayFrom returns the date of the next occurrence of wd from 'from'.
|
||||
func nextWeekdayFrom(from time.Time, wd time.Weekday) time.Time {
|
||||
days := (int(wd) - int(from.Weekday()) + 7) % 7
|
||||
return from.AddDate(0, 0, days)
|
||||
}
|
||||
Reference in New Issue
Block a user