1use std::collections::HashMap;
14
15use citum_schema::grouping::{GroupSort, GroupSortKey, NameSortOrder, SortKey as GroupSortKeyType};
16use citum_schema::locale::Locale;
17#[cfg(test)]
18use citum_schema::reference::ClassExtension;
19
20use crate::reference::Reference;
21use crate::sort_support::{TextCollator, author_sort_key_opt, normalize_sort_text, title_sort_key};
22
23fn compare_optional_years(a_year: Option<i32>, b_year: Option<i32>) -> std::cmp::Ordering {
24 match (a_year, b_year) {
25 (Some(a), Some(b)) => a.cmp(&b),
26 (Some(_), None) => std::cmp::Ordering::Less,
27 (None, Some(_)) => std::cmp::Ordering::Greater,
28 (None, None) => std::cmp::Ordering::Equal,
29 }
30}
31
32pub struct GroupSorter<'a> {
34 locale: &'a Locale,
35 text_collator: TextCollator,
36}
37
38struct CachedReference<'a> {
39 reference: &'a Reference,
40 sort_values: Vec<CachedSortValue>,
41}
42
43enum CachedSortValue {
44 RefType { name: String, rank: Option<usize> },
45 OptionalText(Option<String>),
46 Text(String),
47 Issued(Option<i32>),
48}
49
50enum CompiledSortKey<'a> {
51 RefType {
52 ascending: bool,
53 rank_by_type: Option<HashMap<String, usize>>,
54 },
55 Author {
56 ascending: bool,
57 name_order: NameSortOrder,
58 },
59 Title {
60 ascending: bool,
61 },
62 Issued {
63 ascending: bool,
64 },
65 Field {
66 ascending: bool,
67 field_name: &'a str,
68 },
69}
70
71impl<'a> GroupSorter<'a> {
72 #[must_use]
74 pub fn new(locale: &'a Locale) -> Self {
75 Self {
76 locale,
77 text_collator: TextCollator::new(locale),
78 }
79 }
80
81 #[must_use]
90 pub fn sort_references<'b>(
91 &self,
92 mut references: Vec<&'b Reference>,
93 sort_spec: &GroupSort,
94 ) -> Vec<&'b Reference> {
95 let compiled_keys = self.compile_sort_keys(sort_spec);
96 let mut cached_references = references
97 .drain(..)
98 .map(|reference| CachedReference {
99 reference,
100 sort_values: compiled_keys
101 .iter()
102 .map(|sort_key| self.cache_sort_value(reference, sort_key))
103 .collect(),
104 })
105 .collect::<Vec<_>>();
106
107 cached_references.sort_by(|a, b| self.compare_cached_references(a, b, &compiled_keys));
108 cached_references
109 .into_iter()
110 .map(|entry| entry.reference)
111 .collect()
112 }
113
114 #[must_use]
116 pub fn compare_by_key(
117 &self,
118 a: &Reference,
119 b: &Reference,
120 sort_key: &GroupSortKey,
121 ) -> std::cmp::Ordering {
122 self.compare_by_key_with_context(a, b, sort_key)
123 }
124
125 fn compare_by_key_with_context(
126 &self,
127 a: &Reference,
128 b: &Reference,
129 sort_key: &GroupSortKey,
130 ) -> std::cmp::Ordering {
131 let cmp = match &sort_key.key {
132 GroupSortKeyType::RefType => sort_key.order.as_ref().map_or_else(
133 || a.ref_type().cmp(&b.ref_type()),
134 |order| Self::compare_by_type_order(a, b, order),
135 ),
136 GroupSortKeyType::Author => sort_key.sort_order.as_ref().map_or_else(
137 || self.compare_by_author_with_order(a, b, NameSortOrder::FamilyGiven),
138 |name_order| self.compare_by_author_with_order(a, b, *name_order),
139 ),
140 GroupSortKeyType::Title => self.compare_by_title(a, b),
141 GroupSortKeyType::Issued => Self::compare_by_issued(a, b),
142 GroupSortKeyType::Field(field_name) => Self::compare_by_field(a, b, field_name),
143 };
144
145 if sort_key.ascending {
146 cmp
147 } else {
148 cmp.reverse()
149 }
150 }
151
152 fn compare_by_type_order(a: &Reference, b: &Reference, order: &[String]) -> std::cmp::Ordering {
157 let a_type = a.ref_type();
158 let b_type = b.ref_type();
159
160 let a_pos = order.iter().position(|t| t == &a_type);
161 let b_pos = order.iter().position(|t| t == &b_type);
162
163 match (a_pos, b_pos) {
164 (Some(a_idx), Some(b_idx)) => a_idx.cmp(&b_idx),
165 (Some(_), None) => std::cmp::Ordering::Less, (None, Some(_)) => std::cmp::Ordering::Greater, (None, None) => a_type.cmp(&b_type), }
169 }
170
171 fn compile_sort_keys<'b>(&self, sort_spec: &'b GroupSort) -> Vec<CompiledSortKey<'b>> {
172 sort_spec
173 .template
174 .iter()
175 .map(|sort_key| match &sort_key.key {
176 GroupSortKeyType::RefType => CompiledSortKey::RefType {
177 ascending: sort_key.ascending,
178 rank_by_type: sort_key.order.as_ref().map(|order| {
179 order
180 .iter()
181 .enumerate()
182 .map(|(index, ref_type)| (ref_type.clone(), index))
183 .collect()
184 }),
185 },
186 GroupSortKeyType::Author => CompiledSortKey::Author {
187 ascending: sort_key.ascending,
188 name_order: sort_key.sort_order.unwrap_or(NameSortOrder::FamilyGiven),
189 },
190 GroupSortKeyType::Title => CompiledSortKey::Title {
191 ascending: sort_key.ascending,
192 },
193 GroupSortKeyType::Issued => CompiledSortKey::Issued {
194 ascending: sort_key.ascending,
195 },
196 GroupSortKeyType::Field(field_name) => CompiledSortKey::Field {
197 ascending: sort_key.ascending,
198 field_name,
199 },
200 })
201 .collect()
202 }
203
204 fn cache_sort_value(
205 &self,
206 reference: &Reference,
207 sort_key: &CompiledSortKey<'_>,
208 ) -> CachedSortValue {
209 match sort_key {
210 CompiledSortKey::RefType { rank_by_type, .. } => {
211 let ref_type = reference.ref_type();
212 CachedSortValue::RefType {
213 name: ref_type.clone(),
214 rank: rank_by_type
215 .as_ref()
216 .and_then(|ranks| ranks.get(&ref_type).copied()),
217 }
218 }
219 CompiledSortKey::Author { name_order, .. } => CachedSortValue::OptionalText(
220 self.extract_author_sort_key_opt(reference, *name_order),
221 ),
222 CompiledSortKey::Title { .. } => CachedSortValue::Text(self.title_sort_key(reference)),
223 CompiledSortKey::Issued { .. } => CachedSortValue::Issued(Self::issued_year(reference)),
224 CompiledSortKey::Field { field_name, .. } => {
225 CachedSortValue::Text(Self::field_sort_value(reference, field_name))
226 }
227 }
228 }
229
230 fn compare_cached_references(
231 &self,
232 a: &CachedReference<'_>,
233 b: &CachedReference<'_>,
234 compiled_keys: &[CompiledSortKey<'_>],
235 ) -> std::cmp::Ordering {
236 for (index, sort_key) in compiled_keys.iter().enumerate() {
237 #[allow(
238 clippy::indexing_slicing,
239 reason = "index is derived from compiled_keys"
240 )]
241 let cmp = self.compare_cached_value(&a.sort_values[index], &b.sort_values[index]);
242 let cmp = if Self::is_ascending(sort_key) {
243 cmp
244 } else {
245 cmp.reverse()
246 };
247
248 if cmp != std::cmp::Ordering::Equal {
249 return cmp;
250 }
251 }
252
253 std::cmp::Ordering::Equal
254 }
255
256 fn compare_cached_value(&self, a: &CachedSortValue, b: &CachedSortValue) -> std::cmp::Ordering {
257 match (a, b) {
258 (
259 CachedSortValue::RefType {
260 name: a_name,
261 rank: a_rank,
262 },
263 CachedSortValue::RefType {
264 name: b_name,
265 rank: b_rank,
266 },
267 ) => match (a_rank, b_rank) {
268 (Some(a_idx), Some(b_idx)) => a_idx.cmp(b_idx),
269 (Some(_), None) => std::cmp::Ordering::Less,
270 (None, Some(_)) => std::cmp::Ordering::Greater,
271 (None, None) => a_name.cmp(b_name),
272 },
273 (CachedSortValue::OptionalText(a_text), CachedSortValue::OptionalText(b_text)) => {
274 match (a_text, b_text) {
275 (Some(a_value), Some(b_value)) => self.text_collator.compare(a_value, b_value),
276 (Some(_), None) => std::cmp::Ordering::Less,
277 (None, Some(_)) => std::cmp::Ordering::Greater,
278 (None, None) => std::cmp::Ordering::Equal,
279 }
280 }
281 (CachedSortValue::Text(a_text), CachedSortValue::Text(b_text)) => {
282 self.text_collator.compare(a_text, b_text)
283 }
284 (CachedSortValue::Issued(a_year), CachedSortValue::Issued(b_year)) => {
285 compare_optional_years(*a_year, *b_year)
286 }
287 _ => std::cmp::Ordering::Equal,
288 }
289 }
290
291 fn is_ascending(sort_key: &CompiledSortKey<'_>) -> bool {
292 match sort_key {
293 CompiledSortKey::RefType { ascending, .. }
294 | CompiledSortKey::Author { ascending, .. }
295 | CompiledSortKey::Title { ascending }
296 | CompiledSortKey::Issued { ascending }
297 | CompiledSortKey::Field { ascending, .. } => *ascending,
298 }
299 }
300
301 fn compare_by_author_with_order(
303 &self,
304 a: &Reference,
305 b: &Reference,
306 name_order: NameSortOrder,
307 ) -> std::cmp::Ordering {
308 let a_key = self.extract_author_sort_key_opt(a, name_order);
309 let b_key = self.extract_author_sort_key_opt(b, name_order);
310 match (a_key, b_key) {
311 (Some(a), Some(b)) => self.text_collator.compare(&a, &b),
312 (Some(_), None) => std::cmp::Ordering::Less,
313 (None, Some(_)) => std::cmp::Ordering::Greater,
314 (None, None) => std::cmp::Ordering::Equal,
315 }
316 }
317
318 fn extract_author_sort_key_opt(
324 &self,
325 reference: &Reference,
326 name_order: NameSortOrder,
327 ) -> Option<String> {
328 author_sort_key_opt(reference, name_order, self.locale, true)
329 }
330
331 #[must_use]
333 pub fn extract_author_sort_key(
334 &self,
335 reference: &Reference,
336 name_order: NameSortOrder,
337 ) -> String {
338 self.extract_author_sort_key_opt(reference, name_order)
339 .unwrap_or_default()
340 }
341
342 fn compare_by_title(&self, a: &Reference, b: &Reference) -> std::cmp::Ordering {
344 let a_title = self.title_sort_key(a);
345 let b_title = self.title_sort_key(b);
346 self.text_collator.compare(&a_title, &b_title)
347 }
348
349 fn compare_by_issued(a: &Reference, b: &Reference) -> std::cmp::Ordering {
351 let a_year = Self::issued_year(a);
352 let b_year = Self::issued_year(b);
353 compare_optional_years(a_year, b_year)
354 }
355
356 fn compare_by_field(a: &Reference, b: &Reference, field_name: &str) -> std::cmp::Ordering {
358 Self::field_sort_value(a, field_name).cmp(&Self::field_sort_value(b, field_name))
359 }
360
361 fn title_sort_key(&self, reference: &Reference) -> String {
362 title_sort_key(reference, self.locale)
363 }
364
365 fn issued_year(reference: &Reference) -> Option<i32> {
366 reference
367 .csl_issued_date()
368 .and_then(|d| d.year().parse::<i32>().ok())
369 .filter(|year| *year != 0)
370 }
371
372 fn field_sort_value(reference: &Reference, field_name: &str) -> String {
373 match field_name {
374 "language" => normalize_sort_text(reference.language().unwrap_or_default().as_ref()),
375 _ => String::new(),
377 }
378 }
379}
380
381#[cfg(test)]
382#[allow(
383 clippy::unwrap_used,
384 clippy::expect_used,
385 clippy::panic,
386 clippy::indexing_slicing,
387 clippy::todo,
388 clippy::unimplemented,
389 clippy::unreachable,
390 clippy::get_unwrap,
391 reason = "Panicking is acceptable and often desired in tests."
392)]
393mod tests {
394 use super::*;
395 use citum_schema::grouping::GroupSortKey;
396
397 fn make_locale() -> Locale {
398 Locale::en_us()
399 }
400
401 fn make_reference(
402 id: &str,
403 ref_type: &str,
404 author_family: &str,
405 title: &str,
406 year: i32,
407 ) -> Reference {
408 let json = serde_json::json!({
409 "id": id,
410 "type": ref_type,
411 "author": [{"family": author_family, "given": "Test"}],
412 "issued": {"date-parts": [[year]]},
413 "title": title,
414 "container-title": "Test Container",
415 });
416 let legacy: csl_legacy::csl_json::Reference = serde_json::from_value(json).unwrap();
417 legacy.into()
418 }
419
420 fn make_reference_no_author(id: &str, ref_type: &str, title: &str, year: i32) -> Reference {
421 let json = serde_json::json!({
422 "id": id,
423 "type": ref_type,
424 "issued": {"date-parts": [[year]]},
425 "title": title,
426 "container-title": "Test Container",
427 });
428 let legacy: csl_legacy::csl_json::Reference = serde_json::from_value(json).unwrap();
429 legacy.into()
430 }
431
432 #[test]
433 fn test_type_order_sorting() {
434 let locale = make_locale();
435 let sorter = GroupSorter::new(&locale);
436
437 let journal = make_reference("r1", "article-journal", "Smith", "Title J", 1990);
439 let magazine = make_reference("r2", "article-magazine", "Jones", "Title M", 2000);
440 let newspaper = make_reference("r3", "article-newspaper", "Brown", "Title N", 1985);
441 let book = make_reference("r4", "book", "Davis", "Title B", 1995);
442
443 let mut refs = vec![&book, &newspaper, &journal, &magazine];
444
445 let sort_spec = GroupSort {
446 template: vec![GroupSortKey {
447 key: GroupSortKeyType::RefType,
448 ascending: true,
449 order: Some(vec![
450 "article-journal".to_string(),
451 "article-magazine".to_string(),
452 "article-newspaper".to_string(),
453 ]),
454 sort_order: None,
455 }],
456 };
457
458 refs = sorter.sort_references(refs, &sort_spec);
459
460 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"); }
466
467 #[test]
468 fn test_author_family_given_order() {
469 let locale = make_locale();
470 let sorter = GroupSorter::new(&locale);
471
472 let smith = make_reference("r1", "book", "Smith", "Title", 2000);
473 let jones = make_reference("r2", "book", "Jones", "Title", 2000);
474 let brown = make_reference("r3", "book", "Brown", "Title", 2000);
475
476 let mut refs = vec![&smith, &jones, &brown];
477
478 let sort_spec = GroupSort {
479 template: vec![GroupSortKey {
480 key: GroupSortKeyType::Author,
481 ascending: true,
482 order: None,
483 sort_order: Some(NameSortOrder::FamilyGiven),
484 }],
485 };
486
487 refs = sorter.sort_references(refs, &sort_spec);
488
489 assert_eq!(refs[0].id().unwrap(), "r3"); assert_eq!(refs[1].id().unwrap(), "r2"); assert_eq!(refs[2].id().unwrap(), "r1"); }
494
495 #[test]
496 #[cfg(feature = "icu")]
497 fn test_author_sort_uses_unicode_collation_for_accented_names() {
498 let locale = make_locale();
499 let sorter = GroupSorter::new(&locale);
500
501 let celik = make_reference("r1", "book", "Çelik", "Title", 2000);
502 let zimring = make_reference("r2", "book", "Zimring", "Title", 2000);
503 let o_tuathail = make_reference("r3", "book", "Ó Tuathail", "Title", 2000);
504
505 let mut refs = vec![&o_tuathail, &zimring, &celik];
506
507 let sort_spec = GroupSort {
508 template: vec![GroupSortKey {
509 key: GroupSortKeyType::Author,
510 ascending: true,
511 order: None,
512 sort_order: Some(NameSortOrder::FamilyGiven),
513 }],
514 };
515
516 refs = sorter.sort_references(refs, &sort_spec);
517
518 assert_eq!(refs[0].id().unwrap(), "r1");
519 assert_eq!(refs[1].id().unwrap(), "r3");
520 assert_eq!(refs[2].id().unwrap(), "r2");
521 }
522
523 #[test]
524 #[cfg(feature = "icu")]
525 fn test_title_sort_uses_unicode_collation_for_accented_titles() {
526 let locale = make_locale();
527 let sorter = GroupSorter::new(&locale);
528
529 let accent = make_reference_no_author("r1", "book", "Órbitas del sur", 2000);
530 let plain = make_reference_no_author("r2", "book", "Origins of Theory", 2000);
531 let zeta = make_reference_no_author("r3", "book", "Zebra Studies", 2000);
532
533 let mut refs = vec![&zeta, &plain, &accent];
534
535 let sort_spec = GroupSort {
536 template: vec![GroupSortKey {
537 key: GroupSortKeyType::Title,
538 ascending: true,
539 order: None,
540 sort_order: None,
541 }],
542 };
543
544 refs = sorter.sort_references(refs, &sort_spec);
545
546 assert_eq!(refs[0].id().unwrap(), "r1");
547 assert_eq!(refs[1].id().unwrap(), "r2");
548 assert_eq!(refs[2].id().unwrap(), "r3");
549 }
550
551 #[test]
552 fn test_issued_descending() {
553 let locale = make_locale();
554 let sorter = GroupSorter::new(&locale);
555
556 let old = make_reference("r1", "book", "Smith", "Title", 1990);
557 let new = make_reference("r2", "book", "Jones", "Title", 2020);
558 let mid = make_reference("r3", "book", "Brown", "Title", 2005);
559
560 let mut refs = vec![&old, &new, &mid];
561
562 let sort_spec = GroupSort {
563 template: vec![GroupSortKey {
564 key: GroupSortKeyType::Issued,
565 ascending: false, order: None,
567 sort_order: None,
568 }],
569 };
570
571 refs = sorter.sort_references(refs, &sort_spec);
572
573 assert_eq!(refs[0].id().unwrap(), "r2"); assert_eq!(refs[1].id().unwrap(), "r3"); assert_eq!(refs[2].id().unwrap(), "r1"); }
578
579 #[test]
580 fn test_issued_ascending_places_undated_last() {
581 let locale = make_locale();
582 let sorter = GroupSorter::new(&locale);
583
584 let dated_early = make_reference("r1", "book", "Smith", "Book D", 1999);
585 let dated_late = make_reference("r2", "book", "Jones", "Book B", 2000);
586 let mut undated = make_reference("r3", "book", "Brown", "Book A", 2000);
587 if let ClassExtension::Monograph(monograph) = undated.extension_mut() {
588 monograph.issued = citum_schema::reference::EdtfString(String::new());
589 }
590
591 let mut refs = vec![&undated, &dated_late, &dated_early];
592
593 let sort_spec = GroupSort {
594 template: vec![GroupSortKey {
595 key: GroupSortKeyType::Issued,
596 ascending: true,
597 order: None,
598 sort_order: None,
599 }],
600 };
601
602 refs = sorter.sort_references(refs, &sort_spec);
603
604 assert_eq!(refs[0].id().unwrap(), "r1");
605 assert_eq!(refs[1].id().unwrap(), "r2");
606 assert_eq!(refs[2].id().unwrap(), "r3");
607 }
608
609 #[test]
610 fn test_issued_sort_uses_created_when_issued_is_missing() {
611 let locale = make_locale();
612 let sorter = GroupSorter::new(&locale);
613
614 let dated = make_reference("r1", "book", "Smith", "Book D", 1999);
615 let mut created_only = make_reference("r2", "book", "Jones", "Book C", 2000);
616 if let ClassExtension::Monograph(monograph) = created_only.extension_mut() {
617 monograph.created = citum_schema::reference::EdtfString("1985".to_string());
618 monograph.issued = citum_schema::reference::EdtfString(String::new());
619 }
620
621 let mut refs = vec![&dated, &created_only];
622
623 let sort_spec = GroupSort {
624 template: vec![GroupSortKey {
625 key: GroupSortKeyType::Issued,
626 ascending: true,
627 order: None,
628 sort_order: None,
629 }],
630 };
631
632 refs = sorter.sort_references(refs, &sort_spec);
633
634 assert_eq!(refs[0].id().unwrap(), "r2");
635 assert_eq!(refs[1].id().unwrap(), "r1");
636 }
637
638 #[test]
639 fn test_composite_sort() {
640 let locale = make_locale();
641 let sorter = GroupSorter::new(&locale);
642
643 let smith2020 = make_reference("r1", "book", "Smith", "Title", 2020);
644 let smith2010 = make_reference("r2", "book", "Smith", "Title", 2010);
645 let jones2020 = make_reference("r3", "book", "Jones", "Title", 2020);
646
647 let mut refs = vec![&smith2020, &jones2020, &smith2010];
648
649 let sort_spec = GroupSort {
650 template: vec![
651 GroupSortKey {
652 key: GroupSortKeyType::Author,
653 ascending: true,
654 order: None,
655 sort_order: Some(NameSortOrder::FamilyGiven),
656 },
657 GroupSortKey {
658 key: GroupSortKeyType::Issued,
659 ascending: false, order: None,
661 sort_order: None,
662 },
663 ],
664 };
665
666 refs = sorter.sort_references(refs, &sort_spec);
667
668 assert_eq!(refs[0].id().unwrap(), "r3"); assert_eq!(refs[1].id().unwrap(), "r1"); assert_eq!(refs[2].id().unwrap(), "r2"); }
673
674 #[test]
675 fn test_author_sort_falls_back_to_title_for_missing_names() {
676 let locale = make_locale();
677 let sorter = GroupSorter::new(&locale);
678
679 let no_author = make_reference_no_author("r1", "legal-case", "Brown v. Board", 1954);
680 let brown = make_reference("r2", "book", "Brown", "Title", 2000);
681 let smith = make_reference("r3", "book", "Smith", "Title", 2000);
682
683 let mut refs = vec![&no_author, &smith, &brown];
684
685 let sort_spec = GroupSort {
686 template: vec![GroupSortKey {
687 key: GroupSortKeyType::Author,
688 ascending: true,
689 order: None,
690 sort_order: Some(NameSortOrder::FamilyGiven),
691 }],
692 };
693
694 refs = sorter.sort_references(refs, &sort_spec);
695
696 assert_eq!(refs[0].id().unwrap(), "r2"); assert_eq!(refs[1].id().unwrap(), "r1"); assert_eq!(refs[2].id().unwrap(), "r3"); }
700
701 #[test]
702 fn test_legal_citation_sort() {
703 let locale = make_locale();
704 let sorter = GroupSorter::new(&locale);
705
706 let case_a = make_reference("r1", "legal-case", "", "Doe v. Smith", 1990);
707 let case_b = make_reference("r2", "legal-case", "", "Brown v. Board", 1954);
708
709 let mut refs = vec![&case_a, &case_b];
710
711 let sort_spec = GroupSort {
712 template: vec![
713 GroupSortKey {
714 key: GroupSortKeyType::Title, ascending: true,
716 order: None,
717 sort_order: None,
718 },
719 GroupSortKey {
720 key: GroupSortKeyType::Issued,
721 ascending: true,
722 order: None,
723 sort_order: None,
724 },
725 ],
726 };
727
728 refs = sorter.sort_references(refs, &sort_spec);
729 assert_eq!(refs[0].id().unwrap(), "r2"); }
731
732 #[test]
733 fn test_legal_hierarchy_sort() {
734 let locale = make_locale();
735 let sorter = GroupSorter::new(&locale);
736
737 let statute = make_reference("r1", "statute", "", "Clean Air Act", 1970);
738 let case = make_reference("r2", "legal-case", "", "Roe v. Wade", 1973);
739 let treaty = make_reference("r3", "treaty", "", "Paris Agreement", 2015);
740
741 let mut refs = vec![&treaty, &case, &statute];
742
743 let sort_spec = GroupSort {
744 template: vec![GroupSortKey {
745 key: GroupSortKeyType::RefType,
746 ascending: true,
747 order: Some(vec![
748 "legal-case".to_string(),
749 "statute".to_string(),
750 "treaty".to_string(),
751 ]),
752 sort_order: None,
753 }],
754 };
755
756 refs = sorter.sort_references(refs, &sort_spec);
757
758 assert_eq!(refs[0].id().unwrap(), "r2");
760 assert_eq!(refs[1].id().unwrap(), "r1");
761 assert_eq!(refs[2].id().unwrap(), "r3");
762 }
763}