Skip to content

Commit c5b0777

Browse files
authored
feat: implement sort operation for arrays (#51)
feat: implement sort operation for arrays (closes #22)
1 parent ad53de5 commit c5b0777

File tree

4 files changed

+362
-0
lines changed

4 files changed

+362
-0
lines changed

src/mapping/ast.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,22 @@ pub enum Statement {
108108
},
109109
/// `where <condition>` — filter array elements by condition
110110
Where { condition: Expr, span: Span },
111+
/// `sort .field asc, .field2 desc` — sort array elements
112+
Sort { keys: Vec<SortKey>, span: Span },
113+
}
114+
115+
/// A sort direction.
116+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117+
pub enum SortDirection {
118+
Asc,
119+
Desc,
120+
}
121+
122+
/// A single sort key: path + direction.
123+
#[derive(Debug, Clone, PartialEq)]
124+
pub struct SortKey {
125+
pub path: Path,
126+
pub direction: SortDirection,
111127
}
112128

113129
/// A parsed mapping program: a list of statements.

src/mapping/eval.rs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ fn eval_statement(stmt: &Statement, value: &Value) -> error::Result<Value> {
2727
Statement::Flatten { path, prefix, .. } => eval_flatten(value, path, prefix.as_deref()),
2828
Statement::Nest { paths, target, .. } => eval_nest(value, paths, target),
2929
Statement::Where { condition, .. } => eval_where(value, condition),
30+
Statement::Sort { keys, .. } => eval_sort(value, keys),
3031
}
3132
}
3233

@@ -289,6 +290,50 @@ fn eval_where(value: &Value, condition: &Expr) -> error::Result<Value> {
289290
}
290291
}
291292

293+
// ---------------------------------------------------------------------------
294+
// sort
295+
// ---------------------------------------------------------------------------
296+
297+
fn eval_sort(value: &Value, keys: &[SortKey]) -> error::Result<Value> {
298+
match value {
299+
Value::Array(arr) => {
300+
let mut sorted = arr.clone();
301+
sorted.sort_by(|a, b| {
302+
for key in keys {
303+
let val_a = resolve_path(a, &key.path.segments).unwrap_or(Value::Null);
304+
let val_b = resolve_path(b, &key.path.segments).unwrap_or(Value::Null);
305+
306+
// Nulls always sort last regardless of direction
307+
let ordering = match (&val_a, &val_b) {
308+
(Value::Null, Value::Null) => std::cmp::Ordering::Equal,
309+
(Value::Null, _) => std::cmp::Ordering::Greater, // null last
310+
(_, Value::Null) => std::cmp::Ordering::Less, // null last
311+
_ => compare_values(&val_a, &val_b).unwrap_or(std::cmp::Ordering::Equal),
312+
};
313+
314+
let ordering = match key.direction {
315+
SortDirection::Asc => ordering,
316+
SortDirection::Desc => {
317+
// Reverse, but keep nulls last
318+
match (&val_a, &val_b) {
319+
(Value::Null, _) | (_, Value::Null) => ordering,
320+
_ => ordering.reverse(),
321+
}
322+
}
323+
};
324+
325+
if ordering != std::cmp::Ordering::Equal {
326+
return ordering;
327+
}
328+
}
329+
std::cmp::Ordering::Equal
330+
});
331+
Ok(Value::Array(sorted))
332+
}
333+
_ => Ok(value.clone()), // non-array: no-op
334+
}
335+
}
336+
292337
// ---------------------------------------------------------------------------
293338
// Expression evaluation
294339
// ---------------------------------------------------------------------------

src/mapping/parser.rs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ impl Parser {
100100
TokenKind::Flatten => self.parse_flatten(),
101101
TokenKind::Nest => self.parse_nest(),
102102
TokenKind::Where => self.parse_where(),
103+
TokenKind::Sort => self.parse_sort(),
103104
_ => {
104105
let suggestion = suggest_keyword(&token.kind);
105106
let msg = if let Some(s) = suggestion {
@@ -284,6 +285,43 @@ impl Parser {
284285
})
285286
}
286287

288+
fn parse_sort(&mut self) -> error::Result<Statement> {
289+
let start = self.advance().unwrap(); // consume 'sort'
290+
let mut keys = Vec::new();
291+
292+
// Parse first sort key
293+
let path = self.parse_path()?;
294+
let direction = self.parse_sort_direction();
295+
keys.push(SortKey { path, direction });
296+
297+
// Parse additional sort keys separated by commas
298+
while let Some(TokenKind::Comma) = self.peek_kind() {
299+
self.advance(); // consume comma
300+
let path = self.parse_path()?;
301+
let direction = self.parse_sort_direction();
302+
keys.push(SortKey { path, direction });
303+
}
304+
305+
Ok(Statement::Sort {
306+
keys,
307+
span: start.span,
308+
})
309+
}
310+
311+
fn parse_sort_direction(&mut self) -> SortDirection {
312+
match self.peek_kind() {
313+
Some(TokenKind::Asc) => {
314+
self.advance();
315+
SortDirection::Asc
316+
}
317+
Some(TokenKind::Desc) => {
318+
self.advance();
319+
SortDirection::Desc
320+
}
321+
_ => SortDirection::Asc, // default ascending
322+
}
323+
}
324+
287325
fn parse_path_list(&mut self) -> error::Result<Vec<Path>> {
288326
let mut paths = vec![self.parse_path()?];
289327
while let Some(TokenKind::Comma) = self.peek_kind() {

tests/sort_operation.rs

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
//! Integration tests for issue #22: sort operation.
2+
3+
use indexmap::IndexMap;
4+
use morph::mapping::{eval, parser};
5+
use morph::value::Value;
6+
7+
fn run(mapping: &str, input: &Value) -> Value {
8+
let program = parser::parse_str(mapping).unwrap();
9+
eval::eval(&program, input).unwrap()
10+
}
11+
12+
fn make_map(pairs: &[(&str, Value)]) -> Value {
13+
let mut m = IndexMap::new();
14+
for (k, v) in pairs {
15+
m.insert((*k).to_string(), v.clone());
16+
}
17+
Value::Map(m)
18+
}
19+
20+
fn get_names(val: &Value) -> Vec<String> {
21+
match val {
22+
Value::Array(arr) => arr
23+
.iter()
24+
.map(|v| match v.get_path(".name") {
25+
Some(Value::String(s)) => s.clone(),
26+
_ => "?".into(),
27+
})
28+
.collect(),
29+
_ => panic!("expected array"),
30+
}
31+
}
32+
33+
fn people_data() -> Value {
34+
Value::Array(vec![
35+
make_map(&[
36+
("name", Value::String("Charlie".into())),
37+
("score", Value::Int(70)),
38+
]),
39+
make_map(&[
40+
("name", Value::String("Alice".into())),
41+
("score", Value::Int(90)),
42+
]),
43+
make_map(&[
44+
("name", Value::String("Bob".into())),
45+
("score", Value::Int(80)),
46+
]),
47+
])
48+
}
49+
50+
// ---------------------------------------------------------------------------
51+
// sort .name asc — alphabetical ascending
52+
// ---------------------------------------------------------------------------
53+
54+
#[test]
55+
fn sort_name_asc() {
56+
let result = run("sort .name asc", &people_data());
57+
assert_eq!(get_names(&result), vec!["Alice", "Bob", "Charlie"]);
58+
}
59+
60+
// ---------------------------------------------------------------------------
61+
// sort .name desc — descending
62+
// ---------------------------------------------------------------------------
63+
64+
#[test]
65+
fn sort_name_desc() {
66+
let result = run("sort .name desc", &people_data());
67+
assert_eq!(get_names(&result), vec!["Charlie", "Bob", "Alice"]);
68+
}
69+
70+
// ---------------------------------------------------------------------------
71+
// sort .score desc, .name asc — multi-key
72+
// ---------------------------------------------------------------------------
73+
74+
#[test]
75+
fn sort_multi_key() {
76+
let data = Value::Array(vec![
77+
make_map(&[
78+
("name", Value::String("Bob".into())),
79+
("score", Value::Int(80)),
80+
]),
81+
make_map(&[
82+
("name", Value::String("Alice".into())),
83+
("score", Value::Int(90)),
84+
]),
85+
make_map(&[
86+
("name", Value::String("Charlie".into())),
87+
("score", Value::Int(80)),
88+
]),
89+
make_map(&[
90+
("name", Value::String("Diana".into())),
91+
("score", Value::Int(90)),
92+
]),
93+
]);
94+
95+
let result = run("sort .score desc, .name asc", &data);
96+
// score desc: 90s first, then 80s. Within same score, name asc.
97+
assert_eq!(get_names(&result), vec!["Alice", "Diana", "Bob", "Charlie"]);
98+
}
99+
100+
// ---------------------------------------------------------------------------
101+
// Sort integers
102+
// ---------------------------------------------------------------------------
103+
104+
#[test]
105+
fn sort_integers() {
106+
// Sort objects by an integer field.
107+
let data = Value::Array(vec![
108+
make_map(&[("v", Value::Int(30))]),
109+
make_map(&[("v", Value::Int(10))]),
110+
make_map(&[("v", Value::Int(20))]),
111+
]);
112+
let result = run("sort .v asc", &data);
113+
match &result {
114+
Value::Array(arr) => {
115+
let vals: Vec<i64> = arr
116+
.iter()
117+
.map(|v| match v.get_path(".v") {
118+
Some(Value::Int(i)) => *i,
119+
_ => panic!("expected int"),
120+
})
121+
.collect();
122+
assert_eq!(vals, vec![10, 20, 30]);
123+
}
124+
_ => panic!("expected array"),
125+
}
126+
}
127+
128+
// ---------------------------------------------------------------------------
129+
// Sort floats
130+
// ---------------------------------------------------------------------------
131+
132+
#[test]
133+
fn sort_floats() {
134+
let data = Value::Array(vec![
135+
make_map(&[("v", Value::Float(2.5))]),
136+
make_map(&[("v", Value::Float(1.1))]),
137+
make_map(&[("v", Value::Float(3.7))]),
138+
]);
139+
let result = run("sort .v asc", &data);
140+
match &result {
141+
Value::Array(arr) => {
142+
let vals: Vec<f64> = arr
143+
.iter()
144+
.map(|v| match v.get_path(".v") {
145+
Some(Value::Float(f)) => *f,
146+
_ => panic!("expected float"),
147+
})
148+
.collect();
149+
assert_eq!(vals, vec![1.1, 2.5, 3.7]);
150+
}
151+
_ => panic!("expected array"),
152+
}
153+
}
154+
155+
// ---------------------------------------------------------------------------
156+
// Sort strings
157+
// ---------------------------------------------------------------------------
158+
159+
#[test]
160+
fn sort_strings() {
161+
let data = Value::Array(vec![
162+
make_map(&[("v", Value::String("banana".into()))]),
163+
make_map(&[("v", Value::String("apple".into()))]),
164+
make_map(&[("v", Value::String("cherry".into()))]),
165+
]);
166+
let result = run("sort .v asc", &data);
167+
match &result {
168+
Value::Array(arr) => {
169+
let vals: Vec<&str> = arr
170+
.iter()
171+
.map(|v| match v.get_path(".v") {
172+
Some(Value::String(s)) => s.as_str(),
173+
_ => panic!("expected string"),
174+
})
175+
.collect();
176+
assert_eq!(vals, vec!["apple", "banana", "cherry"]);
177+
}
178+
_ => panic!("expected array"),
179+
}
180+
}
181+
182+
// ---------------------------------------------------------------------------
183+
// Sort with null values (nulls last)
184+
// ---------------------------------------------------------------------------
185+
186+
#[test]
187+
fn sort_nulls_last_asc() {
188+
let data = Value::Array(vec![
189+
make_map(&[("name", Value::String("Charlie".into()))]),
190+
make_map(&[("name", Value::Null)]),
191+
make_map(&[("name", Value::String("Alice".into()))]),
192+
]);
193+
let result = run("sort .name asc", &data);
194+
assert_eq!(get_names(&result), vec!["Alice", "Charlie", "?"]);
195+
// Verify the null is actually last
196+
match &result {
197+
Value::Array(arr) => {
198+
assert_eq!(arr[2].get_path(".name"), Some(&Value::Null));
199+
}
200+
_ => panic!("expected array"),
201+
}
202+
}
203+
204+
#[test]
205+
fn sort_nulls_last_desc() {
206+
let data = Value::Array(vec![
207+
make_map(&[("name", Value::Null)]),
208+
make_map(&[("name", Value::String("Alice".into()))]),
209+
make_map(&[("name", Value::String("Charlie".into()))]),
210+
]);
211+
let result = run("sort .name desc", &data);
212+
// desc: Charlie, Alice, then null last
213+
match &result {
214+
Value::Array(arr) => {
215+
assert_eq!(
216+
arr[0].get_path(".name"),
217+
Some(&Value::String("Charlie".into()))
218+
);
219+
assert_eq!(
220+
arr[1].get_path(".name"),
221+
Some(&Value::String("Alice".into()))
222+
);
223+
assert_eq!(arr[2].get_path(".name"), Some(&Value::Null));
224+
}
225+
_ => panic!("expected array"),
226+
}
227+
}
228+
229+
// ---------------------------------------------------------------------------
230+
// Sort on non-existent field → stable order (all compare equal)
231+
// ---------------------------------------------------------------------------
232+
233+
#[test]
234+
fn sort_nonexistent_field() {
235+
let data = people_data();
236+
let result = run("sort .nonexistent asc", &data);
237+
// All values are null/missing so order is stable (unchanged)
238+
assert_eq!(get_names(&result), vec!["Charlie", "Alice", "Bob"]);
239+
}
240+
241+
// ---------------------------------------------------------------------------
242+
// Sort on non-array → no-op
243+
// ---------------------------------------------------------------------------
244+
245+
#[test]
246+
fn sort_non_array_noop() {
247+
let data = make_map(&[
248+
("name", Value::String("Alice".into())),
249+
("age", Value::Int(30)),
250+
]);
251+
let result = run("sort .name asc", &data);
252+
assert_eq!(result, data);
253+
}
254+
255+
// ---------------------------------------------------------------------------
256+
// Default direction is ascending
257+
// ---------------------------------------------------------------------------
258+
259+
#[test]
260+
fn sort_default_direction_asc() {
261+
let result = run("sort .name", &people_data());
262+
assert_eq!(get_names(&result), vec!["Alice", "Bob", "Charlie"]);
263+
}

0 commit comments

Comments
 (0)