empty? rewrite. (#2641)

This commit is contained in:
Andrés N. Robalino 2020-10-06 05:21:20 -05:00 committed by GitHub
parent df07be6a42
commit 5d945ef869
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 498 additions and 338 deletions

View file

@ -196,7 +196,7 @@ pub fn create_default_context(interactive: bool) -> Result<EvaluationContext, Bo
whole_stream_command(Each),
whole_stream_command(EachGroup),
whole_stream_command(EachWindow),
whole_stream_command(IsEmpty),
whole_stream_command(Empty),
// Table manipulation
whole_stream_command(Move),
whole_stream_command(Merge),

View file

@ -36,6 +36,7 @@ pub(crate) mod drop;
pub(crate) mod du;
pub(crate) mod each;
pub(crate) mod echo;
pub(crate) mod empty;
pub(crate) mod enter;
pub(crate) mod every;
pub(crate) mod exec;
@ -67,7 +68,6 @@ pub(crate) mod history;
pub(crate) mod if_;
pub(crate) mod insert;
pub(crate) mod into_int;
pub(crate) mod is_empty;
pub(crate) mod keep;
pub(crate) mod last;
pub(crate) mod lines;
@ -161,8 +161,8 @@ pub(crate) use each::Each;
pub(crate) use each::EachGroup;
pub(crate) use each::EachWindow;
pub(crate) use echo::Echo;
pub(crate) use empty::Command as Empty;
pub(crate) use if_::If;
pub(crate) use is_empty::IsEmpty;
pub(crate) use nu::NuPlugin;
pub(crate) use update::Command as Update;
pub(crate) mod kill;
@ -280,12 +280,11 @@ mod tests {
fn commands() -> Vec<Command> {
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),
]
}

View file

@ -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<Value>,
}
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<OutputStream, ShellError> {
is_empty(args, registry).await
}
fn examples(&self) -> Vec<Example> {
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<OutputStream, ShellError> {
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, &registry));
let scope = args.call_info.scope.clone();
let (Arguments { rest }, input) = args.process(&registry).await?;
let (columns, default_block): (Vec<ColumnPath>, Option<Block>) = 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<Value>) -> Result<(Vec<ColumnPath>, Option<Block>), 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<Scope>,
mut context: Arc<EvaluationContext>,
input: Value,
default_block: Arc<Option<Block>>,
column_paths: Vec<ColumnPath>,
tag: Arc<Tag>,
) -> Result<OutputStream, ShellError> {
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)
}
}))),
}
}

View file

@ -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 {

View file

@ -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<Tagged<ColumnPath>>, Value),
RowWithField(Tagged<ColumnPath>),
RowWithFieldAndFallback(Box<Tagged<ColumnPath>>, Value),
}
pub struct IsEmpty;
#[derive(Deserialize)]
pub struct IsEmptyArgs {
rest: Vec<Value>,
}
#[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<OutputStream, ShellError> {
is_empty(args, registry).await
}
}
async fn is_empty(
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let name_tag = args.call_info.name_tag.clone();
let registry = registry.clone();
let (IsEmptyArgs { rest }, input) = args.process(&registry).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 {})?)
}
}

View file

@ -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 {

View file

@ -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<Value>,
}
@ -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),

View file

@ -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");
}

View file

@ -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");
})
}

View file

@ -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;

View file

@ -448,6 +448,18 @@ impl From<&str> for Value {
}
}
impl From<bool> for Value {
fn from(s: bool) -> Value {
Value {
value: s.into(),
tag: Tag {
anchor: None,
span: Span::unknown(),
},
}
}
}
impl<T> From<T> for UntaggedValue
where
T: Into<Primitive>,

76
docs/commands/empty.md Normal file
View file

@ -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
═══╩════
```