feat: add simple recurrence support
Co-authored-by: Weber Wei <weberwcwei@users.noreply.github.com>
This commit is contained in:
parent
b7f1ac959f
commit
09b25089e1
@ -1,6 +1,7 @@
|
||||
# Changelog
|
||||
|
||||
## 0.2.0 - 2026-05-04
|
||||
- Add simple recurrence support via `--repeat` and `--no-repeat`
|
||||
- Add EventKit alarm support via `--alarm` and `--clear-alarm`
|
||||
- Add reminder `url` to JSON output when EventKit exposes one
|
||||
- Add `creationDate` to reminder JSON output
|
||||
|
||||
10
README.md
10
README.md
@ -51,6 +51,7 @@ remindctl list Projects --create
|
||||
remindctl add "Buy milk"
|
||||
remindctl add --title "Call mom" --list Personal --due tomorrow
|
||||
remindctl add "Meeting" --due "2026-01-03 09:00" --alarm "2026-01-03 08:55"
|
||||
remindctl add "Take vitamins" --due tomorrow --repeat daily
|
||||
remindctl edit 1 --title "New title" --due 2026-01-04 --clear-alarm
|
||||
remindctl list Work Errands # show reminders from multiple lists
|
||||
remindctl complete 1 2 3
|
||||
@ -63,7 +64,7 @@ remindctl authorize # request permissions
|
||||
- `--json` emits JSON arrays/objects.
|
||||
- `--plain` emits tab-separated lines.
|
||||
- `--quiet` emits counts only.
|
||||
- JSON includes EventKit metadata such as `creationDate`, `url`, and `alarmDate` when available.
|
||||
- JSON includes EventKit metadata such as `creationDate`, `url`, `alarmDate`, and `recurrenceRule` when available.
|
||||
File/image attachments are not exposed by EventKit.
|
||||
|
||||
## Date formats
|
||||
@ -78,6 +79,13 @@ Date-only due inputs create all-day reminders; date-time inputs create timed rem
|
||||
Timed due reminders get a notification alarm at the due time unless `--alarm` sets a different alarm time.
|
||||
Use `edit <id> --clear-alarm` to remove an alarm.
|
||||
|
||||
## Repeat
|
||||
Use `--repeat` with `add` or `edit` for simple recurrence:
|
||||
- `daily`, `weekly`, `biweekly`, `monthly`, `yearly`
|
||||
- `every N days/weeks/months/years`
|
||||
|
||||
Use `edit <id> --no-repeat` to clear recurrence.
|
||||
|
||||
## Permissions
|
||||
Run `remindctl authorize` to trigger the system prompt. If access is denied, enable
|
||||
Terminal (or remindctl) in System Settings → Privacy & Security → Reminders.
|
||||
|
||||
@ -113,6 +113,9 @@ public actor RemindersStore {
|
||||
} else if let dueDate = draft.dueDate, !dueDate.isDateOnly {
|
||||
reminder.addAlarm(EKAlarm(absoluteDate: dueDate.date))
|
||||
}
|
||||
if let recurrenceRule = draft.recurrenceRule {
|
||||
replaceRecurrence(on: reminder, with: recurrenceRule)
|
||||
}
|
||||
try eventStore.save(reminder, commit: true)
|
||||
return item(from: reminder)
|
||||
}
|
||||
@ -140,6 +143,9 @@ public actor RemindersStore {
|
||||
if let alarmDateUpdate = update.alarmDate {
|
||||
replaceAlarms(on: reminder, with: alarmDateUpdate?.date)
|
||||
}
|
||||
if let recurrenceUpdate = update.recurrenceRule {
|
||||
replaceRecurrence(on: reminder, with: recurrenceUpdate)
|
||||
}
|
||||
if let priority = update.priority {
|
||||
reminder.priority = priority.eventKitValue
|
||||
}
|
||||
@ -203,6 +209,7 @@ extension RemindersStore {
|
||||
let dueDateComponents: DateComponents?
|
||||
let dueDateIsAllDay: Bool
|
||||
let alarmDate: Date?
|
||||
let recurrenceRule: RecurrenceRule?
|
||||
let listID: String
|
||||
let listName: String
|
||||
}
|
||||
@ -224,6 +231,7 @@ extension RemindersStore {
|
||||
dueDateComponents: components,
|
||||
dueDateIsAllDay: isAllDay(components),
|
||||
alarmDate: Self.alarmDate(from: reminder),
|
||||
recurrenceRule: Self.recurrenceRule(from: reminder),
|
||||
listID: reminder.calendar.calendarIdentifier,
|
||||
listName: reminder.calendar.title
|
||||
)
|
||||
@ -245,6 +253,7 @@ extension RemindersStore {
|
||||
dueDate: date(from: data.dueDateComponents),
|
||||
dueDateIsAllDay: data.dueDateIsAllDay,
|
||||
alarmDate: data.alarmDate,
|
||||
recurrenceRule: data.recurrenceRule,
|
||||
listID: data.listID,
|
||||
listName: data.listName
|
||||
)
|
||||
@ -296,6 +305,7 @@ extension RemindersStore {
|
||||
dueDate: date(from: components),
|
||||
dueDateIsAllDay: isAllDay(components),
|
||||
alarmDate: Self.alarmDate(from: reminder),
|
||||
recurrenceRule: Self.recurrenceRule(from: reminder),
|
||||
listID: reminder.calendar.calendarIdentifier,
|
||||
listName: reminder.calendar.title
|
||||
)
|
||||
@ -315,4 +325,55 @@ extension RemindersStore {
|
||||
.compactMap(\.absoluteDate)
|
||||
.min()
|
||||
}
|
||||
|
||||
private func replaceRecurrence(on reminder: EKReminder, with rule: RecurrenceRule?) {
|
||||
for existing in reminder.recurrenceRules ?? [] {
|
||||
reminder.removeRecurrenceRule(existing)
|
||||
}
|
||||
guard let rule else { return }
|
||||
reminder.addRecurrenceRule(
|
||||
EKRecurrenceRule(recurrenceWith: rule.eventKitFrequency, interval: rule.interval, end: nil))
|
||||
}
|
||||
|
||||
private static func recurrenceRule(from reminder: EKReminder) -> RecurrenceRule? {
|
||||
guard let rule = reminder.recurrenceRules?.first else { return nil }
|
||||
guard let frequency = RecurrenceFrequency(eventKitFrequency: rule.frequency) else { return nil }
|
||||
return RecurrenceRule(frequency: frequency, interval: rule.interval)
|
||||
}
|
||||
}
|
||||
|
||||
extension RecurrenceFrequency {
|
||||
fileprivate init?(eventKitFrequency: EKRecurrenceFrequency) {
|
||||
switch eventKitFrequency {
|
||||
case .daily:
|
||||
self = .daily
|
||||
case .weekly:
|
||||
self = .weekly
|
||||
case .monthly:
|
||||
self = .monthly
|
||||
case .yearly:
|
||||
self = .yearly
|
||||
@unknown default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var eventKitFrequency: EKRecurrenceFrequency {
|
||||
switch self {
|
||||
case .daily:
|
||||
return .daily
|
||||
case .weekly:
|
||||
return .weekly
|
||||
case .monthly:
|
||||
return .monthly
|
||||
case .yearly:
|
||||
return .yearly
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RecurrenceRule {
|
||||
fileprivate var eventKitFrequency: EKRecurrenceFrequency {
|
||||
frequency.eventKitFrequency
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,37 @@ public enum ReminderPriority: String, Codable, CaseIterable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public enum RecurrenceFrequency: String, Codable, CaseIterable, Sendable {
|
||||
case daily
|
||||
case weekly
|
||||
case monthly
|
||||
case yearly
|
||||
}
|
||||
|
||||
public struct RecurrenceRule: Codable, Sendable, Equatable {
|
||||
public let frequency: RecurrenceFrequency
|
||||
public let interval: Int
|
||||
|
||||
public init(frequency: RecurrenceFrequency, interval: Int = 1) {
|
||||
self.frequency = frequency
|
||||
self.interval = interval
|
||||
}
|
||||
|
||||
public var displayString: String {
|
||||
if interval == 1 {
|
||||
return frequency.rawValue
|
||||
}
|
||||
let unit =
|
||||
switch frequency {
|
||||
case .daily: "days"
|
||||
case .weekly: "weeks"
|
||||
case .monthly: "months"
|
||||
case .yearly: "years"
|
||||
}
|
||||
return "every \(interval) \(unit)"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ReminderList: Identifiable, Codable, Sendable, Equatable {
|
||||
public let id: String
|
||||
public let title: String
|
||||
@ -55,6 +86,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
|
||||
public let dueDate: Date?
|
||||
public let dueDateIsAllDay: Bool
|
||||
public let alarmDate: Date?
|
||||
public let recurrenceRule: RecurrenceRule?
|
||||
public let listID: String
|
||||
public let listName: String
|
||||
|
||||
@ -70,6 +102,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
|
||||
dueDate: Date?,
|
||||
dueDateIsAllDay: Bool = false,
|
||||
alarmDate: Date? = nil,
|
||||
recurrenceRule: RecurrenceRule? = nil,
|
||||
listID: String,
|
||||
listName: String
|
||||
) {
|
||||
@ -84,6 +117,7 @@ public struct ReminderItem: Identifiable, Codable, Sendable, Equatable {
|
||||
self.dueDate = dueDate
|
||||
self.dueDateIsAllDay = dueDateIsAllDay
|
||||
self.alarmDate = alarmDate
|
||||
self.recurrenceRule = recurrenceRule
|
||||
self.listID = listID
|
||||
self.listName = listName
|
||||
}
|
||||
@ -94,6 +128,7 @@ public struct ReminderDraft: Sendable {
|
||||
public let notes: String?
|
||||
public let dueDate: ParsedUserDate?
|
||||
public let alarmDate: ParsedUserDate?
|
||||
public let recurrenceRule: RecurrenceRule?
|
||||
public let priority: ReminderPriority
|
||||
|
||||
public init(
|
||||
@ -101,12 +136,14 @@ public struct ReminderDraft: Sendable {
|
||||
notes: String?,
|
||||
dueDate: ParsedUserDate?,
|
||||
alarmDate: ParsedUserDate? = nil,
|
||||
recurrenceRule: RecurrenceRule? = nil,
|
||||
priority: ReminderPriority
|
||||
) {
|
||||
self.title = title
|
||||
self.notes = notes
|
||||
self.dueDate = dueDate
|
||||
self.alarmDate = alarmDate
|
||||
self.recurrenceRule = recurrenceRule
|
||||
self.priority = priority
|
||||
}
|
||||
}
|
||||
@ -116,6 +153,7 @@ public struct ReminderUpdate: Sendable {
|
||||
public let notes: String?
|
||||
public let dueDate: ParsedUserDate??
|
||||
public let alarmDate: ParsedUserDate??
|
||||
public let recurrenceRule: RecurrenceRule??
|
||||
public let priority: ReminderPriority?
|
||||
public let listName: String?
|
||||
public let isCompleted: Bool?
|
||||
@ -125,6 +163,7 @@ public struct ReminderUpdate: Sendable {
|
||||
notes: String? = nil,
|
||||
dueDate: ParsedUserDate?? = nil,
|
||||
alarmDate: ParsedUserDate?? = nil,
|
||||
recurrenceRule: RecurrenceRule?? = nil,
|
||||
priority: ReminderPriority? = nil,
|
||||
listName: String? = nil,
|
||||
isCompleted: Bool? = nil
|
||||
@ -133,6 +172,7 @@ public struct ReminderUpdate: Sendable {
|
||||
self.notes = notes
|
||||
self.dueDate = dueDate
|
||||
self.alarmDate = alarmDate
|
||||
self.recurrenceRule = recurrenceRule
|
||||
self.priority = priority
|
||||
self.listName = listName
|
||||
self.isCompleted = isCompleted
|
||||
|
||||
@ -23,4 +23,53 @@ enum CommandHelpers {
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
static func parseRecurrence(_ value: String) throws -> RecurrenceRule {
|
||||
let normalized = value.lowercased().trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
switch normalized {
|
||||
case "daily":
|
||||
return RecurrenceRule(frequency: .daily)
|
||||
case "weekly":
|
||||
return RecurrenceRule(frequency: .weekly)
|
||||
case "biweekly":
|
||||
return RecurrenceRule(frequency: .weekly, interval: 2)
|
||||
case "monthly":
|
||||
return RecurrenceRule(frequency: .monthly)
|
||||
case "yearly", "annually":
|
||||
return RecurrenceRule(frequency: .yearly)
|
||||
default:
|
||||
return try parseCustomRecurrence(normalized, original: value)
|
||||
}
|
||||
}
|
||||
|
||||
private static func parseCustomRecurrence(_ normalized: String, original: String) throws -> RecurrenceRule {
|
||||
let parts = normalized.split(separator: " ")
|
||||
guard parts.count == 3, parts[0] == "every", let interval = Int(parts[1]), interval > 0 else {
|
||||
throw invalidRecurrence(original)
|
||||
}
|
||||
|
||||
let frequency: RecurrenceFrequency
|
||||
switch parts[2] {
|
||||
case "day", "days":
|
||||
frequency = .daily
|
||||
case "week", "weeks":
|
||||
frequency = .weekly
|
||||
case "month", "months":
|
||||
frequency = .monthly
|
||||
case "year", "years":
|
||||
frequency = .yearly
|
||||
default:
|
||||
throw invalidRecurrence(original)
|
||||
}
|
||||
return RecurrenceRule(frequency: frequency, interval: interval)
|
||||
}
|
||||
|
||||
private static func invalidRecurrence(_ value: String) -> RemindCoreError {
|
||||
RemindCoreError.operationFailed(
|
||||
"""
|
||||
Invalid repeat value: "\(value)" \
|
||||
(use daily|weekly|biweekly|monthly|yearly or "every N days/weeks/months/years")
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,6 +19,12 @@ enum AddCommand {
|
||||
.make(label: "due", names: [.short("d"), .long("due")], help: "Due date", parsing: .singleValue),
|
||||
.make(label: "alarm", names: [.short("a"), .long("alarm")], help: "Alarm date", parsing: .singleValue),
|
||||
.make(label: "notes", names: [.short("n"), .long("notes")], help: "Notes", parsing: .singleValue),
|
||||
.make(
|
||||
label: "repeat",
|
||||
names: [.short("r"), .long("repeat")],
|
||||
help: "daily|weekly|biweekly|monthly|yearly|every N days/weeks/months/years",
|
||||
parsing: .singleValue
|
||||
),
|
||||
.make(
|
||||
label: "priority",
|
||||
names: [.short("p"), .long("priority")],
|
||||
@ -32,6 +38,7 @@ enum AddCommand {
|
||||
"remindctl add \"Buy milk\"",
|
||||
"remindctl add --title \"Call mom\" --list Personal --due tomorrow",
|
||||
"remindctl add \"Call mom\" --due \"2026-01-03 09:00\" --alarm \"2026-01-03 08:55\"",
|
||||
"remindctl add \"Take vitamins\" --due tomorrow --repeat daily",
|
||||
"remindctl add \"Review docs\" --priority high",
|
||||
]
|
||||
) { values, runtime in
|
||||
@ -58,10 +65,12 @@ enum AddCommand {
|
||||
let notes = values.option("notes")
|
||||
let dueValue = values.option("due")
|
||||
let alarmValue = values.option("alarm")
|
||||
let repeatValue = values.option("repeat")
|
||||
let priorityValue = values.option("priority")
|
||||
|
||||
let dueDate = try dueValue.map(CommandHelpers.parseDueDate)
|
||||
let alarmDate = try alarmValue.map(CommandHelpers.parseDueDate)
|
||||
let recurrenceRule = try repeatValue.map(CommandHelpers.parseRecurrence)
|
||||
let priority = try priorityValue.map(CommandHelpers.parsePriority) ?? .none
|
||||
|
||||
let store = RemindersStore()
|
||||
@ -82,6 +91,7 @@ enum AddCommand {
|
||||
notes: notes,
|
||||
dueDate: dueDate,
|
||||
alarmDate: alarmDate,
|
||||
recurrenceRule: recurrenceRule,
|
||||
priority: priority
|
||||
)
|
||||
let reminder = try await store.createReminder(draft, listName: targetList)
|
||||
|
||||
@ -19,6 +19,12 @@ enum EditCommand {
|
||||
.make(label: "due", names: [.short("d"), .long("due")], help: "Set due date", parsing: .singleValue),
|
||||
.make(label: "alarm", names: [.short("a"), .long("alarm")], help: "Set alarm date", parsing: .singleValue),
|
||||
.make(label: "notes", names: [.short("n"), .long("notes")], help: "Set notes", parsing: .singleValue),
|
||||
.make(
|
||||
label: "repeat",
|
||||
names: [.short("r"), .long("repeat")],
|
||||
help: "daily|weekly|biweekly|monthly|yearly|every N days/weeks/months/years",
|
||||
parsing: .singleValue
|
||||
),
|
||||
.make(
|
||||
label: "priority",
|
||||
names: [.short("p"), .long("priority")],
|
||||
@ -29,6 +35,7 @@ enum EditCommand {
|
||||
flags: [
|
||||
.make(label: "clearDue", names: [.long("clear-due")], help: "Clear due date"),
|
||||
.make(label: "clearAlarm", names: [.long("clear-alarm")], help: "Clear alarm"),
|
||||
.make(label: "noRepeat", names: [.long("no-repeat")], help: "Remove recurrence"),
|
||||
.make(label: "complete", names: [.long("complete")], help: "Mark completed"),
|
||||
.make(label: "incomplete", names: [.long("incomplete")], help: "Mark incomplete"),
|
||||
]
|
||||
@ -38,8 +45,9 @@ enum EditCommand {
|
||||
"remindctl edit 1 --title \"New title\"",
|
||||
"remindctl edit 4A83 --due tomorrow",
|
||||
"remindctl edit 4A83 --alarm \"2026-01-03 08:55\"",
|
||||
"remindctl edit 4A83 --repeat weekly",
|
||||
"remindctl edit 2 --priority high --notes \"Call before noon\"",
|
||||
"remindctl edit 3 --clear-due --clear-alarm",
|
||||
"remindctl edit 3 --clear-due --clear-alarm --no-repeat",
|
||||
]
|
||||
) { values, runtime in
|
||||
guard let input = values.argument(0) else {
|
||||
@ -58,6 +66,7 @@ enum EditCommand {
|
||||
let listName = values.option("list")
|
||||
let notes = values.option("notes")
|
||||
let alarmValue = values.option("alarm")
|
||||
let repeatValue = values.option("repeat")
|
||||
|
||||
var dueUpdate: ParsedUserDate??
|
||||
if let dueValue = values.option("due") {
|
||||
@ -81,6 +90,17 @@ enum EditCommand {
|
||||
alarmUpdate = .some(nil)
|
||||
}
|
||||
|
||||
var recurrenceUpdate: RecurrenceRule??
|
||||
if let repeatValue {
|
||||
recurrenceUpdate = try CommandHelpers.parseRecurrence(repeatValue)
|
||||
}
|
||||
if values.flag("noRepeat") {
|
||||
if recurrenceUpdate != nil {
|
||||
throw RemindCoreError.operationFailed("Use either --repeat or --no-repeat, not both")
|
||||
}
|
||||
recurrenceUpdate = .some(nil)
|
||||
}
|
||||
|
||||
var priority: ReminderPriority?
|
||||
if let priorityValue = values.option("priority") {
|
||||
priority = try CommandHelpers.parsePriority(priorityValue)
|
||||
@ -95,7 +115,7 @@ enum EditCommand {
|
||||
|
||||
let hasChanges =
|
||||
title != nil || listName != nil || notes != nil || dueUpdate != nil || alarmUpdate != nil || priority != nil
|
||||
|| isCompleted != nil
|
||||
|| recurrenceUpdate != nil || isCompleted != nil
|
||||
if !hasChanges {
|
||||
throw RemindCoreError.operationFailed("No changes specified")
|
||||
}
|
||||
@ -105,6 +125,7 @@ enum EditCommand {
|
||||
notes: notes,
|
||||
dueDate: dueUpdate,
|
||||
alarmDate: alarmUpdate,
|
||||
recurrenceRule: recurrenceUpdate,
|
||||
priority: priority,
|
||||
listName: listName,
|
||||
isCompleted: isCompleted
|
||||
|
||||
@ -54,7 +54,8 @@ enum OutputRenderer {
|
||||
reminder.dueDate.map {
|
||||
DateParsing.formatDisplay($0, isDateOnly: reminder.dueDateIsAllDay)
|
||||
} ?? "no due date"
|
||||
Swift.print("✓ \(reminder.title) [\(reminder.listName)] — \(due)")
|
||||
let recurrence = recurrenceSuffix(for: reminder)
|
||||
Swift.print("✓ \(reminder.title) [\(reminder.listName)] — \(due)\(recurrence)")
|
||||
case .plain:
|
||||
Swift.print(plainLine(for: reminder))
|
||||
case .json:
|
||||
@ -104,7 +105,9 @@ enum OutputRenderer {
|
||||
DateParsing.formatDisplay($0, isDateOnly: reminder.dueDateIsAllDay)
|
||||
} ?? "no due date"
|
||||
let priority = reminder.priority == .none ? "" : " priority=\(reminder.priority.rawValue)"
|
||||
Swift.print("[\(index + 1)] [\(status)] \(reminder.title) [\(reminder.listName)] — \(due)\(priority)")
|
||||
let recurrence = recurrenceSuffix(for: reminder)
|
||||
Swift.print(
|
||||
"[\(index + 1)] [\(status)] \(reminder.title) [\(reminder.listName)] — \(due)\(priority)\(recurrence)")
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,4 +182,8 @@ enum OutputRenderer {
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter
|
||||
}
|
||||
|
||||
private static func recurrenceSuffix(for reminder: ReminderItem) -> String {
|
||||
reminder.recurrenceRule.map { " repeat=\($0.displayString)" } ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,6 +18,7 @@ struct ReminderItemCodingTests {
|
||||
priority: .none,
|
||||
dueDate: nil,
|
||||
alarmDate: Date(timeIntervalSince1970: 1_700_000_300),
|
||||
recurrenceRule: RecurrenceRule(frequency: .weekly, interval: 2),
|
||||
listID: "list",
|
||||
listName: "Inbox"
|
||||
)
|
||||
@ -29,5 +30,6 @@ struct ReminderItemCodingTests {
|
||||
#expect(json.contains(#""creationDate""#))
|
||||
#expect(json.contains(#""url":"https:\/\/example.com""#))
|
||||
#expect(json.contains(#""alarmDate""#))
|
||||
#expect(json.contains(#""recurrenceRule""#))
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,13 +22,16 @@ struct HelpPrinterTests {
|
||||
#expect(joined.contains("authorize"))
|
||||
}
|
||||
|
||||
@Test("Add and edit help include alarm options")
|
||||
func alarmHelp() {
|
||||
@Test("Add and edit help include alarm and repeat options")
|
||||
func alarmAndRepeatHelp() {
|
||||
let addHelp = HelpPrinter.renderCommand(rootName: "remindctl", spec: AddCommand.spec).joined(separator: "\n")
|
||||
let editHelp = HelpPrinter.renderCommand(rootName: "remindctl", spec: EditCommand.spec).joined(separator: "\n")
|
||||
|
||||
#expect(addHelp.contains("--alarm"))
|
||||
#expect(addHelp.contains("--repeat"))
|
||||
#expect(editHelp.contains("--alarm"))
|
||||
#expect(editHelp.contains("--clear-alarm"))
|
||||
#expect(editHelp.contains("--repeat"))
|
||||
#expect(editHelp.contains("--no-repeat"))
|
||||
}
|
||||
}
|
||||
|
||||
34
Tests/remindctlTests/RecurrenceParsingTests.swift
Normal file
34
Tests/remindctlTests/RecurrenceParsingTests.swift
Normal file
@ -0,0 +1,34 @@
|
||||
import Testing
|
||||
|
||||
@testable import RemindCore
|
||||
@testable import remindctl
|
||||
|
||||
@MainActor
|
||||
struct RecurrenceParsingTests {
|
||||
@Test("Parse repeat presets")
|
||||
func parsePresets() throws {
|
||||
#expect(try CommandHelpers.parseRecurrence("daily") == RecurrenceRule(frequency: .daily))
|
||||
#expect(try CommandHelpers.parseRecurrence("weekly") == RecurrenceRule(frequency: .weekly))
|
||||
#expect(try CommandHelpers.parseRecurrence("biweekly") == RecurrenceRule(frequency: .weekly, interval: 2))
|
||||
#expect(try CommandHelpers.parseRecurrence("monthly") == RecurrenceRule(frequency: .monthly))
|
||||
#expect(try CommandHelpers.parseRecurrence("yearly") == RecurrenceRule(frequency: .yearly))
|
||||
}
|
||||
|
||||
@Test("Parse custom repeat interval")
|
||||
func parseCustomInterval() throws {
|
||||
#expect(try CommandHelpers.parseRecurrence("every 3 days") == RecurrenceRule(frequency: .daily, interval: 3))
|
||||
#expect(try CommandHelpers.parseRecurrence("every 4 weeks") == RecurrenceRule(frequency: .weekly, interval: 4))
|
||||
#expect(try CommandHelpers.parseRecurrence("every 6 months") == RecurrenceRule(frequency: .monthly, interval: 6))
|
||||
#expect(try CommandHelpers.parseRecurrence("every 2 years") == RecurrenceRule(frequency: .yearly, interval: 2))
|
||||
}
|
||||
|
||||
@Test("Reject invalid repeat interval")
|
||||
func rejectInvalidInterval() {
|
||||
#expect(throws: (any Error).self) {
|
||||
try CommandHelpers.parseRecurrence("every 0 weeks")
|
||||
}
|
||||
#expect(throws: (any Error).self) {
|
||||
try CommandHelpers.parseRecurrence("weekdaily")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user