diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index 8e6df277d..f38205b8e 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -196,7 +196,7 @@ pub fn create_default_context(interactive: bool) -> Result Vec { vec![ - // Table operations whole_stream_command(Append), whole_stream_command(GroupBy), - // Row specific operations whole_stream_command(Insert), whole_stream_command(Update), + whole_stream_command(Empty), ] } diff --git a/crates/nu-cli/src/commands/empty.rs b/crates/nu-cli/src/commands/empty.rs new file mode 100644 index 000000000..ad7993c1c --- /dev/null +++ b/crates/nu-cli/src/commands/empty.rs @@ -0,0 +1,288 @@ +use crate::command_registry::CommandRegistry; +use crate::commands::classified::block::run_block; +use crate::commands::WholeStreamCommand; +use crate::prelude::*; +use nu_errors::ShellError; +use nu_protocol::{ + hir::Block, ColumnPath, Primitive, ReturnSuccess, Scope, Signature, SyntaxShape, UntaggedValue, + Value, +}; +use nu_source::Tagged; +use nu_value_ext::{as_string, ValueExt}; + +use futures::stream::once; +use indexmap::indexmap; + +#[derive(Deserialize)] +pub struct Arguments { + rest: Vec, +} + +pub struct Command; + +#[async_trait] +impl WholeStreamCommand for Command { + fn name(&self) -> &str { + "empty?" + } + + fn signature(&self) -> Signature { + Signature::build("empty?").rest( + SyntaxShape::Any, + "the names of the columns to check emptiness. Pass an optional block to replace if empty", + ) + } + + fn usage(&self) -> &str { + "Check for empty values" + } + + async fn run( + &self, + args: CommandArgs, + registry: &CommandRegistry, + ) -> Result { + is_empty(args, registry).await + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Check if a value is empty", + example: "echo '' | empty?", + result: Some(vec![UntaggedValue::boolean(true).into()]), + }, + Example { + description: "more than one column", + example: "echo [[meal size]; [arepa small] [taco '']] | empty? meal size", + result: Some( + vec![ + UntaggedValue::row(indexmap! { + "meal".to_string() => Value::from(false), + "size".to_string() => Value::from(false), + }) + .into(), + UntaggedValue::row(indexmap! { + "meal".to_string() => Value::from(false), + "size".to_string() => Value::from(true), + }) + .into(), + ], + ), + },Example { + description: "use a block if setting the empty cell contents is wanted", + example: "echo [[2020/04/16 2020/07/10 2020/11/16]; ['' [27] [37]]] | empty? 2020/04/16 { = [33 37] }", + result: Some( + vec![ + UntaggedValue::row(indexmap! { + "2020/04/16".to_string() => UntaggedValue::table(&[UntaggedValue::int(33).into(), UntaggedValue::int(37).into()]).into(), + "2020/07/10".to_string() => UntaggedValue::table(&[UntaggedValue::int(27).into()]).into(), + "2020/11/16".to_string() => UntaggedValue::table(&[UntaggedValue::int(37).into()]).into(), + }) + .into(), + ], + ), + }, + ] + } +} + +async fn is_empty( + args: CommandArgs, + registry: &CommandRegistry, +) -> Result { + let tag = args.call_info.name_tag.clone(); + let name_tag = Arc::new(args.call_info.name_tag.clone()); + let context = Arc::new(EvaluationContext::from_raw(&args, ®istry)); + let scope = args.call_info.scope.clone(); + let (Arguments { rest }, input) = args.process(®istry).await?; + let (columns, default_block): (Vec, Option) = arguments(rest)?; + let default_block = Arc::new(default_block); + + if input.is_empty() { + let stream = futures::stream::iter(vec![ + UntaggedValue::Primitive(Primitive::Nothing).into_value(tag) + ]); + + return Ok(InputStream::from_stream(stream) + .then(move |input| { + let tag = name_tag.clone(); + let scope = scope.clone(); + let context = context.clone(); + let block = default_block.clone(); + let columns = vec![]; + + async { + match process_row(scope, context, input, block, columns, tag).await { + Ok(s) => s, + Err(e) => OutputStream::one(Err(e)), + } + } + }) + .flatten() + .to_output_stream()); + } + + Ok(input + .then(move |input| { + let tag = name_tag.clone(); + let scope = scope.clone(); + let context = context.clone(); + let block = default_block.clone(); + let columns = columns.clone(); + + async { + match process_row(scope, context, input, block, columns, tag).await { + Ok(s) => s, + Err(e) => OutputStream::one(Err(e)), + } + } + }) + .flatten() + .to_output_stream()) +} + +fn arguments(rest: Vec) -> Result<(Vec, Option), ShellError> { + let mut rest = rest; + let mut columns = vec![]; + let mut default = None; + + let last_argument = rest.pop(); + + match last_argument { + Some(Value { + value: UntaggedValue::Block(call), + .. + }) => default = Some(call), + Some(other) => { + let Tagged { item: path, .. } = other.as_column_path()?; + + columns = vec![path]; + } + None => {} + }; + + for argument in rest { + let Tagged { item: path, .. } = argument.as_column_path()?; + + columns.push(path); + } + + Ok((columns, default)) +} + +async fn process_row( + scope: Arc, + mut context: Arc, + input: Value, + default_block: Arc>, + column_paths: Vec, + tag: Arc, +) -> Result { + let _tag = &*tag; + let mut out = Arc::new(None); + let results = Arc::make_mut(&mut out); + + if let Some(default_block) = &*default_block { + let for_block = input.clone(); + let input_stream = once(async { Ok(for_block) }).to_input_stream(); + + let scope = Scope::append_it(scope, input.clone()); + + let mut stream = run_block( + &default_block, + Arc::make_mut(&mut context), + input_stream, + scope, + ) + .await?; + + *results = Some({ + let values = stream.drain_vec().await; + + let errors = context.get_errors(); + + if let Some(error) = errors.first() { + return Err(error.clone()); + } + + if values.len() == 1 { + let value = values + .get(0) + .ok_or_else(|| ShellError::unexpected("No value."))?; + + Value { + value: value.value.clone(), + tag: input.tag.clone(), + } + } else if values.is_empty() { + UntaggedValue::nothing().into_value(&input.tag) + } else { + UntaggedValue::table(&values).into_value(&input.tag) + } + }); + } + + match input { + Value { + value: UntaggedValue::Row(ref r), + ref tag, + } => { + if column_paths.is_empty() { + Ok(OutputStream::one(ReturnSuccess::value({ + let is_empty = input.is_empty(); + + if default_block.is_some() { + if is_empty { + results + .clone() + .unwrap_or_else(|| UntaggedValue::boolean(true).into_value(tag)) + } else { + input.clone() + } + } else { + UntaggedValue::boolean(is_empty).into_value(tag) + } + }))) + } else { + let mut obj = input.clone(); + + for column in column_paths.clone() { + let path = UntaggedValue::Primitive(Primitive::ColumnPath(column.clone())) + .into_value(tag); + let data = r.get_data(&as_string(&path)?).borrow().clone(); + let is_empty = data.is_empty(); + + let default = if default_block.is_some() { + if is_empty { + results + .clone() + .unwrap_or_else(|| UntaggedValue::boolean(true).into_value(tag)) + } else { + data.clone() + } + } else { + UntaggedValue::boolean(is_empty).into_value(tag) + }; + + if let Ok(value) = + obj.swap_data_by_column_path(&column, Box::new(move |_| Ok(default))) + { + obj = value; + } + } + + Ok(OutputStream::one(ReturnSuccess::value(obj))) + } + } + other => Ok(OutputStream::one(ReturnSuccess::value({ + if other.is_empty() { + results + .clone() + .unwrap_or_else(|| UntaggedValue::boolean(true).into_value(other.tag)) + } else { + UntaggedValue::boolean(false).into_value(other.tag) + } + }))), + } +} diff --git a/crates/nu-cli/src/commands/insert.rs b/crates/nu-cli/src/commands/insert.rs index 15cce8a7f..373d675e6 100644 --- a/crates/nu-cli/src/commands/insert.rs +++ b/crates/nu-cli/src/commands/insert.rs @@ -103,13 +103,16 @@ async fn process_row( let result = if values.len() == 1 { let value = values .get(0) - .ok_or_else(|| ShellError::unexpected("No value to insert with"))?; + .ok_or_else(|| ShellError::unexpected("No value to insert with."))?; - value.clone() + Value { + value: value.value.clone(), + tag: input.tag.clone(), + } } else if values.is_empty() { - UntaggedValue::nothing().into_untagged_value() + UntaggedValue::nothing().into_value(&input.tag) } else { - UntaggedValue::table(&values).into_untagged_value() + UntaggedValue::table(&values).into_value(&input.tag) }; match input { diff --git a/crates/nu-cli/src/commands/is_empty.rs b/crates/nu-cli/src/commands/is_empty.rs deleted file mode 100644 index c44324c5c..000000000 --- a/crates/nu-cli/src/commands/is_empty.rs +++ /dev/null @@ -1,218 +0,0 @@ -use crate::command_registry::CommandRegistry; -use crate::commands::WholeStreamCommand; -use crate::prelude::*; -use nu_errors::ShellError; -use nu_protocol::{ColumnPath, ReturnSuccess, Signature, SyntaxShape, UntaggedValue, Value}; -use nu_source::Tagged; -use nu_value_ext::ValueExt; - -enum IsEmptyFor { - Value, - RowWithFieldsAndFallback(Vec>, Value), - RowWithField(Tagged), - RowWithFieldAndFallback(Box>, Value), -} - -pub struct IsEmpty; - -#[derive(Deserialize)] -pub struct IsEmptyArgs { - rest: Vec, -} - -#[async_trait] -impl WholeStreamCommand for IsEmpty { - fn name(&self) -> &str { - "empty?" - } - - fn signature(&self) -> Signature { - Signature::build("empty?").rest( - SyntaxShape::Any, - "the names of the columns to check emptiness followed by the replacement value.", - ) - } - - fn usage(&self) -> &str { - "Checks emptiness. The last value is the replacement value for any empty column(s) given to check against the table." - } - - async fn run( - &self, - args: CommandArgs, - registry: &CommandRegistry, - ) -> Result { - is_empty(args, registry).await - } -} - -async fn is_empty( - args: CommandArgs, - registry: &CommandRegistry, -) -> Result { - let name_tag = args.call_info.name_tag.clone(); - let registry = registry.clone(); - let (IsEmptyArgs { rest }, input) = args.process(®istry).await?; - - if input.is_empty() { - return Ok(OutputStream::one(ReturnSuccess::value( - UntaggedValue::boolean(true).into_value(name_tag), - ))); - } - - Ok(input - .map(move |value| { - let value_tag = value.tag(); - - let action = if rest.len() <= 2 { - let field = rest.get(0); - let replacement_if_true = rest.get(1); - - match (field, replacement_if_true) { - (Some(field), Some(replacement_if_true)) => { - IsEmptyFor::RowWithFieldAndFallback( - Box::new(field.as_column_path()?), - replacement_if_true.clone(), - ) - } - (Some(field), None) => IsEmptyFor::RowWithField(field.as_column_path()?), - (_, _) => IsEmptyFor::Value, - } - } else { - let mut arguments = rest.iter().rev(); - let replacement_if_true = match arguments.next() { - Some(arg) => arg.clone(), - None => UntaggedValue::boolean(value.is_empty()).into_value(&value_tag), - }; - - IsEmptyFor::RowWithFieldsAndFallback( - arguments - .map(|a| a.as_column_path()) - .filter_map(Result::ok) - .collect(), - replacement_if_true, - ) - }; - - match action { - IsEmptyFor::Value => Ok(ReturnSuccess::Value( - UntaggedValue::boolean(value.is_empty()).into_value(value_tag), - )), - IsEmptyFor::RowWithFieldsAndFallback(fields, default) => { - let mut out = value; - - for field in fields.iter() { - let val = crate::commands::get::get_column_path(&field, &out)?; - - let emptiness_value = match out { - obj - @ - Value { - value: UntaggedValue::Row(_), - .. - } => { - if val.is_empty() { - obj.replace_data_at_column_path(&field, default.clone()) - .ok_or_else(|| { - ShellError::labeled_error( - "empty? could not find place to check emptiness", - "column name", - &field.tag, - ) - }) - } else { - Ok(obj) - } - } - _ => Err(ShellError::labeled_error( - "Unrecognized type in stream", - "original value", - &value_tag, - )), - }; - - out = emptiness_value?; - } - - Ok(ReturnSuccess::Value(out)) - } - IsEmptyFor::RowWithField(field) => { - let val = crate::commands::get::get_column_path(&field, &value)?; - - match &value { - obj - @ - Value { - value: UntaggedValue::Row(_), - .. - } => { - if val.is_empty() { - match obj.replace_data_at_column_path( - &field, - UntaggedValue::boolean(true).into_value(&value_tag), - ) { - Some(v) => Ok(ReturnSuccess::Value(v)), - None => Err(ShellError::labeled_error( - "empty? could not find place to check emptiness", - "column name", - &field.tag, - )), - } - } else { - Ok(ReturnSuccess::Value(value)) - } - } - _ => Err(ShellError::labeled_error( - "Unrecognized type in stream", - "original value", - &value_tag, - )), - } - } - IsEmptyFor::RowWithFieldAndFallback(field, default) => { - let val = crate::commands::get::get_column_path(&field, &value)?; - - match &value { - obj - @ - Value { - value: UntaggedValue::Row(_), - .. - } => { - if val.is_empty() { - match obj.replace_data_at_column_path(&field, default) { - Some(v) => Ok(ReturnSuccess::Value(v)), - None => Err(ShellError::labeled_error( - "empty? could not find place to check emptiness", - "column name", - &field.tag, - )), - } - } else { - Ok(ReturnSuccess::Value(value)) - } - } - _ => Err(ShellError::labeled_error( - "Unrecognized type in stream", - "original value", - &value_tag, - )), - } - } - } - }) - .to_output_stream()) -} - -#[cfg(test)] -mod tests { - use super::IsEmpty; - use super::ShellError; - - #[test] - fn examples_work_as_expected() -> Result<(), ShellError> { - use crate::examples::test as test_examples; - - Ok(test_examples(IsEmpty {})?) - } -} diff --git a/crates/nu-cli/src/commands/update.rs b/crates/nu-cli/src/commands/update.rs index 3523a750f..430011fa9 100644 --- a/crates/nu-cli/src/commands/update.rs +++ b/crates/nu-cli/src/commands/update.rs @@ -108,13 +108,16 @@ async fn process_row( let result = if values.len() == 1 { let value = values .get(0) - .ok_or_else(|| ShellError::unexpected("No value to update with"))?; + .ok_or_else(|| ShellError::unexpected("No value to update with."))?; - value.clone() + Value { + value: value.value.clone(), + tag: input.tag.clone(), + } } else if values.is_empty() { - UntaggedValue::nothing().into_untagged_value() + UntaggedValue::nothing().into_value(&input.tag) } else { - UntaggedValue::table(&values).into_untagged_value() + UntaggedValue::table(&values).into_value(&input.tag) }; match input { diff --git a/crates/nu-cli/src/examples.rs b/crates/nu-cli/src/examples.rs index c2478a60a..3d893b177 100644 --- a/crates/nu-cli/src/examples.rs +++ b/crates/nu-cli/src/examples.rs @@ -1,7 +1,7 @@ use nu_errors::ShellError; use nu_protocol::hir::ClassifiedBlock; use nu_protocol::{ - ReturnSuccess, Scope, ShellTypeName, Signature, SyntaxShape, UntaggedValue, Value, + Primitive, ReturnSuccess, Scope, ShellTypeName, Signature, SyntaxShape, UntaggedValue, Value, }; use nu_source::{AnchorLocation, TaggedItem}; @@ -17,7 +17,7 @@ use crate::commands::classified::block::run_block; use crate::commands::command::CommandArgs; use crate::commands::{ whole_stream_command, BuildString, Command, Each, Echo, Get, Keep, StrCollect, - WholeStreamCommand, + WholeStreamCommand, Wrap, }; use crate::evaluation_context::EvaluationContext; use crate::stream::{InputStream, OutputStream}; @@ -41,6 +41,7 @@ pub fn test_examples(cmd: Command) -> Result<(), ShellError> { whole_stream_command(Keep {}), whole_stream_command(Each {}), whole_stream_command(StrCollect), + whole_stream_command(Wrap), cmd, ]); @@ -103,6 +104,7 @@ pub fn test(cmd: impl WholeStreamCommand + 'static) -> Result<(), ShellError> { whole_stream_command(Each {}), whole_stream_command(cmd), whole_stream_command(StrCollect), + whole_stream_command(Wrap), ]); for sample_pipeline in examples { @@ -166,6 +168,7 @@ pub fn test_anchors(cmd: Command) -> Result<(), ShellError> { whole_stream_command(Keep {}), whole_stream_command(Each {}), whole_stream_command(StrCollect), + whole_stream_command(Wrap), cmd, ]); @@ -306,12 +309,13 @@ impl WholeStreamCommand for MockCommand { if open_mock { if let Some(true) = mocked_path { - let mocked_path = Some(mock_path()); - let value = out.tagged(name_tag.span).map_anchored(&mocked_path); - - return Ok(OutputStream::one(Ok(ReturnSuccess::Value( - value.item.into_value(value.tag), - )))); + return Ok(OutputStream::one(Ok(ReturnSuccess::Value(Value { + value: out, + tag: Tag { + anchor: Some(mock_path()), + span: name_tag.span, + }, + })))); } } @@ -324,7 +328,7 @@ impl WholeStreamCommand for MockCommand { struct MockEcho; #[derive(Deserialize)] -pub struct MockEchoArgs { +struct MockEchoArgs { pub rest: Vec, } @@ -360,9 +364,10 @@ impl WholeStreamCommand for MockEcho { let stream = rest.into_iter().map(move |i| { let base_value = base_value.clone(); match i.as_string() { - Ok(s) => OutputStream::one(Ok(ReturnSuccess::Value( - UntaggedValue::string(s).into_value(base_value.tag.clone()), - ))), + Ok(s) => OutputStream::one(Ok(ReturnSuccess::Value(Value { + value: UntaggedValue::Primitive(Primitive::String(s)), + tag: base_value.tag.clone(), + }))), _ => match i { Value { value: UntaggedValue::Table(table), diff --git a/crates/nu-cli/tests/commands/empty.rs b/crates/nu-cli/tests/commands/empty.rs new file mode 100644 index 000000000..5af824609 --- /dev/null +++ b/crates/nu-cli/tests/commands/empty.rs @@ -0,0 +1,86 @@ +use nu_test_support::{nu, pipeline}; + +#[test] +fn reports_emptiness() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [[are_empty]; + [$(= [[check]; [[]] ])] + [$(= [[check]; [""] ])] + [$(= [[check]; [$(wrap)] ])] + ] + | get are_empty + | empty? check + | where check + | count + "# + )); + + assert_eq!(actual.out, "3"); +} + +#[test] +fn sets_block_run_value_for_an_empty_column() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [ + [ first_name, last_name, rusty_at, likes ]; + [ Andrés, Robalino, 10/11/2013, 1 ] + [ Jonathan, Turner, 10/12/2013, 1 ] + [ Jason, Gedge, 10/11/2013, 1 ] + [ Yehuda, Katz, 10/11/2013, '' ] + ] + | empty? likes { = 1 } + | get likes + | math sum + "# + )); + + assert_eq!(actual.out, "4"); +} + +#[test] +fn sets_block_run_value_for_many_empty_columns() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [ + [ boost check ]; + [ 1, [] ] + [ 1, "" ] + [ 1, $(wrap) ] + ] + | empty? boost check { = 1 } + | get boost check + | math sum + "# + )); + + assert_eq!(actual.out, "6"); +} + +#[test] +fn passing_a_block_will_set_contents_on_empty_cells_and_leave_non_empty_ones_untouched() { + let actual = nu!( + cwd: ".", pipeline( + r#" + echo [ + [ NAME, LVL, HP ]; + [ Andrés, 30, 3000 ] + [ Alistair, 29, 2900 ] + [ Arepas, "", "" ] + [ Jorge, 30, 3000 ] + ] + | empty? LVL { = 9 } + | empty? HP { + get LVL | = $it * 1000 + } + | math sum + | get HP + "# + )); + + assert_eq!(actual.out, "17900"); +} diff --git a/crates/nu-cli/tests/commands/is_empty.rs b/crates/nu-cli/tests/commands/is_empty.rs deleted file mode 100644 index da6594713..000000000 --- a/crates/nu-cli/tests/commands/is_empty.rs +++ /dev/null @@ -1,94 +0,0 @@ -use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; -use nu_test_support::playground::Playground; -use nu_test_support::{nu, pipeline}; - -#[test] -fn adds_value_provided_if_column_is_empty() { - Playground::setup("is_empty_test_1", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContentToBeTrimmed( - "likes.csv", - r#" - first_name,last_name,rusty_at,likes - Andrés,Robalino,10/11/2013,1 - Jonathan,Turner,10/12/2013,1 - Jason,Gedge,10/11/2013,1 - Yehuda,Katz,10/11/2013, - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), pipeline( - r#" - open likes.csv - | empty? likes 1 - | get likes - | math sum - | echo $it - "# - )); - - assert_eq!(actual.out, "4"); - }) -} - -#[test] -fn adds_value_provided_for_columns_that_are_empty() { - Playground::setup("is_empty_test_2", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContentToBeTrimmed( - "checks.json", - r#" - [ - {"boost": 1, "check": []}, - {"boost": 1, "check": ""}, - {"boost": 1, "check": {}} - ] - - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), pipeline( - r#" - open checks.json - | empty? boost check 1 - | get boost check - | math sum - | echo $it - "# - )); - - assert_eq!(actual.out, "6"); - }) -} - -#[test] -fn value_emptiness_check() { - Playground::setup("is_empty_test_3", |dirs, sandbox| { - sandbox.with_files(vec![FileWithContentToBeTrimmed( - "checks.json", - r#" - { - "are_empty": [ - {"check": []}, - {"check": ""}, - {"check": {}} - ] - } - "#, - )]); - - let actual = nu!( - cwd: dirs.test(), pipeline( - r#" - open checks.json - | get are_empty.check - | empty? - | where $it - | count - | echo $it - "# - )); - - assert_eq!(actual.out, "3"); - }) -} diff --git a/crates/nu-cli/tests/commands/mod.rs b/crates/nu-cli/tests/commands/mod.rs index 65ab993c8..c8d83d550 100644 --- a/crates/nu-cli/tests/commands/mod.rs +++ b/crates/nu-cli/tests/commands/mod.rs @@ -12,6 +12,7 @@ mod default; mod drop; mod each; mod echo; +mod empty; mod enter; mod every; mod first; @@ -22,7 +23,6 @@ mod headers; mod histogram; mod insert; mod into_int; -mod is_empty; mod keep; mod last; mod lines; diff --git a/crates/nu-protocol/src/value.rs b/crates/nu-protocol/src/value.rs index e58cdcba0..bb9ef173c 100644 --- a/crates/nu-protocol/src/value.rs +++ b/crates/nu-protocol/src/value.rs @@ -448,6 +448,18 @@ impl From<&str> for Value { } } +impl From for Value { + fn from(s: bool) -> Value { + Value { + value: s.into(), + tag: Tag { + anchor: None, + span: Span::unknown(), + }, + } + } +} + impl From for UntaggedValue where T: Into, diff --git a/docs/commands/empty.md b/docs/commands/empty.md new file mode 100644 index 000000000..6a96bbd3a --- /dev/null +++ b/docs/commands/empty.md @@ -0,0 +1,76 @@ +# empty? + +Check for empty values. Pass the column names to check emptiness. Optionally pass a block as the last parameter if setting contents to empty columns is wanted. + +## Examples + +Check if a value is empty +```shell +> echo '' | empty? +true +``` + +Given the following meals +```shell +> echo [[meal size]; [arepa small] [taco '']] +═══╦═══════╦═══════ + # ║ meal ║ size +═══╬═══════╬═══════ + 0 ║ arepa ║ small + 1 ║ taco ║ +═══╩═══════╩═══════ +``` + +Show the empty contents +```shell +> echo [[meal size]; [arepa small] [taco '']] | empty? meal size +═══╦══════╦══════ + # ║ meal ║ size +═══╬══════╬══════ + 0 ║ No ║ No + 1 ║ No ║ Yes +═══╩══════╩══════ +``` + +Let's assume we have a report of totals per day. For simplicity we show just for three days `2020/04/16`, `2020/07/10`, and `2020/11/16`. Like so +```shell +> echo [[2020/04/16 2020/07/10 2020/11/16]; ['' 27 37]] +═══╦════════════╦════════════╦════════════ + # ║ 2020/04/16 ║ 2020/07/10 ║ 2020/11/16 +═══╬════════════╬════════════╬════════════ + 0 ║ ║ 27 ║ 37 +═══╩════════════╩════════════╩════════════ +``` + +In the future, the report now has many totals logged per day. In this example, we have 1 total for the day `2020/07/10` and `2020/11/16` like so +```shell +> echo [[2020/04/16 2020/07/10 2020/11/16]; ['' [27] [37]]] +═══╦════════════╦════════════════╦════════════════ + # ║ 2020/04/16 ║ 2020/07/10 ║ 2020/11/16 +═══╬════════════╬════════════════╬════════════════ + 0 ║ ║ [table 1 rows] ║ [table 1 rows] +═══╩════════════╩════════════════╩════════════════ +``` + +We want to add two totals (numbers `33` and `37`) for the day `2020/04/16` + +Set a table with two numbers for the empty column +```shell +> echo [[2020/04/16 2020/07/10 2020/11/16]; ['' [27] [37]]] | empty? 2020/04/16 { = [33 37] } +═══╦════════════════╦════════════════╦════════════════ + # ║ 2020/04/16 ║ 2020/07/10 ║ 2020/11/16 +═══╬════════════════╬════════════════╬════════════════ + 0 ║ [table 2 rows] ║ [table 1 rows] ║ [table 1 rows] +═══╩════════════════╩════════════════╩════════════════ +``` + +Checking all the numbers +```shell +> echo [[2020/04/16 2020/07/10 2020/11/16]; ['' [27] [37]]] | empty? 2020/04/16 { = [33 37] } | pivot _ totals | get totals +═══╦════ + 0 ║ 33 + 1 ║ 37 + 2 ║ 27 + 3 ║ 37 +═══╩════ +``` \ No newline at end of file