#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::{fmt, str::FromStr};
use std::error::Error;
pub mod prelude {
pub use crate::{
LogReturn, ReturnError, ReturnKind, ReturnKindParseError, ReturnValue, SimpleReturn,
};
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct SimpleReturn {
value: f64,
}
impl SimpleReturn {
pub fn new(value: f64) -> Result<Self, ReturnError> {
validate_return(value).map(|value| Self { value })
}
pub fn from_prices(start_price: f64, end_price: f64) -> Result<Self, ReturnError> {
validate_start_price(start_price)?;
validate_end_price_for_simple_return(end_price)?;
Self::new((end_price / start_price) - 1.0)
}
#[must_use]
pub const fn value(self) -> f64 {
self.value
}
}
impl fmt::Display for SimpleReturn {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.value.fmt(formatter)
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
pub struct LogReturn {
value: f64,
}
impl LogReturn {
pub fn new(value: f64) -> Result<Self, ReturnError> {
validate_return(value).map(|value| Self { value })
}
pub fn from_prices(start_price: f64, end_price: f64) -> Result<Self, ReturnError> {
validate_start_price(start_price)?;
validate_end_price_for_log_return(end_price)?;
Self::new((end_price / start_price).ln())
}
#[must_use]
pub const fn value(self) -> f64 {
self.value
}
}
impl fmt::Display for LogReturn {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
self.value.fmt(formatter)
}
}
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum ReturnKind {
Simple,
Log,
Gross,
Net,
Excess,
Unknown,
Custom(String),
}
impl fmt::Display for ReturnKind {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::Simple => "simple",
Self::Log => "log",
Self::Gross => "gross",
Self::Net => "net",
Self::Excess => "excess",
Self::Unknown => "unknown",
Self::Custom(value) => value.as_str(),
})
}
}
impl FromStr for ReturnKind {
type Err = ReturnKindParseError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let trimmed = value.trim();
if trimmed.is_empty() {
return Err(ReturnKindParseError::Empty);
}
match normalized_token(trimmed).as_str() {
"simple" => Ok(Self::Simple),
"log" => Ok(Self::Log),
"gross" => Ok(Self::Gross),
"net" => Ok(Self::Net),
"excess" => Ok(Self::Excess),
"unknown" => Ok(Self::Unknown),
_ => Ok(Self::Custom(trimmed.to_string())),
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReturnKindParseError {
Empty,
}
impl fmt::Display for ReturnKindParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => formatter.write_str("return kind cannot be empty"),
}
}
}
impl Error for ReturnKindParseError {}
#[derive(Clone, Debug, PartialEq)]
pub struct ReturnValue {
kind: ReturnKind,
value: f64,
}
impl ReturnValue {
pub fn new(kind: ReturnKind, value: f64) -> Result<Self, ReturnError> {
Ok(Self {
kind,
value: validate_return(value)?,
})
}
#[must_use]
pub const fn kind(&self) -> &ReturnKind {
&self.kind
}
#[must_use]
pub const fn value(&self) -> f64 {
self.value
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ReturnError {
NonFiniteReturn,
NonFinitePrice { name: &'static str },
NonPositiveStartPrice,
NegativeEndPrice,
NonPositiveEndPrice,
}
impl fmt::Display for ReturnError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NonFiniteReturn => formatter.write_str("return value must be finite"),
Self::NonFinitePrice { name } => write!(formatter, "{name} price must be finite"),
Self::NonPositiveStartPrice => formatter.write_str("start price must be positive"),
Self::NegativeEndPrice => {
formatter.write_str("end price cannot be negative for simple return")
},
Self::NonPositiveEndPrice => {
formatter.write_str("end price must be positive for log return")
},
}
}
}
impl Error for ReturnError {}
const fn validate_return(value: f64) -> Result<f64, ReturnError> {
if value.is_finite() {
Ok(value)
} else {
Err(ReturnError::NonFiniteReturn)
}
}
fn validate_start_price(value: f64) -> Result<(), ReturnError> {
if !value.is_finite() {
return Err(ReturnError::NonFinitePrice { name: "start" });
}
if value <= 0.0 {
return Err(ReturnError::NonPositiveStartPrice);
}
Ok(())
}
fn validate_end_price_for_simple_return(value: f64) -> Result<(), ReturnError> {
if !value.is_finite() {
return Err(ReturnError::NonFinitePrice { name: "end" });
}
if value < 0.0 {
return Err(ReturnError::NegativeEndPrice);
}
Ok(())
}
fn validate_end_price_for_log_return(value: f64) -> Result<(), ReturnError> {
if !value.is_finite() {
return Err(ReturnError::NonFinitePrice { name: "end" });
}
if value <= 0.0 {
return Err(ReturnError::NonPositiveEndPrice);
}
Ok(())
}
fn normalized_token(value: &str) -> String {
value
.trim()
.chars()
.map(|character| match character {
'_' | ' ' => '-',
other => other.to_ascii_lowercase(),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::{LogReturn, ReturnError, ReturnKind, SimpleReturn};
fn assert_close(left: f64, right: f64) {
assert!((left - right).abs() < 1.0e-12, "left={left}, right={right}");
}
#[test]
fn computes_simple_return() {
let value = SimpleReturn::from_prices(100.0, 105.0).expect("return should compute");
assert_close(value.value(), 0.05);
}
#[test]
fn computes_log_return() {
let value = LogReturn::from_prices(100.0, 105.0).expect("return should compute");
assert_close(value.value(), 1.05_f64.ln());
}
#[test]
fn rejects_zero_start_price() {
assert_eq!(
SimpleReturn::from_prices(0.0, 105.0),
Err(ReturnError::NonPositiveStartPrice)
);
}
#[test]
fn displays_and_parses_return_kind() {
let kind: ReturnKind = "gross".parse().expect("kind should parse");
assert_eq!(kind, ReturnKind::Gross);
assert_eq!(kind.to_string(), "gross");
}
#[test]
fn supports_custom_return_kind() {
let kind: ReturnKind = "after-fee".parse().expect("kind should parse");
assert_eq!(kind, ReturnKind::Custom("after-fee".to_string()));
}
}