dc3cda666e
At 14:41 "напомни в половине первого пообедать" was set for 12:30 the same day, two hours gone, and confirmed as "напомню сегодня в 12:30". dateparser is handed PREFER_DATES_FROM future and does not apply it to an HH:MM time on today's date. parseClock in the stub has always rolled forward, so the two parsers disagreed and the production one was the wrong half. rollPastClockForward runs on the python result. Only a bare clock rolls: a sentence naming its day keeps it, so a deliberate "сегодня в 12:30" stays where he put it, and past by a day or more is not a clock resolved onto today. NamesADay reads weekdays by lemma, the relative day words and the month names, all from the lexicon. Measured against real dateparser in a venv: "в половине первого" 05 Aug 12:30 to 06 Aug 12:30, "в 12:30" the same, "сегодня в 12:30" unchanged, and the relative and named-day cases unchanged. Left open: a reminder he places in the past is still accepted silently. Saying the hour has gone is a phrasing gap, not this fix.
140 lines
4.1 KiB
Go
140 lines
4.1 KiB
Go
package router
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/kami/maven/internal/lexicon"
|
|
"github.com/kami/maven/internal/morph"
|
|
)
|
|
|
|
// MentionsTime reports whether the sentence names a time at all, whether or not
|
|
// a parser could read it.
|
|
//
|
|
// The caller is the multi-turn seam. A reminder whose time slot is empty used to
|
|
// inherit the previous reminder's hour, so "напомни без четверти восемь
|
|
// выходить" landed at 07:30 because the turn before it had (V-543). Inheriting
|
|
// is right when the sentence names no time and wrong when it names one the
|
|
// parser missed, and this is the test that tells those apart. Missing the time
|
|
// he said means asking; inheriting means a wrong alarm he stops thinking about.
|
|
//
|
|
// Every signal here is a closed lexicon class or a digit, so this reads data and
|
|
// decides nothing about meaning.
|
|
func MentionsTime(text string) bool {
|
|
toks := strings.Fields(strings.ToLower(text))
|
|
for i, raw := range toks {
|
|
tok := cleanWord(raw)
|
|
if timeMarkers[tok] {
|
|
return true
|
|
}
|
|
if isDigitClock(tok) {
|
|
return true
|
|
}
|
|
if _, ok := numeralDigit(tok); ok && hasTimeNeighbour(toks, i) {
|
|
return true
|
|
}
|
|
if _, _, ok := halfPastAt(toks, i); ok {
|
|
return true
|
|
}
|
|
if _, _, _, ok := quarterToAt(toks, i); ok {
|
|
return true
|
|
}
|
|
if isWeekday(tok) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isDigitClock reports whether the token is a written clock, "19:30". The
|
|
// minutes must be written as two digits, because a clock is and a score is not:
|
|
// "счёт 3:2" names no time.
|
|
func isDigitClock(tok string) bool {
|
|
h, m, found := strings.Cut(tok, ":")
|
|
if !found || len(m) != 2 {
|
|
return false
|
|
}
|
|
hn, err := strconv.Atoi(h)
|
|
if err != nil || hn < 0 || hn > 23 {
|
|
return false
|
|
}
|
|
mn, err := strconv.Atoi(m)
|
|
return err == nil && mn >= 0 && mn <= 59
|
|
}
|
|
|
|
// NamesADay reports whether the sentence names a calendar day: a weekday, a
|
|
// relative day word, or a month beside a date. A bare clock names none of them,
|
|
// which is what lets a parser roll it forward to the next occurrence.
|
|
//
|
|
// "напомни сегодня в 12:30" names the day, so it stays on it even when 12:30 has
|
|
// passed. Rolling that one forward would move a reminder he placed deliberately.
|
|
func NamesADay(text string) bool {
|
|
for _, raw := range strings.Fields(strings.ToLower(text)) {
|
|
tok := cleanWord(raw)
|
|
if _, ok := lexicon.DayOffset(tok); ok {
|
|
return true
|
|
}
|
|
if isWeekday(tok) || isMonth(tok) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isMonth reports whether the token is a month name. The lexicon holds the
|
|
// genitive, which is the form a spoken date uses: "10 июля".
|
|
func isMonth(tok string) bool {
|
|
for m := 1; m <= 12; m++ {
|
|
if tok == lexicon.MonthGenitive(m) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// isWeekday reports whether the token is a day of the week in any case. The
|
|
// lexicon lists the nominative, and "в пятницу" is what a reminder says, so the
|
|
// match is by lemma — grammar is morph's job, not a second word list.
|
|
func isWeekday(tok string) bool {
|
|
for i := 0; i < 7; i++ {
|
|
if morph.SameWord(tok, lexicon.Weekday(i)) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// timeMarkers — the words that name a time on their own: the qualifiers that
|
|
// turn an hour into a part of the day, the relative day words, the weekdays and
|
|
// the two relative openers. Built from the lexicon at init, so a word added
|
|
// there is a word this reads.
|
|
var timeMarkers = buildTimeMarkers()
|
|
|
|
func buildTimeMarkers() map[string]bool {
|
|
m := map[string]bool{
|
|
"утра": true, "вечера": true, "дня": true, "ночи": true,
|
|
"часа": true, "часов": true, "час": true, "часу": true,
|
|
"минут": true, "минуты": true, "минуту": true,
|
|
"через": true, "полчаса": true, "сейчас": true,
|
|
"am": true, "pm": true, "noon": true, "midnight": true,
|
|
}
|
|
for _, w := range lexicon.PartsOfDay() {
|
|
m[w] = true
|
|
}
|
|
for _, w := range lexicon.DayOffsetWords() {
|
|
m[w] = true
|
|
}
|
|
for i := 0; i < 7; i++ {
|
|
if w := lexicon.Weekday(i); w != "" {
|
|
m[w] = true
|
|
}
|
|
}
|
|
for w := range halfWords {
|
|
m[w] = true
|
|
}
|
|
for w := range minutesTo {
|
|
m[w] = true
|
|
}
|
|
return m
|
|
}
|