crossmate

A collaborative crossword app for iOS
Log | Files | Refs | LICENSE

RecentChanges.swift (3022B)


      1 import Foundation
      2 
      3 /// Reduces the per-cell letter-change ledger (`PeerChange` rows) into the two
      4 /// shapes the receiver's "changed while you were away" surfaces need: the
      5 /// per-cell author map that draws the border highlights, and the per-author net
      6 /// counts that fill the catch-up banner. Both come from one pass over one cutoff
      7 /// so the borders and the banner can never describe different change sets.
      8 ///
      9 /// The ledger already records *when each cell's letter last changed* (see
     10 /// `PeerChange`), so a peer's check — which re-stamps `updatedAt` without
     11 /// touching the letter — never advanced an entry's `changedAt` and so never
     12 /// appears here. We therefore gate purely on `changedAt > since`; no
     13 /// reconstruction of the grid at the cutoff is needed.
     14 ///
     15 /// Each flagged cell is attributed to the author the ledger recorded for its
     16 /// current letter (preserved through checks; a clear credits whoever emptied it).
     17 enum RecentChanges {
     18     /// A peer's changes since a cutoff, in the two shapes the receiver surfaces
     19     /// need: the per-cell author map that draws the border highlights, and the
     20     /// per-author net counts that fill the catch-up banner.
     21     struct Changes: Equatable {
     22         /// Changed positions mapped to the author who wrote the current letter.
     23         var cells: [GridPosition: String]
     24         /// Per-author net counts: `added` for cells whose current letter is
     25         /// non-empty, `cleared` for cells emptied since the cutoff.
     26         var counts: [String: Count]
     27 
     28         static let empty = Changes(cells: [:], counts: [:])
     29     }
     30 
     31     struct Count: Equatable {
     32         var added: Int
     33         var cleared: Int
     34     }
     35 
     36     /// Ledger rows whose letter last changed strictly after `since`, each mapped
     37     /// to the recorded author, plus the per-author add/clear tally over the same
     38     /// set (`excluded` — typically the local user — is dropped, as are rows with
     39     /// no recorded author).
     40     static func changes(
     41         in entries: [PeerChange],
     42         since: Date,
     43         excludingAuthor excluded: String
     44     ) -> Changes {
     45         var cells: [GridPosition: String] = [:]
     46         var counts: [String: Count] = [:]
     47         for entry in entries {
     48             guard entry.changedAt > since else { continue }
     49             guard let writer = entry.authorID, writer != excluded else { continue }
     50             cells[entry.position] = writer
     51             var count = counts[writer] ?? Count(added: 0, cleared: 0)
     52             if entry.letter.isEmpty {
     53                 count.cleared += 1
     54             } else {
     55                 count.added += 1
     56             }
     57             counts[writer] = count
     58         }
     59         return Changes(cells: cells, counts: counts)
     60     }
     61 
     62     /// The cell→author map alone, for the border highlights.
     63     static func changedCells(
     64         in entries: [PeerChange],
     65         since: Date,
     66         excludingAuthor excluded: String
     67     ) -> [GridPosition: String] {
     68         changes(in: entries, since: since, excludingAuthor: excluded).cells
     69     }
     70 }