Merge pull request #4 from MieuxVoter/issue-3

Prepare for a more exhaustive test-suite, using JSON
pull/8/head
Dominique Merle 3 years ago committed by GitHub
commit ba83ad8224
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -19,7 +19,8 @@ repositories {
dependencies {
// Use the JUnit test framework with assertions and benchmarks
testImplementation 'org.junit.jupiter:junit-jupiter:5.6.3'
testImplementation 'org.openjdk.jmh:jmh-generator-annprocess:1.21'
testImplementation 'net.joshka:junit-json-params:1.1.0'
//testImplementation 'org.openjdk.jmh:jmh-generator-annprocess:1.21'
// This dependency is exported to consumers, that is to say found on their compile classpath.
//api 'org.apache.commons:commons-math3:3.6.1'

@ -6,7 +6,7 @@
<groupId>fr.mieuxvoter.mj</groupId>
<artifactId>majority-judgment</artifactId>
<version>0.1.0</version>
<version>0.1.1</version>
<name>majority-judgment</name>
<url>https://mieuxvoter.fr</url>
@ -24,7 +24,18 @@
<version>5.6.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.joshka</groupId>
<artifactId>junit-json-params</artifactId>
<version>1.1.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>javax.json</artifactId>
<version>1.1.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

@ -1,5 +1,6 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
import java.util.Arrays;
import java.util.Comparator;
@ -24,7 +25,7 @@ public class MajorityJudgmentDeliberator implements DeliberatorInterface {
public ResultInterface deliberate(TallyInterface tally) {
ProposalTallyInterface[] tallies = tally.getProposalsTallies();
Long amountOfJudges = tally.getAmountOfJudges();
BigInteger amountOfJudges = tally.getAmountOfJudges();
Integer amountOfProposals = tally.getAmountOfProposals();
Result result = new Result();
@ -69,13 +70,13 @@ public class MajorityJudgmentDeliberator implements DeliberatorInterface {
return result;
}
public String computeScore(ProposalTallyInterface tally, Long amountOfJudges) {
public String computeScore(ProposalTallyInterface tally, BigInteger amountOfJudges) {
return computeScore(tally, amountOfJudges, true, false);
}
public String computeScore(
ProposalTallyInterface tally,
Long amountOfJudges,
BigInteger amountOfJudges,
Boolean favorContestation,
Boolean onlyNumbers
) {
@ -100,14 +101,18 @@ public class MajorityJudgmentDeliberator implements DeliberatorInterface {
analysis.getMedianGrade()
);
if (! onlyNumbers) {
if ( ! onlyNumbers) {
score += "_";
}
score += String.format(
"%0"+digitsForGroup+"d",
amountOfJudges + analysis.getSecondMedianGroupSize() * analysis.getSecondMedianGroupSign()
// amountOfJudges + secondMedianGroupSize * secondMedianGroupSign
amountOfJudges.add(
analysis.getSecondMedianGroupSize().multiply(
BigInteger.valueOf(analysis.getSecondMedianGroupSign())
)
)
);
currentTally.moveJudgments(analysis.getMedianGrade(), analysis.getSecondMedianGrade());

@ -1,33 +1,43 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
import java.util.Arrays;
public class ProposalTally implements ProposalTallyInterface {
protected Long[] tally;
protected BigInteger[] tally;
// Should we allow this as well?
//public ProposalTally() {}
public ProposalTally(Integer[] tally) {
int tallyLength = tally.length;
Long[] doublesTally = new Long[tallyLength];
BigInteger[] bigTally = new BigInteger[tallyLength];
for (int i = 0 ; i < tallyLength ; i++) {
doublesTally[i] = Long.valueOf(tally[i]);
bigTally[i] = BigInteger.valueOf(tally[i]);
}
setTally(doublesTally);
setTally(bigTally);
}
public ProposalTally(Long[] tally) {
int tallyLength = tally.length;
BigInteger[] bigTally = new BigInteger[tallyLength];
for (int i = 0 ; i < tallyLength ; i++) {
bigTally[i] = BigInteger.valueOf(tally[i]);
}
setTally(bigTally);
}
public ProposalTally(BigInteger[] tally) {
setTally(tally);
}
public void setTally(Long[] tally) {
public void setTally(BigInteger[] tally) {
this.tally = tally;
}
@Override
public Long[] getTally() {
public BigInteger[] getTally() {
return this.tally;
}
@ -38,8 +48,19 @@ public class ProposalTally implements ProposalTallyInterface {
@Override
public void moveJudgments(Integer fromGrade, Integer intoGrade) {
this.tally[intoGrade] += this.tally[fromGrade];
this.tally[fromGrade] = 0L;
// this.tally[intoGrade] += this.tally[fromGrade];
this.tally[intoGrade] = this.tally[intoGrade].add(this.tally[fromGrade]);
this.tally[fromGrade] = BigInteger.ZERO;
}
@Override
public BigInteger getAmountOfJudgments() {
BigInteger sum = BigInteger.ZERO;
int tallyLength = this.tally.length;
for (int i = 0 ; i < tallyLength ; i++) {
sum = sum.add(this.tally[i]);
}
return sum;
}
}

@ -1,31 +1,38 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
/**
* Collect useful data on a proposal tally.
* Does NOT compute the rank, but provides all we need
* Does NOT compute the rank, but provides all we need.
*
* This uses BigInteger because in a normalization scenario we use the
* smallest common multiple of the amounts of judges of proposals.
* It makes the code harder to read and understand, but it allows us
* to bypass the floating-point nightmare of the normalization of merit profiles,
* which is one way to handle default grades on some polls.
*/
public class ProposalTallyAnalysis {
protected ProposalTallyInterface tally;
protected Long totalSize = 0L; // amount of judges
protected BigInteger totalSize = BigInteger.ZERO; // amount of judges
protected Integer medianGrade = 0;
protected Long medianGroupSize = 0L; // amount of judges in the median group
protected BigInteger medianGroupSize = BigInteger.ZERO; // amount of judges in the median group
protected Integer contestationGrade = 0; // "best" grade of the contestation group
protected Long contestationGroupSize = 0L; // of lower grades than median
protected BigInteger contestationGroupSize = BigInteger.ZERO; // of lower grades than median
protected Integer adhesionGrade = 0; // "worst" grade of the adhesion group
protected Long adhesionGroupSize = 0L; // of higher grades than median
protected BigInteger adhesionGroupSize = BigInteger.ZERO; // of higher grades than median
protected Integer secondMedianGrade = 0; // grade of the biggest group out of the median
protected Long secondMedianGroupSize = 0L; // either contestation or adhesion
protected BigInteger secondMedianGroupSize = BigInteger.ZERO; // either contestation or adhesion
protected Integer secondMedianGroupSign = 0; // -1 for contestation, +1 for adhesion, 0 for empty group size
@ -42,56 +49,58 @@ public class ProposalTallyAnalysis {
public void reanalyze(ProposalTallyInterface tally, Boolean favorContestation) {
this.tally = tally;
this.totalSize = 0L;
this.totalSize = BigInteger.ZERO;
this.medianGrade = 0;
this.medianGroupSize = 0L;
this.medianGroupSize = BigInteger.ZERO;
this.contestationGrade = 0;
this.contestationGroupSize = 0L;
this.contestationGroupSize = BigInteger.ZERO;
this.adhesionGrade = 0;
this.adhesionGroupSize = 0L;
this.adhesionGroupSize = BigInteger.ZERO;
this.secondMedianGrade = 0;
this.secondMedianGroupSize = 0L;
this.secondMedianGroupSize = BigInteger.ZERO;
this.secondMedianGroupSign = 0;
Long[] gradesTallies = this.tally.getTally();
BigInteger[] gradesTallies = this.tally.getTally();
int amountOfGrades = gradesTallies.length;
for (int grade = 0; grade < amountOfGrades; grade++) {
Long gradeTally = gradesTallies[grade];
assert(0 <= gradeTally); // Negative tallies are not allowed.
this.totalSize += gradeTally;
BigInteger gradeTally = gradesTallies[grade];
//assert(0 <= gradeTally); // Negative tallies are not allowed.
this.totalSize = this.totalSize.add(gradeTally);
}
Integer medianOffset = 1;
if ( ! favorContestation) {
medianOffset = 2;
}
Long medianCursor = (long) Math.floor((this.totalSize + medianOffset) / 2.0);
BigInteger medianCursor = this.totalSize.add(BigInteger.valueOf(medianOffset)).divide(BigInteger.TWO);
// Long medianCursor = (long) Math.floor((this.totalSize + medianOffset) / 2.0);
Long tallyBeforeCursor = 0L;
Long tallyCursor = 0L;
BigInteger tallyBeforeCursor = BigInteger.ZERO;
BigInteger tallyCursor = BigInteger.ZERO;
Boolean foundMedian = false;
Integer contestationGrade = 0;
Integer adhesionGrade = 0;
for (int grade = 0; grade < amountOfGrades; grade++) {
Long gradeTally = gradesTallies[grade];
BigInteger gradeTally = gradesTallies[grade];
tallyBeforeCursor = tallyCursor;
tallyCursor += gradeTally;
tallyCursor = tallyCursor.add(gradeTally);
if ( ! foundMedian) {
if (tallyCursor >= medianCursor) {
if (-1 < tallyCursor.compareTo(medianCursor)) { // tallyCursor >= medianCursor
foundMedian = true;
this.medianGrade = grade;
this.contestationGroupSize = tallyBeforeCursor;
this.medianGroupSize = gradeTally;
this.adhesionGroupSize = this.totalSize - this.contestationGroupSize - this.medianGroupSize;
// this.adhesionGroupSize = this.totalSize - this.contestationGroupSize - this.medianGroupSize;
this.adhesionGroupSize = this.totalSize.subtract(this.contestationGroupSize).subtract(this.medianGroupSize);
} else {
if (0 < gradeTally) {
if (1 == gradeTally.compareTo(BigInteger.ZERO)) { // 0 < gradeTally
contestationGrade = grade;
}
}
} else {
if (0 < gradeTally && 0 == adhesionGrade) {
if (1 == gradeTally.compareTo(BigInteger.ZERO) && 0 == adhesionGrade) {
adhesionGrade = grade;
}
}
@ -99,12 +108,15 @@ public class ProposalTallyAnalysis {
this.contestationGrade = contestationGrade;
this.adhesionGrade = adhesionGrade;
this.secondMedianGroupSize = Math.max(this.contestationGroupSize, this.adhesionGroupSize);
// this.secondMedianGroupSize = Math.max(this.contestationGroupSize, this.adhesionGroupSize);
this.secondMedianGroupSize = this.contestationGroupSize.max(this.adhesionGroupSize);
this.secondMedianGroupSign = 0;
if (this.contestationGroupSize < this.adhesionGroupSize) {
// if (this.contestationGroupSize < this.adhesionGroupSize) {
if (1 == this.adhesionGroupSize.compareTo(this.contestationGroupSize)) {
this.secondMedianGrade = this.adhesionGrade;
this.secondMedianGroupSign = 1;
} else if (this.contestationGroupSize > this.adhesionGroupSize) {
// } else if (this.contestationGroupSize > this.adhesionGroupSize) {
} else if (1 == this.contestationGroupSize.compareTo(this.adhesionGroupSize)) {
this.secondMedianGrade = this.contestationGrade;
this.secondMedianGroupSign = -1;
} else {
@ -116,16 +128,16 @@ public class ProposalTallyAnalysis {
this.secondMedianGroupSign = 1;
}
}
if (0 == this.secondMedianGroupSize) {
if (0 == this.secondMedianGroupSize.compareTo(BigInteger.ZERO)) {
this.secondMedianGroupSign = 0;
}
}
public Long getTotalSize() {
public BigInteger getTotalSize() {
return totalSize;
}
public void setTotalSize(Long totalSize) {
public void setTotalSize(BigInteger totalSize) {
this.totalSize = totalSize;
}
@ -137,11 +149,11 @@ public class ProposalTallyAnalysis {
this.medianGrade = medianGrade;
}
public Long getMedianGroupSize() {
public BigInteger getMedianGroupSize() {
return medianGroupSize;
}
public void setMedianGroupSize(Long medianGroupSize) {
public void setMedianGroupSize(BigInteger medianGroupSize) {
this.medianGroupSize = medianGroupSize;
}
@ -153,11 +165,11 @@ public class ProposalTallyAnalysis {
this.contestationGrade = contestationGrade;
}
public Long getContestationGroupSize() {
public BigInteger getContestationGroupSize() {
return contestationGroupSize;
}
public void setContestationGroupSize(Long contestationGroupSize) {
public void setContestationGroupSize(BigInteger contestationGroupSize) {
this.contestationGroupSize = contestationGroupSize;
}
@ -169,11 +181,11 @@ public class ProposalTallyAnalysis {
this.adhesionGrade = adhesionGrade;
}
public Long getAdhesionGroupSize() {
public BigInteger getAdhesionGroupSize() {
return adhesionGroupSize;
}
public void setAdhesionGroupSize(Long adhesionGroupSize) {
public void setAdhesionGroupSize(BigInteger adhesionGroupSize) {
this.adhesionGroupSize = adhesionGroupSize;
}
@ -185,11 +197,11 @@ public class ProposalTallyAnalysis {
this.secondMedianGrade = secondMedianGrade;
}
public Long getSecondMedianGroupSize() {
public BigInteger getSecondMedianGroupSize() {
return secondMedianGroupSize;
}
public void setSecondMedianGroupSize(Long secondMedianGroupSize) {
public void setSecondMedianGroupSize(BigInteger secondMedianGroupSize) {
this.secondMedianGroupSize = secondMedianGroupSize;
}

@ -1,11 +1,22 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
public interface ProposalTallyInterface {
/**
* The amount of judgments received for each Grade, from "worst" Grade to "best" Grade.
* The tallies of each Grade, that is
* the amount of judgments received for each Grade by the Proposal,
* from "worst" ("most conservative") Grade to "best" Grade.
*/
public BigInteger[] getTally();
/**
* Should be the sum of getTally()
*
* @return The total amount of judgments received by this proposal.
*/
public Long[] getTally();
public BigInteger getAmountOfJudgments();
/**
* Homemade factory to skip the clone() shenanigans.

@ -1,19 +1,26 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
public class Tally implements TallyInterface {
protected ProposalTallyInterface[] proposalsTallies;
protected Long amountOfJudges = 0L;
protected BigInteger amountOfJudges = BigInteger.ZERO;
public Tally(ProposalTallyInterface[] proposalsTallies, Long amountOfJudges) {
public Tally(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges) {
setProposalsTallies(proposalsTallies);
setAmountOfJudges(amountOfJudges);
}
public Tally(ProposalTallyInterface[] proposalsTallies, Long amountOfJudges) {
setProposalsTallies(proposalsTallies);
setAmountOfJudges(BigInteger.valueOf(amountOfJudges));
}
public Tally(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges) {
setProposalsTallies(proposalsTallies);
setAmountOfJudges(Long.valueOf(amountOfJudges));
setAmountOfJudges(BigInteger.valueOf(amountOfJudges));
}
public ProposalTallyInterface[] getProposalsTallies() {
@ -28,11 +35,11 @@ public class Tally implements TallyInterface {
return proposalsTallies.length;
}
public Long getAmountOfJudges() {
public BigInteger getAmountOfJudges() {
return amountOfJudges;
}
public void setAmountOfJudges(Long amountOfJudges) {
public void setAmountOfJudges(BigInteger amountOfJudges) {
this.amountOfJudges = amountOfJudges;
}

@ -1,10 +1,12 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
public interface TallyInterface {
public ProposalTallyInterface[] getProposalsTallies();
public Long getAmountOfJudges();
public BigInteger getAmountOfJudges();
public Integer getAmountOfProposals();

@ -0,0 +1,41 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
public class TallyWithDefaultGrade extends Tally implements TallyInterface {
protected Integer defaultGrade = 0;
public TallyWithDefaultGrade(ProposalTallyInterface[] proposalsTallies, BigInteger amountOfJudges, Integer defaultGrade) {
super(proposalsTallies, amountOfJudges);
this.defaultGrade = defaultGrade;
fillWithDefaultGrade();
}
public TallyWithDefaultGrade(ProposalTallyInterface[] proposalsTallies, Long amountOfJudges, Integer defaultGrade) {
super(proposalsTallies, amountOfJudges);
this.defaultGrade = defaultGrade;
fillWithDefaultGrade();
}
public TallyWithDefaultGrade(ProposalTallyInterface[] proposalsTallies, Integer amountOfJudges, Integer defaultGrade) {
super(proposalsTallies, amountOfJudges);
this.defaultGrade = defaultGrade;
fillWithDefaultGrade();
}
protected void fillWithDefaultGrade() {
int amountOfProposals = getAmountOfProposals();
for (int i = 0 ; i < amountOfProposals ; i++) {
ProposalTallyInterface proposal = getProposalsTallies()[i];
BigInteger amountOfJudgments = proposal.getAmountOfJudgments();
BigInteger missingAmount = this.amountOfJudges.subtract(amountOfJudgments);
int missingSign = missingAmount.compareTo(BigInteger.ZERO);
assert(0 <= missingSign); // ERROR: More judgments than judges!
if (0 < missingSign) {
proposal.getTally()[this.defaultGrade] = proposal.getTally()[this.defaultGrade].add(missingAmount);
}
}
}
}

@ -2,21 +2,27 @@ package fr.mieuxvoter.mj;
import static org.junit.jupiter.api.Assertions.*;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonValue;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import net.joshka.junit.json.params.JsonFileSource;
class MajorityJudgmentDeliberatorTest {
@Test
void testDemoUsage() {
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);
// System.out.println("Score 0: "+result.getProposalResults()[0].getScore());
// System.out.println("Score 1: "+result.getProposalResults()[1].getScore());
ResultInterface result = mj.deliberate(tally);
assertNotNull(result);
assertEquals(2, result.getProposalResults().length);
@ -25,7 +31,7 @@ class MajorityJudgmentDeliberatorTest {
}
@Test
void testUsageWithBigNumbers() {
public void testUsageWithBigNumbers() {
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
TallyInterface tally = new Tally(new ProposalTallyInterface[] {
new ProposalTally(new Long[]{11312415004L, 21153652410L, 24101523299L, 18758623562L}),
@ -44,4 +50,70 @@ 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());
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);
}
DeliberatorInterface mj = new MajorityJudgmentDeliberator();
TallyInterface tally = new Tally(tallies, amountOfParticipants);
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
public void testWithStaticDefaultGrade() {
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);
ResultInterface result = mj.deliberate(tally);
assertNotNull(result);
assertEquals(2, result.getProposalResults().length);
assertEquals(2, result.getProposalResults()[0].getRank());
assertEquals(1, result.getProposalResults()[1].getRank());
}
// @Test
// public void runBenchmarks() throws Exception {
// Options options = new OptionsBuilder()
// .include(this.getClass().getName() + ".*")
// .mode(Mode.AverageTime)
// .warmupTime(TimeValue.seconds(1))
// .warmupIterations(6)
// .threads(1)
// .measurementIterations(6)
// .forks(1)
// .shouldFailOnError(true)
// .shouldDoGC(true)
// .build();
//
// new Runner(options).run();
// }
}

@ -0,0 +1,16 @@
Do add your test cases in the JSON file.
Some of the sample tallies were made using python.
```python
import numpy as np
def randofsum_unbalanced(s, n):
# Where s = sum (e.g. 40 in your case) and n is the output array length (e.g. 4 in your case)
r = np.random.rand(n)
a = np.array(np.round((r/np.sum(r))*s,0),dtype=int)
while np.sum(a) > s:
a[np.random.choice(n)] -= 1
while np.sum(a) < s:
a[np.random.choice(n)] += 1
return a
```

@ -0,0 +1,54 @@
[
{
"title": "Few participants",
"participants": 3,
"tallies": [
[1, 1, 1],
[1, 0, 2],
[3, 0, 0],
[2, 0, 1],
[0, 3, 0]
],
"ranks": [
3,
1,
5,
4,
2
]
},
{
"title": "Thousands of participants",
"participants": 37000,
"tallies": [
[11142, 6970, 4040, 1968, 9888, 2992],
[10141, 8971, 4043, 1965, 8884, 2996],
[14141, 8971, 1043, 1965, 7884, 2996]
],
"ranks": [
1,
2,
3
]
},
{
"title": "Millions of participants",
"participants": 72327456,
"tallies": [
[5272679, 19797001, 10732688, 9612936, 1379840, 16886281, 8646031],
[16354546, 11690342, 9451800, 14245973, 817593, 12461162, 7306040],
[9849171, 17970690, 14276861, 4606692, 16404594, 6760147, 2459301],
[2645563, 12907474, 1278331, 22843261, 8025412, 8964952, 15662463],
[16293252, 12277630, 38348, 14929905, 11087753, 10634266, 7066302]
],
"ranks": [
3,
4,
5,
1,
2
]
}
]
Loading…
Cancel
Save