1use std::collections::HashMap;
19
20use citum_schema::grouping::{GroupSort, GroupSortKey, NameSortOrder, SortKey as GroupSortKeyType};
21use citum_schema::locale::Locale;
22#[cfg(test)]
23use citum_schema::reference::ClassExtension;
24
25use crate::reference::Reference;
26use crate::sort_support::{TextCollator, author_sort_key_opt, normalize_sort_text, title_sort_key};
27
28fn compare_optional_years(a_year: Option<i32>, b_year: Option<i32>) -> std::cmp::Ordering {
29 match (a_year, b_year) {
30 (Some(a), Some(b)) => a.cmp(&b),
31 (Some(_), None) => std::cmp::Ordering::Less,
32 (None, Some(_)) => std::cmp::Ordering::Greater,
33 (None, None) => std::cmp::Ordering::Equal,
34 }
35}
36
37pub struct ReferenceSorter<'a> {
39 locale: &'a Locale,
40 text_collator: TextCollator,
41}
42
43struct CachedReference<'a> {
44 reference: &'a Reference,
45 sort_values: Vec<CachedSortValue>,
46 id: Option<String>,
51}
52
53enum CachedSortValue {
54 RefType { name: String, rank: Option<usize> },
55 OptionalText(Option<String>),
56 Text(String),
57 Issued(Option<i32>),
58}
59
60enum CompiledSortKey<'a> {
61 RefType {
62 ascending: bool,
63 rank_by_type: Option<HashMap<String, usize>>,
64 },
65 Author {
66 ascending: bool,
67 name_order: NameSortOrder,
68 },
69 Title {
70 ascending: bool,
71 },
72 Issued {
73 ascending: bool,
74 },
75 Field {
76 ascending: bool,
77 field_name: &'a str,
78 },
79}
80
81impl<'a> ReferenceSorter<'a> {
82 #[must_use]
84 pub fn new(locale: &'a Locale) -> Self {
85 Self {
86 locale,
87 text_collator: TextCollator::new(locale),
88 }
89 }
90
91 #[must_use]
100 pub fn sort_references<'b>(
101 &self,
102 references: Vec<&'b Reference>,
103 sort_spec: &GroupSort,
104 ) -> Vec<&'b Reference> {
105 self.sort_references_impl(references, sort_spec, false)
106 }
107
108 #[must_use]
119 pub fn sort_references_with_id_tiebreak<'b>(
120 &self,
121 references: Vec<&'b Reference>,
122 sort_spec: &GroupSort,
123 ) -> Vec<&'b Reference> {
124 self.sort_references_impl(references, sort_spec, true)
125 }
126
127 fn sort_references_impl<'b>(
128 &self,
129 mut references: Vec<&'b Reference>,
130 sort_spec: &GroupSort,
131 id_tiebreak: bool,
132 ) -> Vec<&'b Reference> {
133 let compiled_keys = self.compile_sort_keys(sort_spec);
134 if compiled_keys.is_empty() {
135 return references;
136 }
137
138 let mut cached_references = references
139 .drain(..)
140 .map(|reference| CachedReference {
141 reference,
142 sort_values: compiled_keys
143 .iter()
144 .map(|sort_key| self.cache_sort_value(reference, sort_key))
145 .collect(),
146 id: id_tiebreak.then(|| reference.id().map(|id| id.0)).flatten(),
147 })
148 .collect::<Vec<_>>();
149
150 cached_references.sort_by(|a, b| {
151 let cmp = self.compare_cached_references(a, b, &compiled_keys);
152 if cmp != std::cmp::Ordering::Equal || !id_tiebreak {
153 return cmp;
154 }
155 Self::compare_cached_ids(a, b)
156 });
157 cached_references
158 .into_iter()
159 .map(|entry| entry.reference)
160 .collect()
161 }
162
163 fn compare_cached_ids(a: &CachedReference<'_>, b: &CachedReference<'_>) -> std::cmp::Ordering {
167 match (&a.id, &b.id) {
168 (Some(a_id), Some(b_id)) => a_id.as_str().cmp(b_id.as_str()),
169 (Some(_), None) => std::cmp::Ordering::Less,
170 (None, Some(_)) => std::cmp::Ordering::Greater,
171 (None, None) => std::cmp::Ordering::Equal,
172 }
173 }
174
175 #[must_use]
177 pub fn compare_by_key(
178 &self,
179 a: &Reference,
180 b: &Reference,
181 sort_key: &GroupSortKey,
182 ) -> std::cmp::Ordering {
183 self.compare_by_key_with_context(a, b, sort_key)
184 }
185
186 fn compare_by_key_with_context(
187 &self,
188 a: &Reference,
189 b: &Reference,
190 sort_key: &GroupSortKey,
191 ) -> std::cmp::Ordering {
192 let cmp = match &sort_key.key {
193 GroupSortKeyType::RefType => sort_key.order.as_ref().map_or_else(
194 || a.ref_type().cmp(&b.ref_type()),
195 |order| Self::compare_by_type_order(a, b, order),
196 ),
197 GroupSortKeyType::Author => sort_key.sort_order.as_ref().map_or_else(
198 || self.compare_by_author_with_order(a, b, NameSortOrder::FamilyGiven),
199 |name_order| self.compare_by_author_with_order(a, b, *name_order),
200 ),
201 GroupSortKeyType::Title => self.compare_by_title(a, b),
202 GroupSortKeyType::Issued => Self::compare_by_issued(a, b),
203 GroupSortKeyType::Field(field_name) => Self::compare_by_field(a, b, field_name),
204 };
205
206 if sort_key.ascending {
207 cmp
208 } else {
209 cmp.reverse()
210 }
211 }
212
213 fn compare_by_type_order(a: &Reference, b: &Reference, order: &[String]) -> std::cmp::Ordering {
218 let a_type = a.ref_type();
219 let b_type = b.ref_type();
220
221 let a_pos = order.iter().position(|t| t == &a_type);
222 let b_pos = order.iter().position(|t| t == &b_type);
223
224 match (a_pos, b_pos) {
225 (Some(a_idx), Some(b_idx)) => a_idx.cmp(&b_idx),
226 (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => a_type.cmp(&b_type), }
230 }
231
232 fn compile_sort_keys<'b>(&self, sort_spec: &'b GroupSort) -> Vec<CompiledSortKey<'b>> {
233 sort_spec
234 .template
235 .iter()
236 .map(|sort_key| match &sort_key.key {
237 GroupSortKeyType::RefType => CompiledSortKey::RefType {
238 ascending: sort_key.ascending,
239 rank_by_type: sort_key.order.as_ref().map(|order| {
240 order
241 .iter()
242 .enumerate()
243 .map(|(index, ref_type)| (ref_type.clone(), index))
244 .collect()
245 }),
246 },
247 GroupSortKeyType::Author => CompiledSortKey::Author {
248 ascending: sort_key.ascending,
249 name_order: sort_key.sort_order.unwrap_or(NameSortOrder::FamilyGiven),
250 },
251 GroupSortKeyType::Title => CompiledSortKey::Title {
252 ascending: sort_key.ascending,
253 },
254 GroupSortKeyType::Issued => CompiledSortKey::Issued {
255 ascending: sort_key.ascending,
256 },
257 GroupSortKeyType::Field(field_name) => CompiledSortKey::Field {
258 ascending: sort_key.ascending,
259 field_name,
260 },
261 })
262 .collect()
263 }
264
265 fn cache_sort_value(
266 &self,
267 reference: &Reference,
268 sort_key: &CompiledSortKey<'_>,
269 ) -> CachedSortValue {
270 match sort_key {
271 CompiledSortKey::RefType { rank_by_type, .. } => {
272 let ref_type = reference.ref_type();
273 CachedSortValue::RefType {
274 name: ref_type.clone(),
275 rank: rank_by_type
276 .as_ref()
277 .and_then(|ranks| ranks.get(&ref_type).copied()),
278 }
279 }
280 CompiledSortKey::Author { name_order, .. } => CachedSortValue::OptionalText(
281 self.extract_author_sort_key_opt(reference, *name_order),
282 ),
283 CompiledSortKey::Title { .. } => CachedSortValue::Text(self.title_sort_key(reference)),
284 CompiledSortKey::Issued { .. } => CachedSortValue::Issued(Self::issued_year(reference)),
285 CompiledSortKey::Field { field_name, .. } => {
286 CachedSortValue::Text(Self::field_sort_value(reference, field_name))
287 }
288 }
289 }
290
291 fn compare_cached_references(
292 &self,
293 a: &CachedReference<'_>,
294 b: &CachedReference<'_>,
295 compiled_keys: &[CompiledSortKey<'_>],
296 ) -> std::cmp::Ordering {
297 for (index, sort_key) in compiled_keys.iter().enumerate() {
298 #[allow(
299 clippy::indexing_slicing,
300 reason = "index is derived from compiled_keys"
301 )]
302 let cmp = self.compare_cached_value(&a.sort_values[index], &b.sort_values[index]);
303 let cmp = if Self::is_ascending(sort_key) {
304 cmp
305 } else {
306 cmp.reverse()
307 };
308
309 if cmp != std::cmp::Ordering::Equal {
310 return cmp;
311 }
312 }
313
314 std::cmp::Ordering::Equal
315 }
316
317 fn compare_cached_value(&self, a: &CachedSortValue, b: &CachedSortValue) -> std::cmp::Ordering {
318 match (a, b) {
319 (
320 CachedSortValue::RefType {
321 name: a_name,
322 rank: a_rank,
323 },
324 CachedSortValue::RefType {
325 name: b_name,
326 rank: b_rank,
327 },
328 ) => match (a_rank, b_rank) {
329 (Some(a_idx), Some(b_idx)) => a_idx.cmp(b_idx),
330 (Some(_), None) => std::cmp::Ordering::Less,
331 (None, Some(_)) => std::cmp::Ordering::Greater,
332 (None, None) => a_name.cmp(b_name),
333 },
334 (CachedSortValue::OptionalText(a_text), CachedSortValue::OptionalText(b_text)) => {
335 match (a_text, b_text) {
336 (Some(a_value), Some(b_value)) => self.text_collator.compare(a_value, b_value),
337 (Some(_), None) => std::cmp::Ordering::Less,
338 (None, Some(_)) => std::cmp::Ordering::Greater,
339 (None, None) => std::cmp::Ordering::Equal,
340 }
341 }
342 (CachedSortValue::Text(a_text), CachedSortValue::Text(b_text)) => {
343 self.text_collator.compare(a_text, b_text)
344 }
345 (CachedSortValue::Issued(a_year), CachedSortValue::Issued(b_year)) => {
346 compare_optional_years(*a_year, *b_year)
347 }
348 _ => std::cmp::Ordering::Equal,
349 }
350 }
351
352 fn is_ascending(sort_key: &CompiledSortKey<'_>) -> bool {
353 match sort_key {
354 CompiledSortKey::RefType { ascending, .. }
355 | CompiledSortKey::Author { ascending, .. }
356 | CompiledSortKey::Title { ascending }
357 | CompiledSortKey::Issued { ascending }
358 | CompiledSortKey::Field { ascending, .. } => *ascending,
359 }
360 }
361
362 fn compare_by_author_with_order(
364 &self,
365 a: &Reference,
366 b: &Reference,
367 name_order: NameSortOrder,
368 ) -> std::cmp::Ordering {
369 let a_key = self.extract_author_sort_key_opt(a, name_order);
370 let b_key = self.extract_author_sort_key_opt(b, name_order);
371 match (a_key, b_key) {
372 (Some(a), Some(b)) => self.text_collator.compare(&a, &b),
373 (Some(_), None) => std::cmp::Ordering::Less,
374 (None, Some(_)) => std::cmp::Ordering::Greater,
375 (None, None) => std::cmp::Ordering::Equal,
376 }
377 }
378
379 fn extract_author_sort_key_opt(
385 &self,
386 reference: &Reference,
387 name_order: NameSortOrder,
388 ) -> Option<String> {
389 author_sort_key_opt(reference, name_order, self.locale, true)
390 }
391
392 #[must_use]
394 pub fn extract_author_sort_key(
395 &self,
396 reference: &Reference,
397 name_order: NameSortOrder,
398 ) -> String {
399 self.extract_author_sort_key_opt(reference, name_order)
400 .unwrap_or_default()
401 }
402
403 fn compare_by_title(&self, a: &Reference, b: &Reference) -> std::cmp::Ordering {
405 let a_title = self.title_sort_key(a);
406 let b_title = self.title_sort_key(b);
407 self.text_collator.compare(&a_title, &b_title)
408 }
409
410 fn compare_by_issued(a: &Reference, b: &Reference) -> std::cmp::Ordering {
412 let a_year = Self::issued_year(a);
413 let b_year = Self::issued_year(b);
414 compare_optional_years(a_year, b_year)
415 }
416
417 fn compare_by_field(a: &Reference, b: &Reference, field_name: &str) -> std::cmp::Ordering {
419 Self::field_sort_value(a, field_name).cmp(&Self::field_sort_value(b, field_name))
420 }
421
422 fn title_sort_key(&self, reference: &Reference) -> String {
423 title_sort_key(reference, self.locale)
424 }
425
426 fn issued_year(reference: &Reference) -> Option<i32> {
427 reference
428 .effective_issued_date()
429 .and_then(|d| d.year().parse::<i32>().ok())
430 .filter(|year| *year != 0)
431 }
432
433 fn field_sort_value(reference: &Reference, field_name: &str) -> String {
434 match field_name {
435 "language" => normalize_sort_text(reference.language().unwrap_or_default().as_ref()),
436 _ => String::new(),
438 }
439 }
440}
441
442#[cfg(test)]
443#[allow(
444 clippy::unwrap_used,
445 clippy::expect_used,
446 clippy::panic,
447 clippy::indexing_slicing,
448 clippy::todo,
449 clippy::unimplemented,
450 clippy::unreachable,
451 clippy::get_unwrap,
452 reason = "Panicking is acceptable and often desired in tests."
453)]
454mod tests {
455 use super::*;
456 use citum_schema::grouping::GroupSortKey;
457
458 fn make_locale() -> Locale {
459 Locale::en_us()
460 }
461
462 fn make_reference(
463 id: &str,
464 ref_type: &str,
465 author_family: &str,
466 title: &str,
467 year: i32,
468 ) -> Reference {
469 let json = serde_json::json!({
470 "id": id,
471 "type": ref_type,
472 "author": [{"family": author_family, "given": "Test"}],
473 "issued": {"date-parts": [[year]]},
474 "title": title,
475 "container-title": "Test Container",
476 });
477 let legacy: csl_legacy::csl_json::Reference = serde_json::from_value(json).unwrap();
478 legacy.into()
479 }
480
481 fn make_reference_no_author(id: &str, ref_type: &str, title: &str, year: i32) -> Reference {
482 let json = serde_json::json!({
483 "id": id,
484 "type": ref_type,
485 "issued": {"date-parts": [[year]]},
486 "title": title,
487 "container-title": "Test Container",
488 });
489 let legacy: csl_legacy::csl_json::Reference = serde_json::from_value(json).unwrap();
490 legacy.into()
491 }
492
493 #[test]
494 fn test_type_order_sorting() {
495 let locale = make_locale();
496 let sorter = ReferenceSorter::new(&locale);
497
498 let journal = make_reference("r1", "article-journal", "Smith", "Title J", 1990);
500 let magazine = make_reference("r2", "article-magazine", "Jones", "Title M", 2000);
501 let newspaper = make_reference("r3", "article-newspaper", "Brown", "Title N", 1985);
502 let book = make_reference("r4", "book", "Davis", "Title B", 1995);
503
504 let mut refs = vec![&book, &newspaper, &journal, &magazine];
505
506 let sort_spec = GroupSort {
507 template: vec![GroupSortKey {
508 key: GroupSortKeyType::RefType,
509 ascending: true,
510 order: Some(vec![
511 "article-journal".to_string(),
512 "article-magazine".to_string(),
513 "article-newspaper".to_string(),
514 ]),
515 sort_order: None,
516 }],
517 };
518
519 refs = sorter.sort_references(refs, &sort_spec);
520
521 assert_eq!(refs[0].id().unwrap(), "r1"); assert_eq!(refs[1].id().unwrap(), "r2"); assert_eq!(refs[2].id().unwrap(), "r3"); assert_eq!(refs[3].id().unwrap(), "r4"); }
527
528 #[test]
529 fn test_author_family_given_order() {
530 let locale = make_locale();
531 let sorter = ReferenceSorter::new(&locale);
532
533 let smith = make_reference("r1", "book", "Smith", "Title", 2000);
534 let jones = make_reference("r2", "book", "Jones", "Title", 2000);
535 let brown = make_reference("r3", "book", "Brown", "Title", 2000);
536
537 let mut refs = vec![&smith, &jones, &brown];
538
539 let sort_spec = GroupSort {
540 template: vec![GroupSortKey {
541 key: GroupSortKeyType::Author,
542 ascending: true,
543 order: None,
544 sort_order: Some(NameSortOrder::FamilyGiven),
545 }],
546 };
547
548 refs = sorter.sort_references(refs, &sort_spec);
549
550 assert_eq!(refs[0].id().unwrap(), "r3"); assert_eq!(refs[1].id().unwrap(), "r2"); assert_eq!(refs[2].id().unwrap(), "r1"); }
555
556 #[test]
557 #[cfg(feature = "icu")]
558 fn test_author_sort_uses_unicode_collation_for_accented_names() {
559 let locale = make_locale();
560 let sorter = ReferenceSorter::new(&locale);
561
562 let celik = make_reference("r1", "book", "Çelik", "Title", 2000);
563 let zimring = make_reference("r2", "book", "Zimring", "Title", 2000);
564 let o_tuathail = make_reference("r3", "book", "Ó Tuathail", "Title", 2000);
565
566 let mut refs = vec![&o_tuathail, &zimring, &celik];
567
568 let sort_spec = GroupSort {
569 template: vec![GroupSortKey {
570 key: GroupSortKeyType::Author,
571 ascending: true,
572 order: None,
573 sort_order: Some(NameSortOrder::FamilyGiven),
574 }],
575 };
576
577 refs = sorter.sort_references(refs, &sort_spec);
578
579 assert_eq!(refs[0].id().unwrap(), "r1");
580 assert_eq!(refs[1].id().unwrap(), "r3");
581 assert_eq!(refs[2].id().unwrap(), "r2");
582 }
583
584 #[test]
585 #[cfg(feature = "icu")]
586 fn test_title_sort_uses_unicode_collation_for_accented_titles() {
587 let locale = make_locale();
588 let sorter = ReferenceSorter::new(&locale);
589
590 let accent = make_reference_no_author("r1", "book", "Órbitas del sur", 2000);
591 let plain = make_reference_no_author("r2", "book", "Origins of Theory", 2000);
592 let zeta = make_reference_no_author("r3", "book", "Zebra Studies", 2000);
593
594 let mut refs = vec![&zeta, &plain, &accent];
595
596 let sort_spec = GroupSort {
597 template: vec![GroupSortKey {
598 key: GroupSortKeyType::Title,
599 ascending: true,
600 order: None,
601 sort_order: None,
602 }],
603 };
604
605 refs = sorter.sort_references(refs, &sort_spec);
606
607 assert_eq!(refs[0].id().unwrap(), "r1");
608 assert_eq!(refs[1].id().unwrap(), "r2");
609 assert_eq!(refs[2].id().unwrap(), "r3");
610 }
611
612 #[test]
613 fn test_issued_descending() {
614 let locale = make_locale();
615 let sorter = ReferenceSorter::new(&locale);
616
617 let old = make_reference("r1", "book", "Smith", "Title", 1990);
618 let new = make_reference("r2", "book", "Jones", "Title", 2020);
619 let mid = make_reference("r3", "book", "Brown", "Title", 2005);
620
621 let mut refs = vec![&old, &new, &mid];
622
623 let sort_spec = GroupSort {
624 template: vec![GroupSortKey {
625 key: GroupSortKeyType::Issued,
626 ascending: false, order: None,
628 sort_order: None,
629 }],
630 };
631
632 refs = sorter.sort_references(refs, &sort_spec);
633
634 assert_eq!(refs[0].id().unwrap(), "r2"); assert_eq!(refs[1].id().unwrap(), "r3"); assert_eq!(refs[2].id().unwrap(), "r1"); }
639
640 #[test]
641 fn test_issued_ascending_places_undated_last() {
642 let locale = make_locale();
643 let sorter = ReferenceSorter::new(&locale);
644
645 let dated_early = make_reference("r1", "book", "Smith", "Book D", 1999);
646 let dated_late = make_reference("r2", "book", "Jones", "Book B", 2000);
647 let mut undated = make_reference("r3", "book", "Brown", "Book A", 2000);
648 if let ClassExtension::Monograph(monograph) = undated.extension_mut() {
649 monograph.issued = citum_schema::reference::EdtfString(String::new());
650 }
651
652 let mut refs = vec![&undated, &dated_late, &dated_early];
653
654 let sort_spec = GroupSort {
655 template: vec![GroupSortKey {
656 key: GroupSortKeyType::Issued,
657 ascending: true,
658 order: None,
659 sort_order: None,
660 }],
661 };
662
663 refs = sorter.sort_references(refs, &sort_spec);
664
665 assert_eq!(refs[0].id().unwrap(), "r1");
666 assert_eq!(refs[1].id().unwrap(), "r2");
667 assert_eq!(refs[2].id().unwrap(), "r3");
668 }
669
670 #[test]
671 fn test_issued_sort_uses_created_when_issued_is_missing() {
672 let locale = make_locale();
673 let sorter = ReferenceSorter::new(&locale);
674
675 let dated = make_reference("r1", "book", "Smith", "Book D", 1999);
676 let mut created_only = make_reference("r2", "book", "Jones", "Book C", 2000);
677 if let ClassExtension::Monograph(monograph) = created_only.extension_mut() {
678 monograph.created = citum_schema::reference::EdtfString("1985".to_string());
679 monograph.issued = citum_schema::reference::EdtfString(String::new());
680 }
681
682 let mut refs = vec![&dated, &created_only];
683
684 let sort_spec = GroupSort {
685 template: vec![GroupSortKey {
686 key: GroupSortKeyType::Issued,
687 ascending: true,
688 order: None,
689 sort_order: None,
690 }],
691 };
692
693 refs = sorter.sort_references(refs, &sort_spec);
694
695 assert_eq!(refs[0].id().unwrap(), "r2");
696 assert_eq!(refs[1].id().unwrap(), "r1");
697 }
698
699 #[test]
700 fn test_composite_sort() {
701 let locale = make_locale();
702 let sorter = ReferenceSorter::new(&locale);
703
704 let smith2020 = make_reference("r1", "book", "Smith", "Title", 2020);
705 let smith2010 = make_reference("r2", "book", "Smith", "Title", 2010);
706 let jones2020 = make_reference("r3", "book", "Jones", "Title", 2020);
707
708 let mut refs = vec![&smith2020, &jones2020, &smith2010];
709
710 let sort_spec = GroupSort {
711 template: vec![
712 GroupSortKey {
713 key: GroupSortKeyType::Author,
714 ascending: true,
715 order: None,
716 sort_order: Some(NameSortOrder::FamilyGiven),
717 },
718 GroupSortKey {
719 key: GroupSortKeyType::Issued,
720 ascending: false, order: None,
722 sort_order: None,
723 },
724 ],
725 };
726
727 refs = sorter.sort_references(refs, &sort_spec);
728
729 assert_eq!(refs[0].id().unwrap(), "r3"); assert_eq!(refs[1].id().unwrap(), "r1"); assert_eq!(refs[2].id().unwrap(), "r2"); }
734
735 #[test]
736 fn test_author_sort_falls_back_to_title_for_missing_names() {
737 let locale = make_locale();
738 let sorter = ReferenceSorter::new(&locale);
739
740 let no_author = make_reference_no_author("r1", "legal-case", "Brown v. Board", 1954);
741 let brown = make_reference("r2", "book", "Brown", "Title", 2000);
742 let smith = make_reference("r3", "book", "Smith", "Title", 2000);
743
744 let mut refs = vec![&no_author, &smith, &brown];
745
746 let sort_spec = GroupSort {
747 template: vec![GroupSortKey {
748 key: GroupSortKeyType::Author,
749 ascending: true,
750 order: None,
751 sort_order: Some(NameSortOrder::FamilyGiven),
752 }],
753 };
754
755 refs = sorter.sort_references(refs, &sort_spec);
756
757 assert_eq!(refs[0].id().unwrap(), "r2"); assert_eq!(refs[1].id().unwrap(), "r1"); assert_eq!(refs[2].id().unwrap(), "r3"); }
761
762 #[test]
763 fn test_legal_citation_sort() {
764 let locale = make_locale();
765 let sorter = ReferenceSorter::new(&locale);
766
767 let case_a = make_reference("r1", "legal-case", "", "Doe v. Smith", 1990);
768 let case_b = make_reference("r2", "legal-case", "", "Brown v. Board", 1954);
769
770 let mut refs = vec![&case_a, &case_b];
771
772 let sort_spec = GroupSort {
773 template: vec![
774 GroupSortKey {
775 key: GroupSortKeyType::Title, ascending: true,
777 order: None,
778 sort_order: None,
779 },
780 GroupSortKey {
781 key: GroupSortKeyType::Issued,
782 ascending: true,
783 order: None,
784 sort_order: None,
785 },
786 ],
787 };
788
789 refs = sorter.sort_references(refs, &sort_spec);
790 assert_eq!(refs[0].id().unwrap(), "r2"); }
792
793 #[test]
794 fn test_legal_hierarchy_sort() {
795 let locale = make_locale();
796 let sorter = ReferenceSorter::new(&locale);
797
798 let statute = make_reference("r1", "statute", "", "Clean Air Act", 1970);
799 let case = make_reference("r2", "legal-case", "", "Roe v. Wade", 1973);
800 let treaty = make_reference("r3", "treaty", "", "Paris Agreement", 2015);
801
802 let mut refs = vec![&treaty, &case, &statute];
803
804 let sort_spec = GroupSort {
805 template: vec![GroupSortKey {
806 key: GroupSortKeyType::RefType,
807 ascending: true,
808 order: Some(vec![
809 "legal-case".to_string(),
810 "statute".to_string(),
811 "treaty".to_string(),
812 ]),
813 sort_order: None,
814 }],
815 };
816
817 refs = sorter.sort_references(refs, &sort_spec);
818
819 assert_eq!(refs[0].id().unwrap(), "r2");
821 assert_eq!(refs[1].id().unwrap(), "r1");
822 assert_eq!(refs[2].id().unwrap(), "r3");
823 }
824
825 #[test]
828 fn test_id_tiebreak_orders_equal_keys_by_id() {
829 let locale = make_locale();
830 let sorter = ReferenceSorter::new(&locale);
831
832 let c = make_reference("r-c", "book", "Smith", "Same Title", 2000);
833 let a = make_reference("r-a", "book", "Smith", "Same Title", 2000);
834 let b = make_reference("r-b", "book", "Smith", "Same Title", 2000);
835
836 let refs = vec![&c, &a, &b];
837
838 let sort_spec = GroupSort {
839 template: vec![GroupSortKey {
840 key: GroupSortKeyType::Author,
841 ascending: true,
842 order: None,
843 sort_order: Some(NameSortOrder::FamilyGiven),
844 }],
845 };
846
847 let sorted = sorter.sort_references_with_id_tiebreak(refs, &sort_spec);
848
849 assert_eq!(sorted[0].id().unwrap(), "r-a");
850 assert_eq!(sorted[1].id().unwrap(), "r-b");
851 assert_eq!(sorted[2].id().unwrap(), "r-c");
852 }
853
854 #[test]
857 fn test_id_tiebreak_places_missing_id_last() {
858 let locale = make_locale();
859 let sorter = ReferenceSorter::new(&locale);
860
861 let with_id = make_reference("r1", "book", "Smith", "Same Title", 2000);
862 let mut no_id = make_reference("r2", "book", "Smith", "Same Title", 2000);
863 if let ClassExtension::Monograph(monograph) = no_id.extension_mut() {
864 monograph.id = None;
865 }
866
867 let refs = vec![&with_id, &no_id];
868
869 let sort_spec = GroupSort {
870 template: vec![GroupSortKey {
871 key: GroupSortKeyType::Author,
872 ascending: true,
873 order: None,
874 sort_order: Some(NameSortOrder::FamilyGiven),
875 }],
876 };
877
878 let sorted = sorter.sort_references_with_id_tiebreak(refs, &sort_spec);
879
880 assert_eq!(sorted[0].id().unwrap(), "r1");
881 assert!(sorted[1].id().is_none());
882 }
883
884 #[test]
887 fn test_id_tiebreak_with_empty_template_keeps_registry_order() {
888 let locale = make_locale();
889 let sorter = ReferenceSorter::new(&locale);
890
891 let c = make_reference("r-c", "book", "Smith", "Title C", 2000);
892 let a = make_reference("r-a", "book", "Jones", "Title A", 2001);
893
894 let refs = vec![&c, &a];
895 let sort_spec = GroupSort { template: vec![] };
896
897 let sorted = sorter.sort_references_with_id_tiebreak(refs, &sort_spec);
898
899 assert_eq!(sorted[0].id().unwrap(), "r-c");
900 assert_eq!(sorted[1].id().unwrap(), "r-a");
901 }
902}