/
sort.py
190 lines (157 loc) · 6.42 KB
/
sort.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
# -*- coding:utf-8 -*-
# This file is part of tally-pipes.
# Copyright (C) 2014-2016 Sequent Tech Inc <legal@sequentech.io>
# tally-pipes is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published by
# the Free Software Foundation, either version 3 of the License.
# tally-pipes is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
# You should have received a copy of the GNU Affero General Public License
# along with tally-pipes. If not, see <http://www.gnu.org/licenses/>.
import json
from itertools import groupby, chain
from operator import itemgetter
_MAX = 999999999
_ALLOWED_TALLY_TYPES = [
"cumulative",
"plurality-at-large",
"desborda3",
"desborda2",
"desborda",
"borda",
"borda-nauru",
"pairwise-beta",
"cup"
]
def sort_non_iterative(
data_list,
tallies_indexes=[0],
question_indexes=None,
withdrawals=[],
ties_sorting=[],
help=""
):
'''
Sort non iterative questions of the first tally by total_count
- question_indexes are the questions this is applied to
- withdrawals is a list of answer items that have been withdrawed.
- ties_sorting is a list of answer items in order
An answer item follows this format:
{"question_index": 0, "answer_text": "Foo", "answer_id": 0}
'''
for dindex, data in enumerate(data_list):
if tallies_indexes is not None and dindex not in tallies_indexes:
continue
data = data_list[dindex]
# append already listed withdrawals
if 'withdrawals' in data:
withdrawals = withdrawals + data['withdrawals']
for q_num, question in enumerate(data['results']['questions']):
# filter first
if question['tally_type'] not in _ALLOWED_TALLY_TYPES or\
(
question_indexes is not None and\
q_num not in question_indexes
):
continue
# apply removals
q_removed = []
if "removed-candidates" in data:
q_removed = [
removed['answer_id']
for removed in data["removed-candidates"]
if removed['question_index'] == q_num]
question['answers'][:] = [
answer
for answer in question['answers']
if answer['id'] not in q_removed]
q_withdrawals = list(filter(lambda w: w['question_index'] == q_num, withdrawals))
q_withdrawals_ids = list(map(lambda w: w['answer_id'], q_withdrawals))
q_ties_sorting = list(filter(lambda w: w['question_index'] == q_num, ties_sorting))
# add default tie sort
for answer in question['answers']:
answer['tie_sort'] = 0
# add tie sort index to each answer
for i, item in enumerate(q_ties_sorting):
item2 = question['answers'][item['answer_id']]
# first do some checks
assert item2['id'] == item['answer_id']
assert item2['text'] == item['answer_text']
# reverse numbering, to be compatible with total_count sorting
item2['tie_sort'] = len(q_ties_sorting) - i
# sanity check withdrawals
for item in q_withdrawals:
if item['answer_id'] in q_removed:
continue
item2 = None
for item_find in question['answers']:
if item_find['id'] == item['answer_id']:
item2 = item_find
break
# first do some checks
assert item2 is not None
assert item2['text'] == item['answer_text']
# first sort by id, to have a stable sort
question['answers'] = sorted(question['answers'], key=itemgetter('id'))
# then sort by total_count, resolving ties too
question['answers'] = sorted(question['answers'], reverse=True,
key=itemgetter('total_count', 'tie_sort'))
# sort by withdrawn/not withdrawn if possible
if 'withdraw_candidates' in question and question['withdraw_candidates']:
question['answers'] = sorted(question['answers'],
key=itemgetter('withdrawn'))
# mark winners
i = 0
for answer in question['answers']:
if answer['id'] in q_withdrawals_ids or i >= question['num_winners']:
answer['winner_position'] = _MAX
else:
answer['winner_position'] = i
i += 1
# final sort based on winners
question['answers'] = sorted(
question['answers'],
key=itemgetter('winner_position'))
# remove temp data
for answer in question['answers']:
del answer['tie_sort']
if answer['winner_position'] is _MAX:
answer['winner_position'] = None
def sort_non_iterative_losers(
data_list,
tallies_indexes=[0],
question_indexes=None,
help=""
):
'''
Sorts losers by points, not touching the winners' positions
'''
for dindex, data in enumerate(data_list):
if tallies_indexes is not None and dindex not in tallies_indexes:
continue
data = data_list[dindex]
for q_num, question in enumerate(data['results']['questions']):
# filter first
if question['tally_type'] not in _ALLOWED_TALLY_TYPES or\
(
question_indexes is not None and\
q_num not in question_indexes
):
continue
winners = [
winner
for winner in question['answers']
if winner['winner_position'] is not None
]
losers = [
loser
for loser in question['answers']
if loser['winner_position'] is None
]
sorted_losers = sorted(
losers,
key=itemgetter('total_count')
)
question['answers'] = winers + sorted_losers