mirror of
https://github.com/nushell/nushell.git
synced 2025-08-18 09:29:48 +02:00
Create Record
type (#10103)
# Description This PR creates a new `Record` type to reduce duplicate code and possibly bugs as well. (This is an edited version of #9648.) - `Record` implements `FromIterator` and `IntoIterator` and so can be iterated over or collected into. For example, this helps with conversions to and from (hash)maps. (Also, no more `cols.iter().zip(vals)`!) - `Record` has a `push(col, val)` function to help insure that the number of columns is equal to the number of values. I caught a few potential bugs thanks to this (e.g. in the `ls` command). - Finally, this PR also adds a `record!` macro that helps simplify record creation. It is used like so: ```rust record! { "key1" => some_value, "key2" => Value::string("text", span), "key3" => Value::int(optional_int.unwrap_or(0), span), "key4" => Value::bool(config.setting, span), } ``` Since macros hinder formatting, etc., the right hand side values should be relatively short and sweet like the examples above. Where possible, prefer `record!` or `.collect()` on an iterator instead of multiple `Record::push`s, since the first two automatically set the record capacity and do less work overall. # User-Facing Changes Besides the changes in `nu-protocol` the only other breaking changes are to `nu-table::{ExpandedTable::build_map, JustTable::kv_table}`.
This commit is contained in:
@@ -4,8 +4,7 @@ use std::str::FromStr;
|
||||
|
||||
use crate::ast::{CellPath, MatchPattern, PathMember};
|
||||
use crate::engine::{Block, Closure};
|
||||
use crate::ShellError;
|
||||
use crate::{Range, Spanned, Value};
|
||||
use crate::{Range, Record, ShellError, Spanned, Value};
|
||||
use chrono::{DateTime, FixedOffset};
|
||||
|
||||
pub trait FromValue: Sized {
|
||||
@@ -490,11 +489,10 @@ impl FromValue for Vec<Value> {
|
||||
}
|
||||
}
|
||||
|
||||
// A record
|
||||
impl FromValue for (Vec<String>, Vec<Value>) {
|
||||
impl FromValue for Record {
|
||||
fn from_value(v: &Value) -> Result<Self, ShellError> {
|
||||
match v {
|
||||
Value::Record { cols, vals, .. } => Ok((cols.clone(), vals.clone())),
|
||||
Value::Record { val, .. } => Ok(val.clone()),
|
||||
v => Err(ShellError::CantConvert {
|
||||
to_type: "Record".into(),
|
||||
from_type: v.get_type().to_string(),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
use crate::{ShellError, Span, Value};
|
||||
use crate::{Record, ShellError, Span, Value};
|
||||
use std::fmt;
|
||||
|
||||
// Trait definition for a lazy record (where columns are evaluated on-demand)
|
||||
@@ -15,20 +15,14 @@ pub trait LazyRecord<'a>: fmt::Debug + Send + Sync {
|
||||
|
||||
// Convert the lazy record into a regular Value::Record by collecting all its columns
|
||||
fn collect(&'a self) -> Result<Value, ShellError> {
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
|
||||
for column in self.column_names() {
|
||||
cols.push(column.into());
|
||||
let val = self.get_column_value(column)?;
|
||||
vals.push(val);
|
||||
}
|
||||
|
||||
Ok(Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
span: self.span(),
|
||||
})
|
||||
self.column_names()
|
||||
.into_iter()
|
||||
.map(|col| {
|
||||
let val = self.get_column_value(col)?;
|
||||
Ok((col.to_owned(), val))
|
||||
})
|
||||
.collect::<Result<Record, _>>()
|
||||
.map(|record| Value::record(record, self.span()))
|
||||
}
|
||||
|
||||
fn clone_value(&self, span: Span) -> Value;
|
||||
|
@@ -3,6 +3,7 @@ mod from;
|
||||
mod from_value;
|
||||
mod lazy_record;
|
||||
mod range;
|
||||
mod record;
|
||||
mod stream;
|
||||
mod unit;
|
||||
|
||||
@@ -18,19 +19,18 @@ use chrono_humanize::HumanTime;
|
||||
pub use custom_value::CustomValue;
|
||||
use fancy_regex::Regex;
|
||||
pub use from_value::FromValue;
|
||||
use indexmap::map::IndexMap;
|
||||
pub use lazy_record::LazyRecord;
|
||||
use nu_utils::get_system_locale;
|
||||
use nu_utils::locale::get_system_locale_string;
|
||||
use num_format::ToFormattedString;
|
||||
pub use range::*;
|
||||
pub use record::Record;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Write;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
iter,
|
||||
path::PathBuf,
|
||||
{cmp::Ordering, fmt::Debug},
|
||||
};
|
||||
@@ -75,8 +75,7 @@ pub enum Value {
|
||||
span: Span,
|
||||
},
|
||||
Record {
|
||||
cols: Vec<String>,
|
||||
vals: Vec<Value>,
|
||||
val: Record,
|
||||
span: Span,
|
||||
},
|
||||
List {
|
||||
@@ -148,9 +147,8 @@ impl Clone for Value {
|
||||
val: val.clone(),
|
||||
span: *span,
|
||||
},
|
||||
Value::Record { cols, vals, span } => Value::Record {
|
||||
cols: cols.clone(),
|
||||
vals: vals.clone(),
|
||||
Value::Record { val, span } => Value::Record {
|
||||
val: val.clone(),
|
||||
span: *span,
|
||||
},
|
||||
Value::LazyRecord { val, span } => val.clone_value(*span),
|
||||
@@ -367,9 +365,9 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_record(&self) -> Result<(&[String], &[Value]), ShellError> {
|
||||
pub fn as_record(&self) -> Result<&Record, ShellError> {
|
||||
match self {
|
||||
Value::Record { cols, vals, .. } => Ok((cols, vals)),
|
||||
Value::Record { val, .. } => Ok(val),
|
||||
x => Err(ShellError::CantConvert {
|
||||
to_type: "record".into(),
|
||||
from_type: x.get_type().to_string(),
|
||||
@@ -547,12 +545,9 @@ impl Value {
|
||||
Value::Date { .. } => Type::Date,
|
||||
Value::Range { .. } => Type::Range,
|
||||
Value::String { .. } => Type::String,
|
||||
Value::Record { cols, vals, .. } => Type::Record(
|
||||
cols.iter()
|
||||
.zip(vals.iter())
|
||||
.map(|(x, y)| (x.clone(), y.get_type()))
|
||||
.collect(),
|
||||
),
|
||||
Value::Record { val, .. } => {
|
||||
Type::Record(val.iter().map(|(x, y)| (x.clone(), y.get_type())).collect())
|
||||
}
|
||||
Value::List { vals, .. } => {
|
||||
let mut ty = None;
|
||||
for val in vals {
|
||||
@@ -595,9 +590,8 @@ impl Value {
|
||||
|
||||
pub fn get_data_by_key(&self, name: &str) -> Option<Value> {
|
||||
match self {
|
||||
Value::Record { cols, vals, .. } => cols
|
||||
Value::Record { val, .. } => val
|
||||
.iter()
|
||||
.zip(vals.iter())
|
||||
.find(|(col, _)| col == &name)
|
||||
.map(|(_, val)| val.clone()),
|
||||
Value::List { vals, span } => {
|
||||
@@ -676,10 +670,9 @@ impl Value {
|
||||
.collect::<Vec<_>>()
|
||||
.join(separator)
|
||||
),
|
||||
Value::Record { cols, vals, .. } => format!(
|
||||
Value::Record { val, .. } => format!(
|
||||
"{{{}}}",
|
||||
cols.iter()
|
||||
.zip(vals.iter())
|
||||
val.iter()
|
||||
.map(|(x, y)| format!("{}: {}", x, y.into_string(", ", config)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(separator)
|
||||
@@ -739,10 +732,10 @@ impl Value {
|
||||
)
|
||||
}
|
||||
}
|
||||
Value::Record { cols, .. } => format!(
|
||||
Value::Record { val, .. } => format!(
|
||||
"{{record {} field{}}}",
|
||||
cols.len(),
|
||||
if cols.len() == 1 { "" } else { "s" }
|
||||
val.len(),
|
||||
if val.len() == 1 { "" } else { "s" }
|
||||
),
|
||||
Value::LazyRecord { val, .. } => match val.collect() {
|
||||
Ok(val) => val.into_abbreviated_string(config),
|
||||
@@ -800,10 +793,9 @@ impl Value {
|
||||
.collect::<Vec<_>>()
|
||||
.join(separator)
|
||||
),
|
||||
Value::Record { cols, vals, .. } => format!(
|
||||
Value::Record { val, .. } => format!(
|
||||
"{{{}}}",
|
||||
cols.iter()
|
||||
.zip(vals.iter())
|
||||
val.iter()
|
||||
.map(|(x, y)| format!("{}: {}", x, y.into_string_parsable(", ", config)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(separator)
|
||||
@@ -838,10 +830,9 @@ impl Value {
|
||||
.collect::<Vec<_>>()
|
||||
.join(separator)
|
||||
),
|
||||
Value::Record { cols, vals, .. } => format!(
|
||||
Value::Record { val, .. } => format!(
|
||||
"{{{}}}",
|
||||
cols.iter()
|
||||
.zip(vals.iter())
|
||||
val.iter()
|
||||
.map(|(x, y)| format!("{}: {}", x, y.into_string(", ", config)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(separator)
|
||||
@@ -866,7 +857,7 @@ impl Value {
|
||||
match self {
|
||||
Value::String { val, .. } => val.is_empty(),
|
||||
Value::List { vals, .. } => vals.is_empty(),
|
||||
Value::Record { cols, .. } => cols.is_empty(),
|
||||
Value::Record { val, .. } => val.is_empty(),
|
||||
Value::Binary { val, .. } => val.is_empty(),
|
||||
Value::Nothing { .. } => true,
|
||||
_ => false,
|
||||
@@ -981,12 +972,11 @@ impl Value {
|
||||
span: origin_span,
|
||||
optional,
|
||||
} => match &mut current {
|
||||
Value::Record { cols, vals, span } => {
|
||||
let cols = cols.clone();
|
||||
Value::Record { val, span } => {
|
||||
let span = *span;
|
||||
|
||||
// Make reverse iterate to avoid duplicate column leads to first value, actually last value is expected.
|
||||
if let Some(found) = cols.iter().zip(vals.iter()).rev().find(|x| {
|
||||
if let Some(found) = val.iter().rev().find(|x| {
|
||||
if insensitive {
|
||||
x.0.to_lowercase() == column_name.to_lowercase()
|
||||
} else {
|
||||
@@ -998,7 +988,7 @@ impl Value {
|
||||
return Ok(Value::nothing(*origin_span)); // short-circuit
|
||||
} else {
|
||||
if from_user_input {
|
||||
if let Some(suggestion) = did_you_mean(&cols, column_name) {
|
||||
if let Some(suggestion) = did_you_mean(&val.cols, column_name) {
|
||||
return Err(ShellError::DidYouMean(suggestion, *origin_span));
|
||||
}
|
||||
}
|
||||
@@ -1126,12 +1116,12 @@ impl Value {
|
||||
Value::List { vals, .. } => {
|
||||
for val in vals.iter_mut() {
|
||||
match val {
|
||||
Value::Record { cols, vals, .. } => {
|
||||
Value::Record { val: record, .. } => {
|
||||
let mut found = false;
|
||||
for col in cols.iter().zip(vals.iter_mut()) {
|
||||
if col.0 == col_name {
|
||||
for (col, val) in record.iter_mut() {
|
||||
if col == col_name {
|
||||
found = true;
|
||||
col.1.upsert_data_at_cell_path(
|
||||
val.upsert_data_at_cell_path(
|
||||
&cell_path[1..],
|
||||
new_val.clone(),
|
||||
)?
|
||||
@@ -1139,15 +1129,11 @@ impl Value {
|
||||
}
|
||||
if !found {
|
||||
if cell_path.len() == 1 {
|
||||
cols.push(col_name.clone());
|
||||
vals.push(new_val);
|
||||
record.push(col_name, new_val);
|
||||
break;
|
||||
} else {
|
||||
let mut new_col = Value::Record {
|
||||
cols: vec![],
|
||||
vals: vec![],
|
||||
span: new_val.span()?,
|
||||
};
|
||||
let mut new_col =
|
||||
Value::record(Record::new(), new_val.span()?);
|
||||
new_col.upsert_data_at_cell_path(
|
||||
&cell_path[1..],
|
||||
new_val,
|
||||
@@ -1168,30 +1154,25 @@ impl Value {
|
||||
}
|
||||
}
|
||||
}
|
||||
Value::Record { cols, vals, .. } => {
|
||||
Value::Record { val: record, .. } => {
|
||||
let mut found = false;
|
||||
|
||||
for col in cols.iter().zip(vals.iter_mut()) {
|
||||
if col.0 == col_name {
|
||||
for (col, val) in record.iter_mut() {
|
||||
if col == col_name {
|
||||
found = true;
|
||||
|
||||
col.1
|
||||
.upsert_data_at_cell_path(&cell_path[1..], new_val.clone())?
|
||||
val.upsert_data_at_cell_path(&cell_path[1..], new_val.clone())?
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cols.push(col_name.clone());
|
||||
if cell_path.len() == 1 {
|
||||
vals.push(new_val);
|
||||
let new_col = if cell_path.len() == 1 {
|
||||
new_val
|
||||
} else {
|
||||
let mut new_col = Value::Record {
|
||||
cols: vec![],
|
||||
vals: vec![],
|
||||
span: new_val.span()?,
|
||||
};
|
||||
let mut new_col = Value::record(Record::new(), new_val.span()?);
|
||||
new_col.upsert_data_at_cell_path(&cell_path[1..], new_val)?;
|
||||
vals.push(new_col);
|
||||
}
|
||||
new_col
|
||||
};
|
||||
|
||||
record.push(col_name, new_col);
|
||||
}
|
||||
}
|
||||
Value::LazyRecord { val, .. } => {
|
||||
@@ -1275,15 +1256,14 @@ impl Value {
|
||||
for val in vals.iter_mut() {
|
||||
match val {
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
val: record,
|
||||
span: v_span,
|
||||
} => {
|
||||
let mut found = false;
|
||||
for col in cols.iter().zip(vals.iter_mut()) {
|
||||
if col.0 == col_name {
|
||||
for (col, val) in record.iter_mut() {
|
||||
if col == col_name {
|
||||
found = true;
|
||||
col.1.update_data_at_cell_path(
|
||||
val.update_data_at_cell_path(
|
||||
&cell_path[1..],
|
||||
new_val.clone(),
|
||||
)?
|
||||
@@ -1309,18 +1289,15 @@ impl Value {
|
||||
}
|
||||
}
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
val: record,
|
||||
span: v_span,
|
||||
} => {
|
||||
let mut found = false;
|
||||
|
||||
for col in cols.iter().zip(vals.iter_mut()) {
|
||||
if col.0 == col_name {
|
||||
for (col, val) in record.iter_mut() {
|
||||
if col == col_name {
|
||||
found = true;
|
||||
|
||||
col.1
|
||||
.update_data_at_cell_path(&cell_path[1..], new_val.clone())?
|
||||
val.update_data_at_cell_path(&cell_path[1..], new_val.clone())?
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
@@ -1392,16 +1369,15 @@ impl Value {
|
||||
for val in vals.iter_mut() {
|
||||
match val {
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
val: record,
|
||||
span: v_span,
|
||||
} => {
|
||||
let mut found = false;
|
||||
let mut index = 0;
|
||||
cols.retain_mut(|col| {
|
||||
record.cols.retain_mut(|col| {
|
||||
if col == col_name {
|
||||
found = true;
|
||||
vals.remove(index);
|
||||
record.vals.remove(index);
|
||||
false
|
||||
} else {
|
||||
index += 1;
|
||||
@@ -1428,18 +1404,21 @@ impl Value {
|
||||
Ok(())
|
||||
}
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
val: record,
|
||||
span: v_span,
|
||||
} => {
|
||||
let mut found = false;
|
||||
for (i, col) in cols.clone().iter().enumerate() {
|
||||
let mut index = 0;
|
||||
record.cols.retain_mut(|col| {
|
||||
if col == col_name {
|
||||
cols.remove(i);
|
||||
vals.remove(i);
|
||||
found = true;
|
||||
record.vals.remove(index);
|
||||
false
|
||||
} else {
|
||||
index += 1;
|
||||
true
|
||||
}
|
||||
}
|
||||
});
|
||||
if !found && !optional {
|
||||
return Err(ShellError::CantFindColumn {
|
||||
col_name: col_name.to_string(),
|
||||
@@ -1501,15 +1480,14 @@ impl Value {
|
||||
for val in vals.iter_mut() {
|
||||
match val {
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
val: record,
|
||||
span: v_span,
|
||||
} => {
|
||||
let mut found = false;
|
||||
for col in cols.iter().zip(vals.iter_mut()) {
|
||||
if col.0 == col_name {
|
||||
for (col, val) in record.iter_mut() {
|
||||
if col == col_name {
|
||||
found = true;
|
||||
col.1.remove_data_at_cell_path(&cell_path[1..])?
|
||||
val.remove_data_at_cell_path(&cell_path[1..])?
|
||||
}
|
||||
}
|
||||
if !found && !optional {
|
||||
@@ -1532,17 +1510,15 @@ impl Value {
|
||||
Ok(())
|
||||
}
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
val: record,
|
||||
span: v_span,
|
||||
} => {
|
||||
let mut found = false;
|
||||
|
||||
for col in cols.iter().zip(vals.iter_mut()) {
|
||||
if col.0 == col_name {
|
||||
for (col, val) in record.iter_mut() {
|
||||
if col == col_name {
|
||||
found = true;
|
||||
|
||||
col.1.remove_data_at_cell_path(&cell_path[1..])?
|
||||
val.remove_data_at_cell_path(&cell_path[1..])?
|
||||
}
|
||||
}
|
||||
if !found && !optional {
|
||||
@@ -1613,12 +1589,11 @@ impl Value {
|
||||
for val in vals.iter_mut() {
|
||||
match val {
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
val: record,
|
||||
span: v_span,
|
||||
} => {
|
||||
for col in cols.iter().zip(vals.iter_mut()) {
|
||||
if col.0 == col_name {
|
||||
for (col, val) in record.iter_mut() {
|
||||
if col == col_name {
|
||||
if cell_path.len() == 1 {
|
||||
return Err(ShellError::ColumnAlreadyExists {
|
||||
col_name: col_name.to_string(),
|
||||
@@ -1626,7 +1601,7 @@ impl Value {
|
||||
src_span: *v_span,
|
||||
});
|
||||
} else {
|
||||
return col.1.insert_data_at_cell_path(
|
||||
return val.insert_data_at_cell_path(
|
||||
&cell_path[1..],
|
||||
new_val,
|
||||
head_span,
|
||||
@@ -1635,8 +1610,7 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
cols.push(col_name.clone());
|
||||
vals.push(new_val.clone());
|
||||
record.push(col_name, new_val.clone());
|
||||
}
|
||||
// SIGH...
|
||||
Value::Error { error } => return Err(*error.clone()),
|
||||
@@ -1652,12 +1626,11 @@ impl Value {
|
||||
}
|
||||
}
|
||||
Value::Record {
|
||||
cols,
|
||||
vals,
|
||||
val: record,
|
||||
span: v_span,
|
||||
} => {
|
||||
for col in cols.iter().zip(vals.iter_mut()) {
|
||||
if col.0 == col_name {
|
||||
for (col, val) in record.iter_mut() {
|
||||
if col == col_name {
|
||||
if cell_path.len() == 1 {
|
||||
return Err(ShellError::ColumnAlreadyExists {
|
||||
col_name: col_name.to_string(),
|
||||
@@ -1665,7 +1638,7 @@ impl Value {
|
||||
src_span: *v_span,
|
||||
});
|
||||
} else {
|
||||
return col.1.insert_data_at_cell_path(
|
||||
return val.insert_data_at_cell_path(
|
||||
&cell_path[1..],
|
||||
new_val,
|
||||
head_span,
|
||||
@@ -1674,8 +1647,7 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
cols.push(col_name.clone());
|
||||
vals.push(new_val);
|
||||
record.push(col_name, new_val);
|
||||
}
|
||||
Value::LazyRecord { val, span } => {
|
||||
// convert to Record first.
|
||||
@@ -1734,7 +1706,7 @@ impl Value {
|
||||
|
||||
pub fn columns(&self) -> &[String] {
|
||||
match self {
|
||||
Value::Record { cols, .. } => cols,
|
||||
Value::Record { val, .. } => &val.cols,
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
@@ -1777,18 +1749,8 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn record(cols: Vec<String>, vals: Vec<Value>, span: Span) -> Value {
|
||||
Value::Record { cols, vals, span }
|
||||
}
|
||||
|
||||
pub fn record_from_hashmap(map: &HashMap<String, Value>, span: Span) -> Value {
|
||||
let mut cols = vec![];
|
||||
let mut vals = vec![];
|
||||
for (key, val) in map.iter() {
|
||||
cols.push(key.clone());
|
||||
vals.push(val.clone());
|
||||
}
|
||||
Value::record(cols, vals, span)
|
||||
pub fn record(val: Record, span: Span) -> Value {
|
||||
Value::Record { val, span }
|
||||
}
|
||||
|
||||
pub fn list(vals: Vec<Value>, span: Span) -> Value {
|
||||
@@ -1894,12 +1856,8 @@ impl Value {
|
||||
|
||||
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
||||
/// when used in errors.
|
||||
pub fn test_record(cols: Vec<impl Into<String>>, vals: Vec<Value>) -> Value {
|
||||
Value::record(
|
||||
cols.into_iter().map(|s| s.into()).collect(),
|
||||
vals,
|
||||
Span::test_data(),
|
||||
)
|
||||
pub fn test_record(val: Record) -> Value {
|
||||
Value::record(val, Span::test_data())
|
||||
}
|
||||
|
||||
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
||||
@@ -2148,14 +2106,7 @@ impl PartialOrd for Value {
|
||||
Value::CustomValue { .. } => Some(Ordering::Less),
|
||||
Value::MatchPattern { .. } => Some(Ordering::Less),
|
||||
},
|
||||
(
|
||||
Value::Record {
|
||||
cols: lhs_cols,
|
||||
vals: lhs_vals,
|
||||
..
|
||||
},
|
||||
rhs,
|
||||
) => match rhs {
|
||||
(Value::Record { val: lhs, .. }, rhs) => match rhs {
|
||||
Value::Bool { .. } => Some(Ordering::Greater),
|
||||
Value::Int { .. } => Some(Ordering::Greater),
|
||||
Value::Float { .. } => Some(Ordering::Greater),
|
||||
@@ -2164,18 +2115,12 @@ impl PartialOrd for Value {
|
||||
Value::Date { .. } => Some(Ordering::Greater),
|
||||
Value::Range { .. } => Some(Ordering::Greater),
|
||||
Value::String { .. } => Some(Ordering::Greater),
|
||||
Value::Record {
|
||||
cols: rhs_cols,
|
||||
vals: rhs_vals,
|
||||
..
|
||||
} => {
|
||||
Value::Record { val: rhs, .. } => {
|
||||
// reorder cols and vals to make more logically compare.
|
||||
// more general, if two record have same col and values,
|
||||
// the order of cols shouldn't affect the equal property.
|
||||
let (lhs_cols_ordered, lhs_vals_ordered) =
|
||||
reorder_record_inner(lhs_cols, lhs_vals);
|
||||
let (rhs_cols_ordered, rhs_vals_ordered) =
|
||||
reorder_record_inner(rhs_cols, rhs_vals);
|
||||
let (lhs_cols_ordered, lhs_vals_ordered) = reorder_record_inner(lhs);
|
||||
let (rhs_cols_ordered, rhs_vals_ordered) = reorder_record_inner(rhs);
|
||||
|
||||
let result = lhs_cols_ordered.partial_cmp(&rhs_cols_ordered);
|
||||
if result == Some(Ordering::Equal) {
|
||||
@@ -3190,8 +3135,8 @@ impl Value {
|
||||
val: rhs.contains(lhs),
|
||||
span,
|
||||
}),
|
||||
(Value::String { val: lhs, .. }, Value::Record { cols: rhs, .. }) => Ok(Value::Bool {
|
||||
val: rhs.contains(lhs),
|
||||
(Value::String { val: lhs, .. }, Value::Record { val: rhs, .. }) => Ok(Value::Bool {
|
||||
val: rhs.cols.contains(lhs),
|
||||
span,
|
||||
}),
|
||||
(Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => {
|
||||
@@ -3247,8 +3192,8 @@ impl Value {
|
||||
val: !rhs.contains(lhs),
|
||||
span,
|
||||
}),
|
||||
(Value::String { val: lhs, .. }, Value::Record { cols: rhs, .. }) => Ok(Value::Bool {
|
||||
val: !rhs.contains(lhs),
|
||||
(Value::String { val: lhs, .. }, Value::Record { val: rhs, .. }) => Ok(Value::Bool {
|
||||
val: !rhs.cols.contains(lhs),
|
||||
span,
|
||||
}),
|
||||
(Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => {
|
||||
@@ -3648,36 +3593,10 @@ impl Value {
|
||||
}
|
||||
}
|
||||
|
||||
fn reorder_record_inner(cols: &[String], vals: &[Value]) -> (Vec<String>, Vec<Value>) {
|
||||
let mut kv_pairs =
|
||||
iter::zip(cols.to_owned(), vals.to_owned()).collect::<Vec<(String, Value)>>();
|
||||
kv_pairs.sort_by(|a, b| {
|
||||
a.0.partial_cmp(&b.0)
|
||||
.expect("Columns should support compare")
|
||||
});
|
||||
let (mut cols, mut vals) = (vec![], vec![]);
|
||||
for (col, val) in kv_pairs {
|
||||
cols.push(col);
|
||||
vals.push(val);
|
||||
}
|
||||
(cols, vals)
|
||||
}
|
||||
|
||||
/// Create a Value::Record from a spanned hashmap
|
||||
impl From<Spanned<HashMap<String, Value>>> for Value {
|
||||
fn from(input: Spanned<HashMap<String, Value>>) -> Self {
|
||||
let span = input.span;
|
||||
let (cols, vals) = input
|
||||
.item
|
||||
.into_iter()
|
||||
.fold((vec![], vec![]), |mut acc, (k, v)| {
|
||||
acc.0.push(k);
|
||||
acc.1.push(v);
|
||||
acc
|
||||
});
|
||||
|
||||
Value::Record { cols, vals, span }
|
||||
}
|
||||
fn reorder_record_inner(record: &Record) -> (Vec<&String>, Vec<&Value>) {
|
||||
let mut kv_pairs = record.iter().collect::<Vec<_>>();
|
||||
kv_pairs.sort_by_key(|(col, _)| *col);
|
||||
kv_pairs.into_iter().unzip()
|
||||
}
|
||||
|
||||
fn type_compatible(a: Type, b: Type) -> bool {
|
||||
@@ -3688,23 +3607,6 @@ fn type_compatible(a: Type, b: Type) -> bool {
|
||||
matches!((a, b), (Type::Int, Type::Float) | (Type::Float, Type::Int))
|
||||
}
|
||||
|
||||
/// Create a Value::Record from a spanned indexmap
|
||||
impl From<Spanned<IndexMap<String, Value>>> for Value {
|
||||
fn from(input: Spanned<IndexMap<String, Value>>) -> Self {
|
||||
let span = input.span;
|
||||
let (cols, vals) = input
|
||||
.item
|
||||
.into_iter()
|
||||
.fold((vec![], vec![]), |mut acc, (k, v)| {
|
||||
acc.0.push(k);
|
||||
acc.1.push(v);
|
||||
acc
|
||||
});
|
||||
|
||||
Value::Record { cols, vals, span }
|
||||
}
|
||||
}
|
||||
|
||||
/// Is the given year a leap year?
|
||||
#[allow(clippy::nonminimal_bool)]
|
||||
pub fn is_leap_year(year: i32) -> bool {
|
||||
@@ -3985,7 +3887,7 @@ fn get_filesize_format(format_value: &str, filesize_metric: Option<bool>) -> (By
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use super::{Span, Value};
|
||||
use super::{Record, Span, Value};
|
||||
|
||||
mod is_empty {
|
||||
use super::*;
|
||||
@@ -4013,26 +3915,24 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_record() {
|
||||
let no_columns_nor_cell_values = Value::Record {
|
||||
cols: vec![],
|
||||
vals: vec![],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let one_column_and_one_cell_value_with_empty_strings = Value::Record {
|
||||
let no_columns_nor_cell_values = Value::test_record(Record::new());
|
||||
|
||||
let one_column_and_one_cell_value_with_empty_strings = Value::test_record(Record {
|
||||
cols: vec![String::from("")],
|
||||
vals: vec![Value::string("", Span::unknown())],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let one_column_with_a_string_and_one_cell_value_with_empty_string = Value::Record {
|
||||
cols: vec![String::from("column")],
|
||||
vals: vec![Value::string("", Span::unknown())],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
let one_column_with_empty_string_and_one_value_with_a_string = Value::Record {
|
||||
cols: vec![String::from("")],
|
||||
vals: vec![Value::string("text", Span::unknown())],
|
||||
span: Span::unknown(),
|
||||
};
|
||||
});
|
||||
|
||||
let one_column_with_a_string_and_one_cell_value_with_empty_string =
|
||||
Value::test_record(Record {
|
||||
cols: vec![String::from("column")],
|
||||
vals: vec![Value::string("", Span::unknown())],
|
||||
});
|
||||
|
||||
let one_column_with_empty_string_and_one_value_with_a_string =
|
||||
Value::test_record(Record {
|
||||
cols: vec![String::from("")],
|
||||
vals: vec![Value::string("text", Span::unknown())],
|
||||
});
|
||||
|
||||
assert!(no_columns_nor_cell_values.is_empty());
|
||||
assert!(!one_column_and_one_cell_value_with_empty_strings.is_empty());
|
||||
|
99
crates/nu-protocol/src/value/record.rs
Normal file
99
crates/nu-protocol/src/value/record.rs
Normal file
@@ -0,0 +1,99 @@
|
||||
use crate::Value;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct Record {
|
||||
pub cols: Vec<String>,
|
||||
pub vals: Vec<Value>,
|
||||
}
|
||||
|
||||
impl Record {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub fn with_capacity(capacity: usize) -> Self {
|
||||
Self {
|
||||
cols: Vec::with_capacity(capacity),
|
||||
vals: Vec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> Iter {
|
||||
self.into_iter()
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> IterMut {
|
||||
self.into_iter()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.cols.is_empty() || self.vals.is_empty()
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
usize::min(self.cols.len(), self.vals.len())
|
||||
}
|
||||
|
||||
pub fn push(&mut self, col: impl Into<String>, val: Value) {
|
||||
self.cols.push(col.into());
|
||||
self.vals.push(val);
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(String, Value)> for Record {
|
||||
fn from_iter<T: IntoIterator<Item = (String, Value)>>(iter: T) -> Self {
|
||||
let (cols, vals) = iter.into_iter().unzip();
|
||||
Self { cols, vals }
|
||||
}
|
||||
}
|
||||
|
||||
pub type IntoIter = std::iter::Zip<std::vec::IntoIter<String>, std::vec::IntoIter<Value>>;
|
||||
|
||||
impl IntoIterator for Record {
|
||||
type Item = (String, Value);
|
||||
|
||||
type IntoIter = IntoIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.cols.into_iter().zip(self.vals)
|
||||
}
|
||||
}
|
||||
|
||||
pub type Iter<'a> = std::iter::Zip<std::slice::Iter<'a, String>, std::slice::Iter<'a, Value>>;
|
||||
|
||||
impl<'a> IntoIterator for &'a Record {
|
||||
type Item = (&'a String, &'a Value);
|
||||
|
||||
type IntoIter = Iter<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.cols.iter().zip(&self.vals)
|
||||
}
|
||||
}
|
||||
|
||||
pub type IterMut<'a> = std::iter::Zip<std::slice::Iter<'a, String>, std::slice::IterMut<'a, Value>>;
|
||||
|
||||
impl<'a> IntoIterator for &'a mut Record {
|
||||
type Item = (&'a String, &'a mut Value);
|
||||
|
||||
type IntoIter = IterMut<'a>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.cols.iter().zip(&mut self.vals)
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! record {
|
||||
{$($col:expr => $val:expr),+ $(,)?} => {
|
||||
$crate::Record {
|
||||
cols: vec![$($col.into(),)+],
|
||||
vals: vec![$($val,)+]
|
||||
}
|
||||
};
|
||||
{} => {
|
||||
$crate::Record::new()
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user