Browse Source

Merge pull request #9 from MieuxVoter/feat-normalized-lcm

Normalized tally using Least Common Multiple
feat-median-default-grade
Dominique Merle 1 year ago
committed by GitHub
parent
commit
54b64bb48d
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 92
      README.md
  2. 72
      src/main/java/fr/mieuxvoter/mj/CollectedTally.java
  3. 10
      src/main/java/fr/mieuxvoter/mj/DeliberatorInterface.java
  4. 34
      src/main/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberator.java
  5. 79
      src/main/java/fr/mieuxvoter/mj/NormalizedTally.java
  6. 9
      src/main/java/fr/mieuxvoter/mj/ProposalResultInterface.java
  7. 21
      src/main/java/fr/mieuxvoter/mj/ProposalTally.java
  8. 24
      src/main/java/fr/mieuxvoter/mj/Tally.java
  9. 423
      src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java
  10. 38
      src/test/resources/assertions.json

92
README.md

@ -1,16 +1,25 @@
# Majority Judgment Library for Java
[![MIT](https://img.shields.io/github/license/MieuxVoter/majority-judgment-library-java)](./LICENSE.md)
![Build Status](https://img.shields.io/github/workflow/status/MieuxVoter/majority-judgment-library-java/Java%20CI%20with%20Maven)
![Release](https://img.shields.io/github/v/release/MieuxVoter/majority-judgment-library-java?sort=semver)
[![Build Status](https://img.shields.io/github/workflow/status/MieuxVoter/majority-judgment-library-java/Java%20CI%20with%20Maven)](https://github.com/MieuxVoter/majority-judgment-library-java/actions)
[![Release](https://img.shields.io/github/v/release/MieuxVoter/majority-judgment-library-java?sort=semver)](https://github.com/MieuxVoter/majority-judgment-library-java/releases)
[![Join the Discord chat at https://discord.gg/rAAQG9S](https://img.shields.io/discord/705322981102190593.svg)](https://discord.gg/rAAQG9S)
Test-driven java library to help deliberate using Majority Judgment.
Test-driven java library to help deliberate using [Majority Judgment](https://mieuxvoter.fr/index.php/decouvrir/?lang=en).
The goal is to be **scalable**, **reliable**, fast and extensible.
We therefore use a _score-based algorithm_ and _no floating-point arithmetic_ whatsoever.
## Features
- Supports billions of participants
- Supports thousands of proposals
- Handles default grades (static or normalized)
- No floating-point arithmetic
- Room for other deliberators (central, usual)
## Example Usage
Collect the **tallies** for each Proposal (aka. Candidate) by your own means,
@ -25,6 +34,7 @@ Let's say you have the following tally:
| … | | | | | | | |
| | | | | | | | |
> A _Tally_ is the amount of judgments received per grade, by each proposal.
``` java
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
@ -46,8 +56,80 @@ Got more than 2³² judges? Use a `Long[]` in a `ProposalTally`.
Got even more than that ? Use `BigInteger`s !
### Using a static default grade
Want to set a static default grade ? Use a `TallyWithDefaultGrade` instead of a `Tally`.
```java
Integer amountOfJudges = 18;
Integer defaultGrade = 0; // "worst" grade (usually "to reject")
TallyInterface tally = new TallyWithDefaultGrade(new ProposalTallyInterface[] {
// Amounts of judgments received of each grade, from "worst" grade to "best" grade
new ProposalTally(new Integer[]{4, 5, 2, 1, 3, 1, 2}), // Proposal A
new ProposalTally(new Integer[]{3, 6, 2, 1, 3, 1, 2}), // Proposal B
// …
}, amountOfJudges, defaultGrade);
```
### Using normalized tallies
In some polls with a very high amount of proposals, where participants cannot be expected to judge every last one of them, it may make sense to normalize the tallies instead of using a default grade.
To that effect, use a `NormalizedTally` instead of a `Tally`.
```java
TallyInterface tally = new NormalizedTally(new ProposalTallyInterface[] {
// Amounts of judgments received of each grade, from "worst" grade to "best" grade
new ProposalTally(new Integer[]{4, 5, 2, 1, 3, 1, 2}), // Proposal A
new ProposalTally(new Integer[]{3, 6, 2, 1, 3, 1, 2}), // Proposal B
// …
});
```
> This normalization uses the Least Common Multiple, in order to skip floating-point arithmetic.
### Collect a Tally from judgments
It's usually best to use structured queries (eg: in SQL) directly in your database to collect the tallies, since it scales better with high amounts of participants, but if you must you can collect the tally directly from individual judgments, with a `CollectedTally`.
```java
Integer amountOfProposals = 2;
Integer amountOfGrades = 4;
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
CollectedTally tally = new CollectedTally(amountOfProposals, amountOfGrades);
Integer firstProposal = 0;
Integer secondProposal = 1;
Integer gradeReject = 0;
Integer gradePassable = 1;
Integer gradeGood = 2;
Integer gradeExcellent = 3;
// Collect the judgments, one-by-one, with `collect()`
tally.collect(firstProposal, gradeReject);
tally.collect(firstProposal, gradeReject);
tally.collect(firstProposal, gradePassable);
tally.collect(firstProposal, gradePassable);
tally.collect(firstProposal, gradePassable);
tally.collect(firstProposal, gradeExcellent);
tally.collect(firstProposal, gradeExcellent);
tally.collect(secondProposal, gradeReject);
tally.collect(secondProposal, gradeReject);
tally.collect(secondProposal, gradeGood);
tally.collect(secondProposal, gradeGood);
tally.collect(secondProposal, gradeGood);
tally.collect(secondProposal, gradeExcellent);
tally.collect(secondProposal, gradeExcellent);
// …
ResultInterface result = mj.deliberate(tally);
```
## Roadmap
@ -57,11 +139,11 @@ Want to set a static default grade ? Use a `TallyWithDefaultGrade` instead of a
- [x] Score Calculus
- [x] Ranking
- [x] Release v0.1.0
- [ ] Guess the amount of judges
- [x] Guess the amount of judges
- [ ] Allow defining a default grade
- [x] Static Grade (configurable)
- [x] Normalization (using least common multiple)
- [ ] Median Grade
- [ ] Normalization (using least common multiple)
- [ ] Release v0.2.0
- [ ] Publish on package repositories
- [ ] Gradle

72
src/main/java/fr/mieuxvoter/mj/CollectedTally.java

@ -0,0 +1,72 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
public class CollectedTally implements TallyInterface {
Integer amountOfProposals = 0;
Integer amountOfGrades = 0;
ProposalTally[] proposalsTallies;
public CollectedTally(Integer amountOfProposals, Integer amountOfGrades) {
setAmountOfProposals(amountOfProposals);
setAmountOfGrades(amountOfGrades);
proposalsTallies = new ProposalTally[amountOfProposals];
for (int i = 0; i < amountOfProposals; i++) {
ProposalTally proposalTally = new ProposalTally();
Integer[] tally = new Integer[amountOfGrades];
for (int j = 0; j < amountOfGrades; j++) {
tally[j] = 0;
}
proposalTally.setTally(tally);
proposalsTallies[i] = proposalTally;
}
}
@Override
public ProposalTallyInterface[] getProposalsTallies() {
return proposalsTallies;
}
@Override
public BigInteger getAmountOfJudges() {
return guessAmountOfJudges();
}
@Override
public Integer getAmountOfProposals() {
return this.amountOfProposals;
}
public void setAmountOfProposals(Integer amountOfProposals) {
this.amountOfProposals = amountOfProposals;
}
public Integer getAmountOfGrades() {
return amountOfGrades;
}
public void setAmountOfGrades(Integer amountOfGrades) {
this.amountOfGrades = amountOfGrades;
}
protected BigInteger guessAmountOfJudges() {
BigInteger amountOfJudges = BigInteger.ZERO;
for (ProposalTallyInterface proposalTally : getProposalsTallies()) {
amountOfJudges = proposalTally.getAmountOfJudgments().max(amountOfJudges);
}
return amountOfJudges;
}
public void collect(Integer proposal, Integer grade) {
assert(0 <= proposal);
assert(amountOfProposals > proposal);
assert(0 <= grade);
assert(amountOfGrades > grade);
BigInteger[] tally = proposalsTallies[proposal].getTally();
tally[grade] = tally[grade].add(BigInteger.ONE);
}
}

10
src/main/java/fr/mieuxvoter/mj/DeliberatorInterface.java

@ -3,16 +3,18 @@ package fr.mieuxvoter.mj;
/**
* A Deliberator takes in a poll's Tally,
* that is the amount of judgments of each grade received by each Proposal,
* and outputs that poll's Result,
* that is the final rank of each Proposal.
* which holds the amount of judgments of each grade received by each Proposal,
* and outputs that poll's Result, that is the final rank of each Proposal.
*
* Ranks start at 1 ("best"), and increment towards "worst".
* Two proposal may share the same rank, in extreme equality cases.
*
* This is the main API of this library.
*
* See MajorityJudgmentDeliberator for an implementation.
* One could implement other deliberators, such as CentralJudgment or UsualJudgment.
* One could implement other deliberators, such as:
* - CentralJudgmentDeliberator
* - UsualJudgmentDeliberator
*/
public interface DeliberatorInterface {

34
src/main/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberator.java

@ -18,11 +18,10 @@ import java.util.Comparator;
*
* https://en.wikipedia.org/wiki/Majority_judgment
* https://fr.wikipedia.org/wiki/Jugement_majoritaire
*
* Should this class be "final" ?
*/
public class MajorityJudgmentDeliberator implements DeliberatorInterface {
final public class MajorityJudgmentDeliberator implements DeliberatorInterface {
@Override
public ResultInterface deliberate(TallyInterface tally) {
ProposalTallyInterface[] tallies = tally.getProposalsTallies();
BigInteger amountOfJudges = tally.getAmountOfJudges();
@ -41,7 +40,7 @@ public class MajorityJudgmentDeliberator implements DeliberatorInterface {
proposalResults[proposalIndex] = proposalResult;
}
// II. Sort Proposals by score
// II. Sort Proposals by score (lexicographical inverse)
ProposalResult[] proposalResultsSorted = proposalResults.clone();
assert(proposalResultsSorted[0].hashCode() == proposalResults[0].hashCode()); // we need a shallow clone
Arrays.sort(proposalResultsSorted, new Comparator<ProposalResultInterface>() {
@ -70,11 +69,21 @@ public class MajorityJudgmentDeliberator implements DeliberatorInterface {
return result;
}
public String computeScore(ProposalTallyInterface tally, BigInteger amountOfJudges) {
protected String computeScore(ProposalTallyInterface tally, BigInteger amountOfJudges) {
return computeScore(tally, amountOfJudges, true, false);
}
public String computeScore(
/**
* A higher score means a better rank.
* Assumes that grades' tallies are provided from "worst" grade to "best" grade.
*
* @param tally Holds the tallies of each Grade for a single Proposal
* @param amountOfJudges
* @param favorContestation
* @param onlyNumbers Do not use separation characters, match `^[0-9]+$`
* @return
*/
protected String computeScore(
ProposalTallyInterface tally,
BigInteger amountOfJudges,
Boolean favorContestation,
@ -82,8 +91,8 @@ public class MajorityJudgmentDeliberator implements DeliberatorInterface {
) {
ProposalTallyAnalysis analysis = new ProposalTallyAnalysis();
int amountOfGrades = tally.getTally().length;
int digitsForGrade = ("" + amountOfGrades).length();
int digitsForGroup = ("" + amountOfJudges).length() + 1;
int digitsForGrade = countDigits(amountOfGrades);
int digitsForGroup = countDigits(amountOfJudges) + 1;
ProposalTallyInterface currentTally = tally.duplicate();
@ -107,6 +116,7 @@ public class MajorityJudgmentDeliberator implements DeliberatorInterface {
score += String.format(
"%0"+digitsForGroup+"d",
// We offset by amountOfJudges to keep a lexicographical order (no negatives)
// amountOfJudges + secondMedianGroupSize * secondMedianGroupSign
amountOfJudges.add(
analysis.getSecondMedianGroupSize().multiply(
@ -121,4 +131,12 @@ public class MajorityJudgmentDeliberator implements DeliberatorInterface {
return score;
}
protected int countDigits(int number) {
return ("" + number).length();
}
protected int countDigits(BigInteger number) {
return ("" + number).length();
}
}

79
src/main/java/fr/mieuxvoter/mj/NormalizedTally.java

@ -0,0 +1,79 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
import java.security.InvalidParameterException;
/**
* The deliberator expects the proposals' tallies to hold the same amount of judgments.
* This NormalizedTally accepts tallies with disparate amounts of judgments per proposal,
* and normalizes them to their least common multiple, which amounts to using percentages,
* except we don't use floating-point arithmetic.
*
* This is useful when there are too many proposals for judges to be expected to judge them all,
* and all the proposals received reasonably similar amounts of judgments.
*/
public class NormalizedTally extends Tally implements TallyInterface {
public NormalizedTally(ProposalTallyInterface[] proposalsTallies) {
super(proposalsTallies);
initializeFromProposalsTallies(proposalsTallies);
}
public NormalizedTally(TallyInterface tally) {
super(tally.getProposalsTallies());
initializeFromProposalsTallies(tally.getProposalsTallies());
}
protected void initializeFromProposalsTallies(ProposalTallyInterface[] proposalsTallies) {
Integer amountOfProposals = getAmountOfProposals();
// Compute the Least Common Multiple
BigInteger amountOfJudges = BigInteger.ONE;
for (ProposalTallyInterface proposalTally : proposalsTallies) {
amountOfJudges = lcm(amountOfJudges, proposalTally.getAmountOfJudgments());
}
if (0 == amountOfJudges.compareTo(BigInteger.ZERO)) {
throw new InvalidParameterException("Cannot normalize: one or more proposals have no judgments.");
}
// Normalize proposals to the LCM
ProposalTally[] normalizedTallies = new ProposalTally[amountOfProposals];
for (int i = 0 ; i < amountOfProposals ; i++) {
ProposalTallyInterface proposalTally = proposalsTallies[i];
ProposalTally normalizedTally = new ProposalTally(proposalTally);
BigInteger factor = amountOfJudges.divide(proposalTally.getAmountOfJudgments());
Integer amountOfGrades = proposalTally.getTally().length;
BigInteger[] gradesTallies = normalizedTally.getTally();
for (int j = 0 ; j < amountOfGrades; j++) {
gradesTallies[j] = gradesTallies[j].multiply(factor);
}
normalizedTallies[i] = normalizedTally;
}
setProposalsTallies(normalizedTallies);
setAmountOfJudges(amountOfJudges);
}
/**
* Least Common Multiple
*
* http://en.wikipedia.org/wiki/Least_common_multiple
*
* lcm( 6, 9 ) = 18
* lcm( 4, 9 ) = 36
* lcm( 0, 9 ) = 0
* lcm( 0, 0 ) = 0
*
* @author www.java2s.com
* @param a first integer
* @param b second integer
* @return least common multiple of a and b
*/
public static BigInteger lcm(BigInteger a, BigInteger b) {
if (a.signum() == 0 || b.signum() == 0)
return BigInteger.ZERO;
return a.divide(a.gcd(b)).multiply(b).abs();
}
}

9
src/main/java/fr/mieuxvoter/mj/ProposalResultInterface.java

@ -2,20 +2,21 @@ package fr.mieuxvoter.mj;
public interface ProposalResultInterface {
/**
* Rank starts at 1 ("best" proposal), and goes upwards.
* Multiple Proposals may receive the same rank,
* in the extreme case where they received the exact same judgments.
* in the extreme case where they received the exact same judgments,
* or judgment repartition in normalized tallies.
*/
public Integer getRank();
/**
* This score was used to compute the rank.
* It is made of integer characters, with zeroes for padding.
* Reverse lexicographical order: "higher" is "better".
* Inverse lexicographical order: "higher" is "better".
* You're probably never going to need this, but it's here anyway.
*/
public String getScore();
}

21
src/main/java/fr/mieuxvoter/mj/ProposalTally.java

@ -5,27 +5,34 @@ import java.util.Arrays;
public class ProposalTally implements ProposalTallyInterface {
/**
* Amounts of judgments received per grade, from "worst" grade to "best" grade.
* Those are BigIntegers because of our LCM-based normalization shenanigans.
*/
protected BigInteger[] tally;
// Should we allow this as well?
//public ProposalTally() {}
public ProposalTally() {}
public ProposalTally(String[] tally) {
setTally(tally);
}
public ProposalTally(Integer[] tally) {
setTally(tally);
}
public ProposalTally(Long[] tally) {
setTally(tally);
}
public ProposalTally(BigInteger[] tally) {
setTally(tally);
}
public ProposalTally(ProposalTallyInterface proposalTally) {
setTally(Arrays.copyOf(proposalTally.getTally(), proposalTally.getTally().length));
}
public void setTally(String[] tally) {
int tallyLength = tally.length;
BigInteger[] bigTally = new BigInteger[tallyLength];
@ -52,11 +59,11 @@ public class ProposalTally implements ProposalTallyInterface {
}
setTally(bigTally);
}
public void setTally(BigInteger[] tally) {
this.tally = tally;
}
@Override
public BigInteger[] getTally() {
return this.tally;

24
src/main/java/fr/mieuxvoter/mj/Tally.java

@ -2,12 +2,20 @@ package fr.mieuxvoter.mj;
import java.math.BigInteger;
/**
* A Basic implementation of a TallyInterface that reads from an array of ProposalTallyInterface.
*/
public class Tally implements TallyInterface {
protected ProposalTallyInterface[] proposalsTallies;
protected BigInteger amountOfJudges = BigInteger.ZERO;
public Tally(ProposalTallyInterface[] proposalsTallies) {
setProposalsTallies(proposalsTallies);
guessAmountOfJudges();
}
public Tally(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges) {
setProposalsTallies(proposalsTallies);
setAmountOfJudges(amountOfJudges);
@ -17,7 +25,7 @@ public class Tally implements TallyInterface {
setProposalsTallies(proposalsTallies);
setAmountOfJudges(BigInteger.valueOf(amountOfJudges));
}
public Tally(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges) {
setProposalsTallies(proposalsTallies);
setAmountOfJudges(BigInteger.valueOf(amountOfJudges));
@ -42,5 +50,13 @@ public class Tally implements TallyInterface {
public void setAmountOfJudges(BigInteger amountOfJudges) {
this.amountOfJudges = amountOfJudges;
}
protected void guessAmountOfJudges() {
BigInteger amountOfJudges = BigInteger.ZERO;
for (ProposalTallyInterface proposalTally : getProposalsTallies()) {
amountOfJudges = proposalTally.getAmountOfJudgments().max(amountOfJudges);
}
setAmountOfJudges(amountOfJudges);
}
}

423
src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java

@ -2,6 +2,8 @@ package fr.mieuxvoter.mj;
import static org.junit.jupiter.api.Assertions.*;
import java.math.BigInteger;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonValue;
@ -14,13 +16,57 @@ import net.joshka.junit.json.params.JsonFileSource;
class MajorityJudgmentDeliberatorTest {
@DisplayName("Test majority judgment deliberation from JSON assertions")
@ParameterizedTest(name="#{index} {0}")
@JsonFileSource(resources = "/assertions.json")
public void testFromJson(JsonObject datum) {
JsonArray jsonTallies = datum.getJsonArray("tallies");
int amountOfProposals = jsonTallies.size();
BigInteger amountOfParticipants = new BigInteger(datum.get("participants").toString());
ProposalTallyInterface[] tallies = new ProposalTallyInterface[amountOfProposals];
for (int i = 0; i < amountOfProposals; i++) {
JsonArray jsonTally = jsonTallies.getJsonArray(i);
int amountOfGrades = jsonTally.size();
BigInteger[] tally = new BigInteger[amountOfGrades];
for (int g = 0; g < amountOfGrades; g++) {
JsonValue amountForGrade = jsonTally.get(g);
tally[g] = new BigInteger(amountForGrade.toString());
}
tallies[i] = new ProposalTally(tally);
}
String mode = datum.getString("mode", "None");
TallyInterface tally;
if ("StaticDefault".equalsIgnoreCase(mode)) {
tally = new TallyWithDefaultGrade(tallies, amountOfParticipants, datum.getInt("default"));
} else if ("Normalized".equalsIgnoreCase(mode)) {
tally = new NormalizedTally(tallies);
} else {
tally = new Tally(tallies, amountOfParticipants);
}
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
ResultInterface result = mj.deliberate(tally);
assertNotNull(result);
JsonArray jsonRanks = datum.getJsonArray("ranks");
for (int i = 0; i < amountOfProposals; i++) {
assertEquals(
jsonRanks.getInt(i),
result.getProposalResults()[i].getRank(),
"Rank of tally #"+i
);
}
}
@Test
@DisplayName("Test the basic demo usage of the README")
public void testDemoUsage() {
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
TallyInterface tally = new Tally(new ProposalTallyInterface[] {
new ProposalTally(new Integer[]{4, 5, 2, 1, 3, 1, 2}),
new ProposalTally(new Integer[]{3, 6, 2, 1, 3, 1, 2}),
}, 18L);
});
ResultInterface result = mj.deliberate(tally);
@ -31,18 +77,18 @@ class MajorityJudgmentDeliberatorTest {
}
@Test
public void testUsageWithBigNumbers() {
@DisplayName("Test the basic demo usage with billions of participants")
public void testDemoUsageWithBigNumbers() {
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
TallyInterface tally = new Tally(new ProposalTallyInterface[] {
new ProposalTally(new Long[]{11312415004L, 21153652410L, 24101523299L, 18758623562L}),
new ProposalTally(new Long[]{11312415004L, 21153652400L, 24101523299L, 18758623572L}),
// new ProposalTally(new Long[]{14526586452L, 40521123260L, 14745623120L, 40526235129L}),
}, 75326214275L);
});
ResultInterface result = mj.deliberate(tally);
// System.out.println("Score 0: "+result.getProposalResults()[0].getScore());
// System.out.println("Score 1: "+result.getProposalResults()[1].getScore());
// System.out.println("Total "+(11312415004L+21153652410L+24101523299L+18758623562L));
assertNotNull(result);
assertEquals(2, result.getProposalResults().length);
@ -50,61 +96,223 @@ class MajorityJudgmentDeliberatorTest {
assertEquals(1, result.getProposalResults()[1].getRank());
}
@DisplayName("Test majority judgment deliberation")
@ParameterizedTest(name="#{index} {0}")
@JsonFileSource(resources = "/assertions.json")
public void testFromJson(JsonObject datum) {
JsonArray jsonTallies = datum.getJsonArray("tallies");
int amountOfProposals = jsonTallies.size();
Long amountOfParticipants = Long.valueOf(datum.get("participants").toString());
@Test
@DisplayName("Test the collect demo usage of the README")
public void testDemoUsageCollectedTally() {
Integer amountOfProposals = 3;
Integer amountOfGrades = 4;
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
CollectedTally tally = new CollectedTally(amountOfProposals, amountOfGrades);
Integer firstProposal = 0;
Integer secondProposal = 1;
Integer thirdProposal = 2;
Integer gradeReject = 0;
Integer gradePassable = 1;
Integer gradeGood = 2;
Integer gradeExcellent = 3;
tally.collect(firstProposal, gradeReject);
tally.collect(firstProposal, gradeReject);
tally.collect(firstProposal, gradePassable);
tally.collect(firstProposal, gradePassable);
tally.collect(firstProposal, gradePassable);
tally.collect(firstProposal, gradeExcellent);
tally.collect(firstProposal, gradeExcellent);
tally.collect(firstProposal, gradeExcellent);
tally.collect(secondProposal, gradeReject);
tally.collect(secondProposal, gradeReject);
tally.collect(secondProposal, gradeGood);
tally.collect(secondProposal, gradeGood);
tally.collect(secondProposal, gradeGood);
tally.collect(secondProposal, gradeGood);
tally.collect(secondProposal, gradeExcellent);
tally.collect(secondProposal, gradeExcellent);
tally.collect(thirdProposal, gradeReject);
tally.collect(thirdProposal, gradeReject);
tally.collect(thirdProposal, gradePassable);
tally.collect(thirdProposal, gradeGood);
tally.collect(thirdProposal, gradeGood);
tally.collect(thirdProposal, gradeGood);
tally.collect(thirdProposal, gradeExcellent);
tally.collect(thirdProposal, gradeExcellent);
ResultInterface result = mj.deliberate(tally);
assertNotNull(result);
assertEquals(3, result.getProposalResults().length);
assertEquals(3, result.getProposalResults()[0].getRank());
assertEquals(1, result.getProposalResults()[1].getRank());
assertEquals(2, result.getProposalResults()[2].getRank());
}
@Test
@DisplayName("Test the normalized collect demo usage of the README")
public void testDemoUsageNormalizedCollectedTally() {
Integer amountOfProposals = 4;
Integer amountOfGrades = 3;
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
CollectedTally tally = new CollectedTally(amountOfProposals, amountOfGrades);
Integer firstProposal = 0;
Integer secondProposal = 1;
Integer thirdProposal = 2;
Integer fourthProposal = 3;
Integer gradeReject = 0;
Integer gradePassable = 1;
Integer gradeGood = 2;
tally.collect(firstProposal, gradeReject);
tally.collect(firstProposal, gradeReject);
tally.collect(firstProposal, gradePassable);
tally.collect(firstProposal, gradePassable);
tally.collect(firstProposal, gradeGood);
tally.collect(firstProposal, gradeGood);
tally.collect(secondProposal, gradeReject);
tally.collect(secondProposal, gradePassable);
tally.collect(secondProposal, gradeGood);
tally.collect(thirdProposal, gradePassable);
tally.collect(fourthProposal, gradeGood);
ResultInterface result = mj.deliberate(
new NormalizedTally(tally)
);
assertNotNull(result);
assertEquals(4, result.getProposalResults().length);
assertEquals(3, result.getProposalResults()[0].getRank());
assertEquals(3, result.getProposalResults()[1].getRank());
assertEquals(2, result.getProposalResults()[2].getRank());
assertEquals(1, result.getProposalResults()[3].getRank());
}
@Test
@DisplayName("Test with a static default grade (\"worst grade\" == 0)")
public void testWithStaticDefaultGrade() {
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
Long amountOfJudges = 3L;
Integer defaultGrade = 0;
TallyInterface tally = new TallyWithDefaultGrade(new ProposalTallyInterface[] {
new ProposalTally(new Integer[]{ 0, 0, 1 }),
new ProposalTally(new Integer[]{ 0, 3, 0 }),
new ProposalTally(new Integer[]{ 2, 0, 1 }),
}, amountOfJudges, defaultGrade);
ResultInterface result = mj.deliberate(tally);
assertNotNull(result);
assertEquals(3, result.getProposalResults().length);
assertEquals(2, result.getProposalResults()[0].getRank());
assertEquals(1, result.getProposalResults()[1].getRank());
assertEquals(2, result.getProposalResults()[2].getRank());
}
@Test
@DisplayName("Test static default grade with thousands of proposals")
public void testStaticDefaultWithThousandsOfProposals() {
int amountOfProposals = 1337;
Integer amountOfJudges = 60000000;
Integer defaultGrade = 0;
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
ProposalTallyInterface[] tallies = new ProposalTallyInterface[amountOfProposals];
for (int i = 0; i < amountOfProposals; i++) {
JsonArray jsonTally = jsonTallies.getJsonArray(i);
int amountOfGrades = jsonTally.size();
Long[] tally = new Long[amountOfGrades];
for (int g = 0; g < amountOfGrades; g++) {
JsonValue amountForGrade = jsonTally.get(g);
tally[g] = Long.valueOf(amountForGrade.toString());
}
tallies[i] = new ProposalTally(tally);
for (int i = 0 ; i < amountOfProposals ; i++) {
tallies[i] = new ProposalTally(new Integer[]{ 7, 204, 107 });
}
TallyInterface tally = new TallyWithDefaultGrade(tallies, amountOfJudges, defaultGrade);
String mode = datum.getString("mode", "None");
TallyInterface tally;
if ("StaticDefault".equalsIgnoreCase(mode)) {
tally = new TallyWithDefaultGrade(tallies, amountOfParticipants, datum.getInt("default"));
} else {
tally = new Tally(tallies, amountOfParticipants);
ResultInterface result = mj.deliberate(tally);
assertNotNull(result);
assertEquals(amountOfProposals, result.getProposalResults().length);
for (int i = 0 ; i < amountOfProposals ; i++) {
assertEquals(1, result.getProposalResults()[i].getRank());
}
}
// /!. This crashes
// @Test
// @DisplayName("Test static default grade with millions of proposals")
// public void testStaticDefaultWithMillionsOfProposals() {
// int amountOfProposals = 13375111;
// Integer amountOfJudges = 60000000;
// Integer defaultGrade = 0;
// DeliberatorInterface mj = new MajorityJudgmentDeliberator();
// ProposalTallyInterface[] tallies = new ProposalTallyInterface[amountOfProposals];
// for (int i = 0 ; i < amountOfProposals ; i++) {
// tallies[i] = new ProposalTally(new Integer[]{ 7, 204, 107 });
// }
// TallyInterface tally = new TallyWithDefaultGrade(tallies, amountOfJudges, defaultGrade);
//
// ResultInterface result = mj.deliberate(tally);
//
// assertNotNull(result);
// assertEquals(amountOfProposals, result.getProposalResults().length);
// for (int i = 0 ; i < amountOfProposals ; i++) {
// assertEquals(1, result.getProposalResults()[i].getRank());
// }
// }
@Test
@DisplayName("Test normalized tallies with thousands of (prime) proposals")
public void testNormalizedWithThousandsOfPrimeProposals() {
// We're using primes to test the upper bounds of our LCM shenanigans.
// This test takes a long time! (3 seconds)
// List<Integer> generatedPrimes = sieveOfEratosthenes(15000);
// System.out.println(generatedPrimes);
int amountOfProposals = primes.length; // 1437
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
ProposalTallyInterface[] tallies = new ProposalTallyInterface[amountOfProposals];
for (int i = 0 ; i < amountOfProposals ; i++) {
Integer prime = primes[i % primes.length];
tallies[i] = new ProposalTally(new Integer[]{ prime-1, 1, 0 });
}
TallyInterface tally = new NormalizedTally(tallies);
ResultInterface result = mj.deliberate(tally);
assertNotNull(result);
JsonArray jsonRanks = datum.getJsonArray("ranks");
for (int i = 0; i < amountOfProposals; i++) {
assertEquals(amountOfProposals, result.getProposalResults().length);
for (int i = 0 ; i < amountOfProposals ; i++) {
assertEquals(
jsonRanks.getInt(i),
result.getProposalResults()[i].getRank(),
"Rank of tally #"+i
1 + (i % primes.length), result.getProposalResults()[i].getRank(),
"Rank of Proposal #" + i
);
}
}
@Test
public void testWithStaticDefaultGrade() {
@DisplayName("Test normalized tallies with thousands of proposals")
public void testNormalizedWithThousandsOfProposals() {
// This test is faster than the primes one (0.4 seconds),
// since primes are the worst case-scenario for our LCM.
int amountOfProposals = primes.length; // 1437
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
TallyInterface tally = new TallyWithDefaultGrade(new ProposalTallyInterface[] {
new ProposalTally(new Integer[]{ 0, 0, 1 }),
new ProposalTally(new Integer[]{ 0, 3, 0 }),
}, 3L, 0);
ProposalTallyInterface[] tallies = new ProposalTallyInterface[amountOfProposals];
for (int i = 0 ; i < amountOfProposals ; i++) {
tallies[i] = new ProposalTally(new Integer[]{ i, 1, 0 });
}
TallyInterface tally = new NormalizedTally(tallies);
ResultInterface result = mj.deliberate(tally);
assertNotNull(result);
assertEquals(2, result.getProposalResults().length);
assertEquals(2, result.getProposalResults()[0].getRank());
assertEquals(1, result.getProposalResults()[1].getRank());
assertEquals(amountOfProposals, result.getProposalResults().length);
for (int i = 0 ; i < amountOfProposals ; i++) {
assertEquals(
1 + (i % primes.length), result.getProposalResults()[i].getRank(),
"Rank of Proposal #" + i
);
}
}
// @Test
@ -124,4 +332,141 @@ class MajorityJudgmentDeliberatorTest {
// new Runner(options).run();
// }
/**
* Helps us test extreme situations (upper bounds) in normalized tallies,
* since we use the LCM (Least Common Multiple) to avoid floating-point arithmetic.
*/
protected Integer[] primes = new Integer[] {
2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59,
61, 67, 71, 73, 79, 83, 89, 97, 101, 103, 107, 109, 113, 127,
131, 137, 139, 149, 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199,
211, 223, 227, 229, 233, 239, 241, 251, 257, 263, 269, 271, 277, 281, 283,
293, 307, 311, 313, 317, 331, 337, 347, 349, 353, 359, 367, 373, 379, 383,
389, 397, 401, 409, 419, 421, 431, 433, 439, 443, 449, 457, 461, 463, 467,
479, 487, 491, 499, 503, 509, 521, 523, 541, 547, 557, 563, 569, 571, 577,
587, 593, 599, 601, 607, 613, 617, 619, 631, 641, 643, 647, 653, 659, 661,
673, 677, 683, 691, 701, 709, 719, 727, 733, 739, 743, 751, 757, 761, 769,
773, 787, 797, 809, 811, 821, 823, 827, 829, 839, 853, 857, 859, 863, 877,
881, 883, 887, 907, 911, 919, 929, 937, 941, 947, 953, 967, 971, 977, 983,
991, 997, 1009, 1013, 1019, 1021, 1031, 1033, 1039, 1049, 1051, 1061, 1063,
1069, 1087, 1091, 1093, 1097, 1103, 1109, 1117, 1123, 1129, 1151, 1153, 1163,
1171, 1181, 1187, 1193, 1201, 1213, 1217, 1223, 1229, 1231, 1237, 1249, 1259,
1277, 1279, 1283, 1289, 1291, 1297, 1301, 1303, 1307, 1319, 1321, 1327, 1361,
1367, 1373, 1381, 1399, 1409, 1423, 1427, 1429, 1433, 1439, 1447, 1451, 1453,
1459, 1471, 1481, 1483, 1487, 1489, 1493, 1499, 1511, 1523, 1531, 1543, 1549,
1553, 1559, 1567, 1571, 1579, 1583, 1597, 1601, 1607, 1609, 1613, 1619, 1621,
1627, 1637, 1657, 1663, 1667, 1669, 1693, 1697, 1699, 1709, 1721, 1723, 1733,
1741, 1747, 1753, 1759, 1777, 1783, 1787, 1789, 1801, 1811, 1823, 1831, 1847,
1861, 1867, 1871, 1873, 1877, 1879, 1889, 1901, 1907, 1913, 1931, 1933, 1949,
1951, 1973, 1979, 1987, 1993, 1997, 1999, 2003, 2011, 2017, 2027, 2029, 2039,
2053, 2063, 2069, 2081, 2083, 2087, 2089, 2099, 2111, 2113, 2129, 2131, 2137,
2141, 2143, 2153, 2161, 2179, 2203, 2207, 2213, 2221, 2237, 2239, 2243, 2251,
2267, 2269, 2273, 2281, 2287, 2293, 2297, 2309, 2311, 2333, 2339, 2341, 2347,
2351, 2357, 2371, 2377, 2381, 2383, 2389, 2393, 2399, 2411, 2417, 2423, 2437,
2441, 2447, 2459, 2467, 2473, 2477, 2503, 2521, 2531, 2539, 2543, 2549, 2551,
2557, 2579, 2591, 2593, 2609, 2617, 2621, 2633, 2647, 2657, 2659, 2663, 2671,
2677, 2683, 2687, 2689, 2693, 2699, 2707, 2711, 2713, 2719, 2729, 2731, 2741,
2749, 2753, 2767, 2777, 2789, 2791, 2797, 2801, 2803, 2819, 2833, 2837, 2843,
2851, 2857, 2861, 2879, 2887, 2897, 2903, 2909, 2917, 2927, 2939, 2953, 2957,
2963, 2969, 2971, 2999, 3001, 3011, 3019, 3023, 3037, 3041, 3049, 3061, 3067,
3079, 3083, 3089, 3109, 3119, 3121, 3137, 3163, 3167, 3169, 3181, 3187, 3191,
3203, 3209, 3217, 3221, 3229, 3251, 3253, 3257, 3259, 3271, 3299, 3301, 3307,
3313, 3319, 3323, 3329, 3331, 3343, 3347, 3359, 3361, 3371, 3373, 3389, 3391,
3407, 3413, 3433, 3449, 3457, 3461, 3463, 3467, 3469, 3491, 3499, 3511, 3517,
3527, 3529, 3533, 3539, 3541, 3547, 3557, 3559, 3571, 3581, 3583, 3593, 3607,
3613, 3617, 3623, 3631, 3637, 3643, 3659, 3671, 3673, 3677, 3691, 3697, 3701,
3709, 3719, 3727, 3733, 3739, 3761, 3767, 3769, 3779, 3793, 3797, 3803, 3821,
3823, 3833, 3847, 3851, 3853, 3863, 3877, 3881, 3889, 3907, 3911, 3917, 3919,
3923, 3929, 3931, 3943, 3947, 3967, 3989, 4001, 4003, 4007, 4013, 4019, 4021,
4027, 4049, 4051, 4057, 4073, 4079, 4091, 4093, 4099, 4111, 4127, 4129, 4133,
4139, 4153, 4157, 4159, 4177, 4201, 4211, 4217, 4219, 4229, 4231, 4241, 4243,
4253, 4259, 4261, 4271, 4273, 4283, 4289, 4297, 4327, 4337, 4339, 4349, 4357,
4363, 4373, 4391, 4397, 4409, 4421, 4423, 4441, 4447, 4451, 4457, 4463, 4481,
4483, 4493, 4507, 4513, 4517, 4519, 4523, 4547, 4549, 4561, 4567, 4583, 4591,
4597, 4603, 4621, 4637, 4639, 4643, 4649, 4651, 4657, 4663, 4673, 4679, 4691,
4703, 4721, 4723, 4729, 4733, 4751, 4759, 4783, 4787, 4789, 4793, 4799, 4801,
4813, 4817, 4831, 4861, 4871, 4877, 4889, 4903, 4909, 4919, 4931, 4933, 4937,
4943, 4951, 4957, 4967, 4969, 4973, 4987, 4993, 4999,
5003, 5009, 5011, 5021, 5023, 5039, 5051, 5059, 5077, 5081, 5087, 5099, 5101,
5107, 5113, 5119, 5147, 5153, 5167, 5171, 5179, 5189, 5197, 5209, 5227, 5231,
5233, 5237, 5261, 5273, 5279, 5281, 5297, 5303, 5309, 5323, 5333, 5347, 5351,
5381, 5387, 5393, 5399, 5407, 5413, 5417, 5419, 5431, 5437, 5441, 5443, 5449,
5471, 5477, 5479, 5483, 5501, 5503, 5507, 5519, 5521, 5527, 5531, 5557, 5563,
5569, 5573, 5581, 5591, 5623, 5639, 5641, 5647, 5651, 5653, 5657, 5659, 5669,
5683, 5689, 5693, 5701, 5711, 5717, 5737, 5741, 5743, 5749, 5779, 5783, 5791,
5801, 5807, 5813, 5821, 5827, 5839, 5843, 5849, 5851, 5857, 5861, 5867, 5869,
5879, 5881, 5897, 5903, 5923, 5927, 5939, 5953, 5981, 5987, 6007, 6011, 6029,
6037, 6043, 6047, 6053, 6067, 6073, 6079, 6089, 6091, 6101, 6113, 6121, 6131,
6133, 6143, 6151, 6163, 6173, 6197, 6199, 6203, 6211, 6217, 6221, 6229, 6247,
6257, 6263, 6269, 6271, 6277, 6287, 6299, 6301, 6311, 6317, 6323, 6329, 6337,
6343, 6353, 6359, 6361, 6367, 6373, 6379, 6389, 6397, 6421, 6427, 6449, 6451,
6469, 6473, 6481, 6491, 6521, 6529, 6547, 6551, 6553, 6563, 6569, 6571, 6577,
6581, 6599, 6607, 6619, 6637, 6653, 6659, 6661, 6673, 6679, 6689, 6691, 6701,
6703, 6709, 6719, 6733, 6737, 6761, 6763, 6779, 6781, 6791, 6793, 6803, 6823,
6827, 6829, 6833, 6841, 6857, 6863, 6869, 6871, 6883, 6899, 6907, 6911, 6917,
6947, 6949, 6959, 6961, 6967, 6971, 6977, 6983, 6991, 6997, 7001, 7013, 7019,
7027, 7039, 7043, 7057, 7069, 7079, 7103, 7109, 7121, 7127, 7129, 7151, 7159,
7177, 7187, 7193, 7207, 7211, 7213, 7219, 7229, 7237, 7243, 7247, 7253, 7283,
7297, 7307, 7309, 7321, 7331, 7333, 7349, 7351, 7369, 7393, 7411, 7417, 7433,
7451, 7457, 7459, 7477, 7481, 7487, 7489, 7499, 7507, 7517, 7523, 7529, 7537,
7541, 7547, 7549, 7559, 7561, 7573, 7577, 7583, 7589, 7591, 7603, 7607, 7621,
7639, 7643, 7649, 7669, 7673, 7681, 7687, 7691, 7699, 7703, 7717, 7723, 7727,
7741, 7753, 7757, 7759, 7789, 7793, 7817, 7823, 7829, 7841, 7853, 7867, 7873,
7877, 7879, 7883, 7901, 7907, 7919, 7927, 7933, 7937, 7949, 7951, 7963, 7993,
8009, 8011, 8017, 8039, 8053, 8059, 8069, 8081, 8087, 8089, 8093, 8101, 8111,
8117, 8123, 8147, 8161, 8167, 8171, 8179, 8191, 8209, 8219, 8221, 8231, 8233,
8237, 8243, 8263, 8269, 8273, 8287, 8291, 8293, 8297, 8311, 8317, 8329, 8353,
8363, 8369, 8377, 8387, 8389, 8419, 8423, 8429, 8431, 8443, 8447, 8461, 8467,
8501, 8513, 8521, 8527, 8537, 8539, 8543, 8563, 8573, 8581, 8597, 8599, 8609,
8623, 8627, 8629, 8641, 8647, 8663, 8669, 8677, 8681, 8689, 8693, 8699, 8707,
8713, 8719, 8731, 8737, 8741, 8747, 8753, 8761, 8779, 8783, 8803, 8807, 8819,
8821, 8831, 8837, 8839, 8849, 8861, 8863, 8867, 8887, 8893, 8923, 8929, 8933,
8941, 8951, 8963, 8969, 8971, 8999, 9001, 9007, 9011, 9013, 9029, 9041, 9043,
9049, 9059, 9067, 9091, 9103, 9109, 9127, 9133, 9137, 9151, 9157, 9161, 9173,
9181, 9187, 9199, 9203, 9209, 9221, 9227, 9239, 9241, 9257, 9277, 9281, 9283,
9293, 9311, 9319, 9323, 9337, 9341, 9343, 9349, 9371, 9377, 9391, 9397, 9403,
9413, 9419, 9421, 9431, 9433, 9437, 9439, 9461, 9463, 9467, 9473, 9479, 9491,
9497, 9511, 9521, 9533, 9539, 9547, 9551, 9587, 9601, 9613, 9619, 9623, 9629,
9631, 9643, 9649, 9661, 9677, 9679, 9689, 9697, 9719, 9721, 9733, 9739, 9743,
9749, 9767, 9769, 9781, 9787, 9791, 9803, 9811, 9817, 9829, 9833, 9839, 9851,
9857, 9859, 9871, 9883, 9887, 9901, 9907, 9923, 9929, 9931, 9941, 9949, 9967,
9973, 10007, 10009, 10037, 10039, 10061, 10067, 10069, 10079, 10091, 10093,
10099, 10103, 10111, 10133, 10139, 10141, 10151, 10159, 10163, 10169, 10177,
10181, 10193, 10211, 10223, 10243, 10247, 10253, 10259, 10267, 10271, 10273,
10289, 10301, 10303, 10313, 10321, 10331, 10333, 10337, 10343, 10357, 10369,
10391, 10399, 10427, 10429, 10433, 10453, 10457, 10459, 10463, 10477, 10487,
10499, 10501, 10513, 10529, 10531, 10559, 10567, 10589, 10597, 10601, 10607,
10613, 10627, 10631, 10639, 10651, 10657, 10663, 10667, 10687, 10691, 10709,
10711, 10723, 10729, 10733, 10739, 10753, 10771, 10781, 10789, 10799, 10831,
10837, 10847, 10853, 10859, 10861, 10867, 10883, 10889, 10891, 10903, 10909,
10937, 10939, 10949, 10957, 10973, 10979, 10987, 10993, 11003, 11027, 11047,
11057, 11059, 11069, 11071, 11083, 11087, 11093, 11113, 11117, 11119, 11131,
11149, 11159, 11161, 11171, 11173, 11177, 11197, 11213, 11239, 11243, 11251,
11257, 11261, 11273, 11279, 11287, 11299, 11311, 11317, 11321, 11329, 11351,
11353, 11369, 11383, 11393, 11399, 11411, 11423, 11437, 11443, 11447, 11467,
11471, 11483, 11489, 11491, 11497, 11503, 11519, 11527, 11549, 11551, 11579,
11587, 11593, 11597, 11617, 11621, 11633, 11657, 11677, 11681, 11689, 11699,
11701, 11717, 11719, 11731, 11743, 11777, 11779, 11783, 11789, 11801, 11807,
11813, 11821, 11827, 11831, 11833, 11839, 11863, 11867, 11887, 11897, 11903,
11909, 11923, 11927, 11933, 11939, 11941, 11953, 11959, 11969, 11971, 11981
};
// public static List<Integer> sieveOfEratosthenes(int n) {
// boolean prime[] = new boolean[n + 1];
// Arrays.fill(prime, true);
// for (int p = 2; p * p <= n; p++) {
// if (prime[p]) {
// for (int i = p * 2; i <= n; i += p) {
// prime[i] = false;
// }
// }
// }
// List<Integer> primeNumbers = new LinkedList<>();
// for (int i = 2; i <= n; i++) {
// if (prime[i]) {
// primeNumbers.add(i);
// }
// }
// return primeNumbers;
// }
}

38
src/test/resources/assertions.json

@ -89,6 +89,44 @@
1,
2
]
},
{
"title": "Normalization",
"participants": 10,
"mode": "Normalized",
"tallies": [
[ 2, 2, 2, 2, 2 ],
[ 1, 1, 1, 1, 1 ],
[ 0, 0, 5, 0, 0 ],
[ 0, 2, 0, 2, 0 ],
[ 1, 0, 0, 1, 1 ]
],
"ranks": [
3,
3,
2,
5,
1
]
},
{
"title": "Normalization with billions of Participants",
"participants": 10805010410,
"mode": "Normalized",
"tallies": [
[ 2161002082, 2161002082, 2161002082, 2161002082, 2161002082 ],
[ 1080501041, 1080501041, 1080501041, 1080501041, 1080501041 ],
[ 0, 0, 1000092263, 0, 0 ],
[ 0, 1050092837, 0, 1050092837, 0 ],
[ 980091907, 0, 0, 980091907, 980091907 ]
],
"ranks": [
3,
3,
2,
5,
1
]
}
]
Loading…
Cancel
Save