Files
Maven/internal/router/halfpast_test.go
T
claude 8bbdcd2727 half-past hours parse as the hour being entered (V-538)
Russian names a half hour by the hour it is entering, in the genitive, so
"половина восьмого" is 07:30 and never 08:30. Neither date parser read that
shape, so the reminder parsed to nothing.

rewriteHalfPast runs in front of the token pass in SpellOutDigits, so the
python parser and the stub both see "в 7:30". It also reads the contracted
"полвосьмого" and the quarter-to shape "без четверти восемь", which counts
from a cardinal and is 07:45. Minus one is in one place, clockHourBefore, with
twelve rather than zero before one.

Ordinals eleven and twelve added to the lexicon, because a clock reaches them.
Minutes a spoken clock does not use are left alone: a guess here is a missed
dose.

Classifier + onnx over the routing fixture 58/82 to 62/87, three new cases,
none regressed. Python dateparser is not installed on this host, so only the
stub was measured. See docs/evals/2026-08-05-half-past-hours.md.
2026-08-05 14:24:49 +04:00

76 lines
3.3 KiB
Go

package router
import (
"context"
"testing"
"time"
)
// The shape V-538 was filed for. Russian names the hour being entered, so
// "половина восьмого" is 07:30 and never 08:30. Every case here is checked
// against the hour a speaker means, not against the word in the sentence.
func TestRewriteHalfPast(t *testing.T) {
for _, tc := range []struct{ in, want string }{
{"напомни в половине восьмого", "напомни в 7:30"},
{"напомни в половине восьмого вечера", "напомни в 7:30 вечера"},
{"половина восьмого", "в 7:30"},
{"к половине девятого", "к 8:30"},
// The hour before one is twelve, not zero.
{"в половине первого", "в 12:30"},
{"в половине двенадцатого", "в 11:30"},
// Contracted, and with no preposition of its own.
{"разбуди полвосьмого", "разбуди в 7:30"},
{"разбуди пол-восьмого", "разбуди в 7:30"},
{"разбуди пол восьмого", "разбуди в 7:30"},
// Quarter-to counts the same way, from a cardinal.
{"без четверти восемь", "в 7:45"},
{"позвони без двадцати восемь", "позвони в 7:40"},
{"без пяти час", "в 12:55"},
// Untouched: not a time.
{"половина яблока", "половина яблока"},
{"без разницы восемь", "без разницы восемь"},
{"отметь последний пункт", "отметь последний пункт"},
{"напомни в 19:30", "напомни в 19:30"},
{"", ""},
} {
if got := rewriteHalfPast(tc.in); got != tc.want {
t.Errorf("rewriteHalfPast(%q) = %q, want %q", tc.in, got, tc.want)
}
}
}
// A guess in this shape is worse than a refusal, so the minutes a spoken clock
// does not use are left for the parsers to fail on rather than invented.
func TestQuarterToTakesOnlyClockMinutes(t *testing.T) {
for _, in := range []string{"без семи восемь", "без тринадцати восемь"} {
if got := rewriteHalfPast(in); got != in {
t.Errorf("rewriteHalfPast(%q) = %q; those minutes are not a spoken clock", in, got)
}
}
}
// The rewrite is only useful if the parser under it reads the result. The stub
// is the one that always answers, because the host path falls back to it
// whenever python dateparser is missing.
func TestStubParsesAHalfHour(t *testing.T) {
now := time.Date(2026, 8, 5, 9, 0, 0, 0, time.Local)
for _, tc := range []struct {
utt string
hour, min int
}{
{"напомни выпить таблетку в половине восьмого вечера", 19, 30},
{"напомни в половине одиннадцатого", 10, 30},
{"разбуди полвосьмого", 7, 30},
{"позвони без четверти восемь", 7, 45},
} {
got, ok, err := (StubDateTimeParser{}).Parse(context.Background(), tc.utt, now)
if err != nil || !ok {
t.Errorf("Parse(%q) = %v, %v, %v; want a time", tc.utt, got, ok, err)
continue
}
if got.Hour() != tc.hour || got.Minute() != tc.min {
t.Errorf("Parse(%q) = %02d:%02d, want %02d:%02d", tc.utt, got.Hour(), got.Minute(), tc.hour, tc.min)
}
}
}