Skip to content

Add annual location time forecasts - #187

Open
kyleve wants to merge 23 commits into
mainfrom
codex/location-time-forecasts
Open

Add annual location time forecasts#187
kyleve wants to merge 23 commits into
mainfrom
codex/location-time-forecasts

Conversation

@kyleve

@kyleve kyleve commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Posted by an AI agent on kve's behalf.

Summary

  • add current-year location forecasts once three complete months have elapsed
  • show the three leading location estimates and a focused-calendar estimate
  • let the user guide the estimate with one synced inclusive planned-stay date
  • preserve planned-stay revisions through SwiftData, CloudKit conflict resolution, and backup format v3
  • keep the forecast model independent so future percentage goals can compare against it

Behavior and trade-offs

The baseline annualizes days attributed to a region through today. A matching planned stay counts tomorrow through its inclusive departure date at full weight, then resumes the baseline pace for the remaining year. Only one planned stay is active at a time; clearing or expiry writes a tombstone so delayed sync cannot resurrect it.

Testing

  • ./test --no-generate --everything — 1,638 unit tests and 33 snapshot suites passed
  • ./swiftformat --lint
  • ./xcstrings --lint
  • ruby -c Where/Tools/upgrade-backup.rb
  • git diff --check

stay.region == region && stay.through >= today ? stay : nil
}
let plannedEnd = matchingStay.map { min($0.through, lastDay) }
let plannedDays = plannedEnd.map { tomorrow.days(through: $0).count } ?? 0

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Review focus: this is the load-bearing forecast policy. Planned days begin tomorrow, include the selected through-date, and baseline projection resumes the following day. The tests cover the April threshold, matching and non-matching stays, and cross-year stays.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come this has the "I'll be here through" button button but others don't? IMO we should have it on all of them?


public func replacePlannedStayRecord(with record: PlannedStayRecord) async throws {
let context = mutationContext()
for existing in try context.fetch(FetchDescriptor<SDPlannedStay>()) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait, we're fetching them all and deleting them all, for one record?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this feels off

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this is because we can only have one planned stay, which wasn't exactly my intent, but maybe it makes sense for now?

/// User intent that the current stay in `region` continues through an inclusive
/// calendar day. The day is timezone-independent so travel cannot move the
/// asserted departure onto a neighboring date.
public struct PlannedStay: Hashable, Sendable, Codable {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we also record when this was added? Might be useful if only for debugging and any UI that may show planned stays, especially if we allow entering future stays at some point.

final class SDPlannedStay {
var id: UUID?
var regionID: String?
var throughDayKey: String?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this how we usually persist CalendarDays?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think our test data has too many days, if the current date is July 15th, then the stay duration (the regular duration, NOT the stay) should end there too.

@@ -0,0 +1,29 @@
import SwiftUI

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got to here

…orecasts

# Conflicts:
#	Where/Tools/upgrade-backup.rb
#	Where/WhereCore/AGENTS.md
#	Where/WhereCore/README.md
#	Where/WhereCore/Sources/Backup/BackupArchive.swift
#	Where/WhereCore/Sources/Backup/BackupCoordinator.swift
#	Where/WhereCore/Sources/Backup/BackupService.swift
#	Where/WhereCore/Sources/Persistence/SwiftDataStore.swift
#	Where/WhereCore/Sources/Preferences/WherePreferences.swift
#	Where/WhereCore/Tests/BackupCoordinatorTests.swift
#	Where/WhereCore/Tests/BackupServiceTests.swift
#	Where/WhereCore/Tests/WherePreferencesTests.swift
#	Where/WhereUI/AGENTS.md
#	Where/WhereUI/README.md
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.Empty_iPhone.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.Empty_iPhone_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.Focused_iPhone.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.Focused_iPhone_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.FullContent_fullHeight.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPad.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPad_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPad_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPad_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPad_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPhone.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPhone_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPhone_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPhone_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/CalendarContentViewSnapshotTests/calendarContent.WithData_iPhone_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPad_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/LocationsViewSnapshotTests/locations.Loaded_iPhone_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Empty_iPhone_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPad_dark.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_accessibility.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_ax5.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_contrast.png
#	Where/WhereUI/SnapshotTests/__Snapshots__/YearViewSnapshotTests/year.Loaded_iPhone_dark.png
#	Where/WhereUI/Sources/Model/YearReportModel.swift
#	Where/WhereUI/Sources/Preview/PreviewSupport.swift
#	Where/WhereUI/Sources/Primary/CalendarContentView.swift
#	Where/WhereUI/Sources/Resources/Localizable.xcstrings
#	Where/WhereUI/Sources/Settings/LocationSettingsView.swift
#	Where/WhereUI/Sources/Settings/SettingsView.swift
#	Where/WhereUI/Tests/PrivacyPassportCardTests.swift
#	Where/WhereUI/Tests/Support/TestStore.swift
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant