1use std::collections::HashMap;
2use std::fmt::{self, Debug, Display};
3use std::hash::Hash;
4use thiserror::Error;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Comparison<T: Display> {
9 pub winner: T,
10 pub loser: T,
11}
12
13impl<T: Display> fmt::Display for Comparison<T> {
14 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
15 write!(f, "{} > {}", self.winner, self.loser)
16 }
17}
18
19#[derive(Error, Debug)]
21pub enum AsapError<T: Display + Debug> {
22 #[error("Item not found: {0}")]
23 ItemNotFound(T),
24 #[error("Item already exists: {0}")]
25 ItemAlreadyExists(T),
26 #[error("Invalid comparison: both items are the same")]
27 InvalidComparison,
28 #[error("Not enough comparisons to compute scores")]
29 NotEnoughComparisons,
30 #[error("Internal algorithm error: {0}")]
31 InternalError(String),
32 #[error("Serialization error: {0}")]
33 SerializationError(String),
34}
35
36#[derive(Debug, Clone)]
38#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
39#[cfg_attr(
40 feature = "serde",
41 serde(bound(
42 serialize = "T: Clone + Debug + Eq + Hash + Send + Sync + 'static + serde::Serialize",
43 deserialize = "T: Clone + Debug + Eq + Hash + Send + Sync + 'static + serde::de::DeserializeOwned"
44 ))
45)]
46pub struct ComparisonMatrix<T: Clone + Debug + Eq + Hash + Send + Sync + 'static> {
47 item_indices: HashMap<T, usize>,
48 index_to_item: Vec<T>,
49 win_counts: Vec<Vec<usize>>,
50 comparison_count: usize,
51}
52
53impl<T: Clone + Debug + Eq + Hash + Display + Send + Sync + 'static> ComparisonMatrix<T> {
54 pub fn new(items: &[T]) -> Self {
55 let n = items.len();
56 let mut item_indices = HashMap::with_capacity(n);
57 let mut index_to_item = Vec::with_capacity(n);
58
59 for (idx, item) in items.iter().enumerate() {
60 item_indices.insert(item.clone(), idx);
61 index_to_item.push(item.clone());
62 }
63
64 let win_counts = vec![vec![0; n]; n];
65
66 ComparisonMatrix {
67 item_indices,
68 index_to_item,
69 win_counts,
70 comparison_count: 0,
71 }
72 }
73
74 pub fn add_comparison(&mut self, comparison: &Comparison<T>) -> Result<(), AsapError<T>> {
75 if comparison.winner == comparison.loser {
76 return Err(AsapError::InvalidComparison);
77 }
78
79 let winner_idx = self
80 .item_indices
81 .get(&comparison.winner)
82 .ok_or_else(|| AsapError::ItemNotFound(comparison.winner.clone()))?;
83 let loser_idx = self
84 .item_indices
85 .get(&comparison.loser)
86 .ok_or_else(|| AsapError::ItemNotFound(comparison.loser.clone()))?;
87
88 self.win_counts[*winner_idx][*loser_idx] += 1;
89 self.comparison_count += 1;
90
91 Ok(())
92 }
93
94 pub fn get_win_count(&self, item_i: &T, item_j: &T) -> Result<usize, AsapError<T>> {
95 let i_idx = self
96 .item_indices
97 .get(item_i)
98 .ok_or_else(|| AsapError::ItemNotFound(item_i.clone()))?;
99 let j_idx = self
100 .item_indices
101 .get(item_j)
102 .ok_or_else(|| AsapError::ItemNotFound(item_j.clone()))?;
103
104 Ok(self.win_counts[*i_idx][*j_idx])
105 }
106
107 pub fn get_comparison_count(&self, item_i: &T, item_j: &T) -> Result<usize, AsapError<T>> {
108 let i_idx = self
109 .item_indices
110 .get(item_i)
111 .ok_or_else(|| AsapError::ItemNotFound(item_i.clone()))?;
112 let j_idx = self
113 .item_indices
114 .get(item_j)
115 .ok_or_else(|| AsapError::ItemNotFound(item_j.clone()))?;
116
117 Ok(self.win_counts[*i_idx][*j_idx] + self.win_counts[*j_idx][*i_idx])
118 }
119
120 pub fn item_count(&self) -> usize {
121 self.index_to_item.len()
122 }
123
124 pub fn total_comparisons(&self) -> usize {
125 self.comparison_count
126 }
127
128 pub fn items(&self) -> Vec<T> {
129 self.index_to_item.clone()
130 }
131
132 pub fn get_item_index(&self, item: &T) -> Result<usize, AsapError<T>> {
133 self.item_indices
134 .get(item)
135 .copied()
136 .ok_or_else(|| AsapError::ItemNotFound(item.clone()))
137 }
138
139 pub fn get_item_from_index(&self, index: usize) -> Option<T> {
140 self.index_to_item.get(index).cloned()
141 }
142
143 pub fn add_item(&mut self, item: T) -> Result<(), AsapError<T>> {
144 if self.item_indices.contains_key(&item) {
145 return Err(AsapError::ItemAlreadyExists(item));
146 }
147
148 let new_idx = self.index_to_item.len();
149
150 self.item_indices.insert(item.clone(), new_idx);
151 self.index_to_item.push(item);
152
153 self.win_counts.push(vec![0; new_idx + 1]);
154
155 for row in &mut self.win_counts[0..new_idx] {
156 row.push(0);
157 }
158
159 Ok(())
160 }
161
162 pub fn remove_item(&mut self, item_to_remove: &T) -> Result<(), AsapError<T>> {
163 let removed_idx = match self.item_indices.get(item_to_remove) {
164 Some(&idx) => idx,
165 None => return Err(AsapError::ItemNotFound(item_to_remove.clone())),
166 };
167
168 let n_before_removal = self.index_to_item.len();
169 let mut comps_to_remove = 0;
170
171 for j in 0..n_before_removal {
173 if j == removed_idx {
174 for k in 0..n_before_removal {
176 if k != removed_idx {
177 comps_to_remove += self.win_counts[removed_idx][k];
178 }
179 }
180 } else {
181 comps_to_remove += self.win_counts[j][removed_idx];
183 }
184 }
185
186 self.index_to_item.remove(removed_idx);
188
189 self.win_counts.remove(removed_idx);
191 for row in &mut self.win_counts {
192 row.remove(removed_idx);
193 }
194
195 self.item_indices.clear();
197 for (idx, item) in self.index_to_item.iter().enumerate() {
198 self.item_indices.insert(item.clone(), idx);
199 }
200
201 self.comparison_count -= comps_to_remove;
202
203 Ok(())
204 }
205}
206
207#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
209#[cfg_attr(
210 feature = "serde",
211 serde(bound(
212 serialize = "T: Clone + Debug + Eq + Hash + Display + Send + Sync + 'static + serde::Serialize",
213 deserialize = "T: Clone + Debug + Eq + Hash + Display + Send + Sync + 'static + serde::de::DeserializeOwned"
214 ))
215)]
216pub struct RankingModel<T: Clone + Debug + Eq + Hash + Display + Send + Sync + 'static> {
217 pub data: ComparisonMatrix<T>,
218 pub scores: Option<HashMap<T, f64>>,
219 approximate: bool,
220 #[allow(dead_code)]
221 selective_eig: bool,
222}
223
224impl<T: Clone + Debug + Eq + Hash + Display + Send + Sync + 'static> RankingModel<T> {
225 pub fn new(items: &[T]) -> Self {
227 RankingModel {
228 data: ComparisonMatrix::new(items),
229 scores: None,
230 approximate: false,
231 selective_eig: false,
232 }
233 }
234
235 pub fn new_with_options(items: &[T], approximate: bool, selective_eig: bool) -> Self {
237 RankingModel {
238 data: ComparisonMatrix::new(items),
239 scores: None,
240 approximate,
241 selective_eig,
242 }
243 }
244
245 pub fn add_comparison(&mut self, comparison: Comparison<T>) -> Result<(), AsapError<T>> {
247 self.data.add_comparison(&comparison)?;
248 self.scores = None;
249 Ok(())
250 }
251
252 pub fn add_item(&mut self, item: T) -> Result<(), AsapError<T>> {
253 self.data.add_item(item)?;
254 self.scores = None;
255 Ok(())
256 }
257
258 pub fn remove_item(&mut self, item: &T) -> Result<(), AsapError<T>> {
259 self.data.remove_item(item)?;
260 self.scores = None; Ok(())
262 }
263
264 pub fn get_ordering(&mut self) -> Result<Vec<T>, AsapError<T>> {
266 let scores = self.get_scores()?;
267
268 let mut items_with_scores: Vec<_> = scores.iter().collect();
269
270 items_with_scores.sort_by(|a, b| b.1.partial_cmp(a.1).unwrap_or(std::cmp::Ordering::Equal));
271
272 Ok(items_with_scores
273 .into_iter()
274 .map(|(item, _)| item.clone())
275 .collect())
276 }
277
278 pub fn get_scores(&mut self) -> Result<HashMap<T, f64>, AsapError<T>> {
280 if let Some(ref scores) = self.scores {
281 return Ok(scores.clone());
282 }
283
284 let scores = if self.approximate {
285 self.compute_approximate_scores()?
286 } else {
287 self.compute_accurate_scores()?
288 };
289
290 self.scores = Some(scores.clone());
291 Ok(scores)
292 }
293
294 pub fn suggest_comparisons(&self, max: usize) -> Result<Vec<(T, T)>, AsapError<T>> {
296 if self.data.item_count() < 2 {
297 return Err(AsapError::NotEnoughComparisons);
298 }
299
300 let mut pairs_with_gain = Vec::new();
301 let items = self.data.items();
302 let n = items.len();
303
304 let current_scores = match self.scores {
305 Some(ref s) => s.clone(),
306 None => {
307 let mut temp_scores = HashMap::new();
313 for i_idx in 0..n {
314 let item_i = self.data.get_item_from_index(i_idx).ok_or_else(|| {
315 AsapError::InternalError(
316 "Invalid item index in suggest_comparisons".to_string(),
317 )
318 })?;
319 let mut wins = 0;
320 let mut total_comps = 0;
321 for j_idx in 0..n {
322 if i_idx == j_idx {
323 continue;
324 }
325 let item_j = self.data.get_item_from_index(j_idx).ok_or_else(|| {
326 AsapError::InternalError(
327 "Invalid item index in suggest_comparisons".to_string(),
328 )
329 })?;
330 wins += self.data.get_win_count(&item_i, &item_j)?;
331 total_comps += self.data.get_comparison_count(&item_i, &item_j)?;
332 }
333 temp_scores.insert(
334 item_i.clone(),
335 if total_comps > 0 {
336 wins as f64 / total_comps as f64
337 } else {
338 0.5
339 },
340 );
341 }
342 temp_scores
343 }
344 };
345
346 for i in 0..n {
347 for j in (i + 1)..n {
348 let item_i = &items[i];
349 let item_j = &items[j];
350
351 let score_i = *current_scores.get(item_i).unwrap_or(&0.5);
352 let score_j = *current_scores.get(item_j).unwrap_or(&0.5);
353
354 let prob_i_wins = 1.0 / (1.0 + (-10.0 * (score_i - score_j)).exp());
355
356 let info_gain = -(prob_i_wins * prob_i_wins.ln()
357 + (1.0 - prob_i_wins) * (1.0 - prob_i_wins).ln());
358
359 let comparison_count = self.data.get_comparison_count(item_i, item_j)?;
360 let adjusted_gain = info_gain / (1.0 + 0.1 * comparison_count as f64);
361
362 pairs_with_gain.push((adjusted_gain, (item_i.clone(), item_j.clone())));
363 }
364 }
365
366 pairs_with_gain.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
367
368 let result = pairs_with_gain
369 .into_iter()
370 .take(max)
371 .map(|(_, pair)| pair)
372 .collect();
373
374 Ok(result)
375 }
376
377 pub fn ranking_confidence(&self) -> Result<f64, AsapError<T>> {
385 let n = self.data.item_count();
386 if n <= 1 {
387 return Ok(1.0);
388 }
389
390 const MIN_COMPARISONS_PER_ITEM: usize = 5;
391
392 let items = self.data.items();
394 let mut well_compared = 0usize;
395 for i in 0..n {
396 let item_i = self
397 .data
398 .get_item_from_index(i)
399 .ok_or_else(|| AsapError::InternalError("Invalid item index".to_string()))?;
400 let mut total = 0usize;
401 for j in 0..n {
402 if i == j {
403 continue;
404 }
405 let item_j = self
406 .data
407 .get_item_from_index(j)
408 .ok_or_else(|| AsapError::InternalError("Invalid item index".to_string()))?;
409 total += self.data.get_comparison_count(&item_i, &item_j)?;
410 }
411 if total >= MIN_COMPARISONS_PER_ITEM {
412 well_compared += 1;
413 }
414 }
415 let coverage = well_compared as f64 / n as f64;
416
417 let current_scores = match self.scores {
419 Some(ref s) => s.clone(),
420 None => {
421 return Ok(coverage * 0.5);
423 }
424 };
425
426 let score_vec: Vec<f64> = items
427 .iter()
428 .map(|item| *current_scores.get(item).unwrap_or(&0.0))
429 .collect();
430
431 let mean = score_vec.iter().sum::<f64>() / n as f64;
432 let variance = score_vec
433 .iter()
434 .map(|s| (s - mean) * (s - mean))
435 .sum::<f64>()
436 / n as f64;
437 let std_dev = variance.sqrt();
438
439 let cv = if mean.abs() > 1e-10 {
441 std_dev / mean.abs()
442 } else {
443 std_dev
444 };
445 let discrimination = 1.0 / (1.0 + (-5.0 * (cv - 0.3)).exp());
446
447 let confidence = 0.5 * coverage + 0.5 * discrimination;
448 Ok(confidence)
449 }
450
451 pub fn is_sufficiently_confident(&self, threshold: f64) -> Result<bool, AsapError<T>> {
453 let confidence = self.ranking_confidence()?;
454 Ok(confidence >= threshold)
455 }
456
457 fn compute_accurate_scores(&self) -> Result<HashMap<T, f64>, AsapError<T>> {
462 let n = self.data.item_count();
463 let mut scores = HashMap::new();
464
465 if n == 0 {
466 return Ok(scores);
467 }
468
469 let mut wins = vec![vec![0usize; n]; n];
471 let mut total_wins = vec![0usize; n]; let mut games_against = vec![vec![0usize; n]; n]; for i in 0..n {
475 let item_i = self
476 .data
477 .get_item_from_index(i)
478 .ok_or_else(|| AsapError::InternalError("Invalid item index".to_string()))?;
479 for j in (i + 1)..n {
480 let item_j = self
481 .data
482 .get_item_from_index(j)
483 .ok_or_else(|| AsapError::InternalError("Invalid item index".to_string()))?;
484
485 let w_ij = self.data.get_win_count(&item_i, &item_j)?;
486 let w_ji = self.data.get_win_count(&item_j, &item_i)?;
487
488 wins[i][j] = w_ij;
489 wins[j][i] = w_ji;
490 total_wins[i] += w_ij;
491 total_wins[j] += w_ji;
492 let n_ij = w_ij + w_ji;
493 games_against[i][j] = n_ij;
494 games_against[j][i] = n_ij;
495 }
496 }
497
498 let mut p = vec![1.0f64; n];
500
501 const MAX_ITER: usize = 1000;
502 const TOL: f64 = 1e-8;
503
504 for _iter in 0..MAX_ITER {
505 let mut p_new = vec![0.0f64; n];
506 let mut max_change = 0.0f64;
507
508 for i in 0..n {
509 if total_wins[i] == 0 {
510 p_new[i] = TOL;
512 continue;
513 }
514
515 let mut denom = 0.0f64;
517 for j in 0..n {
518 if i == j || games_against[i][j] == 0 {
519 continue;
520 }
521 denom += games_against[i][j] as f64 / (p[i] + p[j]);
522 }
523
524 if denom > 0.0 {
525 p_new[i] = total_wins[i] as f64 / denom;
526 } else {
527 p_new[i] = p[i];
528 }
529 }
530
531 let log_sum: f64 = p_new.iter().map(|x| x.max(TOL).ln()).sum();
533 let log_mean = log_sum / n as f64;
534 let scale = (-log_mean).exp();
535 for x in &mut p_new {
536 *x *= scale;
537 }
538
539 for i in 0..n {
541 let change = ((p_new[i] - p[i]) / p[i].max(TOL)).abs();
542 if change > max_change {
543 max_change = change;
544 }
545 }
546
547 p = p_new;
548
549 if max_change < TOL {
550 break;
551 }
552 }
553
554 for (i, &pi) in p.iter().enumerate().take(n) {
556 let item = self
557 .data
558 .get_item_from_index(i)
559 .ok_or_else(|| AsapError::InternalError("Invalid item index".to_string()))?;
560 scores.insert(item.clone(), pi.max(TOL).ln());
561 }
562
563 Ok(scores)
564 }
565
566 fn compute_approximate_scores(&self) -> Result<HashMap<T, f64>, AsapError<T>> {
569 let n = self.data.item_count();
570 let mut scores = HashMap::new();
571
572 if n == 0 {
573 return Ok(scores);
574 }
575
576 let mut total_wins = vec![0usize; n];
578 let mut games_against = vec![vec![0usize; n]; n];
579
580 for i in 0..n {
581 let item_i = self
582 .data
583 .get_item_from_index(i)
584 .ok_or_else(|| AsapError::InternalError("Invalid item index".to_string()))?;
585 for j in (i + 1)..n {
586 let item_j = self
587 .data
588 .get_item_from_index(j)
589 .ok_or_else(|| AsapError::InternalError("Invalid item index".to_string()))?;
590
591 let w_ij = self.data.get_win_count(&item_i, &item_j)?;
592 let w_ji = self.data.get_win_count(&item_j, &item_i)?;
593
594 total_wins[i] += w_ij;
595 total_wins[j] += w_ji;
596 let n_ij = w_ij + w_ji;
597 games_against[i][j] = n_ij;
598 games_against[j][i] = n_ij;
599 }
600 }
601
602 let mut p = vec![1.0f64; n];
603 const MAX_ITER: usize = 100; const TOL: f64 = 1e-6;
605
606 for _iter in 0..MAX_ITER {
607 let mut p_new = vec![0.0f64; n];
608 let mut max_change = 0.0f64;
609
610 for i in 0..n {
611 if total_wins[i] == 0 {
612 p_new[i] = TOL;
613 continue;
614 }
615
616 let mut denom = 0.0f64;
617 for j in 0..n {
618 if i == j || games_against[i][j] == 0 {
619 continue;
620 }
621 denom += games_against[i][j] as f64 / (p[i] + p[j]);
622 }
623
624 if denom > 0.0 {
625 p_new[i] = total_wins[i] as f64 / denom;
626 } else {
627 p_new[i] = p[i];
628 }
629 }
630
631 let log_sum: f64 = p_new.iter().map(|x| x.max(TOL).ln()).sum();
632 let log_mean = log_sum / n as f64;
633 let scale = (-log_mean).exp();
634 for x in &mut p_new {
635 *x *= scale;
636 }
637
638 for i in 0..n {
639 let change = ((p_new[i] - p[i]) / p[i].max(TOL)).abs();
640 if change > max_change {
641 max_change = change;
642 }
643 }
644
645 p = p_new;
646
647 if max_change < TOL {
648 break;
649 }
650 }
651
652 for (i, &pi) in p.iter().enumerate().take(n) {
653 let item = self
654 .data
655 .get_item_from_index(i)
656 .ok_or_else(|| AsapError::InternalError("Invalid item index".to_string()))?;
657 scores.insert(item.clone(), pi.max(TOL).ln());
658 }
659
660 Ok(scores)
661 }
662}
663
664#[cfg(feature = "serde")]
665impl<
666 T: Clone
667 + Debug
668 + Eq
669 + Hash
670 + Display
671 + Send
672 + Sync
673 + 'static
674 + serde::Serialize
675 + serde::de::DeserializeOwned,
676> RankingModel<T>
677{
678 pub fn to_json(&self) -> Result<String, AsapError<T>> {
679 serde_json::to_string(self)
680 .map_err(|e| AsapError::SerializationError(format!("Failed to serialize: {}", e)))
681 }
682
683 pub fn from_json(json: &str) -> Result<Self, AsapError<T>> {
684 serde_json::from_str(json)
685 .map_err(|e| AsapError::SerializationError(format!("Failed to deserialize: {}", e)))
686 }
687
688 pub fn save_to_file(&self, path: &str) -> Result<(), AsapError<T>> {
689 let json = self.to_json()?;
690 std::fs::write(path, json)
691 .map_err(|e| AsapError::SerializationError(format!("Failed to write file: {}", e)))
692 }
693
694 pub fn load_from_file(path: &str) -> Result<Self, AsapError<T>> {
695 let json = std::fs::read_to_string(path)
696 .map_err(|e| AsapError::SerializationError(format!("Failed to read file: {}", e)))?;
697 Self::from_json(&json)
698 }
699}
700
701#[cfg(not(feature = "serde"))]
702impl<T: Clone + Debug + Eq + Hash + Display + Send + Sync + 'static> RankingModel<T> {
703 }
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 #[test]
718 fn test_comparison_matrix_new() {
719 let items = vec!["A".to_string(), "B".to_string(), "C".to_string()];
720 let matrix = ComparisonMatrix::<String>::new(&items);
721
722 assert_eq!(matrix.item_count(), 3);
723 assert_eq!(matrix.total_comparisons(), 0);
724 }
725
726 #[test]
727 fn test_add_comparison() {
728 let items = vec!["A".to_string(), "B".to_string(), "C".to_string()];
729 let mut matrix = ComparisonMatrix::<String>::new(&items);
730
731 let comparison = Comparison {
732 winner: "A".to_string(),
733 loser: "B".to_string(),
734 };
735
736 matrix.add_comparison(&comparison).unwrap();
737
738 assert_eq!(
739 matrix
740 .get_win_count(&"A".to_string(), &"B".to_string())
741 .unwrap(),
742 1
743 );
744 assert_eq!(
745 matrix
746 .get_win_count(&"B".to_string(), &"A".to_string())
747 .unwrap(),
748 0
749 );
750 assert_eq!(matrix.total_comparisons(), 1);
751 }
752
753 #[test]
754 fn test_comparison_matrix_add_item() {
755 let items = vec!["A".to_string(), "B".to_string()];
756 let mut matrix = ComparisonMatrix::<String>::new(&items);
757
758 matrix.add_item("C".to_string()).unwrap();
759
760 assert_eq!(matrix.item_count(), 3);
761 assert_eq!(
762 matrix
763 .get_win_count(&"A".to_string(), &"C".to_string())
764 .unwrap(),
765 0
766 );
767 assert_eq!(
768 matrix
769 .get_win_count(&"C".to_string(), &"A".to_string())
770 .unwrap(),
771 0
772 );
773
774 let result = matrix.add_item("A".to_string());
775 assert!(result.is_err());
776
777 let comparison = Comparison {
778 winner: "C".to_string(),
779 loser: "A".to_string(),
780 };
781 matrix.add_comparison(&comparison).unwrap();
782
783 assert_eq!(
784 matrix
785 .get_win_count(&"C".to_string(), &"A".to_string())
786 .unwrap(),
787 1
788 );
789 }
790
791 #[test]
792 fn test_ranking_model_new() {
793 let items = vec!["A".to_string(), "B".to_string(), "C".to_string()];
794 let model = RankingModel::<String>::new(&items);
795
796 assert_eq!(model.data.item_count(), 3);
797 assert!(model.scores.is_none());
798 }
799
800 #[test]
801 fn test_ranking_model_add_comparison() {
802 let items = vec!["A".to_string(), "B".to_string(), "C".to_string()];
803 let mut model = RankingModel::<String>::new(&items);
804
805 let comparison = Comparison {
806 winner: "A".to_string(),
807 loser: "B".to_string(),
808 };
809
810 model.add_comparison(comparison).unwrap();
811
812 assert_eq!(model.data.total_comparisons(), 1);
813 assert!(model.scores.is_none());
814 }
815
816 #[test]
817 fn test_ranking_model_add_item() {
818 let items = vec!["A".to_string(), "B".to_string()];
819 let mut model = RankingModel::<String>::new(&items);
820
821 model
822 .add_comparison(Comparison {
823 winner: "A".to_string(),
824 loser: "B".to_string(),
825 })
826 .unwrap();
827 model
828 .add_comparison(Comparison {
829 winner: "A".to_string(),
830 loser: "B".to_string(),
831 })
832 .unwrap();
833
834 let scores_before = model.get_scores().unwrap();
835 assert!(scores_before.contains_key(&"A".to_string()));
836 assert!(scores_before.contains_key(&"B".to_string()));
837
838 model.add_item("C".to_string()).unwrap();
839
840 assert!(model.scores.is_none());
841
842 model
843 .add_comparison(Comparison {
844 winner: "C".to_string(),
845 loser: "A".to_string(),
846 })
847 .unwrap();
848
849 let scores_after = model.get_scores().unwrap();
850 assert!(scores_after.contains_key(&"A".to_string()));
851 assert!(scores_after.contains_key(&"B".to_string()));
852 assert!(scores_after.contains_key(&"C".to_string()));
853
854 assert!(
855 scores_after.get(&"C".to_string()).unwrap()
856 > scores_after.get(&"A".to_string()).unwrap()
857 );
858 }
859
860 #[test]
861 #[cfg(feature = "serde")]
862 fn test_serialization_deserialization() {
863 let items = vec!["A".to_string(), "B".to_string(), "C".to_string()];
864 let mut model = RankingModel::<String>::new(&items);
865
866 model
867 .add_comparison(Comparison {
868 winner: "A".to_string(),
869 loser: "B".to_string(),
870 })
871 .unwrap();
872 model
873 .add_comparison(Comparison {
874 winner: "B".to_string(),
875 loser: "C".to_string(),
876 })
877 .unwrap();
878 model
879 .add_comparison(Comparison {
880 winner: "A".to_string(),
881 loser: "C".to_string(),
882 })
883 .unwrap();
884
885 let original_scores = model.get_scores().unwrap();
886
887 let json = model.to_json().unwrap();
888
889 let mut deserialized_model = RankingModel::<String>::from_json(&json).unwrap();
890
891 assert_eq!(
892 deserialized_model.data.item_count(),
893 model.data.item_count()
894 );
895 assert_eq!(
896 deserialized_model.data.total_comparisons(),
897 model.data.total_comparisons()
898 );
899
900 let deserialized_scores = deserialized_model.get_scores().unwrap();
901 for (item, score) in &original_scores {
902 assert!(deserialized_scores.contains_key(item));
903 assert!((deserialized_scores.get(item).unwrap() - score).abs() < 1e-6);
904 }
905 }
906
907 #[test]
908 fn test_comparison_matrix_remove_item() {
909 let items_initial = vec!["A".to_string(), "B".to_string(), "C".to_string()];
910 let mut matrix = ComparisonMatrix::<String>::new(&items_initial);
911
912 matrix
913 .add_comparison(&Comparison {
914 winner: "A".to_string(),
915 loser: "B".to_string(),
916 })
917 .unwrap(); matrix
919 .add_comparison(&Comparison {
920 winner: "B".to_string(),
921 loser: "C".to_string(),
922 })
923 .unwrap(); matrix
925 .add_comparison(&Comparison {
926 winner: "A".to_string(),
927 loser: "C".to_string(),
928 })
929 .unwrap(); assert_eq!(matrix.item_count(), 3);
932 assert_eq!(matrix.total_comparisons(), 3);
933
934 matrix.remove_item(&"B".to_string()).unwrap();
936
937 assert_eq!(matrix.item_count(), 2);
938 assert_eq!(matrix.total_comparisons(), 1);
946
947 assert!(matrix.get_item_index(&"A".to_string()).is_ok());
948 assert!(matrix.get_item_index(&"C".to_string()).is_ok());
949 assert!(matrix.get_item_index(&"B".to_string()).is_err());
950
951 assert_eq!(
953 matrix
954 .get_win_count(&"A".to_string(), &"C".to_string())
955 .unwrap(),
956 1
957 );
958
959 let a_idx = matrix.get_item_index(&"A".to_string()).unwrap();
961 let c_idx = matrix.get_item_index(&"C".to_string()).unwrap();
962 assert!((a_idx == 0 && c_idx == 1) || (a_idx == 1 && c_idx == 0));
963
964 let result = matrix.remove_item(&"D".to_string());
966 assert!(matches!(result, Err(AsapError::ItemNotFound(_))));
967
968 matrix.remove_item(&"A".to_string()).unwrap();
970 assert_eq!(matrix.item_count(), 1);
971 assert_eq!(matrix.total_comparisons(), 0);
972 assert!(matrix.get_item_index(&"C".to_string()).is_ok());
973
974 matrix.remove_item(&"C".to_string()).unwrap();
976 assert_eq!(matrix.item_count(), 0);
977 assert_eq!(matrix.total_comparisons(), 0);
978 }
979
980 #[test]
981 fn test_ranking_model_remove_item() {
982 let items = vec!["A".to_string(), "B".to_string(), "C".to_string()];
983 let mut model = RankingModel::<String>::new(&items);
984
985 model
986 .add_comparison(Comparison {
987 winner: "A".to_string(),
988 loser: "B".to_string(),
989 })
990 .unwrap();
991 model
992 .add_comparison(Comparison {
993 winner: "B".to_string(),
994 loser: "C".to_string(),
995 })
996 .unwrap();
997
998 let scores_before_removal = model.get_scores().unwrap();
1000 assert!(scores_before_removal.contains_key(&"A".to_string()));
1001 assert!(scores_before_removal.contains_key(&"B".to_string()));
1002 assert!(scores_before_removal.contains_key(&"C".to_string()));
1003 assert!(model.scores.is_some());
1004
1005 model.remove_item(&"B".to_string()).unwrap();
1007
1008 assert_eq!(model.data.item_count(), 2);
1009 assert!(model.scores.is_none()); let scores_after_removal = model.get_scores().unwrap();
1013 assert!(scores_after_removal.contains_key(&"A".to_string()));
1014 assert!(scores_after_removal.contains_key(&"C".to_string()));
1015 assert!(!scores_after_removal.contains_key(&"B".to_string()));
1016
1017 let ordering = model.get_ordering().unwrap();
1019 assert_eq!(ordering.len(), 2);
1020 assert!(ordering.contains(&"A".to_string()));
1021 assert!(ordering.contains(&"C".to_string()));
1022
1023 let result = model.remove_item(&"D".to_string());
1025 assert!(matches!(result, Err(AsapError::ItemNotFound(_))));
1026 }
1027}