citum_engine/processor/sorting.rs
1/*
2SPDX-License-Identifier: MIT OR Apache-2.0
3SPDX-FileCopyrightText: © 2023-2026 Bruce D'Arcus and Citum contributors
4*/
5
6//! Sorting logic for bibliography and citation entries.
7//!
8//! This module handles multi-key sorting of references according to style instructions,
9//! including support for anonymous works and article stripping for title-based sorting.
10
11use crate::reference::Reference;
12use crate::sort_support::{TextCollator, author_sort_key_opt, title_sort_key};
13use citum_schema::grouping::NameSortOrder;
14use citum_schema::locale::Locale;
15use citum_schema::options::{Config, SortKey};
16
17/// Compares two optional years with configurable ascending/descending order.
18///
19/// When both years are present, compares them numerically. If only one is present,
20/// the year-bearing reference comes before the year-less reference. If neither is present,
21/// they are considered equal.
22fn compare_optional_years(
23 a_year: Option<i32>,
24 b_year: Option<i32>,
25 ascending: bool,
26) -> std::cmp::Ordering {
27 let cmp = match (a_year, b_year) {
28 (Some(a), Some(b)) => a.cmp(&b),
29 (Some(_), None) => std::cmp::Ordering::Less,
30 (None, Some(_)) => std::cmp::Ordering::Greater,
31 (None, None) => std::cmp::Ordering::Equal,
32 };
33
34 if ascending { cmp } else { cmp.reverse() }
35}
36
37/// Sorter for bibliography and citation entries.
38///
39/// Sorts references according to style instructions, handling multi-key sorting
40/// and special cases like anonymous works falling back from author to editor to title.
41pub struct Sorter<'a> {
42 /// The configuration containing sort specifications.
43 config: &'a Config,
44 /// The locale used for article stripping in title-based sorts.
45 locale: &'a Locale,
46 /// Locale-aware text comparator for author/title sort keys.
47 text_collator: TextCollator,
48}
49
50impl<'a> Sorter<'a> {
51 /// Creates a new `Sorter` instance.
52 #[must_use]
53 pub fn new(config: &'a Config, locale: &'a Locale) -> Self {
54 Self {
55 config,
56 locale,
57 text_collator: TextCollator::new(locale),
58 }
59 }
60
61 /// Sort references according to style instructions.
62 ///
63 /// This handles multi-key sorting based on the style's `SortSpec`. It includes
64 /// specific logic for handling anonymous works (falling back from author to editor
65 /// to title) and stripping articles for title-based sorting.
66 #[must_use]
67 pub fn sort_references<'b>(&self, references: Vec<&'b Reference>) -> Vec<&'b Reference> {
68 let mut refs = references;
69 let processing = self.config.processing.clone().unwrap_or_default();
70 let proc_config = processing.config();
71
72 if let Some(sort_config) = &proc_config.sort {
73 // Build a composite sort that handles all keys together
74 // Built-in processing defaults are bibliography-facing only; citation
75 // cluster ordering remains explicit at the citation spec level.
76 let resolved = sort_config.resolve();
77 refs.sort_by(|a, b| {
78 for sort in &resolved.template {
79 let cmp = match sort.key {
80 SortKey::Author => {
81 let a_sort_key = author_sort_key_opt(
82 a,
83 NameSortOrder::FamilyGiven,
84 self.locale,
85 true,
86 )
87 .unwrap_or_default();
88 let b_sort_key = author_sort_key_opt(
89 b,
90 NameSortOrder::FamilyGiven,
91 self.locale,
92 true,
93 )
94 .unwrap_or_default();
95
96 if sort.ascending {
97 self.text_collator.compare(&a_sort_key, &b_sort_key)
98 } else {
99 self.text_collator.compare(&b_sort_key, &a_sort_key)
100 }
101 }
102 SortKey::Year => {
103 let a_year = a
104 .csl_issued_date()
105 .and_then(|d| d.year().parse::<i32>().ok())
106 .filter(|year| *year != 0);
107 let b_year = b
108 .csl_issued_date()
109 .and_then(|d| d.year().parse::<i32>().ok())
110 .filter(|year| *year != 0);
111
112 compare_optional_years(a_year, b_year, sort.ascending)
113 }
114 SortKey::Title => {
115 let a_title = title_sort_key(a, self.locale);
116 let b_title = title_sort_key(b, self.locale);
117
118 if sort.ascending {
119 self.text_collator.compare(&a_title, &b_title)
120 } else {
121 self.text_collator.compare(&b_title, &a_title)
122 }
123 }
124 SortKey::CitationNumber => std::cmp::Ordering::Equal,
125 // Handle future SortKey variants (non_exhaustive)
126 _ => std::cmp::Ordering::Equal,
127 };
128
129 // If this key produces a non-equal comparison, use it
130 // Otherwise, continue to the next key
131 if cmp != std::cmp::Ordering::Equal {
132 return cmp;
133 }
134 }
135
136 // Deterministic tiebreaker: compare entry IDs as &str when all sort keys are equal.
137 // None IDs sort last (missing ID > any present ID).
138 match (a.id(), b.id()) {
139 (Some(a_id), Some(b_id)) => a_id.0.as_str().cmp(b_id.0.as_str()),
140 (Some(_), None) => std::cmp::Ordering::Less,
141 (None, Some(_)) => std::cmp::Ordering::Greater,
142 (None, None) => std::cmp::Ordering::Equal,
143 }
144 });
145 }
146
147 refs
148 }
149}