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
forceRefreshparameter
Cache Key
const cacheKey = `results:${election.id}`;Force Refresh
To bypass cache and recalculate:
const results = await getElectionResults(db, election, true); // forceRefresh = trueSimple 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) * 100Results 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 winnerExample:
- 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
-
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 } ] -
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) -
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
-
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):
- Alice > Bob (margin 2)
- Charlie > Alice (margin 2)
Locking:
- Lock Alice > Bob ✓
- Lock Charlie > Alice ✓
- Check: Would create cycle? No (Bob and Charlie not connected yet)
- 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) * 100Results 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) * 100Note: 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
- Caching: Results cached for 60 seconds
- Lazy Calculation: Results computed only when requested
- Efficient Algorithms: O(n) for simple votes, O(n*m²) for Condorcet (n=ballots, m=candidates)
- 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 calculationcomputePollResults(): Poll calculationgetElectionResults(): Main function with caching
src/utils/condorcet.ts:
computeCondorcetResults(): Condorcet/Ranked Pairs calculationbuildPairwiseMatrix(): Build pairwise comparison matrixfindCondorcetWinner(): Check for Condorcet winnerrankedPairs(): Ranked Pairs algorithm
See Also
- voting-system - Ballot types and voting flow
- token-management - Token validation
- api-reference - API endpoint documentation