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:
@@ -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;
|
||||||
|
|||||||
@@ -1649,15 +1649,22 @@ 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"),
|
||||||
|
("\\bevery\\s+single\\s+year\\b", "Yearly"),
|
||||||
("\\bannually\\b", "Yearly"),
|
("\\bannually\\b", "Yearly"),
|
||||||
("\\byearly\\b", "Yearly"),
|
("\\byearly\\b", "Yearly"),
|
||||||
("\\bevery\\s+month\\b", "Monthly"),
|
("\\bevery\\s+month\\b", "Monthly"),
|
||||||
("\\bmonthly\\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"),
|
("\\bevery\\s+week\\b", "Weekly"),
|
||||||
("\\bweekly\\b", "Weekly"),
|
("\\bweekly\\b", "Weekly"),
|
||||||
("\\bevery\\s+day\\b", "Daily"),
|
("\\bevery\\s*day\\b", "Daily"), // matches "every day" AND "everyday"
|
||||||
|
("\\beach\\s+day\\b", "Daily"),
|
||||||
("\\bdaily\\b", "Daily"),
|
("\\bdaily\\b", "Daily"),
|
||||||
]
|
]
|
||||||
for (pat, label) in pats {
|
for (pat, label) in pats {
|
||||||
|
|||||||
51
KisaniCalTests/NLRecurrenceParseTests.swift
Normal file
51
KisaniCalTests/NLRecurrenceParseTests.swift
Normal 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user