Skip to content

Commit e2f54dd

Browse files
committed
feat(approval): improved description of approval approximation from ranked ballots
1 parent a303422 commit e2f54dd

File tree

2 files changed

+42
-10
lines changed

2 files changed

+42
-10
lines changed

abiflib/approval_tally.py

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,17 @@ def convert_to_approval_favorite_viable_half(abifmodel):
153153

154154
approval_jabmod['votelines'].append(new_vline)
155155

156+
# Calculate total ballots processed
157+
total_ballots = sum(vline['qty'] for vline in abifmodel['votelines'])
158+
156159
# Store conversion metadata for notices
157160
approval_jabmod['_conversion_meta'] = {
158161
'method': 'favorite_viable_half',
159162
'original_ballot_type': ballot_type,
160163
'viable_candidates': viable_candidates,
161-
'viable_candidate_maximum': viable_candidate_maximum
164+
'viable_candidate_maximum': viable_candidate_maximum,
165+
'total_ballots': total_ballots,
166+
'candidate_names': abifmodel.get('candidates', {})
162167
}
163168

164169
return approval_jabmod
@@ -260,25 +265,44 @@ def _generate_conversion_notices(conversion_meta):
260265
viable_candidates = conversion_meta.get('viable_candidates', [])
261266
viable_candidate_maximum = conversion_meta.get('viable_candidate_maximum', 0)
262267
original_ballot_type = conversion_meta.get('original_ballot_type', 'unknown')
268+
total_ballots = conversion_meta.get('total_ballots', 0)
269+
270+
# Get candidate display names from conversion metadata
271+
candidate_names = conversion_meta.get('candidate_names', {})
272+
273+
# Convert viable candidates to display names
274+
viable_names = []
275+
for cand_token in viable_candidates:
276+
display_name = candidate_names.get(cand_token, cand_token)
277+
viable_names.append(display_name)
263278

264-
short_text = f"Approval counts estimated from {original_ballot_type} ballots using favorite_viable_half method"
279+
short_text = f"Approval counts estimated from {total_ballots:,} {original_ballot_type} ballots using favorite_viable_half method"
265280

266281
viable_count = len(viable_candidates)
282+
# Format viable names list with proper "and" for last item
283+
if len(viable_names) > 2:
284+
viable_names_str = ", ".join(viable_names[:-1]) + f", and {viable_names[-1]}"
285+
elif len(viable_names) == 2:
286+
viable_names_str = f"{viable_names[0]} and {viable_names[1]}"
287+
else:
288+
viable_names_str = viable_names[0] if viable_names else ""
267289

268290
if (viable_count % 2) == 0:
269291
viable_paren_note = f"(half of {viable_count}). "
270292
else:
271293
viable_paren_note = f"(half of {viable_count}, rounded up). "
294+
272295
long_text = (
273296
f"The 'favorite_viable_half' conversion algorithm: find the candidate with the most "
274297
f"first preferences, and then determine the minimum number of figurative seats that would "
275298
f"need to be open in order for the candidate to exceed the Hare quota with the given first-prefs. "
276-
f"We use this to estimate how many candidates are likely to be viable candidates. "
277-
f"For this election by this calculation, {viable_count} candidates are considered viable. "
299+
f"We use this to estimate how many candidates are likely to be viable candidates.\n\n"
300+
f"Using first-choice vote totals as a rough guide, approximately {viable_count} candidates appear viable: "
301+
f"{viable_names_str}. "
278302
f"The approximation then assumes each voter approves up to {viable_candidate_maximum} "
279303
f"of their top-ranked viable candidates {viable_paren_note}"
280-
f"All candidates ranked at or above the lowest-ranked of each voter's top {viable_candidate_maximum} "
281-
f"viable candidates receive approval."
304+
f"All candidates ranked at or above the lowest-ranked of each ballot's top viable candidates receive approval "
305+
f"(considering up to {viable_candidate_maximum} viable candidates per ballot)."
282306
)
283307

284308
notices.append({

abiflib/text_output.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,18 @@ def format_notices_for_text_output(notices):
162162
result += f"\n[{notice_type}] {notice['short']}\n"
163163

164164
if notice.get('long'):
165-
# Word wrap the long notice at 78 characters
166-
wrapped = textwrap.fill(notice['long'], width=76, initial_indent=' ',
167-
subsequent_indent=' ')
168-
result += f"\n{wrapped}\n"
165+
# Handle paragraph breaks by processing each paragraph separately
166+
paragraphs = notice['long'].split('\n\n')
167+
wrapped_paragraphs = []
168+
for paragraph in paragraphs:
169+
if paragraph.strip(): # Skip empty paragraphs
170+
wrapped = textwrap.fill(paragraph.strip(), width=76,
171+
initial_indent=' ',
172+
subsequent_indent=' ')
173+
wrapped_paragraphs.append(wrapped)
174+
175+
if wrapped_paragraphs:
176+
result += f"\n" + "\n\n".join(wrapped_paragraphs) + "\n"
169177

170178
return result
171179

0 commit comments

Comments
 (0)