Repository navigation
Expand file tree
/
Copy pathQuickAddSelfTest.java
More file actions
1071 lines (1010 loc) · 60.6 KB
/
Copy pathQuickAddSelfTest.java
File metadata and controls
1071 lines (1010 loc) · 60.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import com.qoder.taskdeck.QuickAdd;
import com.qoder.taskdeck.RecurrenceEngine;
import com.qoder.taskdeck.Task;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;
import java.util.TimeZone;
/**
* JVM-only port of app/Tests/TaskDeckKitTests/QuickAddTests.swift and RecurrenceTests.swift.
* Every case is pinned to a fixed Calendar in UTC (America/New_York for the DST run), so no
* result depends on the machine clock. Expected values were captured from the Swift original.
*
* java QuickAddSelfTest print PASS/FAIL per case, exit non-zero on any failure
* java QuickAddSelfTest --dump print the parse cases in the Swift harness's own line
* format, for diffing against the reference implementation
*/
public final class QuickAddSelfTest {
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
private static final TimeZone NY = TimeZone.getTimeZone("America/New_York");
private static int checks;
private static int failures;
private static boolean dumpMode;
public static void main(String[] args) {
for (String arg : args) {
dumpMode = "--dump".equals(arg);
}
System.out.println("QuickAdd self test | anchors Wed 2026-09-23, Fri 25, Sun 27, Mon Oct 5 2026");
anchorsAreStable();
parseCases();
tagNormalisation();
recurrenceEngine();
chains();
dstRun();
hardEdges();
sizeAndGarbage();
purityAndTasks();
System.out.println();
System.out.println(failures == 0
? "PASS all " + checks + " cases green"
: "FAIL " + failures + " of " + checks + " cases red");
System.exit(failures == 0 ? 0 : 1);
}
// MARK: - Fixed world
/** Gregorian with firstWeekday = Sunday, matching the Swift suite's {@code gregorian()}. */
private static Calendar gregorian(TimeZone zone) {
Calendar c = new GregorianCalendar(zone, Locale.US);
c.setFirstDayOfWeek(Calendar.SUNDAY);
c.setLenient(true);
c.set(Calendar.MILLISECOND, 0);
return c;
}
private static Calendar anchor(String key) {
Calendar c = gregorian("nymar".equals(key) || "nyoct".equals(key) ? NY : UTC);
if ("wed".equals(key)) {
c.set(2026, Calendar.SEPTEMBER, 23, 8, 0, 0);
} else if ("fri".equals(key)) {
c.set(2026, Calendar.SEPTEMBER, 25, 18, 30, 0);
} else if ("sun".equals(key)) {
c.set(2026, Calendar.SEPTEMBER, 27, 23, 0, 0);
} else if ("mon".equals(key)) {
c.set(2026, Calendar.OCTOBER, 5, 6, 0, 0);
} else if ("mar7".equals(key) || "nymar".equals(key)) {
c.set(2026, Calendar.MARCH, 7, 8, 0, 0);
} else if ("nyoct".equals(key)) {
c.set(2026, Calendar.OCTOBER, 31, 8, 0, 0);
} else if ("jan30".equals(key)) {
c.set(2026, Calendar.JANUARY, 30, 8, 0, 0);
} else if ("jan31".equals(key)) {
c.set(2026, Calendar.JANUARY, 31, 8, 0, 0);
} else if ("feb28".equals(key)) {
c.set(2026, Calendar.FEBRUARY, 28, 8, 0, 0);
} else if ("dec31".equals(key)) {
c.set(2026, Calendar.DECEMBER, 31, 8, 0, 0);
} else {
throw new IllegalArgumentException("unknown anchor " + key);
}
c.set(Calendar.MILLISECOND, 0);
return c;
}
/** A pinned wall clock reading, "2026-09-23 21:45", as milliseconds in that calendar. */
private static long stamp(Calendar cal, String text) {
Calendar p = gregorian(cal.getTimeZone());
String[] halves = text.split(" ");
String[] ymd = halves[0].split("-");
String[] hm = halves.length > 1 ? halves[1].split(":") : new String[] {"0", "0"};
p.clear();
p.set(Integer.parseInt(ymd[0]), Integer.parseInt(ymd[1]) - 1, Integer.parseInt(ymd[2]),
Integer.parseInt(hm[0]), Integer.parseInt(hm[1]), 0);
return p.getTimeInMillis();
}
private static String show(Calendar cal, long millis) {
SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.US);
fmt.setTimeZone(cal.getTimeZone());
return fmt.format(new Date(millis));
}
private static String unescape(String text) {
if (text.indexOf('\\') < 0) {
return text;
}
StringBuilder sb = new StringBuilder(text.length());
for (int i = 0; i < text.length(); i++) {
char ch = text.charAt(i);
if (ch != '\\' || i + 1 >= text.length()) {
sb.append(ch);
continue;
}
char next = text.charAt(++i);
if (next == 'n') {
sb.append('\n');
} else if (next == 't') {
sb.append('\t');
} else if (next == 'r') {
sb.append('\r');
} else {
sb.append(next);
}
}
return sb.toString();
}
private static String escape(String text) {
return text.replace("\n", "\\n").replace("\t", "\\t").replace("\r", "\\r");
}
// MARK: - Reporting
private static void report(String label, String want, String got) {
checks++;
if (want.equals(got)) {
if (!dumpMode) {
System.out.println("PASS " + label);
}
return;
}
failures++;
System.out.println("FAIL " + label);
System.out.println(" expected: [" + want + "]");
System.out.println(" actual : [" + got + "]");
}
private static void check(String label, boolean ok) {
report(label, "true", ok ? "true" : "false");
}
private static String[] pipeSplit(String row) {
List<String> out = new ArrayList<String>();
int start = 0;
for (int i = 0; i <= row.length(); i++) {
if (i == row.length() || row.charAt(i) == '|') {
out.add(row.substring(start, i));
start = i + 1;
}
}
return out.toArray(new String[out.size()]);
}
private static String joiner(List<String> values) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < values.size(); i++) {
if (i > 0) {
sb.append(',');
}
sb.append(values.get(i));
}
return sb.toString();
}
private static String days(List<Integer> weekdays) {
if (weekdays == null) {
return "";
}
List<String> parts = new ArrayList<String>();
for (Integer day : weekdays) {
parts.add(String.valueOf(day));
}
return joiner(parts);
}
private static List<Integer> dayList(String csv) {
List<Integer> days = new ArrayList<Integer>();
if (csv != null && !"-".equals(csv)) {
for (String part : csv.split(",")) {
days.add(Integer.valueOf(part));
}
}
return days;
}
// MARK: - 1. The anchors themselves
private static void anchorsAreStable() {
System.out.println("== the pinned anchors fall on the intended weekdays");
check("wednesday is DAY_OF_WEEK 4", dow("wed") == 4);
check("friday is DAY_OF_WEEK 6", dow("fri") == 6);
check("sunday is DAY_OF_WEEK 1", dow("sun") == 1);
check("monday is DAY_OF_WEEK 2", dow("mon") == 2);
}
private static int dow(String key) {
return anchor(key).get(Calendar.DAY_OF_WEEK);
}
// MARK: - 2. Parse cases
/**
* Each row is {@code anchor|raw|title|due|timed|recur|weekdays|priority|tags|status|notes|log},
* copied out of the Swift reference run. "-" means "no due date" in the due column and
* "not recurring" in the recur column.
*/
private static final String[] PARSE_CASES = {
// --- date phrases: a bare date resolves to 09:00 and stays untimed ---
"wed|call mom today|call mom|2026-09-23 09:00|false|-||normal||open||",
"wed|call mom tomorrow|call mom|2026-09-24 09:00|false|-||normal||open||",
"wed|call mom tmrw|call mom|2026-09-24 09:00|false|-||normal||open||",
"wed|call mom tmr|call mom|2026-09-24 09:00|false|-||normal||open||",
"wed|review the deck next week|review the deck|2026-09-30 09:00|false|-||normal||open||",
"wed|review the deck next month|review the deck|2026-10-23 09:00|false|-||normal||open||",
"wed|pay rent end of month|pay rent|2026-09-30 09:00|false|-||normal||open||",
"wed|pay rent end of the month|pay rent|2026-09-30 09:00|false|-||normal||open||",
"wed|pay rent month end|pay rent|2026-09-30 09:00|false|-||normal||open||",
"wed|water plants in 3 days|water plants|2026-09-26 09:00|false|-||normal||open||",
"wed|check filters in 2 weeks|check filters|2026-10-07 09:00|false|-||normal||open||",
"wed|renew lease in 1 month|renew lease|2026-10-23 09:00|false|-||normal||open||",
"wed|ship release 5 days from now|ship release|2026-09-28 09:00|false|-||normal||open||",
"wed|file expenses monday|file expenses|2026-09-28 09:00|false|-||normal||open||",
"wed|file expenses on friday|file expenses|2026-09-25 09:00|false|-||normal||open||",
"wed|file expenses next friday|file expenses|2026-10-02 09:00|false|-||normal||open||",
"wed|buy gift 12/25|buy gift|2026-12-25 09:00|false|-||normal||open||",
"wed|cut release 2026-12-25|cut release|2026-12-25 09:00|false|-||normal||open||",
"wed|renew passport dec 5|renew passport|2026-12-05 09:00|false|-||normal||open||",
"wed|renew passport december 5th 2027|renew passport|2027-12-05 09:00|false|-||normal||open||",
"wed|pay dues the 15th|pay dues|2026-10-15 09:00|false|-||normal||open||",
"wed|book flights jan 15|book flights|2027-01-15 09:00|false|-||normal||open||",
"wed|book flights 1/5|book flights|2027-01-05 09:00|false|-||normal||open||",
"wed|buy gift 12/25/27|buy gift|2027-12-25 09:00|false|-||normal||open||",
"wed|renew passport on dec 5, 2027|renew passport|2027-12-05 09:00|false|-||normal||open||",
// --- bare weekday versus next weekday, per anchor ---
"wed|renew visa friday|renew visa|2026-09-25 09:00|false|-||normal||open||",
"fri|renew visa friday|renew visa|2026-10-02 09:00|false|-||normal||open||",
"sun|renew visa friday|renew visa|2026-10-02 09:00|false|-||normal||open||",
"mon|renew visa friday|renew visa|2026-10-09 09:00|false|-||normal||open||",
"wed|renew visa next friday|renew visa|2026-10-02 09:00|false|-||normal||open||",
"fri|renew visa next friday|renew visa|2026-10-02 09:00|false|-||normal||open||",
"sun|renew visa next friday|renew visa|2026-10-09 09:00|false|-||normal||open||",
"mon|renew visa next friday|renew visa|2026-10-16 09:00|false|-||normal||open||",
// --- every weekday name resolves, from the Wednesday anchor ---
"wed|team sync sunday|team sync|2026-09-27 09:00|false|-||normal||open||",
"wed|team sync monday|team sync|2026-09-28 09:00|false|-||normal||open||",
"wed|team sync tuesday|team sync|2026-09-29 09:00|false|-||normal||open||",
"wed|team sync wednesday|team sync|2026-09-30 09:00|false|-||normal||open||",
"wed|team sync thursday|team sync|2026-09-24 09:00|false|-||normal||open||",
"wed|team sync friday|team sync|2026-09-25 09:00|false|-||normal||open||",
"wed|team sync saturday|team sync|2026-09-26 09:00|false|-||normal||open||",
"wed|team sync on sun|team sync|2026-09-27 09:00|false|-||normal||open||",
"wed|team sync next mon|team sync|2026-10-05 09:00|false|-||normal||open||",
"wed|team sync this wed|team sync|2026-09-30 09:00|false|-||normal||open||",
"wed|team sync coming thu|team sync|2026-09-24 09:00|false|-||normal||open||",
// abbreviations need a prefix, and matching is whole-word: "satin" is not Saturday
"wed|team sync satin|team sync satin|-|false|-||normal||open||",
// --- "the 15th" rolls forward once the day has passed ---
"wed|pay the 15th|pay|2026-10-15 09:00|false|-||normal||open||",
"fri|pay the 15th|pay|2026-10-15 09:00|false|-||normal||open||",
"sun|pay the 15th|pay|2026-10-15 09:00|false|-||normal||open||",
"mon|pay the 15th|pay|2026-10-15 09:00|false|-||normal||open||",
"mon|pay on the 15th|pay|2026-10-15 09:00|false|-||normal||open||",
"mon|pay the 5th|pay|2026-10-05 09:00|false|-||normal||open||",
"mon|pay the 31st|pay|2026-10-31 09:00|false|-||normal||open||",
// --- clock forms: all timed, all on today ---
"wed|call the office at 5pm|call the office|2026-09-23 17:00|true|-||normal||open||",
"wed|call the office at 5 pm|call the office|2026-09-23 17:00|true|-||normal||open||",
"wed|call the office at 5|call the office|2026-09-23 17:00|true|-||normal||open||",
"wed|call the office at 17:00|call the office|2026-09-23 17:00|true|-||normal||open||",
"wed|call the office @6p|call the office|2026-09-23 18:00|true|-||normal||open||",
"wed|call the office by 9am|call the office|2026-09-23 09:00|true|-||normal||open||",
"wed|call the office at 5:30 pm|call the office|2026-09-23 17:30|true|-||normal||open||",
"wed|call the office 5:30 pm|call the office|2026-09-23 17:30|true|-||normal||open||",
"wed|call the office at noon|call the office|2026-09-23 12:00|true|-||normal||open||",
"wed|call the office tonight|call the office|2026-09-23 20:00|true|-||normal||open||",
"wed|call the office in the evening|call the office|2026-09-23 19:00|false|-||normal||open||",
"wed|call the office at 12|call the office|2026-09-23 12:00|true|-||normal||open||",
"wed|call the office at 12:45|call the office|2026-09-23 12:45|true|-||normal||open||",
"wed|call the office at midnight|call the office|2026-09-24 00:00|true|-||normal||open||",
"wed|call the office around 3|call the office|2026-09-23 15:00|true|-||normal||open||",
"wed|call the office till 6|call the office|2026-09-23 18:00|true|-||normal||open||",
// --- date plus clock combines regardless of order ---
"wed|file it tomorrow at 5pm|file it|2026-09-24 17:00|true|-||normal||open||",
"wed|file it at 5pm tomorrow|file it|2026-09-24 17:00|true|-||normal||open||",
"wed|file it at 8am tomorrow|file it|2026-09-24 08:00|true|-||normal||open||",
"wed|file it tomorrow|file it|2026-09-24 09:00|false|-||normal||open||",
"wed|file it at 5pm friday|file it|2026-09-25 17:00|true|-||normal||open||",
// --- vague dayparts stay untimed, numeric clocks are timed ---
"wed|ring the bell in the evening|ring the bell|2026-09-23 19:00|false|-||normal||open||",
"wed|ring the bell tomorrow morning|ring the bell|2026-09-24 09:00|false|-||normal||open||",
"wed|ring the bell tomorrow at 9am|ring the bell|2026-09-24 09:00|true|-||normal||open||",
"wed|ring the bell friday night|ring the bell|2026-09-25 21:00|false|-||normal||open||",
"wed|ring the bell this afternoon|ring the bell|2026-09-23 14:00|false|-||normal||open||",
"wed|ring the bell tonight|ring the bell|2026-09-23 20:00|true|-||normal||open||",
"wed|ring the bell at night|ring the bell|2026-09-23 21:00|false|-||normal||open||",
"wed|ring the bell morning|ring the bell|2026-09-23 09:00|false|-||normal||open||",
"wed|ring the bell noon|ring the bell|2026-09-23 12:00|true|-||normal||open||",
// --- a time that already passed rolls to tomorrow ---
"wed|book a slot at 8am|book a slot|2026-09-24 08:00|true|-||normal||open||",
"fri|book a slot at 5pm|book a slot|2026-09-26 17:00|true|-||normal||open||",
"sun|book a slot at 9am|book a slot|2026-09-28 09:00|true|-||normal||open||",
"sun|book a slot @6p|book a slot|2026-09-28 18:00|true|-||normal||open||",
"wed|book a slot at midnight|book a slot|2026-09-24 00:00|true|-||normal||open||",
"mon|book a slot at noon|book a slot|2026-10-05 12:00|true|-||normal||open||",
"sun|book a slot tonight|book a slot|2026-09-27 20:00|true|-||normal||open||",
// --- an untimed day is never rolled into tomorrow ---
"sun|note the win today|note the win|2026-09-27 09:00|false|-||normal||open||",
// --- minutes and hours resolve to an exact instant ---
"wed|ping ops in 30 minutes|ping ops|2026-09-23 08:30|true|-||normal||open||",
"wed|ping ops in 2 hours|ping ops|2026-09-23 10:00|true|-||normal||open||",
"wed|ping ops in 90 minutes|ping ops|2026-09-23 09:30|true|-||normal||open||",
"wed|ping ops in 3 hrs|ping ops|2026-09-23 11:00|true|-||normal||open||",
"wed|ping ops in 1 hour from now|ping ops|2026-09-23 09:00|true|-||normal||open||",
"wed|ping ops in 45 min|ping ops|2026-09-23 08:45|true|-||normal||open||",
"wed|ping ops 20 minutes from now|ping ops|2026-09-23 08:20|true|-||normal||open||",
// --- recurrence phrases map to units ---
"wed|wipe counters every day|wipe counters|-|false|daily||normal||open||",
"wed|wipe counters each day|wipe counters|-|false|daily||normal||open||",
"wed|wipe counters daily|wipe counters|-|false|daily||normal||open||",
"wed|wipe counters every single day|wipe counters|-|false|daily||normal||open||",
"wed|standup every weekday|standup|-|false|weekdays||normal||open||",
"wed|standup each weekday|standup|-|false|weekdays||normal||open||",
"wed|standup weekdays|standup|-|false|weekdays||normal||open||",
"wed|standup on weekdays|standup|-|false|weekdays||normal||open||",
"wed|standup every working day|standup|-|false|weekdays||normal||open||",
"wed|standup every work day|standup|-|false|weekdays||normal||open||",
"wed|notes every week|notes|-|false|weekly||normal||open||",
"wed|notes each week|notes|-|false|weekly||normal||open||",
"wed|notes weekly|notes|-|false|weekly||normal||open||",
"wed|sync every other week|sync|-|false|biweekly||normal||open||",
"wed|sync every two weeks|sync|-|false|biweekly||normal||open||",
"wed|sync every second week|sync|-|false|biweekly||normal||open||",
"wed|sync biweekly|sync|-|false|biweekly||normal||open||",
"wed|sync fortnightly|sync|-|false|biweekly||normal||open||",
"wed|rent every month|rent|-|false|monthly||normal||open||",
"wed|rent each month|rent|-|false|monthly||normal||open||",
"wed|rent monthly|rent|-|false|monthly||normal||open||",
"wed|gifts every year|gifts|-|false|yearly||normal||open||",
"wed|gifts each year|gifts|-|false|yearly||normal||open||",
"wed|gifts yearly|gifts|-|false|yearly||normal||open||",
"wed|gifts annually|gifts|-|false|yearly||normal||open||",
"wed|gym every monday|gym|-|false|onWeekdays|2|normal||open||",
"wed|gym on mondays|gym|-|false|onWeekdays|2|normal||open||",
"wed|gym every mondays|gym|-|false|onWeekdays|2|normal||open||",
"wed|gym every mon and fri|gym|-|false|onWeekdays|2,6|normal||open||",
"wed|gym on tuesdays and thursdays|gym|-|false|onWeekdays|3,5|normal||open||",
"wed|gym every sat|gym|-|false|onWeekdays|7|normal||open||",
"wed|gym every fri, sun and wed|gym|-|false|onWeekdays|1,4,6|normal||open||",
"wed|gym every mon wed fri|gym|-|false|onWeekdays|2,4,6|normal||open||",
"wed|gym on fri and fri|gym|-|false|onWeekdays|6|normal||open||",
// a lone weekday list with no every/on prefix stays dictated text
"wed|gym mon and fri|gym mon and fri|-|false|-||normal||open||",
"wed|standup every weekday at 9am|standup|2026-09-23 09:00|true|weekdays||normal||open||",
"wed|ship on friday|ship|2026-09-25 09:00|false|-||normal||open||",
// recurrence phrases round-trip through the engine's own spoken forms
"wed|task every day|task|-|false|daily||normal||open||",
"wed|task every weekday|task|-|false|weekdays||normal||open||",
"wed|task every week|task|-|false|weekly||normal||open||",
"wed|task every other week|task|-|false|biweekly||normal||open||",
"wed|task every month|task|-|false|monthly||normal||open||",
"wed|task every year|task|-|false|yearly||normal||open||",
"wed|task every sun, mon, tue, wed, thu, fri, sat|task|-|false|onWeekdays|1,2,3,4,5,6,7|normal||open||",
// --- tags survive hyphens, digits and adjacency ---
"wed|file report #client-acme|file report|-|false|-||normal|client-acme|open||",
"wed|file report #a1 #b_2|file report|-|false|-||normal|a1,b_2|open||",
"wed|file report #Client|file report|-|false|-||normal|client|open||",
"wed|file report #auth,#urgent|file report|-|false|-||normal|auth,urgent|open||",
"wed|#zeta #alpha file report|file report|-|false|-||normal|alpha,zeta|open||",
"wed|file report #todo #todo|file report|-|false|-||normal|todo|open||",
"wed|#a#b#c tidy up|tidy up|-|false|-||normal|a,b,c|open||",
// a hash opens a tag only at the head of a token
"wed|ship#release notes|ship#release notes|-|false|-||normal||open||",
"wed|# -- x|#|-|false|-||normal||open|x|",
// --- priority markers and phrases, first one wins ---
"wed|triage !urgent|triage|-|false|-||urgent||open||",
"wed|triage !high|triage|-|false|-||high||open||",
"wed|triage !normal|triage|-|false|-||normal||open||",
"wed|triage !low|triage|-|false|-||low||open||",
"wed|triage !medium|triage|-|false|-||normal||open||",
"wed|triage !med|triage|-|false|-||normal||open||",
"wed|triage !lowest|triage|-|false|-||low||open||",
"wed|triage !highest|triage|-|false|-||urgent||open||",
"wed|triage low priority|triage|-|false|-||low||open||",
"wed|triage high priority|triage|-|false|-||high||open||",
"wed|triage top priority|triage|-|false|-||urgent||open||",
"wed|triage highest priority|triage|-|false|-||urgent||open||",
"wed|triage normal priority|triage|-|false|-||normal||open||",
"wed|triage medium priority|triage|-|false|-||normal||open||",
"wed|triage urgent|triage|-|false|-||urgent||open||",
"wed|triage !high !low|triage|-|false|-||high||open||",
"wed|triage nothing special|triage nothing special|-|false|-||normal||open||",
// --- combined dictated lines ---
"wed|add a note to call the office tomorrow at 5 pm #client|call the office|2026-09-24 17:00|true|-||normal|client|open||",
"wed|urgent fix the login bug today !high #backend #auth|fix the login bug|2026-09-23 09:00|false|-||urgent|auth,backend|open||",
"wed|top priority file expense report next friday #work -- wait for the receipts|file expense report|2026-10-02 09:00|false|-||urgent|work|open|wait for the receipts|",
"wed|todo water the ferns every week !low #plants #office|water the ferns|-|false|weekly||low|office,plants|open||",
"wed|shred the drafts by 9am on 12/24 #admin|shred the drafts|2026-12-24 09:00|true|-||normal|admin|open||",
"wed|#Client #client #Client||-|false|-||normal|client|open||",
"wed|urgent submit the claim tomorrow at 5:30 pm every month #insurance #claims|submit the claim|2026-09-24 17:30|true|monthly||urgent|claims,insurance|open||",
"wed|remind me to renew the domain next month !low #admin -- registrar login in the vault|renew the domain|2026-10-23 09:00|false|-||low|admin|open|registrar login in the vault|",
"mon|file it next monday|file it|2026-10-12 09:00|false|-||normal||open||",
// --- dictation debris is stripped from the front only ---
"wed|add a note to call the dentist|call the dentist|-|false|-||normal||open||",
"wed|add a task write the summary|write the summary|-|false|-||normal||open||",
"wed|add task write the summary|write the summary|-|false|-||normal||open||",
"wed|add note write the summary|write the summary|-|false|-||normal||open||",
"wed|add a reminder call the bank|call the bank|-|false|-||normal||open||",
"wed|reminder to call the bank|call the bank|-|false|-||normal||open||",
"wed|remind me call the bank|call the bank|-|false|-||normal||open||",
"wed|add todo file the taxes|file the taxes|-|false|-||normal||open||",
"wed|note to self buy stamps|buy stamps|-|false|-||normal||open||",
"wed|remind me to call the bank|call the bank|-|false|-||normal||open||",
"wed|todo: rotate the backups|rotate the backups|-|false|-||normal||open||",
"wed|note: rotate the backups|rotate the backups|-|false|-||normal||open||",
"wed|task: rotate the backups|rotate the backups|-|false|-||normal||open||",
"wed|log: rotate the backups|rotate the backups|-|false|-||normal||open||",
"wed|logged: rotate the backups|rotate the backups|-|false|-||normal||open||",
"wed|um so book the flight|book the flight|-|false|-||normal||open||",
"wed|uh call um the dentist|call the dentist|-|false|-||normal||open||",
"wed|so the notes folder needs a rename|the notes folder needs a rename|-|false|-||normal||open||",
"wed|add a note to the annual report about trailing|the annual report about trailing|-|false|-||normal||open||",
"wed|um um um um um um um buy milk|buy milk|-|false|-||normal||open||",
// --- prose with no keywords passes through untouched ---
"wed|buy milk|buy milk|-|false|-||normal||open||",
"wed|the report covers next quarter|the report covers next quarter|-|false|-||normal||open||",
"wed|watch the sunset on the horizon|watch the sunset on the horizon|-|false|-||normal||open||",
"wed|Consider calling the salon|Consider calling the salon|-|false|-||normal||open||",
"wed|write a note about the notes app|write a note about the notes app|-|false|-||normal||open||",
"wed|about trailing whitespace keeps the word|about trailing whitespace keeps the word|-|false|-||normal||open||",
"wed|milk eggs and bread|milk eggs and bread|-|false|-||normal||open||",
// --- only dictation punctuation is dropped from a title ---
"wed|milk, eggs and bread.|milk eggs and bread|-|false|-||normal||open||",
"wed|what about the API?|what about the API|-|false|-||normal||open||",
"wed|don't forget the .env file|don't forget the .env file|-|false|-||normal||open||",
// --- a trailing spoken period is not part of the task ---
"wed|call the office tomorrow.|call the office|2026-09-24 09:00|false|-||normal||open||",
"wed|renew visa next friday.|renew visa|2026-10-02 09:00|false|-||normal||open||",
"wed|wipe counters daily.|wipe counters|-|false|daily||normal||open||",
// --- someday, notes, logs ---
"wed|someday learn spanish|learn spanish|-|false|-||normal||someday||",
"wed|learn spanish tomorrow someday|learn spanish|-|false|-||normal||someday||",
"wed|someday learn russian #languages|learn russian|-|false|-||normal|languages|someday||",
"wed|some day learn portuguese|learn portuguese|-|false|-||normal||someday||",
"wed|someday write the novel every day #ideas|write the novel|-|false|daily||normal|ideas|someday||",
"wed|call the office -- ask about the invoice tomorrow|call the office|-|false|-||normal||open|ask about the invoice tomorrow|",
"wed|call the office -> ask about the invoice|call the office|-|false|-||normal||open|ask about the invoice|",
"wed|call the office note: ask about the invoice|call the office|-|false|-||normal||open|ask about the invoice|",
"wed|ship v1 done: pushed the tag|ship v1|-|false|-||normal||open||pushed the tag",
"wed|ship v1 log: pushed the tag|ship v1|-|false|-||normal||open||pushed the tag",
"wed|ship v1 logged: pushed the tag|ship v1|-|false|-||normal||open||pushed the tag",
"wed|ship v1 did: pushed the tag|ship v1|-|false|-||normal||open||pushed the tag",
"wed|ship v1 -- 2026-09-23 pushed the tag|ship v1|-|false|-||normal||open||2026-09-23 pushed the tag",
"wed|ship v1 -- 9/23 pushed the tag|ship v1|-|false|-||normal||open||9/23 pushed the tag",
"wed|ship v1 -- pushed the tag|ship v1|-|false|-||normal||open|pushed the tag|",
"wed|fix the sink -- tomorrow call the plumber #urgent at 5pm|fix the sink|-|false|-||normal||open|tomorrow call the plumber #urgent at 5pm|",
"wed|fix the sink -> someday #low every day noon|fix the sink|-|false|-||normal||open|someday #low every day noon|",
"wed|-- no title at all|no title at all|-|false|-||normal||open||",
"wed|call the office --|call the office|-|false|-||normal||open||",
"wed|call the office -- tomorrow|call the office|-|false|-||normal||open|tomorrow|",
// --- whitespace is collapsed before parsing ---
"wed| call the\\n\\toffice tomorrow at 5pm |call the office|2026-09-24 17:00|true|-||normal||open||",
// --- empty, whitespace-only and symbol-only input cannot crash ---
"wed|||-|false|-||normal||open||",
"wed| ||-|false|-||normal||open||",
"wed|\\n\\n||-|false|-||normal||open||",
"wed|\\t \\r ||-|false|-||normal||open||",
"wed|###|###|-|false|-||normal||open||",
"wed|!!! ??? ...||-|false|-||normal||open||",
"wed|->||-|false|-||normal||open||",
"wed|-- --||-|false|-||normal||open||",
"wed|#|#|-|false|-||normal||open||",
"wed|every|every|-|false|-||normal||open||",
"wed|on|on|-|false|-||normal||open||",
"wed|at|at|-|false|-||normal||open||",
"wed|the|the|-|false|-||normal||open||",
"wed|in|in|-|false|-||normal||open||",
"wed|next|next|-|false|-||normal||open||",
// --- impossible dates invent nothing, they never overflow ---
"wed|at 99|at 99|-|false|-||normal||open||",
"wed|2026-13-45|2026-13-45|-|false|-||normal||open||",
"wed|feb 31|feb 31|-|false|-||normal||open||",
"wed|the 99th|the 99th|-|false|-||normal||open||",
"wed|13/45|13/45|-|false|-||normal||open||",
"wed|in 9999999 days|in 9999999 days|-|false|-||normal||open||",
"wed|2026-2-30|2026-2-30|-|false|-||normal||open||",
"wed|jan 32|jan 32|-|false|-||normal||open||",
"wed|25:00|25:00|-|false|-||normal||open||",
"wed|at 5:99|at 5:99|-|false|-||normal||open||",
"wed|0/0|0/0|-|false|-||normal||open||",
"wed|999999999999|999999999999|-|false|-||normal||open||",
"wed|by|by|-|false|-||normal||open||",
"wed|5|5|-|false|-||normal||open||",
// a second clock keyword cannot overwrite the first, so it stays as dictated
"wed|noon noon|noon|2026-09-23 12:00|true|-||normal||open||",
"wed|at 0||2026-09-24 00:00|true|-||normal||open||",
"wed|at 0am||2026-09-24 00:00|true|-||normal||open||",
"wed|in 0 days||2026-09-23 09:00|false|-||normal||open||",
// --- unicode and accents survive the tokeniser ---
"wed|caf\u00e9 \u2615 tomorrow|caf\u00e9 \u2615|2026-09-24 09:00|false|-||normal||open||",
"wed|\u00fcber-morgen|\u00fcber-morgen|-|false|-||normal||open||",
"wed|\u65e5\u672c\u8a9e \u306e\u30bf\u30b9\u30af tomorrow|\u65e5\u672c\u8a9e \u306e\u30bf\u30b9\u30af|2026-09-24 09:00|false|-||normal||open||",
"wed|\u660e\u5929 \u660e\u5929 tomorrow|\u660e\u5929 \u660e\u5929|2026-09-24 09:00|false|-||normal||open||",
// --- competing rules: the first claim on the date wins ---
"wed|in 5 minutes tonight||2026-09-23 08:05|true|-||normal||open||",
"wed|tomorrow at 5pm and next week|and next week|2026-09-24 17:00|true|-||normal||open||",
"wed|today today|today|2026-09-23 09:00|false|-||normal||open||",
"wed|5pm tomorrow||2026-09-24 17:00|true|-||normal||open||",
"wed|tomorrow 5pm||2026-09-24 17:00|true|-||normal||open||",
"fri|today||2026-09-25 09:00|false|-||normal||open||",
"fri|tonight||2026-09-25 20:00|true|-||normal||open||",
// --- month arithmetic clamps instead of spilling (UTC controls for the DST run) ---
"mar7|tomorrow at 5pm||2026-03-08 17:00|true|-||normal||open||",
"mar7|in 3 days||2026-03-10 09:00|false|-||normal||open||",
"mar7|today at 5pm||2026-03-07 17:00|true|-||normal||open||",
"nymar|tomorrow at 5pm||2026-03-08 17:00|true|-||normal||open||",
"nymar|in 3 days||2026-03-10 09:00|false|-||normal||open||",
"nymar|today at 5pm||2026-03-07 17:00|true|-||normal||open||",
"nymar|end of month||2026-03-31 09:00|false|-||normal||open||",
"nymar|next week||2026-03-14 09:00|false|-||normal||open||",
"nyoct|tomorrow at 5pm||2026-11-01 17:00|true|-||normal||open||",
"nyoct|in 3 days||2026-11-03 09:00|false|-||normal||open||",
"nyoct|next month||2026-11-30 09:00|false|-||normal||open||",
"jan30|in 1 month||2026-02-28 09:00|false|-||normal||open||",
"jan31|in 1 month||2026-02-28 09:00|false|-||normal||open||",
"jan31|next month||2026-02-28 09:00|false|-||normal||open||",
"feb28|renew lease in 1 month|renew lease|2026-03-28 09:00|false|-||normal||open||",
"feb28|pay the 31st|pay the 31st|-|false|-||normal||open||",
"dec31|in 1 day||2027-01-01 09:00|false|-||normal||open||",
"dec31|jan 5||2027-01-05 09:00|false|-||normal||open||",
"dec31|pay the 3rd|pay|2027-01-03 09:00|false|-||normal||open||",
};
private static void parseCases() {
System.out.println("== quick-add parse, " + PARSE_CASES.length + " cases");
for (String row : PARSE_CASES) {
String[] f = pipeSplit(row);
Calendar cal = anchor(f[0]);
String raw = unescape(f[1]);
String want = tail(f);
if (dumpMode) {
System.out.println("P|" + escape(raw) + "|" + want);
continue;
}
QuickAdd.Parsed p = QuickAdd.parse(raw, cal.getTimeInMillis(), cal);
report("\"" + escape(raw) + "\" @" + f[0], want, canonical(p, cal));
}
}
private static String tail(String[] fields) {
StringBuilder sb = new StringBuilder();
for (int i = 2; i < fields.length; i++) {
if (i > 2) {
sb.append('|');
}
sb.append(fields[i]);
}
return sb.toString();
}
/** The field list in the same shape the Swift harness prints it. */
private static String canonical(QuickAdd.Parsed p, Calendar cal) {
StringBuilder sb = new StringBuilder();
sb.append(p.title).append('|');
sb.append(p.dueMillis == null ? "-" : show(cal, p.dueMillis.longValue())).append('|');
sb.append(p.timed ? "true" : "false").append('|');
sb.append(p.recur == null ? "-" : p.recur).append('|');
sb.append(days(p.recurWeekdays)).append('|');
sb.append(p.priority).append('|');
sb.append(joiner(p.tags)).append('|');
sb.append(p.status).append('|');
sb.append(p.notes).append('|');
sb.append(p.logText);
return sb.toString();
}
private static String due(Calendar cal, String raw) {
QuickAdd.Parsed p = QuickAdd.parse(raw, cal.getTimeInMillis(), cal);
return (p.dueMillis == null ? "-" : show(cal, p.dueMillis.longValue()))
+ " " + (p.timed ? "true" : "false");
}
// MARK: - 3. normalizeTag
private static final String[] TAG_CASES = {
"#Client|client",
"client|client",
"#some tag|some-tag",
" #Mixed Case Tag |mixed-case-tag",
"#a_b-1|a_b-1",
"#some@#$tag|sometag",
"#|",
"|",
"!!!|",
"#--|",
"#trailing-|trailing",
"##double|double",
"#caf\u00e9|caf\u00e9",
"# a _ b -|a-_-b",
};
private static void tagNormalisation() {
System.out.println("== normalizeTag canonicalises arbitrary text");
for (String row : TAG_CASES) {
int cut = row.lastIndexOf('|');
String raw = row.substring(0, cut);
String want = row.substring(cut + 1);
report("normalizeTag(\"" + raw + "\")", want, QuickAdd.normalizeTag(raw));
}
}
// MARK: - 4. RecurrenceEngine
/** {@code zone y-m-d H:MM|unit|weekdays|expect}. */
private static final String[] NEXT_CASES = {
// fixed steps advance by whole days and keep the clock time
"UTC 2026-09-23 21:45|daily|-|2026-09-24 21:45",
"UTC 2026-09-30 21:45|daily|-|2026-10-01 21:45",
"UTC 2026-12-31 21:45|daily|-|2027-01-01 21:45",
"UTC 2028-02-27 21:45|daily|-|2028-02-28 21:45",
"UTC 2026-09-23 21:45|weekly|-|2026-09-30 21:45",
"UTC 2026-09-23 21:45|biweekly|-|2026-10-07 21:45",
"UTC 2026-09-23 06:15|weekdays|-|2026-09-24 06:15",
"UTC 2026-09-23 06:15|weekly|-|2026-09-30 06:15",
"UTC 2026-09-23 06:15|biweekly|-|2026-10-07 06:15",
"UTC 2026-09-23 06:15|monthly|-|2026-10-23 06:15",
"UTC 2026-09-23 06:15|yearly|-|2027-09-23 06:15",
"UTC 2026-09-23 06:15|onWeekdays|2,6|2026-09-25 06:15",
// weekdays skip the weekend
"UTC 2026-09-23 08:00|weekdays|-|2026-09-24 08:00",
"UTC 2026-09-24 08:00|weekdays|-|2026-09-25 08:00",
"UTC 2026-09-25 08:00|weekdays|-|2026-09-28 08:00",
"UTC 2026-09-26 08:00|weekdays|-|2026-09-28 08:00",
"UTC 2026-09-27 08:00|weekdays|-|2026-09-28 08:00",
"UTC 2026-12-24 08:00|weekdays|-|2026-12-25 08:00",
// monthly clamps to the last valid day, leap and non-leap
"UTC 2026-01-31 20:10|monthly|-|2026-02-28 20:10",
"UTC 2026-02-28 20:10|monthly|-|2026-03-31 20:10",
"UTC 2028-01-31 20:10|monthly|-|2028-02-29 20:10",
"UTC 2028-02-29 20:10|monthly|-|2028-03-31 20:10",
"UTC 2026-01-30 20:10|monthly|-|2026-02-28 20:10",
"UTC 2026-03-31 20:10|monthly|-|2026-04-30 20:10",
"UTC 2026-01-15 20:10|monthly|-|2026-02-15 20:10",
"UTC 2026-11-15 20:10|monthly|-|2026-12-15 20:10",
"UTC 2026-12-15 20:10|monthly|-|2027-01-15 20:10",
// yearly keeps month and day, clamping Feb 29 only when needed
"UTC 2028-02-29 07:30|yearly|-|2029-02-28 07:30",
"UTC 2029-02-28 07:30|yearly|-|2030-02-28 07:30",
"UTC 2026-12-25 07:30|yearly|-|2027-12-25 07:30",
"UTC 2026-01-31 07:30|yearly|-|2027-01-31 07:30",
"UTC 2027-02-28 07:30|yearly|-|2028-02-28 07:30",
// onWeekdays picks the next listed weekday
"UTC 2026-09-23 18:00|onWeekdays|2,6|2026-09-25 18:00",
"UTC 2026-09-25 18:00|onWeekdays|2,6|2026-09-28 18:00",
"UTC 2026-09-26 18:00|onWeekdays|2,6|2026-09-28 18:00",
"UTC 2026-09-25 18:00|onWeekdays|6|2026-10-02 18:00",
"UTC 2026-09-26 18:00|onWeekdays|6|2026-10-02 18:00",
"UTC 2026-09-27 18:00|onWeekdays|1|2026-10-04 18:00",
"UTC 2026-09-25 18:00|onWeekdays|2,3,4|2026-09-28 18:00",
"UTC 2026-09-23 18:00|onWeekdays|7,1|2026-09-26 18:00",
// an empty or absurd onWeekdays set repeats weekly instead of hanging
"UTC 2026-09-23 09:00|onWeekdays|-|2026-09-30 09:00",
"UTC 2026-09-23 09:00|onWeekdays|0,9,42|2026-09-30 09:00",
"UTC 2026-09-23 09:00|onWeekdays|1,2,3,4,5,6,7|2026-09-24 09:00",
// a date at the end of the day still steps forward
"UTC 2026-09-23 23:59|daily|-|2026-09-24 23:59",
"UTC 2026-09-23 23:59|weekdays|-|2026-09-24 23:59",
"UTC 2026-09-23 23:59|monthly|-|2026-10-23 23:59",
};
private static void recurrenceEngine() {
System.out.println("== recurrence engine, " + NEXT_CASES.length + " cases");
for (String row : NEXT_CASES) {
String[] f = pipeSplit(row);
int space = f[0].indexOf(' ');
Calendar cal = gregorian(TimeZone.getTimeZone(f[0].substring(0, space)));
String at = f[0].substring(space + 1);
long got = RecurrenceEngine.nextDate(stamp(cal, at), f[1], dayList(f[2]), cal);
report(f[1] + "(" + f[2] + ") after " + at, f[3], show(cal, got));
}
}
/**
* {@code zone y-m-d H:MM|unit|weekdays|steps|final|last-steps sequence}. The engine must
* stay strictly increasing, land on {@code final}, and reproduce the pinned tail.
*/
private static final String[] CHAIN_CASES = {
// a month-end chain never drifts into the middle of the month
"UTC 2026-01-31 09:00|monthly|-|12|2027-01-31 09:00|2/28 3/31 4/30 5/31 6/30 7/31 8/31 9/30 10/31 11/30 12/31 1/31",
// the same chain across a leap February
"UTC 2028-01-31 09:00|monthly|-|12|2029-01-31 09:00|2/29 3/31 4/30 5/31 6/30 7/31 8/31 9/30 10/31 11/30 12/31 1/31",
// a leapling clamps once and then stays clamped, exactly as Swift does
"UTC 2028-02-29 09:00|yearly|-|4|2032-02-28 09:00|2/28 2/28 2/28 2/28",
"UTC 2026-01-31 09:00|monthly|-|25|2028-02-29 09:00|1/31 2/28 3/31 4/30 5/31 6/30 7/31 8/31 9/30 10/31 11/30 12/31 1/31 2/29",
// every unit walks 400 steps without ever stalling
"UTC 2026-01-01 00:00|daily|-|400|2027-02-05 00:00|1/31 2/1 2/2 2/3 2/4 2/5",
"UTC 2026-01-01 00:00|weekdays|-|400|2027-07-15 00:00|7/5 7/6 7/7 7/8 7/9 7/12 7/13 7/14 7/15",
"UTC 2026-01-01 00:00|weekly|-|400|2033-09-01 00:00|7/21 7/28 8/4 8/11 8/18 8/25 9/1",
"UTC 2026-01-01 00:00|biweekly|-|400|2041-05-02 00:00|3/7 3/21 4/4 4/18 5/2",
"UTC 2026-01-01 00:00|monthly|-|400|2059-05-01 00:00|1/1 2/1 3/1 4/1 5/1",
"UTC 2026-01-01 00:00|yearly|-|400|2426-01-01 00:00|1/1",
"UTC 2026-01-01 00:00|onWeekdays|4|400|2033-08-31 00:00|8/3 8/10 8/17 8/24 8/31",
"UTC 2026-01-01 00:00|onWeekdays|-|400|2033-09-01 00:00|7/21 7/28 8/4 8/11 8/18 8/25 9/1",
};
private static void chains() {
System.out.println("== long recurrence chains stay pinned and strictly later");
for (String row : CHAIN_CASES) {
String[] f = pipeSplit(row);
int space = f[0].indexOf(' ');
Calendar cal = gregorian(TimeZone.getTimeZone(f[0].substring(0, space)));
List<Integer> weekdays = dayList(f[2]);
int steps = Integer.parseInt(f[3]);
List<String> walked = new ArrayList<String>();
boolean monotone = true;
long previous = stamp(cal, f[0].substring(space + 1));
for (int i = 0; i < steps; i++) {
long next = RecurrenceEngine.nextDate(previous, f[1], weekdays, cal);
monotone = monotone && next > previous;
previous = next;
Calendar probe = gregorian(cal.getTimeZone());
probe.setTimeInMillis(next);
walked.add((probe.get(Calendar.MONTH) + 1) + "/" + probe.get(Calendar.DAY_OF_MONTH));
}
String label = f[1] + " x" + steps + " from " + f[0].substring(space + 1);
check(label + " stays strictly later", monotone);
report(label + " lands where Swift lands", f[4], show(cal, previous));
String[] want = f[5].split(" ");
StringBuilder tail = new StringBuilder();
for (int i = walked.size() - want.length; i < walked.size(); i++) {
if (tail.length() > 0) {
tail.append(' ');
}
tail.append(walked.get(i));
}
report(label + " keeps its month/day tail", f[5], tail.toString());
}
}
// MARK: - 5. The DST run
private static void dstRun() {
System.out.println("== a real DST zone keeps the wall clock time");
Calendar ny = gregorian(NY);
// Fri 6 Mar 2026 -> Mon 9 Mar spans the spring-forward night: 23:30 stays 23:30.
report("weekdays across spring forward", "2026-03-09 23:30",
step(ny, "2026-03-06 23:30", "weekdays", null));
report("daily across spring forward", "2026-03-08 23:30",
step(ny, "2026-03-07 23:30", "daily", null));
report("weekly across spring forward", "2026-03-14 23:30",
step(ny, "2026-03-07 23:30", "weekly", null));
report("biweekly across spring forward", "2026-03-21 23:30",
step(ny, "2026-03-07 23:30", "biweekly", null));
report("monthly across spring forward", "2026-04-07 23:30",
step(ny, "2026-03-07 23:30", "monthly", null));
report("monthly into the gap month", "2026-03-06 23:30",
step(ny, "2026-02-06 23:30", "monthly", null));
report("yearly across spring forward", "2027-03-07 23:30",
step(ny, "2026-03-07 23:30", "yearly", null));
// 1 Nov 2026 is the fall-back night
report("daily across fall back", "2026-11-01 23:30",
step(ny, "2026-10-31 23:30", "daily", null));
report("weekly across fall back", "2026-11-07 23:30",
step(ny, "2026-10-31 23:30", "weekly", null));
report("monthly across fall back", "2026-11-30 23:30",
step(ny, "2026-10-31 23:30", "monthly", null));
report("yearly across fall back", "2027-10-31 23:30",
step(ny, "2026-10-31 23:30", "yearly", null));
report("onWeekdays across spring forward", "2026-03-09 23:30",
step(ny, "2026-03-06 23:30", "onWeekdays", "2,6"));
// Parsing in New York must land on the same wall clock as the UTC controls.
Calendar march = gregorian(NY);
march.set(2026, Calendar.MARCH, 7, 8, 0, 0);
march.set(Calendar.MILLISECOND, 0);
report("parse tomorrow at 5pm over the gap", "2026-03-08 17:00 true",
due(march, "pay the utility bill tomorrow at 5pm"));
report("parse in 3 days over the gap", "2026-03-10 09:00 false",
due(march, "pay the utility bill in 3 days"));
report("parse next week over the gap", "2026-03-14 09:00 false",
due(march, "pay the utility bill next week"));
report("parse an ordinal over the gap", "2026-03-25 09:00 false",
due(march, "pay the utility bill the 25th"));
report("parse a weekday over the gap", "2026-03-13 09:00 false",
due(march, "pay the utility bill friday"));
// A reading inside the skipped hour is the one thing the two platforms cannot agree
// on: Foundation answers a one-day step with the reading itself and Java with a side
// of the gap. The day and the timed flag are what both platforms do agree on, and
// that is what is pinned here (see the note in RecurrenceEngine.wallAdd).
Calendar gapDay = gregorian(NY);
gapDay.set(2026, Calendar.MARCH, 8, 1, 0, 0);
gapDay.set(Calendar.MILLISECOND, 0);
QuickAdd.Parsed gap = QuickAdd.parse("fix the leak at 2am", gapDay.getTimeInMillis(), gapDay);
check("a skipped hour still lands on the right day",
gap.dueMillis != null && show(gapDay, gap.dueMillis.longValue()).startsWith("2026-03-08"));
check("a skipped hour is still timed", gap.timed);
QuickAdd.Parsed later = QuickAdd.parse("fix the leak tomorrow at 2am",
gapDay.getTimeInMillis(), gapDay);
check("tomorrow at a skipped hour keeps the day",
later.dueMillis != null && show(gapDay, later.dueMillis.longValue()).startsWith("2026-03-09"));
}
private static String step(Calendar cal, String at, String unit, String weekdays) {
long got = RecurrenceEngine.nextDate(stamp(cal, at), unit, dayList(weekdays), cal);
return show(cal, got);
}
// MARK: - 6. Hard edges
private static void hardEdges() {
System.out.println("== hard edges");
Calendar wed = anchor("wed");
Calendar sun = anchor("sun");
// twelve and the low hours resolve without a meridiem
report("at 12am is midnight, already past", "2026-09-24 00:00 true", due(wed, "hand in at 12am"));
report("at 12pm is noon", "2026-09-23 12:00 true", due(wed, "hand in at 12pm"));
report("at 12 stays at noon", "2026-09-23 12:00 true", due(wed, "hand in at 12"));
report("at 11pm", "2026-09-23 23:00 true", due(wed, "hand in at 11pm"));
report("at 7 is tomorrow morning", "2026-09-24 07:00 true", due(wed, "hand in at 7"));
report("at 10 is this morning", "2026-09-23 10:00 true", due(wed, "hand in at 10"));
// a vague daypart never rolls into tomorrow, even when the hour has passed
report("evening at 23:00 stays today", "2026-09-27 19:00 false",
due(sun, "ring the bell in the evening"));
report("afternoon at 23:00 stays today", "2026-09-27 14:00 false",
due(sun, "ring the bell this afternoon"));
// repeated keywords keep the first value
StringBuilder noisy = new StringBuilder("call");
for (int i = 0; i < 200; i++) {
noisy.append(" tomorrow");
}
QuickAdd.Parsed repeated = QuickAdd.parse(noisy.toString(), wed.getTimeInMillis(), wed);
report("200 tomorrows keep the first", "2026-09-24 09:00 false", due(wed, noisy.toString()));
int kept = 0;
for (String word : repeated.title.split(" ")) {
if ("tomorrow".equals(word)) {
kept++;
}
}
report("later duplicates stay as dictated text", "199", String.valueOf(kept));
}
// MARK: - 7. Garbage, size and the keystroke loop
private static void sizeAndGarbage() {
System.out.println("== whitespace, garbage and size");
Calendar wed = anchor("wed");
long now = wed.getTimeInMillis();
String[] garbage = {null, "", " ", "\n\n", "\t \r ", "###", "!!! ??? ...", "->", "-- --",
"#", "#a#", "!bang", "@", "@@", "..", "-->", "<<>>", "every every every", "at at at",
"2026-13-45", "feb 31", "the 99th", "13/45", "in 9999999 days", "in -3 days",
"at 99 pm", "dec 0", "0/0", "999999999999", "\u00a0\u00a0", "\uD83D\uDE00 tomorrow",
"\uD800\uDC00 tomorrow", "\uD83D\uDE00\uD83D\uDE00\uD83D\uDE00", "\u0000\u0001",
"|||-- --", "at 5pm at 5pm at 5pm", "someday someday", "tomorrow tomorrow someday",
"\\", "\\\\\"", "-- -- -- -- -- -- -- --", "note: note: note:", "\u2192 \u2014 \u2013",
"todo todo todo todo todo todo todo todo", "add a note to", "remind me", "um", "so"};
boolean threw = false;
List<String> unsorted = new ArrayList<String>();
for (String raw : garbage) {
try {
QuickAdd.Parsed p = QuickAdd.parse(raw, now, wed);
if (p == null || p.title == null || p.tags == null || p.notes == null
|| p.logText == null || p.status == null || p.priority == null) {
threw = true;
System.out.println(" null field on [" + raw + "]");
}
List<String> sorted = new ArrayList<String>(p.tags);
Collections.sort(sorted);
if (!sorted.equals(p.tags)) {
unsorted.add(escape(String.valueOf(raw)));
}
} catch (RuntimeException e) {
threw = true;
System.out.println(" threw on [" + raw + "]: " + e);
}
}
report("nothing throws on " + garbage.length + " garbage inputs", "false", threw ? "true" : "false");
report("tags come back sorted every time", "", joiner(unsorted));
for (String raw : new String[] {"", " ", "\n\n", "\t \r "}) {
QuickAdd.Parsed p = QuickAdd.parse(raw, now, wed);
report("blank input yields an empty task [" + escape(raw) + "]",
"|-|false|-||normal||open||", canonical(p, wed));
}
report("a line of only debris yields nothing", "|-|false|-||normal||open||",
canonical(QuickAdd.parse("add a note to", now, wed), wed));
// one enormous token, and 4000 ordinary ones
StringBuilder big = new StringBuilder();
for (int i = 0; i < 20000; i++) {
big.append('a');
}
QuickAdd.Parsed huge = QuickAdd.parse(big.toString(), now, wed);
report("a 20000-character token is just a title", "false|20000|0",
(huge.dueMillis != null) + "|" + huge.title.length() + "|" + huge.tags.size());
StringBuilder zh = new StringBuilder();
for (int i = 0; i < 400; i++) {
zh.append("\u660e\u5929 ");
}
report("400 CJK tokens plus tomorrow", "2026-09-24 09:00 false", due(wed, zh + "tomorrow"));
StringBuilder filler = new StringBuilder();
for (int i = 0; i < 4000; i++) {
if (i > 0) {
filler.append(' ');
}
filler.append("item").append(i);
}
long start = System.nanoTime();
QuickAdd.Parsed bulk = QuickAdd.parse(filler + " tomorrow at 5pm #bulk", now, wed);
long bulkMs = (System.nanoTime() - start) / 1000000L;
check("4000 words keep the whole title",
bulk.title.startsWith("item0 item1 item2 ") && bulk.title.endsWith(" item3998 item3999"));
report("4000 words resolve the tail", "2026-09-24 17:00 true|bulk",
show(wed, bulk.dueMillis.longValue()) + " " + bulk.timed + "|" + joiner(bulk.tags));
// 3000 dangling "at" tokens: every one of them is a hot head that matches nothing.
StringBuilder ats = new StringBuilder();
for (int i = 0; i < 3000; i++) {
ats.append(i == 0 ? "at" : " at");
}
start = System.nanoTime();
QuickAdd.Parsed dangling = QuickAdd.parse(ats.toString(), now, wed);
long atMs = (System.nanoTime() - start) / 1000000L;
report("3000 dangling keywords invent no date", "null|" + ats.length(),
String.valueOf(dangling.dueMillis) + "|" + dangling.title.length());
StringBuilder ons = new StringBuilder();
for (int i = 0; i < 1500; i++) {
ons.append(i == 0 ? "on" : " on");
}
start = System.nanoTime();
QuickAdd.Parsed noisyHot = QuickAdd.parse(ons + " tomorrow", now, wed);
long onMs = (System.nanoTime() - start) / 1000000L;
report("1500 weekday-list heads still resolve tomorrow", "2026-09-24 09:00 false",
due(wed, ons + " tomorrow"));
check("a hostile line parses in one pass (bulk " + bulkMs + "ms, at " + atMs
+ "ms, on " + onMs + "ms)", bulkMs < 5000 && atMs < 5000 && onMs < 5000);
// every keystroke of a dictated line, which is what the capture field does
start = System.nanoTime();
QuickAdd.Parsed last = null;
for (int i = 0; i < PROGRESS.length; i++) {
last = QuickAdd.parse(PROGRESS[i], now, wed);
}
long loopMs = (System.nanoTime() - start) / 1000000L;
check(PROGRESS.length + " keystrokes stay responsive (" + loopMs + "ms)", loopMs < 2000);
report("the final keystroke is the full parse",
"call the office|2026-09-24 17:00|true|-||high|client|open||",
canonical(last, wed));
}
private static final String[] PROGRESS = {
"", "a", "ad", "add", "add ", "add a", "add a ", "add a n", "add a no", "add a not",
"add a note", "add a note ", "add a note t", "add a note to", "add a note to ",
"add a note to c", "add a note to ca", "add a note to cal", "add a note to call",
"add a note to call ", "add a note to call t", "add a note to call th",
"add a note to call the", "add a note to call the ", "add a note to call the o",
"add a note to call the of", "add a note to call the off", "add a note to call the offi",
"add a note to call the offic", "add a note to call the office",
"add a note to call the office ", "add a note to call the office t",
"add a note to call the office to", "add a note to call the office tom",
"add a note to call the office toma", "add a note to call the office tomor",
"add a note to call the office tomorr", "add a note to call the office tomorrow",
"add a note to call the office tomorrow ", "add a note to call the office tomorrow a",
"add a note to call the office tomorrow at", "add a note to call the office tomorrow at ",
"add a note to call the office tomorrow at 5", "add a note to call the office tomorrow at 5 ",
"add a note to call the office tomorrow at 5 p", "add a note to call the office tomorrow at 5 pm",
"add a note to call the office tomorrow at 5 pm ", "add a note to call the office tomorrow at 5 pm #",
"add a note to call the office tomorrow at 5 pm #c", "add a note to call the office tomorrow at 5 pm #cl",
"add a note to call the office tomorrow at 5 pm #cli", "add a note to call the office tomorrow at 5 pm #clie",
"add a note to call the office tomorrow at 5 pm #clien", "add a note to call the office tomorrow at 5 pm #client",
"add a note to call the office tomorrow at 5 pm #client ",
"add a note to call the office tomorrow at 5 pm #client !",
"add a note to call the office tomorrow at 5 pm #client !h",
"add a note to call the office tomorrow at 5 pm #client !hi",
"add a note to call the office tomorrow at 5 pm #client !hig",
"add a note to call the office tomorrow at 5 pm #client !high",
};