fix(hurtig tilføjelse): fire ting, der blev læst forkert eller tabt i stilhed
Fundet under arbejdet med genopbygnings-prompten, hvor den rigtige parser blev kørt over en tabel af sætninger. - "at" før et 12-timers klokkeslæt blev stående i titlen: "file the VAT return tomorrow at 10am" blev opgaven "file the VAT return at". Markøren (at, kl, klokken) hører nu med til tiden, også foran "7:30 pm". - "køb 1/2 liter fløde" fik forfaldsdato 1. februar. En d/m efterfulgt af en mængdeenhed (liter, dl, kg, stk, tsk, kop, del, …) er en mængde. Uden enhed er det stadig en dato — "3/4" alene, "møde 3/4 med Anders" og også "3/4 kage", fordi intet i "kage" skiller tredje april fra tre fjerdedele, og en dato, der stille forsvinder, er værre end en, man selv sletter. - Et "#navn" eller "/navn", der ikke fandtes, blev klippet ud af titlen, og opgaven landede et andet sted uden et ord. Serveren har længe sendt unknown_project og unknown_section; fladen siger dem nu i en besked, også når en underopgave beholder sin opgaves projekt. - Titlen i opgaveruden blev læst om, men gentagelsen blev læst under `parsed.recurrence`, et felt der ikke findes, så "hver mandag" forsvandt. Den læser nu `recurrence_rule` og tager også klokkeslæt, projekt og alarm med, og siger gentagelsen på læserens sprog i stedet for et fast "gentagelse". Alarmen lægges før rettelsen, så den skrevne ikke stables oven på standardalarmen. Prøver: to nye Go-prøver i quickadd og to e2e-prøver. Fem plantede fejl, hver fanget af sin egen prøve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ec5cf8f9f9
commit
ed30eb4c36
@@ -304,22 +304,28 @@ func extractPriority(input string, spans []Span) (int, []Span) {
|
||||
var (
|
||||
// An explicit marker: "kl 10", "kl. 10:30", "klokken 16", "at 9.15".
|
||||
reTimeMarked = regexp.MustCompile(`(?i)(?:^|\s)(kl\.?|klokken|at)\s*(\d{1,2})(?:[:.](\d{2}))?\s*(am|pm)?(?:\s|$)`)
|
||||
// A 12-hour suffix carries its own marker: "10am", "7:30 pm".
|
||||
reTimeMeridiem = regexp.MustCompile(`(?i)(?:^|\s)(\d{1,2})(?::(\d{2}))?\s*(am|pm)(?:\s|$)`)
|
||||
// A 12-hour suffix carries its own marker: "10am", "7:30 pm" — and may still
|
||||
// be written after one, "at 10am", "kl 7 pm". The marker is part of the time:
|
||||
// left behind, "tomorrow at 10am" became the title "… at".
|
||||
reTimeMeridiem = regexp.MustCompile(`(?i)(?:^|\s)((?:at|kl\.?|klokken)\s*)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)(?:\s|$)`)
|
||||
// A bare colon time: "16:30". Only ':' — never '.', because "15.3" is a date.
|
||||
reTimeColon = regexp.MustCompile(`(?:^|\s)(\d{1,2}):(\d{2})(?:\s|$)`)
|
||||
)
|
||||
|
||||
func extractTime(input string, spans []Span, locale string) (int, int, bool, []Span) {
|
||||
if m := reTimeMeridiem.FindStringSubmatchIndex(input); m != nil {
|
||||
hour := atoi(input, m[2], m[3])
|
||||
hour := atoi(input, m[4], m[5])
|
||||
minute := 0
|
||||
if m[4] >= 0 {
|
||||
minute = atoi(input, m[4], m[5])
|
||||
if m[6] >= 0 {
|
||||
minute = atoi(input, m[6], m[7])
|
||||
}
|
||||
hour = applyMeridiem(hour, strings.ToLower(input[m[6]:m[7]]))
|
||||
hour = applyMeridiem(hour, strings.ToLower(input[m[8]:m[9]]))
|
||||
if valid(hour, minute) {
|
||||
return hour, minute, true, append(spans, Span{Start: m[2], End: m[7], Kind: KindTime})
|
||||
start := m[4]
|
||||
if m[2] >= 0 {
|
||||
start = m[2]
|
||||
}
|
||||
return hour, minute, true, append(spans, Span{Start: start, End: m[9], Kind: KindTime})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,6 +435,37 @@ var (
|
||||
reNumeric = regexp.MustCompile(`(?:^|\s)(\d{1,2})[/.](\d{1,2})(?:[-/](\d{2,4}))?(?:\s|$)`)
|
||||
)
|
||||
|
||||
// fractionUnits are the words that make "3/4" a quantity rather than a day.
|
||||
//
|
||||
// "køb 1/2 liter fløde" is half a litre, not the first of February — and it is
|
||||
// the ordinary shape of a shopping list or a recipe. Without a unit after it the
|
||||
// slash form stays a date: "3/4" alone, "møde 3/4 med Anders", and also
|
||||
// "3/4 kage", because nothing in "kage" tells the third of April from three
|
||||
// quarters, and a date quietly dropped is worse than one somebody removes.
|
||||
var fractionUnits = map[string]bool{
|
||||
"l": true, "liter": true, "dl": true, "cl": true, "ml": true,
|
||||
"kg": true, "g": true, "gram": true, "pund": true,
|
||||
"tsk": true, "spsk": true, "kop": true, "kopper": true, "glas": true,
|
||||
"stk": true, "stk.": true, "del": true, "dele": true, "pakke": true, "pakker": true,
|
||||
"time": true, "timer": true, "km": true, "m": true, "cm": true,
|
||||
"cup": true, "cups": true, "tsp": true, "tbsp": true, "lb": true, "oz": true,
|
||||
}
|
||||
|
||||
// firstNumericDate is the first "d/m" in the input that is not a quantity — see
|
||||
// fractionUnits. A written year always makes it a date.
|
||||
func firstNumericDate(input string) []int {
|
||||
for _, m := range reNumeric.FindAllStringSubmatchIndex(input, -1) {
|
||||
if m[6] < 0 {
|
||||
rest := strings.Fields(input[m[5]:])
|
||||
if len(rest) > 0 && fractionUnits[strings.ToLower(rest[0])] {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return m
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractDate(input string, spans []Span, now time.Time) (time.Time, bool, []Span) {
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
|
||||
@@ -441,7 +478,7 @@ func extractDate(input string, spans []Span, now time.Time) (time.Time, bool, []
|
||||
}
|
||||
}
|
||||
|
||||
if m := reNumeric.FindStringSubmatchIndex(input); m != nil {
|
||||
if m := firstNumericDate(input); m != nil {
|
||||
day, mo := atoi(input, m[2], m[3]), atoi(input, m[4], m[5])
|
||||
year := today.Year()
|
||||
explicitYear := m[6] >= 0
|
||||
|
||||
@@ -784,3 +784,50 @@ func TestImpossibleReminderClockIsNotKept(t *testing.T) {
|
||||
t.Errorf("ReminderOffsetMin = %d, want none", *got.ReminderOffsetMin)
|
||||
}
|
||||
}
|
||||
|
||||
// A marker written before a 12-hour time belongs to the time. Left behind, it
|
||||
// became the end of the title: "file the VAT return tomorrow at 10am" was the
|
||||
// task "file the VAT return at".
|
||||
func TestMarkerBeforeAMeridiemTimeIsConsumed(t *testing.T) {
|
||||
now := ref(t)
|
||||
for _, tc := range []struct{ in, content, due, time string }{
|
||||
{"file the VAT return tomorrow at 10am", "file the VAT return", "2026-03-11", "10:00"},
|
||||
{"call at 7:30 pm", "call", "2026-03-10", "19:30"},
|
||||
{"call the dentist tomorrow at 9am remind me an hour before", "call the dentist", "2026-03-11", "09:00"},
|
||||
{"møde i morgen kl 7 pm", "møde", "2026-03-11", "19:00"},
|
||||
// Control: a meridiem time with no marker still works, and "at" that is
|
||||
// not in front of a time is a word.
|
||||
{"standup tomorrow 9am", "standup", "2026-03-11", "09:00"},
|
||||
{"look at the report tomorrow", "look at the report", "2026-03-11", ""},
|
||||
} {
|
||||
got := Parse(tc.in, now, "en")
|
||||
if got.Content != tc.content || got.DueDate != tc.due || got.DueTime != tc.time {
|
||||
t.Errorf("%q → %q %s %s, want %q %s %s", tc.in, got.Content, got.DueDate, got.DueTime,
|
||||
tc.content, tc.due, tc.time)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// "1/2 liter" is half a litre. A day/month followed by a unit is a quantity and
|
||||
// stays in the title; without a unit it is still a date, because "3/4" alone and
|
||||
// "møde 3/4 med Anders" are the third of April.
|
||||
func TestAFractionWithAUnitIsNotADate(t *testing.T) {
|
||||
now := ref(t)
|
||||
for _, tc := range []struct{ in, content, due string }{
|
||||
{"køb 1/2 liter fløde", "køb 1/2 liter fløde", ""},
|
||||
{"tilsæt 3/4 dl mælk", "tilsæt 3/4 dl mælk", ""},
|
||||
{"bag med 1/2 kg mel i morgen", "bag med 1/2 kg mel", "2026-03-11"},
|
||||
// A quantity first, a real date after it: the date is still found.
|
||||
{"køb 1/2 liter fløde 15/3", "køb 1/2 liter fløde", "2026-03-15"},
|
||||
// Controls: no unit, or a year — a date.
|
||||
{"møde 3/4", "møde", "2026-04-03"},
|
||||
{"møde 3/4 med Anders", "møde med Anders", "2026-04-03"},
|
||||
{"halvdelen 3/4 kage", "halvdelen kage", "2026-04-03"},
|
||||
{"frist 1/2-2027 liter", "frist liter", "2027-02-01"},
|
||||
} {
|
||||
got := Parse(tc.in, now, "da")
|
||||
if got.Content != tc.content || got.DueDate != tc.due {
|
||||
t.Errorf("%q → %q %q, want %q %q", tc.in, got.Content, got.DueDate, tc.content, tc.due)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user