c07722266a
Four reminders in a row on the box all landed at the first one's hour, each confirmed as if it had been read from the sentence: "напомни без четверти восемь выходить" fired at 07:30. followUpMerge inherits a missing slot from the previous same-intent turn, and a reminder time is one of those slots. It also filled the slot before actionReminder's own fallback parse could run, so inheriting hid a time that did parse. router.MentionsTime tells the two cases apart. A sentence that names no time still inherits, which is the follow-up the seam exists for. A sentence that names one the parser missed keeps an empty slot, so she asks. Missing the hour he said costs a question; borrowing one costs an alarm he stops thinking about. Signals are lexicon classes and digits only: the day qualifiers, parts of day, day offsets, weekdays by lemma through morph, the half-past and quarter-to markers, and a written clock whose minutes are two digits so a score does not pass for one. Fact keys and act fns inherit through the same call and are left alone: a borrowed key answers about the wrong thing out loud, which he hears, while a borrowed hour is silent until it fires.
110 lines
3.2 KiB
Go
110 lines
3.2 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
|
|
}
|
|
|
|
// 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
|
|
}
|