Quick-add: detect "everyday" and "every weekday" recurrence

parseRecurrence required a space ("every day"), so "everyday" (one word) was
missed. Now \bevery\s*day\b matches both, plus "each day". Added Every Weekday
detection ("every weekday", "weekdays") ahead of the broader weekly/daily
patterns. Sub-day cadence ("every hour"/"hourly") is intentionally NOT matched —
the occurrence engine is day-granular, so it stays a one-off rather than being
mislabeled. 6 parser tests added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kutesir
2026-06-24 21:02:57 +03:00
parent af2996876c
commit 8370125ec0
3 changed files with 71 additions and 9 deletions

View File

@@ -11,6 +11,7 @@
096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; }; 096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; };
12E42CE8B8E535FAE6268A0C /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF905C574F34B4EE51A8D21E /* AppGroup.swift */; }; 12E42CE8B8E535FAE6268A0C /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF905C574F34B4EE51A8D21E /* AppGroup.swift */; };
13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.swift */; }; 13E4B9854F595394FC9D5912 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC5E179A9189B0A8C3F856F6 /* ContentView.swift */; };
16FFC465D8646DED9A5C69D1 /* NLRecurrenceParseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BBF7B0464EE05D113396B93 /* NLRecurrenceParseTests.swift */; };
1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722CC4B59E3939724142710 /* ExerciseModels.swift */; }; 1A22EE21460821170E44B1DF /* ExerciseModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5722CC4B59E3939724142710 /* ExerciseModels.swift */; };
205846F9651EDCE0D8207358 /* TaskLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */; }; 205846F9651EDCE0D8207358 /* TaskLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.swift */; };
2885D000426D063F2125804C /* SpeechRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86906905A4FA7A2384B3E24E /* SpeechRecognizer.swift */; }; 2885D000426D063F2125804C /* SpeechRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86906905A4FA7A2384B3E24E /* SpeechRecognizer.swift */; };
@@ -123,6 +124,7 @@
9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = "<group>"; }; 9100804DB1E61EA882CC54DA /* FloatingTabState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FloatingTabState.swift; sourceTree = "<group>"; };
92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = "<group>"; }; 92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = "<group>"; };
93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; }; 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
9BBF7B0464EE05D113396B93 /* NLRecurrenceParseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NLRecurrenceParseTests.swift; sourceTree = "<group>"; };
9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9BD1F84B4F45B12CB9B9F1B2 /* KisaniCal.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = KisaniCal.app; sourceTree = BUILT_PRODUCTS_DIR; };
ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; }; ADF6CCD95A587E26E30F5712 /* CalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarView.swift; sourceTree = "<group>"; };
AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; }; AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = "<group>"; };
@@ -148,6 +150,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
74FAA2B41FCEBC7E3F156F0F /* CalendarGridTests.swift */, 74FAA2B41FCEBC7E3F156F0F /* CalendarGridTests.swift */,
9BBF7B0464EE05D113396B93 /* NLRecurrenceParseTests.swift */,
D72D757FA29923B6C150B5EE /* RecurrenceTests.swift */, D72D757FA29923B6C150B5EE /* RecurrenceTests.swift */,
); );
path = KisaniCalTests; path = KisaniCalTests;
@@ -434,6 +437,7 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
6BCBF3902E38FBAA7348840D /* CalendarGridTests.swift in Sources */, 6BCBF3902E38FBAA7348840D /* CalendarGridTests.swift in Sources */,
16FFC465D8646DED9A5C69D1 /* NLRecurrenceParseTests.swift in Sources */,
4FB79BBC1EE0F6C7EF844B02 /* RecurrenceTests.swift in Sources */, 4FB79BBC1EE0F6C7EF844B02 /* RecurrenceTests.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

View File

@@ -1649,16 +1649,23 @@ struct NLTaskParser {
// MARK: - Recurrence // MARK: - Recurrence
private static func parseRecurrence(in text: String) -> (NSRange, String)? { private static func parseRecurrence(in text: String) -> (NSRange, String)? {
// Order matters: more specific phrases ("every weekday") must precede the
// broader ones ("every week" / "every day") so they win.
let pats: [(String, String)] = [ let pats: [(String, String)] = [
("\\bevery\\s+year\\b", "Yearly"), ("\\bevery\\s+year\\b", "Yearly"),
("\\bannually\\b", "Yearly"), ("\\bevery\\s+single\\s+year\\b", "Yearly"),
("\\byearly\\b", "Yearly"), ("\\bannually\\b", "Yearly"),
("\\bevery\\s+month\\b", "Monthly"), ("\\byearly\\b", "Yearly"),
("\\bmonthly\\b", "Monthly"), ("\\bevery\\s+month\\b", "Monthly"),
("\\bevery\\s+week\\b", "Weekly"), ("\\bmonthly\\b", "Monthly"),
("\\bweekly\\b", "Weekly"), ("\\bevery\\s+weekday(s)?\\b", "Every Weekday"),
("\\bevery\\s+day\\b", "Daily"), ("\\bevery\\s+week\\s*day(s)?\\b", "Every Weekday"),
("\\bdaily\\b", "Daily"), ("\\bweekdays\\b", "Every Weekday"),
("\\bevery\\s+week\\b", "Weekly"),
("\\bweekly\\b", "Weekly"),
("\\bevery\\s*day\\b", "Daily"), // matches "every day" AND "everyday"
("\\beach\\s+day\\b", "Daily"),
("\\bdaily\\b", "Daily"),
] ]
for (pat, label) in pats { for (pat, label) in pats {
if let r = match(pat, in: text) { return (r, label) } if let r = match(pat, in: text) { return (r, label) }

View File

@@ -0,0 +1,51 @@
import XCTest
@testable import KisaniCal
/// Natural-language recurrence detection in the quick-add parser.
final class NLRecurrenceParseTests: XCTestCase {
private func label(_ s: String) -> String? {
NLTaskParser.parse(s).recurrenceLabel
}
func testDailyVariants() {
XCTAssertEqual(label("remind me to pray everyday"), "Daily") // the reported case
XCTAssertEqual(label("remind me to pray every day"), "Daily")
XCTAssertEqual(label("stretch daily"), "Daily")
XCTAssertEqual(label("read each day"), "Daily")
}
func testWeekdayVsWeekly() {
XCTAssertEqual(label("standup every weekday"), "Every Weekday")
XCTAssertEqual(label("standup weekdays"), "Every Weekday")
XCTAssertEqual(label("groceries every week"), "Weekly")
XCTAssertEqual(label("groceries weekly"), "Weekly")
}
func testMonthlyYearly() {
XCTAssertEqual(label("pay rent every month"), "Monthly")
XCTAssertEqual(label("pay rent monthly"), "Monthly")
XCTAssertEqual(label("renew every year"), "Yearly")
XCTAssertEqual(label("renew annually"), "Yearly")
XCTAssertEqual(label("file taxes yearly"), "Yearly")
}
func testBirthdayIsYearly() {
let p = NLTaskParser.parse("Jay's birthday")
XCTAssertTrue(p.isBirthday)
XCTAssertTrue(p.isRecurring)
XCTAssertEqual(p.recurrenceLabel, "Yearly")
}
func testNonRecurringStaysNil() {
XCTAssertNil(label("buy milk"))
XCTAssertNil(label("call mom tomorrow"))
}
/// Sub-day cadence isn't supported by the (day-granular) occurrence engine, so it
/// must NOT be mislabeled as Daily/etc. it stays a one-off.
func testHourlyIsNotMisclassified() {
XCTAssertNil(label("drink water every hour"))
XCTAssertNil(label("check oven hourly"))
}
}