From 167b736aad4d203551861c66291fbadb118da60a Mon Sep 17 00:00:00 2001 From: alexandrev-tibco Date: Sat, 12 Sep 2026 16:19:46 +0200 Subject: [PATCH] Fix streak: el mes en curso pendiente ya no rompe la racha (build 88) #47 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La tarjeta Momentum & Streaks mostraba "Streak 0x" casi todo el mes aunque la racha estuviera intacta: stats() empezaba a contar en el mes en curso y cortaba en el primer mes sin check-in on-time. El check-in del mes en curso está dentro de plazo hasta fin de mes (y con graceDays = 20 se hace normalmente ya entrado el mes siguiente), así que tenerlo pendiente no debe leerse como racha rota. Ahora el recuento arranca en el mes en curso si ya está hecho y, si no, en el anterior. Un mes anterior sin check-in sí rompe la racha, porque su plazo ya venció. Tests: 3 casos nuevos en MonthlyCheckInStoreTests (mes en curso pendiente, mes en curso completado, mes anterior perdido). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01YcDn5ccuRFV83q7xWWokBT --- .../Utilities/MonthlyCheckInStore.swift | 9 ++++ .../Utilities/MonthlyCheckInStoreTests.swift | 50 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/PortfolioJournal/Utilities/MonthlyCheckInStore.swift b/PortfolioJournal/Utilities/MonthlyCheckInStore.swift index 353a6fb..9ca81ef 100644 --- a/PortfolioJournal/Utilities/MonthlyCheckInStore.swift +++ b/PortfolioJournal/Utilities/MonthlyCheckInStore.swift @@ -183,8 +183,17 @@ enum MonthlyCheckInStore { let totalCheckIns = completions.count let onTimeCount = onTimeMonths.count + // The current month's check-in is still within its deadline (end of the + // month), so having it pending must NOT read as a broken streak — that + // made the card show 0x for most of every month. Start counting at the + // current month when it is already done, otherwise at the previous one. + // Any earlier month that is missing has passed its deadline and does + // break the streak, so the walk backwards stays honest. var currentStreak = 0 var cursor = referenceDate.startOfMonth + if !onTimeMonths.contains(cursor) { + cursor = cursor.adding(months: -1).startOfMonth + } while onTimeMonths.contains(cursor) { currentStreak += 1 cursor = cursor.adding(months: -1).startOfMonth diff --git a/PortfolioJournalTests/Utilities/MonthlyCheckInStoreTests.swift b/PortfolioJournalTests/Utilities/MonthlyCheckInStoreTests.swift index a37462b..ec55490 100644 --- a/PortfolioJournalTests/Utilities/MonthlyCheckInStoreTests.swift +++ b/PortfolioJournalTests/Utilities/MonthlyCheckInStoreTests.swift @@ -50,6 +50,56 @@ final class MonthlyCheckInStoreTests: XCTestCase { XCTAssertNotNil(januaryEntry?.completionDate) } + // MARK: - Current streak + + /// Completing June–August and looking at the card on 12 September (the + /// September check-in is still within its deadline) must show a live streak, + /// not 0x. + func testCurrentStreakCountsWhenCurrentMonthIsStillPending() { + let calendar = Calendar(identifier: .gregorian) + for month in 6...8 { + let monthDate = calendar.date(from: DateComponents(year: 2026, month: month, day: 15))! + let completion = calendar.date(from: DateComponents(year: 2026, month: month, day: 28))! + MonthlyCheckInStore.setCompletionDate(completion, for: monthDate) + } + + let referenceDate = calendar.date(from: DateComponents(year: 2026, month: 9, day: 12))! + let stats = MonthlyCheckInStore.stats(referenceDate: referenceDate) + + XCTAssertEqual(stats.currentStreak, 3) + } + + /// The current month being pending is forgiven; an earlier month whose + /// deadline already passed is not. + func testCurrentStreakBreaksWhenPreviousMonthWasMissed() { + let calendar = Calendar(identifier: .gregorian) + for month in 6...8 { + let monthDate = calendar.date(from: DateComponents(year: 2026, month: month, day: 15))! + let completion = calendar.date(from: DateComponents(year: 2026, month: month, day: 28))! + MonthlyCheckInStore.setCompletionDate(completion, for: monthDate) + } + + // September was never checked in and October is under way. + let referenceDate = calendar.date(from: DateComponents(year: 2026, month: 10, day: 12))! + let stats = MonthlyCheckInStore.stats(referenceDate: referenceDate) + + XCTAssertEqual(stats.currentStreak, 0) + } + + func testCurrentStreakCountsCurrentMonthWhenAlreadyCompleted() { + let calendar = Calendar(identifier: .gregorian) + for month in 7...9 { + let monthDate = calendar.date(from: DateComponents(year: 2026, month: month, day: 15))! + let completion = calendar.date(from: DateComponents(year: 2026, month: month, day: 28))! + MonthlyCheckInStore.setCompletionDate(completion, for: monthDate) + } + + let referenceDate = calendar.date(from: DateComponents(year: 2026, month: 9, day: 29))! + let stats = MonthlyCheckInStore.stats(referenceDate: referenceDate) + + XCTAssertEqual(stats.currentStreak, 3) + } + func testSetCompletionDateForVeryOldMonthStillCompletesEntry() { let calendar = Calendar(identifier: .gregorian) let oldMonthDate = calendar.date(from: DateComponents(year: 2024, month: 1, day: 10))!