a named time that did not parse never borrows the last one (V-543)
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.
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package router
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMentionsTime(t *testing.T) {
|
||||
for _, s := range []string{
|
||||
"напомни в семь вечера позвонить маме",
|
||||
"напомни в 19:30 позвонить маме",
|
||||
"напомни без четверти восемь выходить",
|
||||
"напомни в половине первого пообедать",
|
||||
"разбуди меня полвосьмого",
|
||||
"напомни завтра принять лекарство",
|
||||
"напомни в пятницу забрать заказ",
|
||||
"напомни через двадцать минут",
|
||||
"напомни утром выпить таблетку",
|
||||
"remind me at noon to stretch",
|
||||
} {
|
||||
if !MentionsTime(s) {
|
||||
t.Errorf("MentionsTime(%q) = false; this sentence names a time", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A sentence with no time in it must not read as one, or a real follow-up stops
|
||||
// inheriting the hour it meant.
|
||||
func TestMentionsTimeIgnoresSentencesWithoutOne(t *testing.T) {
|
||||
for _, s := range []string{
|
||||
"напомни позвонить маме",
|
||||
"и ещё полить цветы",
|
||||
"напомни про счёт за свет",
|
||||
"купить три яблока",
|
||||
"перезапусти докер",
|
||||
"счёт 3:2 в нашу пользу",
|
||||
"",
|
||||
} {
|
||||
if MentionsTime(s) {
|
||||
t.Errorf("MentionsTime(%q) = true; there is no time in it", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user