@snapshot-labs/snapshot.js 0.12.50 → 0.12.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/snapshot.cjs.js +117 -0
- package/dist/snapshot.esm.js +117 -0
- package/dist/snapshot.min.js +6 -6
- package/dist/src/index.d.ts +1 -0
- package/dist/src/utils.d.ts +1 -0
- package/dist/src/voting/copeland.d.ts +18 -0
- package/dist/src/voting/index.d.ts +2 -0
- package/dist/src/voting/quadratic.d.ts +4 -4
- package/dist/src/voting/types.d.ts +10 -26
- package/package.json +1 -1
- package/src/schemas/proposal.json +1 -0
- package/src/voting/__snapshots__/copeland.spec.js.snap +134 -0
- package/src/voting/copeland.spec.js +129 -0
- package/src/voting/copeland.ts +155 -0
- package/src/voting/examples/copeland.json +32 -0
- package/src/voting/index.ts +2 -0
- package/src/voting/quadratic.ts +4 -4
- package/src/voting/types.ts +8 -29
package/dist/snapshot.cjs.js
CHANGED
|
@@ -1090,6 +1090,7 @@ var definitions$1 = {
|
|
|
1090
1090
|
"approval",
|
|
1091
1091
|
"ranked-choice",
|
|
1092
1092
|
"quadratic",
|
|
1093
|
+
"copeland",
|
|
1093
1094
|
"weighted",
|
|
1094
1095
|
"custom",
|
|
1095
1096
|
"basic"
|
|
@@ -3944,6 +3945,121 @@ class RankedChoiceVoting {
|
|
|
3944
3945
|
}
|
|
3945
3946
|
}
|
|
3946
3947
|
|
|
3948
|
+
// CopelandVoting implements ranked choice voting using Copeland's method
|
|
3949
|
+
// This method compares each pair of choices and awards points based on pairwise victories
|
|
3950
|
+
class CopelandVoting {
|
|
3951
|
+
constructor(proposal, votes, strategies, selected) {
|
|
3952
|
+
this.proposal = proposal;
|
|
3953
|
+
this.votes = votes;
|
|
3954
|
+
this.strategies = strategies;
|
|
3955
|
+
this.selected = selected;
|
|
3956
|
+
}
|
|
3957
|
+
// Validates if a vote choice is valid for the given proposal
|
|
3958
|
+
// Allows partial ranking (not all choices need to be ranked)
|
|
3959
|
+
static isValidChoice(voteChoice, proposalChoices) {
|
|
3960
|
+
if (!Array.isArray(voteChoice) ||
|
|
3961
|
+
voteChoice.length === 0 ||
|
|
3962
|
+
voteChoice.length > proposalChoices.length ||
|
|
3963
|
+
new Set(voteChoice).size !== voteChoice.length) {
|
|
3964
|
+
return false;
|
|
3965
|
+
}
|
|
3966
|
+
return voteChoice.every((choice) => Number.isInteger(choice) &&
|
|
3967
|
+
choice >= 1 &&
|
|
3968
|
+
choice <= proposalChoices.length);
|
|
3969
|
+
}
|
|
3970
|
+
// Returns only the valid votes
|
|
3971
|
+
getValidVotes() {
|
|
3972
|
+
return this.votes.filter((vote) => CopelandVoting.isValidChoice(vote.choice, this.proposal.choices));
|
|
3973
|
+
}
|
|
3974
|
+
// Calculates the Copeland scores for each choice
|
|
3975
|
+
getScores() {
|
|
3976
|
+
const validVotes = this.getValidVotes();
|
|
3977
|
+
const choicesCount = this.proposal.choices.length;
|
|
3978
|
+
const pairwiseComparisons = Array.from({ length: choicesCount }, () => Array(choicesCount).fill(0));
|
|
3979
|
+
// Calculate pairwise comparisons
|
|
3980
|
+
for (const vote of validVotes) {
|
|
3981
|
+
for (let i = 0; i < vote.choice.length; i++) {
|
|
3982
|
+
for (let j = i + 1; j < vote.choice.length; j++) {
|
|
3983
|
+
const winner = vote.choice[i] - 1;
|
|
3984
|
+
const loser = vote.choice[j] - 1;
|
|
3985
|
+
pairwiseComparisons[winner][loser] += vote.balance;
|
|
3986
|
+
pairwiseComparisons[loser][winner] -= vote.balance;
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
// Calculate Copeland scores
|
|
3991
|
+
const scores = Array(choicesCount).fill(0);
|
|
3992
|
+
for (let i = 0; i < choicesCount; i++) {
|
|
3993
|
+
for (let j = 0; j < choicesCount; j++) {
|
|
3994
|
+
if (i !== j) {
|
|
3995
|
+
if (pairwiseComparisons[i][j] > 0) {
|
|
3996
|
+
scores[i]++;
|
|
3997
|
+
}
|
|
3998
|
+
else if (pairwiseComparisons[i][j] < 0) {
|
|
3999
|
+
scores[j]++;
|
|
4000
|
+
}
|
|
4001
|
+
else {
|
|
4002
|
+
scores[i] += 0.5;
|
|
4003
|
+
scores[j] += 0.5;
|
|
4004
|
+
}
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
return scores;
|
|
4009
|
+
}
|
|
4010
|
+
// Calculates the Copeland scores for each choice, broken down by strategy
|
|
4011
|
+
getScoresByStrategy() {
|
|
4012
|
+
const validVotes = this.getValidVotes();
|
|
4013
|
+
const choicesCount = this.proposal.choices.length;
|
|
4014
|
+
const strategiesCount = this.strategies.length;
|
|
4015
|
+
const pairwiseComparisons = Array.from({ length: choicesCount }, () => Array.from({ length: choicesCount }, () => Array(strategiesCount).fill(0)));
|
|
4016
|
+
// Calculate pairwise comparisons for each strategy
|
|
4017
|
+
for (const vote of validVotes) {
|
|
4018
|
+
for (let i = 0; i < vote.choice.length; i++) {
|
|
4019
|
+
for (let j = i + 1; j < vote.choice.length; j++) {
|
|
4020
|
+
const winner = vote.choice[i] - 1;
|
|
4021
|
+
const loser = vote.choice[j] - 1;
|
|
4022
|
+
for (let s = 0; s < strategiesCount; s++) {
|
|
4023
|
+
pairwiseComparisons[winner][loser][s] += vote.scores[s];
|
|
4024
|
+
pairwiseComparisons[loser][winner][s] -= vote.scores[s];
|
|
4025
|
+
}
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
// Calculate Copeland scores for each strategy
|
|
4030
|
+
const scores = Array.from({ length: choicesCount }, () => Array(strategiesCount).fill(0));
|
|
4031
|
+
for (let i = 0; i < choicesCount; i++) {
|
|
4032
|
+
for (let j = 0; j < choicesCount; j++) {
|
|
4033
|
+
if (i !== j) {
|
|
4034
|
+
for (let s = 0; s < strategiesCount; s++) {
|
|
4035
|
+
if (pairwiseComparisons[i][j][s] > 0) {
|
|
4036
|
+
scores[i][s]++;
|
|
4037
|
+
}
|
|
4038
|
+
else if (pairwiseComparisons[i][j][s] < 0) {
|
|
4039
|
+
scores[j][s]++;
|
|
4040
|
+
}
|
|
4041
|
+
else {
|
|
4042
|
+
scores[i][s] += 0.5;
|
|
4043
|
+
scores[j][s] += 0.5;
|
|
4044
|
+
}
|
|
4045
|
+
}
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
}
|
|
4049
|
+
return scores;
|
|
4050
|
+
}
|
|
4051
|
+
// Calculates the total score (sum of all valid vote balances)
|
|
4052
|
+
getScoresTotal() {
|
|
4053
|
+
return this.getValidVotes().reduce((total, vote) => total + vote.balance, 0);
|
|
4054
|
+
}
|
|
4055
|
+
// Returns a string representation of the selected choices
|
|
4056
|
+
getChoiceString() {
|
|
4057
|
+
return this.selected
|
|
4058
|
+
.map((choice) => this.proposal.choices[choice - 1])
|
|
4059
|
+
.join(', ');
|
|
4060
|
+
}
|
|
4061
|
+
}
|
|
4062
|
+
|
|
3947
4063
|
function percentageOfTotal(i, values, total) {
|
|
3948
4064
|
const reducedTotal = total.reduce((a, b) => a + b, 0);
|
|
3949
4065
|
const percent = (values[i] / reducedTotal) * 100;
|
|
@@ -4016,6 +4132,7 @@ var voting = {
|
|
|
4016
4132
|
approval: ApprovalVoting,
|
|
4017
4133
|
quadratic: QuadraticVoting,
|
|
4018
4134
|
'ranked-choice': RankedChoiceVoting,
|
|
4135
|
+
copeland: CopelandVoting,
|
|
4019
4136
|
weighted: WeightedVoting,
|
|
4020
4137
|
basic: SingleChoiceVoting
|
|
4021
4138
|
};
|
package/dist/snapshot.esm.js
CHANGED
|
@@ -1080,6 +1080,7 @@ var definitions$1 = {
|
|
|
1080
1080
|
"approval",
|
|
1081
1081
|
"ranked-choice",
|
|
1082
1082
|
"quadratic",
|
|
1083
|
+
"copeland",
|
|
1083
1084
|
"weighted",
|
|
1084
1085
|
"custom",
|
|
1085
1086
|
"basic"
|
|
@@ -3934,6 +3935,121 @@ class RankedChoiceVoting {
|
|
|
3934
3935
|
}
|
|
3935
3936
|
}
|
|
3936
3937
|
|
|
3938
|
+
// CopelandVoting implements ranked choice voting using Copeland's method
|
|
3939
|
+
// This method compares each pair of choices and awards points based on pairwise victories
|
|
3940
|
+
class CopelandVoting {
|
|
3941
|
+
constructor(proposal, votes, strategies, selected) {
|
|
3942
|
+
this.proposal = proposal;
|
|
3943
|
+
this.votes = votes;
|
|
3944
|
+
this.strategies = strategies;
|
|
3945
|
+
this.selected = selected;
|
|
3946
|
+
}
|
|
3947
|
+
// Validates if a vote choice is valid for the given proposal
|
|
3948
|
+
// Allows partial ranking (not all choices need to be ranked)
|
|
3949
|
+
static isValidChoice(voteChoice, proposalChoices) {
|
|
3950
|
+
if (!Array.isArray(voteChoice) ||
|
|
3951
|
+
voteChoice.length === 0 ||
|
|
3952
|
+
voteChoice.length > proposalChoices.length ||
|
|
3953
|
+
new Set(voteChoice).size !== voteChoice.length) {
|
|
3954
|
+
return false;
|
|
3955
|
+
}
|
|
3956
|
+
return voteChoice.every((choice) => Number.isInteger(choice) &&
|
|
3957
|
+
choice >= 1 &&
|
|
3958
|
+
choice <= proposalChoices.length);
|
|
3959
|
+
}
|
|
3960
|
+
// Returns only the valid votes
|
|
3961
|
+
getValidVotes() {
|
|
3962
|
+
return this.votes.filter((vote) => CopelandVoting.isValidChoice(vote.choice, this.proposal.choices));
|
|
3963
|
+
}
|
|
3964
|
+
// Calculates the Copeland scores for each choice
|
|
3965
|
+
getScores() {
|
|
3966
|
+
const validVotes = this.getValidVotes();
|
|
3967
|
+
const choicesCount = this.proposal.choices.length;
|
|
3968
|
+
const pairwiseComparisons = Array.from({ length: choicesCount }, () => Array(choicesCount).fill(0));
|
|
3969
|
+
// Calculate pairwise comparisons
|
|
3970
|
+
for (const vote of validVotes) {
|
|
3971
|
+
for (let i = 0; i < vote.choice.length; i++) {
|
|
3972
|
+
for (let j = i + 1; j < vote.choice.length; j++) {
|
|
3973
|
+
const winner = vote.choice[i] - 1;
|
|
3974
|
+
const loser = vote.choice[j] - 1;
|
|
3975
|
+
pairwiseComparisons[winner][loser] += vote.balance;
|
|
3976
|
+
pairwiseComparisons[loser][winner] -= vote.balance;
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
// Calculate Copeland scores
|
|
3981
|
+
const scores = Array(choicesCount).fill(0);
|
|
3982
|
+
for (let i = 0; i < choicesCount; i++) {
|
|
3983
|
+
for (let j = 0; j < choicesCount; j++) {
|
|
3984
|
+
if (i !== j) {
|
|
3985
|
+
if (pairwiseComparisons[i][j] > 0) {
|
|
3986
|
+
scores[i]++;
|
|
3987
|
+
}
|
|
3988
|
+
else if (pairwiseComparisons[i][j] < 0) {
|
|
3989
|
+
scores[j]++;
|
|
3990
|
+
}
|
|
3991
|
+
else {
|
|
3992
|
+
scores[i] += 0.5;
|
|
3993
|
+
scores[j] += 0.5;
|
|
3994
|
+
}
|
|
3995
|
+
}
|
|
3996
|
+
}
|
|
3997
|
+
}
|
|
3998
|
+
return scores;
|
|
3999
|
+
}
|
|
4000
|
+
// Calculates the Copeland scores for each choice, broken down by strategy
|
|
4001
|
+
getScoresByStrategy() {
|
|
4002
|
+
const validVotes = this.getValidVotes();
|
|
4003
|
+
const choicesCount = this.proposal.choices.length;
|
|
4004
|
+
const strategiesCount = this.strategies.length;
|
|
4005
|
+
const pairwiseComparisons = Array.from({ length: choicesCount }, () => Array.from({ length: choicesCount }, () => Array(strategiesCount).fill(0)));
|
|
4006
|
+
// Calculate pairwise comparisons for each strategy
|
|
4007
|
+
for (const vote of validVotes) {
|
|
4008
|
+
for (let i = 0; i < vote.choice.length; i++) {
|
|
4009
|
+
for (let j = i + 1; j < vote.choice.length; j++) {
|
|
4010
|
+
const winner = vote.choice[i] - 1;
|
|
4011
|
+
const loser = vote.choice[j] - 1;
|
|
4012
|
+
for (let s = 0; s < strategiesCount; s++) {
|
|
4013
|
+
pairwiseComparisons[winner][loser][s] += vote.scores[s];
|
|
4014
|
+
pairwiseComparisons[loser][winner][s] -= vote.scores[s];
|
|
4015
|
+
}
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
// Calculate Copeland scores for each strategy
|
|
4020
|
+
const scores = Array.from({ length: choicesCount }, () => Array(strategiesCount).fill(0));
|
|
4021
|
+
for (let i = 0; i < choicesCount; i++) {
|
|
4022
|
+
for (let j = 0; j < choicesCount; j++) {
|
|
4023
|
+
if (i !== j) {
|
|
4024
|
+
for (let s = 0; s < strategiesCount; s++) {
|
|
4025
|
+
if (pairwiseComparisons[i][j][s] > 0) {
|
|
4026
|
+
scores[i][s]++;
|
|
4027
|
+
}
|
|
4028
|
+
else if (pairwiseComparisons[i][j][s] < 0) {
|
|
4029
|
+
scores[j][s]++;
|
|
4030
|
+
}
|
|
4031
|
+
else {
|
|
4032
|
+
scores[i][s] += 0.5;
|
|
4033
|
+
scores[j][s] += 0.5;
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
return scores;
|
|
4040
|
+
}
|
|
4041
|
+
// Calculates the total score (sum of all valid vote balances)
|
|
4042
|
+
getScoresTotal() {
|
|
4043
|
+
return this.getValidVotes().reduce((total, vote) => total + vote.balance, 0);
|
|
4044
|
+
}
|
|
4045
|
+
// Returns a string representation of the selected choices
|
|
4046
|
+
getChoiceString() {
|
|
4047
|
+
return this.selected
|
|
4048
|
+
.map((choice) => this.proposal.choices[choice - 1])
|
|
4049
|
+
.join(', ');
|
|
4050
|
+
}
|
|
4051
|
+
}
|
|
4052
|
+
|
|
3937
4053
|
function percentageOfTotal(i, values, total) {
|
|
3938
4054
|
const reducedTotal = total.reduce((a, b) => a + b, 0);
|
|
3939
4055
|
const percent = (values[i] / reducedTotal) * 100;
|
|
@@ -4006,6 +4122,7 @@ var voting = {
|
|
|
4006
4122
|
approval: ApprovalVoting,
|
|
4007
4123
|
quadratic: QuadraticVoting,
|
|
4008
4124
|
'ranked-choice': RankedChoiceVoting,
|
|
4125
|
+
copeland: CopelandVoting,
|
|
4009
4126
|
weighted: WeightedVoting,
|
|
4010
4127
|
basic: SingleChoiceVoting
|
|
4011
4128
|
};
|