use chrono::{DateTime, NaiveDateTime};
use polars::prelude::{PolarsError, Series};
use std::fmt;
#[derive(Debug)]
pub enum FinalyticsError {
DataFetch { source: String, message: String },
DataParse { source: String, message: String },
DtypeMismatch {
column: String,
expected: String,
actual: String,
},
NullValues { column: String, null_count: usize },
ColumnNotFound { name: String },
DataFrameOperation { message: String },
InsufficientData {
required: usize,
actual: usize,
context: String,
},
NonFiniteResult { context: String },
OptimizationFailed { objective: String, message: String },
InvalidParameter { name: String, message: String },
Polars(PolarsError),
External(Box<dyn std::error::Error + Send + Sync>),
}
impl fmt::Display for FinalyticsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FinalyticsError::DataFetch { source, message } => {
write!(f, "[DataFetch] {source}: {message}")
}
FinalyticsError::DataParse { source, message } => {
write!(f, "[DataParse] {source}: {message}")
}
FinalyticsError::DtypeMismatch {
column,
expected,
actual,
} => {
write!(
f,
"[DtypeMismatch] Column '{column}': expected {expected}, got {actual}"
)
}
FinalyticsError::NullValues { column, null_count } => {
write!(
f,
"[NullValues] Column '{column}' contains {null_count} null value(s)"
)
}
FinalyticsError::ColumnNotFound { name } => {
write!(f, "[ColumnNotFound] Column '{name}' not found in DataFrame")
}
FinalyticsError::DataFrameOperation { message } => {
write!(f, "[DataFrameOperation] {message}")
}
FinalyticsError::InsufficientData {
required,
actual,
context,
} => {
write!(
f,
"[InsufficientData] {context}: need at least {required} observations, got {actual}"
)
}
FinalyticsError::NonFiniteResult { context } => {
write!(f, "[NonFiniteResult] {context}: result is NaN or infinite")
}
FinalyticsError::OptimizationFailed { objective, message } => {
write!(f, "[OptimizationFailed] Objective '{objective}': {message}")
}
FinalyticsError::InvalidParameter { name, message } => {
write!(f, "[InvalidParameter] '{name}': {message}")
}
FinalyticsError::Polars(e) => write!(f, "[Polars] {e}"),
FinalyticsError::External(e) => write!(f, "{e}"),
}
}
}
impl std::error::Error for FinalyticsError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
FinalyticsError::Polars(e) => Some(e),
FinalyticsError::External(e) => Some(e.as_ref()),
_ => None,
}
}
}
impl From<PolarsError> for FinalyticsError {
fn from(e: PolarsError) -> Self {
FinalyticsError::Polars(e)
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for FinalyticsError {
fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
FinalyticsError::External(e)
}
}
impl From<Box<dyn std::error::Error>> for FinalyticsError {
fn from(e: Box<dyn std::error::Error>) -> Self {
FinalyticsError::External(e.to_string().into())
}
}
impl From<String> for FinalyticsError {
fn from(s: String) -> Self {
FinalyticsError::External(s.into())
}
}
impl From<&str> for FinalyticsError {
fn from(s: &str) -> Self {
FinalyticsError::External(s.into())
}
}
impl From<serde_json::Error> for FinalyticsError {
fn from(e: serde_json::Error) -> Self {
FinalyticsError::DataParse {
source: "serde_json".into(),
message: e.to_string(),
}
}
}
impl From<reqwest::Error> for FinalyticsError {
fn from(e: reqwest::Error) -> Self {
FinalyticsError::DataFetch {
source: "reqwest".into(),
message: e.to_string(),
}
}
}
impl From<std::io::Error> for FinalyticsError {
fn from(e: std::io::Error) -> Self {
FinalyticsError::External(Box::new(e))
}
}
impl From<std::num::ParseFloatError> for FinalyticsError {
fn from(e: std::num::ParseFloatError) -> Self {
FinalyticsError::DataParse {
source: "parse_float".into(),
message: e.to_string(),
}
}
}
impl From<std::num::ParseIntError> for FinalyticsError {
fn from(e: std::num::ParseIntError) -> Self {
FinalyticsError::DataParse {
source: "parse_int".into(),
message: e.to_string(),
}
}
}
impl From<chrono::ParseError> for FinalyticsError {
fn from(e: chrono::ParseError) -> Self {
FinalyticsError::DataParse {
source: "chrono".into(),
message: e.to_string(),
}
}
}
pub fn series_to_vec_f64(series: &Series, name: &str) -> Result<Vec<f64>, FinalyticsError> {
let ca = series.f64().map_err(|_| FinalyticsError::DtypeMismatch {
column: name.to_string(),
expected: "Float64".to_string(),
actual: format!("{:?}", series.dtype()),
})?;
let null_count = ca.null_count();
if null_count > 0 {
return Err(FinalyticsError::NullValues {
column: name.to_string(),
null_count,
});
}
Ok(ca.into_no_null_iter().collect())
}
pub fn series_to_optional_vec_f64(
series: &Series,
name: &str,
) -> Result<Vec<Option<f64>>, FinalyticsError> {
let ca = series.f64().map_err(|_| FinalyticsError::DtypeMismatch {
column: name.to_string(),
expected: "Float64".to_string(),
actual: format!("{:?}", series.dtype()),
})?;
Ok(ca.into_iter().collect())
}
pub fn column_to_vec_f64(
df: &polars::prelude::DataFrame,
name: &str,
) -> Result<Vec<f64>, FinalyticsError> {
let col = df
.column(name)
.map_err(|_| FinalyticsError::ColumnNotFound {
name: name.to_string(),
})?;
series_to_vec_f64(
col.as_series().unwrap_or_else(|| {
panic!("BUG: DataFrame column '{name}' could not be viewed as Series");
}),
name,
)
}
pub fn require_min_length(
series: &Series,
min_len: usize,
context: &str,
) -> Result<(), FinalyticsError> {
let actual = series.len() - series.null_count();
if actual < min_len {
return Err(FinalyticsError::InsufficientData {
required: min_len,
actual,
context: context.to_string(),
});
}
Ok(())
}
pub fn col_as_series(col: &polars::prelude::Column) -> Result<&Series, FinalyticsError> {
col.as_series()
.ok_or_else(|| FinalyticsError::DataFrameOperation {
message: format!("Column '{}' could not be viewed as a Series", col.name()),
})
}
pub fn series_to_naive_datetimes(
series: &Series,
name: &str,
) -> Result<Vec<NaiveDateTime>, FinalyticsError> {
let ca = series
.datetime()
.map_err(|_| FinalyticsError::DtypeMismatch {
column: name.to_string(),
expected: "Datetime".to_string(),
actual: format!("{:?}", series.dtype()),
})?;
let mut out = Vec::with_capacity(ca.len());
for opt_val in ca.into_iter() {
let millis = opt_val.ok_or_else(|| FinalyticsError::NullValues {
column: name.to_string(),
null_count: 1,
})?;
let dt = DateTime::from_timestamp_millis(millis)
.ok_or_else(|| FinalyticsError::DataParse {
source: "timestamp".to_string(),
message: format!(
"Column '{name}': millisecond value {millis} is not a valid timestamp"
),
})?
.naive_local();
out.push(dt);
}
Ok(out)
}
pub fn new_indicator<T, E: std::fmt::Display>(
name: &str,
f: impl FnOnce() -> Result<T, E>,
) -> Result<T, FinalyticsError> {
f().map_err(|e| FinalyticsError::InvalidParameter {
name: name.to_string(),
message: e.to_string(),
})
}