crossmate

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

GridStateMerger.swift (2203B)


      1 import Foundation
      2 
      3 /// Reduces every `(author, device)` `MovesValue` for a single game into one
      4 /// `GridState`. Per-cell last-writer-wins on wall-clock `updatedAt`; ties are
      5 /// broken first by the writing user's `authorID` (lex-min wins), then by
      6 /// `deviceID`, so the output is deterministic regardless of input order. The
      7 /// merged `GridCell.authorID` is the *cell-level* preserved author from the
      8 /// winning entry — not the parent record's author — so reveal-of-correct and
      9 /// same-letter rewrites can hand off authorship without losing it.
     10 enum GridStateMerger {
     11 
     12     static func merge(_ moves: [MovesValue]) -> GridState {
     13         var winners: [GridPosition: Winner] = [:]
     14         for view in moves {
     15             for (position, cell) in view.cells {
     16                 let candidate = Winner(
     17                     cell: cell,
     18                     writerAuthorID: view.authorID,
     19                     deviceID: view.deviceID
     20                 )
     21                 if let current = winners[position] {
     22                     if shouldReplace(current: current, with: candidate) {
     23                         winners[position] = candidate
     24                     }
     25                 } else {
     26                     winners[position] = candidate
     27                 }
     28             }
     29         }
     30 
     31         var grid: GridState = [:]
     32         for (position, winner) in winners {
     33             grid[position] = GridCell(
     34                 letter: winner.cell.letter,
     35                 markKind: winner.cell.markKind,
     36                 checkedWrong: winner.cell.checkedWrong,
     37                 authorID: winner.cell.authorID
     38             )
     39         }
     40         return grid
     41     }
     42 
     43     private struct Winner {
     44         var cell: TimestampedCell
     45         var writerAuthorID: String
     46         var deviceID: String
     47     }
     48 
     49     private static func shouldReplace(current: Winner, with candidate: Winner) -> Bool {
     50         if candidate.cell.updatedAt != current.cell.updatedAt {
     51             return candidate.cell.updatedAt > current.cell.updatedAt
     52         }
     53         if candidate.writerAuthorID != current.writerAuthorID {
     54             return candidate.writerAuthorID < current.writerAuthorID
     55         }
     56         return candidate.deviceID < current.deviceID
     57     }
     58 }