Skip to main content

oxihuman_core/
sort_key.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Sort-key builder for composite sort keys over multiple fields.
6
7/// A sort direction.
8#[allow(dead_code)]
9#[derive(Debug, Clone, PartialEq)]
10pub enum SortDir {
11    Asc,
12    Desc,
13}
14
15/// A single sort criterion.
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct SortCriterion {
19    pub field: String,
20    pub dir: SortDir,
21    pub numeric: bool,
22}
23
24/// Composite sort key builder.
25#[allow(dead_code)]
26#[derive(Debug, Clone, Default)]
27pub struct SortKey {
28    criteria: Vec<SortCriterion>,
29}
30
31#[allow(dead_code)]
32impl SortKey {
33    pub fn new() -> Self {
34        Self {
35            criteria: Vec::new(),
36        }
37    }
38
39    /// Add an ascending field.
40    pub fn asc(mut self, field: &str) -> Self {
41        self.criteria.push(SortCriterion {
42            field: field.to_string(),
43            dir: SortDir::Asc,
44            numeric: false,
45        });
46        self
47    }
48
49    /// Add a descending field.
50    pub fn desc(mut self, field: &str) -> Self {
51        self.criteria.push(SortCriterion {
52            field: field.to_string(),
53            dir: SortDir::Desc,
54            numeric: false,
55        });
56        self
57    }
58
59    /// Add a numeric ascending field.
60    pub fn asc_num(mut self, field: &str) -> Self {
61        self.criteria.push(SortCriterion {
62            field: field.to_string(),
63            dir: SortDir::Asc,
64            numeric: true,
65        });
66        self
67    }
68
69    /// Add a numeric descending field.
70    pub fn desc_num(mut self, field: &str) -> Self {
71        self.criteria.push(SortCriterion {
72            field: field.to_string(),
73            dir: SortDir::Desc,
74            numeric: true,
75        });
76        self
77    }
78
79    pub fn criterion_count(&self) -> usize {
80        self.criteria.len()
81    }
82
83    pub fn is_empty(&self) -> bool {
84        self.criteria.is_empty()
85    }
86
87    /// Compare two string-keyed records using the composite key.
88    pub fn compare(
89        &self,
90        a: &std::collections::HashMap<String, String>,
91        b: &std::collections::HashMap<String, String>,
92    ) -> std::cmp::Ordering {
93        for c in &self.criteria {
94            let va = a.get(&c.field).map(|s| s.as_str()).unwrap_or("");
95            let vb = b.get(&c.field).map(|s| s.as_str()).unwrap_or("");
96            let ord = if c.numeric {
97                let na: f64 = va.parse().unwrap_or(0.0);
98                let nb: f64 = vb.parse().unwrap_or(0.0);
99                na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal)
100            } else {
101                va.cmp(vb)
102            };
103            let ord = if c.dir == SortDir::Desc {
104                ord.reverse()
105            } else {
106                ord
107            };
108            if ord != std::cmp::Ordering::Equal {
109                return ord;
110            }
111        }
112        std::cmp::Ordering::Equal
113    }
114
115    /// Sort a slice of records in-place.
116    pub fn sort(&self, records: &mut [std::collections::HashMap<String, String>]) {
117        records.sort_by(|a, b| self.compare(a, b));
118    }
119
120    pub fn clear(&mut self) {
121        self.criteria.clear();
122    }
123
124    /// Return a string representation of the sort key.
125    pub fn to_string_repr(&self) -> String {
126        self.criteria
127            .iter()
128            .map(|c| {
129                let dir = if c.dir == SortDir::Asc { "asc" } else { "desc" };
130                format!("{}:{}", c.field, dir)
131            })
132            .collect::<Vec<_>>()
133            .join(",")
134    }
135}
136
137pub fn new_sort_key() -> SortKey {
138    SortKey::new()
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use std::collections::HashMap;
145
146    fn rec(pairs: &[(&str, &str)]) -> HashMap<String, String> {
147        pairs
148            .iter()
149            .map(|(k, v)| (k.to_string(), v.to_string()))
150            .collect()
151    }
152
153    #[test]
154    fn asc_sort() {
155        let key = SortKey::new().asc("name");
156        let mut rows = vec![rec(&[("name", "bob")]), rec(&[("name", "alice")])];
157        key.sort(&mut rows);
158        assert_eq!(rows[0]["name"], "alice");
159    }
160
161    #[test]
162    fn desc_sort() {
163        let key = SortKey::new().desc("name");
164        let mut rows = vec![rec(&[("name", "alice")]), rec(&[("name", "zoo")])];
165        key.sort(&mut rows);
166        assert_eq!(rows[0]["name"], "zoo");
167    }
168
169    #[test]
170    fn numeric_asc() {
171        let key = SortKey::new().asc_num("score");
172        let mut rows = vec![
173            rec(&[("score", "10")]),
174            rec(&[("score", "2")]),
175            rec(&[("score", "20")]),
176        ];
177        key.sort(&mut rows);
178        assert_eq!(rows[0]["score"], "2");
179    }
180
181    #[test]
182    fn numeric_desc() {
183        let key = SortKey::new().desc_num("score");
184        let mut rows = vec![rec(&[("score", "5")]), rec(&[("score", "100")])];
185        key.sort(&mut rows);
186        assert_eq!(rows[0]["score"], "100");
187    }
188
189    #[test]
190    fn criterion_count() {
191        let key = SortKey::new().asc("a").desc("b");
192        assert_eq!(key.criterion_count(), 2);
193    }
194
195    #[test]
196    fn empty_key_no_change() {
197        let key = SortKey::new();
198        let mut rows = vec![rec(&[("x", "z")]), rec(&[("x", "a")])];
199        key.sort(&mut rows);
200        assert_eq!(rows[0]["x"], "z"); // unchanged
201    }
202
203    #[test]
204    fn to_string_repr() {
205        let key = SortKey::new().asc("name").desc("score");
206        assert_eq!(key.to_string_repr(), "name:asc,score:desc");
207    }
208
209    #[test]
210    fn missing_field_treats_as_empty() {
211        let key = SortKey::new().asc("missing");
212        let mut rows = vec![rec(&[("x", "a")]), rec(&[("y", "b")])];
213        key.sort(&mut rows);
214        assert_eq!(rows.len(), 2);
215    }
216
217    #[test]
218    fn clear_criteria() {
219        let mut key = SortKey::new().asc("a");
220        key.clear();
221        assert!(key.is_empty());
222    }
223
224    #[test]
225    fn stable_equal_elements() {
226        let key = SortKey::new().asc("val");
227        let mut rows = vec![rec(&[("val", "same")]), rec(&[("val", "same")])];
228        key.sort(&mut rows);
229        assert_eq!(rows.len(), 2);
230    }
231}