weather asks the dictionary before guessing case (V-530)
locationCandidates reversed endings by hand to turn "в Казани" into the nominative the geocoder wants. internal/morph knows the answer for the places it has, so it goes first and the reversals stay behind it for the ones it does not: "Твери" and "Перми" come back unchanged. The four-rune floor was there to stop a two-letter stem, so it now tests the stem instead. "Уфе" was under the floor and "Уфа" was never tried. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -231,8 +231,10 @@ fact or a route is the defect; a regex over structured input — HTML, MIME, JSO
|
||||
argv list — is not. Before writing a Russian word list, pick one of these:
|
||||
|
||||
- **`internal/lexicon`** — closed classes, in `lexicon_ru_v1.json`. Interrogatives,
|
||||
capture verbs, cardinals, day offsets, weekdays, months, spoken hours. Editing a word is
|
||||
a data change, and there is exactly one copy: months used to live in three files.
|
||||
capture verbs, reminder verbs, cardinals, day offsets, parts of day, weekdays, months,
|
||||
spoken hours. Editing a word is a data change, and there is exactly one copy: months used
|
||||
to live in three files. Cardinals carry the oblique forms, because a spoken time declines
|
||||
and `в семь` / `к семи` are one hour.
|
||||
- **`internal/morph`** — grammar, from the vendored golem Russian dictionary. `IsVerbForm`
|
||||
and `SameWord`. Note that lemma matching is BROADER than stem-plus-one-ending, so a verb
|
||||
slot that means the imperative must be matched exactly — `говори` and `говорил` are one
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/kami/maven/internal/morph"
|
||||
)
|
||||
|
||||
type OpenMeteoProvider struct {
|
||||
@@ -101,10 +104,20 @@ func (p *OpenMeteoProvider) CurrentWeather(ctx context.Context, location string)
|
||||
// sentence, in order. He says "какая погода в Казани", so the word arrives in
|
||||
// the prepositional case and the geocoder wants the nominative (Vikunja #421).
|
||||
//
|
||||
// Two cheap reversals cover most of what he says: a final "е" is usually a
|
||||
// nominative "а" (Москве → Москва) or nothing at all (Лондоне → Лондон), and a
|
||||
// final "и" is usually a soft sign (Казани → Казань). Indeclinable names —
|
||||
// Тбилиси, Сочи, Осло — are already nominative and the first candidate answers.
|
||||
// The dictionary answers first (Vikunja #530). internal/morph lemmatises
|
||||
// "Уфе" to "Уфа" and "Москве" to "Москва", which is the same question this
|
||||
// used to guess at by reversing endings, asked of something that knows.
|
||||
//
|
||||
// The reversals stay behind it, because the dictionary does not know every
|
||||
// place: "Твери" and "Перми" come back unchanged, and a final "и" is usually a
|
||||
// soft sign. A final "е" is usually a nominative "а" (Москве → Москва) or
|
||||
// nothing at all (Лондоне → Лондон). Indeclinable names — Тбилиси, Сочи, Осло —
|
||||
// are already nominative and the first candidate answers, which is why the word
|
||||
// as spoken is always tried before anything derived from it.
|
||||
//
|
||||
// There used to be a four-rune floor here, so "Уфе" was asked as spoken and
|
||||
// "Уфа" was never tried. The floor was there to stop a two-letter stem, and the
|
||||
// stem length is what it now tests.
|
||||
//
|
||||
// Nothing here is a guess about the weather: a wrong candidate finds no city
|
||||
// and the caller says so. It only decides which strings are worth asking about.
|
||||
@@ -121,8 +134,9 @@ func locationCandidates(location string) []string {
|
||||
}
|
||||
out = append(out, s)
|
||||
}
|
||||
add(titleFirst(morph.Lemma(location)))
|
||||
r := []rune(location)
|
||||
if len(r) < 4 {
|
||||
if len(r) < 3 {
|
||||
return out
|
||||
}
|
||||
stem := string(r[:len(r)-1])
|
||||
@@ -139,6 +153,17 @@ func locationCandidates(location string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// titleFirst restores the leading capital a place name carries. morph.Lemma
|
||||
// answers lowercased, because a lemma is a dictionary entry and the dictionary
|
||||
// has no opinion about proper nouns.
|
||||
func titleFirst(s string) string {
|
||||
r := []rune(s)
|
||||
if len(r) == 0 {
|
||||
return s
|
||||
}
|
||||
return string(unicode.ToUpper(r[0])) + string(r[1:])
|
||||
}
|
||||
|
||||
func (p *OpenMeteoProvider) geocode(ctx context.Context, location string) (lat, lon float64, name string, err error) {
|
||||
for _, cand := range locationCandidates(location) {
|
||||
lat, lon, name, err = p.geocodeOne(ctx, cand)
|
||||
|
||||
@@ -86,14 +86,20 @@ func TestStubProvider(t *testing.T) {
|
||||
|
||||
// TestLocationCandidates — he speaks the prepositional case and the geocoder
|
||||
// wants the nominative (Vikunja #421).
|
||||
//
|
||||
// The dictionary answers before the reversals now, so the nominative it knows
|
||||
// comes second and anything derived by hand follows (V-530). "Уфе" used to fall
|
||||
// under a four-rune floor and was asked as spoken, so "Уфа" was never tried.
|
||||
func TestLocationCandidates(t *testing.T) {
|
||||
cases := map[string][]string{
|
||||
"Москве": {"Москве", "Москва", "Москв"},
|
||||
"Казани": {"Казани", "Казань", "Казан"},
|
||||
"Лондоне": {"Лондоне", "Лондона", "Лондон"},
|
||||
"Лондоне": {"Лондоне", "Лондон", "Лондона"},
|
||||
"Тбилиси": {"Тбилиси", "Тбились", "Тбилис"},
|
||||
"Berlin": {"Berlin"},
|
||||
"Уфе": {"Уфе"}, // too short to strip — asked as spoken
|
||||
"Уфе": {"Уфе", "Уфа", "Уф"},
|
||||
// The dictionary does not know it, so the soft-sign reversal answers.
|
||||
"Твери": {"Твери", "Тверь", "Твер"},
|
||||
}
|
||||
for in, want := range cases {
|
||||
got := locationCandidates(in)
|
||||
|
||||
Reference in New Issue
Block a user