1
\$\begingroup\$

Long one here, but election transparency is worth a rough code review.

The Iowa Caucuses have numerous errors in delegate awards because they did their computations manually (their closed-source app failed).

In response I released a Swift "Caucus Report" iOS app for the upcoming Nevada caucuses. It is open sourced at https://github.com/darrellroot/CaucusReport

Most of the algorithm for calculating delegates is documented at https://nvdems.com/wp-content/uploads/2020/02/Caucus-Memo_-Delegate-Count-Scenarios-and-Tie-Breakers.pdf

The one other piece of data required (from the delegate selection plan) is viability percentage:

  • For precincts electing 2 delegates, viability is 25% of voting attendees + early voters (rounded up)
  • For precincts electing 3 delegates, viability is 1/6th of voting attendees + early voters (rounded up)
  • For precincts electing 4 delegates, viability is 15% of voting attendees + early voters (rounded up)

(they have precincts with 1 delegate but don't document a viability percentage for 1-delegate precincts...so my app refuses to deal with that case at this time)

Below is my summary of the procedure and documented algorithm, but ambitious reviewers should go to the source.

Here's the procedure for the caucus:

  1. Early voters already expressed their 1st, 2nd, and 3rd preference
  2. Attendees will be counted and a viability number of votes announced
  3. Attendees will choose their candidate (1st alignment)
  4. Candidates with insufficient votes (total of early and attendee) will be announced as non-viable. Attendees have the option of moving to another candidate, or staying with a non-viable candidate.
  5. Early voters for non-viable candidates have their votes reassigned to their 2nd or 3rd choice, if they are viable.
  6. Early and attendee are counted (2nd alignment)

The algorithm for awarding delegates based on the 2nd alignment vote is as follows: (I really recommend reviewing "caucus math" at https://nvdems.com/wp-content/uploads/2020/02/Caucus-Memo_-Delegate-Count-Scenarios-and-Tie-Breakers.pdf).

  • A "caucus formula" is calculated: NumberOfVotes x precinctDelegates / totalParticipants Numbers >= .5 are rounded up. Numbers < .5 are rounded down. That's the number of delegates for each candidate with the following exceptions:
  • Unallocated Delegates​: I​f any delegates are remaining ​after rounding​, give out the extra delegate(s) starting with the candidate that is closest to rounding up to the next whole number.
  • Too Many Delegates: If you've given out more delegates than your precinct was allocated ​after rounding​, subtract the extra delegate(s) starting with the candidate that is furthest away from rounding up to the next whole number.
  • You cannot take away a viable group’s only delegate (this may result in more delegates being assigned than were planned for the precinct)
  • Note: If there is an exact decimal tie, use a game of chance to break the tie.

That's an awful algorithm, but I've translated it to the following:

  1. Calculate the "caucus formula" for each candidate.
  2. Assign one delegate to each viable candidate, subtracting 1.0 from the "caucus formula" each time you assign a delegate.
  3. If more delegates are to be assigned, give them to the highest remaining "caucus formula" (from viable candidates only), decrementing the formula by 1.0. Repeat until all delegates are assigned.
  4. If there is a caucus formula tie with insufficient delegates to assign, stop and document the tie for random card draw.

Here's an example of a test case using the model (I have successful test cases for every example in the "Caucus Math" document):

func test4c() {
    let model = Model()

    model.precinctDelegates = 8
    model.totalAttendees = 100
    model.attendeeVote2["Bennet"] = 15
    model.attendeeVote2["Biden"] = 45
    model.attendeeVote2["Bloomberg"] = 20
    model.attendeeVote2["Booker"] = 20

    let (delegates, coinToss) = model.calculateDelegates()
    XCTAssert(delegates["Bennet"] == 1)
    XCTAssert(delegates["Biden"] == 3)
    XCTAssert(delegates["Bloomberg"] == 1)
    XCTAssert(delegates["Booker"] == 1)
    XCTAssert(model.viabilityPercentage == 0.15)
    XCTAssert(model.delegateFactor(candidate: "Bennet") == 1.2)
    XCTAssert(model.delegateFactor(candidate: "Biden") == 3.6)
    XCTAssert(model.delegateFactor(candidate: "Bloomberg") == 1.6)
    XCTAssert(model.delegateFactor(candidate: "Booker") == 1.6)

    XCTAssert(coinToss == "Draw cards for 2 delegates between Biden Bloomberg Booker\n")

}

And here's the model. All the "stored properties" are near the top. Everything below the "extension" is a computed property with no stored data. The delegate computation is near the bottom.

//  Model.swift
//  CaucusReport
//
//  Created by Darrell Root on 2/9/20.
//  Copyright © 2020 net.networkmom. All rights reserved.
//

import Foundation

class Model: ObservableObject, Codable {

    static let nevadaCounties = ["Test","Carson City", "Churchill","Clark","Douglas","Elko","Esmeralda","Eureka","Humboldt","Lander","Lincoln","Lyon","Mineral","Nye","Pershing","Storey","Washoe","White Pine"]

    static let originalCandidates = ["Bennet","Biden","Bloomberg","Booker","Buttigieg","Delaney","Gabbard","Kloubuchar","Patrick","Sanders","Steyer","Warren","Williamson","Yang","Uncommitted","Write-Ins"]

    @Published var realMode = false
    @Published var county: String = "Test"
    @Published var precinct: String = ""
    @Published var precinctDelegates: Int = 2 {
        didSet {
            if precinctDelegates < 2 {
                precinctDelegates = 2
            }
        }
    }
    @Published var totalAttendees = 0 {
        didSet {
            if totalAttendees < 0 {
                totalAttendees = 0
            }
        }
    }
    @Published var totalEarlyVoters = 0 {
        didSet {
            if totalEarlyVoters < 0 {
                totalEarlyVoters = 0
            }
        }
    }

    @Published var candidates = Model.originalCandidates

    @Published var earlyVote1: [String: Int] = [:] {
        didSet {
            for key in earlyVote1.keys {
                if earlyVote1[key]! < 0 {
                    earlyVote1[key] = 0
                }
            }
        }
    }
    @Published var attendeeVote1: [String: Int] = [:] {
        didSet {
            for key in attendeeVote1.keys {
                if attendeeVote1[key]! < 0 {
                    attendeeVote1[key] = 0
                }
            }
        }
    }
    @Published var earlyVote2: [String: Int] = [:] {
        didSet {
            for key in earlyVote2.keys {
                if earlyVote2[key]! < 0 {
                    earlyVote2[key] = 0
                }
            }
        }
    }
    @Published var attendeeVote2: [String: Int] = [:] {
        didSet {
            for key in attendeeVote2.keys {
                if attendeeVote2[key]! < 0 {
                    attendeeVote2[key] = 0
                }
            }
        }
    }


    init() {
        for candidate in candidates {
            earlyVote1[candidate] = 0
            attendeeVote1[candidate] = 0
            earlyVote2[candidate] = 0
            attendeeVote2[candidate] = 0
        }
    }

    enum CodingKeys: CodingKey {
        case realMode
        case county
        case precinct
        case precinctDelegates
        case candidates
        case totalAttendees
        case totalEarlyVoters
        case earlyVote1
        case earlyVote2
        case attendeeVote1
        case attendeeVote2
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        realMode = try container.decode(Bool.self, forKey: .realMode)
        county = try container.decode(String.self, forKey: .county)
        precinct = try container.decode(String.self, forKey: .precinct)
        precinctDelegates = try container.decode(Int.self, forKey: .precinctDelegates)
        candidates = try container.decode([String].self, forKey: .candidates)
        totalAttendees = try container.decode(Int.self, forKey: .totalAttendees)
        totalEarlyVoters = try container.decode(Int.self, forKey: .totalEarlyVoters)
        earlyVote1 = try container.decode([String: Int].self, forKey: .earlyVote1)
        earlyVote2 = try container.decode([String: Int].self, forKey: .earlyVote2)
        attendeeVote1 = try container.decode([String: Int].self, forKey: .attendeeVote1)
        attendeeVote2 = try container.decode([String: Int].self, forKey: .attendeeVote2)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(realMode, forKey: .realMode)
        try container.encode(county, forKey: .county)
        try container.encode(precinct, forKey: .precinct)
        try container.encode(precinctDelegates, forKey: .precinctDelegates)
        try container.encode(totalAttendees, forKey: .totalAttendees)
        try container.encode(totalEarlyVoters, forKey: .totalEarlyVoters)
        try container.encode(candidates, forKey: .candidates)
        try container.encode(earlyVote1, forKey: .earlyVote1)
        try container.encode(earlyVote2, forKey: .earlyVote2)
        try container.encode(attendeeVote1, forKey: .attendeeVote1)
        try container.encode(attendeeVote2, forKey: .attendeeVote2)
    }
}

extension Model { // everything below here are computed properties, no permanent storage

    func saveData() {
        let encoder = JSONEncoder()
        if let encoded = try? encoder.encode(self) {
            let defaults = UserDefaults.standard
            defaults.set(encoded, forKey: "SavedModel")
            debugPrint("saved model")
        } else {
            debugPrint("failed to save model")
        }
    }

    var totalRegistrations: Int {
        return totalAttendees + totalEarlyVoters
    }

    var viability: Int {
        let doubleViability = (Double(totalAttendees + totalEarlyVoters) * viabilityPercentage).rounded(.up)
        return Int(doubleViability)
    }

    func align1Total(candidate: String) -> Int {
        return (earlyVote1[candidate] ?? 0) + (attendeeVote1[candidate] ?? 0)
    }

    func align2Total(candidate: String) -> Int {
        return (earlyVote2[candidate] ?? 0) + (attendeeVote2[candidate] ?? 0)
    }

    var early1GrandTotal: Int {
        var grandTotal = 0
        for candidate in self.candidates {
            if candidate != "Uncommitted" {
                grandTotal = grandTotal + (earlyVote1[candidate] ?? 0)
            }
        }
        return grandTotal
    }

    var early2GrandTotal: Int {
        var grandTotal = 0
        for candidate in self.candidates {
            if candidate != "Uncommitted" {
                grandTotal = grandTotal + (earlyVote2[candidate] ?? 0)
            }
        }
        return grandTotal
    }


    var viableCandidates: [String] {
        var result: [String] = []
        for candidate in candidates {
            if viable2(candidate: candidate) {
                result.append(candidate)
            }
        }
        return result
    }

    var attendee1GrandTotal: Int {
        var grandTotal = 0
        for candidate in self.candidates {
            grandTotal = grandTotal + (attendeeVote1[candidate] ?? 0)
        }
        return grandTotal
    }

    var attendee2GrandTotal: Int {
        var grandTotal = 0
        for candidate in self.candidates {
            grandTotal = grandTotal + (attendeeVote2[candidate] ?? 0)
        }
        return grandTotal
    }


    var align1GrandTotal: Int {
        return early1GrandTotal + attendee1GrandTotal
    }

    var align2GrandTotal: Int {
        return early2GrandTotal + attendee2GrandTotal
    }

    var viabilityPercentage: Double {
        switch self.precinctDelegates {
        case 2:
            return 0.25
        case 3:
            return 0.166667
        case 4...:
            return 0.15
        default:
            debugPrint("should not get here")
            return 0.25
        }
    }

    func viable1(candidate: String) -> Bool {
        if candidate == "Uncommitted" {
            return false
        }
        if align1Total(candidate: candidate) >= viability {
            return true
        } else {
            return false
        }
    }
    func viable2(candidate: String) -> Bool {
        if candidate == "Uncommitted" {
            return false
        }
        if align2Total(candidate: candidate) >= viability {
            return true
        } else {
            return false
        }
    }


    func reset() {
        realMode = false
        county = ""
        precinct = ""
        precinctDelegates = 2
        totalAttendees = 0
        totalEarlyVoters = 0
        candidates = Model.originalCandidates
        earlyVote1 = [:]
        attendeeVote1 = [:]
        earlyVote2 = [:]
        attendeeVote2 = [:]
        for candidate in candidates {
            earlyVote1[candidate] = 0
            attendeeVote1[candidate] = 0
            earlyVote2[candidate] = 0
            attendeeVote2[candidate] = 0
        }

    }

    //adding write-in candidates not supported
    /*func addCandidate(name: String) {
        if candidates.contains(name) {
            debugPrint("already added")
        } else {
            candidates.append(name)
            earlyVote1[name] = 0
            attendeeVote1[name] = 0
            earlyVote2[name] = 0
            attendeeVote2[name] = 0
        }
    }*/

    var align1Tweet: String? {
        var output: String = ""
        if self.realMode {
            output += "#NevadaCaucus Align1\n"
        } else {
            output += "#NevadaCaucusTest Align1\n"
        }
        output += "County \(county) precinct \(precinct) Delegates \(precinctDelegates)\n"
        for candidate in candidates {
            if align1Total(candidate: candidate) == 0 {
                continue
            }
            output += "\(candidate) \(align1Total(candidate: candidate))"
            if viable1(candidate: candidate) {
                output += " viable\n"
            } else {
                output += "\n"
            }
        }
        output += "#CaucusReportApp\n"
        debugPrint("tweet characters \(output.count)")
        return output.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
    }

    var align2Tweet: String? {
        var output: String = ""
        if self.realMode {
            output += "#NevadaCaucus Align2\n"
        } else {
            output += "#NevadaCaucusTest Align2\n"
        }
        output += "County \(county) Precinct \(precinct) Delegates \(precinctDelegates)\n"
        output += "VOTES\n"
        for candidate in viableCandidates {
            output += "\(candidate) \(align2Total(candidate: candidate))\n"
        }
        output += delegateMessage()
        output += "#CaucusReportApp\n"

        debugPrint("tweet characters \(output.count)")
        return output.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
    }

    func delegateFactor(candidate: String) -> Double {
        guard totalRegistrations > 0 else { return 0.0 }
        return (Double(align2Total(candidate: candidate)) * Double(precinctDelegates) / Double(totalRegistrations)).rounded(toPlaces: 4)
    }

    func calculateDelegates() -> (delegates: [String:Int], coinToss: String) {
        var usedDelegates = 0
        var candidateDelegates: [String: Int] = [:]
        var coinToss: String = "No card draws\n"
        guard viableCandidates.count > 0 else {
            coinToss = "No viable candidates"
            return (delegates: candidateDelegates, coinToss: coinToss)
        }
        func remainingFactor(candidate: String) -> Double {
            return delegateFactor(candidate: candidate) - Double(candidateDelegates[candidate]!)
        }

        func findHighestRemainingFactor() -> [String] {
            var largestFactor =  -1000.0 // initially set to low value
            var highestCandidates: [String] = []
            for candidate in viableCandidates {
                if remainingFactor(candidate: candidate) > largestFactor {
                    largestFactor = remainingFactor(candidate: candidate)
                    highestCandidates = [candidate]
                } else if remainingFactor(candidate: candidate) == largestFactor {
                    highestCandidates.append(candidate)
                }
            }
            return highestCandidates
        }

        // initially set delegates to 0
        for candidate in candidates {
            candidateDelegates[candidate] = 0
        }

        // all viable candidates get 1 delegate even if that
        // requires more delegates than assigned to precinct
        for candidate in viableCandidates {
            candidateDelegates[candidate] = candidateDelegates[candidate]! + 1
            usedDelegates = usedDelegates + 1
        }

        // keep assigning delegates to candiates until precinct is
        // out of delegates.  precinct might already be out
        // of delegates if viableCandidates.count >= precinctDelegates
        while usedDelegates < precinctDelegates {
            let nextWinners: [String] = findHighestRemainingFactor()
            if nextWinners.count <= (precinctDelegates - usedDelegates) {
                for winner in nextWinners {
                    candidateDelegates[winner] = candidateDelegates[winner]! + 1
                    usedDelegates = usedDelegates + 1
                }
                // if we have more tied winners this round than
                // remaining delegages, initiate a coin toss
            } else { //nextWinners.count > remaining delegates
                var winnerString = ""
                for winner in nextWinners {
                    winnerString = winnerString + " \(winner)"
                }
                coinToss = "Draw cards for \(precinctDelegates - usedDelegates) delegates between\(winnerString)\n"
                usedDelegates = precinctDelegates
            }
        }//while
        return (delegates: candidateDelegates, coinToss: coinToss)

    }

    func delegateMessage() -> String {
        var result = "DELEGATES\n"
        let (delegates, coinToss) = calculateDelegates()
        for (candidate,delegateCount) in delegates {
            if delegateCount > 0 {
                result += "\(candidate) \(delegateCount)\n"
            }
        }
        result += coinToss
        return result
    }

    var validResult: Bool {
        if viable2(candidate: "Write-ins") {
            return false
        }
        if align2GrandTotal <= 0 {
            return false
        }
        if totalRegistrations <= 0 {
            return false
        }
        return true
    }

    var invalidMessage: String {
        return """
        INVALID DATA DETECTED
        Registrations on Precinct screen could be 0
        Total votes in alignment 2 could be 0
        Write-ins in total could be viable
        (if too many write-ins, need to calculate manually)
        """
    }
}
\$\endgroup\$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.