Commit 6a95a830c0
Verified · cmc
Layout: unified · split
Sources/OrgCore/Capture/CaptureSchedule.swift added +33
| @@ -0,0 +1,33 @@ | |||
| 1 | import Foundation | ||
| 2 | |||
| 3 | extension Capture { | ||
| 4 | /// A filled entry scheduled on `day` (as `Days` numbers it), for capturing from an agenda | ||
| 5 | /// day: `SCHEDULED: <2026-10-09 Fri>` goes on the planning line after the heading. Text that | ||
| 6 | /// is not an entry, or already has an active timestamp (`%t`, `%^t`), is left as it is. | ||
| 7 | /// `cursor` is a UTF-16 offset and moves with the text after it. | ||
| 8 | public static func scheduling(_ text: String, cursor: Int, on day: Int) -> (text: String, cursor: Int) { | ||
| 9 | let ns = text as NSString | ||
| 10 | guard text.range(of: "^\\*+ ", options: .regularExpression) != nil, | ||
| 11 | text.range(of: "<[0-9]{4}-[0-9]{2}-[0-9]{2}", options: .regularExpression) == nil else { return (text, cursor) } | ||
| 12 | let d = Days.date(day) | ||
| 13 | let name = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][Days.weekday(day)] | ||
| 14 | let stamp = String(format: "SCHEDULED: <%04d-%02d-%02d %@>", d.year, d.month, d.day, name) | ||
| 15 | let headingEnd = ns.range(of: "\n").location | ||
| 16 | let insertion: (at: Int, text: String) | ||
| 17 | if headingEnd == NSNotFound { | ||
| 18 | insertion = (ns.length, "\n" + stamp) | ||
| 19 | } else { | ||
| 20 | let rest = ns.substring(from: headingEnd + 1) | ||
| 21 | let lineEnd = (rest as NSString).range(of: "\n").location | ||
| 22 | let next = lineEnd == NSNotFound ? rest : (rest as NSString).substring(to: lineEnd) | ||
| 23 | if next.range(of: "^\\s*(CLOSED|DEADLINE|SCHEDULED):", options: .regularExpression) != nil { | ||
| 24 | insertion = (headingEnd + 1 + (next as NSString).length, " " + stamp) | ||
| 25 | } else { | ||
| 26 | insertion = (headingEnd + 1, stamp + "\n") | ||
| 27 | } | ||
| 28 | } | ||
| 29 | let result = ns.replacingCharacters(in: NSRange(location: insertion.at, length: 0), with: insertion.text) | ||
| 30 | let moved = cursor > insertion.at || (cursor == insertion.at && headingEnd != NSNotFound && cursor > headingEnd) | ||
| 31 | return (result, moved ? cursor + (insertion.text as NSString).length : cursor) | ||
| 32 | } | ||
| 33 | } | ||
Tests/OrgCoreTests/CaptureScheduleTests.swift added +36
| @@ -0,0 +1,36 @@ | |||
| 1 | import Testing | ||
| 2 | @testable import OrgCore | ||
| 3 | |||
| 4 | struct CaptureScheduleTests { | ||
| 5 | static let day = Days.absolute(year: 2026, month: 10, day: 9) | ||
| 6 | |||
| 7 | @Test func addsPlanningLineAfterHeading() { | ||
| 8 | let text = "* TODO \n body" | ||
| 9 | let result = Capture.scheduling(text, cursor: 7, on: Self.day) | ||
| 10 | #expect(result.text == "* TODO \nSCHEDULED: <2026-10-09 Fri>\n body") | ||
| 11 | #expect(result.cursor == 7) | ||
| 12 | } | ||
| 13 | |||
| 14 | @Test func headingAlone() { | ||
| 15 | let result = Capture.scheduling("* TODO ", cursor: 7, on: Self.day) | ||
| 16 | #expect(result.text == "* TODO \nSCHEDULED: <2026-10-09 Fri>") | ||
| 17 | #expect(result.cursor == 7) | ||
| 18 | } | ||
| 19 | |||
| 20 | @Test func cursorInBodyMoves() { | ||
| 21 | let text = "* TODO x\n" | ||
| 22 | let result = Capture.scheduling(text, cursor: 9, on: Self.day) | ||
| 23 | #expect(result.text == "* TODO x\nSCHEDULED: <2026-10-09 Fri>\n") | ||
| 24 | #expect(result.cursor == 9 + 28) | ||
| 25 | } | ||
| 26 | |||
| 27 | @Test func joinsExistingPlanningLine() { | ||
| 28 | let result = Capture.scheduling("* DONE x\nCLOSED: [2026-10-01 Thu]\n", cursor: 0, on: Self.day) | ||
| 29 | #expect(result.text == "* DONE x\nCLOSED: [2026-10-01 Thu] SCHEDULED: <2026-10-09 Fri>\n") | ||
| 30 | } | ||
| 31 | |||
| 32 | @Test func leavesDatedAndNonEntries() { | ||
| 33 | #expect(Capture.scheduling("* TODO x\n<2026-10-09 Fri>", cursor: 0, on: Self.day).text == "* TODO x\n<2026-10-09 Fri>") | ||
| 34 | #expect(Capture.scheduling("- [ ] item", cursor: 0, on: Self.day).text == "- [ ] item") | ||
| 35 | } | ||
| 36 | } | ||