forked from extern/nushell
# Description Apparently some strftime formats are already localized and when you "double localize" them, they don't work. This PR fixes that so that %x %X %r %c don't go through the localization step. Example: %x %X ### Before ```nushell ❯ date now | format date "%x %X %p" 09/08/2023 08 AM ``` ### After ```nushell ❯ date now | format date "%x %X %p" 09/08/23 08:09:14 AM ``` I started to make one format_datetime to rule them all but one returns a string and one returns a value. If we convert to the string, we lose the nice error messages. If we change to value, more code has to be changed elsewhere. So, I decided to just leave two functions. # User-Facing Changes <!-- List of all changes that impact the user experience here. This helps us keep track of breaking changes. --> # Tests + Formatting <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> # After Submitting <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. -->
3998 lines
162 KiB
Rust
3998 lines
162 KiB
Rust
mod custom_value;
|
|
mod from;
|
|
mod from_value;
|
|
mod lazy_record;
|
|
mod range;
|
|
mod record;
|
|
mod stream;
|
|
mod unit;
|
|
|
|
use crate::ast::{Bits, Boolean, CellPath, Comparison, MatchPattern, PathMember};
|
|
use crate::ast::{Math, Operator};
|
|
use crate::engine::EngineState;
|
|
use crate::ShellError;
|
|
use crate::{did_you_mean, BlockId, Config, Span, Spanned, Type, VarId};
|
|
|
|
use byte_unit::ByteUnit;
|
|
use chrono::{DateTime, Datelike, Duration, FixedOffset, Locale, TimeZone};
|
|
use chrono_humanize::HumanTime;
|
|
pub use custom_value::CustomValue;
|
|
use fancy_regex::Regex;
|
|
pub use from_value::FromValue;
|
|
pub use lazy_record::LazyRecord;
|
|
use nu_utils::get_system_locale;
|
|
use nu_utils::locale::get_system_locale_string;
|
|
use num_format::ToFormattedString;
|
|
pub use range::*;
|
|
pub use record::Record;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
use std::fmt::Write;
|
|
use std::{
|
|
borrow::Cow,
|
|
fmt::{Display, Formatter, Result as FmtResult},
|
|
path::PathBuf,
|
|
{cmp::Ordering, fmt::Debug},
|
|
};
|
|
pub use stream::*;
|
|
pub use unit::*;
|
|
|
|
/// Core structured values that pass through the pipeline in Nushell.
|
|
// NOTE: Please do not reorder these enum cases without thinking through the
|
|
// impact on the PartialOrd implementation and the global sort order
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub enum Value {
|
|
Bool {
|
|
val: bool,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Int {
|
|
val: i64,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Float {
|
|
val: f64,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Filesize {
|
|
val: i64,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Duration {
|
|
val: i64,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Date {
|
|
val: DateTime<FixedOffset>,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Range {
|
|
val: Box<Range>,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
String {
|
|
val: String,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Record {
|
|
val: Record,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
List {
|
|
vals: Vec<Value>,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Block {
|
|
val: BlockId,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Closure {
|
|
val: BlockId,
|
|
captures: HashMap<VarId, Value>,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Nothing {
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Error {
|
|
error: Box<ShellError>,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
Binary {
|
|
val: Vec<u8>,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
CellPath {
|
|
val: CellPath,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
#[serde(skip_serializing)]
|
|
CustomValue {
|
|
val: Box<dyn CustomValue>,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
#[serde(skip)]
|
|
LazyRecord {
|
|
val: Box<dyn for<'a> LazyRecord<'a>>,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
MatchPattern {
|
|
val: Box<MatchPattern>,
|
|
// note: spans are being refactored out of Value
|
|
// please use .span() instead of matching this span value
|
|
internal_span: Span,
|
|
},
|
|
}
|
|
|
|
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, internal_span } => Value::Range {
|
|
val: val.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::Record { val, internal_span } => Value::Record {
|
|
val: val.clone(),
|
|
internal_span: *internal_span,
|
|
},
|
|
Value::LazyRecord { val, internal_span } => val.clone_value(*internal_span),
|
|
Value::List {
|
|
vals,
|
|
internal_span,
|
|
} => Value::List {
|
|
vals: vals.clone(),
|
|
internal_span: *internal_span,
|
|
},
|
|
Value::Block { val, internal_span } => Value::Block {
|
|
val: *val,
|
|
internal_span: *internal_span,
|
|
},
|
|
Value::Closure {
|
|
val,
|
|
captures,
|
|
internal_span,
|
|
} => Value::Closure {
|
|
val: *val,
|
|
captures: captures.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::CustomValue { val, internal_span } => val.clone_value(*internal_span),
|
|
Value::MatchPattern { val, internal_span } => Value::MatchPattern {
|
|
val: val.clone(),
|
|
internal_span: *internal_span,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Value {
|
|
pub fn as_bool(&self) -> Result<bool, ShellError> {
|
|
match self {
|
|
Value::Bool { val, .. } => Ok(*val),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "boolean".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_int(&self) -> Result<i64, ShellError> {
|
|
match self {
|
|
Value::Int { val, .. } => Ok(*val),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "integer".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_float(&self) -> Result<f64, ShellError> {
|
|
match self {
|
|
Value::Float { val, .. } => Ok(*val),
|
|
Value::Int { val, .. } => Ok(*val as f64),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "float".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_filesize(&self) -> Result<i64, ShellError> {
|
|
match self {
|
|
Value::Filesize { val, .. } => Ok(*val),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "filesize".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_duration(&self) -> Result<i64, ShellError> {
|
|
match self {
|
|
Value::Duration { val, .. } => Ok(*val),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "duration".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_date(&self) -> Result<DateTime<FixedOffset>, ShellError> {
|
|
match self {
|
|
Value::Date { val, .. } => Ok(*val),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "date".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_range(&self) -> Result<&Range, ShellError> {
|
|
match self {
|
|
Value::Range { val, .. } => Ok(val.as_ref()),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "range".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Converts into string values that can be changed into string natively
|
|
pub fn as_string(&self) -> Result<String, ShellError> {
|
|
match self {
|
|
Value::Int { val, .. } => Ok(val.to_string()),
|
|
Value::Float { val, .. } => Ok(val.to_string()),
|
|
Value::String { val, .. } => Ok(val.to_string()),
|
|
Value::Binary { val, .. } => Ok(match std::str::from_utf8(val) {
|
|
Ok(s) => s.to_string(),
|
|
Err(_) => {
|
|
return Err(ShellError::CantConvert {
|
|
to_type: "string".into(),
|
|
from_type: "binary".into(),
|
|
span: self.span(),
|
|
help: None,
|
|
});
|
|
}
|
|
}),
|
|
Value::Date { val, .. } => Ok(val.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "string".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_spanned_string(&self) -> Result<Spanned<String>, ShellError> {
|
|
let span = self.span();
|
|
match self {
|
|
Value::String { val, .. } => Ok(Spanned {
|
|
item: val.to_string(),
|
|
span,
|
|
}),
|
|
Value::Binary { val, .. } => Ok(match std::str::from_utf8(val) {
|
|
Ok(s) => Spanned {
|
|
item: s.to_string(),
|
|
span,
|
|
},
|
|
Err(_) => {
|
|
return Err(ShellError::CantConvert {
|
|
to_type: "string".into(),
|
|
from_type: "binary".into(),
|
|
span: self.span(),
|
|
help: None,
|
|
})
|
|
}
|
|
}),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "string".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_char(&self) -> Result<char, ShellError> {
|
|
let span = self.span();
|
|
|
|
match self {
|
|
Value::String { val, .. } => {
|
|
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,
|
|
}),
|
|
}
|
|
}
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "char".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span,
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_path(&self) -> Result<PathBuf, ShellError> {
|
|
match self {
|
|
Value::String { val, .. } => Ok(PathBuf::from(val)),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "path".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_record(&self) -> Result<&Record, ShellError> {
|
|
match self {
|
|
Value::Record { val, .. } => Ok(val),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "record".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_list(&self) -> Result<&[Value], ShellError> {
|
|
match self {
|
|
Value::List { vals, .. } => Ok(vals),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "list".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_block(&self) -> Result<BlockId, ShellError> {
|
|
match self {
|
|
Value::Block { val, .. } => Ok(*val),
|
|
Value::Closure { val, .. } => Ok(*val),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "block".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_closure(&self) -> Result<(BlockId, &HashMap<VarId, Value>), ShellError> {
|
|
match self {
|
|
Value::Closure { val, captures, .. } => Ok((*val, captures)),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "closure".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_binary(&self) -> Result<&[u8], ShellError> {
|
|
match self {
|
|
Value::Binary { val, .. } => Ok(val),
|
|
Value::String { val, .. } => Ok(val.as_bytes()),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "binary".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_cell_path(&self) -> Result<&CellPath, ShellError> {
|
|
match self {
|
|
Value::CellPath { val, .. } => Ok(val),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "cell path".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_custom_value(&self) -> Result<&dyn CustomValue, ShellError> {
|
|
match self {
|
|
Value::CustomValue { val, .. } => Ok(val.as_ref()),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "custom value".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_lazy_record(&self) -> Result<&dyn for<'a> LazyRecord<'a>, ShellError> {
|
|
match self {
|
|
Value::LazyRecord { val, .. } => Ok(val.as_ref()),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "lazy record".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn as_match_pattern(&self) -> Result<&MatchPattern, ShellError> {
|
|
match self {
|
|
Value::MatchPattern { val, .. } => Ok(val.as_ref()),
|
|
x => Err(ShellError::CantConvert {
|
|
to_type: "match-pattern".into(),
|
|
from_type: x.get_type().to_string(),
|
|
span: self.span(),
|
|
help: None,
|
|
}),
|
|
}
|
|
}
|
|
|
|
/// Get the span for the current 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::Record { internal_span, .. }
|
|
| Value::List { internal_span, .. }
|
|
| Value::Block { internal_span, .. }
|
|
| Value::Closure { internal_span, .. }
|
|
| Value::Nothing { internal_span, .. }
|
|
| Value::Binary { internal_span, .. }
|
|
| Value::CellPath { internal_span, .. }
|
|
| Value::CustomValue { internal_span, .. }
|
|
| Value::LazyRecord { internal_span, .. }
|
|
| Value::MatchPattern { internal_span, .. }
|
|
| Value::Error { internal_span, .. } => *internal_span,
|
|
}
|
|
}
|
|
|
|
/// Update the value with a new span
|
|
pub fn with_span(mut self, new_span: Span) -> Value {
|
|
match &mut 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::Record { internal_span, .. }
|
|
| Value::LazyRecord { internal_span, .. }
|
|
| Value::List { internal_span, .. }
|
|
| Value::Closure { internal_span, .. }
|
|
| Value::Block { internal_span, .. }
|
|
| Value::Nothing { internal_span, .. }
|
|
| Value::Binary { internal_span, .. }
|
|
| Value::CellPath { internal_span, .. }
|
|
| Value::CustomValue { internal_span, .. }
|
|
| Value::MatchPattern { internal_span, .. } => *internal_span = new_span,
|
|
Value::Error { .. } => (),
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
/// Get the type of the current Value
|
|
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::Record { val, .. } => {
|
|
Type::Record(val.iter().map(|(x, y)| (x.clone(), y.get_type())).collect())
|
|
}
|
|
Value::List { vals, .. } => {
|
|
let mut ty = None;
|
|
for val in vals {
|
|
let val_ty = val.get_type();
|
|
match &ty {
|
|
Some(x) => {
|
|
if &val_ty != x {
|
|
if x.is_numeric() && val_ty.is_numeric() {
|
|
ty = Some(Type::Number)
|
|
} else {
|
|
ty = Some(Type::Any);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
None => ty = Some(val_ty),
|
|
}
|
|
}
|
|
|
|
match ty {
|
|
Some(Type::Record(columns)) => Type::Table(columns),
|
|
Some(ty) => Type::List(Box::new(ty)),
|
|
None => Type::List(Box::new(Type::Any)),
|
|
}
|
|
}
|
|
Value::LazyRecord { val, .. } => match val.collect() {
|
|
Ok(val) => val.get_type(),
|
|
Err(..) => Type::Error,
|
|
},
|
|
Value::Nothing { .. } => Type::Nothing,
|
|
Value::Block { .. } => Type::Block,
|
|
Value::Closure { .. } => Type::Closure,
|
|
Value::Error { .. } => Type::Error,
|
|
Value::Binary { .. } => Type::Binary,
|
|
Value::CellPath { .. } => Type::CellPath,
|
|
Value::CustomValue { val, .. } => Type::Custom(val.typetag_name().into()),
|
|
Value::MatchPattern { .. } => Type::MatchPattern,
|
|
}
|
|
}
|
|
|
|
pub fn get_data_by_key(&self, name: &str) -> Option<Value> {
|
|
let span = self.span();
|
|
match self {
|
|
Value::Record { val, .. } => val
|
|
.iter()
|
|
.find(|(col, _)| col == &name)
|
|
.map(|(_, val)| val.clone()),
|
|
Value::List { vals, .. } => {
|
|
let mut out = vec![];
|
|
for item in vals {
|
|
match item {
|
|
Value::Record { .. } => match item.get_data_by_key(name) {
|
|
Some(v) => out.push(v),
|
|
None => out.push(Value::nothing(span)),
|
|
},
|
|
_ => out.push(Value::nothing(span)),
|
|
}
|
|
}
|
|
|
|
if !out.is_empty() {
|
|
Some(Value::list(out, span))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
// Convert Value into String, but propagate errors.
|
|
pub fn nonerror_into_string(
|
|
&self,
|
|
separator: &str,
|
|
config: &Config,
|
|
) -> Result<String, ShellError> {
|
|
if let Value::Error { error, .. } = self {
|
|
Err(*error.to_owned())
|
|
} else {
|
|
Ok(self.into_string(separator, config))
|
|
}
|
|
}
|
|
|
|
/// Convert Value into string. Note that Streams will be consumed.
|
|
pub fn into_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, .. } => val.to_string(),
|
|
Value::Filesize { val, .. } => format_filesize_from_conf(*val, config),
|
|
Value::Duration { val, .. } => format_duration(*val),
|
|
|
|
Value::Date { val, .. } => match &config.datetime_normal_format {
|
|
Some(format) => self.format_datetime(val, format),
|
|
None => {
|
|
format!(
|
|
"{} ({})",
|
|
if val.year() >= 0 {
|
|
val.to_rfc2822()
|
|
} else {
|
|
val.to_rfc3339()
|
|
},
|
|
HumanTime::from(*val),
|
|
)
|
|
}
|
|
},
|
|
Value::Range { val, .. } => {
|
|
format!(
|
|
"{}..{}",
|
|
val.from.into_string(", ", config),
|
|
val.to.into_string(", ", config)
|
|
)
|
|
}
|
|
Value::String { val, .. } => val.clone(),
|
|
Value::List { vals: val, .. } => format!(
|
|
"[{}]",
|
|
val.iter()
|
|
.map(|x| x.into_string(", ", config))
|
|
.collect::<Vec<_>>()
|
|
.join(separator)
|
|
),
|
|
Value::Record { val, .. } => format!(
|
|
"{{{}}}",
|
|
val.iter()
|
|
.map(|(x, y)| format!("{}: {}", x, y.into_string(", ", config)))
|
|
.collect::<Vec<_>>()
|
|
.join(separator)
|
|
),
|
|
Value::LazyRecord { val, .. } => {
|
|
let collected = match val.collect() {
|
|
Ok(val) => val,
|
|
Err(error) => Value::Error {
|
|
error: Box::new(error),
|
|
internal_span: span,
|
|
},
|
|
};
|
|
collected.into_string(separator, config)
|
|
}
|
|
Value::Block { val, .. } => format!("<Block {val}>"),
|
|
Value::Closure { val, .. } => format!("<Closure {val}>"),
|
|
Value::Nothing { .. } => String::new(),
|
|
Value::Error { error, .. } => format!("{error:?}"),
|
|
Value::Binary { val, .. } => format!("{val:?}"),
|
|
Value::CellPath { val, .. } => val.into_string(),
|
|
Value::CustomValue { val, .. } => val.value_string(),
|
|
Value::MatchPattern { val, .. } => format!("<Pattern: {:?}>", val),
|
|
}
|
|
}
|
|
|
|
/// Convert Value into string. Note that Streams will be consumed.
|
|
pub fn into_abbreviated_string(&self, config: &Config) -> String {
|
|
match self {
|
|
Value::Bool { val, .. } => val.to_string(),
|
|
Value::Int { val, .. } => val.to_string(),
|
|
Value::Float { val, .. } => val.to_string(),
|
|
Value::Filesize { val, .. } => format_filesize_from_conf(*val, config),
|
|
Value::Duration { val, .. } => format_duration(*val),
|
|
Value::Date { val, .. } => match &config.datetime_table_format {
|
|
Some(format) => self.format_datetime(val, format),
|
|
None => HumanTime::from(*val).to_string(),
|
|
},
|
|
Value::Range { val, .. } => {
|
|
format!(
|
|
"{}..{}",
|
|
val.from.into_string(", ", config),
|
|
val.to.into_string(", ", config)
|
|
)
|
|
}
|
|
Value::String { val, .. } => val.to_string(),
|
|
Value::List { ref 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" }
|
|
),
|
|
Value::LazyRecord { val, .. } => match val.collect() {
|
|
Ok(val) => val.into_abbreviated_string(config),
|
|
Err(error) => format!("{error:?}"),
|
|
},
|
|
Value::Block { val, .. } => format!("<Block {val}>"),
|
|
Value::Closure { val, .. } => format!("<Closure {val}>"),
|
|
Value::Nothing { .. } => String::new(),
|
|
Value::Error { error, .. } => format!("{error:?}"),
|
|
Value::Binary { val, .. } => format!("{val:?}"),
|
|
Value::CellPath { val, .. } => val.into_string(),
|
|
Value::CustomValue { val, .. } => val.value_string(),
|
|
Value::MatchPattern { .. } => "<Pattern>".into(),
|
|
}
|
|
}
|
|
|
|
fn format_datetime<Tz: TimeZone>(&self, date_time: &DateTime<Tz>, formatter: &str) -> String
|
|
where
|
|
Tz::Offset: Display,
|
|
{
|
|
let mut formatter_buf = String::new();
|
|
// These are already in locale format, so we don't need to localize them
|
|
let format = if ["%x", "%X", "%r"]
|
|
.iter()
|
|
.any(|item| formatter.contains(item))
|
|
{
|
|
date_time.format(formatter)
|
|
} else {
|
|
let locale: Locale = get_system_locale_string()
|
|
.map(|l| l.replace('-', "_")) // `chrono::Locale` needs something like `xx_xx`, rather than `xx-xx`
|
|
.unwrap_or_else(|| String::from("en_US"))
|
|
.as_str()
|
|
.try_into()
|
|
.unwrap_or(Locale::en_US);
|
|
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
|
|
}
|
|
|
|
/// Convert Value into a debug string
|
|
pub fn debug_value(&self) -> String {
|
|
format!("{self:#?}")
|
|
}
|
|
|
|
/// Convert Value into a parsable string (quote strings)
|
|
/// bugbug other, rarer types not handled
|
|
|
|
pub fn into_string_parsable(&self, separator: &str, config: &Config) -> String {
|
|
match self {
|
|
// give special treatment to the simple types to make them parsable
|
|
Value::String { val, .. } => format!("'{}'", val),
|
|
|
|
// recurse back into this function for recursive formatting
|
|
Value::List { vals: val, .. } => format!(
|
|
"[{}]",
|
|
val.iter()
|
|
.map(|x| x.into_string_parsable(", ", config))
|
|
.collect::<Vec<_>>()
|
|
.join(separator)
|
|
),
|
|
Value::Record { val, .. } => format!(
|
|
"{{{}}}",
|
|
val.iter()
|
|
.map(|(x, y)| format!("{}: {}", x, y.into_string_parsable(", ", config)))
|
|
.collect::<Vec<_>>()
|
|
.join(separator)
|
|
),
|
|
|
|
// defer to standard handling for types where standard representation is parsable
|
|
_ => self.into_string(separator, config),
|
|
}
|
|
}
|
|
|
|
/// Convert Value into string. Note that Streams will be consumed.
|
|
pub fn debug_string(&self, separator: &str, config: &Config) -> String {
|
|
match self {
|
|
Value::Bool { val, .. } => val.to_string(),
|
|
Value::Int { val, .. } => val.to_string(),
|
|
Value::Float { val, .. } => val.to_string(),
|
|
Value::Filesize { val, .. } => format_filesize_from_conf(*val, config),
|
|
Value::Duration { val, .. } => format_duration(*val),
|
|
Value::Date { val, .. } => format!("{val:?}"),
|
|
Value::Range { val, .. } => {
|
|
format!(
|
|
"{}..{}",
|
|
val.from.into_string(", ", config),
|
|
val.to.into_string(", ", config)
|
|
)
|
|
}
|
|
Value::String { val, .. } => val.clone(),
|
|
Value::List { vals: val, .. } => format!(
|
|
"[{}]",
|
|
val.iter()
|
|
.map(|x| x.into_string(", ", config))
|
|
.collect::<Vec<_>>()
|
|
.join(separator)
|
|
),
|
|
Value::Record { val, .. } => format!(
|
|
"{{{}}}",
|
|
val.iter()
|
|
.map(|(x, y)| format!("{}: {}", x, y.into_string(", ", config)))
|
|
.collect::<Vec<_>>()
|
|
.join(separator)
|
|
),
|
|
Value::LazyRecord { val, .. } => match val.collect() {
|
|
Ok(val) => val.debug_string(separator, config),
|
|
Err(error) => format!("{error:?}"),
|
|
},
|
|
Value::Block { val, .. } => format!("<Block {val}>"),
|
|
Value::Closure { val, .. } => format!("<Closure {val}>"),
|
|
Value::Nothing { .. } => String::new(),
|
|
Value::Error { error, .. } => format!("{error:?}"),
|
|
Value::Binary { val, .. } => format!("{val:?}"),
|
|
Value::CellPath { val, .. } => val.into_string(),
|
|
Value::CustomValue { val, .. } => val.value_string(),
|
|
Value::MatchPattern { val, .. } => format!("<Pattern {:?}>", val),
|
|
}
|
|
}
|
|
|
|
/// Check if the content is empty
|
|
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 { .. })
|
|
}
|
|
|
|
/// Follow a given cell path into the value: for example accessing select elements in a stream or list
|
|
pub fn follow_cell_path(
|
|
self,
|
|
cell_path: &[PathMember],
|
|
insensitive: bool,
|
|
) -> Result<Value, ShellError> {
|
|
self.follow_cell_path_helper(cell_path, insensitive, true)
|
|
}
|
|
|
|
pub fn follow_cell_path_not_from_user_input(
|
|
self,
|
|
cell_path: &[PathMember],
|
|
insensitive: bool,
|
|
) -> Result<Value, ShellError> {
|
|
self.follow_cell_path_helper(cell_path, insensitive, false)
|
|
}
|
|
|
|
fn follow_cell_path_helper(
|
|
self,
|
|
cell_path: &[PathMember],
|
|
insensitive: bool,
|
|
from_user_input: bool,
|
|
) -> Result<Value, ShellError> {
|
|
let mut current = self;
|
|
|
|
for member in cell_path {
|
|
// FIXME: this uses a few extra clones for simplicity, but there may be a way
|
|
// to traverse the path without them
|
|
match member {
|
|
PathMember::Int {
|
|
val: count,
|
|
span: origin_span,
|
|
optional,
|
|
} => {
|
|
// Treat a numeric path member as `select <val>`
|
|
match &mut current {
|
|
Value::List { vals: val, .. } => {
|
|
if let Some(item) = val.get(*count) {
|
|
current = item.clone();
|
|
} else if *optional {
|
|
return Ok(Value::nothing(*origin_span)); // short-circuit
|
|
} else if val.is_empty() {
|
|
return Err(ShellError::AccessEmptyContent { span: *origin_span })
|
|
} else {
|
|
return Err(ShellError::AccessBeyondEnd { max_idx:val.len()-1,span: *origin_span });
|
|
}
|
|
}
|
|
Value::Binary { val, .. } => {
|
|
if let Some(item) = val.get(*count) {
|
|
current = Value::int(*item as i64, *origin_span);
|
|
} else if *optional {
|
|
return Ok(Value::nothing(*origin_span)); // short-circuit
|
|
} else if val.is_empty() {
|
|
return Err(ShellError::AccessEmptyContent { span: *origin_span })
|
|
} else {
|
|
return Err(ShellError::AccessBeyondEnd { max_idx:val.len()-1,span: *origin_span });
|
|
}
|
|
}
|
|
Value::Range { val, .. } => {
|
|
if let Some(item) = val.clone().into_range_iter(None)?.nth(*count) {
|
|
current = item.clone();
|
|
} else if *optional {
|
|
return Ok(Value::nothing(*origin_span)); // short-circuit
|
|
} else {
|
|
return Err(ShellError::AccessBeyondEndOfStream {
|
|
span: *origin_span
|
|
});
|
|
}
|
|
}
|
|
Value::CustomValue { val, .. } => {
|
|
current = match val.follow_path_int(*count, *origin_span) {
|
|
Ok(val) => val,
|
|
Err(err) => {
|
|
if *optional {
|
|
return Ok(Value::nothing(*origin_span));
|
|
// short-circuit
|
|
} else {
|
|
return Err(err);
|
|
}
|
|
}
|
|
};
|
|
}
|
|
Value::Nothing { .. } if *optional => {
|
|
return Ok(Value::nothing(*origin_span)); // short-circuit
|
|
}
|
|
// Records (and tables) are the only built-in which support column names,
|
|
// so only use this message for them.
|
|
Value::Record { .. } => {
|
|
return 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, .. } => return Err(*error.to_owned()),
|
|
x => {
|
|
return Err(ShellError::IncompatiblePathAccess { type_name:format!("{}",x.get_type()), span: *origin_span })
|
|
}
|
|
}
|
|
}
|
|
PathMember::String {
|
|
val: column_name,
|
|
span: origin_span,
|
|
optional,
|
|
} => {
|
|
let span = current.span();
|
|
|
|
match &mut current {
|
|
Value::Record { val, .. } => {
|
|
// Make reverse iterate to avoid duplicate column leads to first value, actually last value is expected.
|
|
if let Some(found) = val.iter().rev().find(|x| {
|
|
if insensitive {
|
|
x.0.to_lowercase() == column_name.to_lowercase()
|
|
} else {
|
|
x.0 == column_name
|
|
}
|
|
}) {
|
|
current = found.1.clone();
|
|
} else if *optional {
|
|
return Ok(Value::nothing(*origin_span)); // short-circuit
|
|
} else {
|
|
if from_user_input {
|
|
if let Some(suggestion) = did_you_mean(&val.cols, column_name) {
|
|
return Err(ShellError::DidYouMean(
|
|
suggestion,
|
|
*origin_span,
|
|
));
|
|
}
|
|
}
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: column_name.to_string(),
|
|
span: *origin_span,
|
|
src_span: span,
|
|
});
|
|
}
|
|
}
|
|
Value::LazyRecord { val, .. } => {
|
|
let columns = val.column_names();
|
|
|
|
if columns.contains(&column_name.as_str()) {
|
|
current = val.get_column_value(column_name)?;
|
|
} else if *optional {
|
|
return Ok(Value::nothing(*origin_span)); // short-circuit
|
|
} else {
|
|
if from_user_input {
|
|
if let Some(suggestion) = did_you_mean(&columns, column_name) {
|
|
return Err(ShellError::DidYouMean(
|
|
suggestion,
|
|
*origin_span,
|
|
));
|
|
}
|
|
}
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: column_name.to_string(),
|
|
span: *origin_span,
|
|
src_span: span,
|
|
});
|
|
}
|
|
}
|
|
// String access of Lists always means Table access.
|
|
// Create a List which contains each matching value for contained
|
|
// records in the source list.
|
|
Value::List { vals, .. } => {
|
|
// TODO: this should stream instead of collecting
|
|
let mut output = vec![];
|
|
for val in vals {
|
|
// only look in records; this avoids unintentionally recursing into deeply nested tables
|
|
if matches!(val, Value::Record { .. }) {
|
|
if let Ok(result) = val.clone().follow_cell_path(
|
|
&[PathMember::String {
|
|
val: column_name.clone(),
|
|
span: *origin_span,
|
|
optional: *optional,
|
|
}],
|
|
insensitive,
|
|
) {
|
|
output.push(result);
|
|
} else {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: column_name.to_string(),
|
|
span: *origin_span,
|
|
src_span: val.span(),
|
|
});
|
|
}
|
|
} else if *optional && matches!(val, Value::Nothing { .. }) {
|
|
output.push(Value::nothing(*origin_span));
|
|
} else {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: column_name.to_string(),
|
|
span: *origin_span,
|
|
src_span: val.span(),
|
|
});
|
|
}
|
|
}
|
|
|
|
current = Value::list(output, span);
|
|
}
|
|
Value::CustomValue { val, .. } => {
|
|
current = val.follow_path_string(column_name.clone(), *origin_span)?;
|
|
}
|
|
Value::Nothing { .. } if *optional => {
|
|
return Ok(Value::nothing(*origin_span)); // short-circuit
|
|
}
|
|
Value::Error { error, .. } => return Err(*error.to_owned()),
|
|
x => {
|
|
return Err(ShellError::IncompatiblePathAccess {
|
|
type_name: format!("{}", x.get_type()),
|
|
span: *origin_span,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// If a single Value::Error was produced by the above (which won't happen if nullify_errors is true), unwrap it now.
|
|
// Note that Value::Errors inside Lists remain as they are, so that the rest of the list can still potentially be used.
|
|
if let Value::Error { error, .. } = current {
|
|
Err(*error)
|
|
} else {
|
|
Ok(current)
|
|
}
|
|
}
|
|
|
|
/// Follow a given cell path into the value: for example accessing select elements in a stream or list
|
|
pub fn upsert_cell_path(
|
|
&mut self,
|
|
cell_path: &[PathMember],
|
|
callback: Box<dyn FnOnce(&Value) -> Value>,
|
|
) -> Result<(), ShellError> {
|
|
let orig = self.clone();
|
|
|
|
let new_val = callback(&orig.follow_cell_path(cell_path, false)?);
|
|
|
|
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> {
|
|
match cell_path.first() {
|
|
Some(path_member) => match path_member {
|
|
PathMember::String {
|
|
val: col_name,
|
|
span,
|
|
..
|
|
} => match self {
|
|
Value::List { vals, .. } => {
|
|
for val in vals.iter_mut() {
|
|
match val {
|
|
Value::Record { val: record, .. } => {
|
|
let mut found = false;
|
|
for (col, val) in record.iter_mut() {
|
|
if col == col_name {
|
|
found = true;
|
|
val.upsert_data_at_cell_path(
|
|
&cell_path[1..],
|
|
new_val.clone(),
|
|
)?
|
|
}
|
|
}
|
|
if !found {
|
|
if cell_path.len() == 1 {
|
|
record.push(col_name, new_val);
|
|
break;
|
|
} else {
|
|
let mut new_col =
|
|
Value::record(Record::new(), new_val.span());
|
|
new_col.upsert_data_at_cell_path(
|
|
&cell_path[1..],
|
|
new_val,
|
|
)?;
|
|
vals.push(new_col);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
Value::Error { error, .. } => return Err(*error.to_owned()),
|
|
v => {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v.span(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Value::Record { val: record, .. } => {
|
|
let mut found = false;
|
|
|
|
for (col, val) in record.iter_mut() {
|
|
if col == col_name {
|
|
found = true;
|
|
val.upsert_data_at_cell_path(&cell_path[1..], new_val.clone())?
|
|
}
|
|
}
|
|
if !found {
|
|
let new_col = if cell_path.len() == 1 {
|
|
new_val
|
|
} else {
|
|
let mut new_col = Value::record(Record::new(), new_val.span());
|
|
new_col.upsert_data_at_cell_path(&cell_path[1..], new_val)?;
|
|
new_col
|
|
};
|
|
|
|
record.push(col_name, new_col);
|
|
}
|
|
}
|
|
Value::LazyRecord { val, .. } => {
|
|
// convert to Record first.
|
|
let mut record = val.collect()?;
|
|
record.upsert_data_at_cell_path(cell_path, new_val)?;
|
|
*self = record
|
|
}
|
|
Value::Error { error, .. } => return Err(*error.to_owned()),
|
|
v => {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v.span(),
|
|
})
|
|
}
|
|
},
|
|
PathMember::Int {
|
|
val: row_num, span, ..
|
|
} => match self {
|
|
Value::List { vals, .. } => {
|
|
if let Some(v) = vals.get_mut(*row_num) {
|
|
v.upsert_data_at_cell_path(&cell_path[1..], new_val)?
|
|
} else if vals.len() == *row_num && cell_path.len() == 1 {
|
|
// If the upsert is at 1 + the end of the list, it's OK.
|
|
// Otherwise, it's prohibited.
|
|
vals.push(new_val);
|
|
} else {
|
|
return Err(ShellError::InsertAfterNextFreeIndex {
|
|
available_idx: vals.len(),
|
|
span: *span,
|
|
});
|
|
}
|
|
}
|
|
Value::Error { error, .. } => return Err(*error.to_owned()),
|
|
v => {
|
|
return Err(ShellError::NotAList {
|
|
dst_span: *span,
|
|
src_span: v.span(),
|
|
})
|
|
}
|
|
},
|
|
},
|
|
None => {
|
|
*self = new_val;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Follow a given cell path into the value: for example accessing select elements in a stream or list
|
|
pub fn update_cell_path<'a>(
|
|
&mut self,
|
|
cell_path: &[PathMember],
|
|
callback: Box<dyn FnOnce(&Value) -> Value + 'a>,
|
|
) -> Result<(), ShellError> {
|
|
let orig = self.clone();
|
|
|
|
let new_val = callback(&orig.follow_cell_path(cell_path, false)?);
|
|
|
|
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> {
|
|
let v_span = self.span();
|
|
|
|
match cell_path.first() {
|
|
Some(path_member) => match path_member {
|
|
PathMember::String {
|
|
val: col_name,
|
|
span,
|
|
..
|
|
} => match self {
|
|
Value::List { vals, .. } => {
|
|
for val in vals.iter_mut() {
|
|
let v_span = val.span();
|
|
match val {
|
|
Value::Record { val: record, .. } => {
|
|
let mut found = false;
|
|
for (col, val) in record.iter_mut() {
|
|
if col == col_name {
|
|
found = true;
|
|
val.update_data_at_cell_path(
|
|
&cell_path[1..],
|
|
new_val.clone(),
|
|
)?
|
|
}
|
|
}
|
|
if !found {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v_span,
|
|
});
|
|
}
|
|
}
|
|
Value::Error { error, .. } => return Err(*error.to_owned()),
|
|
v => {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v.span(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Value::Record { val: record, .. } => {
|
|
let mut found = false;
|
|
|
|
for (col, val) in record.iter_mut() {
|
|
if col == col_name {
|
|
found = true;
|
|
val.update_data_at_cell_path(&cell_path[1..], new_val.clone())?
|
|
}
|
|
}
|
|
if !found {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v_span,
|
|
});
|
|
}
|
|
}
|
|
Value::LazyRecord { val, .. } => {
|
|
// convert to Record first.
|
|
let mut record = val.collect()?;
|
|
record.update_data_at_cell_path(cell_path, new_val)?;
|
|
*self = record
|
|
}
|
|
Value::Error { error, .. } => return Err(*error.to_owned()),
|
|
v => {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v.span(),
|
|
})
|
|
}
|
|
},
|
|
PathMember::Int {
|
|
val: row_num, span, ..
|
|
} => match self {
|
|
Value::List { vals, .. } => {
|
|
if let Some(v) = vals.get_mut(*row_num) {
|
|
v.update_data_at_cell_path(&cell_path[1..], new_val)?
|
|
} else 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.to_owned()),
|
|
v => {
|
|
return Err(ShellError::NotAList {
|
|
dst_span: *span,
|
|
src_span: v.span(),
|
|
})
|
|
}
|
|
},
|
|
},
|
|
None => {
|
|
*self = new_val;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn remove_data_at_cell_path(&mut self, cell_path: &[PathMember]) -> Result<(), ShellError> {
|
|
match cell_path.len() {
|
|
0 => Ok(()),
|
|
1 => {
|
|
let path_member = cell_path.first().expect("there is a first");
|
|
let v_span = self.span();
|
|
match path_member {
|
|
PathMember::String {
|
|
val: col_name,
|
|
span,
|
|
optional,
|
|
} => match self {
|
|
Value::List { vals, .. } => {
|
|
for val in vals.iter_mut() {
|
|
let v_span = val.span();
|
|
|
|
match val {
|
|
Value::Record { val: record, .. } => {
|
|
let mut found = false;
|
|
let mut index = 0;
|
|
record.cols.retain_mut(|col| {
|
|
if col == col_name {
|
|
found = true;
|
|
record.vals.remove(index);
|
|
false
|
|
} else {
|
|
index += 1;
|
|
true
|
|
}
|
|
});
|
|
if !found && !optional {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v_span,
|
|
});
|
|
}
|
|
}
|
|
v => {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v.span(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
Value::Record { val: record, .. } => {
|
|
let mut found = false;
|
|
let mut index = 0;
|
|
record.cols.retain_mut(|col| {
|
|
if col == col_name {
|
|
found = true;
|
|
record.vals.remove(index);
|
|
false
|
|
} else {
|
|
index += 1;
|
|
true
|
|
}
|
|
});
|
|
if !found && !optional {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v_span,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
Value::LazyRecord { val, .. } => {
|
|
// convert to Record first.
|
|
let mut record = val.collect()?;
|
|
record.remove_data_at_cell_path(cell_path)?;
|
|
*self = record;
|
|
Ok(())
|
|
}
|
|
v => Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *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(),
|
|
}),
|
|
},
|
|
}
|
|
}
|
|
_ => {
|
|
let path_member = cell_path.first().expect("there is a first");
|
|
let v_span = self.span();
|
|
match path_member {
|
|
PathMember::String {
|
|
val: col_name,
|
|
span,
|
|
optional,
|
|
} => match self {
|
|
Value::List { vals, .. } => {
|
|
for val in vals.iter_mut() {
|
|
let v_span = val.span();
|
|
match val {
|
|
Value::Record { val: record, .. } => {
|
|
let mut found = false;
|
|
for (col, val) in record.iter_mut() {
|
|
if col == col_name {
|
|
found = true;
|
|
val.remove_data_at_cell_path(&cell_path[1..])?
|
|
}
|
|
}
|
|
if !found && !optional {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v_span,
|
|
});
|
|
}
|
|
}
|
|
v => {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v.span(),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
Value::Record { val: record, .. } => {
|
|
let mut found = false;
|
|
|
|
for (col, val) in record.iter_mut() {
|
|
if col == col_name {
|
|
found = true;
|
|
val.remove_data_at_cell_path(&cell_path[1..])?
|
|
}
|
|
}
|
|
if !found && !optional {
|
|
return Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v_span,
|
|
});
|
|
}
|
|
Ok(())
|
|
}
|
|
Value::LazyRecord { val, .. } => {
|
|
// convert to Record first.
|
|
let mut record = val.collect()?;
|
|
record.remove_data_at_cell_path(cell_path)?;
|
|
*self = record;
|
|
Ok(())
|
|
}
|
|
v => Err(ShellError::CantFindColumn {
|
|
col_name: col_name.to_string(),
|
|
span: *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) {
|
|
v.remove_data_at_cell_path(&cell_path[1..])
|
|
} 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(),
|
|
}),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn insert_data_at_cell_path(
|
|
&mut self,
|
|
cell_path: &[PathMember],
|
|
new_val: Value,
|
|
head_span: Span,
|
|
) -> Result<(), ShellError> {
|
|
let v_span = self.span();
|
|
match cell_path.first() {
|
|
Some(path_member) => match path_member {
|
|
PathMember::String {
|
|
val: col_name,
|
|
span,
|
|
..
|
|
} => match self {
|
|
Value::List { vals, .. } => {
|
|
for val in vals.iter_mut() {
|
|
let v_span = val.span();
|
|
match val {
|
|
Value::Record { val: record, .. } => {
|
|
for (col, val) in record.iter_mut() {
|
|
if col == col_name {
|
|
if cell_path.len() == 1 {
|
|
return Err(ShellError::ColumnAlreadyExists {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v_span,
|
|
});
|
|
} else {
|
|
return val.insert_data_at_cell_path(
|
|
&cell_path[1..],
|
|
new_val,
|
|
head_span,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
record.push(col_name, new_val.clone());
|
|
}
|
|
// SIGH...
|
|
Value::Error { error, .. } => return Err(*error.clone()),
|
|
_ => {
|
|
return Err(ShellError::UnsupportedInput(
|
|
"expected table or record".into(),
|
|
format!("input type: {:?}", val.get_type()),
|
|
head_span,
|
|
*span,
|
|
))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Value::Record { val: record, .. } => {
|
|
for (col, val) in record.iter_mut() {
|
|
if col == col_name {
|
|
if cell_path.len() == 1 {
|
|
return Err(ShellError::ColumnAlreadyExists {
|
|
col_name: col_name.to_string(),
|
|
span: *span,
|
|
src_span: v_span,
|
|
});
|
|
} else {
|
|
return val.insert_data_at_cell_path(
|
|
&cell_path[1..],
|
|
new_val,
|
|
head_span,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
record.push(col_name, new_val);
|
|
}
|
|
Value::LazyRecord { val, .. } => {
|
|
// convert to Record first.
|
|
let mut record = val.collect()?;
|
|
record.insert_data_at_cell_path(cell_path, new_val, v_span)?;
|
|
*self = record
|
|
}
|
|
other => {
|
|
return Err(ShellError::UnsupportedInput(
|
|
"table or record".into(),
|
|
format!("input type: {:?}", other.get_type()),
|
|
head_span,
|
|
*span,
|
|
))
|
|
}
|
|
},
|
|
PathMember::Int {
|
|
val: row_num, span, ..
|
|
} => match self {
|
|
Value::List { vals, .. } => {
|
|
if let Some(v) = vals.get_mut(*row_num) {
|
|
v.insert_data_at_cell_path(&cell_path[1..], new_val, head_span)?
|
|
} else if vals.len() == *row_num && cell_path.len() == 1 {
|
|
// If the insert is at 1 + the end of the list, it's OK.
|
|
// Otherwise, it's prohibited.
|
|
vals.push(new_val);
|
|
} else {
|
|
return Err(ShellError::InsertAfterNextFreeIndex {
|
|
available_idx: vals.len(),
|
|
span: *span,
|
|
});
|
|
}
|
|
}
|
|
v => {
|
|
return Err(ShellError::NotAList {
|
|
dst_span: *span,
|
|
src_span: v.span(),
|
|
})
|
|
}
|
|
},
|
|
},
|
|
None => {
|
|
*self = new_val;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
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) -> &[String] {
|
|
match self {
|
|
Value::Record { val, .. } => &val.cols,
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
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: i64, span: Span) -> Value {
|
|
Value::Filesize {
|
|
val,
|
|
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: Box::new(val),
|
|
internal_span: span,
|
|
}
|
|
}
|
|
|
|
pub fn string(val: impl Into<String>, span: Span) -> Value {
|
|
Value::String {
|
|
val: val.into(),
|
|
internal_span: span,
|
|
}
|
|
}
|
|
|
|
pub fn record(val: Record, span: Span) -> Value {
|
|
Value::Record {
|
|
val,
|
|
internal_span: span,
|
|
}
|
|
}
|
|
|
|
pub fn list(vals: Vec<Value>, span: Span) -> Value {
|
|
Value::List {
|
|
vals,
|
|
internal_span: span,
|
|
}
|
|
}
|
|
|
|
pub fn block(val: BlockId, span: Span) -> Value {
|
|
Value::Block {
|
|
val,
|
|
internal_span: span,
|
|
}
|
|
}
|
|
|
|
pub fn closure(val: BlockId, captures: HashMap<VarId, Value>, span: Span) -> Value {
|
|
Value::Closure {
|
|
val,
|
|
captures,
|
|
internal_span: span,
|
|
}
|
|
}
|
|
|
|
/// Create a new `Nothing` value
|
|
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_value(val: Box<dyn CustomValue>, span: Span) -> Value {
|
|
Value::CustomValue {
|
|
val,
|
|
internal_span: span,
|
|
}
|
|
}
|
|
|
|
pub fn lazy_record(val: Box<dyn for<'a> LazyRecord<'a>>, span: Span) -> Value {
|
|
Value::LazyRecord {
|
|
val,
|
|
internal_span: span,
|
|
}
|
|
}
|
|
|
|
pub fn match_pattern(val: MatchPattern, span: Span) -> Value {
|
|
Value::MatchPattern {
|
|
val: Box::new(val),
|
|
internal_span: span,
|
|
}
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_bool(val: bool) -> Value {
|
|
Value::bool(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_int(val: i64) -> Value {
|
|
Value::int(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_float(val: f64) -> Value {
|
|
Value::float(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_filesize(val: i64) -> Value {
|
|
Value::filesize(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_duration(val: i64) -> Value {
|
|
Value::duration(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_date(val: DateTime<FixedOffset>) -> Value {
|
|
Value::date(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_range(val: Range) -> Value {
|
|
Value::range(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_string(val: impl Into<String>) -> Value {
|
|
Value::string(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_record(val: Record) -> Value {
|
|
Value::record(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_list(vals: Vec<Value>) -> Value {
|
|
Value::list(vals, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_block(val: BlockId) -> Value {
|
|
Value::block(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_closure(val: BlockId, captures: HashMap<VarId, Value>) -> Value {
|
|
Value::closure(val, captures, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_nothing() -> Value {
|
|
Value::nothing(Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_binary(val: impl Into<Vec<u8>>) -> Value {
|
|
Value::binary(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_cell_path(val: CellPath) -> Value {
|
|
Value::cell_path(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_custom_value(val: Box<dyn CustomValue>) -> Value {
|
|
Value::custom_value(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_lazy_record(val: Box<dyn for<'a> LazyRecord<'a>>) -> Value {
|
|
Value::lazy_record(val, Span::test_data())
|
|
}
|
|
|
|
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
|
|
/// when used in errors.
|
|
pub fn test_match_pattern(val: MatchPattern) -> Value {
|
|
Value::match_pattern(val, Span::test_data())
|
|
}
|
|
}
|
|
|
|
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> {
|
|
// Compare two floating point numbers. The decision interval for equality is dynamically
|
|
// scaled as the value being compared increases in magnitude.
|
|
fn compare_floats(val: f64, other: f64) -> Option<Ordering> {
|
|
let prec = f64::EPSILON.max(val.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::Filesize { .. } => Some(Ordering::Less),
|
|
Value::Duration { .. } => Some(Ordering::Less),
|
|
Value::Date { .. } => Some(Ordering::Less),
|
|
Value::Range { .. } => Some(Ordering::Less),
|
|
Value::String { .. } => Some(Ordering::Less),
|
|
Value::Record { .. } => Some(Ordering::Less),
|
|
Value::LazyRecord { .. } => Some(Ordering::Less),
|
|
Value::List { .. } => Some(Ordering::Less),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Less),
|
|
Value::Duration { .. } => Some(Ordering::Less),
|
|
Value::Date { .. } => Some(Ordering::Less),
|
|
Value::Range { .. } => Some(Ordering::Less),
|
|
Value::String { .. } => Some(Ordering::Less),
|
|
Value::Record { .. } => Some(Ordering::Less),
|
|
Value::LazyRecord { .. } => Some(Ordering::Less),
|
|
Value::List { .. } => Some(Ordering::Less),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Less),
|
|
Value::Duration { .. } => Some(Ordering::Less),
|
|
Value::Date { .. } => Some(Ordering::Less),
|
|
Value::Range { .. } => Some(Ordering::Less),
|
|
Value::String { .. } => Some(Ordering::Less),
|
|
Value::Record { .. } => Some(Ordering::Less),
|
|
Value::LazyRecord { .. } => Some(Ordering::Less),
|
|
Value::List { .. } => Some(Ordering::Less),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { val: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::Duration { .. } => Some(Ordering::Less),
|
|
Value::Date { .. } => Some(Ordering::Less),
|
|
Value::Range { .. } => Some(Ordering::Less),
|
|
Value::String { .. } => Some(Ordering::Less),
|
|
Value::Record { .. } => Some(Ordering::Less),
|
|
Value::LazyRecord { .. } => Some(Ordering::Less),
|
|
Value::List { .. } => Some(Ordering::Less),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { val: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::Date { .. } => Some(Ordering::Less),
|
|
Value::Range { .. } => Some(Ordering::Less),
|
|
Value::String { .. } => Some(Ordering::Less),
|
|
Value::Record { .. } => Some(Ordering::Less),
|
|
Value::LazyRecord { .. } => Some(Ordering::Less),
|
|
Value::List { .. } => Some(Ordering::Less),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { val: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::Range { .. } => Some(Ordering::Less),
|
|
Value::String { .. } => Some(Ordering::Less),
|
|
Value::Record { .. } => Some(Ordering::Less),
|
|
Value::LazyRecord { .. } => Some(Ordering::Less),
|
|
Value::List { .. } => Some(Ordering::Less),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { val: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::String { .. } => Some(Ordering::Less),
|
|
Value::Record { .. } => Some(Ordering::Less),
|
|
Value::LazyRecord { .. } => Some(Ordering::Less),
|
|
Value::List { .. } => Some(Ordering::Less),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { val: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::Record { .. } => Some(Ordering::Less),
|
|
Value::LazyRecord { .. } => Some(Ordering::Less),
|
|
Value::List { .. } => Some(Ordering::Less),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { .. } => Some(Ordering::Greater),
|
|
Value::Record { val: rhs, .. } => {
|
|
// reorder cols and vals to make more logically compare.
|
|
// more general, if two record have same col and values,
|
|
// the order of cols shouldn't affect the equal property.
|
|
let (lhs_cols_ordered, lhs_vals_ordered) = reorder_record_inner(lhs);
|
|
let (rhs_cols_ordered, rhs_vals_ordered) = reorder_record_inner(rhs);
|
|
|
|
let result = lhs_cols_ordered.partial_cmp(&rhs_cols_ordered);
|
|
if result == Some(Ordering::Equal) {
|
|
lhs_vals_ordered.partial_cmp(&rhs_vals_ordered)
|
|
} else {
|
|
result
|
|
}
|
|
}
|
|
Value::LazyRecord { val, .. } => {
|
|
if let Ok(rhs) = val.collect() {
|
|
self.partial_cmp(&rhs)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
Value::List { .. } => Some(Ordering::Less),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { .. } => Some(Ordering::Greater),
|
|
Value::Record { .. } => Some(Ordering::Greater),
|
|
Value::LazyRecord { .. } => Some(Ordering::Greater),
|
|
Value::List { vals: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::Block { .. } => Some(Ordering::Less),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => Some(Ordering::Less),
|
|
},
|
|
(Value::Block { val: lhs, .. }, rhs) => match rhs {
|
|
Value::Bool { .. } => Some(Ordering::Greater),
|
|
Value::Int { .. } => Some(Ordering::Greater),
|
|
Value::Float { .. } => Some(Ordering::Greater),
|
|
Value::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { .. } => Some(Ordering::Greater),
|
|
Value::Record { .. } => Some(Ordering::Greater),
|
|
Value::List { .. } => Some(Ordering::Greater),
|
|
Value::LazyRecord { .. } => Some(Ordering::Greater),
|
|
Value::Block { val: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::Closure { .. } => Some(Ordering::Less),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { .. } => Some(Ordering::Greater),
|
|
Value::Record { .. } => Some(Ordering::Greater),
|
|
Value::LazyRecord { .. } => Some(Ordering::Greater),
|
|
Value::List { .. } => Some(Ordering::Greater),
|
|
Value::Block { .. } => Some(Ordering::Greater),
|
|
Value::Closure { val: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::Nothing { .. } => Some(Ordering::Less),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => Some(Ordering::Less),
|
|
},
|
|
(Value::Nothing { .. }, rhs) => match rhs {
|
|
Value::Bool { .. } => Some(Ordering::Greater),
|
|
Value::Int { .. } => Some(Ordering::Greater),
|
|
Value::Float { .. } => Some(Ordering::Greater),
|
|
Value::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { .. } => Some(Ordering::Greater),
|
|
Value::Record { .. } => Some(Ordering::Greater),
|
|
Value::LazyRecord { .. } => Some(Ordering::Greater),
|
|
Value::List { .. } => Some(Ordering::Greater),
|
|
Value::Block { .. } => Some(Ordering::Greater),
|
|
Value::Closure { .. } => Some(Ordering::Greater),
|
|
Value::Nothing { .. } => Some(Ordering::Equal),
|
|
Value::Error { .. } => Some(Ordering::Less),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => Some(Ordering::Less),
|
|
},
|
|
(Value::Error { .. }, rhs) => match rhs {
|
|
Value::Bool { .. } => Some(Ordering::Greater),
|
|
Value::Int { .. } => Some(Ordering::Greater),
|
|
Value::Float { .. } => Some(Ordering::Greater),
|
|
Value::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { .. } => Some(Ordering::Greater),
|
|
Value::Record { .. } => Some(Ordering::Greater),
|
|
Value::LazyRecord { .. } => Some(Ordering::Greater),
|
|
Value::List { .. } => Some(Ordering::Greater),
|
|
Value::Block { .. } => Some(Ordering::Greater),
|
|
Value::Closure { .. } => Some(Ordering::Greater),
|
|
Value::Nothing { .. } => Some(Ordering::Greater),
|
|
Value::Error { .. } => Some(Ordering::Equal),
|
|
Value::Binary { .. } => Some(Ordering::Less),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { .. } => Some(Ordering::Greater),
|
|
Value::Record { .. } => Some(Ordering::Greater),
|
|
Value::LazyRecord { .. } => Some(Ordering::Greater),
|
|
Value::List { .. } => Some(Ordering::Greater),
|
|
Value::Block { .. } => Some(Ordering::Greater),
|
|
Value::Closure { .. } => Some(Ordering::Greater),
|
|
Value::Nothing { .. } => Some(Ordering::Greater),
|
|
Value::Error { .. } => Some(Ordering::Greater),
|
|
Value::Binary { val: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::CellPath { .. } => Some(Ordering::Less),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => 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::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { .. } => Some(Ordering::Greater),
|
|
Value::Record { .. } => Some(Ordering::Greater),
|
|
Value::LazyRecord { .. } => Some(Ordering::Greater),
|
|
Value::List { .. } => Some(Ordering::Greater),
|
|
Value::Block { .. } => Some(Ordering::Greater),
|
|
Value::Closure { .. } => Some(Ordering::Greater),
|
|
Value::Nothing { .. } => Some(Ordering::Greater),
|
|
Value::Error { .. } => Some(Ordering::Greater),
|
|
Value::Binary { .. } => Some(Ordering::Greater),
|
|
Value::CellPath { val: rhs, .. } => lhs.partial_cmp(rhs),
|
|
Value::CustomValue { .. } => Some(Ordering::Less),
|
|
Value::MatchPattern { .. } => Some(Ordering::Less),
|
|
},
|
|
(Value::CustomValue { val: lhs, .. }, rhs) => lhs.partial_cmp(rhs),
|
|
(Value::LazyRecord { val, .. }, rhs) => {
|
|
if let Ok(val) = val.collect() {
|
|
val.partial_cmp(rhs)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
(Value::MatchPattern { .. }, rhs) => match rhs {
|
|
Value::Bool { .. } => Some(Ordering::Greater),
|
|
Value::Int { .. } => Some(Ordering::Greater),
|
|
Value::Float { .. } => Some(Ordering::Greater),
|
|
Value::Filesize { .. } => Some(Ordering::Greater),
|
|
Value::Duration { .. } => Some(Ordering::Greater),
|
|
Value::Date { .. } => Some(Ordering::Greater),
|
|
Value::Range { .. } => Some(Ordering::Greater),
|
|
Value::String { .. } => Some(Ordering::Greater),
|
|
Value::Record { .. } => Some(Ordering::Greater),
|
|
Value::LazyRecord { .. } => Some(Ordering::Greater),
|
|
Value::List { .. } => Some(Ordering::Greater),
|
|
Value::Block { .. } => Some(Ordering::Greater),
|
|
Value::Closure { .. } => Some(Ordering::Greater),
|
|
Value::Nothing { .. } => Some(Ordering::Greater),
|
|
Value::Error { .. } => Some(Ordering::Greater),
|
|
Value::Binary { .. } => Some(Ordering::Greater),
|
|
Value::CellPath { .. } => Some(Ordering::Greater),
|
|
Value::CustomValue { .. } => Some(Ordering::Greater),
|
|
Value::MatchPattern { .. } => None,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PartialEq for Value {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.partial_cmp(other).map_or(false, Ordering::is_eq)
|
|
}
|
|
}
|
|
|
|
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: "Consider using floating point values for increased range by promoting operand with 'into decimal'. 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::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: "".into(),
|
|
})
|
|
}
|
|
}
|
|
(Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
|
|
if let Some(val) = lhs.checked_add(*rhs) {
|
|
Ok(Value::duration(val, span))
|
|
} else {
|
|
Err(ShellError::OperatorOverflow {
|
|
msg: "add operation overflowed".into(),
|
|
span,
|
|
help: "".into(),
|
|
})
|
|
}
|
|
}
|
|
(Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
|
|
if let Some(val) = lhs.checked_add(*rhs) {
|
|
Ok(Value::filesize(val, span))
|
|
} else {
|
|
Err(ShellError::OperatorOverflow {
|
|
msg: "add operation overflowed".into(),
|
|
span,
|
|
help: "".into(),
|
|
})
|
|
}
|
|
}
|
|
|
|
(Value::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(self.span(), Operator::Math(Math::Plus), op, rhs)
|
|
}
|
|
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn append(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
|
match (self, rhs) {
|
|
(Value::List { vals: lhs, .. }, Value::List { vals: rhs, .. }) => {
|
|
let mut lhs = lhs.clone();
|
|
let mut rhs = rhs.clone();
|
|
lhs.append(&mut rhs);
|
|
Ok(Value::list(lhs, span))
|
|
}
|
|
(Value::List { vals: lhs, .. }, val) => {
|
|
let mut lhs = lhs.clone();
|
|
lhs.push(val.clone());
|
|
Ok(Value::list(lhs, span))
|
|
}
|
|
(val, Value::List { vals: rhs, .. }) => {
|
|
let mut rhs = rhs.clone();
|
|
rhs.insert(0, val.clone());
|
|
Ok(Value::list(rhs, span))
|
|
}
|
|
(Value::String { val: lhs, .. }, Value::String { val: rhs, .. }) => {
|
|
Ok(Value::string(lhs.to_string() + rhs, span))
|
|
}
|
|
(Value::Binary { val: lhs, .. }, Value::Binary { val: rhs, .. }) => {
|
|
let mut val = lhs.clone();
|
|
val.extend(rhs);
|
|
Ok(Value::binary(val, span))
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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: "Consider using floating point values for increased range by promoting operand with 'into decimal'. 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: "".into(),
|
|
})
|
|
}
|
|
}
|
|
(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: "".into(),
|
|
}),
|
|
}
|
|
}
|
|
(Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
|
|
if let Some(val) = lhs.checked_sub(*rhs) {
|
|
Ok(Value::duration(val, span))
|
|
} else {
|
|
Err(ShellError::OperatorOverflow {
|
|
msg: "subtraction operation overflowed".into(),
|
|
span,
|
|
help: "".into(),
|
|
})
|
|
}
|
|
}
|
|
(Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
|
|
if let Some(val) = lhs.checked_sub(*rhs) {
|
|
Ok(Value::filesize(val, span))
|
|
} else {
|
|
Err(ShellError::OperatorOverflow {
|
|
msg: "add operation overflowed".into(),
|
|
span,
|
|
help: "".into(),
|
|
})
|
|
}
|
|
}
|
|
|
|
(Value::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(self.span(), Operator::Math(Math::Minus), op, rhs)
|
|
}
|
|
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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: "Consider using floating point values for increased range by promoting operand with 'into decimal'. 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, .. }) => {
|
|
Ok(Value::filesize(*lhs * *rhs, span))
|
|
}
|
|
(Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
Ok(Value::filesize(*lhs * *rhs, span))
|
|
}
|
|
(Value::Float { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
|
|
Ok(Value::filesize((*lhs * *rhs as f64) as i64, span))
|
|
}
|
|
(Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
|
|
Ok(Value::filesize((*lhs as f64 * *rhs) as i64, span))
|
|
}
|
|
(Value::Int { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
|
|
Ok(Value::duration(*lhs * *rhs, span))
|
|
}
|
|
(Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
Ok(Value::duration(*lhs * *rhs, 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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(self.span(), Operator::Math(Math::Multiply), op, rhs)
|
|
}
|
|
(Value::Int { val: lhs, .. }, Value::String { val: rhs, .. }) => {
|
|
let mut res = String::new();
|
|
for _ in 0..*lhs {
|
|
res.push_str(rhs)
|
|
}
|
|
Ok(Value::string(res, span))
|
|
}
|
|
(Value::String { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
let mut res = String::new();
|
|
for _ in 0..*rhs {
|
|
res.push_str(lhs)
|
|
}
|
|
Ok(Value::string(res, span))
|
|
}
|
|
(Value::Int { val: lhs, .. }, Value::List { vals: rhs, .. }) => {
|
|
let mut res = vec![];
|
|
for _ in 0..*lhs {
|
|
res.append(&mut rhs.clone())
|
|
}
|
|
Ok(Value::list(res, span))
|
|
}
|
|
(Value::List { vals: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
let mut res = vec![];
|
|
for _ in 0..*rhs {
|
|
res.append(&mut lhs.clone())
|
|
}
|
|
Ok(Value::list(res, span))
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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 {
|
|
if lhs % rhs == 0 {
|
|
Ok(Value::int(lhs / rhs, span))
|
|
} else {
|
|
Ok(Value::float((*lhs as f64) / (*rhs as f64), span))
|
|
}
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(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 != 0 {
|
|
if lhs % rhs == 0 {
|
|
Ok(Value::int(lhs / rhs, span))
|
|
} else {
|
|
Ok(Value::float((*lhs as f64) / (*rhs as f64), span))
|
|
}
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
if *rhs != 0 {
|
|
Ok(Value::filesize(
|
|
((*lhs as f64) / (*rhs as f64)) as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
|
|
if *rhs != 0.0 {
|
|
Ok(Value::filesize((*lhs as f64 / rhs) as i64, span))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
|
|
if *rhs != 0 {
|
|
if lhs % rhs == 0 {
|
|
Ok(Value::int(lhs / rhs, span))
|
|
} else {
|
|
Ok(Value::float((*lhs as f64) / (*rhs as f64), span))
|
|
}
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
if *rhs != 0 {
|
|
Ok(Value::duration(
|
|
((*lhs as f64) / (*rhs as f64)) as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Duration { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
|
|
if *rhs != 0.0 {
|
|
Ok(Value::duration(((*lhs as f64) / rhs) as i64, span))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(self.span(), Operator::Math(Math::Divide), op, rhs)
|
|
}
|
|
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn floor_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 {
|
|
Ok(Value::int(
|
|
(*lhs as f64 / *rhs as f64)
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Int { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
|
|
if *rhs != 0.0 {
|
|
Ok(Value::int(
|
|
(*lhs as f64 / *rhs)
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Float { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
if *rhs != 0 {
|
|
Ok(Value::int(
|
|
(*lhs / *rhs as f64)
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Float { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
|
|
if *rhs != 0.0 {
|
|
Ok(Value::int(
|
|
(lhs / rhs)
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Filesize { val: lhs, .. }, Value::Filesize { val: rhs, .. }) => {
|
|
if *rhs != 0 {
|
|
Ok(Value::int(
|
|
(*lhs as f64 / *rhs as f64)
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Filesize { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
if *rhs != 0 {
|
|
Ok(Value::filesize(
|
|
((*lhs as f64) / (*rhs as f64))
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Filesize { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
|
|
if *rhs != 0.0 {
|
|
Ok(Value::filesize(
|
|
(*lhs as f64 / *rhs)
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Duration { val: lhs, .. }, Value::Duration { val: rhs, .. }) => {
|
|
if *rhs != 0 {
|
|
Ok(Value::int(
|
|
(*lhs as f64 / *rhs as f64)
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Duration { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
if *rhs != 0 {
|
|
Ok(Value::duration(
|
|
(*lhs as f64 / *rhs as f64)
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::Duration { val: lhs, .. }, Value::Float { val: rhs, .. }) => {
|
|
if *rhs != 0.0 {
|
|
Ok(Value::duration(
|
|
(*lhs as f64 / *rhs)
|
|
.clamp(std::i64::MIN as f64, std::i64::MAX as f64)
|
|
.floor() as i64,
|
|
span,
|
|
))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(Value::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(self.span(), Operator::Math(Math::Divide), op, rhs)
|
|
}
|
|
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn lt(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
|
if let (Value::CustomValue { 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())
|
|
&& (self.get_type() != Type::Any)
|
|
&& (rhs.get_type() != Type::Any)
|
|
{
|
|
return Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
});
|
|
}
|
|
|
|
if let Some(ordering) = self.partial_cmp(rhs) {
|
|
Ok(Value::bool(matches!(ordering, Ordering::Less), span))
|
|
} else {
|
|
Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
})
|
|
}
|
|
}
|
|
|
|
pub fn lte(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
|
if let (Value::CustomValue { 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())
|
|
&& (self.get_type() != Type::Any)
|
|
&& (rhs.get_type() != Type::Any)
|
|
{
|
|
return Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
});
|
|
}
|
|
|
|
self.partial_cmp(rhs)
|
|
.map(|ordering| Value::bool(matches!(ordering, Ordering::Less | Ordering::Equal), span))
|
|
.ok_or(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
})
|
|
}
|
|
|
|
pub fn gt(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
|
if let (Value::CustomValue { 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())
|
|
&& (self.get_type() != Type::Any)
|
|
&& (rhs.get_type() != Type::Any)
|
|
{
|
|
return Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
});
|
|
}
|
|
|
|
self.partial_cmp(rhs)
|
|
.map(|ordering| Value::bool(matches!(ordering, Ordering::Greater), span))
|
|
.ok_or(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
})
|
|
}
|
|
|
|
pub fn gte(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
|
if let (Value::CustomValue { 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())
|
|
&& (self.get_type() != Type::Any)
|
|
&& (rhs.get_type() != Type::Any)
|
|
{
|
|
return Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
});
|
|
}
|
|
|
|
match self.partial_cmp(rhs) {
|
|
Some(ordering) => Ok(Value::bool(
|
|
matches!(ordering, Ordering::Greater | Ordering::Equal),
|
|
span,
|
|
)),
|
|
None => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn eq(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
|
if let (Value::CustomValue { val: lhs, .. }, rhs) = (self, rhs) {
|
|
return lhs.operation(
|
|
self.span(),
|
|
Operator::Comparison(Comparison::Equal),
|
|
op,
|
|
rhs,
|
|
);
|
|
}
|
|
|
|
if let Some(ordering) = self.partial_cmp(rhs) {
|
|
Ok(Value::bool(matches!(ordering, Ordering::Equal), span))
|
|
} else {
|
|
match (self, rhs) {
|
|
(Value::Nothing { .. }, _) | (_, Value::Nothing { .. }) => {
|
|
Ok(Value::bool(false, span))
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn ne(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
|
if let (Value::CustomValue { val: lhs, .. }, rhs) = (self, rhs) {
|
|
return lhs.operation(
|
|
self.span(),
|
|
Operator::Comparison(Comparison::NotEqual),
|
|
op,
|
|
rhs,
|
|
);
|
|
}
|
|
|
|
if let Some(ordering) = self.partial_cmp(rhs) {
|
|
Ok(Value::bool(!matches!(ordering, Ordering::Equal), span))
|
|
} else {
|
|
match (self, rhs) {
|
|
(Value::Nothing { .. }, _) | (_, Value::Nothing { .. }) => {
|
|
Ok(Value::bool(true, span))
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.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.cols.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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(self.span(), Operator::Comparison(Comparison::In), op, rhs)
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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.cols.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::CustomValue { val: lhs, .. }, rhs) => lhs.operation(
|
|
self.span(),
|
|
Operator::Comparison(Comparison::NotIn),
|
|
op,
|
|
rhs,
|
|
),
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.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(
|
|
format!("{e}"),
|
|
"value originated from here".into(),
|
|
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(
|
|
format!("{e}"),
|
|
"value originated from here".into(),
|
|
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::CustomValue { val: lhs, .. }, rhs) => lhs.operation(
|
|
span,
|
|
if invert {
|
|
Operator::Comparison(Comparison::NotRegexMatch)
|
|
} else {
|
|
Operator::Comparison(Comparison::RegexMatch)
|
|
},
|
|
op,
|
|
rhs,
|
|
),
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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::CustomValue { val: lhs, .. }, rhs) => lhs.operation(
|
|
self.span(),
|
|
Operator::Comparison(Comparison::StartsWith),
|
|
op,
|
|
rhs,
|
|
),
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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::CustomValue { val: lhs, .. }, rhs) => lhs.operation(
|
|
self.span(),
|
|
Operator::Comparison(Comparison::EndsWith),
|
|
op,
|
|
rhs,
|
|
),
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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, .. }) => {
|
|
Ok(Value::int(*lhs << rhs, span))
|
|
}
|
|
(Value::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Bits(Bits::ShiftLeft), op, rhs)
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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, .. }) => {
|
|
Ok(Value::int(*lhs >> rhs, span))
|
|
}
|
|
(Value::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Bits(Bits::ShiftRight), op, rhs)
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Bits(Bits::BitOr), op, rhs)
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Bits(Bits::BitXor), op, rhs)
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Bits(Bits::BitAnd), op, rhs)
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn modulo(&self, op: Span, rhs: &Value, span: Span) -> Result<Value, ShellError> {
|
|
match (self, rhs) {
|
|
(Value::Int { val: lhs, .. }, Value::Int { val: rhs, .. }) => {
|
|
if *rhs != 0 {
|
|
Ok(Value::int(lhs % rhs, span))
|
|
} else {
|
|
Err(ShellError::DivisionByZero { span: op })
|
|
}
|
|
}
|
|
(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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Math(Math::Modulo), op, rhs)
|
|
}
|
|
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Boolean(Boolean::And), op, rhs)
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Boolean(Boolean::Or), op, rhs)
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Boolean(Boolean::Xor), op, rhs)
|
|
}
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
pub fn pow(&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_pow(*rhs as u32) {
|
|
Ok(Value::int(val, span))
|
|
} else {
|
|
Err(ShellError::OperatorOverflow { msg: "pow operation overflowed".into(), span, help: "Consider using floating point values for increased range by promoting operand with 'into decimal'. 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::CustomValue { val: lhs, .. }, rhs) => {
|
|
lhs.operation(span, Operator::Math(Math::Pow), op, rhs)
|
|
}
|
|
|
|
_ => Err(ShellError::OperatorMismatch {
|
|
op_span: op,
|
|
lhs_ty: self.get_type().to_string(),
|
|
lhs_span: self.span(),
|
|
rhs_ty: rhs.get_type().to_string(),
|
|
rhs_span: rhs.span(),
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
fn reorder_record_inner(record: &Record) -> (Vec<&String>, Vec<&Value>) {
|
|
let mut kv_pairs = record.iter().collect::<Vec<_>>();
|
|
kv_pairs.sort_by_key(|(col, _)| *col);
|
|
kv_pairs.into_iter().unzip()
|
|
}
|
|
|
|
fn type_compatible(a: Type, b: Type) -> bool {
|
|
if a == b {
|
|
return true;
|
|
}
|
|
|
|
matches!((a, b), (Type::Int, Type::Float) | (Type::Float, Type::Int))
|
|
}
|
|
|
|
/// Is the given year a leap year?
|
|
#[allow(clippy::nonminimal_bool)]
|
|
pub fn is_leap_year(year: i32) -> bool {
|
|
(year % 4 == 0) && (year % 100 != 0 || (year % 100 == 0 && year % 400 == 0))
|
|
}
|
|
|
|
#[derive(Clone, Copy)]
|
|
pub enum TimePeriod {
|
|
Nanos(i64),
|
|
Micros(i64),
|
|
Millis(i64),
|
|
Seconds(i64),
|
|
Minutes(i64),
|
|
Hours(i64),
|
|
Days(i64),
|
|
Weeks(i64),
|
|
Months(i64),
|
|
Years(i64),
|
|
}
|
|
|
|
impl TimePeriod {
|
|
pub fn to_text(self) -> Cow<'static, str> {
|
|
match self {
|
|
Self::Nanos(n) => format!("{n} ns").into(),
|
|
Self::Micros(n) => format!("{n} µs").into(),
|
|
Self::Millis(n) => format!("{n} ms").into(),
|
|
Self::Seconds(n) => format!("{n} sec").into(),
|
|
Self::Minutes(n) => format!("{n} min").into(),
|
|
Self::Hours(n) => format!("{n} hr").into(),
|
|
Self::Days(n) => format!("{n} day").into(),
|
|
Self::Weeks(n) => format!("{n} wk").into(),
|
|
Self::Months(n) => format!("{n} month").into(),
|
|
Self::Years(n) => format!("{n} yr").into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for TimePeriod {
|
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
|
write!(f, "{}", self.to_text())
|
|
}
|
|
}
|
|
|
|
pub fn format_duration(duration: i64) -> String {
|
|
let (sign, periods) = format_duration_as_timeperiod(duration);
|
|
|
|
let text = periods
|
|
.into_iter()
|
|
.map(|p| p.to_text().to_string().replace(' ', ""))
|
|
.collect::<Vec<String>>();
|
|
|
|
format!(
|
|
"{}{}",
|
|
if sign == -1 { "-" } else { "" },
|
|
text.join(" ").trim()
|
|
)
|
|
}
|
|
|
|
pub fn format_duration_as_timeperiod(duration: i64) -> (i32, Vec<TimePeriod>) {
|
|
// Attribution: most of this is taken from chrono-humanize-rs. Thanks!
|
|
// https://gitlab.com/imp/chrono-humanize-rs/-/blob/master/src/humantime.rs
|
|
// Current duration doesn't know a date it's based on, weeks is the max time unit it can normalize into.
|
|
// Don't guess or estimate how many years or months it might contain.
|
|
|
|
let (sign, duration) = if duration >= 0 {
|
|
(1, duration)
|
|
} else {
|
|
(-1, -duration)
|
|
};
|
|
|
|
let dur = Duration::nanoseconds(duration);
|
|
|
|
/// Split this a duration into number of whole weeks and the remainder
|
|
fn split_weeks(duration: Duration) -> (Option<i64>, Duration) {
|
|
let weeks = duration.num_weeks();
|
|
let remainder = duration - Duration::weeks(weeks);
|
|
normalize_split(weeks, remainder)
|
|
}
|
|
|
|
/// Split this a duration into number of whole days and the remainder
|
|
fn split_days(duration: Duration) -> (Option<i64>, Duration) {
|
|
let days = duration.num_days();
|
|
let remainder = duration - Duration::days(days);
|
|
normalize_split(days, remainder)
|
|
}
|
|
|
|
/// Split this a duration into number of whole hours and the remainder
|
|
fn split_hours(duration: Duration) -> (Option<i64>, Duration) {
|
|
let hours = duration.num_hours();
|
|
let remainder = duration - Duration::hours(hours);
|
|
normalize_split(hours, remainder)
|
|
}
|
|
|
|
/// Split this a duration into number of whole minutes and the remainder
|
|
fn split_minutes(duration: Duration) -> (Option<i64>, Duration) {
|
|
let minutes = duration.num_minutes();
|
|
let remainder = duration - Duration::minutes(minutes);
|
|
normalize_split(minutes, remainder)
|
|
}
|
|
|
|
/// Split this a duration into number of whole seconds and the remainder
|
|
fn split_seconds(duration: Duration) -> (Option<i64>, Duration) {
|
|
let seconds = duration.num_seconds();
|
|
let remainder = duration - Duration::seconds(seconds);
|
|
normalize_split(seconds, remainder)
|
|
}
|
|
|
|
/// Split this a duration into number of whole milliseconds and the remainder
|
|
fn split_milliseconds(duration: Duration) -> (Option<i64>, Duration) {
|
|
let millis = duration.num_milliseconds();
|
|
let remainder = duration - Duration::milliseconds(millis);
|
|
normalize_split(millis, remainder)
|
|
}
|
|
|
|
/// Split this a duration into number of whole seconds and the remainder
|
|
fn split_microseconds(duration: Duration) -> (Option<i64>, Duration) {
|
|
let micros = duration.num_microseconds().unwrap_or_default();
|
|
let remainder = duration - Duration::microseconds(micros);
|
|
normalize_split(micros, remainder)
|
|
}
|
|
|
|
/// Split this a duration into number of whole seconds and the remainder
|
|
fn split_nanoseconds(duration: Duration) -> (Option<i64>, Duration) {
|
|
let nanos = duration.num_nanoseconds().unwrap_or_default();
|
|
let remainder = duration - Duration::nanoseconds(nanos);
|
|
normalize_split(nanos, remainder)
|
|
}
|
|
|
|
fn normalize_split(
|
|
wholes: impl Into<Option<i64>>,
|
|
remainder: Duration,
|
|
) -> (Option<i64>, Duration) {
|
|
let wholes = wholes.into().map(i64::abs).filter(|x| *x > 0);
|
|
(wholes, remainder)
|
|
}
|
|
|
|
let mut periods = vec![];
|
|
|
|
let (weeks, remainder) = split_weeks(dur);
|
|
if let Some(weeks) = weeks {
|
|
periods.push(TimePeriod::Weeks(weeks));
|
|
}
|
|
|
|
let (days, remainder) = split_days(remainder);
|
|
if let Some(days) = days {
|
|
periods.push(TimePeriod::Days(days));
|
|
}
|
|
|
|
let (hours, remainder) = split_hours(remainder);
|
|
if let Some(hours) = hours {
|
|
periods.push(TimePeriod::Hours(hours));
|
|
}
|
|
|
|
let (minutes, remainder) = split_minutes(remainder);
|
|
if let Some(minutes) = minutes {
|
|
periods.push(TimePeriod::Minutes(minutes));
|
|
}
|
|
|
|
let (seconds, remainder) = split_seconds(remainder);
|
|
if let Some(seconds) = seconds {
|
|
periods.push(TimePeriod::Seconds(seconds));
|
|
}
|
|
|
|
let (millis, remainder) = split_milliseconds(remainder);
|
|
if let Some(millis) = millis {
|
|
periods.push(TimePeriod::Millis(millis));
|
|
}
|
|
|
|
let (micros, remainder) = split_microseconds(remainder);
|
|
if let Some(micros) = micros {
|
|
periods.push(TimePeriod::Micros(micros));
|
|
}
|
|
|
|
let (nanos, _remainder) = split_nanoseconds(remainder);
|
|
if let Some(nanos) = nanos {
|
|
periods.push(TimePeriod::Nanos(nanos));
|
|
}
|
|
|
|
if periods.is_empty() {
|
|
periods.push(TimePeriod::Seconds(0));
|
|
}
|
|
|
|
(sign, periods)
|
|
}
|
|
|
|
pub fn format_filesize_from_conf(num_bytes: i64, config: &Config) -> String {
|
|
// We need to take into account config.filesize_metric so, if someone asks for KB
|
|
// and filesize_metric is false, return KiB
|
|
format_filesize(
|
|
num_bytes,
|
|
config.filesize_format.as_str(),
|
|
Some(config.filesize_metric),
|
|
)
|
|
}
|
|
|
|
// filesize_metric is explicit when printed a value according to user config;
|
|
// other places (such as `format filesize`) don't.
|
|
pub fn format_filesize(
|
|
num_bytes: i64,
|
|
format_value: &str,
|
|
filesize_metric: Option<bool>,
|
|
) -> String {
|
|
// Allow the user to specify how they want their numbers formatted
|
|
|
|
// When format_value is "auto" or an invalid value, the returned ByteUnit doesn't matter
|
|
// and is always B.
|
|
let filesize_format_var = get_filesize_format(format_value, filesize_metric);
|
|
|
|
let byte = byte_unit::Byte::from_bytes(num_bytes.unsigned_abs() as u128);
|
|
let adj_byte = if filesize_format_var.1 == "auto" {
|
|
// When filesize_metric is None, format_value should never be "auto", so this
|
|
// unwrap_or() should always work.
|
|
byte.get_appropriate_unit(!filesize_metric.unwrap_or(false))
|
|
} else {
|
|
byte.get_adjusted_unit(filesize_format_var.0)
|
|
};
|
|
|
|
match adj_byte.get_unit() {
|
|
byte_unit::ByteUnit::B => {
|
|
let locale = get_system_locale();
|
|
let locale_byte = adj_byte.get_value() as u64;
|
|
let locale_byte_string = locale_byte.to_formatted_string(&locale);
|
|
let locale_signed_byte_string = if num_bytes.is_negative() {
|
|
format!("-{locale_byte_string}")
|
|
} else {
|
|
locale_byte_string
|
|
};
|
|
|
|
if filesize_format_var.1 == "auto" {
|
|
format!("{locale_signed_byte_string} B")
|
|
} else {
|
|
locale_signed_byte_string
|
|
}
|
|
}
|
|
_ => {
|
|
if num_bytes.is_negative() {
|
|
format!("-{}", adj_byte.format(1))
|
|
} else {
|
|
adj_byte.format(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_filesize_format(format_value: &str, filesize_metric: Option<bool>) -> (ByteUnit, &str) {
|
|
macro_rules! either {
|
|
($in:ident, $metric:ident, $binary:ident) => {
|
|
(
|
|
// filesize_metric always overrides the unit of
|
|
// filesize_format.
|
|
match filesize_metric {
|
|
Some(true) => byte_unit::ByteUnit::$metric,
|
|
Some(false) => byte_unit::ByteUnit::$binary,
|
|
None => {
|
|
if $in.ends_with("ib") {
|
|
byte_unit::ByteUnit::$binary
|
|
} else {
|
|
byte_unit::ByteUnit::$metric
|
|
}
|
|
}
|
|
},
|
|
"",
|
|
)
|
|
};
|
|
}
|
|
match format_value {
|
|
"b" => (byte_unit::ByteUnit::B, ""),
|
|
"kb" | "kib" => either!(format_value, KB, KiB),
|
|
"mb" | "mib" => either!(format_value, MB, MiB),
|
|
"gb" | "gib" => either!(format_value, GB, GiB),
|
|
"tb" | "tib" => either!(format_value, TB, TiB),
|
|
"pb" | "pib" => either!(format_value, TB, TiB),
|
|
"eb" | "eib" => either!(format_value, EB, EiB),
|
|
_ => (byte_unit::ByteUnit::B, "auto"),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
|
|
use super::{Record, Span, Value};
|
|
|
|
mod is_empty {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_string() {
|
|
let value = Value::string("", Span::unknown());
|
|
assert!(value.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_list() {
|
|
let list_with_no_values = Value::list(vec![], Span::unknown());
|
|
let list_with_one_empty_string =
|
|
Value::list(vec![Value::string("", Span::unknown())], Span::unknown());
|
|
|
|
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 {
|
|
cols: vec![String::from("")],
|
|
vals: vec![Value::string("", Span::unknown())],
|
|
});
|
|
|
|
let one_column_with_a_string_and_one_cell_value_with_empty_string =
|
|
Value::test_record(Record {
|
|
cols: vec![String::from("column")],
|
|
vals: vec![Value::string("", Span::unknown())],
|
|
});
|
|
|
|
let one_column_with_empty_string_and_one_value_with_a_string =
|
|
Value::test_record(Record {
|
|
cols: vec![String::from("")],
|
|
vals: vec![Value::string("text", Span::unknown())],
|
|
});
|
|
|
|
assert!(no_columns_nor_cell_values.is_empty());
|
|
assert!(!one_column_and_one_cell_value_with_empty_strings.is_empty());
|
|
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::list(vec![Value::int(0, Span::unknown())], Span::unknown());
|
|
let list_of_floats =
|
|
Value::list(vec![Value::float(0.0, Span::unknown())], Span::unknown());
|
|
let list_of_ints_and_floats = Value::list(
|
|
vec![
|
|
Value::int(0, Span::unknown()),
|
|
Value::float(0.0, Span::unknown()),
|
|
],
|
|
Span::unknown(),
|
|
);
|
|
let list_of_ints_and_floats_and_bools = Value::list(
|
|
vec![
|
|
Value::int(0, Span::unknown()),
|
|
Value::float(0.0, Span::unknown()),
|
|
Value::bool(false, Span::unknown()),
|
|
],
|
|
Span::unknown(),
|
|
);
|
|
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::Any))
|
|
);
|
|
assert_eq!(
|
|
list_of_ints_and_floats.get_type(),
|
|
Type::List(Box::new(Type::Number))
|
|
);
|
|
}
|
|
}
|
|
|
|
mod into_string {
|
|
use chrono::{DateTime, FixedOffset, NaiveDateTime};
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_datetime() {
|
|
let string = Value::date(
|
|
DateTime::from_naive_utc_and_offset(
|
|
NaiveDateTime::from_timestamp_millis(-123456789).unwrap(),
|
|
FixedOffset::east_opt(0).unwrap(),
|
|
),
|
|
Span::unknown(),
|
|
)
|
|
.into_string("", &Default::default());
|
|
|
|
// We need to cut the humanized part off for tests to work, because
|
|
// it is relative to current time.
|
|
let formatted = string.split('(').next().unwrap();
|
|
assert_eq!("Tue, 30 Dec 1969 13:42:23 +0000 ", formatted);
|
|
}
|
|
|
|
#[test]
|
|
fn test_negative_year_datetime() {
|
|
let string = Value::date(
|
|
DateTime::from_naive_utc_and_offset(
|
|
NaiveDateTime::from_timestamp_millis(-72135596800000).unwrap(),
|
|
FixedOffset::east_opt(0).unwrap(),
|
|
),
|
|
Span::unknown(),
|
|
)
|
|
.into_string("", &Default::default());
|
|
|
|
// We need to cut the humanized part off for tests to work, because
|
|
// it is relative to current time.
|
|
let formatted = string.split(' ').next().unwrap();
|
|
assert_eq!("-0316-02-11T06:13:20+00:00", formatted);
|
|
}
|
|
}
|
|
}
|