Skip to content

Commit ac71f26

Browse files
authored
Made sure apportionment works with non-consecutive list/candidate numbers (#2807)
1 parent 718964b commit ac71f26

File tree

7 files changed

+360
-67
lines changed

7 files changed

+360
-67
lines changed

backend/apportionment/src/candidate_nomination/mod.rs

Lines changed: 132 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,12 @@ mod structs;
33
use tracing::{debug, info};
44

55
use self::structs::{Candidate, ListCandidateNomination, PreferenceThreshold};
6-
pub use structs::CandidateNominationResult;
7-
86
use super::{
97
ApportionmentError, ApportionmentInput, CandidateVotesTrait, ListVotesTrait,
108
fraction::Fraction,
11-
structs::{CandidateNominationInputType, CandidateNumber, LARGE_COUNCIL_THRESHOLD},
9+
structs::{CandidateNominationInputType, CandidateNumber, LARGE_COUNCIL_THRESHOLD, ListNumber},
1210
};
11+
pub use structs::CandidateNominationResult;
1312

1413
/// Candidate nomination
1514
pub(crate) fn candidate_nomination<'a, T: ApportionmentInput>(
@@ -97,12 +96,16 @@ fn candidate_nomination_per_list<'a, T: ListVotesTrait>(
9796
seats: u32,
9897
list_votes: &'a [T],
9998
preference_threshold: Fraction,
100-
total_seats: &[u32],
99+
total_seats: &[(ListNumber, u32)],
101100
) -> Result<Vec<ListCandidateNomination<'a, T::Cv>>, ApportionmentError> {
102101
let mut list_candidate_nomination: Vec<ListCandidateNomination<T::Cv>> = vec![];
103-
for (index, list) in list_votes.iter().enumerate() {
104-
let list_seats = total_seats[index];
105-
let candidate_votes = list.candidate_votes();
102+
for list in list_votes {
103+
let (list_number, list_seats) = total_seats
104+
.iter()
105+
.find(|(number, _)| *number == list.number())
106+
.expect("Total seats exists")
107+
.to_owned();
108+
let candidate_votes = &list.candidate_votes();
106109
let candidate_votes_meeting_preference_threshold =
107110
candidate_votes_meeting_preference_threshold(preference_threshold, candidate_votes);
108111
let preferential_candidate_nomination = preferential_candidate_nomination::<T::Cv>(
@@ -142,8 +145,8 @@ fn candidate_nomination_per_list<'a, T: ListVotesTrait>(
142145
}
143146
};
144147

145-
list_candidate_nomination.push(ListCandidateNomination::<T::Cv> {
146-
list_number: list.number(),
148+
list_candidate_nomination.push(ListCandidateNomination {
149+
list_number,
147150
list_seats,
148151
preferential_candidate_nomination,
149152
other_candidate_nomination,
@@ -267,8 +270,10 @@ mod tests {
267270
structs::ListNumber,
268271
test_helpers::{
269272
ApportionmentInputMock, CandidateVotesMock,
273+
candidate_nomination_fixture_with_given_list_numbers_and_number_of_seats,
270274
candidate_nomination_fixture_with_given_number_of_seats,
271275
seat_assignment_fixture_with_given_candidate_votes,
276+
seat_assignment_fixture_with_given_list_numbers_candidate_numbers_and_votes,
272277
},
273278
};
274279

@@ -341,6 +346,124 @@ mod tests {
341346
(chosen_candidates, not_chosen_candidates)
342347
}
343348

349+
/// Candidate nomination with non-consecutive list and candidate numbers
350+
///
351+
/// List seats: [(1, 8), (2, 3), (4, 2), (5, 1), (7, 1)]
352+
/// List 1: Preferential candidate nominations of candidates 1, 4, 3, 5 and 12 and other candidate nominations of candidates 7, 8 and 9
353+
/// List 2: Preferential candidate nomination of candidate 2 and 6 and other candidate nomination of candidates 3
354+
/// List 3: Preferential candidate nomination of candidate 1 and 4 and no other candidate nominations
355+
/// List 4: Preferential candidate nomination of candidate 1 and no other candidate nominations
356+
/// List 5: Preferential candidate nomination of candidate 3 and no other candidate nominations
357+
#[test]
358+
fn test_with_lt_19_seats_and_non_consecutive_list_and_candidate_numbers() {
359+
let quota = Fraction::new(5104, 15);
360+
let seat_assignment_input =
361+
seat_assignment_fixture_with_given_list_numbers_candidate_numbers_and_votes(
362+
15,
363+
vec![
364+
(
365+
1,
366+
vec![
367+
(1, 1069),
368+
(3, 303),
369+
(4, 321),
370+
(5, 210),
371+
(7, 36),
372+
(8, 101),
373+
(9, 79),
374+
(10, 121),
375+
(11, 150),
376+
(12, 181),
377+
],
378+
),
379+
(2, vec![(2, 452), (3, 39), (4, 81), (6, 274), (7, 131)]),
380+
(4, vec![(1, 229), (2, 147), (4, 191)]),
381+
(5, vec![(1, 347), (3, 189)]),
382+
(7, vec![(3, 266), (2, 187)]),
383+
],
384+
);
385+
let input = candidate_nomination_fixture_with_given_list_numbers_and_number_of_seats(
386+
quota,
387+
&seat_assignment_input,
388+
vec![(1, 8), (2, 3), (4, 2), (5, 1), (7, 1)],
389+
);
390+
let result = candidate_nomination::<ApportionmentInputMock>(&input).unwrap();
391+
392+
assert_eq!(result.preference_threshold.percentage, 50);
393+
assert_eq!(
394+
result.preference_threshold.number_of_votes,
395+
quota * Fraction::new(result.preference_threshold.percentage, 100)
396+
);
397+
check_list_candidate_nomination(
398+
&result.list_candidate_nomination[0],
399+
&[1, 4, 3, 5, 12],
400+
&[7, 8, 9],
401+
&[1, 4, 3, 5, 12, 7, 8, 9, 10, 11],
402+
);
403+
check_list_candidate_nomination(
404+
&result.list_candidate_nomination[1],
405+
&[2, 6],
406+
&[3],
407+
&[2, 6, 3, 4, 7],
408+
);
409+
check_list_candidate_nomination(
410+
&result.list_candidate_nomination[2],
411+
&[1, 4],
412+
&[],
413+
&[1, 4, 2],
414+
);
415+
check_list_candidate_nomination(&result.list_candidate_nomination[3], &[1], &[], &[]);
416+
check_list_candidate_nomination(&result.list_candidate_nomination[4], &[3], &[], &[]);
417+
418+
let lists = input.list_votes;
419+
check_chosen_candidates(
420+
&result.chosen_candidates,
421+
&lists[0].number,
422+
&[
423+
&lists[0].candidate_votes[..7],
424+
&lists[0].candidate_votes[10..],
425+
]
426+
.concat(),
427+
&lists[0].candidate_votes[8..9],
428+
);
429+
check_chosen_candidates(
430+
&result.chosen_candidates,
431+
&lists[1].number,
432+
&[
433+
&lists[1].candidate_votes[..2],
434+
&lists[1].candidate_votes[3..4],
435+
]
436+
.concat(),
437+
&[
438+
&lists[1].candidate_votes[2..3],
439+
&lists[1].candidate_votes[4..],
440+
]
441+
.concat(),
442+
);
443+
check_chosen_candidates(
444+
&result.chosen_candidates,
445+
&lists[2].number,
446+
&[
447+
&lists[2].candidate_votes[..1],
448+
&lists[2].candidate_votes[2..],
449+
]
450+
.concat(),
451+
&lists[2].candidate_votes[1..2],
452+
);
453+
check_chosen_candidates(
454+
&result.chosen_candidates,
455+
&lists[3].number,
456+
&lists[3].candidate_votes[..1],
457+
&lists[3].candidate_votes[2..],
458+
);
459+
check_chosen_candidates(
460+
&result.chosen_candidates,
461+
&lists[4].number,
462+
&lists[4].candidate_votes[..1],
463+
&lists[4].candidate_votes[2..],
464+
);
465+
}
466+
344467
/// Candidate nomination with ranking change due to preferential candidate nomination
345468
///
346469
/// Actual case from GR2022

backend/apportionment/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ pub use self::{
1414
};
1515
use self::{
1616
candidate_nomination::candidate_nomination,
17-
seat_assignment::seat_assignment,
18-
structs::{ApportionmentOutput, as_candidate_nomination_input},
17+
seat_assignment::{as_candidate_nomination_input, seat_assignment},
18+
structs::ApportionmentOutput,
1919
};
2020

2121
pub fn process<T: ApportionmentInput>(

0 commit comments

Comments
 (0)