mod custom_value;
mod duration;
mod filesize;
mod from_value;
mod glob;
mod into_value;
mod range;
#[cfg(test)]
mod test_derive;
pub mod record;
pub use custom_value::CustomValue;
pub use duration::*;
pub use filesize::*;
pub use from_value::FromValue;
pub use glob::*;
pub use into_value::{IntoValue, TryIntoValue};
pub use nu_utils::MultiLife;
pub use range::{FloatRange, IntRange, Range};
pub use record::Record;
use crate::{
BlockId, Config, ShellError, Signals, Span, Type,
ast::{Bits, Boolean, CellPath, Comparison, Math, Operator, PathMember},
did_you_mean,
engine::{Closure, EngineState},
};
use chrono::{DateTime, Datelike, Duration, FixedOffset, Local, Locale, TimeZone};
use chrono_humanize::HumanTime;
use fancy_regex::Regex;
use nu_utils::{
ObviousFloat, SharedCow, contains_emoji,
locale::{LOCALE_OVERRIDE_ENV_VAR, get_system_locale_string},
};
use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
cmp::Ordering,
fmt::{Debug, Display, Write},
ops::{Bound, ControlFlow},
path::PathBuf,
};
#[derive(Debug, Serialize, Deserialize)]
pub enum Value {
#[non_exhaustive]
Bool {
val: bool,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Int {
val: i64,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Float {
val: f64,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
String {
val: String,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Glob {
val: String,
no_expand: bool,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Filesize {
val: Filesize,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Duration {
val: i64,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Date {
val: DateTime<FixedOffset>,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Range {
val: Box<Range>,
#[serde(skip)]
signals: Option<Signals>,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Record {
val: SharedCow<Record>,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
List {
vals: Vec<Value>,
#[serde(skip)]
signals: Option<Signals>,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Closure {
val: Box<Closure>,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Error {
error: Box<ShellError>,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Binary {
val: Vec<u8>,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
CellPath {
val: CellPath,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Custom {
val: Box<dyn CustomValue>,
#[serde(rename = "span")]
internal_span: Span,
},
#[non_exhaustive]
Nothing {
#[serde(rename = "span")]
internal_span: Span,
},
}
const _: () = assert!(std::mem::size_of::<Value>() <= 56);
impl Clone for Value {
fn clone(&self) -> Self {
match self {
Value::Bool { val, internal_span } => Value::bool(*val, *internal_span),
Value::Int { val, internal_span } => Value::int(*val, *internal_span),
Value::Filesize { val, internal_span } => Value::Filesize {
val: *val,
internal_span: *internal_span,
},
Value::Duration { val, internal_span } => Value::Duration {
val: *val,
internal_span: *internal_span,
},
Value::Date { val, internal_span } => Value::Date {
val: *val,
internal_span: *internal_span,
},
Value::Range {
val,
signals,
internal_span,
} => Value::Range {
val: val.clone(),
signals: signals.clone(),
internal_span: *internal_span,
},
Value::Float { val, internal_span } => Value::float(*val, *internal_span),
Value::String { val, internal_span } => Value::String {
val: val.clone(),
internal_span: *internal_span,
},
Value::Glob {
val,
no_expand: quoted,
internal_span,
} => Value::Glob {
val: val.clone(),
no_expand: *quoted,
internal_span: *internal_span,
},
Value::Record { val, internal_span } => Value::Record {
val: val.clone(),
internal_span: *internal_span,
},
Value::List {
vals,
signals,
internal_span,
} => Value::List {
vals: vals.clone(),
signals: signals.clone(),
internal_span: *internal_span,
},
Value::Closure { val, internal_span } => Value::Closure {
val: val.clone(),
internal_span: *internal_span,
},
Value::Nothing { internal_span } => Value::Nothing {
internal_span: *internal_span,
},
Value::Error {
error,
internal_span,
} => Value::Error {
error: error.clone(),
internal_span: *internal_span,
},
Value::Binary { val, internal_span } => Value::Binary {
val: val.clone(),
internal_span: *internal_span,
},
Value::CellPath { val, internal_span } => Value::CellPath {
val: val.clone(),
internal_span: *internal_span,
},
Value::Custom { val, internal_span } => val.clone_value(*internal_span),
}
}
}
enum CellPathMutation {
Upsert,
Update,
Insert { head_span: Span },
Remove { member: PathMember },
}
impl Value {
fn cant_convert_to<T>(&self, typ: &str) -> Result<T, ShellError> {
Err(ShellError::CantConvert {
to_type: typ.into(),
from_type: self.get_type().to_string(),
span: self.span(),
help: None,
})
}
pub fn as_bool(&self) -> Result<bool, ShellError> {
if let Value::Bool { val, .. } = self {
Ok(*val)
} else {
self.cant_convert_to("boolean")
}
}
pub fn as_int(&self) -> Result<i64, ShellError> {
if let Value::Int { val, .. } = self {
Ok(*val)
} else {
self.cant_convert_to("int")
}
}
pub fn as_float(&self) -> Result<f64, ShellError> {
if let Value::Float { val, .. } = self {
Ok(*val)
} else {
self.cant_convert_to("float")
}
}
pub fn coerce_float(&self) -> Result<f64, ShellError> {
match self {
Value::Float { val, .. } => Ok(*val),
Value::Int { val, .. } => Ok(*val as f64),
val => val.cant_convert_to("float"),
}
}
pub fn as_filesize(&self) -> Result<Filesize, ShellError> {
if let Value::Filesize { val, .. } = self {
Ok(*val)
} else {
self.cant_convert_to("filesize")
}
}
pub fn as_duration(&self) -> Result<i64, ShellError> {
if let Value::Duration { val, .. } = self {
Ok(*val)
} else {
self.cant_convert_to("duration")
}
}
pub fn as_date(&self) -> Result<DateTime<FixedOffset>, ShellError> {
if let Value::Date { val, .. } = self {
Ok(*val)
} else {
self.cant_convert_to("datetime")
}
}
pub fn as_range(&self) -> Result<Range, ShellError> {
if let Value::Range { val, .. } = self {
Ok(**val)
} else {
self.cant_convert_to("range")
}
}
pub fn into_range(self) -> Result<Range, ShellError> {
if let Value::Range { val, .. } = self {
Ok(*val)
} else {
self.cant_convert_to("range")
}
}
pub fn as_str(&self) -> Result<&str, ShellError> {
if let Value::String { val, .. } = self {
Ok(val)
} else {
self.cant_convert_to("string")
}
}
pub fn into_string(self) -> Result<String, ShellError> {
if let Value::String { val, .. } = self {
Ok(val)
} else {
self.cant_convert_to("string")
}
}
pub fn coerce_str(&self) -> Result<Cow<'_, str>, ShellError> {
match self {
Value::Bool { val, .. } => Ok(Cow::Owned(val.to_string())),
Value::Int { val, .. } => Ok(Cow::Owned(val.to_string())),
Value::Float { val, .. } => Ok(Cow::Owned(val.to_string())),
Value::String { val, .. } => Ok(Cow::Borrowed(val)),
Value::Glob { val, .. } => Ok(Cow::Borrowed(val)),
Value::Binary { val, .. } => match std::str::from_utf8(val) {
Ok(s) => Ok(Cow::Borrowed(s)),
Err(_) => self.cant_convert_to("string"),
},
Value::Date { val, .. } => Ok(Cow::Owned(
val.to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
)),
val => val.cant_convert_to("string"),
}
}
pub fn coerce_string(&self) -> Result<String, ShellError> {
self.coerce_str().map(Cow::into_owned)
}
pub fn coerce_into_string(self) -> Result<String, ShellError> {
let span = self.span();
match self {
Value::Bool { val, .. } => Ok(val.to_string()),
Value::Int { val, .. } => Ok(val.to_string()),
Value::Float { val, .. } => Ok(val.to_string()),
Value::String { val, .. } => Ok(val),
Value::Glob { val, .. } => Ok(val),
Value::Binary { val, .. } => match String::from_utf8(val) {
Ok(s) => Ok(s),
Err(err) => Value::binary(err.into_bytes(), span).cant_convert_to("string"),
},
Value::Date { val, .. } => Ok(val.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)),
val => val.cant_convert_to("string"),
}
}
pub fn as_char(&self) -> Result<char, ShellError> {
let span = self.span();
if let Value::String { val, .. } = self {
let mut chars = val.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => Ok(c),
_ => Err(ShellError::MissingParameter {
param_name: "single character separator".into(),
span,
}),
}
} else {
self.cant_convert_to("char")
}
}
pub fn to_path(&self) -> Result<PathBuf, ShellError> {
if let Value::String { val, .. } = self {
Ok(PathBuf::from(val))
} else {
self.cant_convert_to("path")
}
}
pub fn as_record(&self) -> Result<&Record, ShellError> {
if let Value::Record { val, .. } = self {
Ok(val)
} else {
self.cant_convert_to("record")
}
}
pub fn into_record(self) -> Result<Record, ShellError> {
if let Value::Record { val, .. } = self {
Ok(val.into_owned())
} else {
self.cant_convert_to("record")
}
}
pub fn as_list(&self) -> Result<&[Value], ShellError> {
if let Value::List { vals, .. } = self {
Ok(vals)
} else {
self.cant_convert_to("list")
}
}
pub fn into_list(self) -> Result<Vec<Value>, ShellError> {
if let Value::List { vals, .. } = self {
Ok(vals)
} else {
self.cant_convert_to("list")
}
}
pub fn as_closure(&self) -> Result<&Closure, ShellError> {
if let Value::Closure { val, .. } = self {
Ok(val)
} else {
self.cant_convert_to("closure")
}
}
pub fn into_closure(self) -> Result<Closure, ShellError> {
if let Value::Closure { val, .. } = self {
Ok(*val)
} else {
self.cant_convert_to("closure")
}
}
pub fn as_binary(&self) -> Result<&[u8], ShellError> {
if let Value::Binary { val, .. } = self {
Ok(val)
} else {
self.cant_convert_to("binary")
}
}
pub fn into_binary(self) -> Result<Vec<u8>, ShellError> {
if let Value::Binary { val, .. } = self {
Ok(val)
} else {
self.cant_convert_to("binary")
}
}
pub fn coerce_binary(&self) -> Result<&[u8], ShellError> {
match self {
Value::Binary { val, .. } => Ok(val),
Value::String { val, .. } => Ok(val.as_bytes()),
val => val.cant_convert_to("binary"),
}
}
pub fn coerce_into_binary(self) -> Result<Vec<u8>, ShellError> {
match self {
Value::Binary { val, .. } => Ok(val),
Value::String { val, .. } => Ok(val.into_bytes()),
val => val.cant_convert_to("binary"),
}
}
pub fn as_cell_path(&self) -> Result<&CellPath, ShellError> {
if let Value::CellPath { val, .. } = self {
Ok(val)
} else {
self.cant_convert_to("cell path")
}
}
pub fn into_cell_path(self) -> Result<CellPath, ShellError> {
if let Value::CellPath { val, .. } = self {
Ok(val)
} else {
self.cant_convert_to("cell path")
}
}
pub fn coerce_bool(&self) -> Result<bool, ShellError> {
match self {
Value::Bool { val: false, .. } | Value::Int { val: 0, .. } | Value::Nothing { .. } => {
Ok(false)
}
Value::Float { val, .. } if val <= &f64::EPSILON => Ok(false),
Value::String { val, .. } => match val.trim().to_ascii_lowercase().as_str() {
"" | "0" | "false" => Ok(false),
_ => Ok(true),
},
Value::Bool { .. } | Value::Int { .. } | Value::Float { .. } => Ok(true),
_ => self.cant_convert_to("bool"),
}
}
pub fn as_custom_value(&self) -> Result<&dyn CustomValue, ShellError> {
if let Value::Custom { val, .. } = self {
Ok(val.as_ref())
} else {
self.cant_convert_to("custom value")
}
}
pub fn into_custom_value(self) -> Result<Box<dyn CustomValue>, ShellError> {
if let Value::Custom { val, .. } = self {
Ok(val)
} else {
self.cant_convert_to("custom value")
}
}
pub fn span(&self) -> Span {
match self {
Value::Bool { internal_span, .. }
| Value::Int { internal_span, .. }
| Value::Float { internal_span, .. }
| Value::Filesize { internal_span, .. }
| Value::Duration { internal_span, .. }
| Value::Date { internal_span, .. }
| Value::Range { internal_span, .. }
| Value::String { internal_span, .. }
| Value::Glob { internal_span, .. }
| Value::Record { internal_span, .. }
| Value::List { internal_span, .. }
| Value::Closure { internal_span, .. }
| Value::Nothing { internal_span, .. }
| Value::Binary { internal_span, .. }
| Value::CellPath { internal_span, .. }
| Value::Custom { internal_span, .. }
| Value::Error { internal_span, .. } => *internal_span,
}
}
pub fn set_span(&mut self, new_span: Span) {
match self {
Value::Bool { internal_span, .. }
| Value::Int { internal_span, .. }
| Value::Float { internal_span, .. }
| Value::Filesize { internal_span, .. }
| Value::Duration { internal_span, .. }
| Value::Date { internal_span, .. }
| Value::Range { internal_span, .. }
| Value::String { internal_span, .. }
| Value::Glob { internal_span, .. }
| Value::Record { internal_span, .. }
| Value::List { internal_span, .. }
| Value::Closure { internal_span, .. }
| Value::Nothing { internal_span, .. }
| Value::Binary { internal_span, .. }
| Value::CellPath { internal_span, .. }
| Value::Custom { internal_span, .. } => *internal_span = new_span,
Value::Error { .. } => (),
}
}
pub fn with_span(mut self, new_span: Span) -> Value {
self.set_span(new_span);
self
}
pub fn get_type(&self) -> Type {
match self {
Value::Bool { .. } => Type::Bool,
Value::Int { .. } => Type::Int,
Value::Float { .. } => Type::Float,
Value::Filesize { .. } => Type::Filesize,
Value::Duration { .. } => Type::Duration,
Value::Date { .. } => Type::Date,
Value::Range { .. } => Type::Range,
Value::String { .. } => Type::String,
Value::Glob { .. } => Type::Glob,
Value::Record { val, .. } => {
Type::Record(val.iter().map(|(x, y)| (x.clone(), y.get_type())).collect())
}
Value::List { vals, .. } => {
let ty = Type::supertype_of(vals.iter().map(Value::get_type)).unwrap_or(Type::Any);
match ty {
Type::Record(columns) => Type::Table(columns),
ty => Type::list(ty),
}
}
Value::Nothing { .. } => Type::Nothing,
Value::Closure { .. } => Type::Closure,
Value::Error { .. } => Type::Error,
Value::Binary { .. } => Type::Binary,
Value::CellPath { .. } => Type::CellPath,
Value::Custom { val, .. } => Type::Custom(val.type_name().into()),
}
}
pub fn get_type_shallow(&self) -> Type {
match self {
Value::Bool { .. } => Type::Bool,
Value::Int { .. } => Type::Int,
Value::Float { .. } => Type::Float,
Value::Filesize { .. } => Type::Filesize,
Value::Duration { .. } => Type::Duration,
Value::Date { .. } => Type::Date,
Value::Range { .. } => Type::Range,
Value::String { .. } => Type::String,
Value::Glob { .. } => Type::Glob,
Value::Record { .. } => Type::record(),
Value::List { .. } => Type::list(Type::Any),
Value::Nothing { .. } => Type::Nothing,
Value::Closure { .. } => Type::Closure,
Value::Error { .. } => Type::Error,
Value::Binary { .. } => Type::Binary,
Value::CellPath { .. } => Type::CellPath,
Value::Custom { val, .. } => Type::Custom(val.type_name().into()),
}
}
pub fn is_subtype_of(&self, other: &Type) -> bool {
let record_compatible = |val: &Value, other: &[(String, Type)]| match val {
Value::Record { val, .. } => other
.iter()
.all(|(key, ty)| val.get(key).is_some_and(|inner| inner.is_subtype_of(ty))),
_ => false,
};
match (self, other) {
(_, Type::Any) => true,
(val, Type::OneOf(types)) => types.iter().any(|t| val.is_subtype_of(t)),
(
Value::Bool { .. }
| Value::Int { .. }
| Value::Float { .. }
| Value::String { .. }
| Value::Glob { .. }
| Value::Filesize { .. }
| Value::Duration { .. }
| Value::Date { .. }
| Value::Range { .. }
| Value::Closure { .. }
| Value::Error { .. }
| Value::Binary { .. }
| Value::CellPath { .. }
| Value::Nothing { .. },
_,
) => self.get_type().is_subtype_of(other),
(val @ Value::Record { .. }, Type::Record(inner)) => record_compatible(val, inner),
(Value::List { vals, .. }, Type::List(inner)) => {
vals.iter().all(|val| val.is_subtype_of(inner))
}
(Value::List { vals, .. }, Type::Table(inner)) => {
vals.iter().all(|val| record_compatible(val, inner))
}
(Value::Custom { val, .. }, Type::Custom(inner)) => val.type_name() == **inner,
(Value::Record { .. } | Value::List { .. } | Value::Custom { .. }, _) => false,
}
}
pub fn get_data_by_key(&self, name: &str) -> Option<Value> {
let span = self.span();
match self {
Value::Record { val, .. } => val.get(name).cloned(),
Value::List { vals, .. } => {
let out = vals
.iter()
.map(|item| {
item.as_record()
.ok()
.and_then(|val| val.get(name).cloned())
.unwrap_or(Value::nothing(span))
})
.collect::<Vec<_>>();
if !out.is_empty() {
Some(Value::list(out, span))
} else {
None
}
}
_ => None,
}
}
fn format_datetime<Tz: TimeZone>(&self, date_time: &DateTime<Tz>, formatter: &str) -> String
where
Tz::Offset: Display,
{
let mut formatter_buf = String::new();
let locale = if let Ok(l) =
std::env::var(LOCALE_OVERRIDE_ENV_VAR).or_else(|_| std::env::var("LC_TIME"))
{
let locale_str = l.split('.').next().unwrap_or("en_US");
locale_str.try_into().unwrap_or(Locale::en_US)
} else {
get_system_locale_string()
.map(|l| l.replace('-', "_")) .unwrap_or_else(|| String::from("en_US"))
.as_str()
.try_into()
.unwrap_or(Locale::en_US)
};
let format = date_time.format_localized(formatter, locale);
match formatter_buf.write_fmt(format_args!("{format}")) {
Ok(_) => (),
Err(_) => formatter_buf = format!("Invalid format string {formatter}"),
}
formatter_buf
}
pub fn to_expanded_string(&self, separator: &str, config: &Config) -> String {
let span = self.span();
match self {
Value::Bool { val, .. } => val.to_string(),
Value::Int { val, .. } => val.to_string(),
Value::Float { val, .. } => ObviousFloat(*val).to_string(),
Value::Filesize { val, .. } => config.filesize.format(*val).to_string(),
Value::Duration { val, .. } => format_duration(*val),
Value::Date { val, .. } => match &config.datetime_format.normal {
Some(format) => self.format_datetime(val, format),
None => {
format!(
"{} ({})",
if val.year() >= 0 && val.year() <= 9999 {
val.to_rfc2822()
} else {
val.to_rfc3339()
},
human_time_from_now(val),
)
}
},
Value::Range { val, .. } => val.to_string(),
Value::String { val, .. } => val.clone(),
Value::Glob { val, .. } => val.clone(),
Value::List { vals: val, .. } => format!(
"[{}]",
val.iter()
.map(|x| x.to_expanded_string(", ", config))
.collect::<Vec<_>>()
.join(separator)
),
Value::Record { val, .. } => format!(
"{{{}}}",
val.iter()
.map(|(x, y)| format!("{}: {}", x, y.to_expanded_string(", ", config)))
.collect::<Vec<_>>()
.join(separator)
),
Value::Closure { val, .. } => format!("closure_{}", val.block_id.get()),
Value::Nothing { .. } => String::new(),
Value::Error { error, .. } => format!("{error:?}"),
Value::Binary { val, .. } => format!("{val:?}"),
Value::CellPath { val, .. } => val.to_string(),
Value::Custom { val, .. } => val
.to_base_value(span)
.map(|val| val.to_expanded_string(separator, config))
.unwrap_or_else(|_| format!("<{}>", val.type_name())),
}
}
pub fn to_abbreviated_string(&self, config: &Config) -> String {
match self {
Value::Date { val, .. } => match &config.datetime_format.table {
Some(format) => self.format_datetime(val, format),
None => human_time_from_now(val).to_string(),
},
Value::List { vals, .. } => {
if !vals.is_empty() && vals.iter().all(|x| matches!(x, Value::Record { .. })) {
format!(
"[table {} row{}]",
vals.len(),
if vals.len() == 1 { "" } else { "s" }
)
} else {
format!(
"[list {} item{}]",
vals.len(),
if vals.len() == 1 { "" } else { "s" }
)
}
}
Value::Record { val, .. } => format!(
"{{record {} field{}}}",
val.len(),
if val.len() == 1 { "" } else { "s" }
),
val => val.to_expanded_string(", ", config),
}
}
pub fn to_parsable_string(&self, separator: &str, config: &Config) -> String {
match self {
Value::String { val, .. } => format!("'{val}'"),
Value::List { vals: val, .. } => format!(
"[{}]",
val.iter()
.map(|x| x.to_parsable_string(", ", config))
.collect::<Vec<_>>()
.join(separator)
),
Value::Record { val, .. } => format!(
"{{{}}}",
val.iter()
.map(|(x, y)| format!("{}: {}", x, y.to_parsable_string(", ", config)))
.collect::<Vec<_>>()
.join(separator)
),
_ => self.to_expanded_string(separator, config),
}
}
pub fn to_debug_string(&self) -> String {
match self {
Value::String { val, .. } => {
if contains_emoji(val) {
format!(
"{:#?}",
Value::string(val.escape_unicode().to_string(), self.span())
)
} else {
format!("{self:#?}")
}
}
_ => format!("{self:#?}"),
}
}
pub fn try_put_int_path_member_on_top(cell_paths: &[PathMember]) -> Option<Vec<PathMember>> {
if !nu_experimental::REORDER_CELL_PATHS.get() {
return None;
}
let idx = cell_paths
.iter()
.position(|pm| matches!(pm, PathMember::Int { .. }));
idx.map(|idx| {
let mut cell_paths = cell_paths.to_vec();
cell_paths[0..idx + 1].rotate_right(1);
cell_paths
})
}
pub fn follow_cell_path<'out>(
&'out self,
cell_path: &[PathMember],
) -> Result<Cow<'out, Value>, ShellError> {
let mut store: Value = Value::test_nothing();
let mut current: MultiLife<'out, '_, Value> = MultiLife::Out(self);
let reorder_cell_paths = nu_experimental::REORDER_CELL_PATHS.get();
let mut members: Vec<_> = if reorder_cell_paths {
cell_path.iter().map(Some).collect()
} else {
Vec::new()
};
let mut members = members.as_mut_slice();
let mut cell_path = cell_path;
loop {
let member = if reorder_cell_paths {
while let Some(None) = members.first() {
members = &mut members[1..];
}
if members.is_empty() {
break;
}
let member = if let Value::List { .. } = &*current {
members
.iter_mut()
.find(|x| matches!(x, Some(PathMember::Int { .. })))
.and_then(Option::take)
} else {
None
};
let Some(member) = member.or_else(|| members.first_mut().and_then(Option::take))
else {
break;
};
member
} else {
match cell_path {
[first, rest @ ..] => {
cell_path = rest;
first
}
_ => break,
}
};
current = match current {
MultiLife::Out(current) => match get_value_member(current, member)? {
ControlFlow::Break(span) => return Ok(Cow::Owned(Value::nothing(span))),
ControlFlow::Continue(x) => match x {
Cow::Borrowed(x) => MultiLife::Out(x),
Cow::Owned(x) => {
store = x;
MultiLife::Local(&store)
}
},
},
MultiLife::Local(current) => match get_value_member(current, member)? {
ControlFlow::Break(span) => return Ok(Cow::Owned(Value::nothing(span))),
ControlFlow::Continue(x) => match x {
Cow::Borrowed(x) => MultiLife::Local(x),
Cow::Owned(x) => {
store = x;
MultiLife::Local(&store)
}
},
},
};
}
if let Value::Error { error, .. } = &*current {
Err(error.as_ref().clone())
} else {
Ok(match current {
MultiLife::Out(x) => Cow::Borrowed(x),
MultiLife::Local(x) => {
let x = if std::ptr::eq(x, &store) {
store
} else {
x.clone()
};
Cow::Owned(x)
}
})
}
}
pub fn upsert_cell_path(
&mut self,
cell_path: &[PathMember],
callback: Box<dyn FnOnce(&Value) -> Value>,
) -> Result<(), ShellError> {
let new_val = callback(self.follow_cell_path(cell_path)?.as_ref());
match new_val {
Value::Error { error, .. } => Err(*error),
new_val => self.upsert_data_at_cell_path(cell_path, new_val),
}
}
pub fn upsert_data_at_cell_path(
&mut self,
cell_path: &[PathMember],
new_val: Value,
) -> Result<(), ShellError> {
self.mutate_data_at_cell_path(cell_path, new_val, &CellPathMutation::Upsert)
}
pub fn update_cell_path<'a>(
&mut self,
cell_path: &[PathMember],
callback: Box<dyn FnOnce(&Value) -> Value + 'a>,
) -> Result<(), ShellError> {
let new_val = callback(self.follow_cell_path(cell_path)?.as_ref());
match new_val {
Value::Error { error, .. } => Err(*error),
new_val => self.update_data_at_cell_path(cell_path, new_val),
}
}
pub fn update_data_at_cell_path(
&mut self,
cell_path: &[PathMember],
new_val: Value,
) -> Result<(), ShellError> {
self.mutate_data_at_cell_path(cell_path, new_val, &CellPathMutation::Update)
}
pub fn remove_data_at_cell_path(&mut self, cell_path: &[PathMember]) -> Result<(), ShellError> {
let Some((member, path)) = cell_path.split_last() else {
return Ok(());
};
self.mutate_data_at_cell_path(
path,
Value::nothing(Span::unknown()),
&CellPathMutation::Remove {
member: member.clone(),
},
)
}
pub fn insert_data_at_cell_path(
&mut self,
cell_path: &[PathMember],
new_val: Value,
head_span: Span,
) -> Result<(), ShellError> {
self.mutate_data_at_cell_path(cell_path, new_val, &CellPathMutation::Insert { head_span })
}
fn remove_member(&mut self, member: &PathMember) -> Result<(), ShellError> {
let v_span = self.span();
match member {
PathMember::String {
val: col_name,
span,
optional,
casing,
} => match self {
Value::List { vals, .. } => {
for val in vals.iter_mut() {
let v_span = val.span();
match val {
Value::Record { val: record, .. } => {
let value = record.to_mut().cased_mut(*casing).remove(col_name);
if value.is_none() && !optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: Some(*span),
src_span: v_span,
});
}
}
v => {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: Some(*span),
src_span: v.span(),
});
}
}
}
Ok(())
}
Value::Record { val: record, .. } => {
if record
.to_mut()
.cased_mut(*casing)
.remove(col_name)
.is_none()
&& !optional
{
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: Some(*span),
src_span: v_span,
});
}
Ok(())
}
v => Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: Some(*span),
src_span: v.span(),
}),
},
PathMember::Int {
val: row_num,
span,
optional,
} => match self {
Value::List { vals, .. } => {
if vals.get_mut(*row_num).is_some() {
vals.remove(*row_num);
Ok(())
} else if *optional {
Ok(())
} else if vals.is_empty() {
Err(ShellError::AccessEmptyContent { span: *span })
} else {
Err(ShellError::AccessBeyondEnd {
max_idx: vals.len() - 1,
span: *span,
})
}
}
v => Err(ShellError::NotAList {
dst_span: *span,
src_span: v.span(),
}),
},
}
}
fn mutate_record_at_string_member(
record: &mut Record,
member: &PathMember,
src_span: Span,
path: &[PathMember],
new_val: Value,
action: &CellPathMutation,
) -> Result<(), ShellError> {
let PathMember::String {
val: col_name,
span,
casing,
optional,
} = member
else {
return Err(ShellError::NushellFailed {
msg: "mutate_record_at_string_member called with non-String PathMember".into(),
});
};
if let Some(val) = record.cased_mut(*casing).get_mut(col_name) {
if path.is_empty() && matches!(action, CellPathMutation::Insert { .. }) {
return Err(ShellError::ColumnAlreadyExists {
col_name: col_name.to_owned(),
span: *span,
src_span,
});
}
val.mutate_data_at_cell_path(path, new_val, action)
} else {
match action {
CellPathMutation::Update | CellPathMutation::Remove { .. } => {
if !optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.to_owned(),
span: Some(*span),
src_span,
});
}
Ok(())
}
_ => {
let new_col = Value::with_data_at_cell_path(path, new_val)?;
record.push(col_name, new_col);
Ok(())
}
}
}
}
fn mutate_data_at_cell_path(
&mut self,
cell_path: &[PathMember],
new_val: Value,
action: &CellPathMutation,
) -> Result<(), ShellError> {
let v_span = self.span();
let Some((member, path)) = cell_path.split_first() else {
match action {
CellPathMutation::Remove { member } => return self.remove_member(member),
_ => {
*self = new_val;
return Ok(());
}
}
};
match member {
PathMember::String {
val: col_name,
span,
optional,
..
} => match self {
Value::List { vals, .. } => {
if !matches!(action, CellPathMutation::Remove { .. })
&& let Some(new_cell_path) = Self::try_put_int_path_member_on_top(cell_path)
{
self.mutate_data_at_cell_path(&new_cell_path, new_val.clone(), action)?;
} else {
for val in vals.iter_mut() {
let v_span = val.span();
match val {
Value::Record { val: record, .. } => {
Self::mutate_record_at_string_member(
record.to_mut(),
member,
v_span,
path,
new_val.clone(),
action,
)?;
}
Value::Error { error, .. } => return Err(*error.clone()),
v => match action {
CellPathMutation::Insert { head_span } => {
return Err(ShellError::UnsupportedInput {
msg: "expected table or record".into(),
input: format!("input type: {:?}", v.get_type()),
msg_span: *head_span,
input_span: *span,
});
}
CellPathMutation::Update | CellPathMutation::Remove { .. } => {
if !*optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: Some(*span),
src_span: v.span(),
});
}
}
CellPathMutation::Upsert => {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: Some(*span),
src_span: v.span(),
});
}
},
}
}
}
}
Value::Record { val: record, .. } => {
Self::mutate_record_at_string_member(
record.to_mut(),
member,
v_span,
path,
new_val,
action,
)?;
}
Value::Error { error, .. } => return Err(*error.clone()),
v => match action {
CellPathMutation::Insert { head_span } => {
return Err(ShellError::UnsupportedInput {
msg: "table or record".into(),
input: format!("input type: {:?}", v.get_type()),
msg_span: *head_span,
input_span: *span,
});
}
CellPathMutation::Update | CellPathMutation::Remove { .. } => {
if !*optional {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: Some(*span),
src_span: v.span(),
});
}
}
CellPathMutation::Upsert => {
return Err(ShellError::CantFindColumn {
col_name: col_name.clone(),
span: Some(*span),
src_span: v.span(),
});
}
},
},
PathMember::Int {
val: row_num,
span,
optional,
} => match self {
Value::List { vals, .. } => {
if let Some(v) = vals.get_mut(*row_num) {
if path.is_empty() && matches!(action, CellPathMutation::Insert { .. }) {
vals.insert(*row_num, new_val);
} else {
v.mutate_data_at_cell_path(path, new_val, action)?;
}
} else {
match action {
CellPathMutation::Upsert | CellPathMutation::Insert { .. } => {
if vals.len() != *row_num {
return Err(ShellError::InsertAfterNextFreeIndex {
available_idx: vals.len(),
span: *span,
});
}
vals.push(Value::with_data_at_cell_path(path, new_val)?);
}
CellPathMutation::Update | CellPathMutation::Remove { .. } => {
if !*optional {
if vals.is_empty() {
return Err(ShellError::AccessEmptyContent { span: *span });
} else {
return Err(ShellError::AccessBeyondEnd {
max_idx: vals.len() - 1,
span: *span,
});
}
}
}
}
}
}
Value::Error { error, .. } => return Err(*error.clone()),
_ => {
return Err(ShellError::NotAList {
dst_span: *span,
src_span: v_span,
});
}
},
}
Ok(())
}
fn with_data_at_cell_path(cell_path: &[PathMember], value: Value) -> Result<Value, ShellError> {
if let Some((member, path)) = cell_path.split_first() {
let span = value.span();
match member {
PathMember::String { val, .. } => Ok(Value::record(
std::iter::once((val.clone(), Value::with_data_at_cell_path(path, value)?))
.collect(),
span,
)),
PathMember::Int { val, .. } => {
if *val == 0usize {
Ok(Value::list(
vec![Value::with_data_at_cell_path(path, value)?],
span,
))
} else {
Err(ShellError::InsertAfterNextFreeIndex {
available_idx: 0,
span,
})
}
}
}
} else {
Ok(value)
}
}
pub fn recurse_mut<E>(
&mut self,
f: &mut impl FnMut(&mut Value) -> Result<(), E>,
) -> Result<(), E> {
f(self)?;
match self {
Value::Record { val, .. } => val
.to_mut()
.iter_mut()
.try_for_each(|(_, rec_value)| rec_value.recurse_mut(f)),
Value::List { vals, .. } => vals
.iter_mut()
.try_for_each(|list_value| list_value.recurse_mut(f)),
Value::Closure { val, .. } => val
.captures
.iter_mut()
.map(|(_, captured_value)| captured_value)
.try_for_each(|captured_value| captured_value.recurse_mut(f)),
Value::Bool { .. }
| Value::Int { .. }
| Value::Float { .. }
| Value::Filesize { .. }
| Value::Duration { .. }
| Value::Date { .. }
| Value::Range { .. }
| Value::String { .. }
| Value::Glob { .. }
| Value::Nothing { .. }
| Value::Error { .. }
| Value::Binary { .. }
| Value::CellPath { .. } => Ok(()),
Value::Custom { .. } => Ok(()),
}
}
pub fn is_empty(&self) -> bool {
match self {
Value::String { val, .. } => val.is_empty(),
Value::List { vals, .. } => vals.is_empty(),
Value::Record { val, .. } => val.is_empty(),
Value::Binary { val, .. } => val.is_empty(),
Value::Nothing { .. } => true,
_ => false,
}
}
pub fn is_nothing(&self) -> bool {
matches!(self, Value::Nothing { .. })
}
pub fn is_error(&self) -> bool {
matches!(self, Value::Error { .. })
}
pub fn unwrap_error(self) -> Result<Self, ShellError> {
match self {
Self::Error { error, .. } => Err(*error),
val => Ok(val),
}
}
pub fn is_true(&self) -> bool {
matches!(self, Value::Bool { val: true, .. })
}
pub fn is_false(&self) -> bool {
matches!(self, Value::Bool { val: false, .. })
}
pub fn columns(&self) -> impl Iterator<Item = &String> {
let opt = match self {
Value::Record { val, .. } => Some(val.columns()),
_ => None,
};
opt.into_iter().flatten()
}
pub fn memory_size(&self) -> usize {
match self {
Value::Bool { .. } => std::mem::size_of::<Self>(),
Value::Int { .. } => std::mem::size_of::<Self>(),
Value::Float { .. } => std::mem::size_of::<Self>(),
Value::Filesize { .. } => std::mem::size_of::<Self>(),
Value::Duration { .. } => std::mem::size_of::<Self>(),
Value::Date { .. } => std::mem::size_of::<Self>(),
Value::Range { val, .. } => std::mem::size_of::<Self>() + val.memory_size(),
Value::String { val, .. } => std::mem::size_of::<Self>() + val.capacity(),
Value::Glob { val, .. } => std::mem::size_of::<Self>() + val.capacity(),
Value::Record { val, .. } => std::mem::size_of::<Self>() + val.memory_size(),
Value::List { vals, .. } => {
std::mem::size_of::<Self>() + vals.iter().map(|v| v.memory_size()).sum::<usize>()
}
Value::Closure { val, .. } => std::mem::size_of::<Self>() + val.memory_size(),
Value::Nothing { .. } => std::mem::size_of::<Self>(),
Value::Error { error, .. } => {
std::mem::size_of::<Self>() + std::mem::size_of_val(error)
}
Value::Binary { val, .. } => std::mem::size_of::<Self>() + val.capacity(),
Value::CellPath { val, .. } => std::mem::size_of::<Self>() + val.memory_size(),
Value::Custom { val, .. } => std::mem::size_of::<Self>() + val.memory_size(),
}
}
pub fn bool(val: bool, span: Span) -> Value {
Value::Bool {
val,
internal_span: span,
}
}
pub fn int(val: i64, span: Span) -> Value {
Value::Int {
val,
internal_span: span,
}
}
pub fn float(val: f64, span: Span) -> Value {
Value::Float {
val,
internal_span: span,
}
}
pub fn filesize(val: impl Into<Filesize>, span: Span) -> Value {
Value::Filesize {
val: val.into(),
internal_span: span,
}
}
pub fn duration(val: i64, span: Span) -> Value {
Value::Duration {
val,
internal_span: span,
}
}
pub fn date(val: DateTime<FixedOffset>, span: Span) -> Value {
Value::Date {
val,
internal_span: span,
}
}
pub fn range(val: Range, span: Span) -> Value {
Value::Range {
val: val.into(),
signals: None,
internal_span: span,
}
}
pub fn string(val: impl Into<String>, span: Span) -> Value {
Value::String {
val: val.into(),
internal_span: span,
}
}
pub fn glob(val: impl Into<String>, no_expand: bool, span: Span) -> Value {
Value::Glob {
val: val.into(),
no_expand,
internal_span: span,
}
}
pub fn record(val: Record, span: Span) -> Value {
Value::Record {
val: SharedCow::new(val),
internal_span: span,
}
}
pub fn list(vals: Vec<Value>, span: Span) -> Value {
Value::List {
vals,
signals: None,
internal_span: span,
}
}
pub fn closure(val: Closure, span: Span) -> Value {
Value::Closure {
val: val.into(),
internal_span: span,
}
}
pub fn nothing(span: Span) -> Value {
Value::Nothing {
internal_span: span,
}
}
pub fn error(error: ShellError, span: Span) -> Value {
Value::Error {
error: Box::new(error),
internal_span: span,
}
}
pub fn binary(val: impl Into<Vec<u8>>, span: Span) -> Value {
Value::Binary {
val: val.into(),
internal_span: span,
}
}
pub fn cell_path(val: CellPath, span: Span) -> Value {
Value::CellPath {
val,
internal_span: span,
}
}
pub fn custom(val: Box<dyn CustomValue>, span: Span) -> Value {
Value::Custom {
val,
internal_span: span,
}
}
pub fn test_bool(val: bool) -> Value {
Value::bool(val, Span::test_data())
}
pub fn test_int(val: i64) -> Value {
Value::int(val, Span::test_data())
}
pub fn test_float(val: f64) -> Value {
Value::float(val, Span::test_data())
}
pub fn test_filesize(val: impl Into<Filesize>) -> Value {
Value::filesize(val, Span::test_data())
}
pub fn test_duration(val: i64) -> Value {
Value::duration(val, Span::test_data())
}
pub fn test_date(val: DateTime<FixedOffset>) -> Value {
Value::date(val, Span::test_data())
}
pub fn test_range(val: Range) -> Value {
Value::range(val, Span::test_data())
}
pub fn test_string(val: impl Into<String>) -> Value {
Value::string(val, Span::test_data())
}
pub fn test_glob(val: impl Into<String>) -> Value {
Value::glob(val, false, Span::test_data())
}
pub fn test_record(val: Record) -> Value {
Value::record(val, Span::test_data())
}
pub fn test_list(vals: Vec<Value>) -> Value {
Value::list(vals, Span::test_data())
}
pub fn test_closure(val: Closure) -> Value {
Value::closure(val, Span::test_data())
}
pub fn test_nothing() -> Value {
Value::nothing(Span::test_data())
}
pub fn test_binary(val: impl Into<Vec<u8>>) -> Value {
Value::binary(val, Span::test_data())
}
pub fn test_cell_path(val: CellPath) -> Value {
Value::cell_path(val, Span::test_data())
}
pub fn test_custom_value(val: Box<dyn CustomValue>) -> Value {
Value::custom(val, Span::test_data())
}
pub fn test_values() -> Vec<Value> {
vec![
Value::test_bool(false),
Value::test_int(0),
Value::test_filesize(0),
Value::test_duration(0),
Value::test_date(DateTime::UNIX_EPOCH.into()),
Value::test_range(Range::IntRange(IntRange {
start: 0,
step: 1,
end: Bound::Excluded(0),
})),
Value::test_float(0.0),
Value::test_string(String::new()),
Value::test_record(Record::new()),
Value::test_list(Vec::new()),
Value::test_closure(Closure {
block_id: BlockId::new(0),
captures: Vec::new(),
}),
Value::test_nothing(),
Value::error(
ShellError::NushellFailed { msg: String::new() },
Span::test_data(),
),
Value::test_binary(Vec::new()),
Value::test_cell_path(CellPath {
members: Vec::new(),
}),
]
}
#[track_caller]
pub fn assert_eq(&self, other: impl IntoValue) {
let other = other.into_value(Span::test_data());
assert_eq!(self, &other)
}
pub fn inject_signals(&mut self, engine_state: &EngineState) {
match self {
Value::List { signals: s, .. } | Value::Range { signals: s, .. } => {
*s = Some(engine_state.signals().clone());
}
_ => (),
}
}
}
fn get_value_member<'a>(
current: &'a Value,
member: &PathMember,
) -> Result<ControlFlow<Span, Cow<'a, Value>>, ShellError> {
match member {
PathMember::Int {
val: count,
span: origin_span,
optional,
} => {
match current {
Value::List { vals, .. } => {
if *count < vals.len() {
Ok(ControlFlow::Continue(Cow::Borrowed(&vals[*count])))
} else if *optional {
Ok(ControlFlow::Break(*origin_span))
} else if vals.is_empty() {
Err(ShellError::AccessEmptyContent { span: *origin_span })
} else {
Err(ShellError::AccessBeyondEnd {
max_idx: vals.len() - 1,
span: *origin_span,
})
}
}
Value::Binary { val, .. } => {
if let Some(item) = val.get(*count) {
Ok(ControlFlow::Continue(Cow::Owned(Value::int(
*item as i64,
*origin_span,
))))
} else if *optional {
Ok(ControlFlow::Break(*origin_span))
} else if val.is_empty() {
Err(ShellError::AccessEmptyContent { span: *origin_span })
} else {
Err(ShellError::AccessBeyondEnd {
max_idx: val.len() - 1,
span: *origin_span,
})
}
}
Value::Range { val, .. } => {
if let Some(item) = val
.into_range_iter(current.span(), Signals::empty())
.nth(*count)
{
Ok(ControlFlow::Continue(Cow::Owned(item)))
} else if *optional {
Ok(ControlFlow::Break(*origin_span))
} else {
Err(ShellError::AccessBeyondEndOfStream {
span: *origin_span,
})
}
}
Value::Custom { val, .. } => {
match val.follow_path_int(current.span(), *count, *origin_span, *optional)
{
Ok(val) => Ok(ControlFlow::Continue(Cow::Owned(val))),
Err(err) => {
if *optional {
Ok(ControlFlow::Break(*origin_span))
} else {
Err(err)
}
}
}
}
Value::Nothing { .. } if *optional => Ok(ControlFlow::Break(*origin_span)),
Value::Record { .. } => Err(ShellError::TypeMismatch {
err_message:"Can't access record values with a row index. Try specifying a column name instead".into(),
span: *origin_span,
}),
Value::Error { error, .. } => Err(*error.clone()),
x => Err(ShellError::IncompatiblePathAccess { type_name: format!("{}", x.get_type()), span: *origin_span }),
}
}
PathMember::String {
val: column_name,
span: origin_span,
optional,
casing,
} => {
let span = current.span();
match current {
Value::Record { val, .. } => {
let found = val.cased(*casing).get(column_name);
if let Some(found) = found {
Ok(ControlFlow::Continue(Cow::Borrowed(found)))
} else if *optional {
Ok(ControlFlow::Break(*origin_span))
} else if let Some(suggestion) = did_you_mean(val.columns(), column_name) {
Err(ShellError::DidYouMean {
suggestion,
span: *origin_span,
})
} else {
Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: Some(*origin_span),
src_span: span,
})
}
}
Value::List { vals, .. } => {
let list = vals
.iter()
.map(|val| {
let val_span = val.span();
match val {
Value::Record { val, .. } => {
let found = val.cased(*casing).get(column_name);
if let Some(found) = found {
Ok(found.clone())
} else if *optional {
Ok(Value::nothing(*origin_span))
} else if let Some(suggestion) =
did_you_mean(val.columns(), column_name)
{
Err(ShellError::DidYouMean {
suggestion,
span: *origin_span,
})
} else {
Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: Some(*origin_span),
src_span: val_span,
})
}
}
Value::Nothing { .. } if *optional => {
Ok(Value::nothing(*origin_span))
}
_ => Err(ShellError::CantFindColumn {
col_name: column_name.clone(),
span: Some(*origin_span),
src_span: val_span,
}),
}
})
.collect::<Result<_, _>>()?;
Ok(ControlFlow::Continue(Cow::Owned(Value::list(list, span))))
}
Value::Custom { val, .. } => {
match val.follow_path_string(
current.span(),
column_name.clone(),
*origin_span,
*optional,
*casing,
) {
Ok(val) => Ok(ControlFlow::Continue(Cow::Owned(val))),
Err(err) => {
if *optional {
Ok(ControlFlow::Break(*origin_span))
} else {
Err(err)
}
}
}
}
Value::Nothing { .. } if *optional => Ok(ControlFlow::Break(*origin_span)),
Value::Error { error, .. } => Err(error.as_ref().clone()),
x => Err(ShellError::IncompatiblePathAccess {
type_name: format!("{}", x.get_type()),
span: *origin_span,
}),
}
}
}
}
impl Default for Value {
fn default() -> Self {
Value::Nothing {
internal_span: Span::unknown(),
}
}
}
impl PartialOrd for Value {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
fn compare_floats(val: f64, other: f64) -> Option<Ordering> {
let prec = f64::EPSILON.max(val.abs().max(other.abs()) * f64::EPSILON);
if (other - val).abs() <= prec {
return Some(Ordering::Equal);
}
val.partial_cmp(&other)
}
match (self, other) {
(Value::Bool { val: lhs, .. }, rhs) => match rhs {
Value::Bool { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Int { .. } => Some(Ordering::Less),
Value::Float { .. } => Some(Ordering::Less),
Value::String { .. } => Some(Ordering::Less),
Value::Glob { .. } => Some(Ordering::Less),
Value::Filesize { .. } => Some(Ordering::Less),
Value::Duration { .. } => Some(Ordering::Less),
Value::Date { .. } => Some(Ordering::Less),
Value::Range { .. } => Some(Ordering::Less),
Value::Record { .. } => Some(Ordering::Less),
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Int { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Float { val: rhs, .. } => compare_floats(*lhs as f64, *rhs),
Value::String { .. } => Some(Ordering::Less),
Value::Glob { .. } => Some(Ordering::Less),
Value::Filesize { .. } => Some(Ordering::Less),
Value::Duration { .. } => Some(Ordering::Less),
Value::Date { .. } => Some(Ordering::Less),
Value::Range { .. } => Some(Ordering::Less),
Value::Record { .. } => Some(Ordering::Less),
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Float { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { val: rhs, .. } => compare_floats(*lhs, *rhs as f64),
Value::Float { val: rhs, .. } => compare_floats(*lhs, *rhs),
Value::String { .. } => Some(Ordering::Less),
Value::Glob { .. } => Some(Ordering::Less),
Value::Filesize { .. } => Some(Ordering::Less),
Value::Duration { .. } => Some(Ordering::Less),
Value::Date { .. } => Some(Ordering::Less),
Value::Range { .. } => Some(Ordering::Less),
Value::Record { .. } => Some(Ordering::Less),
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::String { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Glob { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Filesize { .. } => Some(Ordering::Less),
Value::Duration { .. } => Some(Ordering::Less),
Value::Date { .. } => Some(Ordering::Less),
Value::Range { .. } => Some(Ordering::Less),
Value::Record { .. } => Some(Ordering::Less),
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Glob { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Glob { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Filesize { .. } => Some(Ordering::Less),
Value::Duration { .. } => Some(Ordering::Less),
Value::Date { .. } => Some(Ordering::Less),
Value::Range { .. } => Some(Ordering::Less),
Value::Record { .. } => Some(Ordering::Less),
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Filesize { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Duration { .. } => Some(Ordering::Less),
Value::Date { .. } => Some(Ordering::Less),
Value::Range { .. } => Some(Ordering::Less),
Value::Record { .. } => Some(Ordering::Less),
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Duration { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Date { .. } => Some(Ordering::Less),
Value::Range { .. } => Some(Ordering::Less),
Value::Record { .. } => Some(Ordering::Less),
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Date { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { .. } => Some(Ordering::Greater),
Value::Date { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Range { .. } => Some(Ordering::Less),
Value::Record { .. } => Some(Ordering::Less),
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Range { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { .. } => Some(Ordering::Greater),
Value::Date { .. } => Some(Ordering::Greater),
Value::Range { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Record { .. } => Some(Ordering::Less),
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Record { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { .. } => Some(Ordering::Greater),
Value::Date { .. } => Some(Ordering::Greater),
Value::Range { .. } => Some(Ordering::Greater),
Value::Record { val: rhs, .. } => {
let mut lhs = lhs.clone().into_owned();
let mut rhs = rhs.clone().into_owned();
lhs.sort_cols();
rhs.sort_cols();
for (a, b) in lhs.columns().zip(rhs.columns()) {
let result = a.partial_cmp(b);
if result != Some(Ordering::Equal) {
return result;
}
}
for (a, b) in lhs.values().zip(rhs.values()) {
let result = a.partial_cmp(b);
if result != Some(Ordering::Equal) {
return result;
}
}
lhs.len().partial_cmp(&rhs.len())
}
Value::List { .. } => Some(Ordering::Less),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::List { vals: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { .. } => Some(Ordering::Greater),
Value::Date { .. } => Some(Ordering::Greater),
Value::Range { .. } => Some(Ordering::Greater),
Value::Record { .. } => Some(Ordering::Greater),
Value::List { vals: rhs, .. } => lhs.partial_cmp(rhs),
Value::Closure { .. } => Some(Ordering::Less),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Closure { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { .. } => Some(Ordering::Greater),
Value::Date { .. } => Some(Ordering::Greater),
Value::Range { .. } => Some(Ordering::Greater),
Value::Record { .. } => Some(Ordering::Greater),
Value::List { .. } => Some(Ordering::Greater),
Value::Closure { val: rhs, .. } => lhs.block_id.partial_cmp(&rhs.block_id),
Value::Error { .. } => Some(Ordering::Less),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Error { .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { .. } => Some(Ordering::Greater),
Value::Date { .. } => Some(Ordering::Greater),
Value::Range { .. } => Some(Ordering::Greater),
Value::Record { .. } => Some(Ordering::Greater),
Value::List { .. } => Some(Ordering::Greater),
Value::Closure { .. } => Some(Ordering::Greater),
Value::Error { .. } => Some(Ordering::Equal),
Value::Binary { .. } => Some(Ordering::Less),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Binary { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { .. } => Some(Ordering::Greater),
Value::Date { .. } => Some(Ordering::Greater),
Value::Range { .. } => Some(Ordering::Greater),
Value::Record { .. } => Some(Ordering::Greater),
Value::List { .. } => Some(Ordering::Greater),
Value::Closure { .. } => Some(Ordering::Greater),
Value::Error { .. } => Some(Ordering::Greater),
Value::Binary { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::CellPath { .. } => Some(Ordering::Less),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::CellPath { val: lhs, .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { .. } => Some(Ordering::Greater),
Value::Date { .. } => Some(Ordering::Greater),
Value::Range { .. } => Some(Ordering::Greater),
Value::Record { .. } => Some(Ordering::Greater),
Value::List { .. } => Some(Ordering::Greater),
Value::Closure { .. } => Some(Ordering::Greater),
Value::Error { .. } => Some(Ordering::Greater),
Value::Binary { .. } => Some(Ordering::Greater),
Value::CellPath { val: rhs, .. } => lhs.partial_cmp(rhs),
Value::Custom { .. } => Some(Ordering::Less),
Value::Nothing { .. } => Some(Ordering::Less),
},
(Value::Custom { val: lhs, .. }, rhs) => lhs.partial_cmp(rhs),
(Value::Nothing { .. }, rhs) => match rhs {
Value::Bool { .. } => Some(Ordering::Greater),
Value::Int { .. } => Some(Ordering::Greater),
Value::Float { .. } => Some(Ordering::Greater),
Value::String { .. } => Some(Ordering::Greater),
Value::Glob { .. } => Some(Ordering::Greater),
Value::Filesize { .. } => Some(Ordering::Greater),
Value::Duration { .. } => Some(Ordering::Greater),
Value::Date { .. } => Some(Ordering::Greater),
Value::Range { .. } => Some(Ordering::Greater),
Value::Record { .. } => Some(Ordering::Greater),
Value::List { .. } => Some(Ordering::Greater),
Value::Closure { .. } => Some(Ordering::Greater),
Value::Error { .. } => Some(Ordering::Greater),
Value::Binary { .. } => Some(Ordering::Greater),
Value::CellPath { .. } => Some(Ordering::Greater),
Value::Custom { .. } => Some(Ordering::Greater),
Value::Nothing { .. } => Some(Ordering::Equal),
},
}
}
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
self.partial_cmp(other).is_some_and(Ordering::is_eq)
}
}
fn checked_duration_operation<F>(a: i64, b: i64, op: F, span: Span) -> Result<Value, ShellError>
where
F: Fn(i64, i64) -> Option<i64>,
{
if let Some(val) = op(a, b) {
Ok(Value::duration(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "operation overflowed".to_owned(),
span,
help: None,
})
}
}
impl Value {
pub fn add(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = lhs.checked_add(*rhs) {
Ok(Value::int(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "add operation overflowed".into(),
span,
help: Some("Consider using floating point values for increased range by promoting operand with 'into float'. Note: float has reduced precision!".into()),
})
}
}
(Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
Ok(Value::float(*lhs as f64 + *rhs, span))
}
(Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
Ok(Value::float(*lhs + *rhs as f64, span))
}
(Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
Ok(Value::float(lhs + rhs, span))
}
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::string(lhs.to_string() + rhs, span))
}
(Value::Duration { val: lhs, .. }, Value::Date { val: rhs, .. }) => {
if let Some(val) = rhs.checked_add_signed(chrono::Duration::nanoseconds(*lhs)) {
Ok(Value::date(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "addition operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Date { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
if let Some(val) = lhs.checked_add_signed(chrono::Duration::nanoseconds(*rhs)) {
Ok(Value::date(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "addition operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
checked_duration_operation(*lhs, *rhs, i64::checked_add, span)
}
(Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
if let Some(val) = *lhs + *rhs {
Ok(Value::filesize(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "add operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(self.span(), Operator::Math(Math::Add), op, rhs)
}
_ => Err(operator_type_error(
Operator::Math(Math::Add),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::String { .. }
| Value::Date { .. }
| Value::Duration { .. }
| Value::Filesize { .. },
)
},
)),
}
}
pub fn sub(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = lhs.checked_sub(*rhs) {
Ok(Value::int(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "subtraction operation overflowed".into(),
span,
help: Some("Consider using floating point values for increased range by promoting operand with 'into float'. Note: float has reduced precision!".into()),
})
}
}
(Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
Ok(Value::float(*lhs as f64 - *rhs, span))
}
(Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
Ok(Value::float(*lhs - *rhs as f64, span))
}
(Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
Ok(Value::float(lhs - rhs, span))
}
(Value::Date { val: lhs, .. }, Value::Date { val: rhs, .. }) => {
let result = lhs.signed_duration_since(*rhs);
if let Some(v) = result.num_nanoseconds() {
Ok(Value::duration(v, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "subtraction operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Date { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
match lhs.checked_sub_signed(chrono::Duration::nanoseconds(*rhs)) {
Some(val) => Ok(Value::date(val, span)),
_ => Err(ShellError::OperatorOverflow {
msg: "subtraction operation overflowed".into(),
span,
help: None,
}),
}
}
(Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
checked_duration_operation(*lhs, *rhs, i64::checked_sub, span)
}
(Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
if let Some(val) = *lhs - *rhs {
Ok(Value::filesize(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "add operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(self.span(), Operator::Math(Math::Subtract), op, rhs)
}
_ => Err(operator_type_error(
Operator::Math(Math::Subtract),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::Date { .. }
| Value::Duration { .. }
| Value::Filesize { .. },
)
},
)),
}
}
pub fn mul(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = lhs.checked_mul(*rhs) {
Ok(Value::int(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "multiply operation overflowed".into(),
span,
help: Some("Consider using floating point values for increased range by promoting operand with 'into float'. Note: float has reduced precision!".into()),
})
}
}
(Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
Ok(Value::float(*lhs as f64 * *rhs, span))
}
(Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
Ok(Value::float(*lhs * *rhs as f64, span))
}
(Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
Ok(Value::float(lhs * rhs, span))
}
(Value::Int { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
if let Some(val) = *lhs * *rhs {
Ok(Value::filesize(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "multiply operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = *lhs * *rhs {
Ok(Value::filesize(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "multiply operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Float { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
if let Some(val) = *lhs * *rhs {
Ok(Value::filesize(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "multiply operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if let Some(val) = *lhs * *rhs {
Ok(Value::filesize(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "multiply operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Int { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
checked_duration_operation(*lhs, *rhs, i64::checked_mul, span)
}
(Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
checked_duration_operation(*lhs, *rhs, i64::checked_mul, span)
}
(Value::Duration { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
Ok(Value::duration((*lhs as f64 * *rhs) as i64, span))
}
(Value::Float { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
Ok(Value::duration((*lhs * *rhs as f64) as i64, span))
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(self.span(), Operator::Math(Math::Multiply), op, rhs)
}
_ => Err(operator_type_error(
Operator::Math(Math::Multiply),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::Duration { .. }
| Value::Filesize { .. },
)
},
)),
}
}
pub fn div(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Ok(Value::float(*lhs as f64 / *rhs as f64, span))
}
}
(Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if *rhs != 0.0 {
Ok(Value::float(*lhs as f64 / *rhs, span))
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if *rhs != 0 {
Ok(Value::float(*lhs / *rhs as f64, span))
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if *rhs != 0.0 {
Ok(Value::float(lhs / rhs, span))
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
if *rhs == Filesize::ZERO {
Err(ShellError::DivisionByZero { span: op })
} else {
Ok(Value::float(lhs.get() as f64 / rhs.get() as f64, span))
}
}
(Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = lhs.get().checked_div(*rhs) {
Ok(Value::filesize(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if *rhs != 0.0 {
if let Ok(val) = Filesize::try_from(lhs.get() as f64 / rhs) {
Ok(Value::filesize(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Ok(Value::float(*lhs as f64 / *rhs as f64, span))
}
}
(Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = lhs.checked_div(*rhs) {
Ok(Value::duration(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Duration { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if *rhs != 0.0 {
let val = *lhs as f64 / rhs;
if i64::MIN as f64 <= val && val <= i64::MAX as f64 {
Ok(Value::duration(val as i64, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(self.span(), Operator::Math(Math::Divide), op, rhs)
}
_ => Err(operator_type_error(
Operator::Math(Math::Divide),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::Duration { .. }
| Value::Filesize { .. },
)
},
)),
}
}
pub fn floor_div(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
fn checked_div_floor_i64(dividend: i64, divisor: i64) -> Option<i64> {
let quotient = dividend.checked_div(divisor)?;
let remainder = dividend.checked_rem(divisor)?;
if (remainder > 0 && divisor < 0) || (remainder < 0 && divisor > 0) {
Some(quotient - 1)
} else {
Some(quotient)
}
}
fn checked_div_floor_f64(dividend: f64, divisor: f64) -> Option<f64> {
if divisor == 0.0 {
None
} else {
Some((dividend / divisor).floor())
}
}
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_i64(*lhs, *rhs) {
Ok(Value::int(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_f64(*lhs as f64, *rhs) {
Ok(Value::float(val, span))
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_f64(*lhs, *rhs as f64) {
Ok(Value::float(val, span))
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_f64(*lhs, *rhs) {
Ok(Value::float(val, span))
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_i64(lhs.get(), rhs.get()) {
Ok(Value::int(val, span))
} else if *rhs == Filesize::ZERO {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_i64(lhs.get(), *rhs) {
Ok(Value::filesize(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_f64(lhs.get() as f64, *rhs) {
if let Ok(val) = Filesize::try_from(val) {
Ok(Value::filesize(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_i64(*lhs, *rhs) {
Ok(Value::int(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_i64(*lhs, *rhs) {
Ok(Value::duration(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Duration { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if let Some(val) = checked_div_floor_f64(*lhs as f64, *rhs) {
if i64::MIN as f64 <= val && val <= i64::MAX as f64 {
Ok(Value::duration(val as i64, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(self.span(), Operator::Math(Math::FloorDivide), op, rhs)
}
_ => Err(operator_type_error(
Operator::Math(Math::FloorDivide),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::Duration { .. }
| Value::Filesize { .. },
)
},
)),
}
}
pub fn modulo(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
fn checked_mod_i64(dividend: i64, divisor: i64) -> Option<i64> {
let remainder = dividend.checked_rem(divisor)?;
if (remainder > 0 && divisor < 0) || (remainder < 0 && divisor > 0) {
Some(remainder + divisor)
} else {
Some(remainder)
}
}
fn checked_mod_f64(dividend: f64, divisor: f64) -> Option<f64> {
if divisor == 0.0 {
None
} else {
let remainder = dividend % divisor;
if (remainder > 0.0 && divisor < 0.0) || (remainder < 0.0 && divisor > 0.0) {
Some(remainder + divisor)
} else {
Some(remainder)
}
}
}
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = checked_mod_i64(*lhs, *rhs) {
Ok(Value::int(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "modulo operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if let Some(val) = checked_mod_f64(*lhs as f64, *rhs) {
Ok(Value::float(val, span))
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = checked_mod_f64(*lhs, *rhs as f64) {
Ok(Value::float(val, span))
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if let Some(val) = checked_mod_f64(*lhs, *rhs) {
Ok(Value::float(val, span))
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
if let Some(val) = checked_mod_i64(lhs.get(), rhs.get()) {
Ok(Value::filesize(val, span))
} else if *rhs == Filesize::ZERO {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "modulo operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = checked_mod_i64(lhs.get(), *rhs) {
Ok(Value::filesize(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "modulo operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if let Some(val) = checked_mod_f64(lhs.get() as f64, *rhs) {
if let Ok(val) = Filesize::try_from(val) {
Ok(Value::filesize(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "modulo operation overflowed".into(),
span,
help: None,
})
}
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
if let Some(val) = checked_mod_i64(*lhs, *rhs) {
Ok(Value::duration(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = checked_mod_i64(*lhs, *rhs) {
Ok(Value::duration(val, span))
} else if *rhs == 0 {
Err(ShellError::DivisionByZero { span: op })
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
}
(Value::Duration { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
if let Some(val) = checked_mod_f64(*lhs as f64, *rhs) {
if i64::MIN as f64 <= val && val <= i64::MAX as f64 {
Ok(Value::duration(val as i64, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "division operation overflowed".into(),
span,
help: None,
})
}
} else {
Err(ShellError::DivisionByZero { span: op })
}
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Math(Math::Modulo), op, rhs)
}
_ => Err(operator_type_error(
Operator::Math(Math::Modulo),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::Duration { .. }
| Value::Filesize { .. },
)
},
)),
}
}
pub fn pow(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhsv, .. }) => {
if *rhsv < 0 {
return Err(ShellError::IncorrectValue {
msg: "Negative exponent for integer power is unsupported; use floats instead.".into(),
val_span: rhs.span(),
call_span: op,
});
}
if let Some(val) = lhs.checked_pow(*rhsv as u32) {
Ok(Value::int(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "pow operation overflowed".into(),
span,
help: Some("Consider using floating point values for increased range by promoting operand with 'into float'. Note: float has reduced precision!".into()),
})
}
}
(Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
Ok(Value::float((*lhs as f64).powf(*rhs), span))
}
(Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
Ok(Value::float(lhs.powf(*rhs as f64), span))
}
(Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
Ok(Value::float(lhs.powf(*rhs), span))
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Math(Math::Pow), op, rhs)
}
_ => Err(operator_type_error(
Operator::Math(Math::Pow),
op,
self,
rhs,
|val| matches!(val, Value::Int { .. } | Value::Float { .. }),
)),
}
}
pub fn concat(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => {
Ok(Value::list([lhs.as_slice(), rhs.as_slice()].concat(), span))
}
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::string([lhs.as_str(), rhs.as_str()].join(""), span))
}
(Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => Ok(Value::binary(
[lhs.as_slice(), rhs.as_slice()].concat(),
span,
)),
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(self.span(), Operator::Math(Math::Concatenate), op, rhs)
}
_ => {
let help = if matches!(self, Value::List { .. })
|| matches!(rhs, Value::List { .. })
{
Some(
"if you meant to append a value to a list or a record to a table, use the `append` command or wrap the value in a list. For example: `$list ++ $value` should be `$list ++ [$value]` or `$list | append $value`.",
)
} else {
None
};
let is_supported = |val: &Value| {
matches!(
val,
Value::List { .. }
| Value::String { .. }
| Value::Binary { .. }
| Value::Custom { .. }
)
};
Err(match (is_supported(self), is_supported(rhs)) {
(true, true) => ShellError::OperatorIncompatibleTypes {
op: Operator::Math(Math::Concatenate),
lhs: self.get_type(),
rhs: rhs.get_type(),
op_span: op,
lhs_span: self.span(),
rhs_span: rhs.span(),
help,
},
(true, false) => ShellError::OperatorUnsupportedType {
op: Operator::Math(Math::Concatenate),
unsupported: rhs.get_type(),
op_span: op,
unsupported_span: rhs.span(),
help,
},
(false, _) => ShellError::OperatorUnsupportedType {
op: Operator::Math(Math::Concatenate),
unsupported: self.get_type(),
op_span: op,
unsupported_span: self.span(),
help,
},
})
}
}
}
pub fn lt(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
if let (Value::Custom { val: lhs, .. }, rhs) = (self, rhs) {
return lhs.operation(
self.span(),
Operator::Comparison(Comparison::LessThan),
op,
rhs,
);
}
if matches!(self, Value::Nothing { .. }) || matches!(rhs, Value::Nothing { .. }) {
return Ok(Value::nothing(span));
}
if !type_compatible(self.get_type(), rhs.get_type()) {
return Err(operator_type_error(
Operator::Comparison(Comparison::LessThan),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::String { .. }
| Value::Filesize { .. }
| Value::Duration { .. }
| Value::Date { .. }
| Value::Bool { .. }
| Value::Nothing { .. }
)
},
));
}
Ok(Value::bool(
matches!(self.partial_cmp(rhs), Some(Ordering::Less)),
span,
))
}
pub fn lte(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
if let (Value::Custom { val: lhs, .. }, rhs) = (self, rhs) {
return lhs.operation(
self.span(),
Operator::Comparison(Comparison::LessThanOrEqual),
op,
rhs,
);
}
if matches!(self, Value::Nothing { .. }) || matches!(rhs, Value::Nothing { .. }) {
return Ok(Value::nothing(span));
}
if !type_compatible(self.get_type(), rhs.get_type()) {
return Err(operator_type_error(
Operator::Comparison(Comparison::LessThanOrEqual),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::String { .. }
| Value::Filesize { .. }
| Value::Duration { .. }
| Value::Date { .. }
| Value::Bool { .. }
| Value::Nothing { .. }
)
},
));
}
Ok(Value::bool(
matches!(
self.partial_cmp(rhs),
Some(Ordering::Less | Ordering::Equal)
),
span,
))
}
pub fn gt(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
if let (Value::Custom { val: lhs, .. }, rhs) = (self, rhs) {
return lhs.operation(
self.span(),
Operator::Comparison(Comparison::GreaterThan),
op,
rhs,
);
}
if matches!(self, Value::Nothing { .. }) || matches!(rhs, Value::Nothing { .. }) {
return Ok(Value::nothing(span));
}
if !type_compatible(self.get_type(), rhs.get_type()) {
return Err(operator_type_error(
Operator::Comparison(Comparison::GreaterThan),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::String { .. }
| Value::Filesize { .. }
| Value::Duration { .. }
| Value::Date { .. }
| Value::Bool { .. }
| Value::Nothing { .. }
)
},
));
}
Ok(Value::bool(
matches!(self.partial_cmp(rhs), Some(Ordering::Greater)),
span,
))
}
pub fn gte(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
if let (Value::Custom { val: lhs, .. }, rhs) = (self, rhs) {
return lhs.operation(
self.span(),
Operator::Comparison(Comparison::GreaterThanOrEqual),
op,
rhs,
);
}
if matches!(self, Value::Nothing { .. }) || matches!(rhs, Value::Nothing { .. }) {
return Ok(Value::nothing(span));
}
if !type_compatible(self.get_type(), rhs.get_type()) {
return Err(operator_type_error(
Operator::Comparison(Comparison::GreaterThanOrEqual),
op,
self,
rhs,
|val| {
matches!(
val,
Value::Int { .. }
| Value::Float { .. }
| Value::String { .. }
| Value::Filesize { .. }
| Value::Duration { .. }
| Value::Date { .. }
| Value::Bool { .. }
| Value::Nothing { .. }
)
},
));
}
Ok(Value::bool(
matches!(
self.partial_cmp(rhs),
Some(Ordering::Greater | Ordering::Equal)
),
span,
))
}
pub fn eq(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
if let (Value::Custom { val: lhs, .. }, rhs) = (self, rhs) {
return lhs.operation(
self.span(),
Operator::Comparison(Comparison::Equal),
op,
rhs,
);
}
Ok(Value::bool(
matches!(self.partial_cmp(rhs), Some(Ordering::Equal)),
span,
))
}
pub fn ne(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
if let (Value::Custom { val: lhs, .. }, rhs) = (self, rhs) {
return lhs.operation(
self.span(),
Operator::Comparison(Comparison::NotEqual),
op,
rhs,
);
}
Ok(Value::bool(
!matches!(self.partial_cmp(rhs), Some(Ordering::Equal)),
span,
))
}
pub fn r#in(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(lhs, Value::Range { val: rhs, .. }) => Ok(Value::bool(rhs.contains(lhs), span)),
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::bool(rhs.contains(lhs), span))
}
(lhs, Value::List { vals: rhs, .. }) => Ok(Value::bool(rhs.contains(lhs), span)),
(Value::String { val: lhs, .. }, Value::Record { val: rhs, .. }) => {
Ok(Value::bool(rhs.contains(lhs), span))
}
(Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => {
let val = rhs.members.iter().any(|member| match (self, member) {
(Value::Int { val: lhs, .. }, PathMember::Int { val: rhs, .. }) => {
*lhs == *rhs as i64
}
(Value::String { val: lhs, .. }, PathMember::String { val: rhs, .. }) => {
lhs == rhs
}
(Value::String { .. }, PathMember::Int { .. })
| (Value::Int { .. }, PathMember::String { .. }) => false,
_ => unreachable!(
"outer match arm ensures `self` is either a `String` or `Int` variant"
),
});
Ok(Value::bool(val, span))
}
(Value::CellPath { val: lhs, .. }, Value::CellPath { val: rhs, .. }) => {
Ok(Value::bool(
rhs.members
.windows(lhs.members.len())
.any(|member_window| member_window == rhs.members),
span,
))
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(self.span(), Operator::Comparison(Comparison::In), op, rhs)
}
(lhs, rhs) => Err(
if let Value::List { .. }
| Value::Range { .. }
| Value::String { .. }
| Value::Record { .. }
| Value::Custom { .. } = rhs
{
ShellError::OperatorIncompatibleTypes {
op: Operator::Comparison(Comparison::In),
lhs: lhs.get_type(),
rhs: rhs.get_type(),
op_span: op,
lhs_span: lhs.span(),
rhs_span: rhs.span(),
help: None,
}
} else {
ShellError::OperatorUnsupportedType {
op: Operator::Comparison(Comparison::In),
unsupported: rhs.get_type(),
op_span: op,
unsupported_span: rhs.span(),
help: None,
}
},
),
}
}
pub fn not_in(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(lhs, Value::Range { val: rhs, .. }) => Ok(Value::bool(!rhs.contains(lhs), span)),
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::bool(!rhs.contains(lhs), span))
}
(lhs, Value::List { vals: rhs, .. }) => Ok(Value::bool(!rhs.contains(lhs), span)),
(Value::String { val: lhs, .. }, Value::Record { val: rhs, .. }) => {
Ok(Value::bool(!rhs.contains(lhs), span))
}
(Value::String { .. } | Value::Int { .. }, Value::CellPath { val: rhs, .. }) => {
let val = rhs.members.iter().any(|member| match (self, member) {
(Value::Int { val: lhs, .. }, PathMember::Int { val: rhs, .. }) => {
*lhs != *rhs as i64
}
(Value::String { val: lhs, .. }, PathMember::String { val: rhs, .. }) => {
lhs != rhs
}
(Value::String { .. }, PathMember::Int { .. })
| (Value::Int { .. }, PathMember::String { .. }) => true,
_ => unreachable!(
"outer match arm ensures `self` is either a `String` or `Int` variant"
),
});
Ok(Value::bool(val, span))
}
(Value::CellPath { val: lhs, .. }, Value::CellPath { val: rhs, .. }) => {
Ok(Value::bool(
rhs.members
.windows(lhs.members.len())
.all(|member_window| member_window != rhs.members),
span,
))
}
(Value::Custom { val: lhs, .. }, rhs) => lhs.operation(
self.span(),
Operator::Comparison(Comparison::NotIn),
op,
rhs,
),
(lhs, rhs) => Err(
if let Value::List { .. }
| Value::Range { .. }
| Value::String { .. }
| Value::Record { .. }
| Value::Custom { .. } = rhs
{
ShellError::OperatorIncompatibleTypes {
op: Operator::Comparison(Comparison::NotIn),
lhs: lhs.get_type(),
rhs: rhs.get_type(),
op_span: op,
lhs_span: lhs.span(),
rhs_span: rhs.span(),
help: None,
}
} else {
ShellError::OperatorUnsupportedType {
op: Operator::Comparison(Comparison::NotIn),
unsupported: rhs.get_type(),
op_span: op,
unsupported_span: rhs.span(),
help: None,
}
},
),
}
}
pub fn has(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
rhs.r#in(op, self, span)
}
pub fn not_has(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
rhs.r#not_in(op, self, span)
}
pub fn regex_match(
&self,
engine_state: &EngineState,
op: Span,
rhs: &Value,
invert: bool,
span: Span,
) -> Result<Value, ShellError> {
let rhs_span = rhs.span();
match (self, rhs) {
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
let is_match = match engine_state.regex_cache.try_lock() {
Ok(mut cache) => {
if let Some(regex) = cache.get(rhs) {
regex.is_match(lhs)
} else {
let regex =
Regex::new(rhs).map_err(|e| ShellError::UnsupportedInput {
msg: format!("{e}"),
input: "value originated from here".into(),
msg_span: span,
input_span: rhs_span,
})?;
let ret = regex.is_match(lhs);
cache.put(rhs.clone(), regex);
ret
}
}
Err(_) => {
let regex = Regex::new(rhs).map_err(|e| ShellError::UnsupportedInput {
msg: format!("{e}"),
input: "value originated from here".into(),
msg_span: span,
input_span: rhs_span,
})?;
regex.is_match(lhs)
}
};
Ok(Value::bool(
if invert {
!is_match.unwrap_or(false)
} else {
is_match.unwrap_or(true)
},
span,
))
}
(Value::Custom { val: lhs, .. }, rhs) => lhs.operation(
span,
if invert {
Operator::Comparison(Comparison::NotRegexMatch)
} else {
Operator::Comparison(Comparison::RegexMatch)
},
op,
rhs,
),
_ => Err(operator_type_error(
if invert {
Operator::Comparison(Comparison::NotRegexMatch)
} else {
Operator::Comparison(Comparison::RegexMatch)
},
op,
self,
rhs,
|val| matches!(val, Value::String { .. }),
)),
}
}
pub fn starts_with(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::bool(lhs.starts_with(rhs), span))
}
(Value::Custom { val: lhs, .. }, rhs) => lhs.operation(
self.span(),
Operator::Comparison(Comparison::StartsWith),
op,
rhs,
),
_ => Err(operator_type_error(
Operator::Comparison(Comparison::StartsWith),
op,
self,
rhs,
|val| matches!(val, Value::String { .. }),
)),
}
}
pub fn not_starts_with(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::bool(!lhs.starts_with(rhs), span))
}
(Value::Custom { val: lhs, .. }, rhs) => lhs.operation(
self.span(),
Operator::Comparison(Comparison::NotStartsWith),
op,
rhs,
),
_ => Err(operator_type_error(
Operator::Comparison(Comparison::NotStartsWith),
op,
self,
rhs,
|val| matches!(val, Value::String { .. }),
)),
}
}
pub fn ends_with(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::bool(lhs.ends_with(rhs), span))
}
(Value::Custom { val: lhs, .. }, rhs) => lhs.operation(
self.span(),
Operator::Comparison(Comparison::EndsWith),
op,
rhs,
),
_ => Err(operator_type_error(
Operator::Comparison(Comparison::EndsWith),
op,
self,
rhs,
|val| matches!(val, Value::String { .. }),
)),
}
}
pub fn not_ends_with(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
Ok(Value::bool(!lhs.ends_with(rhs), span))
}
(Value::Custom { val: lhs, .. }, rhs) => lhs.operation(
self.span(),
Operator::Comparison(Comparison::NotEndsWith),
op,
rhs,
),
_ => Err(operator_type_error(
Operator::Comparison(Comparison::NotEndsWith),
op,
self,
rhs,
|val| matches!(val, Value::String { .. }),
)),
}
}
pub fn bit_or(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
Ok(Value::int(*lhs | rhs, span))
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Bits(Bits::BitOr), op, rhs)
}
_ => Err(operator_type_error(
Operator::Bits(Bits::BitOr),
op,
self,
rhs,
|val| matches!(val, Value::Int { .. }),
)),
}
}
pub fn bit_xor(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
Ok(Value::int(*lhs ^ rhs, span))
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Bits(Bits::BitXor), op, rhs)
}
_ => Err(operator_type_error(
Operator::Bits(Bits::BitXor),
op,
self,
rhs,
|val| matches!(val, Value::Int { .. }),
)),
}
}
pub fn bit_and(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
Ok(Value::int(*lhs & rhs, span))
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Bits(Bits::BitAnd), op, rhs)
}
_ => Err(operator_type_error(
Operator::Bits(Bits::BitAnd),
op,
self,
rhs,
|val| matches!(val, Value::Int { .. }),
)),
}
}
pub fn bit_shl(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = (*rhs).try_into().ok().and_then(|rhs| lhs.checked_shl(rhs)) {
Ok(Value::int(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "right operand to bit-shl exceeds available bits in underlying data"
.into(),
span,
help: Some(format!("Limit operand to 0 <= rhs < {}", i64::BITS)),
})
}
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Bits(Bits::ShiftLeft), op, rhs)
}
_ => Err(operator_type_error(
Operator::Bits(Bits::ShiftLeft),
op,
self,
rhs,
|val| matches!(val, Value::Int { .. }),
)),
}
}
pub fn bit_shr(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
if let Some(val) = (*rhs).try_into().ok().and_then(|rhs| lhs.checked_shr(rhs)) {
Ok(Value::int(val, span))
} else {
Err(ShellError::OperatorOverflow {
msg: "right operand to bit-shr exceeds available bits in underlying data"
.into(),
span,
help: Some(format!("Limit operand to 0 <= rhs < {}", i64::BITS)),
})
}
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Bits(Bits::ShiftRight), op, rhs)
}
_ => Err(operator_type_error(
Operator::Bits(Bits::ShiftRight),
op,
self,
rhs,
|val| matches!(val, Value::Int { .. }),
)),
}
}
pub fn or(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Bool { val: lhs, .. }, Value::Bool { val: rhs, .. }) => {
Ok(Value::bool(*lhs || *rhs, span))
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Boolean(Boolean::Or), op, rhs)
}
_ => Err(operator_type_error(
Operator::Boolean(Boolean::Or),
op,
self,
rhs,
|val| matches!(val, Value::Bool { .. }),
)),
}
}
pub fn xor(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Bool { val: lhs, .. }, Value::Bool { val: rhs, .. }) => {
Ok(Value::bool((*lhs && !*rhs) || (!*lhs && *rhs), span))
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Boolean(Boolean::Xor), op, rhs)
}
_ => Err(operator_type_error(
Operator::Boolean(Boolean::Xor),
op,
self,
rhs,
|val| matches!(val, Value::Bool { .. }),
)),
}
}
pub fn and(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
match (self, rhs) {
(Value::Bool { val: lhs, .. }, Value::Bool { val: rhs, .. }) => {
Ok(Value::bool(*lhs && *rhs, span))
}
(Value::Custom { val: lhs, .. }, rhs) => {
lhs.operation(span, Operator::Boolean(Boolean::And), op, rhs)
}
_ => Err(operator_type_error(
Operator::Boolean(Boolean::And),
op,
self,
rhs,
|val| matches!(val, Value::Bool { .. }),
)),
}
}
}
fn type_compatible(a: Type, b: Type) -> bool {
if a == b {
return true;
}
matches!((a, b), (Type::Int, Type::Float) | (Type::Float, Type::Int))
}
fn operator_type_error(
op: Operator,
op_span: Span,
lhs: &Value,
rhs: &Value,
is_supported: fn(&Value) -> bool,
) -> ShellError {
let is_supported = |val| is_supported(val) || matches!(val, Value::Custom { .. });
match (is_supported(lhs), is_supported(rhs)) {
(true, true) => ShellError::OperatorIncompatibleTypes {
op,
lhs: lhs.get_type(),
rhs: rhs.get_type(),
op_span,
lhs_span: lhs.span(),
rhs_span: rhs.span(),
help: None,
},
(true, false) => ShellError::OperatorUnsupportedType {
op,
unsupported: rhs.get_type(),
op_span,
unsupported_span: rhs.span(),
help: None,
},
(false, _) => ShellError::OperatorUnsupportedType {
op,
unsupported: lhs.get_type(),
op_span,
unsupported_span: lhs.span(),
help: None,
},
}
}
pub fn human_time_from_now(val: &DateTime<FixedOffset>) -> HumanTime {
let now = Local::now().with_timezone(val.offset());
let delta = *val - now;
match delta.num_nanoseconds() {
Some(num_nanoseconds) => {
let delta_seconds = num_nanoseconds as f64 / 1_000_000_000.0;
let delta_seconds_rounded = delta_seconds.round() as i64;
HumanTime::from(Duration::seconds(delta_seconds_rounded))
}
None => {
let delta_years = val.year() - now.year();
HumanTime::from(Duration::days(delta_years as i64 * 365))
}
}
}
#[cfg(test)]
mod tests {
use super::{Record, Value};
use crate::record;
mod at_cell_path {
use crate::casing::Casing;
use crate::{IntoValue, ShellError, Span};
use super::super::PathMember;
use super::*;
#[test]
fn test_record_with_data_at_cell_path() {
let value_to_insert = Value::test_string("value");
let span = Span::test_data();
assert_eq!(
Value::with_data_at_cell_path(
&[
PathMember::test_string("a".to_string(), false, Casing::Sensitive),
PathMember::test_string("b".to_string(), false, Casing::Sensitive),
PathMember::test_string("c".to_string(), false, Casing::Sensitive),
PathMember::test_string("d".to_string(), false, Casing::Sensitive),
],
value_to_insert,
),
Ok(record!(
"a" => record!(
"b" => record!(
"c" => record!(
"d" => Value::test_string("value")
).into_value(span)
).into_value(span)
).into_value(span)
)
.into_value(span))
);
}
#[test]
fn test_lists_with_data_at_cell_path() {
let value_to_insert = Value::test_string("value");
assert_eq!(
Value::with_data_at_cell_path(
&[
PathMember::test_int(0, false),
PathMember::test_int(0, false),
PathMember::test_int(0, false),
PathMember::test_int(0, false),
],
value_to_insert.clone(),
),
Ok(Value::test_list(vec![Value::test_list(vec![
Value::test_list(vec![Value::test_list(vec![value_to_insert])])
])]))
);
}
#[test]
fn test_mixed_with_data_at_cell_path() {
let value_to_insert = Value::test_string("value");
let span = Span::test_data();
assert_eq!(
Value::with_data_at_cell_path(
&[
PathMember::test_string("a".to_string(), false, Casing::Sensitive),
PathMember::test_int(0, false),
PathMember::test_string("b".to_string(), false, Casing::Sensitive),
PathMember::test_int(0, false),
PathMember::test_string("c".to_string(), false, Casing::Sensitive),
PathMember::test_int(0, false),
PathMember::test_string("d".to_string(), false, Casing::Sensitive),
PathMember::test_int(0, false),
],
value_to_insert.clone(),
),
Ok(record!(
"a" => Value::test_list(vec![record!(
"b" => Value::test_list(vec![record!(
"c" => Value::test_list(vec![record!(
"d" => Value::test_list(vec![value_to_insert])
).into_value(span)])
).into_value(span)])
).into_value(span)])
)
.into_value(span))
);
}
#[test]
fn test_nested_upsert_data_at_cell_path() {
let span = Span::test_data();
let mut base_value = record!(
"a" => Value::test_list(vec![])
)
.into_value(span);
let value_to_insert = Value::test_string("value");
let res = base_value.upsert_data_at_cell_path(
&[
PathMember::test_string("a".to_string(), false, Casing::Sensitive),
PathMember::test_int(0, false),
PathMember::test_string("b".to_string(), false, Casing::Sensitive),
PathMember::test_int(0, false),
],
value_to_insert.clone(),
);
assert_eq!(res, Ok(()));
assert_eq!(
base_value,
record!(
"a" => Value::test_list(vec![
record!(
"b" => Value::test_list(vec![value_to_insert])
)
.into_value(span)
])
)
.into_value(span)
);
}
#[test]
fn test_nested_insert_data_at_cell_path() {
let span = Span::test_data();
let mut base_value = record!(
"a" => Value::test_list(vec![])
)
.into_value(span);
let value_to_insert = Value::test_string("value");
let res = base_value.insert_data_at_cell_path(
&[
PathMember::test_string("a".to_string(), false, Casing::Sensitive),
PathMember::test_int(0, false),
PathMember::test_string("b".to_string(), false, Casing::Sensitive),
PathMember::test_int(0, false),
],
value_to_insert.clone(),
span,
);
assert_eq!(res, Ok(()));
assert_eq!(
base_value,
record!(
"a" => Value::test_list(vec![
record!(
"b" => Value::test_list(vec![value_to_insert])
)
.into_value(span)
])
)
.into_value(span)
);
}
#[test]
fn update_existing_record_field() {
let span = Span::test_data();
let mut val = record!("a" => Value::test_int(1)).into_value(span);
let res = val.update_data_at_cell_path(
&[PathMember::test_string(
"a".into(),
false,
Casing::Sensitive,
)],
Value::test_int(2),
);
assert_eq!(res, Ok(()));
assert_eq!(val, record!("a" => Value::test_int(2)).into_value(span));
}
#[test]
fn update_existing_list_element() {
let mut val = Value::test_list(vec![Value::test_int(10), Value::test_int(20)]);
let res = val
.update_data_at_cell_path(&[PathMember::test_int(1, false)], Value::test_int(99));
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![Value::test_int(10), Value::test_int(99)])
);
}
#[test]
fn update_missing_record_field_errors() {
let span = Span::test_data();
let mut val = record!("a" => Value::test_int(1)).into_value(span);
let res = val.update_data_at_cell_path(
&[PathMember::test_string(
"b".into(),
false,
Casing::Sensitive,
)],
Value::test_int(2),
);
assert!(matches!(res, Err(ShellError::CantFindColumn { .. })));
}
#[test]
fn update_out_of_bounds_list_errors() {
let mut val = Value::test_list(vec![Value::test_int(1)]);
let res =
val.update_data_at_cell_path(&[PathMember::test_int(5, false)], Value::test_int(2));
assert!(matches!(res, Err(ShellError::AccessBeyondEnd { .. })));
}
#[test]
fn update_empty_list_errors() {
let mut val = Value::test_list(vec![]);
let res =
val.update_data_at_cell_path(&[PathMember::test_int(0, false)], Value::test_int(2));
assert!(matches!(res, Err(ShellError::AccessEmptyContent { .. })));
}
#[test]
fn update_optional_missing_field_ok() {
let span = Span::test_data();
let mut val = record!("a" => Value::test_int(1)).into_value(span);
let res = val.update_data_at_cell_path(
&[PathMember::test_string("z".into(), true, Casing::Sensitive)],
Value::test_int(2),
);
assert_eq!(res, Ok(()));
assert_eq!(val, record!("a" => Value::test_int(1)).into_value(span));
}
#[test]
fn update_optional_out_of_bounds_ok() {
let mut val = Value::test_list(vec![Value::test_int(1)]);
let res =
val.update_data_at_cell_path(&[PathMember::test_int(5, true)], Value::test_int(2));
assert_eq!(res, Ok(()));
assert_eq!(val, Value::test_list(vec![Value::test_int(1)]));
}
#[test]
fn update_nested_record_field() {
let span = Span::test_data();
let mut val = record!(
"a" => record!("b" => Value::test_int(1)).into_value(span)
)
.into_value(span);
let res = val.update_data_at_cell_path(
&[
PathMember::test_string("a".into(), false, Casing::Sensitive),
PathMember::test_string("b".into(), false, Casing::Sensitive),
],
Value::test_int(99),
);
assert_eq!(res, Ok(()));
assert_eq!(
val,
record!(
"a" => record!("b" => Value::test_int(99)).into_value(span)
)
.into_value(span)
);
}
#[test]
fn update_in_table() {
let span = Span::test_data();
let mut val = Value::test_list(vec![
record!("x" => Value::test_int(1)).into_value(span),
record!("x" => Value::test_int(2)).into_value(span),
]);
let res = val.update_data_at_cell_path(
&[PathMember::test_string(
"x".into(),
false,
Casing::Sensitive,
)],
Value::test_int(0),
);
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![
record!("x" => Value::test_int(0)).into_value(span),
record!("x" => Value::test_int(0)).into_value(span),
])
);
}
#[test]
fn remove_record_column() {
let span = Span::test_data();
let mut val =
record!("a" => Value::test_int(1), "b" => Value::test_int(2)).into_value(span);
let res = val.remove_data_at_cell_path(&[PathMember::test_string(
"a".into(),
false,
Casing::Sensitive,
)]);
assert_eq!(res, Ok(()));
assert_eq!(val, record!("b" => Value::test_int(2)).into_value(span));
}
#[test]
fn remove_list_element() {
let mut val = Value::test_list(vec![
Value::test_int(10),
Value::test_int(20),
Value::test_int(30),
]);
let res = val.remove_data_at_cell_path(&[PathMember::test_int(1, false)]);
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![Value::test_int(10), Value::test_int(30)])
);
}
#[test]
fn remove_nested_field() {
let span = Span::test_data();
let mut val = record!(
"a" => record!("b" => Value::test_int(1), "c" => Value::test_int(2)).into_value(span)
)
.into_value(span);
let res = val.remove_data_at_cell_path(&[
PathMember::test_string("a".into(), false, Casing::Sensitive),
PathMember::test_string("b".into(), false, Casing::Sensitive),
]);
assert_eq!(res, Ok(()));
assert_eq!(
val,
record!(
"a" => record!("c" => Value::test_int(2)).into_value(span)
)
.into_value(span)
);
}
#[test]
fn remove_column_from_table() {
let span = Span::test_data();
let mut val = Value::test_list(vec![
record!("x" => Value::test_int(1), "y" => Value::test_int(2)).into_value(span),
record!("x" => Value::test_int(3), "y" => Value::test_int(4)).into_value(span),
]);
let res = val.remove_data_at_cell_path(&[PathMember::test_string(
"x".into(),
false,
Casing::Sensitive,
)]);
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![
record!("y" => Value::test_int(2)).into_value(span),
record!("y" => Value::test_int(4)).into_value(span),
])
);
}
#[test]
fn upsert_overwrite_existing_record_field() {
let span = Span::test_data();
let mut val = record!("a" => Value::test_int(1)).into_value(span);
let res = val.upsert_data_at_cell_path(
&[PathMember::test_string(
"a".into(),
false,
Casing::Sensitive,
)],
Value::test_int(99),
);
assert_eq!(res, Ok(()));
assert_eq!(val, record!("a" => Value::test_int(99)).into_value(span));
}
#[test]
fn upsert_overwrite_existing_list_element() {
let mut val = Value::test_list(vec![Value::test_int(10), Value::test_int(20)]);
let res = val
.upsert_data_at_cell_path(&[PathMember::test_int(0, false)], Value::test_int(99));
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![Value::test_int(99), Value::test_int(20)])
);
}
#[test]
fn upsert_creates_new_record_field() {
let span = Span::test_data();
let mut val = record!("a" => Value::test_int(1)).into_value(span);
let res = val.upsert_data_at_cell_path(
&[PathMember::test_string(
"b".into(),
false,
Casing::Sensitive,
)],
Value::test_int(2),
);
assert_eq!(res, Ok(()));
assert_eq!(
val,
record!("a" => Value::test_int(1), "b" => Value::test_int(2)).into_value(span)
);
}
#[test]
fn upsert_appends_to_list() {
let mut val = Value::test_list(vec![Value::test_int(1)]);
let res =
val.upsert_data_at_cell_path(&[PathMember::test_int(1, false)], Value::test_int(2));
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![Value::test_int(1), Value::test_int(2)])
);
}
#[test]
fn upsert_in_table() {
let span = Span::test_data();
let mut val = Value::test_list(vec![
record!("x" => Value::test_int(1)).into_value(span),
record!("x" => Value::test_int(2)).into_value(span),
]);
let res = val.upsert_data_at_cell_path(
&[PathMember::test_string(
"x".into(),
false,
Casing::Sensitive,
)],
Value::test_int(0),
);
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![
record!("x" => Value::test_int(0)).into_value(span),
record!("x" => Value::test_int(0)).into_value(span),
])
);
}
#[test]
fn insert_new_record_field() {
let span = Span::test_data();
let mut val = record!("a" => Value::test_int(1)).into_value(span);
let res = val.insert_data_at_cell_path(
&[PathMember::test_string(
"b".into(),
false,
Casing::Sensitive,
)],
Value::test_int(2),
span,
);
assert_eq!(res, Ok(()));
assert_eq!(
val,
record!("a" => Value::test_int(1), "b" => Value::test_int(2)).into_value(span)
);
}
#[test]
fn insert_existing_record_field_errors() {
let span = Span::test_data();
let mut val = record!("a" => Value::test_int(1)).into_value(span);
let res = val.insert_data_at_cell_path(
&[PathMember::test_string(
"a".into(),
false,
Casing::Sensitive,
)],
Value::test_int(2),
span,
);
assert!(matches!(res, Err(ShellError::ColumnAlreadyExists { .. })));
}
#[test]
fn insert_at_existing_list_index_shifts() {
let mut val = Value::test_list(vec![Value::test_int(1), Value::test_int(2)]);
let span = Span::test_data();
let res = val.insert_data_at_cell_path(
&[PathMember::test_int(0, false)],
Value::test_int(99),
span,
);
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![
Value::test_int(99),
Value::test_int(1),
Value::test_int(2),
])
);
}
#[test]
fn insert_appends_at_end_of_list() {
let mut val = Value::test_list(vec![Value::test_int(1)]);
let span = Span::test_data();
let res = val.insert_data_at_cell_path(
&[PathMember::test_int(1, false)],
Value::test_int(2),
span,
);
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![Value::test_int(1), Value::test_int(2)])
);
}
#[test]
fn insert_beyond_end_errors() {
let mut val = Value::test_list(vec![Value::test_int(1)]);
let span = Span::test_data();
let res = val.insert_data_at_cell_path(
&[PathMember::test_int(5, false)],
Value::test_int(2),
span,
);
assert!(matches!(
res,
Err(ShellError::InsertAfterNextFreeIndex { .. })
));
}
#[test]
fn insert_existing_column_in_table_errors() {
let span = Span::test_data();
let mut val =
Value::test_list(vec![record!("x" => Value::test_int(1)).into_value(span)]);
let res = val.insert_data_at_cell_path(
&[PathMember::test_string(
"x".into(),
false,
Casing::Sensitive,
)],
Value::test_int(0),
span,
);
assert!(matches!(res, Err(ShellError::ColumnAlreadyExists { .. })));
}
#[test]
fn insert_new_column_in_table() {
let span = Span::test_data();
let mut val =
Value::test_list(vec![record!("x" => Value::test_int(1)).into_value(span)]);
let res = val.insert_data_at_cell_path(
&[PathMember::test_string(
"y".into(),
false,
Casing::Sensitive,
)],
Value::test_int(2),
span,
);
assert_eq!(res, Ok(()));
assert_eq!(
val,
Value::test_list(vec![
record!("x" => Value::test_int(1), "y" => Value::test_int(2)).into_value(span),
])
);
}
}
mod is_empty {
use super::*;
#[test]
fn test_string() {
let value = Value::test_string("");
assert!(value.is_empty());
}
#[test]
fn test_list() {
let list_with_no_values = Value::test_list(vec![]);
let list_with_one_empty_string = Value::test_list(vec![Value::test_string("")]);
assert!(list_with_no_values.is_empty());
assert!(!list_with_one_empty_string.is_empty());
}
#[test]
fn test_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! {
"" => Value::test_string(""),
});
let one_column_with_a_string_and_one_cell_value_with_empty_string =
Value::test_record(record! {
"column" => Value::test_string(""),
});
let one_column_with_empty_string_and_one_value_with_a_string =
Value::test_record(record! {
"" => Value::test_string("text"),
});
assert!(no_columns_nor_cell_values.is_empty());
assert!(!one_column_and_one_cell_value_with_empty_strings.is_empty());
assert!(!one_column_with_a_string_and_one_cell_value_with_empty_string.is_empty());
assert!(!one_column_with_empty_string_and_one_value_with_a_string.is_empty());
}
}
mod get_type {
use crate::Type;
use super::*;
#[test]
fn test_list() {
let list_of_ints = Value::test_list(vec![Value::test_int(0)]);
let list_of_floats = Value::test_list(vec![Value::test_float(0.0)]);
let list_of_ints_and_floats =
Value::test_list(vec![Value::test_int(0), Value::test_float(0.0)]);
let list_of_ints_and_floats_and_bools = Value::test_list(vec![
Value::test_int(0),
Value::test_float(0.0),
Value::test_bool(false),
]);
assert_eq!(list_of_ints.get_type(), Type::List(Box::new(Type::Int)));
assert_eq!(list_of_floats.get_type(), Type::List(Box::new(Type::Float)));
assert_eq!(
list_of_ints_and_floats_and_bools.get_type(),
Type::List(Box::new(Type::OneOf(
vec![Type::Number, Type::Bool].into_boxed_slice()
)))
);
assert_eq!(
list_of_ints_and_floats.get_type(),
Type::List(Box::new(Type::Number))
);
}
}
mod is_subtype {
use crate::Type;
use super::*;
fn assert_subtype_equivalent(value: &Value, ty: &Type) {
assert_eq!(value.is_subtype_of(ty), value.get_type().is_subtype_of(ty));
}
#[test]
fn test_list() {
let ty_int_list = Type::list(Type::Int);
let ty_str_list = Type::list(Type::String);
let ty_any_list = Type::list(Type::Any);
let ty_list_list_int = Type::list(Type::list(Type::Int));
let list = Value::test_list(vec![
Value::test_int(1),
Value::test_int(2),
Value::test_int(3),
]);
assert_subtype_equivalent(&list, &ty_int_list);
assert_subtype_equivalent(&list, &ty_str_list);
assert_subtype_equivalent(&list, &ty_any_list);
let list = Value::test_list(vec![
Value::test_int(1),
Value::test_string("hi"),
Value::test_int(3),
]);
assert_subtype_equivalent(&list, &ty_int_list);
assert_subtype_equivalent(&list, &ty_str_list);
assert_subtype_equivalent(&list, &ty_any_list);
let list = Value::test_list(vec![Value::test_list(vec![Value::test_int(1)])]);
assert_subtype_equivalent(&list, &ty_list_list_int);
let ty_table = Type::Table(Box::new([
("a".into(), Type::Int),
("b".into(), Type::Int),
("c".into(), Type::Int),
]));
let empty = Value::test_list(vec![]);
assert_subtype_equivalent(&empty, &ty_any_list);
assert!(empty.is_subtype_of(&ty_int_list));
assert!(empty.is_subtype_of(&ty_table));
}
#[test]
fn test_record() {
let ty_abc = Type::Record(Box::new([
("a".into(), Type::Int),
("b".into(), Type::Int),
("c".into(), Type::Int),
]));
let ty_ab = Type::Record(Box::new([("a".into(), Type::Int), ("b".into(), Type::Int)]));
let ty_inner = Type::Record(Box::new([("inner".into(), ty_abc.clone())]));
let record_abc = Value::test_record(record! {
"a" => Value::test_int(1),
"b" => Value::test_int(2),
"c" => Value::test_int(3),
});
let record_ab = Value::test_record(record! {
"a" => Value::test_int(1),
"b" => Value::test_int(2),
});
assert_subtype_equivalent(&record_abc, &ty_abc);
assert_subtype_equivalent(&record_abc, &ty_ab);
assert_subtype_equivalent(&record_ab, &ty_abc);
assert_subtype_equivalent(&record_ab, &ty_ab);
let record_inner = Value::test_record(record! {
"inner" => record_abc
});
assert_subtype_equivalent(&record_inner, &ty_inner);
}
#[test]
fn test_table() {
let ty_abc = Type::Table(Box::new([
("a".into(), Type::Int),
("b".into(), Type::Int),
("c".into(), Type::Int),
]));
let ty_ab = Type::Table(Box::new([("a".into(), Type::Int), ("b".into(), Type::Int)]));
let ty_list_any = Type::list(Type::Any);
let record_abc = Value::test_record(record! {
"a" => Value::test_int(1),
"b" => Value::test_int(2),
"c" => Value::test_int(3),
});
let record_ab = Value::test_record(record! {
"a" => Value::test_int(1),
"b" => Value::test_int(2),
});
let table_abc = Value::test_list(vec![record_abc.clone(), record_abc.clone()]);
let table_ab = Value::test_list(vec![record_ab.clone(), record_ab.clone()]);
assert_subtype_equivalent(&table_abc, &ty_abc);
assert_subtype_equivalent(&table_abc, &ty_ab);
assert_subtype_equivalent(&table_ab, &ty_abc);
assert_subtype_equivalent(&table_ab, &ty_ab);
assert_subtype_equivalent(&table_abc, &ty_list_any);
let table_mixed = Value::test_list(vec![record_abc.clone(), record_ab.clone()]);
assert_subtype_equivalent(&table_mixed, &ty_abc);
assert!(table_mixed.is_subtype_of(&ty_ab));
let ty_a = Type::Table(Box::new([("a".into(), Type::Any)]));
let table_mixed_types = Value::test_list(vec![
Value::test_record(record! {
"a" => Value::test_int(1),
}),
Value::test_record(record! {
"a" => Value::test_string("a"),
}),
]);
assert!(table_mixed_types.is_subtype_of(&ty_a));
}
}
mod into_string {
use chrono::{DateTime, FixedOffset};
use super::*;
#[test]
fn test_datetime() {
let date = DateTime::from_timestamp_millis(-123456789)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap());
let string = Value::test_date(date).to_expanded_string("", &Default::default());
let formatted = string.split('(').next().unwrap();
assert_eq!("Tue, 30 Dec 1969 13:42:23 +0000 ", formatted);
}
#[test]
fn test_negative_year_datetime() {
let date = DateTime::from_timestamp_millis(-72135596800000)
.unwrap()
.with_timezone(&FixedOffset::east_opt(0).unwrap());
let string = Value::test_date(date).to_expanded_string("", &Default::default());
let formatted = string.split(' ').next().unwrap();
assert_eq!("-0316-02-11T06:13:20+00:00", formatted);
}
}
#[test]
fn test_env_as_bool() {
assert_eq!(Value::test_bool(false).coerce_bool(), Ok(false));
assert_eq!(Value::test_int(0).coerce_bool(), Ok(false));
assert_eq!(Value::test_float(0.0).coerce_bool(), Ok(false));
assert_eq!(Value::test_string("").coerce_bool(), Ok(false));
assert_eq!(Value::test_string("0").coerce_bool(), Ok(false));
assert_eq!(Value::test_nothing().coerce_bool(), Ok(false));
assert_eq!(Value::test_bool(true).coerce_bool(), Ok(true));
assert_eq!(Value::test_int(1).coerce_bool(), Ok(true));
assert_eq!(Value::test_float(1.0).coerce_bool(), Ok(true));
assert_eq!(Value::test_string("1").coerce_bool(), Ok(true));
assert_eq!(Value::test_int(42).coerce_bool(), Ok(true));
assert_eq!(Value::test_float(0.5).coerce_bool(), Ok(true));
assert_eq!(Value::test_string("not zero").coerce_bool(), Ok(true));
assert!(Value::test_record(Record::default()).coerce_bool().is_err());
assert!(
Value::test_list(vec![Value::test_int(1)])
.coerce_bool()
.is_err()
);
assert!(
Value::test_date(
chrono::DateTime::parse_from_rfc3339("2024-01-01T12:00:00+00:00").unwrap(),
)
.coerce_bool()
.is_err()
);
assert!(Value::test_glob("*.rs").coerce_bool().is_err());
assert!(Value::test_binary(vec![1, 2, 3]).coerce_bool().is_err());
assert!(Value::test_duration(3600).coerce_bool().is_err());
}
mod memory_size {
use super::*;
#[test]
fn test_primitive_sizes() {
let base_size = std::mem::size_of::<Value>();
assert_eq!(Value::test_bool(true).memory_size(), base_size);
assert_eq!(Value::test_int(42).memory_size(), base_size);
assert_eq!(Value::test_float(1.5).memory_size(), base_size);
assert_eq!(Value::test_nothing().memory_size(), base_size);
}
#[test]
fn test_string_size() {
let s = "hello world";
let val = Value::test_string(s);
let base_size = std::mem::size_of::<Value>();
let string_val = String::from(s);
let expected = base_size + string_val.capacity();
assert_eq!(val.memory_size(), expected);
}
#[test]
fn test_binary_size() {
let data = vec![1, 2, 3, 4, 5];
let val = Value::test_binary(data.clone());
let base_size = std::mem::size_of::<Value>();
let expected = base_size + data.capacity();
assert_eq!(val.memory_size(), expected);
}
#[test]
fn test_list_size() {
let list = Value::test_list(vec![
Value::test_int(1),
Value::test_int(2),
Value::test_int(3),
]);
let base_size = std::mem::size_of::<Value>();
let element_size = std::mem::size_of::<Value>();
let expected = base_size + 3 * element_size;
assert_eq!(list.memory_size(), expected);
}
#[test]
fn test_record_size() {
let record = Value::test_record(record! {
"a" => Value::test_int(1),
"b" => Value::test_string("hello"),
});
let base_size = std::mem::size_of::<Value>();
let record_base_size = std::mem::size_of::<Record>();
let key1_size = String::from("a").capacity();
let key2_size = String::from("b").capacity();
let val1_size = std::mem::size_of::<Value>();
let val2_base_size = std::mem::size_of::<Value>();
let val2_string_size = String::from("hello").capacity();
let expected = base_size
+ record_base_size
+ key1_size
+ key2_size
+ val1_size
+ (val2_base_size + val2_string_size);
assert_eq!(record.memory_size(), expected);
}
#[test]
fn test_nested_structure_size() {
let inner_record = Value::test_record(record! {
"x" => Value::test_int(10),
"y" => Value::test_string("test"),
});
let list = Value::test_list(vec![inner_record]);
let record_size = list.memory_size();
let base_size = std::mem::size_of::<Value>();
assert!(record_size > base_size);
let simple_list = Value::test_list(vec![Value::test_int(1)]);
assert!(record_size > simple_list.memory_size());
}
}
}