1use chrono::NaiveDate;
2
3#[derive(Debug)]
4pub enum DataError {
5 Empty,
6 LengthMismatch { dates: usize, values: usize },
7}
8
9impl std::fmt::Display for DataError {
10 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
11 match self {
12 DataError::Empty => write!(f, "time series is empty"),
13 DataError::LengthMismatch { dates, values } => write!(
14 f,
15 "time series length mismatch: {} dates vs {} values",
16 dates, values
17 ),
18 }
19 }
20}
21
22impl std::error::Error for DataError {}
23
24#[derive(Clone, Debug)]
25pub struct ReturnSeries {
26 pub dates: Vec<NaiveDate>,
27 pub values: Vec<f64>,
28 pub name: Option<String>,
29}
30
31impl ReturnSeries {
32 pub fn new(
33 dates: Vec<NaiveDate>,
34 values: Vec<f64>,
35 name: Option<String>,
36 ) -> Result<Self, DataError> {
37 if dates.is_empty() || values.is_empty() {
38 return Err(DataError::Empty);
39 }
40
41 if dates.len() != values.len() {
42 return Err(DataError::LengthMismatch {
43 dates: dates.len(),
44 values: values.len(),
45 });
46 }
47
48 let mut paired: Vec<(NaiveDate, f64)> = dates.into_iter().zip(values.into_iter()).collect();
49 paired.sort_by_key(|(d, _)| *d);
50
51 let (sorted_dates, sorted_values): (Vec<_>, Vec<_>) = paired.into_iter().unzip();
52
53 Ok(Self {
54 dates: sorted_dates,
55 values: sorted_values,
56 name,
57 })
58 }
59
60 pub fn len(&self) -> usize {
61 self.dates.len()
62 }
63
64 pub fn is_empty(&self) -> bool {
65 self.dates.is_empty()
66 }
67
68 pub fn date_range(&self) -> Option<(NaiveDate, NaiveDate)> {
69 if self.dates.is_empty() {
70 None
71 } else {
72 Some((
73 *self.dates.first().expect("len checked"),
74 *self.dates.last().expect("len checked"),
75 ))
76 }
77 }
78}
79
80pub fn align_start_dates(a: &ReturnSeries, b: &ReturnSeries) -> (ReturnSeries, ReturnSeries) {
81 let idx_a = first_non_zero_index(&a.values).unwrap_or(0);
82 let idx_b = first_non_zero_index(&b.values).unwrap_or(0);
83 let start_idx = idx_a.max(idx_b);
84
85 let slice_a = ReturnSeries {
86 dates: a.dates[start_idx..].to_vec(),
87 values: a.values[start_idx..].to_vec(),
88 name: a.name.clone(),
89 };
90
91 let slice_b = ReturnSeries {
92 dates: b.dates[start_idx..].to_vec(),
93 values: b.values[start_idx..].to_vec(),
94 name: b.name.clone(),
95 };
96
97 (slice_a, slice_b)
98}
99
100fn first_non_zero_index(values: &[f64]) -> Option<usize> {
101 values.iter().position(|v| !v.is_nan() && *v != 0.0)
102}