diff --git a/cmd/mavend/historyq.go b/cmd/mavend/historyq.go index ea85394..1375dbd 100644 --- a/cmd/mavend/historyq.go +++ b/cmd/mavend/historyq.go @@ -6,6 +6,8 @@ import ( "log" "strings" "time" + + "github.com/kami/maven/internal/morph" ) // Command history — "что я тебе говорил?", "что ты записала сегодня?" @@ -15,18 +17,34 @@ import ( // storage: everything he tapped in is already a row with a source and a // timestamp, and this only reads them back. -// historyMarkers — the ways he asks what he told her. Each entry is a pair of -// substrings that must BOTH appear, because either half alone is a different -// question: "что я говорил про сервер" is a recall question the notes pass -// answers better, and "что ты записала" with no "что" is not a question at all. -var historyMarkers = [][2]string{ - {"что я", "говорил"}, - {"что я", "сказал"}, - {"что я", "рассказ"}, - {"что ты", "записал"}, - {"что ты", "запомнил"}, - {"что я", "отмечал"}, - {"что я", "отметил"}, +// A history question needs three things in one utterance: the interrogative, +// whose turn is being asked about, and a verb of saying or recording. Any two of +// them are a different question. "что я говорил про сервер" names a topic and +// the notes pass answers it better; "записал молоко" is a capture. +// +// The verbs are matched by lemma through internal/morph, not by a truncated +// prefix (Vikunja #530). The pairs here used to hold "рассказ" and "записал", +// which is the defect V-528 fixed in complaint.go: "рассказ" is also the noun, +// so "что я рассказал ей" and "что я читал рассказ" were the same string test. +// Aspect pairs are separate lemmas in the dictionary, so both members are listed. +var ( + // historySpokenVerbs — what HE did. "что я тебе говорил". + historySpokenVerbs = []string{"говорить", "сказать", "рассказать", "рассказывать", "отметить", "отмечать"} + + // historyRecordedVerbs — what SHE did with it. "что ты записала сегодня". + historyRecordedVerbs = []string{"записать", "запомнить", "отметить", "отмечать"} + + // firstPersonSubjects and secondPersonSubjects — whose turn the question is + // about. Only the subject forms: "что я тебе говорил" is his turn, and the + // dative "тебе" in it is not the subject. + firstPersonSubjects = []string{"я"} + secondPersonSubjects = []string{"ты"} +) + +// historyMarkersEn — the English pairs, kept as substrings because the +// dictionary is Russian. Each half alone is a different question, the same way +// the Russian test needs all three parts. +var historyMarkersEn = [][2]string{ {"what did i", "tell"}, {"what did you", "record"}, } @@ -47,11 +65,56 @@ func isHistoryQuery(u string) bool { return false } } - for _, pair := range historyMarkers { + for _, pair := range historyMarkersEn { if strings.Contains(s, pair[0]) && strings.Contains(s, pair[1]) { return true } } + toks := historyTokens(s) + if !hasAny(toks, "что", "чего") { + return false + } + if hasAny(toks, firstPersonSubjects...) && hasVerbForm(toks, historySpokenVerbs) { + return true + } + return hasAny(toks, secondPersonSubjects...) && hasVerbForm(toks, historyRecordedVerbs) +} + +// historyTokens splits an utterance into bare words. The punctuation goes +// because "говорил?" is the same word as "говорил". +func historyTokens(s string) []string { + toks := strings.Fields(s) + out := make([]string, 0, len(toks)) + for _, t := range toks { + if t = strings.Trim(t, ".,!?;:—–-()\"'«»"); t != "" { + out = append(out, t) + } + } + return out +} + +func hasAny(toks []string, want ...string) bool { + for _, t := range toks { + for _, w := range want { + if t == w { + return true + } + } + } + return false +} + +// hasVerbForm reports whether any token is a form of any of the lemmas. Both +// sides go through the dictionary, so a caller may name the infinitive and he +// may say the past tense. +func hasVerbForm(toks []string, lemmas []string) bool { + for _, t := range toks { + for _, l := range lemmas { + if morph.SameWord(t, l) { + return true + } + } + } return false } diff --git a/cmd/mavend/historyq_test.go b/cmd/mavend/historyq_test.go index b19342a..94ebf06 100644 --- a/cmd/mavend/historyq_test.go +++ b/cmd/mavend/historyq_test.go @@ -41,6 +41,17 @@ func TestIsHistoryQuery(t *testing.T) { {"что я тебе говорил?", true}, {"что ты записала сегодня?", true}, {"что я отмечал?", true}, + // Forms the truncated prefixes did not reach. The dictionary answers + // these because it lemmatises both sides (V-530). + {"что я тебе рассказывал?", true}, + {"что я сказала вчера", true}, + {"что ты запомнила?", true}, + // The noun, not the verb. "рассказ" was a prefix of the old pair, so + // this read as a history question — the same defect V-528 fixed in + // complaint.go, where "лаг" matched "лагерь". + {"что я читал рассказ", false}, + // A verb of saying with nobody saying it. + {"что записать?", false}, // A named topic is a recall question, and the notes pass answers it // better than a list of the last five facts does. {"что я говорил про сервер?", false},