Skip to content

Commit

Permalink
Added testing for Schulze functions. (#104)
Browse files Browse the repository at this point in the history
* Added pandas and pytest to requirements.txt

* updated module name in test_main.py

* Wrote tests for schulze

* Removed dead code

* Updated license

* Updated dates
  • Loading branch information
CheatCodeSam authored Feb 5, 2025
1 parent 27f236d commit 14965cb
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 112 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ build/

**/*/node_modules

/meta/
/meta/

test/*.db
test/meta
10 changes: 6 additions & 4 deletions elekto/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@
"""

from collections import defaultdict
from typing import Dict, List, Tuple
from .types import BallotType


def schulze_d(candidates, ballots):
# Higher rank numbers receive higher preference
def schulze_d(candidates: List[str], ballots: BallotType):
d = {(V, W): 0 for V in candidates for W in candidates if V != W}
for voter in ballots.keys():
for V, Vr in ballots[voter]:
Expand All @@ -33,7 +35,7 @@ def schulze_d(candidates, ballots):
return d


def schulze_p(candidates, d):
def schulze_p(candidates: List[str], d: Dict[Tuple[str, str], int]):
p = {}
for X in candidates:
for Y in candidates:
Expand All @@ -52,7 +54,7 @@ def schulze_p(candidates, d):
return p


def schulze_rank(candidates, p, no_winners=1):
def schulze_rank(candidates: List[str], p: Dict[Tuple[str, str], int], no_winners=1):
wins = defaultdict(list)

for V in candidates:
Expand Down
13 changes: 8 additions & 5 deletions elekto/core/election.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,23 @@
#
# Author(s): Manish Sahani <[email protected]>

from typing import List
from .types import BallotType
from pandas import DataFrame
from elekto.core import schulze_d, schulze_p, schulze_rank


class Election:
def __init__(self, candidates, ballots):
def __init__(self, candidates: List[str], ballots: BallotType, no_winners=1):
self.candidates = candidates
self.ballots = ballots
self.no_winners = no_winners
self.d = {}
self.p = {}

def schulze(self):
self.d = schulze_d(self.candidates, self.ballots)
self.p = schulze_p(self.candidates, self.d)
self.ranks = schulze_rank(self.candidates, self.p)
self.ranks = schulze_rank(self.candidates, self.p, self.no_winners)

return self

Expand All @@ -46,9 +49,9 @@ def build(candidates, ballots):
return Election(candidates, pref)

@ staticmethod
def from_csv(df, no_winners):
def from_csv(df: DataFrame, no_winners: int):
candidates = list(df.columns)
ballots = {}
ballots: BallotType = {}

for v, row in df.iterrows():
ballots[v] = []
Expand Down
19 changes: 19 additions & 0 deletions elekto/core/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2024 The Elekto Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Author(s): Carson Weeks <[email protected]>

from typing import Dict, List, Tuple, TypeAlias

BallotType: TypeAlias = Dict[int, List[Tuple[str, int]]]
49 changes: 37 additions & 12 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
Flask>=2.0.3
python-dotenv>=0.19.2
requests>=2.27.1
PyYAML>=6.0
authlib>=0.15.5
SQLAlchemy==1.4.49
mysqlclient>=2.1.0
psycopg2>=2.9.3
markdown2>=2.4.2
uwsgi>=2.0.20
Flask-WTF>=1.0.0
PyNaCl>=1.5.0
authlib==1.4.0
blinker==1.9.0
certifi==2024.12.14
cffi==1.17.1
charset-normalizer==3.4.1
click==8.1.8
cryptography==44.0.0
flask==3.1.0
flask-wtf==1.2.2
greenlet==3.1.1
idna==3.10
iniconfig==2.0.0
itsdangerous==2.2.0
jinja2==3.1.5
markdown2==2.5.2
markupsafe==3.0.2
mysqlclient==2.2.7
numpy==2.2.2
packaging==24.2
pandas==2.2.3
pluggy==1.5.0
psycopg2==2.9.10
pycparser==2.22
pynacl==1.5.0
pytest==8.3.4
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
pytz==2024.2
pyyaml==6.0.2
requests==2.32.3
six==1.17.0
sqlalchemy==1.4.49
tzdata==2025.1
urllib3==2.3.0
uwsgi==2.0.28
werkzeug==3.1.3
wtforms==3.2.1
2 changes: 0 additions & 2 deletions test/.gitignore

This file was deleted.

15 changes: 15 additions & 0 deletions test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Copyright 2025 The Elekto Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Author(s): Carson Weeks <[email protected]>
103 changes: 103 additions & 0 deletions test/test_core_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# Copyright 2025 The Elekto Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Author(s): Carson Weeks <[email protected]>

import os
import sys
import pytest

from elekto.core import schulze_d, schulze_p, schulze_rank # noqa

def test_schulze_d():
candidates = ["A", "B", "C"]
ballots = {
"voter1": [("A", 3), ("B", 2), ("C", 1)],
"voter2": [("B", 3), ("C", 2), ("A", 1)]
}

expected_d = {
("A", "B"): 1,
("A", "C"): 1,
("B", "A"): 1,
("B", "C"): 2,
("C", "A"): 1,
("C", "B"): 0,
}

result = schulze_d(candidates, ballots)
print(result)
assert result == expected_d

def test_schulze_p():
candidates = ["A", "B", "C", "D"]
d = {
("A", "B"): 12, ("B", "A"): 9,
("A", "C"): 7, ("C", "A"): 14,
("A", "D"): 16, ("D", "A"): 3,
("B", "C"): 5, ("C", "B"): 10,
("B", "D"): 18, ("D", "B"): 1,
("C", "D"): 2, ("D", "C"): 20,
}

expected_p = {
("A", "B"): 12,
("B", "A"): 14,
("A", "C"): 16,
("C", "A"): 14,
("A", "D"): 16,
("D", "A"): 14,
("B", "C"): 18,
("C", "B"): 12,
("B", "D"): 18,
("D", "B"): 12,
("C", "D"): 14,
("D", "C"): 20,
}

result = schulze_p(candidates, d)
print(result)
assert result == expected_p

def test_schulze_rank_simple():
candidates = ["A", "B", "C"]
p = {
("A", "B"): 10, ("B", "A"): 5,
("A", "C"): 15, ("C", "A"): 2,
("B", "C"): 8, ("C", "B"): 3,
}

expected = [
(0, ["C"]),
(1, ["B"]),
(2, ["A"])
]

result = schulze_rank(candidates, p)
assert result == expected

def test_schulze_rank_tie():
candidates = ["A", "B", "C"]
p = {
("A", "B"): 10, ("B", "A"): 5,
("B", "C"): 10, ("C", "B"): 5,
("C", "A"): 10, ("A", "C"): 5,
}
expected = [
(1, ["A", "B", "C"])
]

result = schulze_rank(candidates, p)
assert result == expected

88 changes: 0 additions & 88 deletions test/test_main.py

This file was deleted.

0 comments on commit 14965cb

Please sign in to comment.