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: <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) }