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, collator_locale_id, flat_names_sort_key, normalize_sort_text,
29 title_sort_key_with_options,
30};
31
32enum PrimaryContributorSpec<'a> {
33 Citation(&'a citum_schema::CitationSpec),
34 Bibliography(&'a citum_schema::BibliographySpec),
35}
36
37fn compare_optional_years(a_year: Option<i32>, b_year: Option<i32>) -> std::cmp::Ordering {
38 match (a_year, b_year) {
39 (Some(a), Some(b)) => a.cmp(&b),
40 (Some(_), None) => std::cmp::Ordering::Less,
41 (None, Some(_)) => std::cmp::Ordering::Greater,
42 (None, None) => std::cmp::Ordering::Equal,
43 }
44}
45
46pub struct ReferenceSorter<'a> {
48 locale: &'a Locale,
49 text_collator: TextCollator,
50 sort_key_options: SortKeyOptions,
51 config: Option<&'a Config>,
52 primary_contributor_spec: Option<PrimaryContributorSpec<'a>>,
53 primary_contributor_may_be_list: bool,
54}
55
56struct CachedReference<'a> {
57 reference: &'a Reference,
58 sort_values: Vec<CachedSortValue>,
59 id: Option<String>,
64}
65
66enum CachedSortValue {
67 RefType { name: String, rank: Option<usize> },
68 OptionalText(Option<String>),
69 Text(String),
70 Issued(Option<i32>),
71}
72
73enum CompiledSortKey<'a> {
74 RefType {
75 ascending: bool,
76 rank_by_type: Option<HashMap<String, usize>>,
77 },
78 Author {
79 ascending: bool,
80 name_order: NameSortOrder,
81 },
82 Title {
83 ascending: bool,
84 },
85 Issued {
86 ascending: bool,
87 },
88 Field {
89 ascending: bool,
90 field_name: &'a str,
91 },
92}
93
94impl<'a> ReferenceSorter<'a> {
95 #[must_use]
97 pub fn new(locale: &'a Locale) -> Self {
98 Self {
99 locale,
100 text_collator: TextCollator::new(locale),
101 sort_key_options: SortKeyOptions::uniform(),
102 config: None,
103 primary_contributor_spec: None,
104 primary_contributor_may_be_list: false,
105 }
106 }
107
108 #[must_use]
110 pub fn with_bibliography_config(locale: &'a Locale, config: &'a Config) -> Self {
111 Self {
112 locale,
113 text_collator: TextCollator::new_for_locale_id(collator_locale_id(locale, config)),
114 sort_key_options: SortKeyOptions::from_config(config),
115 config: Some(config),
116 primary_contributor_spec: None,
117 primary_contributor_may_be_list: false,
118 }
119 }
120
121 #[must_use]
123 pub fn with_bibliography_spec(mut self, spec: &'a citum_schema::BibliographySpec) -> Self {
124 self.primary_contributor_spec = Some(PrimaryContributorSpec::Bibliography(spec));
125 self.primary_contributor_may_be_list = bibliography_may_have_list_primary(spec);
126 self
127 }
128
129 #[must_use]
131 pub fn with_citation_spec(mut self, spec: &'a citum_schema::CitationSpec) -> Self {
132 self.primary_contributor_spec = Some(PrimaryContributorSpec::Citation(spec));
133 self.primary_contributor_may_be_list = citation_may_have_list_primary(spec);
134 self
135 }
136
137 #[must_use]
146 pub fn sort_references<'b>(
147 &self,
148 references: Vec<&'b Reference>,
149 sort_spec: &GroupSort,
150 ) -> Vec<&'b Reference> {
151 self.sort_references_impl(references, sort_spec, false)
152 }
153
154 #[must_use]
165 pub fn sort_references_with_id_tiebreak<'b>(
166 &self,
167 references: Vec<&'b Reference>,
168 sort_spec: &GroupSort,
169 ) -> Vec<&'b Reference> {
170 self.sort_references_impl(references, sort_spec, true)
171 }
172
173 fn sort_references_impl<'b>(
174 &self,
175 mut references: Vec<&'b Reference>,
176 sort_spec: &GroupSort,
177 id_tiebreak: bool,
178 ) -> Vec<&'b Reference> {
179 let compiled_keys = self.compile_sort_keys(&sort_spec.template);
180 if compiled_keys.is_empty() {
181 return references;
182 }
183
184 let mut cached_references = references
185 .drain(..)
186 .map(|reference| CachedReference {
187 reference,
188 sort_values: compiled_keys
189 .iter()
190 .map(|sort_key| self.cache_sort_value(reference, sort_key))
191 .collect(),
192 id: id_tiebreak.then(|| reference.id().map(|id| id.0)).flatten(),
193 })
194 .collect::<Vec<_>>();
195
196 cached_references.sort_by(|a, b| {
197 let cmp = self.compare_cached_references(a, b, &compiled_keys);
198 if cmp != std::cmp::Ordering::Equal || !id_tiebreak {
199 return cmp;
200 }
201 Self::compare_cached_ids(a, b)
202 });
203 cached_references
204 .into_iter()
205 .map(|entry| entry.reference)
206 .collect()
207 }
208
209 fn compare_cached_ids(a: &CachedReference<'_>, b: &CachedReference<'_>) -> std::cmp::Ordering {
213 match (&a.id, &b.id) {
214 (Some(a_id), Some(b_id)) => a_id.as_str().cmp(b_id.as_str()),
215 (Some(_), None) => std::cmp::Ordering::Less,
216 (None, Some(_)) => std::cmp::Ordering::Greater,
217 (None, None) => std::cmp::Ordering::Equal,
218 }
219 }
220
221 #[must_use]
223 pub fn compare_by_key(
224 &self,
225 a: &Reference,
226 b: &Reference,
227 sort_key: &GroupSortKey,
228 ) -> std::cmp::Ordering {
229 self.compare_by_key_with_context(a, b, sort_key)
230 }
231
232 pub(crate) fn sort_by_keys<T>(
247 &self,
248 mut items: Vec<T>,
249 template: &[GroupSortKey],
250 reference_of: impl Fn(&T) -> Option<&Reference>,
251 ) -> Vec<T> {
252 let compiled_keys = self.compile_sort_keys(template);
253 let mut decorated = items
254 .drain(..)
255 .map(|item| {
256 let sort_values = reference_of(&item).map(|reference| {
257 compiled_keys
258 .iter()
259 .map(|sort_key| self.cache_sort_value(reference, sort_key))
260 .collect::<Vec<_>>()
261 });
262 (item, sort_values)
263 })
264 .collect::<Vec<_>>();
265
266 decorated.sort_by(|(_, a), (_, b)| match (a, b) {
267 (Some(a), Some(b)) => self.compare_cached_values(a, b, &compiled_keys),
268 (Some(_), None) => std::cmp::Ordering::Less,
269 (None, Some(_)) => std::cmp::Ordering::Greater,
270 (None, None) => std::cmp::Ordering::Equal,
271 });
272
273 decorated.into_iter().map(|(item, _)| item).collect()
274 }
275
276 fn compare_by_key_with_context(
277 &self,
278 a: &Reference,
279 b: &Reference,
280 sort_key: &GroupSortKey,
281 ) -> std::cmp::Ordering {
282 let cmp = match &sort_key.key {
283 GroupSortKeyType::RefType => sort_key.order.as_ref().map_or_else(
284 || a.ref_type().cmp(&b.ref_type()),
285 |order| Self::compare_by_type_order(a, b, order),
286 ),
287 GroupSortKeyType::Author => sort_key.sort_order.as_ref().map_or_else(
288 || self.compare_by_author_with_order(a, b, NameSortOrder::FamilyGiven),
289 |name_order| self.compare_by_author_with_order(a, b, *name_order),
290 ),
291 GroupSortKeyType::Title => self.compare_by_title(a, b),
292 GroupSortKeyType::Issued => Self::compare_by_issued(a, b),
293 GroupSortKeyType::Field(field_name) => Self::compare_by_field(a, b, field_name),
294 };
295
296 if sort_key.ascending {
297 cmp
298 } else {
299 cmp.reverse()
300 }
301 }
302
303 fn compare_by_type_order(a: &Reference, b: &Reference, order: &[String]) -> std::cmp::Ordering {
308 let a_type = a.ref_type();
309 let b_type = b.ref_type();
310
311 let a_pos = order.iter().position(|t| t == &a_type);
312 let b_pos = order.iter().position(|t| t == &b_type);
313
314 match (a_pos, b_pos) {
315 (Some(a_idx), Some(b_idx)) => a_idx.cmp(&b_idx),
316 (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => a_type.cmp(&b_type), }
320 }
321
322 fn compile_sort_keys<'b>(&self, template: &'b [GroupSortKey]) -> Vec<CompiledSortKey<'b>> {
323 template
324 .iter()
325 .map(|sort_key| match &sort_key.key {
326 GroupSortKeyType::RefType => CompiledSortKey::RefType {
327 ascending: sort_key.ascending,
328 rank_by_type: sort_key.order.as_ref().map(|order| {
329 order
330 .iter()
331 .enumerate()
332 .map(|(index, ref_type)| (ref_type.clone(), index))
333 .collect()
334 }),
335 },
336 GroupSortKeyType::Author => CompiledSortKey::Author {
337 ascending: sort_key.ascending,
338 name_order: sort_key.sort_order.unwrap_or(NameSortOrder::FamilyGiven),
339 },
340 GroupSortKeyType::Title => CompiledSortKey::Title {
341 ascending: sort_key.ascending,
342 },
343 GroupSortKeyType::Issued => CompiledSortKey::Issued {
344 ascending: sort_key.ascending,
345 },
346 GroupSortKeyType::Field(field_name) => CompiledSortKey::Field {
347 ascending: sort_key.ascending,
348 field_name,
349 },
350 })
351 .collect()
352 }
353
354 fn cache_sort_value(
355 &self,
356 reference: &Reference,
357 sort_key: &CompiledSortKey<'_>,
358 ) -> CachedSortValue {
359 match sort_key {
360 CompiledSortKey::RefType { rank_by_type, .. } => {
361 let ref_type = reference.ref_type();
362 CachedSortValue::RefType {
363 name: ref_type.clone(),
364 rank: rank_by_type
365 .as_ref()
366 .and_then(|ranks| ranks.get(&ref_type).copied()),
367 }
368 }
369 CompiledSortKey::Author { name_order, .. } => CachedSortValue::OptionalText(
370 self.extract_author_sort_key_opt(reference, *name_order),
371 ),
372 CompiledSortKey::Title { .. } => CachedSortValue::Text(self.title_sort_key(reference)),
373 CompiledSortKey::Issued { .. } => CachedSortValue::Issued(Self::issued_year(reference)),
374 CompiledSortKey::Field { field_name, .. } => {
375 CachedSortValue::Text(Self::field_sort_value(reference, field_name))
376 }
377 }
378 }
379
380 fn compare_cached_references(
381 &self,
382 a: &CachedReference<'_>,
383 b: &CachedReference<'_>,
384 compiled_keys: &[CompiledSortKey<'_>],
385 ) -> std::cmp::Ordering {
386 self.compare_cached_values(&a.sort_values, &b.sort_values, compiled_keys)
387 }
388
389 fn compare_cached_values(
394 &self,
395 a: &[CachedSortValue],
396 b: &[CachedSortValue],
397 compiled_keys: &[CompiledSortKey<'_>],
398 ) -> std::cmp::Ordering {
399 for (index, sort_key) in compiled_keys.iter().enumerate() {
400 #[allow(
401 clippy::indexing_slicing,
402 reason = "index is derived from compiled_keys"
403 )]
404 let cmp = self.compare_cached_value(&a[index], &b[index]);
405 let cmp = if Self::is_ascending(sort_key) {
406 cmp
407 } else {
408 cmp.reverse()
409 };
410
411 if cmp != std::cmp::Ordering::Equal {
412 return cmp;
413 }
414 }
415
416 std::cmp::Ordering::Equal
417 }
418
419 fn compare_cached_value(&self, a: &CachedSortValue, b: &CachedSortValue) -> std::cmp::Ordering {
420 match (a, b) {
421 (
422 CachedSortValue::RefType {
423 name: a_name,
424 rank: a_rank,
425 },
426 CachedSortValue::RefType {
427 name: b_name,
428 rank: b_rank,
429 },
430 ) => match (a_rank, b_rank) {
431 (Some(a_idx), Some(b_idx)) => a_idx.cmp(b_idx),
432 (Some(_), None) => std::cmp::Ordering::Less,
433 (None, Some(_)) => std::cmp::Ordering::Greater,
434 (None, None) => a_name.cmp(b_name),
435 },
436 (CachedSortValue::OptionalText(a_text), CachedSortValue::OptionalText(b_text)) => {
437 match (a_text, b_text) {
438 (Some(a_value), Some(b_value)) => self.text_collator.compare(a_value, b_value),
439 (Some(_), None) => std::cmp::Ordering::Less,
440 (None, Some(_)) => std::cmp::Ordering::Greater,
441 (None, None) => std::cmp::Ordering::Equal,
442 }
443 }
444 (CachedSortValue::Text(a_text), CachedSortValue::Text(b_text)) => {
445 self.text_collator.compare(a_text, b_text)
446 }
447 (CachedSortValue::Issued(a_year), CachedSortValue::Issued(b_year)) => {
448 compare_optional_years(*a_year, *b_year)
449 }
450 _ => std::cmp::Ordering::Equal,
451 }
452 }
453
454 fn is_ascending(sort_key: &CompiledSortKey<'_>) -> bool {
455 match sort_key {
456 CompiledSortKey::RefType { ascending, .. }
457 | CompiledSortKey::Author { ascending, .. }
458 | CompiledSortKey::Title { ascending }
459 | CompiledSortKey::Issued { ascending }
460 | CompiledSortKey::Field { ascending, .. } => *ascending,
461 }
462 }
463
464 fn compare_by_author_with_order(
466 &self,
467 a: &Reference,
468 b: &Reference,
469 name_order: NameSortOrder,
470 ) -> std::cmp::Ordering {
471 let a_key = self.extract_author_sort_key_opt(a, name_order);
472 let b_key = self.extract_author_sort_key_opt(b, name_order);
473 match (a_key, b_key) {
474 (Some(a), Some(b)) => self.text_collator.compare(&a, &b),
475 (Some(_), None) => std::cmp::Ordering::Less,
476 (None, Some(_)) => std::cmp::Ordering::Greater,
477 (None, None) => std::cmp::Ordering::Equal,
478 }
479 }
480
481 fn extract_author_sort_key_opt(
487 &self,
488 reference: &Reference,
489 name_order: NameSortOrder,
490 ) -> Option<String> {
491 let default_config = Config::default();
492 let config = self.config.unwrap_or(&default_config);
493
494 if let Some(component) = self.primary_contributor_component(reference)
495 && component.contributor.is_multiple()
496 {
497 let names = crate::values::contributor::merged::semantic_names(
498 &component,
499 reference,
500 config,
501 self.locale,
502 );
503 if let Some(key) = flat_names_sort_key(&names, name_order) {
504 return Some(key);
505 }
506 }
513 let substitute =
514 citum_schema::options::SubstituteConfig::resolve_or_default(config.substitute.as_ref());
515 let primary_key = match crate::values::contributor::substitute::effective_primary(
516 reference,
517 substitute.as_ref(),
518 config,
519 self.locale,
520 ) {
521 Some(crate::values::contributor::substitute::EffectivePrimary::Contributor {
522 contributor,
523 ..
524 }) => crate::sort_support::contributor_sort_key(
525 &contributor,
526 name_order,
527 &self.sort_key_options,
528 ),
529 Some(crate::values::contributor::substitute::EffectivePrimary::Merged(roles)) => {
530 let names = crate::values::contributor::merged::semantic_names(
531 &citum_schema::template::TemplateContributor {
532 contributor: roles,
533 ..Default::default()
534 },
535 reference,
536 config,
537 self.locale,
538 );
539 flat_names_sort_key(&names, name_order)
540 }
541 Some(crate::values::contributor::substitute::EffectivePrimary::Title { .. }) | None => {
542 None
543 }
544 };
545 primary_key.or_else(|| {
546 Some(title_sort_key_with_options(
547 reference,
548 self.locale,
549 &self.sort_key_options,
550 ))
551 .filter(|key| !key.is_empty())
552 })
553 }
554
555 fn primary_contributor_component(
556 &self,
557 reference: &Reference,
558 ) -> Option<citum_schema::template::TemplateContributor> {
559 if !self.primary_contributor_may_be_list {
560 return None;
561 }
562 let language = reference.language().map(|language| language.to_string());
563 match self.primary_contributor_spec.as_ref()? {
564 PrimaryContributorSpec::Citation(spec) => {
565 primary_contributor_for_citation(spec, reference)
566 }
567 PrimaryContributorSpec::Bibliography(spec) => {
568 primary_contributor_for_bibliography(spec, reference, language.as_deref())
569 }
570 }
571 }
572
573 #[must_use]
575 pub fn extract_author_sort_key(
576 &self,
577 reference: &Reference,
578 name_order: NameSortOrder,
579 ) -> String {
580 self.extract_author_sort_key_opt(reference, name_order)
581 .unwrap_or_default()
582 }
583
584 fn compare_by_title(&self, a: &Reference, b: &Reference) -> std::cmp::Ordering {
586 let a_title = self.title_sort_key(a);
587 let b_title = self.title_sort_key(b);
588 self.text_collator.compare(&a_title, &b_title)
589 }
590
591 fn compare_by_issued(a: &Reference, b: &Reference) -> std::cmp::Ordering {
593 let a_year = Self::issued_year(a);
594 let b_year = Self::issued_year(b);
595 compare_optional_years(a_year, b_year)
596 }
597
598 fn compare_by_field(a: &Reference, b: &Reference, field_name: &str) -> std::cmp::Ordering {
600 Self::field_sort_value(a, field_name).cmp(&Self::field_sort_value(b, field_name))
601 }
602
603 fn title_sort_key(&self, reference: &Reference) -> String {
604 title_sort_key_with_options(reference, self.locale, &self.sort_key_options)
605 }
606
607 fn issued_year(reference: &Reference) -> Option<i32> {
608 reference
609 .effective_issued_date()
610 .and_then(|d| d.year().parse::<i32>().ok())
611 .filter(|year| *year != 0)
612 }
613
614 fn field_sort_value(reference: &Reference, field_name: &str) -> String {
615 match field_name {
616 "language" => normalize_sort_text(reference.language().unwrap_or_default().as_ref()),
617 _ => String::new(),
619 }
620 }
621}
622
623fn first_contributor_component_ref(
624 template: &[citum_schema::template::TemplateComponent],
625) -> Option<&citum_schema::template::TemplateContributor> {
626 for component in template {
627 match component {
628 citum_schema::template::TemplateComponent::Contributor(contributor) => {
629 return Some(contributor);
630 }
631 citum_schema::template::TemplateComponent::Group(group) => {
632 if let Some(contributor) = first_contributor_component_ref(&group.group) {
633 return Some(contributor);
634 }
635 }
636 _ => {}
637 }
638 }
639 None
640}
641
642fn first_contributor_component(
643 template: &[citum_schema::template::TemplateComponent],
644) -> Option<citum_schema::template::TemplateContributor> {
645 first_contributor_component_ref(template).cloned()
646}
647
648fn template_has_list_primary(template: &[citum_schema::template::TemplateComponent]) -> bool {
649 first_contributor_component_ref(template)
650 .is_some_and(|component| component.contributor.is_multiple())
651}
652
653fn variants_may_have_list_primary(variants: &citum_schema::template::TemplateVariants) -> bool {
654 variants
655 .values()
656 .any(|variant| variant.as_template().is_none_or(template_has_list_primary))
657}
658
659pub(crate) fn citation_may_have_list_primary(spec: &citum_schema::CitationSpec) -> bool {
663 spec.template
664 .as_deref()
665 .is_some_and(template_has_list_primary)
666 || spec
667 .template_ref
668 .as_ref()
669 .and_then(citum_schema::template::TemplateReference::citation_template)
670 .as_deref()
671 .is_some_and(template_has_list_primary)
672 || spec.locales.as_ref().is_some_and(|locales| {
673 locales
674 .iter()
675 .any(|localized| template_has_list_primary(&localized.template))
676 })
677 || spec
678 .type_variants
679 .as_ref()
680 .is_some_and(variants_may_have_list_primary)
681 || spec
682 .integral
683 .as_deref()
684 .is_some_and(citation_may_have_list_primary)
685 || spec
686 .non_integral
687 .as_deref()
688 .is_some_and(citation_may_have_list_primary)
689 || spec
690 .subsequent
691 .as_deref()
692 .is_some_and(citation_may_have_list_primary)
693 || spec
694 .ibid
695 .as_deref()
696 .is_some_and(citation_may_have_list_primary)
697}
698
699fn bibliography_may_have_list_primary(spec: &citum_schema::BibliographySpec) -> bool {
700 spec.template
701 .as_deref()
702 .is_some_and(template_has_list_primary)
703 || spec
704 .template_ref
705 .as_ref()
706 .and_then(citum_schema::template::TemplateReference::bibliography_template)
707 .as_deref()
708 .is_some_and(template_has_list_primary)
709 || spec.locales.as_ref().is_some_and(|locales| {
710 locales
711 .iter()
712 .any(|localized| template_has_list_primary(&localized.template))
713 })
714 || spec
715 .type_variants
716 .as_ref()
717 .is_some_and(variants_may_have_list_primary)
718}
719
720pub(crate) fn primary_contributor_for_citation(
722 spec: &citum_schema::CitationSpec,
723 reference: &Reference,
724) -> Option<citum_schema::template::TemplateContributor> {
725 let language = reference.language().map(|language| language.to_string());
726 let template = spec.resolve_template_for_type(&reference.ref_type(), language.as_deref())?;
727 first_contributor_component(&template)
728}
729
730fn primary_contributor_for_bibliography(
731 spec: &citum_schema::BibliographySpec,
732 reference: &Reference,
733 language: Option<&str>,
734) -> Option<citum_schema::template::TemplateContributor> {
735 let template = spec.resolve_template_for_type(&reference.ref_type(), language)?;
736 first_contributor_component(&template)
737}
738
739#[cfg(test)]
740#[allow(
741 clippy::unwrap_used,
742 clippy::expect_used,
743 clippy::panic,
744 clippy::indexing_slicing,
745 clippy::todo,
746 clippy::unimplemented,
747 clippy::unreachable,
748 clippy::get_unwrap,
749 reason = "Panicking is acceptable and often desired in tests."
750)]
751mod tests {
752 use super::*;
753 use citum_schema::grouping::GroupSortKey;
754 use citum_schema::options::{MultilingualConfig, SortingConfig, SortingMultilingualMode};
755 use citum_schema::reference::contributor::MultilingualName;
756 use citum_schema::reference::types::MultilingualComplex;
757 use citum_schema::reference::{
758 Contributor, ContributorList, DateValue, Monograph, MonographType, MultilingualString,
759 StructuredName, Title,
760 };
761 use std::collections::HashMap;
762
763 fn make_locale() -> Locale {
764 Locale::en_us()
765 }
766
767 fn make_reference(
768 id: &str,
769 ref_type: &str,
770 author_family: &str,
771 title: &str,
772 year: i32,
773 ) -> Reference {
774 let json = serde_json::json!({
775 "id": id,
776 "type": ref_type,
777 "author": [{"family": author_family, "given": "Test"}],
778 "issued": {"date-parts": [[year]]},
779 "title": title,
780 "container-title": "Test Container",
781 });
782 let legacy: csl_legacy::csl_json::Reference = serde_json::from_value(json).unwrap();
783 legacy.into()
784 }
785
786 fn make_reference_no_author(id: &str, ref_type: &str, title: &str, year: i32) -> Reference {
787 let json = serde_json::json!({
788 "id": id,
789 "type": ref_type,
790 "issued": {"date-parts": [[year]]},
791 "title": title,
792 "container-title": "Test Container",
793 });
794 let legacy: csl_legacy::csl_json::Reference = serde_json::from_value(json).unwrap();
795 legacy.into()
796 }
797
798 fn romanized_config() -> Config {
799 Config {
800 sorting: Some(SortingConfig {
801 multilingual: Some(SortingMultilingualMode::Romanized),
802 ..Default::default()
803 }),
804 multilingual: Some(MultilingualConfig {
805 preferred_transliteration: Some(vec!["ru-Latn-alalc97".to_string()]),
806 ..Default::default()
807 }),
808 ..Default::default()
809 }
810 }
811
812 fn multilingual_author_reference(
813 id: &str,
814 original_family: MultilingualString,
815 sort_as: Option<&str>,
816 transliteration_family: Option<&str>,
817 title: Title,
818 ) -> Reference {
819 let transliterations = transliteration_family.map_or_else(HashMap::new, |family| {
820 HashMap::from([(
821 "ru-Latn-alalc97".to_string(),
822 StructuredName {
823 family: family.into(),
824 given: "Lev".into(),
825 ..Default::default()
826 },
827 )])
828 });
829
830 Reference::Monograph(Box::new(Monograph {
831 id: Some(id.into()),
832 r#type: MonographType::Book,
833 title: Some(title),
834 author: Some(Contributor::ContributorList(ContributorList(vec![
835 Contributor::Multilingual(MultilingualName {
836 original: StructuredName {
837 family: original_family,
838 given: "Лев".into(),
839 ..Default::default()
840 },
841 lang: Some("ru".into()),
842 sort_as: sort_as.map(str::to_string),
843 transliterations,
844 translations: HashMap::new(),
845 }),
846 ]))),
847 issued: DateValue::new("1869".to_string()),
848 ..Default::default()
849 }))
850 }
851
852 fn title_sort(sorter: &ReferenceSorter<'_>, references: Vec<&Reference>) -> Vec<String> {
853 let sort_spec = GroupSort {
854 template: vec![GroupSortKey {
855 key: GroupSortKeyType::Title,
856 ascending: true,
857 order: None,
858 sort_order: None,
859 }],
860 };
861
862 sorter
863 .sort_references(references, &sort_spec)
864 .into_iter()
865 .map(|reference| reference.id().expect("test reference id").to_string())
866 .collect()
867 }
868
869 #[test]
870 fn test_type_order_sorting() {
871 let locale = make_locale();
872 let sorter = ReferenceSorter::new(&locale);
873
874 let journal = make_reference("r1", "article-journal", "Smith", "Title J", 1990);
876 let magazine = make_reference("r2", "article-magazine", "Jones", "Title M", 2000);
877 let newspaper = make_reference("r3", "article-newspaper", "Brown", "Title N", 1985);
878 let book = make_reference("r4", "book", "Davis", "Title B", 1995);
879
880 let mut refs = vec![&book, &newspaper, &journal, &magazine];
881
882 let sort_spec = GroupSort {
883 template: vec![GroupSortKey {
884 key: GroupSortKeyType::RefType,
885 ascending: true,
886 order: Some(vec![
887 "article-journal".to_string(),
888 "article-magazine".to_string(),
889 "article-newspaper".to_string(),
890 ]),
891 sort_order: None,
892 }],
893 };
894
895 refs = sorter.sort_references(refs, &sort_spec);
896
897 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"); }
903
904 #[test]
905 fn test_author_family_given_order() {
906 let locale = make_locale();
907 let sorter = ReferenceSorter::new(&locale);
908
909 let smith = make_reference("r1", "book", "Smith", "Title", 2000);
910 let jones = make_reference("r2", "book", "Jones", "Title", 2000);
911 let brown = make_reference("r3", "book", "Brown", "Title", 2000);
912
913 let mut refs = vec![&smith, &jones, &brown];
914
915 let sort_spec = GroupSort {
916 template: vec![GroupSortKey {
917 key: GroupSortKeyType::Author,
918 ascending: true,
919 order: None,
920 sort_order: Some(NameSortOrder::FamilyGiven),
921 }],
922 };
923
924 refs = sorter.sort_references(refs, &sort_spec);
925
926 assert_eq!(refs[0].id().unwrap(), "r3"); assert_eq!(refs[1].id().unwrap(), "r2"); assert_eq!(refs[2].id().unwrap(), "r1"); }
931
932 #[test]
933 #[cfg(feature = "icu")]
934 fn test_author_sort_uses_unicode_collation_for_accented_names() {
935 let locale = make_locale();
936 let sorter = ReferenceSorter::new(&locale);
937
938 let celik = make_reference("r1", "book", "Çelik", "Title", 2000);
939 let zimring = make_reference("r2", "book", "Zimring", "Title", 2000);
940 let o_tuathail = make_reference("r3", "book", "Ó Tuathail", "Title", 2000);
941
942 let mut refs = vec![&o_tuathail, &zimring, &celik];
943
944 let sort_spec = GroupSort {
945 template: vec![GroupSortKey {
946 key: GroupSortKeyType::Author,
947 ascending: true,
948 order: None,
949 sort_order: Some(NameSortOrder::FamilyGiven),
950 }],
951 };
952
953 refs = sorter.sort_references(refs, &sort_spec);
954
955 assert_eq!(refs[0].id().unwrap(), "r1");
956 assert_eq!(refs[1].id().unwrap(), "r3");
957 assert_eq!(refs[2].id().unwrap(), "r2");
958 }
959
960 #[test]
961 #[cfg(feature = "icu")]
962 fn test_title_sort_uses_unicode_collation_for_accented_titles() {
963 let locale = make_locale();
964 let sorter = ReferenceSorter::new(&locale);
965
966 let accent = make_reference_no_author("r1", "book", "Órbitas del sur", 2000);
967 let plain = make_reference_no_author("r2", "book", "Origins of Theory", 2000);
968 let zeta = make_reference_no_author("r3", "book", "Zebra Studies", 2000);
969
970 let mut refs = vec![&zeta, &plain, &accent];
971
972 let sort_spec = GroupSort {
973 template: vec![GroupSortKey {
974 key: GroupSortKeyType::Title,
975 ascending: true,
976 order: None,
977 sort_order: None,
978 }],
979 };
980
981 refs = sorter.sort_references(refs, &sort_spec);
982
983 assert_eq!(refs[0].id().unwrap(), "r1");
984 assert_eq!(refs[1].id().unwrap(), "r2");
985 assert_eq!(refs[2].id().unwrap(), "r3");
986 }
987
988 #[test]
989 fn test_issued_descending() {
990 let locale = make_locale();
991 let sorter = ReferenceSorter::new(&locale);
992
993 let old = make_reference("r1", "book", "Smith", "Title", 1990);
994 let new = make_reference("r2", "book", "Jones", "Title", 2020);
995 let mid = make_reference("r3", "book", "Brown", "Title", 2005);
996
997 let mut refs = vec![&old, &new, &mid];
998
999 let sort_spec = GroupSort {
1000 template: vec![GroupSortKey {
1001 key: GroupSortKeyType::Issued,
1002 ascending: false, order: None,
1004 sort_order: None,
1005 }],
1006 };
1007
1008 refs = sorter.sort_references(refs, &sort_spec);
1009
1010 assert_eq!(refs[0].id().unwrap(), "r2"); assert_eq!(refs[1].id().unwrap(), "r3"); assert_eq!(refs[2].id().unwrap(), "r1"); }
1015
1016 #[test]
1017 fn test_issued_ascending_places_undated_last() {
1018 let locale = make_locale();
1019 let sorter = ReferenceSorter::new(&locale);
1020
1021 let dated_early = make_reference("r1", "book", "Smith", "Book D", 1999);
1022 let dated_late = make_reference("r2", "book", "Jones", "Book B", 2000);
1023 let mut undated = make_reference("r3", "book", "Brown", "Book A", 2000);
1024 if let ClassExtension::Monograph(monograph) = undated.extension_mut() {
1025 monograph.issued = citum_schema::reference::DateValue::new(String::new());
1026 }
1027
1028 let mut refs = vec![&undated, &dated_late, &dated_early];
1029
1030 let sort_spec = GroupSort {
1031 template: vec![GroupSortKey {
1032 key: GroupSortKeyType::Issued,
1033 ascending: true,
1034 order: None,
1035 sort_order: None,
1036 }],
1037 };
1038
1039 refs = sorter.sort_references(refs, &sort_spec);
1040
1041 assert_eq!(refs[0].id().unwrap(), "r1");
1042 assert_eq!(refs[1].id().unwrap(), "r2");
1043 assert_eq!(refs[2].id().unwrap(), "r3");
1044 }
1045
1046 #[test]
1047 fn test_issued_sort_uses_created_when_issued_is_missing() {
1048 let locale = make_locale();
1049 let sorter = ReferenceSorter::new(&locale);
1050
1051 let dated = make_reference("r1", "book", "Smith", "Book D", 1999);
1052 let mut created_only = make_reference("r2", "book", "Jones", "Book C", 2000);
1053 if let ClassExtension::Monograph(monograph) = created_only.extension_mut() {
1054 monograph.created = citum_schema::reference::DateValue::new("1985".to_string());
1055 monograph.issued = citum_schema::reference::DateValue::new(String::new());
1056 }
1057
1058 let mut refs = vec![&dated, &created_only];
1059
1060 let sort_spec = GroupSort {
1061 template: vec![GroupSortKey {
1062 key: GroupSortKeyType::Issued,
1063 ascending: true,
1064 order: None,
1065 sort_order: None,
1066 }],
1067 };
1068
1069 refs = sorter.sort_references(refs, &sort_spec);
1070
1071 assert_eq!(refs[0].id().unwrap(), "r2");
1072 assert_eq!(refs[1].id().unwrap(), "r1");
1073 }
1074
1075 #[test]
1076 fn test_composite_sort() {
1077 let locale = make_locale();
1078 let sorter = ReferenceSorter::new(&locale);
1079
1080 let smith2020 = make_reference("r1", "book", "Smith", "Title", 2020);
1081 let smith2010 = make_reference("r2", "book", "Smith", "Title", 2010);
1082 let jones2020 = make_reference("r3", "book", "Jones", "Title", 2020);
1083
1084 let mut refs = vec![&smith2020, &jones2020, &smith2010];
1085
1086 let sort_spec = GroupSort {
1087 template: vec![
1088 GroupSortKey {
1089 key: GroupSortKeyType::Author,
1090 ascending: true,
1091 order: None,
1092 sort_order: Some(NameSortOrder::FamilyGiven),
1093 },
1094 GroupSortKey {
1095 key: GroupSortKeyType::Issued,
1096 ascending: false, order: None,
1098 sort_order: None,
1099 },
1100 ],
1101 };
1102
1103 refs = sorter.sort_references(refs, &sort_spec);
1104
1105 assert_eq!(refs[0].id().unwrap(), "r3"); assert_eq!(refs[1].id().unwrap(), "r1"); assert_eq!(refs[2].id().unwrap(), "r2"); }
1110
1111 #[test]
1112 fn test_author_sort_falls_back_to_title_for_missing_names() {
1113 let locale = make_locale();
1114 let sorter = ReferenceSorter::new(&locale);
1115
1116 let no_author = make_reference_no_author("r1", "legal-case", "Brown v. Board", 1954);
1117 let brown = make_reference("r2", "book", "Brown", "Title", 2000);
1118 let smith = make_reference("r3", "book", "Smith", "Title", 2000);
1119
1120 let mut refs = vec![&no_author, &smith, &brown];
1121
1122 let sort_spec = GroupSort {
1123 template: vec![GroupSortKey {
1124 key: GroupSortKeyType::Author,
1125 ascending: true,
1126 order: None,
1127 sort_order: Some(NameSortOrder::FamilyGiven),
1128 }],
1129 };
1130
1131 refs = sorter.sort_references(refs, &sort_spec);
1132
1133 assert_eq!(refs[0].id().unwrap(), "r2"); assert_eq!(refs[1].id().unwrap(), "r1"); assert_eq!(refs[2].id().unwrap(), "r3"); }
1137
1138 #[test]
1139 fn test_legal_citation_sort() {
1140 let locale = make_locale();
1141 let sorter = ReferenceSorter::new(&locale);
1142
1143 let case_a = make_reference("r1", "legal-case", "", "Doe v. Smith", 1990);
1144 let case_b = make_reference("r2", "legal-case", "", "Brown v. Board", 1954);
1145
1146 let mut refs = vec![&case_a, &case_b];
1147
1148 let sort_spec = GroupSort {
1149 template: vec![
1150 GroupSortKey {
1151 key: GroupSortKeyType::Title, ascending: true,
1153 order: None,
1154 sort_order: None,
1155 },
1156 GroupSortKey {
1157 key: GroupSortKeyType::Issued,
1158 ascending: true,
1159 order: None,
1160 sort_order: None,
1161 },
1162 ],
1163 };
1164
1165 refs = sorter.sort_references(refs, &sort_spec);
1166 assert_eq!(refs[0].id().unwrap(), "r2"); }
1168
1169 #[test]
1170 fn test_legal_hierarchy_sort() {
1171 let locale = make_locale();
1172 let sorter = ReferenceSorter::new(&locale);
1173
1174 let statute = make_reference("r1", "statute", "", "Clean Air Act", 1970);
1175 let case = make_reference("r2", "legal-case", "", "Roe v. Wade", 1973);
1176 let treaty = make_reference("r3", "treaty", "", "Paris Agreement", 2015);
1177
1178 let mut refs = vec![&treaty, &case, &statute];
1179
1180 let sort_spec = GroupSort {
1181 template: vec![GroupSortKey {
1182 key: GroupSortKeyType::RefType,
1183 ascending: true,
1184 order: Some(vec![
1185 "legal-case".to_string(),
1186 "statute".to_string(),
1187 "treaty".to_string(),
1188 ]),
1189 sort_order: None,
1190 }],
1191 };
1192
1193 refs = sorter.sort_references(refs, &sort_spec);
1194
1195 assert_eq!(refs[0].id().unwrap(), "r2");
1197 assert_eq!(refs[1].id().unwrap(), "r1");
1198 assert_eq!(refs[2].id().unwrap(), "r3");
1199 }
1200
1201 #[test]
1204 fn test_id_tiebreak_orders_equal_keys_by_id() {
1205 let locale = make_locale();
1206 let sorter = ReferenceSorter::new(&locale);
1207
1208 let c = make_reference("r-c", "book", "Smith", "Same Title", 2000);
1209 let a = make_reference("r-a", "book", "Smith", "Same Title", 2000);
1210 let b = make_reference("r-b", "book", "Smith", "Same Title", 2000);
1211
1212 let refs = vec![&c, &a, &b];
1213
1214 let sort_spec = GroupSort {
1215 template: vec![GroupSortKey {
1216 key: GroupSortKeyType::Author,
1217 ascending: true,
1218 order: None,
1219 sort_order: Some(NameSortOrder::FamilyGiven),
1220 }],
1221 };
1222
1223 let sorted = sorter.sort_references_with_id_tiebreak(refs, &sort_spec);
1224
1225 assert_eq!(sorted[0].id().unwrap(), "r-a");
1226 assert_eq!(sorted[1].id().unwrap(), "r-b");
1227 assert_eq!(sorted[2].id().unwrap(), "r-c");
1228 }
1229
1230 #[test]
1233 fn test_id_tiebreak_places_missing_id_last() {
1234 let locale = make_locale();
1235 let sorter = ReferenceSorter::new(&locale);
1236
1237 let with_id = make_reference("r1", "book", "Smith", "Same Title", 2000);
1238 let mut no_id = make_reference("r2", "book", "Smith", "Same Title", 2000);
1239 if let ClassExtension::Monograph(monograph) = no_id.extension_mut() {
1240 monograph.id = None;
1241 }
1242
1243 let refs = vec![&with_id, &no_id];
1244
1245 let sort_spec = GroupSort {
1246 template: vec![GroupSortKey {
1247 key: GroupSortKeyType::Author,
1248 ascending: true,
1249 order: None,
1250 sort_order: Some(NameSortOrder::FamilyGiven),
1251 }],
1252 };
1253
1254 let sorted = sorter.sort_references_with_id_tiebreak(refs, &sort_spec);
1255
1256 assert_eq!(sorted[0].id().unwrap(), "r1");
1257 assert!(sorted[1].id().is_none());
1258 }
1259
1260 #[test]
1263 fn test_id_tiebreak_with_empty_template_keeps_registry_order() {
1264 let locale = make_locale();
1265 let sorter = ReferenceSorter::new(&locale);
1266
1267 let c = make_reference("r-c", "book", "Smith", "Title C", 2000);
1268 let a = make_reference("r-a", "book", "Jones", "Title A", 2001);
1269
1270 let refs = vec![&c, &a];
1271 let sort_spec = GroupSort { template: vec![] };
1272
1273 let sorted = sorter.sort_references_with_id_tiebreak(refs, &sort_spec);
1274
1275 assert_eq!(sorted[0].id().unwrap(), "r-c");
1276 assert_eq!(sorted[1].id().unwrap(), "r-a");
1277 }
1278
1279 #[test]
1280 fn romanized_author_sort_uses_hidden_sort_as() {
1281 let locale = make_locale();
1282 let config = romanized_config();
1283 let sorter = ReferenceSorter::with_bibliography_config(&locale, &config);
1284 let reference = multilingual_author_reference(
1285 "tolstoy",
1286 "Толстой".into(),
1287 Some("Tolstoy"),
1288 Some("Tolstoĭ"),
1289 Title::Single("War and Peace".to_string()),
1290 );
1291
1292 assert_eq!(
1293 sorter.extract_author_sort_key(&reference, NameSortOrder::FamilyGiven),
1294 "Tolstoy"
1295 );
1296 }
1297
1298 #[test]
1299 fn uniform_author_sort_ignores_hidden_sort_as() {
1300 let locale = make_locale();
1301 let sorter = ReferenceSorter::new(&locale);
1302 let reference = multilingual_author_reference(
1303 "tolstoy",
1304 "Толстой".into(),
1305 Some("Tolstoy"),
1306 Some("Tolstoĭ"),
1307 Title::Single("War and Peace".to_string()),
1308 );
1309
1310 assert_eq!(
1311 sorter.extract_author_sort_key(&reference, NameSortOrder::FamilyGiven),
1312 "Толстой"
1313 );
1314 }
1315
1316 #[test]
1317 fn romanized_author_sort_falls_back_to_matched_transliteration() {
1318 let locale = make_locale();
1319 let config = romanized_config();
1320 let sorter = ReferenceSorter::with_bibliography_config(&locale, &config);
1321 let reference = multilingual_author_reference(
1322 "tolstoy",
1323 "Толстой".into(),
1324 None,
1325 Some("Tolstoĭ"),
1326 Title::Single("War and Peace".to_string()),
1327 );
1328
1329 assert_eq!(
1330 sorter.extract_author_sort_key(&reference, NameSortOrder::FamilyGiven),
1331 "Tolstoĭ"
1332 );
1333 }
1334
1335 #[test]
1336 fn holistic_sort_as_wins_over_part_level_sort_as() {
1337 let locale = make_locale();
1338 let config = romanized_config();
1339 let sorter = ReferenceSorter::with_bibliography_config(&locale, &config);
1340 let family = MultilingualString::Complex(MultilingualComplex {
1341 original: "Толстой".to_string(),
1342 lang: Some("ru".into()),
1343 sort_as: Some("Part Level".to_string()),
1344 transliterations: HashMap::new(),
1345 translations: HashMap::new(),
1346 });
1347 let reference = multilingual_author_reference(
1348 "tolstoy",
1349 family,
1350 Some("Whole Name"),
1351 Some("Tolstoĭ"),
1352 Title::Single("War and Peace".to_string()),
1353 );
1354
1355 assert_eq!(
1356 sorter.extract_author_sort_key(&reference, NameSortOrder::FamilyGiven),
1357 "Whole Name"
1358 );
1359 }
1360
1361 #[test]
1362 fn title_sort_uses_hidden_sort_as_under_romanized_mode() {
1363 let locale = make_locale();
1364 let config = romanized_config();
1365 let sorter = ReferenceSorter::with_bibliography_config(&locale, &config);
1366 let cyrillic = multilingual_author_reference(
1367 "cyrillic-title",
1368 "Smith".into(),
1369 None,
1370 None,
1371 Title::Multilingual(MultilingualComplex {
1372 original: "Война и мир".to_string(),
1373 lang: Some("ru".into()),
1374 sort_as: Some("Academic War and Peace".to_string()),
1375 transliterations: HashMap::new(),
1376 translations: HashMap::new(),
1377 }),
1378 );
1379 let latin = make_reference_no_author("latin-title", "book", "Beta Studies", 2000);
1380
1381 assert_eq!(
1382 title_sort(&sorter, vec![&latin, &cyrillic]),
1383 vec!["cyrillic-title".to_string(), "latin-title".to_string()]
1384 );
1385 }
1386}