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