From 8370125ec0b41fd268892b9e5153e2a9920f01a3 Mon Sep 17 00:00:00 2001 From: kutesir Date: Wed, 24 Jun 2026 21:02:57 +0300 Subject: [PATCH] Quick-add: detect "everyday" and "every weekday" recurrence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- KisaniCal.xcodeproj/project.pbxproj | 4 ++ KisaniCal/Views/TodayView.swift | 25 ++++++---- KisaniCalTests/NLRecurrenceParseTests.swift | 51 +++++++++++++++++++++ 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 KisaniCalTests/NLRecurrenceParseTests.swift diff --git a/KisaniCal.xcodeproj/project.pbxproj b/KisaniCal.xcodeproj/project.pbxproj index 21a3435..f3a0070 100644 --- a/KisaniCal.xcodeproj/project.pbxproj +++ b/KisaniCal.xcodeproj/project.pbxproj @@ -11,6 +11,7 @@ 096804560A2F0A7D74E64780 /* TaskItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC2AFB4FA765383740767CB /* TaskItem.swift */; }; 12E42CE8B8E535FAE6268A0C /* AppGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF905C574F34B4EE51A8D21E /* AppGroup.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 */; }; 205846F9651EDCE0D8207358 /* TaskLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0513EB8C7F6B6853B9E93E09 /* TaskLiveActivity.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 = ""; }; 92824ED40ECD41EFD4F78BEC /* ISSUES.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = ISSUES.md; sourceTree = ""; }; 93D045FE3DEB1D22D908A29F /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; + 9BBF7B0464EE05D113396B93 /* NLRecurrenceParseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NLRecurrenceParseTests.swift; sourceTree = ""; }; 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 = ""; }; AF905C574F34B4EE51A8D21E /* AppGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppGroup.swift; sourceTree = ""; }; @@ -148,6 +150,7 @@ isa = PBXGroup; children = ( 74FAA2B41FCEBC7E3F156F0F /* CalendarGridTests.swift */, + 9BBF7B0464EE05D113396B93 /* NLRecurrenceParseTests.swift */, D72D757FA29923B6C150B5EE /* RecurrenceTests.swift */, ); path = KisaniCalTests; @@ -434,6 +437,7 @@ buildActionMask = 2147483647; files = ( 6BCBF3902E38FBAA7348840D /* CalendarGridTests.swift in Sources */, + 16FFC465D8646DED9A5C69D1 /* NLRecurrenceParseTests.swift in Sources */, 4FB79BBC1EE0F6C7EF844B02 /* RecurrenceTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/KisaniCal/Views/TodayView.swift b/KisaniCal/Views/TodayView.swift index 842245f..73b3e1c 100644 --- a/KisaniCal/Views/TodayView.swift +++ b/KisaniCal/Views/TodayView.swift @@ -1649,16 +1649,23 @@ struct NLTaskParser { // MARK: - Recurrence 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)] = [ - ("\\bevery\\s+year\\b", "Yearly"), - ("\\bannually\\b", "Yearly"), - ("\\byearly\\b", "Yearly"), - ("\\bevery\\s+month\\b", "Monthly"), - ("\\bmonthly\\b", "Monthly"), - ("\\bevery\\s+week\\b", "Weekly"), - ("\\bweekly\\b", "Weekly"), - ("\\bevery\\s+day\\b", "Daily"), - ("\\bdaily\\b", "Daily"), + ("\\bevery\\s+year\\b", "Yearly"), + ("\\bevery\\s+single\\s+year\\b", "Yearly"), + ("\\bannually\\b", "Yearly"), + ("\\byearly\\b", "Yearly"), + ("\\bevery\\s+month\\b", "Monthly"), + ("\\bmonthly\\b", "Monthly"), + ("\\bevery\\s+weekday(s)?\\b", "Every Weekday"), + ("\\bevery\\s+week\\s*day(s)?\\b", "Every Weekday"), + ("\\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 { if let r = match(pat, in: text) { return (r, label) } diff --git a/KisaniCalTests/NLRecurrenceParseTests.swift b/KisaniCalTests/NLRecurrenceParseTests.swift new file mode 100644 index 0000000..abe78af --- /dev/null +++ b/KisaniCalTests/NLRecurrenceParseTests.swift @@ -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")) + } +}