feat(todo): quick add reads on before a day, durations, every !30m and !1pm
`on` (German `am`) before a day is taken with it, as `at` before a time; `for 2h`, `for 1h30m`, `for 90 min`, `für 1,5 Std.` set the date's duration where there is a time, and stay text without one; every `!30m` counts, each once, sorted, a task without a date due at the latest; and `!1pm`, `!13:00`, `!tomorrow 9am`, `!Monday 8:30` are reminders at a time of their own, a time without a day on the task's day, else the next to come. Todoist's newsletter's four examples parse whole (ADR 0033, D18). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
3c68a22a5f
commit
d40eca6e50
1 changed files with 422 additions and 24 deletions
|
|
@ -295,6 +295,229 @@ void main() {
|
|||
});
|
||||
});
|
||||
|
||||
group('on before a day, durations, reminders (D18)', () {
|
||||
const de = [QuickAddWords.de, QuickAddWords.en];
|
||||
|
||||
test(
|
||||
'on before a day is taken with it; on before nothing it reads stays',
|
||||
() {
|
||||
final added = parse('Pay rent on Friday');
|
||||
expect(added.title, 'Pay rent');
|
||||
expect(added.date?.day, const Day(2026, 11, 6));
|
||||
expect(parse('Trip on 7 Nov').date?.day, const Day(2026, 11, 7));
|
||||
expect(parse('Trip on 7 Nov').title, 'Trip');
|
||||
expect(parse('Work on report').title, 'Work on report');
|
||||
expect(parse('Work on report').date, isNull);
|
||||
expect(parse('Move on').title, 'Move on');
|
||||
},
|
||||
);
|
||||
|
||||
test('am before a day and um before a time, in German; am after a '
|
||||
'number still the morning', () {
|
||||
final added = parse('Zahnarzt am Montag um 9 Uhr', words: de);
|
||||
expect(added.title, 'Zahnarzt');
|
||||
expect(added.date, const TaskDate(Day(2026, 11, 2), time: 9 * 60));
|
||||
expect(
|
||||
parse('Feier am 24.12.', words: de).date?.day,
|
||||
const Day(2026, 12, 24),
|
||||
);
|
||||
expect(parse('Feier am 24.12.', words: de).title, 'Feier');
|
||||
expect(parse('Steuer jedes Jahr am 1. März', words: de).title, 'Steuer');
|
||||
expect(
|
||||
parse('Anrufen morgen um 9 Uhr', words: de).date,
|
||||
TaskDate(today.plus(1), time: 9 * 60),
|
||||
);
|
||||
expect(parse('Anrufen morgen um 9 Uhr', words: de).title, 'Anrufen');
|
||||
expect(parse('Call 9 am', words: de).date?.time, 9 * 60);
|
||||
expect(
|
||||
parse('Treffen am Bahnhof', words: de).title,
|
||||
'Treffen am Bahnhof',
|
||||
);
|
||||
});
|
||||
|
||||
test('for and a duration, with a time', () {
|
||||
Duration? lasts(String text, [List<QuickAddWords>? words]) =>
|
||||
parse(text, words: words ?? const [QuickAddWords.en]).date?.duration;
|
||||
expect(lasts('Meet at 3pm for 2h'), const Duration(hours: 2));
|
||||
expect(lasts('Meet at 3pm for 30m'), const Duration(minutes: 30));
|
||||
expect(lasts('Meet at 3pm for 1h30m'), const Duration(minutes: 90));
|
||||
expect(lasts('Meet at 3pm for 1h 30m'), const Duration(minutes: 90));
|
||||
expect(lasts('Meet at 3pm for 90 min'), const Duration(minutes: 90));
|
||||
expect(lasts('Meet at 3pm for 2 hours'), const Duration(hours: 2));
|
||||
expect(lasts('Meet at 3pm for 1.5 hours'), const Duration(minutes: 90));
|
||||
expect(
|
||||
lasts('Treffen um 15 Uhr für 2 Std', de),
|
||||
const Duration(hours: 2),
|
||||
);
|
||||
expect(
|
||||
lasts('Treffen um 15 Uhr für 30 Min', de),
|
||||
const Duration(minutes: 30),
|
||||
);
|
||||
expect(
|
||||
lasts('Treffen um 15 Uhr für 1,5 Std', de),
|
||||
const Duration(minutes: 90),
|
||||
);
|
||||
expect(
|
||||
lasts('Treffen um 15 Uhr für 1,5 Std.', de),
|
||||
const Duration(minutes: 90),
|
||||
);
|
||||
expect(parse('Meet for 2h at 3pm').title, 'Meet');
|
||||
expect(
|
||||
parse('Treffen um 15 Uhr für 1,5 Std.', words: de).title,
|
||||
'Treffen',
|
||||
);
|
||||
});
|
||||
|
||||
test('a duration without a time stays text, as one that is none', () {
|
||||
expect(parse('Read for 2h').title, 'Read for 2h');
|
||||
expect(parse('Read for 2h').tokens, isEmpty);
|
||||
final day = parse('Read tomorrow for 2h');
|
||||
expect(day.title, 'Read for 2h');
|
||||
expect(day.date, TaskDate(today.plus(1)));
|
||||
expect(parse('Cook for 4 people at 6pm').date?.duration, isNull);
|
||||
expect(parse('Cook for 4 people at 6pm').title, 'Cook for 4 people');
|
||||
expect(
|
||||
parse('Sleep at 10pm for 30h').date?.duration,
|
||||
isNull,
|
||||
reason: 'longer than a day',
|
||||
);
|
||||
});
|
||||
|
||||
test('every !30m counts, each once, the nearest to the time last', () {
|
||||
const text = 'Meeting 3pm !30m !1h !30m';
|
||||
final added = parse(text);
|
||||
expect(added.title, 'Meeting');
|
||||
expect(added.reminders, [30, 60]);
|
||||
expect(
|
||||
[
|
||||
for (final token in added.tokens)
|
||||
if (token.kind == QuickAddKind.reminder)
|
||||
text.substring(token.start, token.end),
|
||||
],
|
||||
['!30m', '!1h', '!30m'],
|
||||
);
|
||||
});
|
||||
|
||||
test('several !30m without a date: due at the latest, each reminding '
|
||||
'that long from now', () {
|
||||
final added = parse('Leave !30m !1h');
|
||||
expect(added.date, TaskDate(today, time: 11 * 60 + 15));
|
||||
expect(added.reminders, [0, 30]);
|
||||
expect(parse('Pay tomorrow !30m !1h').title, 'Pay !30m !1h');
|
||||
});
|
||||
|
||||
test('! before a time: a reminder at it, on the task’s day, else the '
|
||||
'next to come', () {
|
||||
final at = parse('Call !1pm');
|
||||
expect(at.title, 'Call');
|
||||
expect(at.date, isNull);
|
||||
expect(at.reminderTimes, [ReminderTime(today, 13 * 60)]);
|
||||
expect(parse('Call !13:00').reminderTimes, [
|
||||
ReminderTime(today, 13 * 60),
|
||||
]);
|
||||
expect(
|
||||
parse('Call !9am').reminderTimes,
|
||||
[ReminderTime(today.plus(1), 9 * 60)],
|
||||
reason: 'past already today',
|
||||
);
|
||||
expect(parse('Call Friday !9am').reminderTimes, [
|
||||
const ReminderTime(Day(2026, 11, 6), 9 * 60),
|
||||
]);
|
||||
final both = parse('Dentist Friday 3pm !1pm !30m');
|
||||
expect(both.date, const TaskDate(Day(2026, 11, 6), time: 15 * 60));
|
||||
expect(both.reminders, [30]);
|
||||
expect(both.reminderTimes, [
|
||||
const ReminderTime(Day(2026, 11, 6), 13 * 60),
|
||||
]);
|
||||
expect(both.title, 'Dentist');
|
||||
expect(
|
||||
parse('Standup every weekday 9am !8:30').reminderTimes,
|
||||
[const ReminderTime(Day(2026, 11, 2), 8 * 60 + 30)],
|
||||
reason: "on the recurrence's first day",
|
||||
);
|
||||
});
|
||||
|
||||
test('! before a day and a time: a reminder then', () {
|
||||
const text = 'Pay !tomorrow 9am';
|
||||
final added = parse(text);
|
||||
expect(added.title, 'Pay');
|
||||
expect(added.date, isNull);
|
||||
expect(added.reminderTimes, [ReminderTime(today.plus(1), 9 * 60)]);
|
||||
expect(
|
||||
[
|
||||
for (final token in added.tokens)
|
||||
(token.kind, text.substring(token.start, token.end)),
|
||||
],
|
||||
[(QuickAddKind.reminderTime, '!tomorrow 9am')],
|
||||
);
|
||||
expect(parse('Pay !Monday 8:30').reminderTimes, [
|
||||
const ReminderTime(Day(2026, 11, 2), 8 * 60 + 30),
|
||||
]);
|
||||
expect(parse('Zahlen !morgen um 9 Uhr', words: de).reminderTimes, [
|
||||
ReminderTime(today.plus(1), 9 * 60),
|
||||
]);
|
||||
});
|
||||
|
||||
test('a ! before no time, or tapped off, stays text', () {
|
||||
expect(parse('Pay !tomorrow').title, 'Pay !tomorrow');
|
||||
expect(parse('Pay !tomorrow').date, isNull);
|
||||
expect(parse('Wow !9').title, 'Wow !9');
|
||||
expect(parse('Wow !soon').reminderTimes, isEmpty);
|
||||
final kept = parse('Call !1pm !2pm', ignored: {'!1pm'});
|
||||
expect(kept.title, 'Call !1pm');
|
||||
expect(kept.reminderTimes, [ReminderTime(today, 14 * 60)]);
|
||||
});
|
||||
|
||||
group("Todoist's newsletter's four", () {
|
||||
test('Check work emails every weekday at 9am !30m', () {
|
||||
final added = parse('Check work emails every weekday at 9am !30m');
|
||||
expect(added.title, 'Check work emails');
|
||||
expect(
|
||||
added.date,
|
||||
const TaskDate(
|
||||
Day(2026, 11, 2),
|
||||
time: 9 * 60,
|
||||
repeat: Repeat(RepeatUnit.week, weekdays: {1, 2, 3, 4, 5}),
|
||||
),
|
||||
);
|
||||
expect(added.reminders, [30]);
|
||||
});
|
||||
|
||||
test('Prepare document on Monday at 10am for 2h', () {
|
||||
final added = parse('Prepare document on Monday at 10am for 2h');
|
||||
expect(added.title, 'Prepare document');
|
||||
expect(
|
||||
added.date,
|
||||
const TaskDate(
|
||||
Day(2026, 11, 2),
|
||||
time: 10 * 60,
|
||||
duration: Duration(hours: 2),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('Stop watching that viral cat video !1pm !2pm !3pm !4pm', () {
|
||||
final added = parse(
|
||||
'Stop watching that viral cat video !1pm !2pm !3pm !4pm',
|
||||
);
|
||||
expect(added.title, 'Stop watching that viral cat video');
|
||||
expect(added.date, isNull);
|
||||
expect(added.reminderTimes, [
|
||||
for (final hour in [13, 14, 15, 16]) ReminderTime(today, hour * 60),
|
||||
]);
|
||||
});
|
||||
|
||||
test('File my tax return every year on 1st March', () {
|
||||
final added = parse('File my tax return every year on 1st March');
|
||||
expect(added.title, 'File my tax return');
|
||||
expect(
|
||||
added.date,
|
||||
const TaskDate(Day(2027, 3, 1), repeat: Repeat(RepeatUnit.year)),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('tokens', () {
|
||||
test('each says where it is in the text and what it is', () {
|
||||
const text = 'Call #Shopping tomorrow p1 @errand';
|
||||
|
|
|
|||
Loading…
Reference in a new issue