use crate::ExpirationDate;
use crate::error::SimulationError;
use crate::utils::TimeFrame;
use crate::utils::time::convert_time_frame;
use positive::Positive;
use serde::{Serialize, Serializer};
use std::convert::TryInto;
use std::fmt::{Display, Formatter};
use std::ops::AddAssign;
use tracing::debug;
#[derive(Debug, Copy, Clone)]
pub struct Xstep<T>
where
T: Copy + TryInto<Positive> + AddAssign + Display,
{
index: i32,
step_size_in_time: T,
time_unit: TimeFrame,
datetime: ExpirationDate,
}
impl<T> Xstep<T>
where
T: Copy + TryInto<Positive> + AddAssign + Display,
{
pub fn new(value: T, time_unit: TimeFrame, datetime: ExpirationDate) -> Self {
let datetime = match datetime {
ExpirationDate::Days(_) => datetime,
ExpirationDate::DateTime(_) => panic!(
"ExpirationDate::DateTime is not supported for Step yet. Please use ExpirationDate::Days instead."
),
};
Self {
index: 0,
step_size_in_time: value,
time_unit,
datetime,
}
}
pub fn index(&self) -> &i32 {
&self.index
}
pub fn step_size_in_time(&self) -> &T {
&self.step_size_in_time
}
pub fn time_unit(&self) -> &TimeFrame {
&self.time_unit
}
pub fn datetime(&self) -> &ExpirationDate {
&self.datetime
}
pub fn days_left(&self) -> Result<Positive, SimulationError> {
Ok(self.datetime.get_days()?)
}
pub fn next(&self) -> Result<Self, SimulationError> {
let days = self.datetime.get_days().unwrap();
if days == Positive::ZERO {
return Err("Cannot generate next step. Expiration date is already reached.".into());
}
let days_to_rest = convert_time_frame(
self.step_size_in_time.try_into().map_err(|_| {
SimulationError::step_error("Failed to convert step size to Positive")
})?,
&self.time_unit,
&TimeFrame::Day,
);
let datetime = if days_to_rest <= days {
ExpirationDate::Days(days - days_to_rest)
} else {
ExpirationDate::Days(Positive::ZERO)
};
debug!(
"days_to_rest: {}, days: {}, datetime: {}",
days_to_rest, days, datetime
);
Ok(Self {
index: self.index + 1,
step_size_in_time: self.step_size_in_time,
time_unit: self.time_unit,
datetime,
})
}
pub fn previous(&self) -> Result<Self, SimulationError> {
let days = self.datetime.get_days().unwrap();
let days_to_rest = convert_time_frame(
self.step_size_in_time.try_into().map_err(|_| {
SimulationError::step_error("Failed to convert step size to Positive")
})?,
&self.time_unit,
&TimeFrame::Day,
);
let datetime = ExpirationDate::Days(days + days_to_rest);
Ok(Self {
index: self.index - 1,
step_size_in_time: self.step_size_in_time,
time_unit: self.time_unit,
datetime,
})
}
}
impl<T> Display for Xstep<T>
where
T: Copy + TryInto<Positive> + AddAssign + Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"Xstep {{ index: {}, value: {}, time_unit: {:?}, datetime: {} }}",
self.index, self.step_size_in_time, self.time_unit, self.datetime
)
}
}
impl<T> Serialize for Xstep<T>
where
T: Copy + TryInto<Positive> + AddAssign + Display + Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
use serde::ser::SerializeStruct;
let step_size_in_time: Positive =
self.step_size_in_time.try_into().unwrap_or(Positive::ZERO);
let mut state = serializer.serialize_struct("Xstep", 4)?;
state.serialize_field("index", &self.index)?;
state.serialize_field("step_size_in_time", &step_size_in_time)?;
state.serialize_field("time_unit", &self.time_unit)?;
state.serialize_field("datetime", &self.datetime)?;
state.end()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::ExpirationDate;
use positive::pos_or_panic;
#[test]
fn test_days_left() {
let mut step = Xstep::new(
1.5f64,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
);
step.index = 42;
assert!(step.next().is_ok());
assert_eq!(step.days_left().unwrap(), pos_or_panic!(30.0));
let step1 = step.next().unwrap();
assert_eq!(*step1.index(), 43);
assert_eq!(step1.days_left().unwrap(), pos_or_panic!(28.5));
let step2 = step1.next().unwrap();
assert_eq!(*step2.index(), 44);
assert_eq!(step2.days_left().unwrap(), pos_or_panic!(27.0));
let step3 = step2.next().unwrap();
assert_eq!(*step3.index(), 45);
assert_eq!(step3.days_left().unwrap(), pos_or_panic!(25.5));
let step4 = step3.previous().unwrap();
assert_eq!(*step4.index(), 44);
assert_eq!(step4.days_left().unwrap(), pos_or_panic!(27.0));
}
}
#[cfg(test)]
mod tests_serialize {
use super::*;
use crate::model::ExpirationDate;
use positive::pos_or_panic;
use rust_decimal_macros::dec;
use serde_json::{Value, json};
#[test]
fn test_serialized_structure() {
let mut step = Xstep::new(
1.5f64,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
);
step.index = 42;
let serialized = serde_json::to_string(&step).unwrap();
let parsed: Value = serde_json::from_str(&serialized).unwrap();
assert!(parsed.is_object());
assert!(parsed.get("index").unwrap().is_i64());
assert!(parsed.get("step_size_in_time").unwrap().is_number());
assert!(
parsed.get("time_unit").unwrap().is_object()
|| parsed.get("time_unit").unwrap().is_string()
);
assert!(parsed.get("datetime").unwrap().is_object());
assert_eq!(parsed["index"], json!(42));
assert_eq!(parsed["step_size_in_time"], json!(1.5));
}
#[test]
fn test_serialization_value_conversion() {
let step_f64 = Xstep::new(2.5f64, TimeFrame::Day, ExpirationDate::Days(Positive::ONE));
let step_decimal = Xstep::new(
dec!(2.5),
TimeFrame::Day,
ExpirationDate::Days(Positive::ONE),
);
let step_positive = Xstep::new(
pos_or_panic!(2.5),
TimeFrame::Day,
ExpirationDate::Days(Positive::ONE),
);
let json_f64 = serde_json::to_string(&step_f64).unwrap();
let json_decimal = serde_json::to_string(&step_decimal).unwrap();
let json_positive = serde_json::to_string(&step_positive).unwrap();
let parsed_f64: Value = serde_json::from_str(&json_f64).unwrap();
let parsed_decimal: Value = serde_json::from_str(&json_decimal).unwrap();
let parsed_positive: Value = serde_json::from_str(&json_positive).unwrap();
assert_eq!(parsed_f64["step_size_in_time"], json!(2.5));
assert_eq!(parsed_decimal["step_size_in_time"], json!(2.5));
assert_eq!(parsed_positive["step_size_in_time"], json!(2.5));
}
#[test]
fn test_serialization_format_identity() {
let step_f64 = Xstep::new(3.1f64, TimeFrame::Hour, ExpirationDate::Days(Positive::ONE));
let step_decimal = Xstep::new(
dec!(3.1),
TimeFrame::Hour,
ExpirationDate::Days(Positive::ONE),
);
let step_positive = Xstep::new(
pos_or_panic!(3.1),
TimeFrame::Hour,
ExpirationDate::Days(Positive::ONE),
);
let json_f64 = serde_json::to_string(&step_f64).unwrap();
let json_decimal = serde_json::to_string(&step_decimal).unwrap();
let json_positive = serde_json::to_string(&step_positive).unwrap();
assert_eq!(json_f64, json_decimal);
assert_eq!(json_decimal, json_positive);
}
#[test]
fn test_serialization_edge_cases() {
let step_zero = Xstep::new(
0.01f64,
TimeFrame::Minute,
ExpirationDate::Days(Positive::ZERO),
);
let json_zero = serde_json::to_string(&step_zero).unwrap();
let parsed_zero: Value = serde_json::from_str(&json_zero).unwrap();
assert_eq!(parsed_zero["step_size_in_time"], json!(0.01));
let step_small = Xstep::new(
0.00001f64,
TimeFrame::Minute,
ExpirationDate::Days(Positive::ONE),
);
let json_small = serde_json::to_string(&step_small).unwrap();
let parsed_small: Value = serde_json::from_str(&json_small).unwrap();
assert!(parsed_small["step_size_in_time"].as_f64().unwrap() > 0.0);
assert!(parsed_small["step_size_in_time"].as_f64().unwrap() < 0.0001);
let step_large = Xstep::new(
1_000_000.01f64,
TimeFrame::Minute,
ExpirationDate::Days(Positive::ONE),
);
let json_large = serde_json::to_string(&step_large).unwrap();
let parsed_large: Value = serde_json::from_str(&json_large).unwrap();
assert_eq!(parsed_large["step_size_in_time"], json!(1_000_000.01));
}
#[test]
fn test_serialization_precision() {
let step = Xstep::new(
1.23456789f64,
TimeFrame::Day,
ExpirationDate::Days(Positive::ONE),
);
let serialized = serde_json::to_string(&step).unwrap();
let parsed: Value = serde_json::from_str(&serialized).unwrap();
let value = parsed["step_size_in_time"].as_f64().unwrap();
assert!((value - 1.23456789).abs() < 0.0000001);
}
#[test]
fn test_datetime_serialization() {
let step_days = Xstep::new(
1.0f64,
TimeFrame::Day,
ExpirationDate::Days(pos_or_panic!(30.0)),
);
let serialized_days = serde_json::to_string(&step_days).unwrap();
let parsed_days: Value = serde_json::from_str(&serialized_days).unwrap();
assert!(parsed_days["datetime"].is_object());
assert!(parsed_days["datetime"].get("days").is_some());
}
#[test]
#[should_panic(expected = "ExpirationDate::DateTime is not supported for Step yet")]
fn test_datetime_constructor_panics() {
let date_time = chrono::Utc::now();
let _step = Xstep::new(1.0f64, TimeFrame::Day, ExpirationDate::DateTime(date_time));
}
}