nushell/crates/nu-data/src/base.rs

467 lines
17 KiB
Rust
Raw Normal View History

pub mod shape;
2019-11-04 16:47:03 +01:00
use bigdecimal::BigDecimal;
use chrono::{DateTime, FixedOffset, Utc};
2019-05-26 08:54:41 +02:00
use derive_new::new;
use nu_errors::ShellError;
use nu_protocol::{
hir, Primitive, ShellTypeName, SpannedTypeName, TaggedDictBuilder, UntaggedValue, Value,
};
use nu_source::{Span, Tag};
2019-12-09 19:52:01 +01:00
use nu_value_ext::ValueExt;
use num_bigint::BigInt;
2019-08-02 21:15:07 +02:00
use serde::{Deserialize, Serialize};
2019-05-10 18:59:12 +02:00
2019-05-28 04:01:37 +02:00
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Clone, new, Serialize)]
2019-05-26 08:54:41 +02:00
pub struct Operation {
pub(crate) left: Value,
pub(crate) operator: hir::Operator,
pub(crate) right: Value,
2019-05-26 08:54:41 +02:00
}
2019-08-02 21:15:07 +02:00
#[derive(Serialize, Deserialize)]
pub enum Switch {
Present,
Absent,
}
impl std::convert::TryFrom<Option<&Value>> for Switch {
type Error = ShellError;
fn try_from(value: Option<&Value>) -> Result<Switch, ShellError> {
match value {
None => Ok(Switch::Absent),
Some(value) => match &value.value {
UntaggedValue::Primitive(Primitive::Boolean(true)) => Ok(Switch::Present),
_ => Err(ShellError::type_error("Boolean", value.spanned_type_name())),
},
}
}
}
pub fn select_fields(obj: &Value, fields: &[String], tag: impl Into<Tag>) -> Value {
let mut out = TaggedDictBuilder::new(tag);
let descs = obj.data_descriptors();
for column_name in fields {
match descs.iter().find(|d| *d == column_name) {
None => out.insert_untagged(column_name, UntaggedValue::nothing()),
Some(desc) => out.insert_value(desc.clone(), obj.get_data(desc).borrow().clone()),
}
}
out.into_value()
2019-05-10 18:59:12 +02:00
}
pub fn reject_fields(obj: &Value, fields: &[String], tag: impl Into<Tag>) -> Value {
let mut out = TaggedDictBuilder::new(tag);
let descs = obj.data_descriptors();
for desc in descs {
2019-08-29 04:44:08 +02:00
if fields.iter().any(|field| *field == desc) {
continue;
} else {
out.insert_value(desc.clone(), obj.get_data(&desc).borrow().clone())
}
}
out.into_value()
}
2019-05-16 04:42:44 +02:00
pub enum CompareValues {
Ints(i64, i64),
Filesizes(u64, u64),
BigInts(BigInt, BigInt),
Decimals(BigDecimal, BigDecimal),
2019-05-28 08:45:18 +02:00
String(String, String),
Date(DateTime<FixedOffset>, DateTime<FixedOffset>),
2021-06-04 09:39:12 +02:00
DateDuration(DateTime<FixedOffset>, BigInt),
TimeDuration(BigInt, BigInt),
Booleans(bool, bool),
2019-05-28 08:45:18 +02:00
}
impl CompareValues {
pub fn compare(&self) -> std::cmp::Ordering {
2019-05-28 08:45:18 +02:00
match self {
CompareValues::BigInts(left, right) => left.cmp(right),
2019-05-28 08:45:18 +02:00
CompareValues::Ints(left, right) => left.cmp(right),
CompareValues::Filesizes(left, right) => left.cmp(right),
CompareValues::Decimals(left, right) => left.cmp(right),
2019-05-28 08:45:18 +02:00
CompareValues::String(left, right) => left.cmp(right),
2019-11-17 06:48:48 +01:00
CompareValues::Date(left, right) => left.cmp(right),
CompareValues::DateDuration(left, right) => {
// FIXME: Not sure if I could do something better with the Span.
2021-06-04 09:39:12 +02:00
let duration = Primitive::into_chrono_duration(
Primitive::Duration(right.clone()),
Span::unknown(),
)
.expect("Could not convert nushell Duration into chrono Duration.");
let right: DateTime<FixedOffset> = Utc::now()
.checked_sub_signed(duration)
.expect("Data overflow")
.into();
2019-11-17 06:48:48 +01:00
right.cmp(left)
}
CompareValues::Booleans(left, right) => left.cmp(right),
CompareValues::TimeDuration(left, right) => left.cmp(right),
2019-05-28 08:45:18 +02:00
}
}
}
pub fn coerce_compare(
left: &UntaggedValue,
right: &UntaggedValue,
2019-11-04 16:47:03 +01:00
) -> Result<CompareValues, (&'static str, &'static str)> {
match (left, right) {
(UntaggedValue::Primitive(left), UntaggedValue::Primitive(right)) => {
coerce_compare_primitive(left, right)
}
2019-05-28 08:45:18 +02:00
2019-06-24 02:55:31 +02:00
_ => Err((left.type_name(), right.type_name())),
2019-05-28 08:45:18 +02:00
}
}
pub fn coerce_compare_primitive(
2019-06-24 02:55:31 +02:00
left: &Primitive,
right: &Primitive,
2019-11-04 16:47:03 +01:00
) -> Result<CompareValues, (&'static str, &'static str)> {
2019-05-28 08:45:18 +02:00
use Primitive::*;
2019-06-24 02:55:31 +02:00
Ok(match (left, right) {
(Int(left), Int(right)) => CompareValues::Ints(*left, *right),
(Int(left), BigInt(right)) => {
CompareValues::BigInts(num_bigint::BigInt::from(*left), right.clone())
}
(BigInt(left), Int(right)) => {
CompareValues::BigInts(left.clone(), num_bigint::BigInt::from(*right))
}
(BigInt(left), BigInt(right)) => CompareValues::BigInts(left.clone(), right.clone()),
(Int(left), Decimal(right)) => {
CompareValues::Decimals(BigDecimal::from(*left), right.clone())
}
(BigInt(left), Decimal(right)) => {
CompareValues::Decimals(BigDecimal::from(left.clone()), right.clone())
}
(Decimal(left), Decimal(right)) => CompareValues::Decimals(left.clone(), right.clone()),
(Decimal(left), Int(right)) => {
CompareValues::Decimals(left.clone(), BigDecimal::from(*right))
}
(Decimal(left), BigInt(right)) => {
CompareValues::Decimals(left.clone(), BigDecimal::from(right.clone()))
}
(Decimal(left), Filesize(right)) => {
CompareValues::Decimals(left.clone(), BigDecimal::from(*right))
}
(Filesize(left), Filesize(right)) => CompareValues::Filesizes(*left, *right),
2020-07-11 04:17:37 +02:00
(Filesize(left), Decimal(right)) => {
CompareValues::Decimals(BigDecimal::from(*left), right.clone())
}
(Nothing, Nothing) => CompareValues::Booleans(true, true),
2019-06-24 02:55:31 +02:00
(String(left), String(right)) => CompareValues::String(left.clone(), right.clone()),
(Date(left), Date(right)) => CompareValues::Date(*left, *right),
2021-06-04 09:39:12 +02:00
(Date(left), Duration(right)) => CompareValues::DateDuration(*left, right.clone()),
(Boolean(left), Boolean(right)) => CompareValues::Booleans(*left, *right),
(Boolean(left), Nothing) => CompareValues::Booleans(*left, false),
(Nothing, Boolean(right)) => CompareValues::Booleans(false, *right),
2021-07-07 21:21:02 +02:00
(String(left), Nothing) => CompareValues::String(left.clone(), std::string::String::new()),
(Nothing, String(right)) => {
CompareValues::String(std::string::String::new(), right.clone())
}
(FilePath(left), String(right)) => {
CompareValues::String(left.as_path().display().to_string(), right.clone())
}
(String(left), FilePath(right)) => {
CompareValues::String(left.clone(), right.as_path().display().to_string())
}
(Duration(left), Duration(right)) => {
CompareValues::TimeDuration(left.clone(), right.clone())
}
2019-06-24 02:55:31 +02:00
_ => return Err((left.type_name(), right.type_name())),
})
2019-05-28 08:45:18 +02:00
}
#[cfg(test)]
mod tests {
use nu_errors::ShellError;
use nu_protocol::UntaggedValue;
use nu_source::SpannedItem;
use nu_test_support::value::*;
use nu_value_ext::ValueExt;
use indexmap::indexmap;
#[test]
2020-01-02 08:07:17 +01:00
fn gets_matching_field_from_a_row() -> Result<(), ShellError> {
let row = UntaggedValue::row(indexmap! {
"amigos".into() => table(&[string("andres"),string("jonathan"),string("yehuda")])
})
.into_untagged_value();
assert_eq!(
2020-01-02 08:07:17 +01:00
row.get_data_by_key("amigos".spanned_unknown())
.ok_or_else(|| ShellError::unexpected("Failure during testing"))?,
table(&[string("andres"), string("jonathan"), string("yehuda")])
);
2020-01-02 08:07:17 +01:00
Ok(())
}
#[test]
2020-01-02 08:07:17 +01:00
fn gets_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> {
let field_path = column_path("package.version").as_column_path()?;
let (version, tag) = string("0.4.0").into_parts();
let value = UntaggedValue::row(indexmap! {
"package".into() =>
row(indexmap! {
"name".into() => string("nu"),
"version".into() => string("0.4.0")
})
});
assert_eq!(
2020-01-02 08:07:17 +01:00
*value.into_value(tag).get_data_by_column_path(
&field_path.item,
2020-01-02 08:07:17 +01:00
Box::new(error_callback("package.version"))
)?,
version
2020-01-02 08:07:17 +01:00
);
Ok(())
}
2019-11-04 16:47:03 +01:00
#[test]
2020-01-02 08:07:17 +01:00
fn gets_first_matching_field_from_rows_with_same_field_inside_a_table() -> Result<(), ShellError>
{
let field_path = column_path("package.authors.name").as_column_path()?;
2019-11-04 16:47:03 +01:00
let (_, tag) = string("Andrés N. Robalino").into_parts();
let value = UntaggedValue::row(indexmap! {
2019-11-04 16:47:03 +01:00
"package".into() => row(indexmap! {
"name".into() => string("nu"),
"version".into() => string("0.4.0"),
"authors".into() => table(&[
2019-11-04 16:47:03 +01:00
row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
row(indexmap!{"name".into() => string("Jonathan Turner")}),
row(indexmap!{"name".into() => string("Yehuda Katz")})
])
})
});
assert_eq!(
2020-01-02 08:07:17 +01:00
value.into_value(tag).get_data_by_column_path(
&field_path.item,
2020-01-02 08:07:17 +01:00
Box::new(error_callback("package.authors.name"))
)?,
table(&[
2019-11-04 16:47:03 +01:00
string("Andrés N. Robalino"),
string("Jonathan Turner"),
string("Yehuda Katz")
])
2020-01-02 08:07:17 +01:00
);
Ok(())
2019-11-04 16:47:03 +01:00
}
#[test]
2020-01-02 08:07:17 +01:00
fn column_path_that_contains_just_a_number_gets_a_row_from_a_table() -> Result<(), ShellError> {
let field_path = column_path("package.authors.0").as_column_path()?;
let (_, tag) = string("Andrés N. Robalino").into_parts();
let value = UntaggedValue::row(indexmap! {
"package".into() => row(indexmap! {
"name".into() => string("nu"),
"version".into() => string("0.4.0"),
"authors".into() => table(&[
row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
row(indexmap!{"name".into() => string("Jonathan Turner")}),
row(indexmap!{"name".into() => string("Yehuda Katz")})
])
})
});
assert_eq!(
2020-01-02 08:07:17 +01:00
*value.into_value(tag).get_data_by_column_path(
&field_path.item,
2020-01-02 08:07:17 +01:00
Box::new(error_callback("package.authors.0"))
)?,
UntaggedValue::row(indexmap! {
"name".into() => string("Andrés N. Robalino")
})
);
2020-01-02 08:07:17 +01:00
Ok(())
}
#[test]
2020-01-02 08:07:17 +01:00
fn column_path_that_contains_just_a_number_gets_a_row_from_a_row() -> Result<(), ShellError> {
let field_path = column_path(r#"package.authors."0""#).as_column_path()?;
let (_, tag) = string("Andrés N. Robalino").into_parts();
let value = UntaggedValue::row(indexmap! {
"package".into() => row(indexmap! {
"name".into() => string("nu"),
"version".into() => string("0.4.0"),
"authors".into() => row(indexmap! {
"0".into() => row(indexmap!{"name".into() => string("Andrés N. Robalino")}),
"1".into() => row(indexmap!{"name".into() => string("Jonathan Turner")}),
"2".into() => row(indexmap!{"name".into() => string("Yehuda Katz")}),
})
})
});
assert_eq!(
2020-01-02 08:07:17 +01:00
*value.into_value(tag).get_data_by_column_path(
&field_path.item,
2020-01-02 08:07:17 +01:00
Box::new(error_callback("package.authors.\"0\""))
)?,
UntaggedValue::row(indexmap! {
"name".into() => string("Andrés N. Robalino")
})
);
2020-01-02 08:07:17 +01:00
Ok(())
}
#[test]
2020-01-02 08:07:17 +01:00
fn replaces_matching_field_from_a_row() -> Result<(), ShellError> {
let field_path = column_path("amigos").as_column_path()?;
let sample = UntaggedValue::row(indexmap! {
"amigos".into() => table(&[
string("andres"),
string("jonathan"),
string("yehuda"),
]),
});
let replacement = string("jonas");
let actual = sample
.into_untagged_value()
.replace_data_at_column_path(&field_path.item, replacement)
.ok_or_else(|| ShellError::untagged_runtime_error("Could not replace column"))?;
assert_eq!(actual, row(indexmap! {"amigos".into() => string("jonas")}));
2020-01-02 08:07:17 +01:00
Ok(())
}
#[test]
2020-01-02 08:07:17 +01:00
fn replaces_matching_field_from_nested_rows_inside_a_row() -> Result<(), ShellError> {
let field_path = column_path(r#"package.authors."los.3.caballeros""#).as_column_path()?;
let sample = UntaggedValue::row(indexmap! {
"package".into() => row(indexmap! {
"authors".into() => row(indexmap! {
"los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]),
"los.3.amigos".into() => table(&[string("andres::yehuda::jonathan")]),
"los.3.caballeros".into() => table(&[string("andres::yehuda::jonathan")])
})
})
});
let replacement = table(&[string("yehuda::jonathan::andres")]);
let tag = replacement.tag.clone();
let actual = sample
.into_value(&tag)
.replace_data_at_column_path(&field_path.item, replacement.clone())
.ok_or_else(|| {
ShellError::labeled_error(
"Could not replace column",
"could not replace column",
&tag,
)
})?;
assert_eq!(
actual,
UntaggedValue::row(indexmap! {
"package".into() => row(indexmap! {
"authors".into() => row(indexmap! {
"los.3.mosqueteros".into() => table(&[string("andres::yehuda::jonathan")]),
"los.3.amigos".into() => table(&[string("andres::yehuda::jonathan")]),
"los.3.caballeros".into() => replacement})})})
.into_value(tag)
);
2020-01-02 08:07:17 +01:00
Ok(())
}
#[test]
2020-01-02 08:07:17 +01:00
fn replaces_matching_field_from_rows_inside_a_table() -> Result<(), ShellError> {
let field_path =
column_path(r#"shell_policy.releases."nu.version.arepa""#).as_column_path()?;
let sample = UntaggedValue::row(indexmap! {
"shell_policy".into() => row(indexmap! {
"releases".into() => table(&[
row(indexmap! {
"nu.version.arepa".into() => row(indexmap! {
"code".into() => string("0.4.0"), "tag_line".into() => string("GitHub-era")
})
}),
row(indexmap! {
"nu.version.taco".into() => row(indexmap! {
"code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era")
})
}),
row(indexmap! {
"nu.version.stable".into() => row(indexmap! {
"code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era")
})
})
])
})
});
let replacement = row(indexmap! {
"code".into() => string("0.5.0"),
"tag_line".into() => string("CABALLEROS")
});
let tag = replacement.tag.clone();
let actual = sample
.into_value(tag.clone())
.replace_data_at_column_path(&field_path.item, replacement.clone())
.ok_or_else(|| {
ShellError::labeled_error(
"Could not replace column",
"could not replace column",
&tag,
)
})?;
assert_eq!(
actual,
UntaggedValue::row(indexmap! {
"shell_policy".into() => row(indexmap! {
"releases".into() => table(&[
row(indexmap! {
"nu.version.arepa".into() => replacement
}),
row(indexmap! {
"nu.version.taco".into() => row(indexmap! {
"code".into() => string("0.3.0"), "tag_line".into() => string("GitHub-era")
})
}),
row(indexmap! {
"nu.version.stable".into() => row(indexmap! {
"code".into() => string("0.2.0"), "tag_line".into() => string("GitHub-era")
})
})
])
})
}).into_value(&tag)
);
2020-01-02 08:07:17 +01:00
Ok(())
}
}