Files
jhspetersson--fselect/src/util/variant.rs
T
2026-07-13 12:31:47 +08:00

529 lines
14 KiB
Rust

use std::fmt::{Display, Error, Formatter};
use chrono::NaiveDateTime;
use crate::util::{format_datetime, parse_datetime, parse_filesize, str_to_bool};
#[derive(Clone, Debug)]
pub enum VariantType {
String,
Int,
Float,
Bool,
DateTime,
}
#[derive(Clone, Debug)]
pub struct Variant {
value_type: VariantType,
string_value: String,
int_value: Option<i64>,
float_value: Option<f64>,
bool_value: Option<bool>,
dt_from: Option<NaiveDateTime>,
dt_to: Option<NaiveDateTime>,
}
impl Variant {
pub fn empty(value_type: VariantType) -> Variant {
Variant {
value_type,
string_value: String::new(),
int_value: None,
float_value: None,
bool_value: None,
dt_from: None,
dt_to: None,
}
}
pub fn get_type(&self) -> &VariantType {
&self.value_type
}
pub fn from_int(value: i64) -> Variant {
Variant {
value_type: VariantType::Int,
string_value: format!("{}", value),
int_value: Some(value),
float_value: Some(value as f64),
bool_value: None,
dt_from: None,
dt_to: None,
}
}
pub fn from_float(value: f64) -> Variant {
if value.is_nan() || value.is_infinite() {
return Variant::empty(VariantType::Float);
}
let value = value + 0.0;
Variant {
value_type: VariantType::Float,
string_value: format!("{}", value),
int_value: Some(value as i64),
float_value: Some(value),
bool_value: None,
dt_from: None,
dt_to: None,
}
}
pub fn from_string(value: &String) -> Variant {
Variant {
value_type: VariantType::String,
string_value: value.to_owned(),
int_value: None,
float_value: None,
bool_value: None,
dt_from: None,
dt_to: None,
}
}
pub fn from_signed_string(value: &String, minus: bool) -> Variant {
let string_value = match minus {
true => {
let mut result = String::from("-");
result += &value.to_owned();
result
}
false => value.to_owned(),
};
Variant {
value_type: VariantType::String,
string_value,
int_value: None,
float_value: None,
bool_value: None,
dt_from: None,
dt_to: None,
}
}
pub fn from_bool(value: bool) -> Variant {
Variant {
value_type: VariantType::Bool,
string_value: match value {
true => String::from("true"),
_ => String::from("false"),
},
int_value: match value {
true => Some(1),
_ => Some(0),
},
float_value: None,
bool_value: Some(value),
dt_from: None,
dt_to: None,
}
}
pub fn from_datetime(value: NaiveDateTime) -> Variant {
Variant {
value_type: VariantType::DateTime,
string_value: format_datetime(&value),
int_value: Some(0),
float_value: None,
bool_value: None,
dt_from: Some(value),
dt_to: Some(value),
}
}
pub fn to_int(&self) -> i64 {
match self.int_value {
Some(i) => i,
None => {
if let Some(f) = self.float_value {
return f as i64;
}
let int_value = self.string_value.parse::<i64>();
match int_value {
Ok(i) => i,
_ => match self.string_value.parse::<f64>() {
Ok(f) if f.is_finite() => f as i64,
_ => match parse_filesize(&self.string_value) {
Some(size) => size as i64,
_ => 0,
},
},
}
}
}
}
pub fn to_float(&self) -> f64 {
if let Some(f) = self.float_value {
return f;
}
match self.int_value {
Some(i) => i as f64,
None => {
let float_value = self.string_value.parse::<f64>();
match float_value {
Ok(f) if f.is_finite() => f,
_ => match parse_filesize(&self.string_value) {
Some(size) => size as f64,
_ => 0.0,
},
}
}
}
}
pub fn to_bool(&self) -> bool {
if let Some(value) = self.bool_value {
return value;
}
if !self.string_value.is_empty()
&& let Some(value) = str_to_bool(&self.string_value) {
return value;
}
if let Some(int_value) = self.int_value {
int_value != 0
} else if let Some(float_value) = self.float_value {
float_value != 0.0
} else {
false
}
}
pub fn to_datetime(&self) -> Result<(NaiveDateTime, NaiveDateTime), String> {
if let Some(dt_from) = self.dt_from {
Ok((dt_from, self.dt_to.unwrap()))
} else {
match parse_datetime(&self.string_value) {
Ok((dt_from, dt_to)) => {
Ok((dt_from, dt_to))
}
_ => Err(String::from("Can't parse datetime: ") + &self.string_value),
}
}
}
}
impl Display for Variant {
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
write!(f, "{}", self.string_value)
}
}
#[cfg(test)]
// 3.14 is used throughout as an arbitrary float test value, not as an
// approximation of PI, so the approx_constant lint does not apply here.
#[allow(clippy::approx_constant)]
mod tests {
use super::*;
use chrono::{NaiveDate, Timelike};
#[test]
fn from_int() {
let v = Variant::from_int(42);
assert_eq!(v.to_string(), "42");
assert_eq!(v.to_int(), 42);
assert_eq!(v.to_float(), 42.0);
assert!(matches!(v.get_type(), VariantType::Int));
}
#[test]
fn from_int_negative() {
let v = Variant::from_int(-7);
assert_eq!(v.to_string(), "-7");
assert_eq!(v.to_int(), -7);
assert_eq!(v.to_float(), -7.0);
}
#[test]
fn from_int_zero() {
let v = Variant::from_int(0);
assert_eq!(v.to_int(), 0);
assert_eq!(v.to_float(), 0.0);
assert_eq!(v.to_string(), "0");
}
#[test]
fn from_float() {
let v = Variant::from_float(3.14);
assert_eq!(v.to_float(), 3.14);
assert_eq!(v.to_int(), 3);
assert_eq!(v.to_string(), "3.14");
assert!(matches!(v.get_type(), VariantType::Float));
}
#[test]
fn from_float_negative() {
let v = Variant::from_float(-2.5);
assert_eq!(v.to_float(), -2.5);
assert_eq!(v.to_int(), -2);
}
#[test]
fn from_float_whole_number() {
let v = Variant::from_float(5.0);
assert_eq!(v.to_float(), 5.0);
assert_eq!(v.to_int(), 5);
}
#[test]
fn from_string_plain() {
let v = Variant::from_string(&String::from("hello"));
assert_eq!(v.to_string(), "hello");
assert!(matches!(v.get_type(), VariantType::String));
}
#[test]
fn from_string_numeric() {
let v = Variant::from_string(&String::from("123"));
assert_eq!(v.to_string(), "123");
assert_eq!(v.to_int(), 123);
assert_eq!(v.to_float(), 123.0);
}
#[test]
fn from_string_non_numeric_to_int() {
let v = Variant::from_string(&String::from("abc"));
assert_eq!(v.to_int(), 0);
}
#[test]
fn from_string_non_numeric_to_float() {
let v = Variant::from_string(&String::from("abc"));
assert_eq!(v.to_float(), 0.0);
}
#[test]
fn from_string_filesize_to_int() {
let v = Variant::from_string(&String::from("1k"));
assert_eq!(v.to_int(), 1024);
}
#[test]
fn from_string_filesize_to_float() {
let v = Variant::from_string(&String::from("1k"));
assert_eq!(v.to_float(), 1024.0);
}
#[test]
fn from_string_float_value() {
let v = Variant::from_string(&String::from("3.14"));
assert_eq!(v.to_float(), 3.14);
}
#[test]
fn from_signed_string_positive() {
let v = Variant::from_signed_string(&String::from("42"), false);
assert_eq!(v.to_string(), "42");
assert!(matches!(v.get_type(), VariantType::String));
}
#[test]
fn from_signed_string_negative() {
let v = Variant::from_signed_string(&String::from("42"), true);
assert_eq!(v.to_string(), "-42");
}
#[test]
fn from_bool_true() {
let v = Variant::from_bool(true);
assert!(v.to_bool());
assert_eq!(v.to_string(), "true");
assert_eq!(v.to_int(), 1);
assert!(matches!(v.get_type(), VariantType::Bool));
}
#[test]
fn from_bool_false() {
let v = Variant::from_bool(false);
assert!(!v.to_bool());
assert_eq!(v.to_string(), "false");
assert_eq!(v.to_int(), 0);
}
#[test]
fn from_datetime() {
let dt = NaiveDate::from_ymd_opt(2024, 6, 15).unwrap().and_hms_opt(10, 30, 0).unwrap();
let v = Variant::from_datetime(dt);
assert_eq!(v.to_string(), "2024-06-15 10:30:00");
assert!(matches!(v.get_type(), VariantType::DateTime));
let (from, to) = v.to_datetime().unwrap();
assert_eq!(from, dt);
assert_eq!(to, dt);
}
#[test]
fn to_datetime_from_string() {
let v = Variant::from_string(&String::from("2024-06-15"));
let result = v.to_datetime();
assert!(result.is_ok());
}
#[test]
fn to_datetime_date_only_returns_day_range() {
let v = Variant::from_string(&String::from("2024-06-15"));
let (start, finish) = v.to_datetime().unwrap();
// Date-only should span midnight to 23:59:59
assert_eq!(start.hour(), 0);
assert_eq!(finish.hour(), 23);
assert_eq!(finish.minute(), 59);
assert_eq!(finish.second(), 59);
}
#[test]
fn to_datetime_from_invalid_string() {
let v = Variant::from_string(&String::from("not-a-date"));
let result = v.to_datetime();
assert!(result.is_err());
}
#[test]
fn to_bool_from_string_true_variants() {
for val in &["true", "1", "yes", "y", "on"] {
let v = Variant::from_string(&String::from(*val));
assert!(v.to_bool(), "expected true for '{}'", val);
}
}
#[test]
fn to_bool_from_string_false_variants() {
for val in &["false", "0", "no", "n", "off"] {
let v = Variant::from_string(&String::from(*val));
assert!(!v.to_bool(), "expected false for '{}'", val);
}
}
#[test]
fn to_bool_from_string_unknown() {
let v = Variant::from_string(&String::from("maybe"));
assert!(!v.to_bool());
}
#[test]
fn to_bool_from_int_one() {
let v = Variant::from_int(1);
assert!(v.to_bool());
}
#[test]
fn to_bool_from_int_zero() {
let v = Variant::from_int(0);
assert!(!v.to_bool());
}
#[test]
fn to_bool_from_int_other() {
let v = Variant::from_int(42);
assert!(v.to_bool(), "non-zero int should be truthy");
let v = Variant::from_int(-1);
assert!(v.to_bool(), "negative int should be truthy");
}
#[test]
fn to_bool_from_float_one() {
let v = Variant::from_float(1.0);
assert!(v.to_bool());
}
#[test]
fn to_bool_from_float_zero() {
let v = Variant::from_float(0.0);
assert!(!v.to_bool());
}
#[test]
fn to_bool_from_float_nonzero() {
let v = Variant::from_float(3.14);
assert!(v.to_bool(), "non-zero float should be truthy");
}
#[test]
fn empty_variant() {
let v = Variant::empty(VariantType::String);
assert_eq!(v.to_string(), "");
assert!(matches!(v.get_type(), VariantType::String));
}
#[test]
fn empty_variant_to_int() {
let v = Variant::empty(VariantType::Int);
assert_eq!(v.to_int(), 0);
}
#[test]
fn empty_variant_to_bool() {
let v = Variant::empty(VariantType::Bool);
assert!(!v.to_bool());
}
#[test]
fn display_trait() {
let v = Variant::from_int(99);
assert_eq!(format!("{}", v), "99");
}
#[test]
fn display_trait_string() {
let v = Variant::from_string(&String::from("test"));
assert_eq!(format!("{}", v), "test");
}
#[test]
fn from_string_negative_to_int() {
let v = Variant::from_string(&String::from("-42"));
assert_eq!(v.to_int(), -42);
}
#[test]
fn from_string_negative_to_float() {
let v = Variant::from_string(&String::from("-42"));
assert_eq!(v.to_float(), -42.0);
}
#[test]
fn from_string_nan_to_float_not_nan() {
let v = Variant::from_string(&String::from("NaN"));
assert!(!v.to_float().is_nan(), "to_float should not return NaN from string");
}
#[test]
fn from_string_inf_to_float_not_inf() {
let v = Variant::from_string(&String::from("inf"));
assert!(v.to_float().is_finite(), "to_float should not return inf from string");
}
#[test]
fn from_string_neg_inf_to_float_not_inf() {
let v = Variant::from_string(&String::from("-inf"));
assert!(v.to_float().is_finite(), "to_float should not return -inf from string");
}
#[test]
fn from_string_infinity_to_float_not_inf() {
let v = Variant::from_string(&String::from("infinity"));
assert!(v.to_float().is_finite(), "to_float should not return infinity from string");
}
#[test]
fn from_string_float_to_int() {
let v = Variant::from_string(&String::from("3.14"));
assert_eq!(v.to_int(), 3);
}
#[test]
fn from_string_negative_float_to_int() {
let v = Variant::from_string(&String::from("-2.7"));
assert_eq!(v.to_int(), -2);
}
#[test]
fn clone() {
let v1 = Variant::from_int(10);
let v2 = v1.clone();
assert_eq!(v2.to_int(), 10);
assert_eq!(v2.to_string(), "10");
}
}