1use std::collections::HashMap;
19
20use citum_schema::grouping::{GroupSort, GroupSortKey, NameSortOrder, SortKey as GroupSortKeyType};
21use citum_schema::locale::Locale;
22use citum_schema::options::Config;
23#[cfg(test)]
24use citum_schema::reference::ClassExtension;
25
26use crate::reference::Reference;
27use crate::sort_support::{
28 SortKeyOptions, TextCollator, author_sort_key_opt_with_options, collator_locale_id,
29 normalize_sort_text, title_sort_key_with_options,
30};
31
32fn compare_optional_years(a_year: Option<i32>, b_year: Option<i32>) -> std::cmp::Ordering {
33 match (a_year, b_year) {
34 (Some(a), Some(b)) => a.cmp(&b),
35 (Some(_), None) => std::cmp::Ordering::Less,
36 (None, Some(_)) => std::cmp::Ordering::Greater,
37 (None, None) => std::cmp::Ordering::Equal,
38 }
39}
40
41pub struct ReferenceSorter<'a> {
43 locale: &'a Locale,
44 text_collator: TextCollator,
45 sort_key_options: SortKeyOptions,
46}
47
48struct CachedReference<'a> {
49 reference: &'a Reference,
50 sort_values: Vec<CachedSortValue>,
51 id: Option<String>,
56}
57
58enum CachedSortValue {
59 RefType { name: String, rank: Option<usize> },
60 OptionalText(Option<String>),
61 Text(String),
62 Issued(Option<i32>),
63}
64
65enum CompiledSortKey<'a> {
66 RefType {
67 ascending: bool,
68 rank_by_type: Option<HashMap<String, usize>>,
69 },
70 Author {
71 ascending: bool,
72 name_order: NameSortOrder,
73 },
74 Title {
75 ascending: bool,
76 },
77 Issued {
78 ascending: bool,
79 },
80 Field {
81 ascending: bool,
82 field_name: &'a str,
83 },
84}
85
86impl<'a> ReferenceSorter<'a> {
87 #[must_use]
89 pub fn new(locale: &'a Locale) -> Self {
90 Self {
91 locale,
92 text_collator: TextCollator::new(locale),
93 sort_key_options: SortKeyOptions::uniform(),
94 }
95 }
96
97 #[must_use]
99 pub fn with_bibliography_config(locale: &'a Locale, config: &Config) -> Self {
100 Self {
101 locale,
102 text_collator: TextCollator::new_for_locale_id(collator_locale_id(locale, config)),
103 sort_key_options: SortKeyOptions::from_config(config),
104 }
105 }
106
107 #[must_use]
116 pub fn sort_references<'b>(
117 &self,
118 references: Vec<&'b Reference>,
119 sort_spec: &GroupSort,
120 ) -> Vec<&'b Reference> {
121 self.sort_references_impl(references, sort_spec, false)
122 }
123
124 #[must_use]
135 pub fn sort_references_with_id_tiebreak<'b>(
136 &self,
137 references: Vec<&'b Reference>,
138 sort_spec: &GroupSort,
139 ) -> Vec<&'b Reference> {
140 self.sort_references_impl(references, sort_spec, true)
141 }
142
143 fn sort_references_impl<'b>(
144 &self,
145 mut references: Vec<&'b Reference>,
146 sort_spec: &GroupSort,
147 id_tiebreak: bool,
148 ) -> Vec<&'b Reference> {
149 let compiled_keys = self.compile_sort_keys(sort_spec);
150 if compiled_keys.is_empty() {
151 return references;
152 }
153
154 let mut cached_references = references
155 .drain(..)
156 .map(|reference| CachedReference {
157 reference,
158 sort_values: compiled_keys
159 .iter()
160 .map(|sort_key| self.cache_sort_value(reference, sort_key))
161 .collect(),
162 id: id_tiebreak.then(|| reference.id().map(|id| id.0)).flatten(),
163 })
164 .collect::<Vec<_>>();
165
166 cached_references.sort_by(|a, b| {
167 let cmp = self.compare_cached_references(a, b, &compiled_keys);
168 if cmp != std::cmp::Ordering::Equal || !id_tiebreak {
169 return cmp;
170 }
171 Self::compare_cached_ids(a, b)
172 });
173 cached_references
174 .into_iter()
175 .map(|entry| entry.reference)
176 .collect()
177 }
178
179 fn compare_cached_ids(a: &CachedReference<'_>, b: &CachedReference<'_>) -> std::cmp::Ordering {
183 match (&a.id, &b.id) {
184 (Some(a_id), Some(b_id)) => a_id.as_str().cmp(b_id.as_str()),
185 (Some(_), None) => std::cmp::Ordering::Less,
186 (None, Some(_)) => std::cmp::Ordering::Greater,
187 (None, None) => std::cmp::Ordering::Equal,
188 }
189 }
190
191 #[must_use]
193 pub fn compare_by_key(
194 &self,
195 a: &Reference,
196 b: &Reference,
197 sort_key: &GroupSortKey,
198 ) -> std::cmp::Ordering {
199 self.compare_by_key_with_context(a, b, sort_key)
200 }
201
202 fn compare_by_key_with_context(
203 &self,
204 a: &Reference,
205 b: &Reference,
206 sort_key: &GroupSortKey,
207 ) -> std::cmp::Ordering {
208 let cmp = match &sort_key.key {
209 GroupSortKeyType::RefType => sort_key.order.as_ref().map_or_else(
210 || a.ref_type().cmp(&b.ref_type()),
211 |order| Self::compare_by_type_order(a, b, order),
212 ),
213 GroupSortKeyType::Author => sort_key.sort_order.as_ref().map_or_else(
214 || self.compare_by_author_with_order(a, b, NameSortOrder::FamilyGiven),
215 |name_order| self.compare_by_author_with_order(a, b, *name_order),
216 ),
217 GroupSortKeyType::Title => self.compare_by_title(a, b),
218 GroupSortKeyType::Issued => Self::compare_by_issued(a, b),
219 GroupSortKeyType::Field(field_name) => Self::compare_by_field(a, b, field_name),
220 };
221
222 if sort_key.ascending {
223 cmp
224 } else {
225 cmp.reverse()
226 }
227 }
228
229 fn compare_by_type_order(a: &Reference, b: &Reference, order: &[String]) -> std::cmp::Ordering {
234 let a_type = a.ref_type();
235 let b_type = b.ref_type();
236
237 let a_pos = order.iter().position(|t| t == &a_type);
238 let b_pos = order.iter().position(|t| t == &b_type);
239
240 match (a_pos, b_pos) {
241 (Some(a_idx), Some(b_idx)) => a_idx.cmp(&b_idx),
242 (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => a_type.cmp(&b_type), }
246 }
247
248 fn compile_sort_keys<'b>(&self, sort_spec: &'b GroupSort) -> Vec<CompiledSortKey<'b>> {
249 sort_spec
250 .template
251 .iter()
252 .map(|sort_key| match &sort_key.key {
253 GroupSortKeyType::RefType => CompiledSortKey::RefType {
254 ascending: sort_key.ascending,
255 rank_by_type: sort_key.order.as_ref().map(|order| {
256 order
257 .iter()
258 .enumerate()
259 .map(|(index, ref_type)| (ref_type.clone(), index))
260 .collect()
261 }),
262 },
263 GroupSortKeyType::Author => CompiledSortKey::Author {
264 ascending: sort_key.ascending,
265 name_order: sort_key.sort_order.unwrap_or(NameSortOrder::FamilyGiven),
266 },
267 GroupSortKeyType::Title => CompiledSortKey::Title {
268 ascending: sort_key.ascending,
269 },
270 GroupSortKeyType::Issued => CompiledSortKey::Issued {
271 ascending: sort_key.ascending,
272 },
273 GroupSortKeyType::Field(field_name) => CompiledSortKey::Field {
274 ascending: sort_key.ascending,
275 field_name,
276 },
277 })
278 .collect()
279 }
280
281 fn cache_sort_value(
282 &self,
283 reference: &Reference,
284 sort_key: &CompiledSortKey<'_>,
285 ) -> CachedSortValue {
286 match sort_key {
287 CompiledSortKey::RefType { rank_by_type, .. } => {
288 let ref_type = reference.ref_type();
289 CachedSortValue::RefType {
290 name: ref_type.clone(),
291 rank: rank_by_type
292 .as_ref()
293 .and_then(|ranks| ranks.get(&ref_type).copied()),
294 }
295 }
296 CompiledSortKey::Author { name_order, .. } => CachedSortValue::OptionalText(
297 self.extract_author_sort_key_opt(reference, *name_order),
298 ),
299 CompiledSortKey::Title { .. } => CachedSortValue::Text(self.title_sort_key(reference)),
300 CompiledSortKey::Issued { .. } => CachedSortValue::Issued(Self::issued_year(reference)),
301 CompiledSortKey::Field { field_name, .. } => {
302 CachedSortValue::Text(Self::field_sort_value(reference, field_name))
303 }
304 }
305 }
306
307 fn compare_cached_references(
308 &self,
309 a: &CachedReference<'_>,
310 b: &CachedReference<'_>,
311 compiled_keys: &[CompiledSortKey<'_>],
312 ) -> std::cmp::Ordering {
313 for (index, sort_key) in compiled_keys.iter().enumerate() {
314 #[allow(
315 clippy::indexing_slicing,
316 reason = "index is derived from compiled_keys"
317 )]
318 let cmp = self.compare_cached_value(&a.sort_values[index], &b.sort_values[index]);
319 let cmp = if Self::is_ascending(sort_key) {
320 cmp
321 } else {
322 cmp.reverse()
323 };
324
325 if cmp != std::cmp::Ordering::Equal {
326 return cmp;
327 }
328 }
329
330 std::cmp::Ordering::Equal
331 }
332
333 fn compare_cached_value(&self, a: &CachedSortValue, b: &CachedSortValue) -> std::cmp::Ordering {
334 match (a, b) {
335 (
336 CachedSortValue::RefType {
337 name: a_name,
338 rank: a_rank,
339 },
340 CachedSortValue::RefType {
341 name: b_name,
342 rank: b_rank,
343 },
344 ) => match (a_rank, b_rank) {
345 (Some(a_idx), Some(b_idx)) => a_idx.cmp(b_idx),
346 (Some(_), None) => std::cmp::Ordering::Less,
347 (None, Some(_)) => std::cmp::Ordering::Greater,
348 (None, None) => a_name.cmp(b_name),
349 },
350 (CachedSortValue::OptionalText(a_text), CachedSortValue::OptionalText(b_text)) => {
351 match (a_text, b_text) {
352 (Some(a_value), Some(b_value)) => self.text_collator.compare(a_value, b_value),
353 (Some(_), None) => std::cmp::Ordering::Less,
354 (None, Some(_)) => std::cmp::Ordering::Greater,
355 (None, None) => std::cmp::Ordering::Equal,
356 }
357 }
358 (CachedSortValue::Text(a_text), CachedSortValue::Text(b_text)) => {
359 self.text_collator.compare(a_text, b_text)
360 }
361 (CachedSortValue::Issued(a_year), CachedSortValue::Issued(b_year)) => {
362 compare_optional_years(*a_year, *b_year)
363 }
364 _ => std::cmp::Ordering::Equal,
365 }
366 }
367
368 fn is_ascending(sort_key: &CompiledSortKey<'_>) -> bool {
369 match sort_key {
370 CompiledSortKey::RefType { ascending, .. }
371 | CompiledSortKey::Author { ascending, .. }
372 | CompiledSortKey::Title { ascending }
373 | CompiledSortKey::Issued { ascending }
374 | CompiledSortKey::Field { ascending, .. } => *ascending,
375 }
376 }
377
378 fn compare_by_author_with_order(
380 &self,
381 a: &Reference,
382 b: &Reference,
383 name_order: NameSortOrder,
384 ) -> std::cmp::Ordering {
385 let a_key = self.extract_author_sort_key_opt(a, name_order);
386 let b_key = self.extract_author_sort_key_opt(b, name_order);
387 match (a_key, b_key) {
388 (Some(a), Some(b)) => self.text_collator.compare(&a, &b),
389 (Some(_), None) => std::cmp::Ordering::Less,
390 (None, Some(_)) => std::cmp::Ordering::Greater,
391 (None, None) => std::cmp::Ordering::Equal,
392 }
393 }
394
395 fn extract_author_sort_key_opt(
401 &self,
402 reference: &Reference,
403 name_order: NameSortOrder,
404 ) -> Option<String> {
405 author_sort_key_opt_with_options(
406 reference,
407 name_order,
408 self.locale,
409 true,
410 &self.sort_key_options,
411 )
412 }
413
414 #[must_use]
416 pub fn extract_author_sort_key(
417 &self,
418 reference: &Reference,
419 name_order: NameSortOrder,
420 ) -> String {
421 self.extract_author_sort_key_opt(reference, name_order)
422 .unwrap_or_default()
423 }
424
425 fn compare_by_title(&self, a: &Reference, b: &Reference) -> std::cmp::Ordering {
427 let a_title = self.title_sort_key(a);
428 let b_title = self.title_sort_key(b);
429 self.text_collator.compare(&a_title, &b_title)
430 }
431
432 fn compare_by_issued(a: &Reference, b: &Reference) -> std::cmp::Ordering {
434 let a_year = Self::issued_year(a);
435 let b_year = Self::issued_year(b);
436 compare_optional_years(a_year, b_year)
437 }
438
439 fn compare_by_field(a: &Reference, b: &Reference, field_name: &str) -> std::cmp::Ordering {
441 Self::field_sort_value(a, field_name).cmp(&Self::field_sort_value(b, field_name))
442 }
443
444 fn title_sort_key(&self, reference: &Reference) -> String {
445 title_sort_key_with_options(reference, self.locale, &self.sort_key_options)
446 }
447
448 fn issued_year(reference: &Reference) -> Option<i32> {
449 reference
450 .effective_issued_date()
451 .and_then(|d| d.year().parse::<i32>().ok())
452 .filter(|year| *year != 0)
453 }
454
455 fn field_sort_value(reference: &Reference, field_name: &str) -> String {
456 match field_name {
457 "language" => normalize_sort_text(reference.language().unwrap_or_default().as_ref()),
458 _ => String::new(),
460 }
461 }
462}
463
464#[cfg(test)]
465#[allow(
466 clippy::unwrap_used,
467 clippy::expect_used,
468 clippy::panic,
469 clippy::indexing_slicing,
470 clippy::todo,
471 clippy::unimplemented,
472 clippy::unreachable,
473 clippy::get_unwrap,
474 reason = "Panicking is acceptable and often desired in tests."
475)]
476mod tests {
477 use super::*;
478 use citum_schema::grouping::GroupSortKey;
479 use citum_schema::options::{MultilingualConfig, SortingConfig, SortingMultilingualMode};
480 use citum_schema::reference::contributor::MultilingualName;
481 use citum_schema::reference::types::MultilingualComplex;
482 use citum_schema::reference::{
483 Contributor, ContributorList, EdtfString, Monograph, MonographType, MultilingualString,
484 StructuredName, Title,
485 };
486 use std::collections::HashMap;
487
488 fn make_locale() -> Locale {
489 Locale::en_us()
490 }
491
492 fn make_reference(
493 id: &str,
494 ref_type: &str,
495 author_family: &str,
496 title: &str,
497 year: i32,
498 ) -> Reference {
499 let json = serde_json::json!({
500 "id": id,
501 "type": ref_type,
502 "author": [{"family": author_family, "given": "Test"}],
503 "issued": {"date-parts": [[year]]},
504 "title": title,
505 "container-title": "Test Container",
506 });
507 let legacy: csl_legacy::csl_json::Reference = serde_json::from_value(json).unwrap();
508 legacy.into()
509 }
510
511 fn make_reference_no_author(id: &str, ref_type: &str, title: &str, year: i32) -> Reference {
512 let json = serde_json::json!({
513 "id": id,
514 "type": ref_type,
515 "issued": {"date-parts": [[year]]},
516 "title": title,
517 "container-title": "Test Container",
518 });
519 let legacy: csl_legacy::csl_json::Reference = serde_json::from_value(json).unwrap();
520 legacy.into()
521 }
522
523 fn romanized_config() -> Config {
524 Config {
525 sorting: Some(SortingConfig {
526 multilingual: Some(SortingMultilingualMode::Romanized),
527 ..Default::default()
528 }),
529 multilingual: Some(MultilingualConfig {
530 preferred_transliteration: Some(vec!["ru-Latn-alalc97".to_string()]),
531 ..Default::default()
532 }),
533 ..Default::default()
534 }
535 }
536
537 fn multilingual_author_reference(
538 id: &str,
539 original_family: MultilingualString,
540 sort_as: Option<&str>,
541 transliteration_family: Option<&str>,
542 title: Title,
543 ) -> Reference {
544 let transliterations = transliteration_family.map_or_else(HashMap::new, |family| {
545 HashMap::from([(
546 "ru-Latn-alalc97".to_string(),
547 StructuredName {
548 family: family.into(),
549 given: "Lev".into(),
550 ..Default::default()
551 },
552 )])
553 });
554
555 Reference::Monograph(Box::new(Monograph {
556 id: Some(id.into()),
557 r#type: MonographType::Book,
558 title: Some(title),
559 author: Some(Contributor::ContributorList(ContributorList(vec![
560 Contributor::Multilingual(MultilingualName {
561 original: StructuredName {
562 family: original_family,
563 given: "Лев".into(),
564 ..Default::default()
565 },
566 lang: Some("ru".into()),
567 sort_as: sort_as.map(str::to_string),
568 transliterations,
569 translations: HashMap::new(),
570 }),
571 ]))),
572 issued: EdtfString("1869".to_string()),
573 ..Default::default()
574 }))
575 }
576
577 fn title_sort(sorter: &ReferenceSorter<'_>, references: Vec<&Reference>) -> Vec<String> {
578 let sort_spec = GroupSort {
579 template: vec![GroupSortKey {
580 key: GroupSortKeyType::Title,
581 ascending: true,
582 order: None,
583 sort_order: None,
584 }],
585 };
586
587 sorter
588 .sort_references(references, &sort_spec)
589 .into_iter()
590 .map(|reference| reference.id().expect("test reference id").to_string())
591 .collect()
592 }
593
594 #[test]
595 fn test_type_order_sorting() {
596 let locale = make_locale();
597 let sorter = ReferenceSorter::new(&locale);
598
599 let journal = make_reference("r1", "article-journal", "Smith", "Title J", 1990);
601 let magazine = make_reference("r2", "article-magazine", "Jones", "Title M", 2000);
602 let newspaper = make_reference("r3", "article-newspaper", "Brown", "Title N", 1985);
603 let book = make_reference("r4", "book", "Davis", "Title B", 1995);
604
605 let mut refs = vec![&book, &newspaper, &journal, &magazine];
606
607 let sort_spec = GroupSort {
608 template: vec![GroupSortKey {
609 key: GroupSortKeyType::RefType,
610 ascending: true,
611 order: Some(vec![
612 "article-journal".to_string(),
613 "article-magazine".to_string(),
614 "article-newspaper".to_string(),
615 ]),
616 sort_order: None,
617 }],
618 };
619
620 refs = sorter.sort_references(refs, &sort_spec);
621
622 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"); }
628
629 #[test]
630 fn test_author_family_given_order() {
631 let locale = make_locale();
632 let sorter = ReferenceSorter::new(&locale);
633
634 let smith = make_reference("r1", "book", "Smith", "Title", 2000);
635 let jones = make_reference("r2", "book", "Jones", "Title", 2000);
636 let brown = make_reference("r3", "book", "Brown", "Title", 2000);
637
638 let mut refs = vec![&smith, &jones, &brown];
639
640 let sort_spec = GroupSort {
641 template: vec![GroupSortKey {
642 key: GroupSortKeyType::Author,
643 ascending: true,
644 order: None,
645 sort_order: Some(NameSortOrder::FamilyGiven),
646 }],
647 };
648
649 refs = sorter.sort_references(refs, &sort_spec);
650
651 assert_eq!(refs[0].id().unwrap(), "r3"); assert_eq!(refs[1].id().unwrap(), "r2"); assert_eq!(refs[2].id().unwrap(), "r1"); }
656
657 #[test]
658 #[cfg(feature = "icu")]
659 fn test_author_sort_uses_unicode_collation_for_accented_names() {
660 let locale = make_locale();
661 let sorter = ReferenceSorter::new(&locale);
662
663 let celik = make_reference("r1", "book", "Çelik", "Title", 2000);
664 let zimring = make_reference("r2", "book", "Zimring", "Title", 2000);
665 let o_tuathail = make_reference("r3", "book", "Ó Tuathail", "Title", 2000);
666
667 let mut refs = vec![&o_tuathail, &zimring, &celik];
668
669 let sort_spec = GroupSort {
670 template: vec![GroupSortKey {
671 key: GroupSortKeyType::Author,
672 ascending: true,
673 order: None,
674 sort_order: Some(NameSortOrder::FamilyGiven),
675 }],
676 };
677
678 refs = sorter.sort_references(refs, &sort_spec);
679
680 assert_eq!(refs[0].id().unwrap(), "r1");
681 assert_eq!(refs[1].id().unwrap(), "r3");
682 assert_eq!(refs[2].id().unwrap(), "r2");
683 }
684
685 #[test]
686 #[cfg(feature = "icu")]
687 fn test_title_sort_uses_unicode_collation_for_accented_titles() {
688 let locale = make_locale();
689 let sorter = ReferenceSorter::new(&locale);
690
691 let accent = make_reference_no_author("r1", "book", "Órbitas del sur", 2000);
692 let plain = make_reference_no_author("r2", "book", "Origins of Theory", 2000);
693 let zeta = make_reference_no_author("r3", "book", "Zebra Studies", 2000);
694
695 let mut refs = vec![&zeta, &plain, &accent];
696
697 let sort_spec = GroupSort {
698 template: vec![GroupSortKey {
699 key: GroupSortKeyType::Title,
700 ascending: true,
701 order: None,
702 sort_order: None,
703 }],
704 };
705
706 refs = sorter.sort_references(refs, &sort_spec);
707
708 assert_eq!(refs[0].id().unwrap(), "r1");
709 assert_eq!(refs[1].id().unwrap(), "r2");
710 assert_eq!(refs[2].id().unwrap(), "r3");
711 }
712
713 #[test]
714 fn test_issued_descending() {
715 let locale = make_locale();
716 let sorter = ReferenceSorter::new(&locale);
717
718 let old = make_reference("r1", "book", "Smith", "Title", 1990);
719 let new = make_reference("r2", "book", "Jones", "Title", 2020);
720 let mid = make_reference("r3", "book", "Brown", "Title", 2005);
721
722 let mut refs = vec![&old, &new, &mid];
723
724 let sort_spec = GroupSort {
725 template: vec![GroupSortKey {
726 key: GroupSortKeyType::Issued,
727 ascending: false, order: None,
729 sort_order: None,
730 }],
731 };
732
733 refs = sorter.sort_references(refs, &sort_spec);
734
735 assert_eq!(refs[0].id().unwrap(), "r2"); assert_eq!(refs[1].id().unwrap(), "r3"); assert_eq!(refs[2].id().unwrap(), "r1"); }
740
741 #[test]
742 fn test_issued_ascending_places_undated_last() {
743 let locale = make_locale();
744 let sorter = ReferenceSorter::new(&locale);
745
746 let dated_early = make_reference("r1", "book", "Smith", "Book D", 1999);
747 let dated_late = make_reference("r2", "book", "Jones", "Book B", 2000);
748 let mut undated = make_reference("r3", "book", "Brown", "Book A", 2000);
749 if let ClassExtension::Monograph(monograph) = undated.extension_mut() {
750 monograph.issued = citum_schema::reference::EdtfString(String::new());
751 }
752
753 let mut refs = vec![&undated, &dated_late, &dated_early];
754
755 let sort_spec = GroupSort {
756 template: vec![GroupSortKey {
757 key: GroupSortKeyType::Issued,
758 ascending: true,
759 order: None,
760 sort_order: None,
761 }],
762 };
763
764 refs = sorter.sort_references(refs, &sort_spec);
765
766 assert_eq!(refs[0].id().unwrap(), "r1");
767 assert_eq!(refs[1].id().unwrap(), "r2");
768 assert_eq!(refs[2].id().unwrap(), "r3");
769 }
770
771 #[test]
772 fn test_issued_sort_uses_created_when_issued_is_missing() {
773 let locale = make_locale();
774 let sorter = ReferenceSorter::new(&locale);
775
776 let dated = make_reference("r1", "book", "Smith", "Book D", 1999);
777 let mut created_only = make_reference("r2", "book", "Jones", "Book C", 2000);
778 if let ClassExtension::Monograph(monograph) = created_only.extension_mut() {
779 monograph.created = citum_schema::reference::EdtfString("1985".to_string());
780 monograph.issued = citum_schema::reference::EdtfString(String::new());
781 }
782
783 let mut refs = vec![&dated, &created_only];
784
785 let sort_spec = GroupSort {
786 template: vec![GroupSortKey {
787 key: GroupSortKeyType::Issued,
788 ascending: true,
789 order: None,
790 sort_order: None,
791 }],
792 };
793
794 refs = sorter.sort_references(refs, &sort_spec);
795
796 assert_eq!(refs[0].id().unwrap(), "r2");
797 assert_eq!(refs[1].id().unwrap(), "r1");
798 }
799
800 #[test]
801 fn test_composite_sort() {
802 let locale = make_locale();
803 let sorter = ReferenceSorter::new(&locale);
804
805 let smith2020 = make_reference("r1", "book", "Smith", "Title", 2020);
806 let smith2010 = make_reference("r2", "book", "Smith", "Title", 2010);
807 let jones2020 = make_reference("r3", "book", "Jones", "Title", 2020);
808
809 let mut refs = vec![&smith2020, &jones2020, &smith2010];
810
811 let sort_spec = GroupSort {
812 template: vec![
813 GroupSortKey {
814 key: GroupSortKeyType::Author,
815 ascending: true,
816 order: None,
817 sort_order: Some(NameSortOrder::FamilyGiven),
818 },
819 GroupSortKey {
820 key: GroupSortKeyType::Issued,
821 ascending: false, order: None,
823 sort_order: None,
824 },
825 ],
826 };
827
828 refs = sorter.sort_references(refs, &sort_spec);
829
830 assert_eq!(refs[0].id().unwrap(), "r3"); assert_eq!(refs[1].id().unwrap(), "r1"); assert_eq!(refs[2].id().unwrap(), "r2"); }
835
836 #[test]
837 fn test_author_sort_falls_back_to_title_for_missing_names() {
838 let locale = make_locale();
839 let sorter = ReferenceSorter::new(&locale);
840
841 let no_author = make_reference_no_author("r1", "legal-case", "Brown v. Board", 1954);
842 let brown = make_reference("r2", "book", "Brown", "Title", 2000);
843 let smith = make_reference("r3", "book", "Smith", "Title", 2000);
844
845 let mut refs = vec![&no_author, &smith, &brown];
846
847 let sort_spec = GroupSort {
848 template: vec![GroupSortKey {
849 key: GroupSortKeyType::Author,
850 ascending: true,
851 order: None,
852 sort_order: Some(NameSortOrder::FamilyGiven),
853 }],
854 };
855
856 refs = sorter.sort_references(refs, &sort_spec);
857
858 assert_eq!(refs[0].id().unwrap(), "r2"); assert_eq!(refs[1].id().unwrap(), "r1"); assert_eq!(refs[2].id().unwrap(), "r3"); }
862
863 #[test]
864 fn test_legal_citation_sort() {
865 let locale = make_locale();
866 let sorter = ReferenceSorter::new(&locale);
867
868 let case_a = make_reference("r1", "legal-case", "", "Doe v. Smith", 1990);
869 let case_b = make_reference("r2", "legal-case", "", "Brown v. Board", 1954);
870
871 let mut refs = vec![&case_a, &case_b];
872
873 let sort_spec = GroupSort {
874 template: vec![
875 GroupSortKey {
876 key: GroupSortKeyType::Title, ascending: true,
878 order: None,
879 sort_order: None,
880 },
881 GroupSortKey {
882 key: GroupSortKeyType::Issued,
883 ascending: true,
884 order: None,
885 sort_order: None,
886 },
887 ],
888 };
889
890 refs = sorter.sort_references(refs, &sort_spec);
891 assert_eq!(refs[0].id().unwrap(), "r2"); }
893
894 #[test]
895 fn test_legal_hierarchy_sort() {
896 let locale = make_locale();
897 let sorter = ReferenceSorter::new(&locale);
898
899 let statute = make_reference("r1", "statute", "", "Clean Air Act", 1970);
900 let case = make_reference("r2", "legal-case", "", "Roe v. Wade", 1973);
901 let treaty = make_reference("r3", "treaty", "", "Paris Agreement", 2015);
902
903 let mut refs = vec![&treaty, &case, &statute];
904
905 let sort_spec = GroupSort {
906 template: vec![GroupSortKey {
907 key: GroupSortKeyType::RefType,
908 ascending: true,
909 order: Some(vec![
910 "legal-case".to_string(),
911 "statute".to_string(),
912 "treaty".to_string(),
913 ]),
914 sort_order: None,
915 }],
916 };
917
918 refs = sorter.sort_references(refs, &sort_spec);
919
920 assert_eq!(refs[0].id().unwrap(), "r2");
922 assert_eq!(refs[1].id().unwrap(), "r1");
923 assert_eq!(refs[2].id().unwrap(), "r3");
924 }
925
926 #[test]
929 fn test_id_tiebreak_orders_equal_keys_by_id() {
930 let locale = make_locale();
931 let sorter = ReferenceSorter::new(&locale);
932
933 let c = make_reference("r-c", "book", "Smith", "Same Title", 2000);
934 let a = make_reference("r-a", "book", "Smith", "Same Title", 2000);
935 let b = make_reference("r-b", "book", "Smith", "Same Title", 2000);
936
937 let refs = vec![&c, &a, &b];
938
939 let sort_spec = GroupSort {
940 template: vec![GroupSortKey {
941 key: GroupSortKeyType::Author,
942 ascending: true,
943 order: None,
944 sort_order: Some(NameSortOrder::FamilyGiven),
945 }],
946 };
947
948 let sorted = sorter.sort_references_with_id_tiebreak(refs, &sort_spec);
949
950 assert_eq!(sorted[0].id().unwrap(), "r-a");
951 assert_eq!(sorted[1].id().unwrap(), "r-b");
952 assert_eq!(sorted[2].id().unwrap(), "r-c");
953 }
954
955 #[test]
958 fn test_id_tiebreak_places_missing_id_last() {
959 let locale = make_locale();
960 let sorter = ReferenceSorter::new(&locale);
961
962 let with_id = make_reference("r1", "book", "Smith", "Same Title", 2000);
963 let mut no_id = make_reference("r2", "book", "Smith", "Same Title", 2000);
964 if let ClassExtension::Monograph(monograph) = no_id.extension_mut() {
965 monograph.id = None;
966 }
967
968 let refs = vec![&with_id, &no_id];
969
970 let sort_spec = GroupSort {
971 template: vec![GroupSortKey {
972 key: GroupSortKeyType::Author,
973 ascending: true,
974 order: None,
975 sort_order: Some(NameSortOrder::FamilyGiven),
976 }],
977 };
978
979 let sorted = sorter.sort_references_with_id_tiebreak(refs, &sort_spec);
980
981 assert_eq!(sorted[0].id().unwrap(), "r1");
982 assert!(sorted[1].id().is_none());
983 }
984
985 #[test]
988 fn test_id_tiebreak_with_empty_template_keeps_registry_order() {
989 let locale = make_locale();
990 let sorter = ReferenceSorter::new(&locale);
991
992 let c = make_reference("r-c", "book", "Smith", "Title C", 2000);
993 let a = make_reference("r-a", "book", "Jones", "Title A", 2001);
994
995 let refs = vec![&c, &a];
996 let sort_spec = GroupSort { template: vec![] };
997
998 let sorted = sorter.sort_references_with_id_tiebreak(refs, &sort_spec);
999
1000 assert_eq!(sorted[0].id().unwrap(), "r-c");
1001 assert_eq!(sorted[1].id().unwrap(), "r-a");
1002 }
1003
1004 #[test]
1005 fn romanized_author_sort_uses_hidden_sort_as() {
1006 let locale = make_locale();
1007 let config = romanized_config();
1008 let sorter = ReferenceSorter::with_bibliography_config(&locale, &config);
1009 let reference = multilingual_author_reference(
1010 "tolstoy",
1011 "Толстой".into(),
1012 Some("Tolstoy"),
1013 Some("Tolstoĭ"),
1014 Title::Single("War and Peace".to_string()),
1015 );
1016
1017 assert_eq!(
1018 sorter.extract_author_sort_key(&reference, NameSortOrder::FamilyGiven),
1019 "Tolstoy"
1020 );
1021 }
1022
1023 #[test]
1024 fn uniform_author_sort_ignores_hidden_sort_as() {
1025 let locale = make_locale();
1026 let sorter = ReferenceSorter::new(&locale);
1027 let reference = multilingual_author_reference(
1028 "tolstoy",
1029 "Толстой".into(),
1030 Some("Tolstoy"),
1031 Some("Tolstoĭ"),
1032 Title::Single("War and Peace".to_string()),
1033 );
1034
1035 assert_eq!(
1036 sorter.extract_author_sort_key(&reference, NameSortOrder::FamilyGiven),
1037 "Толстой"
1038 );
1039 }
1040
1041 #[test]
1042 fn romanized_author_sort_falls_back_to_matched_transliteration() {
1043 let locale = make_locale();
1044 let config = romanized_config();
1045 let sorter = ReferenceSorter::with_bibliography_config(&locale, &config);
1046 let reference = multilingual_author_reference(
1047 "tolstoy",
1048 "Толстой".into(),
1049 None,
1050 Some("Tolstoĭ"),
1051 Title::Single("War and Peace".to_string()),
1052 );
1053
1054 assert_eq!(
1055 sorter.extract_author_sort_key(&reference, NameSortOrder::FamilyGiven),
1056 "Tolstoĭ"
1057 );
1058 }
1059
1060 #[test]
1061 fn holistic_sort_as_wins_over_part_level_sort_as() {
1062 let locale = make_locale();
1063 let config = romanized_config();
1064 let sorter = ReferenceSorter::with_bibliography_config(&locale, &config);
1065 let family = MultilingualString::Complex(MultilingualComplex {
1066 original: "Толстой".to_string(),
1067 lang: Some("ru".into()),
1068 sort_as: Some("Part Level".to_string()),
1069 transliterations: HashMap::new(),
1070 translations: HashMap::new(),
1071 });
1072 let reference = multilingual_author_reference(
1073 "tolstoy",
1074 family,
1075 Some("Whole Name"),
1076 Some("Tolstoĭ"),
1077 Title::Single("War and Peace".to_string()),
1078 );
1079
1080 assert_eq!(
1081 sorter.extract_author_sort_key(&reference, NameSortOrder::FamilyGiven),
1082 "Whole Name"
1083 );
1084 }
1085
1086 #[test]
1087 fn title_sort_uses_hidden_sort_as_under_romanized_mode() {
1088 let locale = make_locale();
1089 let config = romanized_config();
1090 let sorter = ReferenceSorter::with_bibliography_config(&locale, &config);
1091 let cyrillic = multilingual_author_reference(
1092 "cyrillic-title",
1093 "Smith".into(),
1094 None,
1095 None,
1096 Title::Multilingual(MultilingualComplex {
1097 original: "Война и мир".to_string(),
1098 lang: Some("ru".into()),
1099 sort_as: Some("Academic War and Peace".to_string()),
1100 transliterations: HashMap::new(),
1101 translations: HashMap::new(),
1102 }),
1103 );
1104 let latin = make_reference_no_author("latin-title", "book", "Beta Studies", 2000);
1105
1106 assert_eq!(
1107 title_sort(&sorter, vec![&latin, &cyrillic]),
1108 vec!["cyrillic-title".to_string(), "latin-title".to_string()]
1109 );
1110 }
1111}