From f40e301d581cba333db074b681c06234d26b2f29 Mon Sep 17 00:00:00 2001 From: domi41 Date: Mon, 10 May 2021 17:51:08 +0200 Subject: [PATCH] feat: normalized tally using Least Common Multiple We need peer-reviewed tests! Implements #7 --- .../java/fr/mieuxvoter/mj/ProposalTally.java | 21 ++++--- src/main/java/fr/mieuxvoter/mj/Tally.java | 21 +++++-- .../fr/mieuxvoter/mj/TallyNormalized.java | 61 +++++++++++++++++++ .../mj/MajorityJudgmentDeliberatorTest.java | 17 +++--- src/test/resources/assertions.json | 19 ++++++ 5 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 src/main/java/fr/mieuxvoter/mj/TallyNormalized.java diff --git a/src/main/java/fr/mieuxvoter/mj/ProposalTally.java b/src/main/java/fr/mieuxvoter/mj/ProposalTally.java index a10f954..8f13f6c 100644 --- a/src/main/java/fr/mieuxvoter/mj/ProposalTally.java +++ b/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; diff --git a/src/main/java/fr/mieuxvoter/mj/Tally.java b/src/main/java/fr/mieuxvoter/mj/Tally.java index fe406ea..9087a21 100644 --- a/src/main/java/fr/mieuxvoter/mj/Tally.java +++ b/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); + } + } diff --git a/src/main/java/fr/mieuxvoter/mj/TallyNormalized.java b/src/main/java/fr/mieuxvoter/mj/TallyNormalized.java new file mode 100644 index 0000000..0a81ec0 --- /dev/null +++ b/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(); + } + +} diff --git a/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java b/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java index 14d9860..046856a 100644 --- a/src/test/java/fr/mieuxvoter/mj/MajorityJudgmentDeliberatorTest.java +++ b/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); } diff --git a/src/test/resources/assertions.json b/src/test/resources/assertions.json index 7e8deb5..94a1d8e 100644 --- a/src/test/resources/assertions.json +++ b/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 + ] } ]