Merge the ordinal lexicon change (#177)
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