
High-performance Majority Judgment engine ranking candidates from grade tallies, scalable to trillions of voters and millions of candidates, exact integer arithmetic, LCM normalization, and extensible resolvers.
Test-driven Kotlin library to help deliberate (rank candidates) using Majority Judgment.
The goal is to be scalable, reliable, fast and extensible.
Collect the tallies for each candidate by your own means,
provide them to MajorityJudgment.deliberate(), and get back the rank of each candidate,
with some additional analysis data.
Let's say you have the following tally, from a poll with 18 voters:
| To Reject | Poor | Passable | Somewhat Good | Good | Very Good | Excellent | |
|---|---|---|---|---|---|---|---|
| Arancini | 4 | 4 | 3 | 1 | 3 | 1 | 2 |
| Burger | 3 | 6 | 2 | 2 | 2 | 1 | 2 |
| Chips | 5 | 3 | 0 | 2 | 3 | 2 | 3 |
| … | |||||||
The values in the table are the amount of judgments received per grade, by each candidate. They are entirely fabricated for the purpose of this example and do not reflect reality.
val mj = MajorityJudgment()
val tally = PollTally(
candidatesTallies = listOf(
CandidateTally(gradesTallies = arrayOf(4, 4, 3, 1, 3, 1, 2)),
CandidateTally(gradesTallies = arrayOf(3, 6, 2, 2, 2, 1, 2)),
CandidateTally(gradesTallies = arrayOf(5, 3, 0, 2, 3, 2, 3)),
),
)
val result = mj.deliberate(tally)
print(result.candidateResults.map { it.rank }) // [ 2, 3, 1 ]
print(result.candidateResultsRanked.map { it.index }) // [ 2, 0, 1 ][!TIP] The images of the merit profiles were generated using our online merit profile generator.
Sometimes, depending on how you've set up your poll, some candidates may receive more judgments than others.
Majority Judgment only works if the merit profiles are balanced, that is holding the same total amount of judgments.
This balancing strategy — using the lowest grade as the default grade — is recommended for most polls.
It incentivizes candidates to be clear, and to promote themselves.
val mj = MajorityJudgment()
val tally = StaticDefaultBalancedPollTally(
candidatesTallies = listOf(
CandidateTally(gradesTallies = arrayOf(1, 2, 3, 4)),
CandidateTally(gradesTallies = arrayOf(0, 0, 3, 4)),
),
)
println(tally.candidatesTallies[1].gradesTallies) // [ 3, 0, 3, 4 ]
assertContentEquals(
expected = arrayOf(3, 0, 3, 4).map { BigInteger.fromInt(it) }.toTypedArray(),
actual = tally.candidatesTallies[1].gradesTallies,
message = "Correct ranks",
)
val result = mj.deliberate(tally)
println(result.candidateResults.map { it.rank }) // [ 1, 2 ]
println(result.candidateResultsRanked.map { it.index }) // [ 0, 1 ]When there are too many candidates to vote on all of them, a good balancing strategy can be normalization.
We provide a Least Common Multiple based normalization.
To that effect you can use the NormalizationBalancedPollTally:
val mj = MajorityJudgment()
val tally = NormalizationBalancedPollTally(
candidatesTallies = listOf(
CandidateTally(gradesTallies = arrayOf(1, 2, 3, 4, 5)), // 15 judgments total
CandidateTally(gradesTallies = arrayOf(1, 1, 1, 1, 1)), // 5 judgments only
CandidateTally(gradesTallies = arrayOf(1, 1, 0, 0, 1)), // 3 judgments only
),
)
// Now the candidates' tallies are balanced
println(tally.candidatesTallies[0].gradesTallies.contentToString()) // [1, 2, 3, 4, 5]
println(tally.candidatesTallies[1].gradesTallies.contentToString()) // [3, 3, 3, 3, 3]
println(tally.candidatesTallies[2].gradesTallies.contentToString()) // [5, 5, 0, 0, 5]
val result = mj.deliberate(tally)
println(result.candidateResults.map { it.rank }) // [1, 2, 3]
println(result.candidateResultsRanked.map { it.index }) // [0, 1, 2]We have a bunch of unit tests.
./kotlin test
We use the term Candidate instead of the term Option, which is the term used in political science when studying voting systems. The rationale behind this is simple: Option is polysemic enough already in computer science. The term Candidate is sapiocentric, and therefore is not perfect, but it has the very important properties of being common and not ambiguous.
For a while we experimented with the term Proposal, but it's also polysemic so we dropped it.
There are two reasons why we want to be able to handle huge integers, beyond what 64 bits can provide us.
I did not want to depend on an external lib, but it's worth it, I promise.
When you want to do normalization using percentages or floating numbers, you risk having inaccurate results. Even if the probability of inaccurate results is low (very low!), it's uncool.
Voting systems must be 100% reliable, I believe. Also, I'm a computer engineer, and IEEE 754 fuels my nightmares.
Hence, we do normalization using LCM, and those multiples can grow fast if you're unlucky and fall on primes.
There's a way to normalize accurately without pre-computing the LCM, but it caps the amount of supported voters to about 3e9. That is a reasonable upper limit in 2026, but I'd rather support arbitrarily large amounts of voters, especially with the upcoming demographic boost wrought by the birth of AIs.
Although we can sort candidates using their deep majority gauges, the scalar majority merit is a very nice thing to have around, as it provides multiple advantages:
The scalar majority merit grows very big very quickly, and requires support for large numbers, as 64 bits are not enough, not by a long shot. Its size is on the scale of the amount of voters to the power of the amount of grades.
Test-driven Kotlin library to help deliberate (rank candidates) using Majority Judgment.
The goal is to be scalable, reliable, fast and extensible.
Collect the tallies for each candidate by your own means,
provide them to MajorityJudgment.deliberate(), and get back the rank of each candidate,
with some additional analysis data.
Let's say you have the following tally, from a poll with 18 voters:
| To Reject | Poor | Passable | Somewhat Good | Good | Very Good | Excellent | |
|---|---|---|---|---|---|---|---|
| Arancini | 4 | 4 | 3 | 1 | 3 | 1 | 2 |
| Burger | 3 | 6 | 2 | 2 | 2 | 1 | 2 |
| Chips | 5 | 3 | 0 | 2 | 3 | 2 | 3 |
| … | |||||||
The values in the table are the amount of judgments received per grade, by each candidate. They are entirely fabricated for the purpose of this example and do not reflect reality.
val mj = MajorityJudgment()
val tally = PollTally(
candidatesTallies = listOf(
CandidateTally(gradesTallies = arrayOf(4, 4, 3, 1, 3, 1, 2)),
CandidateTally(gradesTallies = arrayOf(3, 6, 2, 2, 2, 1, 2)),
CandidateTally(gradesTallies = arrayOf(5, 3, 0, 2, 3, 2, 3)),
),
)
val result = mj.deliberate(tally)
print(result.candidateResults.map { it.rank }) // [ 2, 3, 1 ]
print(result.candidateResultsRanked.map { it.index }) // [ 2, 0, 1 ][!TIP] The images of the merit profiles were generated using our online merit profile generator.
Sometimes, depending on how you've set up your poll, some candidates may receive more judgments than others.
Majority Judgment only works if the merit profiles are balanced, that is holding the same total amount of judgments.
This balancing strategy — using the lowest grade as the default grade — is recommended for most polls.
It incentivizes candidates to be clear, and to promote themselves.
val mj = MajorityJudgment()
val tally = StaticDefaultBalancedPollTally(
candidatesTallies = listOf(
CandidateTally(gradesTallies = arrayOf(1, 2, 3, 4)),
CandidateTally(gradesTallies = arrayOf(0, 0, 3, 4)),
),
)
println(tally.candidatesTallies[1].gradesTallies) // [ 3, 0, 3, 4 ]
assertContentEquals(
expected = arrayOf(3, 0, 3, 4).map { BigInteger.fromInt(it) }.toTypedArray(),
actual = tally.candidatesTallies[1].gradesTallies,
message = "Correct ranks",
)
val result = mj.deliberate(tally)
println(result.candidateResults.map { it.rank }) // [ 1, 2 ]
println(result.candidateResultsRanked.map { it.index }) // [ 0, 1 ]When there are too many candidates to vote on all of them, a good balancing strategy can be normalization.
We provide a Least Common Multiple based normalization.
To that effect you can use the NormalizationBalancedPollTally:
val mj = MajorityJudgment()
val tally = NormalizationBalancedPollTally(
candidatesTallies = listOf(
CandidateTally(gradesTallies = arrayOf(1, 2, 3, 4, 5)), // 15 judgments total
CandidateTally(gradesTallies = arrayOf(1, 1, 1, 1, 1)), // 5 judgments only
CandidateTally(gradesTallies = arrayOf(1, 1, 0, 0, 1)), // 3 judgments only
),
)
// Now the candidates' tallies are balanced
println(tally.candidatesTallies[0].gradesTallies.contentToString()) // [1, 2, 3, 4, 5]
println(tally.candidatesTallies[1].gradesTallies.contentToString()) // [3, 3, 3, 3, 3]
println(tally.candidatesTallies[2].gradesTallies.contentToString()) // [5, 5, 0, 0, 5]
val result = mj.deliberate(tally)
println(result.candidateResults.map { it.rank }) // [1, 2, 3]
println(result.candidateResultsRanked.map { it.index }) // [0, 1, 2]We have a bunch of unit tests.
./kotlin test
We use the term Candidate instead of the term Option, which is the term used in political science when studying voting systems. The rationale behind this is simple: Option is polysemic enough already in computer science. The term Candidate is sapiocentric, and therefore is not perfect, but it has the very important properties of being common and not ambiguous.
For a while we experimented with the term Proposal, but it's also polysemic so we dropped it.
There are two reasons why we want to be able to handle huge integers, beyond what 64 bits can provide us.
I did not want to depend on an external lib, but it's worth it, I promise.
When you want to do normalization using percentages or floating numbers, you risk having inaccurate results. Even if the probability of inaccurate results is low (very low!), it's uncool.
Voting systems must be 100% reliable, I believe. Also, I'm a computer engineer, and IEEE 754 fuels my nightmares.
Hence, we do normalization using LCM, and those multiples can grow fast if you're unlucky and fall on primes.
There's a way to normalize accurately without pre-computing the LCM, but it caps the amount of supported voters to about 3e9. That is a reasonable upper limit in 2026, but I'd rather support arbitrarily large amounts of voters, especially with the upcoming demographic boost wrought by the birth of AIs.
Although we can sort candidates using their deep majority gauges, the scalar majority merit is a very nice thing to have around, as it provides multiple advantages:
The scalar majority merit grows very big very quickly, and requires support for large numbers, as 64 bits are not enough, not by a long shot. Its size is on the scale of the amount of voters to the power of the amount of grades.