Skip to content

Commit 299d199

Browse files
authored
allow group-by and split-by to work with other values (#14086)
# Description This PR updates `group-by` and `split-by` to allow other nushell Values to be used, namely bools. ### Before ```nushell ❯ [false, false, true, false, true, false] | group-by | table -e Error: nu::shell::cant_convert × Can't convert to string. ╭─[entry #1:1:2] 1 │ [false, false, true, false, true, false] | group-by | table -e · ──┬── · ╰── can't convert bool to string ╰──── ``` ### After ```nushell ❯ [false, false, true, false, true, false] | group-by | table -e ╭───────┬───────────────╮ │ │ ╭───┬───────╮ │ │ false │ │ 0 │ false │ │ │ │ │ 1 │ false │ │ │ │ │ 2 │ false │ │ │ │ │ 3 │ false │ │ │ │ ╰───┴───────╯ │ │ │ ╭───┬──────╮ │ │ true │ │ 0 │ true │ │ │ │ │ 1 │ true │ │ │ │ ╰───┴──────╯ │ ╰───────┴───────────────╯ ``` # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
1 parent 5e784d3 commit 299d199

File tree

3 files changed

+70
-43
lines changed

3 files changed

+70
-43
lines changed

crates/nu-command/src/filters/group_by.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ impl Command for GroupBy {
3838
"Splits a list or table into groups, and returns a record containing those groups."
3939
}
4040

41+
fn extra_description(&self) -> &str {
42+
r#"the group-by command makes some assumptions:
43+
- if the input data is not a string, the grouper will convert the key to string but the values will remain in their original format. e.g. with bools, "true" and true would be in the same group (see example).
44+
- datetime is formatted based on your configuration setting. use `format date` to change the format.
45+
- filesize is formatted based on your configuration setting. use `format filesize` to change the format.
46+
- some nushell values are not supported, such as closures."#
47+
}
48+
4149
fn run(
4250
&self,
4351
engine_state: &EngineState,
@@ -114,6 +122,20 @@ impl Command for GroupBy {
114122
}),
115123
])),
116124
},
125+
Example {
126+
description: "Group bools, whether they are strings or actual bools",
127+
example: r#"[true "true" false "false"] | group-by"#,
128+
result: Some(Value::test_record(record! {
129+
"true" => Value::test_list(vec![
130+
Value::test_bool(true),
131+
Value::test_string("true"),
132+
]),
133+
"false" => Value::test_list(vec![
134+
Value::test_bool(false),
135+
Value::test_string("false"),
136+
]),
137+
})),
138+
}
117139
]
118140
}
119141
}
@@ -127,6 +149,7 @@ pub fn group_by(
127149
let head = call.head;
128150
let grouper: Option<Value> = call.opt(engine_state, stack, 0)?;
129151
let to_table = call.has_flag(engine_state, stack, "to-table")?;
152+
let config = engine_state.get_config();
130153

131154
let values: Vec<Value> = input.into_iter().collect();
132155
if values.is_empty() {
@@ -137,7 +160,7 @@ pub fn group_by(
137160
Some(grouper) => {
138161
let span = grouper.span();
139162
match grouper {
140-
Value::CellPath { val, .. } => group_cell_path(val, values)?,
163+
Value::CellPath { val, .. } => group_cell_path(val, values, config)?,
141164
Value::Closure { val, .. } => {
142165
group_closure(values, span, *val, engine_state, stack)?
143166
}
@@ -149,7 +172,7 @@ pub fn group_by(
149172
}
150173
}
151174
}
152-
None => group_no_grouper(values)?,
175+
None => group_no_grouper(values, config)?,
153176
};
154177

155178
let value = if to_table {
@@ -164,6 +187,7 @@ pub fn group_by(
164187
fn group_cell_path(
165188
column_name: CellPath,
166189
values: Vec<Value>,
190+
config: &nu_protocol::Config,
167191
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
168192
let mut groups = IndexMap::<_, Vec<_>>::new();
169193

@@ -176,18 +200,21 @@ fn group_cell_path(
176200
continue; // likely the result of a failed optional access, ignore this value
177201
}
178202

179-
let key = key.coerce_string()?;
203+
let key = key.to_abbreviated_string(config);
180204
groups.entry(key).or_default().push(value);
181205
}
182206

183207
Ok(groups)
184208
}
185209

186-
fn group_no_grouper(values: Vec<Value>) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
210+
fn group_no_grouper(
211+
values: Vec<Value>,
212+
config: &nu_protocol::Config,
213+
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
187214
let mut groups = IndexMap::<_, Vec<_>>::new();
188215

189216
for value in values.into_iter() {
190-
let key = value.coerce_string()?;
217+
let key = value.to_abbreviated_string(config);
191218
groups.entry(key).or_default().push(value);
192219
}
193220

@@ -203,12 +230,13 @@ fn group_closure(
203230
) -> Result<IndexMap<String, Vec<Value>>, ShellError> {
204231
let mut groups = IndexMap::<_, Vec<_>>::new();
205232
let mut closure = ClosureEval::new(engine_state, stack, closure);
233+
let config = engine_state.get_config();
206234

207235
for value in values {
208236
let key = closure
209237
.run_with_value(value.clone())?
210238
.into_value(span)?
211-
.coerce_into_string()?;
239+
.to_abbreviated_string(config);
212240

213241
groups.entry(key).or_default().push(value);
214242
}

crates/nu-command/src/filters/split_by.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,16 @@ pub fn split_by(
8484
input: PipelineData,
8585
) -> Result<PipelineData, ShellError> {
8686
let name = call.head;
87-
87+
let config = engine_state.get_config();
8888
let splitter: Option<Value> = call.opt(engine_state, stack, 0)?;
8989

9090
match splitter {
9191
Some(v) => {
9292
let splitter = Some(Spanned {
93-
item: v.coerce_into_string()?,
93+
item: v.to_abbreviated_string(config),
9494
span: name,
9595
});
96-
Ok(split(splitter.as_ref(), input, name)?)
96+
Ok(split(splitter.as_ref(), input, name, config)?)
9797
}
9898
// This uses the same format as the 'requires a column name' error in sort_utils.rs
9999
None => Err(ShellError::GenericError {
@@ -110,6 +110,7 @@ pub fn split(
110110
column_name: Option<&Spanned<String>>,
111111
values: PipelineData,
112112
span: Span,
113+
config: &nu_protocol::Config,
113114
) -> Result<PipelineData, ShellError> {
114115
let grouper = if let Some(column_name) = column_name {
115116
Grouper::ByColumn(Some(column_name.clone()))
@@ -127,7 +128,7 @@ pub fn split(
127128
};
128129

129130
match group_key {
130-
Some(group_key) => Ok(group_key.coerce_string()?),
131+
Some(group_key) => Ok(group_key.to_abbreviated_string(config)),
131132
None => Err(ShellError::CantFindColumn {
132133
col_name: column_name.item.to_string(),
133134
span: Some(column_name.span),
@@ -136,12 +137,12 @@ pub fn split(
136137
}
137138
};
138139

139-
data_split(values, Some(&block), span)
140+
data_split(values, Some(&block), span, config)
140141
}
141142
Grouper::ByColumn(None) => {
142-
let block = move |_, row: &Value| row.coerce_string();
143+
let block = move |_, row: &Value| Ok(row.to_abbreviated_string(config));
143144

144-
data_split(values, Some(&block), span)
145+
data_split(values, Some(&block), span, config)
145146
}
146147
}
147148
}
@@ -151,14 +152,15 @@ fn data_group(
151152
values: &Value,
152153
grouper: Option<&dyn Fn(usize, &Value) -> Result<String, ShellError>>,
153154
span: Span,
155+
config: &nu_protocol::Config,
154156
) -> Result<Value, ShellError> {
155157
let mut groups: IndexMap<String, Vec<Value>> = IndexMap::new();
156158

157159
for (idx, value) in values.clone().into_pipeline_data().into_iter().enumerate() {
158160
let group_key = if let Some(ref grouper) = grouper {
159161
grouper(idx, &value)
160162
} else {
161-
value.coerce_string()
163+
Ok(value.to_abbreviated_string(config))
162164
};
163165

164166
let group = groups.entry(group_key?).or_default();
@@ -179,6 +181,7 @@ pub fn data_split(
179181
value: PipelineData,
180182
splitter: Option<&dyn Fn(usize, &Value) -> Result<String, ShellError>>,
181183
dst_span: Span,
184+
config: &nu_protocol::Config,
182185
) -> Result<PipelineData, ShellError> {
183186
let mut splits = indexmap::IndexMap::new();
184187

@@ -188,7 +191,7 @@ pub fn data_split(
188191
match v {
189192
Value::Record { val: grouped, .. } => {
190193
for (outer_key, list) in grouped.into_owned() {
191-
match data_group(&list, splitter, span) {
194+
match data_group(&list, splitter, span, config) {
192195
Ok(grouped_vals) => {
193196
if let Value::Record { val: sub, .. } = grouped_vals {
194197
for (inner_key, subset) in sub.into_owned() {

crates/nu-command/tests/commands/group_by.rs

Lines changed: 24 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,37 +23,33 @@ fn groups() {
2323

2424
#[test]
2525
fn errors_if_given_unknown_column_name() {
26-
let sample = r#"
27-
{
28-
"nu": {
29-
"committers": [
30-
{"name": "Andrés N. Robalino"},
31-
{"name": "JT Turner"},
32-
{"name": "Yehuda Katz"}
33-
],
34-
"releases": [
35-
{"version": "0.2"}
36-
{"version": "0.8"},
37-
{"version": "0.9999999"}
38-
],
39-
"0xATYKARNU": [
40-
["Th", "e", " "],
41-
["BIG", " ", "UnO"],
42-
["punto", "cero"]
43-
]
44-
}
45-
}
46-
"#;
26+
let sample = r#"{
27+
"nu": {
28+
"committers": [
29+
{"name": "Andrés N. Robalino"},
30+
{"name": "JT Turner"},
31+
{"name": "Yehuda Katz"}
32+
],
33+
"releases": [
34+
{"version": "0.2"}
35+
{"version": "0.8"},
36+
{"version": "0.9999999"}
37+
],
38+
"0xATYKARNU": [
39+
["Th", "e", " "],
40+
["BIG", " ", "UnO"],
41+
["punto", "cero"]
42+
]
43+
}
44+
}
45+
"#;
4746

4847
let actual = nu!(pipeline(&format!(
49-
r#"
50-
'{sample}'
51-
| from json
52-
| group-by {{|| get nu.releases.version }}
53-
"#
48+
r#"'{sample}'
49+
| from json
50+
| group-by {{|| get nu.releases.missing_column }}"#
5451
)));
55-
56-
assert!(actual.err.contains("can't convert list<string> to string"));
52+
assert!(actual.err.contains("cannot find column"));
5753
}
5854

5955
#[test]

0 commit comments

Comments
 (0)