ordinal selection asks the lexicon, and declines a half hour (V-522)
Group 1 of the sweep listed cmd/mavend/ordinal.go, and it was still
picking a position by stem prefix: {"перв", 1}, {"втор", 2}. The lexicon
already carries every form with its position and "последний" as -1, up to
twelve rather than five, so parseOrdinal reads that instead. "вторым" and
"седьмую" were missed before and now land.
A wider set opens one hole the stems did not have. Russian names a half
hour with the genitive ordinal of the hour it is entering, so "в половине
восьмого" would read as the eighth thing she read out. The forms of
"половина" move into the lexicon as half_hour, where the clock rewrite in
internal/router/halfpast.go and this refusal read one copy, and
parseOrdinal skips an ordinal standing behind one.
Six new parseOrdinal cases. cmd/mavend, internal/router, internal/lexicon
and internal/calendar all pass.
This commit is contained in:
+23
-25
@@ -8,6 +8,7 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/kami/maven/internal/dialogue"
|
||||
"github.com/kami/maven/internal/lexicon"
|
||||
"github.com/kami/maven/internal/store"
|
||||
)
|
||||
|
||||
@@ -26,44 +27,41 @@ import (
|
||||
// does not say what to do with it. Acting on the bare word would guess, and a
|
||||
// wrong guess here closes work he never finished.
|
||||
|
||||
// candidateOrdinals — the words that pick a position, by index. Prefix match,
|
||||
// because Russian declines them: "первый", "первую", "первое".
|
||||
var candidateOrdinals = []struct {
|
||||
word string
|
||||
nth int
|
||||
}{
|
||||
{"перв", 1}, {"втор", 2}, {"трет", 3}, {"четв", 4}, {"пят", 5},
|
||||
{"first", 1}, {"second", 2}, {"third", 3},
|
||||
}
|
||||
// The position words come from the lexicon, which lists every form with its
|
||||
// position and "последний" as -1 (V-522). They used to be stem prefixes here —
|
||||
// {"перв", 1}, {"втор", 2} — which is the shape that sweep removed: a stem
|
||||
// decides meaning by guessing where a word ends, and "трет" also opens
|
||||
// "third-party". The lexicon runs to twelve rather than five, so he can pick
|
||||
// past the fifth of a longer list; resolveCandidate already answers a position
|
||||
// she did not read.
|
||||
|
||||
// candidateDigits — "второй" said as a number. Matched whole, never by prefix:
|
||||
// "15" starts with "1" and is a time, not a position.
|
||||
// "15" starts with "1" and is a time, not a position. Digits are not a Russian
|
||||
// word list, so they stay here rather than in the lexicon.
|
||||
var candidateDigits = map[string]int{"1": 1, "2": 2, "3": 3, "4": 4, "5": 5}
|
||||
|
||||
// candidateLast — "последний" picks the end of the list whatever its length.
|
||||
var candidateLast = []string{"последн", "last"}
|
||||
|
||||
// parseOrdinal reads which position he named. 0 and false when he named none.
|
||||
// A negative result means the last one.
|
||||
func parseOrdinal(text string) (int, bool) {
|
||||
// Token by token, not substring: " 1" would otherwise match inside
|
||||
// "напомни в 15:00" and turn a reminder into a selection.
|
||||
for _, tok := range strings.FieldsFunc(strings.ToLower(text), func(r rune) bool {
|
||||
toks := strings.FieldsFunc(strings.ToLower(text), func(r rune) bool {
|
||||
return !unicode.IsLetter(r) && !unicode.IsDigit(r)
|
||||
}) {
|
||||
for _, w := range candidateLast {
|
||||
if strings.HasPrefix(tok, w) {
|
||||
return -1, true
|
||||
}
|
||||
}
|
||||
})
|
||||
for i, tok := range toks {
|
||||
if n, ok := candidateDigits[tok]; ok {
|
||||
return n, true
|
||||
}
|
||||
for _, o := range candidateOrdinals {
|
||||
// Prefix, because Russian declines them: "первый", "первую".
|
||||
if strings.HasPrefix(tok, o.word) {
|
||||
return o.nth, true
|
||||
}
|
||||
// A spoken half hour names the hour it is entering with the same
|
||||
// genitive ordinal: "в половине восьмого" is 07:30, not the eighth
|
||||
// thing she read out. She reads a list and he answers with a time
|
||||
// often enough that this has to be declined here, or the reminder
|
||||
// becomes a selection.
|
||||
if i > 0 && lexicon.IsHalfHour(toks[i-1]) {
|
||||
continue
|
||||
}
|
||||
if n, ok := lexicon.Ordinal(tok); ok {
|
||||
return n, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
|
||||
@@ -27,6 +27,16 @@ func TestParseOrdinalReadsThePosition(t *testing.T) {
|
||||
{"", 0, false},
|
||||
// A digit inside a time is not a position.
|
||||
{"напомни в 15:00", 0, false},
|
||||
// Forms the stem list used to miss, and positions past its fifth.
|
||||
{"вторым", 2, true},
|
||||
{"седьмую", 7, true},
|
||||
{"одиннадцатый", 11, true},
|
||||
// A spoken half hour names its hour with the same genitive ordinal, so
|
||||
// this is 07:30 and not the eighth thing she read out (V-522).
|
||||
{"напомни в половине восьмого", 0, false},
|
||||
{"полвосьмого", 0, false},
|
||||
// The ordinal still wins when the half word is not in front of it.
|
||||
{"восьмую сделал", 8, true},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got, ok := parseOrdinal(c.text)
|
||||
|
||||
@@ -63,7 +63,7 @@ func mustLoad() lexiconFile {
|
||||
for _, name := range []string{
|
||||
"interrogatives", "capture_verbs", "narrative_requests", "cardinals", "ordinals",
|
||||
"day_offsets", "weekdays", "months_genitive", "hours_spoken",
|
||||
"not_place_after_v", "parts_of_day", "reminder_verbs",
|
||||
"not_place_after_v", "parts_of_day", "reminder_verbs", "half_hour",
|
||||
} {
|
||||
s, ok := f.Sets[name]
|
||||
if !ok || (len(s.Words) == 0 && len(s.Values) == 0) {
|
||||
@@ -112,6 +112,23 @@ func PartsOfDay() []string { return words("parts_of_day") }
|
||||
// ReminderVerbs returns the imperatives that open a reminder.
|
||||
func ReminderVerbs() []string { return words("reminder_verbs") }
|
||||
|
||||
// HalfHourWords returns those forms, for a caller folding every time word into
|
||||
// one set rather than asking about one word.
|
||||
func HalfHourWords() []string { return words("half_hour") }
|
||||
|
||||
// IsHalfHour reports whether a word introduces a spoken half hour, so the
|
||||
// ordinal after it is an hour rather than a position. One caller reads that
|
||||
// ordinal as the hour and another has to decline it; both ask here.
|
||||
func IsHalfHour(word string) bool {
|
||||
w := norm(word)
|
||||
for _, h := range ru.Sets["half_hour"].Words {
|
||||
if w == h {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Cardinal reports the value of a spoken number word. The word is compared
|
||||
// lowercased and trimmed, because it arrives from a tokenizer that may not have
|
||||
// done either.
|
||||
|
||||
@@ -166,6 +166,13 @@
|
||||
"напомни", "напомните", "напомнить", "напоминай",
|
||||
"remind"
|
||||
]
|
||||
},
|
||||
"half_hour": {
|
||||
"note": "The forms of \"половина\" that introduce a spoken half hour: \"в половине восьмого\", \"к половине\", the bare \"пол\" of \"полвосьмого\". The set matters to two callers and for opposite reasons (V-522). The clock rewrite reads the ordinal after one of these as the hour being entered, and the ordinal-selection turn has to REFUSE that ordinal, because \"в половине восьмого\" names a time and not the eighth thing she read out.",
|
||||
"words": [
|
||||
"половина", "половине", "половину", "половины", "пол",
|
||||
"half"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,9 @@ import (
|
||||
// had one and added when it did not, because the stub scans for "в" before a
|
||||
// clock and the contracted "полвосьмого" carries no preposition at all.
|
||||
|
||||
// halfWords — the forms of "половина" a spoken time uses. "в половине",
|
||||
// "половина", "к половине", "полвосьмого". Closed and tiny; the ordinal beside
|
||||
// them is what carries the hour, and that comes from the lexicon.
|
||||
var halfWords = map[string]bool{
|
||||
"половина": true, "половине": true, "половину": true, "половины": true,
|
||||
"пол": true, "half": true,
|
||||
}
|
||||
// The forms of "половина" a spoken time uses are a closed class and live in the
|
||||
// lexicon as half_hour, because the ordinal-selection turn has to decline the
|
||||
// same ordinal this file reads (V-522). Ask lexicon.IsHalfHour.
|
||||
|
||||
// minutesTo — the words that name the minutes in a "без X" hour. "четверти" is
|
||||
// the only one that is not a number; the rest are cardinals and are read as
|
||||
@@ -74,7 +70,7 @@ func halfPastAt(toks []string, i int) (hour, width int, ok bool) {
|
||||
return h, 1, true
|
||||
}
|
||||
}
|
||||
if !halfWords[head] || i+1 >= len(toks) {
|
||||
if !lexicon.IsHalfHour(head) || i+1 >= len(toks) {
|
||||
return 0, 0, false
|
||||
}
|
||||
h, ok := enteredHour(cleanWord(toks[i+1]))
|
||||
|
||||
@@ -129,7 +129,7 @@ func buildTimeMarkers() map[string]bool {
|
||||
m[w] = true
|
||||
}
|
||||
}
|
||||
for w := range halfWords {
|
||||
for _, w := range lexicon.HalfHourWords() {
|
||||
m[w] = true
|
||||
}
|
||||
for w := range minutesTo {
|
||||
|
||||
Reference in New Issue
Block a user