Sweep tail: four files the Russian sweep missed, plus a dead duplicate grammar #169
@@ -2,12 +2,20 @@ package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/kami/maven/internal/lexicon"
|
||||
)
|
||||
|
||||
// reminderMarker — the words that open a reminder. Stripped because they are
|
||||
// the instruction, not the thing to say at the hour.
|
||||
var reminderMarker = regexp.MustCompile(`(?i)^\s*(?:напомни(?:те)?|напомнить|remind)\s*(?:мне|me)?[\s,:—-]*`)
|
||||
//
|
||||
// The verbs come from the lexicon (Vikunja #530). They are a closed set of the
|
||||
// commands she answers to, exactly like capture_verbs, and the literal that
|
||||
// stood here knew four of them.
|
||||
var reminderMarker = regexp.MustCompile(`(?i)^\s*(?:` + alternation(lexicon.ReminderVerbs()) +
|
||||
`)\s*(?:мне|me)?[\s,:—-]*`)
|
||||
|
||||
// reminderTimeWords — the time expressions a reminder carries, removed from
|
||||
// the body because the fire time is already a column. Ordered longest-first
|
||||
@@ -17,12 +25,36 @@ var reminderMarker = regexp.MustCompile(`(?i)^\s*(?:напомни(?:те)?|на
|
||||
// Go's \b is ASCII-only and never fires next to a Cyrillic letter, so the word
|
||||
// boundaries here are written out as whitespace or an end of string — the same
|
||||
// trap the agenda grammars hit.
|
||||
//
|
||||
// The Russian word lists are gone (Vikunja #530). The day words are
|
||||
// lexicon.DayOffsetWords, which is why "вчера" and "позавчера" are stripped now
|
||||
// and were not before, and the times of day are lexicon.PartsOfDay. What is
|
||||
// still written out here is the shape of a clock reading — a preposition, digits,
|
||||
// a colon — which is structured input rather than a claim about Russian.
|
||||
var reminderTimeWords = []*regexp.Regexp{
|
||||
regexp.MustCompile(`(?i)(^|\s)через\s+\S+(\s+(часа?|часов|минут[уы]?|секунд[уы]?|дня|дней|недел[юи]))?(\s|$)`),
|
||||
regexp.MustCompile(`(?i)(^|\s)(в|во)\s+\d{1,2}(:\d{2})?(\s*(часа?|часов))?(\s*(утра|вечера|дня|ночи))?(\s|$)`),
|
||||
regexp.MustCompile(`(?i)(^|\s)(завтра|послезавтра|сегодня|вечером|утром|днём|днем|ночью)(\s|$)`),
|
||||
regexp.MustCompile(`(?i)(^|\s)(` + alternation(lexicon.DayOffsetWords()) + `)(\s|$)`),
|
||||
regexp.MustCompile(`(?i)(^|\s)(` + alternation(lexicon.PartsOfDay()) + `)(\s|$)`),
|
||||
regexp.MustCompile(`(?i)(^|\s)(at|in)\s+\d{1,2}(:\d{2})?\s*(am|pm)?(\s|$)`),
|
||||
regexp.MustCompile(`(?i)(^|\s)(tomorrow|today|tonight)(\s|$)`),
|
||||
}
|
||||
|
||||
// alternation folds a lexicon set into one regexp branch, longest member first
|
||||
// so "послезавтра" is not matched as "завтра" with a tail left behind. Sorted
|
||||
// rather than taken as given, because two members of equal length must still
|
||||
// produce the same pattern on every build.
|
||||
func alternation(set []string) string {
|
||||
out := make([]string, 0, len(set))
|
||||
for _, w := range set {
|
||||
out = append(out, regexp.QuoteMeta(w))
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if len(out[i]) != len(out[j]) {
|
||||
return len(out[i]) > len(out[j])
|
||||
}
|
||||
return out[i] < out[j]
|
||||
})
|
||||
return strings.Join(out, "|")
|
||||
}
|
||||
|
||||
// reminderBody is what she says at the hour.
|
||||
|
||||
@@ -63,7 +63,7 @@ func mustLoad() lexiconFile {
|
||||
for _, name := range []string{
|
||||
"interrogatives", "capture_verbs", "narrative_requests", "cardinals",
|
||||
"day_offsets", "weekdays", "months_genitive", "hours_spoken",
|
||||
"not_place_after_v",
|
||||
"not_place_after_v", "parts_of_day", "reminder_verbs",
|
||||
} {
|
||||
s, ok := f.Sets[name]
|
||||
if !ok || (len(s.Words) == 0 && len(s.Values) == 0) {
|
||||
@@ -104,6 +104,14 @@ func FirstPerson() []string { return words("first_person") }
|
||||
// NotPlaceAfterV returns the words that follow "в" without naming a place.
|
||||
func NotPlaceAfterV() []string { return words("not_place_after_v") }
|
||||
|
||||
// PartsOfDay returns the one-word names for a time of day: "вечером", "утром".
|
||||
// They say which part of a day and never which day, so a caller that needs the
|
||||
// day wants DayOffsetWords instead.
|
||||
func PartsOfDay() []string { return words("parts_of_day") }
|
||||
|
||||
// ReminderVerbs returns the imperatives that open a reminder.
|
||||
func ReminderVerbs() []string { return words("reminder_verbs") }
|
||||
|
||||
// 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.
|
||||
|
||||
@@ -38,37 +38,37 @@
|
||||
]
|
||||
},
|
||||
"cardinals": {
|
||||
"note": "Number words as spoken, with the gender variants Russian requires: один/одна/одно and два/две agree with the noun that follows. Values are the number itself. Twenties and up are compounds and are read as their parts, so only the round members are listed.",
|
||||
"note": "Number words as spoken, with the gender variants Russian requires (один/одна/одно and два/две agree with the noun that follows) and the oblique forms, because a spoken time declines: \"в семь\", \"к семи\", \"около семи\" are three forms of one hour (Vikunja #530). Values are the number itself. Twenties and up are compounds and are read as their parts, so only the round members are listed.",
|
||||
"values": {
|
||||
"ноль": 0, "нуль": 0, "zero": 0,
|
||||
"один": 1, "одна": 1, "одно": 1, "one": 1,
|
||||
"два": 2, "две": 2, "two": 2,
|
||||
"три": 3, "three": 3,
|
||||
"четыре": 4, "four": 4,
|
||||
"пять": 5, "five": 5,
|
||||
"шесть": 6, "six": 6,
|
||||
"семь": 7, "seven": 7,
|
||||
"восемь": 8, "eight": 8,
|
||||
"девять": 9, "nine": 9,
|
||||
"десять": 10, "ten": 10,
|
||||
"одиннадцать": 11, "eleven": 11,
|
||||
"двенадцать": 12, "twelve": 12,
|
||||
"тринадцать": 13, "thirteen": 13,
|
||||
"четырнадцать": 14, "fourteen": 14,
|
||||
"пятнадцать": 15, "fifteen": 15,
|
||||
"шестнадцать": 16, "sixteen": 16,
|
||||
"семнадцать": 17, "seventeen": 17,
|
||||
"восемнадцать": 18, "eighteen": 18,
|
||||
"девятнадцать": 19, "nineteen": 19,
|
||||
"двадцать": 20, "twenty": 20,
|
||||
"тридцать": 30, "thirty": 30,
|
||||
"сорок": 40, "forty": 40,
|
||||
"пятьдесят": 50, "fifty": 50,
|
||||
"шестьдесят": 60, "sixty": 60,
|
||||
"семьдесят": 70, "seventy": 70,
|
||||
"восемьдесят": 80, "eighty": 80,
|
||||
"девяносто": 90, "ninety": 90,
|
||||
"сто": 100, "hundred": 100
|
||||
"один": 1, "одна": 1, "одно": 1, "одного": 1, "одной": 1, "одну": 1, "one": 1,
|
||||
"два": 2, "две": 2, "двух": 2, "two": 2,
|
||||
"три": 3, "трёх": 3, "трех": 3, "three": 3,
|
||||
"четыре": 4, "четырёх": 4, "четырех": 4, "four": 4,
|
||||
"пять": 5, "пяти": 5, "five": 5,
|
||||
"шесть": 6, "шести": 6, "six": 6,
|
||||
"семь": 7, "семи": 7, "seven": 7,
|
||||
"восемь": 8, "восьми": 8, "eight": 8,
|
||||
"девять": 9, "девяти": 9, "nine": 9,
|
||||
"десять": 10, "десяти": 10, "ten": 10,
|
||||
"одиннадцать": 11, "одиннадцати": 11, "eleven": 11,
|
||||
"двенадцать": 12, "двенадцати": 12, "twelve": 12,
|
||||
"тринадцать": 13, "тринадцати": 13, "thirteen": 13,
|
||||
"четырнадцать": 14, "четырнадцати": 14, "fourteen": 14,
|
||||
"пятнадцать": 15, "пятнадцати": 15, "fifteen": 15,
|
||||
"шестнадцать": 16, "шестнадцати": 16, "sixteen": 16,
|
||||
"семнадцать": 17, "семнадцати": 17, "seventeen": 17,
|
||||
"восемнадцать": 18, "восемнадцати": 18, "eighteen": 18,
|
||||
"девятнадцать": 19, "девятнадцати": 19, "nineteen": 19,
|
||||
"двадцать": 20, "двадцати": 20, "twenty": 20,
|
||||
"тридцать": 30, "тридцати": 30, "thirty": 30,
|
||||
"сорок": 40, "сорока": 40, "forty": 40,
|
||||
"пятьдесят": 50, "пятидесяти": 50, "fifty": 50,
|
||||
"шестьдесят": 60, "шестидесяти": 60, "sixty": 60,
|
||||
"семьдесят": 70, "семидесяти": 70, "seventy": 70,
|
||||
"восемьдесят": 80, "восьмидесяти": 80, "eighty": 80,
|
||||
"девяносто": 90, "девяноста": 90, "ninety": 90,
|
||||
"сто": 100, "ста": 100, "hundred": 100
|
||||
}
|
||||
},
|
||||
"day_offsets": {
|
||||
@@ -134,6 +134,20 @@
|
||||
"сутках", "часах", "минутах", "секундах", "неделе", "месяце", "году",
|
||||
"начале", "конце", "середине", "течение", "течении"
|
||||
]
|
||||
},
|
||||
"parts_of_day": {
|
||||
"note": "The times of day named as one word, in the instrumental case Russian uses for when something happens. A day has as many parts as it has, so this set is finished. They are not day offsets: \"вечером\" says which part of a day, never which day (Vikunja #530).",
|
||||
"words": [
|
||||
"утром", "днём", "днем", "вечером", "ночью",
|
||||
"morning", "afternoon", "evening", "night"
|
||||
]
|
||||
},
|
||||
"reminder_verbs": {
|
||||
"note": "The imperatives that mean \"remind me\", in the forms he speaks. The same kind of set as capture_verbs and decided the same way: it is her vocabulary, not a discovery about Russian (Vikunja #530).",
|
||||
"words": [
|
||||
"напомни", "напомните", "напомнить", "напоминай",
|
||||
"remind"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-32
@@ -1,38 +1,34 @@
|
||||
package router
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
// ruNumerals — spoken numbers as digits, for the clock hours and the minutes
|
||||
// that follow them. Every case ending he might say is listed rather than
|
||||
// stemmed: "в семь", "к семи", "около семи" are three forms of one hour, and a
|
||||
// prefix rule short enough to cover them also matches "семья".
|
||||
"github.com/kami/maven/internal/lexicon"
|
||||
)
|
||||
|
||||
// hourNouns — the two words that are the hour noun as often as they are the
|
||||
// number one. "в час дня" means one o'clock, so rewriting it to "в 1 дня" is
|
||||
// right either way.
|
||||
//
|
||||
// Stops at thirty, which is as far as a spoken time goes ("без двадцати
|
||||
// восемь", "в половине шестого"). Anything larger is said in digits.
|
||||
var ruNumerals = map[string]string{
|
||||
"один": "1", "одного": "1", "одну": "1", "час": "1", "часу": "1",
|
||||
"два": "2", "две": "2", "двух": "2",
|
||||
"три": "3", "трёх": "3", "трех": "3",
|
||||
"четыре": "4", "четырёх": "4", "четырех": "4",
|
||||
"пять": "5", "пяти": "5",
|
||||
"шесть": "6", "шести": "6",
|
||||
"семь": "7", "семи": "7",
|
||||
"восемь": "8", "восьми": "8",
|
||||
"девять": "9", "девяти": "9",
|
||||
"десять": "10", "десяти": "10",
|
||||
"одиннадцать": "11", "одиннадцати": "11",
|
||||
"двенадцать": "12", "двенадцати": "12",
|
||||
"тринадцать": "13", "тринадцати": "13",
|
||||
"четырнадцать": "14", "четырнадцати": "14",
|
||||
"пятнадцать": "15", "пятнадцати": "15",
|
||||
"шестнадцать": "16", "шестнадцати": "16",
|
||||
"семнадцать": "17", "семнадцати": "17",
|
||||
"восемнадцать": "18", "восемнадцати": "18",
|
||||
"девятнадцать": "19", "девятнадцати": "19",
|
||||
"двадцать": "20", "двадцати": "20",
|
||||
"тридцать": "30", "тридцати": "30",
|
||||
"сорок": "40", "сорока": "40",
|
||||
"пятьдесят": "50", "пятидесяти": "50",
|
||||
// They are not cardinals and do not belong in the lexicon's number set: nobody
|
||||
// counts "час яблок". Everything else this file reads comes from
|
||||
// lexicon.Cardinal, which is where the number words live complete, oblique forms
|
||||
// included (Vikunja #530). The table here used to be a second copy that stopped
|
||||
// at fifty and disagreed with the lexicon about its own members.
|
||||
var hourNouns = map[string]string{"час": "1", "часу": "1"}
|
||||
|
||||
// numeralDigit reports the digits a spoken number is written as, for a clock
|
||||
// hour or the minutes after it.
|
||||
func numeralDigit(word string) (string, bool) {
|
||||
if d, ok := hourNouns[word]; ok {
|
||||
return d, true
|
||||
}
|
||||
n, ok := lexicon.Cardinal(word)
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
return strconv.Itoa(n), true
|
||||
}
|
||||
|
||||
// numeralContext — the words that make a numeral a time. A numeral is only
|
||||
@@ -67,7 +63,7 @@ func SpellOutDigits(text string) string {
|
||||
copy(out, toks)
|
||||
for i, tok := range toks {
|
||||
key := strings.ToLower(strings.Trim(tok, ".,!?;:«»\"'"))
|
||||
digit, ok := ruNumerals[key]
|
||||
digit, ok := numeralDigit(key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user