Overview

The results calculation system computes election results based on the ballot type. Results are cached for performance and recalculated when needed. The system supports three calculation methods: Simple Vote, Condorcet (with Ranked Pairs), and Poll.

Results Caching

Cache Strategy

Results are cached in memory for 60 seconds to improve performance:

  • First request computes results
  • Subsequent requests use cached results
  • Cache expires after 60 seconds
  • Force refresh available via forceRefresh parameter

Cache Key

const cacheKey = `results:${election.id}`;

Force Refresh

To bypass cache and recalculate:

const results = await getElectionResults(db, election, true); // forceRefresh = true

Simple Vote Results

Calculation Method

Simple vote results count the number of each choice (YES, NO, ABSTAIN) and calculate percentages.

Algorithm

1. Initialize counters: yes = 0, no = 0, abstain = 0
2. For each ballot:
   - Parse payload JSON
   - If choice is "YES": yes++
   - If choice is "NO": no++
   - If choice is "ABSTAIN": abstain++
3. Calculate total = yes + no + abstain
4. Calculate percentages:
   - yesPercent = (yes / total) * 100
   - noPercent = (no / total) * 100
   - abstainPercent = (abstain / total) * 100

Results Format

interface SimpleResults {
  yes: number;
  no: number;
  abstain: number;
  total: number;
  yesPercent: number;
  noPercent: number;
  abstainPercent: number;
}

Example

Input (Ballots):

  • 45 votes for YES
  • 30 votes for NO
  • 5 votes for ABSTAIN

Output:

{
  "yes": 45,
  "no": 30,
  "abstain": 5,
  "total": 80,
  "yesPercent": 56.25,
  "noPercent": 37.5,
  "abstainPercent": 6.25
}

Condorcet Results

Overview

The Condorcet method determines winners by comparing candidates pairwise. If a candidate beats all others in head-to-head comparisons, they are the Condorcet winner. If no Condorcet winner exists, the system uses the Ranked Pairs method as a tiebreaker.

Step 1: Build Pairwise Matrix

For each pair of candidates (A, B), count how many voters ranked A above B.

Example:

  • 3 candidates: Alice, Bob, Charlie
  • 10 ballots with rankings

Matrix:

        Alice  Bob  Charlie
Alice    -     7    8
Bob      3     -    6
Charlie  2     4    -

This means:

  • 7 voters ranked Alice above Bob
  • 8 voters ranked Alice above Charlie
  • 6 voters ranked Bob above Charlie
  • etc.

Step 2: Check for Condorcet Winner

A Condorcet winner beats all other candidates pairwise.

Algorithm:

For each candidate:
  For each other candidate:
    If this candidate doesn't beat or tie the other:
      Not a Condorcet winner
  If beats all others:
    Return as Condorcet winner

Example:

  • Alice beats Bob (7 > 3) ✓
  • Alice beats Charlie (8 > 2) ✓
  • Alice is Condorcet winner

Step 3: Ranked Pairs (Tiebreaker)

If no Condorcet winner exists, use Ranked Pairs method.

Algorithm

  1. Build Pairs List: Create list of all candidate pairs with margins

    pairs = [
      { candidateA: "Alice", candidateB: "Bob", winsA: 7, winsB: 3, margin: 4 },
      { candidateA: "Alice", candidateB: "Charlie", winsA: 8, winsB: 2, margin: 6 },
      { candidateA: "Bob", candidateB: "Charlie", winsA: 6, winsB: 4, margin: 2 }
    ]
  2. Sort by Margin: Sort pairs by margin (descending), then by winsA (descending)

    // Sorted: Alice>Charlie (margin 6), Alice>Bob (margin 4), Bob>Charlie (margin 2)
  3. Lock in Pairs: For each pair (in sorted order):

    • Add to graph
    • Check for cycles using DFS
    • If no cycle: lock in the pair
    • If cycle: skip the pair
  4. Topological Sort: Build final ranking from locked pairs

    // Locked pairs: Alice→Charlie, Alice→Bob, Bob→Charlie
    // Ranking: Alice > Bob > Charlie

Cycle Detection

Ranked Pairs prevents cycles (A beats B, B beats C, C beats A) by:

  • Building a directed graph of locked pairs
  • Using DFS to detect cycles before locking
  • Skipping pairs that would create cycles

Example: Ranked Pairs

Scenario: No Condorcet winner

  • Alice beats Bob: 6-4
  • Bob beats Charlie: 5-5 (tie, skip)
  • Charlie beats Alice: 6-4

Pairs (sorted by margin):

  1. Alice > Bob (margin 2)
  2. Charlie > Alice (margin 2)

Locking:

  1. Lock Alice > Bob ✓
  2. Lock Charlie > Alice ✓
  3. Check: Would create cycle? No (Bob and Charlie not connected yet)
  4. Final ranking: Charlie > Alice > Bob

Results Format

interface CondorcetResults {
  winner: string | null;  // Candidate ID or null
  ranking: string[];      // Full ranking (winner first)
  pairwiseMatrix: Array<{
    candidateA: string;
    candidateB: string;
    winsA: number;
    winsB: number;
    margin: number;
  }>;
  method: "condorcet" | "ranked_pairs";
}

Edge Cases

Single Candidate:

  • Automatically wins
  • Method: “condorcet”

No Candidates:

  • Returns null winner, empty ranking
  • Method: “ranked_pairs”

Ties:

  • Tied pairs are skipped in Ranked Pairs
  • If all pairs tie, ranking is alphabetical

Poll Results

Single Choice Polls

Calculation:

1. Initialize count for each option: 0
2. For each ballot:
   - Parse selected_option
   - Increment count for that option
3. Calculate percentages: (count / total_votes) * 100

Results Format:

{
  "options": [
    { "option_id": "opt1", "option_name": "Option A", "count": 30, "percent": 60.0 },
    { "option_id": "opt2", "option_name": "Option B", "count": 20, "percent": 40.0 }
  ],
  "total_votes": 50,
  "total_selections": 50,
  "response_type": "single"
}

Multiple Choice Polls

Calculation:

1. Initialize count for each option: 0
2. For each ballot:
   - Parse selected_options array
   - Deduplicate options (in case of duplicates)
   - For each unique option: increment count
   - Increment total_selections
3. Calculate percentages: (count / total_votes) * 100

Note: Percentages may exceed 100% conceptually, but each option’s percentage is calculated as (count / total_votes) * 100, so percentages sum to more than 100% when voters select multiple options.

Results Format:

{
  "options": [
    { "option_id": "opt1", "option_name": "Option A", "count": 40, "percent": 80.0 },
    { "option_id": "opt2", "option_name": "Option B", "count": 35, "percent": 70.0 },
    { "option_id": "opt3", "option_name": "Option C", "count": 15, "percent": 30.0 }
  ],
  "total_votes": 50,
  "total_selections": 90,  // Sum of all selections
  "response_type": "multiple"
}

Turnout Calculation

All results include turnout statistics:

turnout: {
  total_tokens: number;      // Total tokens generated
  used_tokens: number;        // Tokens used (votes cast)
  unused_tokens: number;     // Tokens not used
  turnout_percent: number;    // (used_tokens / total_tokens) * 100
}

Example:

{
  "turnout": {
    "total_tokens": 100,
    "used_tokens": 80,
    "unused_tokens": 20,
    "turnout_percent": 80.0
  }
}

Invalid Ballots

The system handles invalid ballots gracefully:

  • Invalid JSON: Skipped
  • Missing required fields: Skipped
  • Invalid candidate/option IDs: Skipped
  • Wrong ballot type: Skipped

Invalid ballots are logged but don’t affect results calculation.

Performance Considerations

Optimization Strategies

  1. Caching: Results cached for 60 seconds
  2. Lazy Calculation: Results computed only when requested
  3. Efficient Algorithms: O(n) for simple votes, O(n*m²) for Condorcet (n=ballots, m=candidates)
  4. Database Indexing: Ballots indexed by election_id

Scalability

  • Simple Votes: Scales linearly with ballot count
  • Condorcet: Scales with ballot count × candidate count²
  • Polls: Scales linearly with ballot count

For large elections (1000+ ballots, 10+ candidates), consider:

  • Increasing cache duration
  • Pre-computing results
  • Using background jobs for calculation

API Usage

Get Results

Endpoint: GET /e/:id/results

Response:

{
  "election_id": "elec_123",
  "ballot_type": "SIMPLE_TRIPLE",
  "results": { /* SimpleResults, CondorcetResults, or PollResults */ },
  "turnout": { /* Turnout stats */ }
}

Force Refresh

Endpoint: GET /e/:id/results?refresh=true

Forces recalculation, bypassing cache.

Code Reference

Key Functions

src/utils/results.ts:

  • computeSimpleResults(): Simple vote calculation
  • computePollResults(): Poll calculation
  • getElectionResults(): Main function with caching

src/utils/condorcet.ts:

  • computeCondorcetResults(): Condorcet/Ranked Pairs calculation
  • buildPairwiseMatrix(): Build pairwise comparison matrix
  • findCondorcetWinner(): Check for Condorcet winner
  • rankedPairs(): Ranked Pairs algorithm

See Also