use std::sync::OnceLock;
use serde::Deserialize;
use crate::plan::ClassReturns;
const EMBEDDED: &str = include_str!("../../market/shiller.toml");
#[derive(Debug, Clone, Copy, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct HistoricalYear {
pub year: i16,
pub stocks: f64,
pub bonds: f64,
pub cash: f64,
pub inflation: f64,
}
impl HistoricalYear {
#[must_use]
pub fn returns(&self) -> ClassReturns {
[self.stocks, self.bonds, self.cash]
}
}
#[derive(Debug, thiserror::Error)]
pub enum HistoryError {
#[error("invalid historical record: {0}")]
Parse(#[from] toml::de::Error),
#[error("the historical record's years must run one after another, and there must be some")]
NotConsecutive,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct Document {
years: Vec<HistoricalYear>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct History {
years: Vec<HistoricalYear>,
}
impl History {
#[must_use]
pub fn embedded() -> &'static Self {
static EMBEDDED_HISTORY: OnceLock<History> = OnceLock::new();
EMBEDDED_HISTORY
.get_or_init(|| Self::from_toml_str(EMBEDDED).expect("the embedded history parses"))
}
pub fn from_toml_str(text: &str) -> Result<Self, HistoryError> {
let Document { years } = toml::from_str(text)?;
let is_consecutive = years
.windows(2)
.all(|pair| pair[1].year == pair[0].year + 1);
if years.is_empty() || !is_consecutive {
return Err(HistoryError::NotConsecutive);
}
Ok(Self { years })
}
#[must_use]
pub fn first_year(&self) -> i16 {
self.years[0].year
}
#[must_use]
pub fn last_year(&self) -> i16 {
self.years[self.years.len() - 1].year
}
#[must_use]
pub fn len(&self) -> usize {
self.years.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.years.is_empty()
}
#[must_use]
pub(crate) fn sequence(
&self,
start: i16,
length: usize,
wrap: bool,
) -> Option<Vec<HistoricalYear>> {
let at = usize::try_from(start - self.first_year())
.ok()
.filter(|&at| at < self.years.len())?;
if !wrap && at + length > self.years.len() {
return None;
}
Some(
self.years
.iter()
.copied()
.cycle()
.skip(at)
.take(length)
.collect(),
)
}
#[must_use]
pub fn years(&self) -> &[HistoricalYear] {
&self.years
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_embedded_record_runs_from_1871_to_2025() {
let history = History::embedded();
assert_eq!((history.first_year(), history.last_year()), (1871, 2025));
}
#[test]
fn a_sequence_starts_in_its_year_and_wraps_only_when_asked() {
let history = History::embedded();
let from_1966 = history.sequence(1966, 3, false).unwrap();
let years: Vec<i16> = from_1966.iter().map(|year| year.year).collect();
assert_eq!(years, [1966, 1967, 1968]);
assert_eq!(from_1966[0], history.years()[1966 - 1871]);
let wrapped = history.sequence(2024, 3, true).unwrap();
let years: Vec<i16> = wrapped.iter().map(|year| year.year).collect();
assert_eq!(years, [2024, 2025, 1871]);
assert!(history.sequence(2024, 3, false).is_none());
}
#[test]
fn a_record_that_skips_a_year_is_refused() {
let text = "years = [\n { year = 2000, stocks = 0.0, bonds = 0.0, cash = 0.0, inflation = 0.0 },\n { year = 2002, stocks = 0.0, bonds = 0.0, cash = 0.0, inflation = 0.0 },\n]\n";
assert!(matches!(
History::from_toml_str(text),
Err(HistoryError::NotConsecutive)
));
}
}