Browse Source

feat: normalized tally using Least Common Multiple

We need peer-reviewed tests!

Implements #7
feat-normalized-lcm
Dominique Merle 1 year ago
parent
commit
f40e301d58
  1. 21
      src/main/java/fr/mieuxvoter/mj/ProposalTally.java
  2. 21
      src/main/java/fr/mieuxvoter/mj/Tally.java
  3. 61
      src/main/java/fr/mieuxvoter/mj/TallyNormalized.java
  4. 17
      src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java
  5. 19
      src/test/resources/assertions.json

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;

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

@ -5,9 +5,14 @@ import java.math.BigInteger;
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 +22,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 +47,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);
}
}

61
src/main/java/fr/mieuxvoter/mj/TallyNormalized.java

@ -0,0 +1,61 @@
package fr.mieuxvoter.mj;
import java.math.BigInteger;
import java.security.InvalidParameterException;
public class TallyNormalized extends Tally implements TallyInterface {
public TallyNormalized(ProposalTallyInterface[] proposalsTallies) {
super(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();
}
}

17
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;
@ -20,7 +22,7 @@ class MajorityJudgmentDeliberatorTest {
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 +33,17 @@ class MajorityJudgmentDeliberatorTest {
}
@Test
public void testUsageWithBigNumbers() {
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);
@ -56,15 +57,15 @@ class MajorityJudgmentDeliberatorTest {
public void testFromJson(JsonObject datum) {
JsonArray jsonTallies = datum.getJsonArray("tallies");
int amountOfProposals = jsonTallies.size();
Long amountOfParticipants = Long.valueOf(datum.get("participants").toString());
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();
Long[] tally = new Long[amountOfGrades];
BigInteger[] tally = new BigInteger[amountOfGrades];
for (int g = 0; g < amountOfGrades; g++) {
JsonValue amountForGrade = jsonTally.get(g);
tally[g] = Long.valueOf(amountForGrade.toString());
tally[g] = new BigInteger(amountForGrade.toString());
}
tallies[i] = new ProposalTally(tally);
}
@ -73,6 +74,8 @@ class MajorityJudgmentDeliberatorTest {
TallyInterface tally;
if ("StaticDefault".equalsIgnoreCase(mode)) {
tally = new TallyWithDefaultGrade(tallies, amountOfParticipants, datum.getInt("default"));
} else if ("Normalized".equalsIgnoreCase(mode)) {
tally = new TallyNormalized(tallies);
} else {
tally = new Tally(tallies, amountOfParticipants);
}

19
src/test/resources/assertions.json

@ -89,6 +89,25 @@
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
]
}
]
Loading…
Cancel
Save