From 66239c031db043525186db4a94f6d4b6e05d360b Mon Sep 17 00:00:00 2001 From: EmirVildanov <reddog201030@gmail.com> Date: Mon, 6 Mar 2023 15:16:32 +0300 Subject: [PATCH] feat: add statistics transformation Value methods --- sbroad-core/src/ir/value.rs | 316 ++++++++++++++++++++++++ sbroad-core/src/ir/value/double.rs | 2 +- sbroad-core/src/ir/value/tests.rs | 376 +++++++++++++++++++++++++++++ 3 files changed, 693 insertions(+), 1 deletion(-) diff --git a/sbroad-core/src/ir/value.rs b/sbroad-core/src/ir/value.rs index 9d98fa787d..f5b4fd85c3 100644 --- a/sbroad-core/src/ir/value.rs +++ b/sbroad-core/src/ir/value.rs @@ -1,5 +1,6 @@ //! Value module. +use std::cmp::Ordering; use std::fmt::{self, Display}; use std::hash::Hash; use std::num::NonZeroI32; @@ -129,6 +130,44 @@ pub enum Value { Tuple(Tuple), } +/// Custom Ordering using Trivalent instead of simple Equal. +/// We cannot even derive `PartialOrd` for Values because of Doubles. +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] +pub enum TrivalentOrdering { + Less, + Equal, + Greater, + Unknown, +} + +impl From<Ordering> for TrivalentOrdering { + fn from(value: Ordering) -> Self { + match value { + Ordering::Less => TrivalentOrdering::Less, + Ordering::Equal => TrivalentOrdering::Equal, + Ordering::Greater => TrivalentOrdering::Greater, + } + } +} + +impl TrivalentOrdering { + /// Transforms `TrivalentOrdering` to Ordering. + /// + /// # Errors + /// Unacceptable `TrivalentOrdering` to transform + pub fn to_ordering(&self) -> Result<Ordering, SbroadError> { + match self { + Self::Less => Ok(Ordering::Less), + Self::Equal => Ok(Ordering::Equal), + Self::Greater => Ok(Ordering::Greater), + Self::Unknown => Err(SbroadError::Invalid( + Entity::Value, + Some("Can not cast Unknown to Ordering".to_string()), + )), + } + } +} + /// As a side effect, `NaN == NaN` is true. /// We should manually care about this case in the code. impl Eq for Value {} @@ -234,7 +273,155 @@ impl From<Trivalent> for Value { } } +/// Helper function to extract inner numerical value from `value` and cast it to `Decimal`. +/// +/// # Errors +/// - Inner `value` field is not numerical. +#[allow(dead_code)] +fn value_to_decimal_or_error(value: &Value) -> Result<Decimal, SbroadError> { + match value { + Value::Integer(s) => Ok(Decimal::from(*s)), + Value::Unsigned(s) => Ok(Decimal::from(*s)), + Value::Double(s) => { + let from_string_cast = Decimal::from_str(&format!("{s}")); + if let Ok(d) = from_string_cast { + Ok(d) + } else { + Err(SbroadError::Invalid( + Entity::Value, + Some(format!("Can't cast {value:?} to decimal")), + )) + } + } + Value::Decimal(s) => Ok(*s), + _ => Err(SbroadError::Invalid( + Entity::Value, + Some(format!("{value:?} must be numerical")), + )), + } +} + impl Value { + /// Adding. Applicable only to numerical values. + /// + /// # Errors + /// - Passed values are not numerical. + #[allow(dead_code)] + fn add(&self, other: &Value) -> Result<Value, SbroadError> { + let self_decimal = value_to_decimal_or_error(self)?; + let other_decimal = value_to_decimal_or_error(other)?; + + Ok(Value::from(self_decimal + other_decimal)) + } + + /// Subtraction. Applicable only to numerical values. + /// + /// # Errors + /// - Passed values are not numerical. + #[allow(dead_code)] + fn sub(&self, other: &Value) -> Result<Value, SbroadError> { + let self_decimal = value_to_decimal_or_error(self)?; + let other_decimal = value_to_decimal_or_error(other)?; + + Ok(Value::from(self_decimal - other_decimal)) + } + + /// Multiplication. Applicable only to numerical values. + /// + /// # Errors + /// - Passed values are not numerical. + #[allow(dead_code)] + fn mult(&self, other: &Value) -> Result<Value, SbroadError> { + let self_decimal = value_to_decimal_or_error(self)?; + let other_decimal = value_to_decimal_or_error(other)?; + + Ok(Value::from(self_decimal * other_decimal)) + } + + /// Division. Applicable only to numerical values. + /// + /// # Errors + /// - Passed values are not numerical. + #[allow(dead_code)] + fn div(&self, other: &Value) -> Result<Value, SbroadError> { + let self_decimal = value_to_decimal_or_error(self)?; + let other_decimal = value_to_decimal_or_error(other)?; + + if other_decimal == 0 { + Err(SbroadError::Invalid( + Entity::Value, + Some(format!("Can not divide {self:?} by zero {other:?}")), + )) + } else { + Ok(Value::from(self_decimal / other_decimal)) + } + } + + /// Negation. Applicable only to numerical values. + /// + /// # Errors + /// - Passed value is not numerical. + #[allow(dead_code)] + fn negate(&self) -> Result<Value, SbroadError> { + let self_decimal = value_to_decimal_or_error(self)?; + + Ok(Value::from(-self_decimal)) + } + + /// Concatenation. Applicable only to `Value::String`. + /// + /// # Errors + /// - Passed values are not `Value::String`. + #[allow(dead_code)] + fn concat(&self, other: &Value) -> Result<Value, SbroadError> { + let (Value::String(s), Value::String(o)) = (self, other) else { + return Err( + SbroadError::Invalid( + Entity::Value, + Some(format!("{self:?} and {other:?} must be strings to be concatenated")) + ) + ) + }; + + Ok(Value::from(format!("{s}{o}"))) + } + + /// Logical AND. Applicable only to `Value::Boolean`. + /// + /// # Errors + /// - Passed values are not `Value::Boolean`. + #[allow(dead_code)] + fn and(&self, other: &Value) -> Result<Value, SbroadError> { + let (Value::Boolean(s), Value::Boolean(o)) = (self, other) else { + return Err( + SbroadError::Invalid( + Entity::Value, + Some(format!("{self:?} and {other:?} must be booleans to be applied to AND operation")) + ) + ) + }; + + Ok(Value::from(*s && *o)) + } + + /// Logical OR. Applicable only to `Value::Boolean`. + /// + /// # Errors + /// - Passed values are not `Value::Boolean`. + #[allow(dead_code)] + fn or(&self, other: &Value) -> Result<Value, SbroadError> { + let (Value::Boolean(s), Value::Boolean(o)) = (self, other) else { + return Err( + SbroadError::Invalid( + Entity::Value, + Some(format!("{self:?} and {other:?} must be booleans to be applied to OR operation")) + ) + ) + }; + + Ok(Value::from(*s || *o)) + } + /// Checks equality of the two values. /// The result uses three-valued logic. #[must_use] @@ -317,6 +504,135 @@ impl Value { } } + /// Compares two values. + /// The result uses four-valued logic (standard `Ordering` variants and + /// `Unknown` in case `Null` was met). + /// + /// Returns `None` in case of + /// * String casting Error or types mismatch. + /// * Float `NaN` comparison occured. + #[must_use] + #[allow(clippy::too_many_lines)] + pub fn partial_cmp(&self, other: &Value) -> Option<TrivalentOrdering> { + match self { + Value::Boolean(s) => match other { + Value::Boolean(o) => TrivalentOrdering::from(s.cmp(o)).into(), + Value::Null => TrivalentOrdering::Unknown.into(), + Value::Unsigned(_) + | Value::Integer(_) + | Value::Decimal(_) + | Value::Double(_) + | Value::String(_) + | Value::Tuple(_) => None, + }, + Value::Null => TrivalentOrdering::Unknown.into(), + Value::Integer(s) => match other { + Value::Boolean(_) | Value::String(_) | Value::Tuple(_) => None, + Value::Null => TrivalentOrdering::Unknown.into(), + Value::Integer(o) => TrivalentOrdering::from(s.cmp(o)).into(), + Value::Decimal(o) => TrivalentOrdering::from(Decimal::from(*s).cmp(o)).into(), + // If double can't be converted to decimal without error then it is not equal to integer. + Value::Double(o) => { + let self_converted = Decimal::from_str(&format!("{s}")); + let other_converted = Decimal::from_str(&format!("{o}")); + match (self_converted, other_converted) { + (Ok(d1), Ok(d2)) => TrivalentOrdering::from(d1.cmp(&d2)).into(), + _ => None, + } + } + Value::Unsigned(o) => { + TrivalentOrdering::from(Decimal::from(*s).cmp(&Decimal::from(*o))).into() + } + }, + Value::Double(s) => match other { + Value::Boolean(_) | Value::String(_) | Value::Tuple(_) => None, + Value::Null => TrivalentOrdering::Unknown.into(), + Value::Integer(o) => { + if let Some(ord) = s.partial_cmp(&Double::from(*o)) { + TrivalentOrdering::from(ord).into() + } else { + None + } + } + // If double can't be converted to decimal without error then it is not equal to decimal. + Value::Decimal(o) => { + if let Ok(d) = Decimal::from_str(&format!("{s}")) { + TrivalentOrdering::from(d.cmp(o)).into() + } else { + None + } + } + Value::Double(o) => { + if let Some(ord) = s.partial_cmp(o) { + TrivalentOrdering::from(ord).into() + } else { + None + } + } + // If double can't be converted to decimal without error then it is not equal to unsigned. + Value::Unsigned(o) => { + if let Ok(d) = Decimal::from_str(&format!("{s}")) { + TrivalentOrdering::from(d.cmp(&Decimal::from(*o))).into() + } else { + None + } + } + }, + Value::Decimal(s) => match other { + Value::Boolean(_) | Value::String(_) | Value::Tuple(_) => None, + Value::Null => TrivalentOrdering::Unknown.into(), + Value::Integer(o) => TrivalentOrdering::from(s.cmp(&Decimal::from(*o))).into(), + Value::Decimal(o) => TrivalentOrdering::from(s.cmp(o)).into(), + // If double can't be converted to decimal without error then it is not equal to decimal. + Value::Double(o) => { + if let Ok(d) = Decimal::from_str(&format!("{o}")) { + TrivalentOrdering::from(s.cmp(&d)).into() + } else { + None + } + } + Value::Unsigned(o) => TrivalentOrdering::from(s.cmp(&Decimal::from(*o))).into(), + }, + Value::Unsigned(s) => match other { + Value::Boolean(_) | Value::String(_) | Value::Tuple(_) => None, + Value::Null => TrivalentOrdering::Unknown.into(), + Value::Integer(o) => { + TrivalentOrdering::from(Decimal::from(*s).cmp(&Decimal::from(*o))).into() + } + Value::Decimal(o) => TrivalentOrdering::from(Decimal::from(*s).cmp(o)).into(), + // If double can't be converted to decimal without error then it is not equal to unsigned. + Value::Double(o) => { + if let Ok(d) = Decimal::from_str(&format!("{o}")) { + TrivalentOrdering::from(Decimal::from(*s).cmp(&d)).into() + } else { + None + } + } + Value::Unsigned(o) => TrivalentOrdering::from(s.cmp(o)).into(), + }, + Value::String(s) => match other { + Value::Boolean(_) + | Value::Integer(_) + | Value::Decimal(_) + | Value::Double(_) + | Value::Unsigned(_) + | Value::Tuple(_) => None, + Value::Null => TrivalentOrdering::Unknown.into(), + Value::String(o) => TrivalentOrdering::from(s.cmp(o)).into(), + }, + Value::Tuple(_) => match other { + Value::Boolean(_) + | Value::Integer(_) + | Value::Decimal(_) + | Value::Double(_) + | Value::Unsigned(_) + | Value::String(_) + | Value::Tuple(_) => None, + Value::Null => TrivalentOrdering::Unknown.into(), + }, + } + } + /// Cast a value to a different type. /// /// # Errors diff --git a/sbroad-core/src/ir/value/double.rs b/sbroad-core/src/ir/value/double.rs index e332144c91..19388f7680 100644 --- a/sbroad-core/src/ir/value/double.rs +++ b/sbroad-core/src/ir/value/double.rs @@ -9,7 +9,7 @@ use crate::errors::{Entity, SbroadError}; use serde::{Deserialize, Serialize}; use tarantool::tlua; -#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] +#[derive(Serialize, Deserialize, PartialEq, PartialOrd, Debug, Clone)] pub struct Double { pub value: f64, } diff --git a/sbroad-core/src/ir/value/tests.rs b/sbroad-core/src/ir/value/tests.rs index 8a7bf83782..d4153af171 100644 --- a/sbroad-core/src/ir/value/tests.rs +++ b/sbroad-core/src/ir/value/tests.rs @@ -221,6 +221,369 @@ fn equivalence() { assert_eq!(Trivalent::Unknown, Value::from("hello").eq(&Value::Null)); } +#[test] +#[allow(clippy::too_many_lines)] +fn partial_comparing() { + // Boolean + assert_eq!( + TrivalentOrdering::Greater, + Value::Boolean(true) + .partial_cmp(&Value::Boolean(false)) + .unwrap() + ); + assert_eq!( + None, + Value::Boolean(true).partial_cmp(&Value::from(Double::from(1e0_f64))) + ); + assert_eq!( + None, + Value::Boolean(true).partial_cmp(&Value::from(decimal!(1e0))) + ); + assert_eq!(None, Value::Boolean(false).partial_cmp(&Value::from(0_u64))); + assert_eq!(None, Value::Boolean(true).partial_cmp(&Value::from(1_i64))); + assert_eq!(None, Value::Boolean(false).partial_cmp(&Value::from(""))); + assert_eq!( + None, + Value::Boolean(true).partial_cmp(&Value::from("hello")) + ); + assert_eq!( + TrivalentOrdering::Equal, + Value::Boolean(true) + .partial_cmp(&Value::Boolean(true)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Unknown, + Value::Boolean(true).partial_cmp(&Value::Null).unwrap() + ); + + // Decimal + assert_eq!( + TrivalentOrdering::Equal, + Value::Decimal(decimal!(0.000)) + .partial_cmp(&Value::from(0_u64)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Equal, + Value::Decimal(decimal!(0.000)) + .partial_cmp(&Value::from(decimal!(0))) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Equal, + Value::Decimal(decimal!(0.000)) + .partial_cmp(&Value::from(0_u64)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Equal, + Value::Decimal(decimal!(0.000)) + .partial_cmp(&Value::from(0_i64)) + .unwrap() + ); + assert_eq!( + None, + Value::Decimal(decimal!(0.000)).partial_cmp(&Value::from(false)) + ); + assert_eq!( + None, + Value::Decimal(decimal!(0.000)).partial_cmp(&Value::from("")) + ); + assert_eq!( + TrivalentOrdering::Unknown, + Value::Decimal(decimal!(0.000)) + .partial_cmp(&Value::Null) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Less, + Value::Decimal(decimal!(0.000)) + .partial_cmp(&Value::Decimal(decimal!(1.000))) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Less, + Value::Decimal(decimal!(0.000)) + .partial_cmp(&Value::Unsigned(1)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Greater, + Value::Decimal(decimal!(1.000)) + .partial_cmp(&Value::Unsigned(0)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Greater, + Value::Decimal(decimal!(1.000)) + .partial_cmp(&Value::Double(Double { value: 0.0 })) + .unwrap() + ); + + // Double + assert_eq!( + TrivalentOrdering::Equal, + Value::Double(Double::from(0.000_f64)) + .partial_cmp(&Value::from(0_u64)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Equal, + Value::Double(Double::from(0.000_f64)) + .partial_cmp(&Value::from(0_i64)) + .unwrap() + ); + assert_eq!( + None, + Value::Double(Double::from(0.000_f64)).partial_cmp(&Value::from(false)) + ); + assert_eq!( + None, + Value::Double(Double::from(0.000_f64)).partial_cmp(&Value::from("")) + ); + assert_eq!( + TrivalentOrdering::Unknown, + Value::Double(Double::from(0.000_f64)) + .partial_cmp(&Value::Null) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Equal, + Value::Double(Double::from(f64::INFINITY)) + .partial_cmp(&Value::from(f64::INFINITY)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Equal, + Value::Double(Double::from(f64::NEG_INFINITY)) + .partial_cmp(&Value::from(f64::NEG_INFINITY)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Unknown, + Value::Double(Double::from(f64::NAN)) + .partial_cmp(&Value::from(f64::NAN)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Less, + Value::Double(Double::from(0.0)) + .partial_cmp(&Value::from(1)) + .unwrap() + ); + assert_eq!( + None, + Value::Double(Double::from(f64::NAN)).partial_cmp(&Value::Double(Double::from(1.0))) + ); + assert_eq!( + TrivalentOrdering::Unknown, + Value::Double(Double::from(1.0)) + .partial_cmp(&Value::from(f64::NAN)) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Greater, + Value::Double(Double::from(1.0)) + .partial_cmp(&Value::from(0.5)) + .unwrap() + ); + + // Null + assert_eq!( + TrivalentOrdering::Unknown, + Value::Null.partial_cmp(&Value::Null).unwrap() + ); + assert_eq!( + TrivalentOrdering::Unknown, + Value::Null.partial_cmp(&Value::Boolean(false)).unwrap() + ); + assert_eq!( + TrivalentOrdering::Unknown, + Value::Null + .partial_cmp(&Value::Double(f64::NAN.into())) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Unknown, + Value::Null.partial_cmp(&Value::from("")).unwrap() + ); + + // String + assert_eq!( + TrivalentOrdering::Less, + Value::from("hello") + .partial_cmp(&Value::from("hello ")) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Equal, + Value::from("hello") + .partial_cmp(&Value::from("hello".to_string())) + .unwrap() + ); + assert_eq!( + TrivalentOrdering::Unknown, + Value::from("hello").partial_cmp(&Value::Null).unwrap() + ); +} + +#[test] +#[allow(clippy::too_many_lines)] +fn arithmetic() { + // Add + assert_eq!( + Value::Decimal(decimal!(1.000)), + Value::Decimal(decimal!(0.000)) + .add(&Value::from(1.000)) + .unwrap() + ); + assert_eq!( + Value::Decimal(decimal!(1.000)), + Value::Decimal(decimal!(0.000)) + .add(&Value::Double(Double { value: 1.0 })) + .unwrap() + ); + assert_eq!( + Err(SbroadError::Invalid( + Entity::Value, + Some(format!( + "Can't cast Double(Double {{ value: NaN }}) to decimal" + )), + )), + Value::Double(Double::from(f64::NAN)).add(&Value::Integer(1)) + ); + + // Sub + assert_eq!( + Value::Decimal(decimal!(1.000)), + Value::Decimal(decimal!(2.000)) + .sub(&Value::Double(Double { value: 1.0 })) + .unwrap() + ); + assert_eq!( + Value::Decimal(decimal!(5.500)), + Value::Decimal(decimal!(8.000)) + .sub(&Value::from(2.500)) + .unwrap() + ); + + // Mult + assert_eq!( + Value::Decimal(decimal!(8.000)), + Value::Decimal(decimal!(2.000)) + .mult(&Value::from(4)) + .unwrap() + ); + assert_eq!( + Value::Decimal(decimal!(3.999)), + Value::from(3).mult(&Value::from(1.333)).unwrap() + ); + assert_eq!( + Value::Decimal(decimal!(555)), + Value::from(5.0).mult(&Value::Unsigned(111)).unwrap() + ); + + // Div + assert_eq!( + Value::Decimal(decimal!(3)), + Value::from(9.0).div(&Value::Unsigned(3)).unwrap() + ); + assert_eq!( + Value::Decimal(decimal!(1)), + Value::Integer(2).div(&Value::Unsigned(2)).unwrap() + ); + assert_eq!( + Err(SbroadError::Invalid( + Entity::Value, + Some(format!("String(\"\") must be numerical")) + )), + Value::from("").div(&Value::Unsigned(2)) + ); + assert_eq!( + Err(SbroadError::Invalid( + Entity::Value, + Some(format!("Can not divide Integer(1) by zero Integer(0)")) + )), + Value::Integer(1).div(&Value::Integer(0)) + ); + + // Negate + assert_eq!( + Value::Decimal(decimal!(-3)), + Value::from(3.0).negate().unwrap() + ); + assert_eq!( + Value::Decimal(decimal!(-1)), + Value::Integer(1).negate().unwrap() + ); + assert_eq!( + Value::Decimal(decimal!(0)), + Value::Double(Double::from(0.0)).negate().unwrap() + ); +} + +#[test] +fn concatenation() { + assert_eq!( + Value::from("hello"), + Value::from("").concat(&Value::from("hello")).unwrap() + ); + assert_eq!( + Value::from("ab"), + Value::from("a").concat(&Value::from("b")).unwrap() + ); + assert_eq!( + Err(SbroadError::Invalid( + Entity::Value, + Some(format!( + "Integer(1) and String(\"b\") must be strings to be concatenated" + )) + )), + Value::Integer(1).concat(&Value::from("b")) + ) +} + +#[test] +fn and_or() { + // And + assert_eq!( + Value::from(true), + Value::from(true).and(&Value::from(true)).unwrap() + ); + assert_eq!( + Value::from(false), + Value::from(false).and(&Value::from(true)).unwrap() + ); + assert_eq!( + Value::from(false), + Value::from(true).and(&Value::from(false)).unwrap() + ); + assert_eq!( + Value::from(false), + Value::from(false).and(&Value::from(false)).unwrap() + ); + + // Or + assert_eq!( + Value::from(true), + Value::from(true).or(&Value::from(false)).unwrap() + ); + assert_eq!( + Value::from(false), + Value::from(false).or(&Value::from(false)).unwrap() + ); + assert_eq!( + Err(SbroadError::Invalid( + Entity::Value, + Some(format!( + "Integer(1) and Boolean(false) must be booleans to be applied to OR operation" + )) + )), + Value::Integer(1).or(&Value::from(false)) + ); +} + #[test] fn trivalent() { assert_eq!( @@ -237,3 +600,16 @@ fn trivalent() { ); assert_eq!(Trivalent::Unknown, Value::Null.eq(&Value::Null)); } + +#[test] +fn trivalent_ordering() { + assert_eq!( + TrivalentOrdering::Less, + TrivalentOrdering::from(false.cmp(&true)) + ); + assert_eq!(TrivalentOrdering::Equal, TrivalentOrdering::from(1.cmp(&1))); + assert_eq!( + TrivalentOrdering::Greater, + TrivalentOrdering::from("b".cmp("")) + ); +} -- GitLab