Skip to main content

use_return/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7/// Common return primitives.
8pub mod prelude {
9    pub use crate::{
10        LogReturn, ReturnError, ReturnKind, ReturnKindParseError, ReturnValue, SimpleReturn,
11    };
12}
13
14/// A finite simple return value.
15#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
16pub struct SimpleReturn {
17    value: f64,
18}
19
20impl SimpleReturn {
21    /// Creates a simple return from a finite value.
22    ///
23    /// # Errors
24    ///
25    /// Returns [`ReturnError::NonFiniteReturn`] when `value` is not finite.
26    pub fn new(value: f64) -> Result<Self, ReturnError> {
27        validate_return(value).map(|value| Self { value })
28    }
29
30    /// Computes a simple return as `end_price / start_price - 1.0`.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`ReturnError`] when either price is not finite, the start price is not positive,
35    /// or the end price is negative.
36    pub fn from_prices(start_price: f64, end_price: f64) -> Result<Self, ReturnError> {
37        validate_start_price(start_price)?;
38        validate_end_price_for_simple_return(end_price)?;
39
40        Self::new((end_price / start_price) - 1.0)
41    }
42
43    /// Returns the stored return value.
44    #[must_use]
45    pub const fn value(self) -> f64 {
46        self.value
47    }
48}
49
50impl fmt::Display for SimpleReturn {
51    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
52        self.value.fmt(formatter)
53    }
54}
55
56/// A finite log return value.
57#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
58pub struct LogReturn {
59    value: f64,
60}
61
62impl LogReturn {
63    /// Creates a log return from a finite value.
64    ///
65    /// # Errors
66    ///
67    /// Returns [`ReturnError::NonFiniteReturn`] when `value` is not finite.
68    pub fn new(value: f64) -> Result<Self, ReturnError> {
69        validate_return(value).map(|value| Self { value })
70    }
71
72    /// Computes a log return as `ln(end_price / start_price)`.
73    ///
74    /// # Errors
75    ///
76    /// Returns [`ReturnError`] when either price is not finite, the start price is not positive,
77    /// or the end price is not positive.
78    pub fn from_prices(start_price: f64, end_price: f64) -> Result<Self, ReturnError> {
79        validate_start_price(start_price)?;
80        validate_end_price_for_log_return(end_price)?;
81
82        Self::new((end_price / start_price).ln())
83    }
84
85    /// Returns the stored return value.
86    #[must_use]
87    pub const fn value(self) -> f64 {
88        self.value
89    }
90}
91
92impl fmt::Display for LogReturn {
93    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94        self.value.fmt(formatter)
95    }
96}
97
98/// Descriptive return kind vocabulary.
99#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
100pub enum ReturnKind {
101    /// Simple arithmetic return.
102    Simple,
103    /// Logarithmic return.
104    Log,
105    /// Gross return vocabulary.
106    Gross,
107    /// Net return vocabulary.
108    Net,
109    /// Excess return vocabulary.
110    Excess,
111    /// Unknown return kind.
112    Unknown,
113    /// Caller-defined return kind.
114    Custom(String),
115}
116
117impl fmt::Display for ReturnKind {
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        formatter.write_str(match self {
120            Self::Simple => "simple",
121            Self::Log => "log",
122            Self::Gross => "gross",
123            Self::Net => "net",
124            Self::Excess => "excess",
125            Self::Unknown => "unknown",
126            Self::Custom(value) => value.as_str(),
127        })
128    }
129}
130
131impl FromStr for ReturnKind {
132    type Err = ReturnKindParseError;
133
134    fn from_str(value: &str) -> Result<Self, Self::Err> {
135        let trimmed = value.trim();
136        if trimmed.is_empty() {
137            return Err(ReturnKindParseError::Empty);
138        }
139
140        match normalized_token(trimmed).as_str() {
141            "simple" => Ok(Self::Simple),
142            "log" => Ok(Self::Log),
143            "gross" => Ok(Self::Gross),
144            "net" => Ok(Self::Net),
145            "excess" => Ok(Self::Excess),
146            "unknown" => Ok(Self::Unknown),
147            _ => Ok(Self::Custom(trimmed.to_string())),
148        }
149    }
150}
151
152/// Errors returned while parsing return kinds.
153#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154pub enum ReturnKindParseError {
155    /// The input was empty after trimming whitespace.
156    Empty,
157}
158
159impl fmt::Display for ReturnKindParseError {
160    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161        match self {
162            Self::Empty => formatter.write_str("return kind cannot be empty"),
163        }
164    }
165}
166
167impl Error for ReturnKindParseError {}
168
169/// A return value paired with descriptive return-kind vocabulary.
170#[derive(Clone, Debug, PartialEq)]
171pub struct ReturnValue {
172    kind: ReturnKind,
173    value: f64,
174}
175
176impl ReturnValue {
177    /// Creates a return value from a kind and finite numeric value.
178    ///
179    /// # Errors
180    ///
181    /// Returns [`ReturnError::NonFiniteReturn`] when `value` is not finite.
182    pub fn new(kind: ReturnKind, value: f64) -> Result<Self, ReturnError> {
183        Ok(Self {
184            kind,
185            value: validate_return(value)?,
186        })
187    }
188
189    /// Returns the return kind.
190    #[must_use]
191    pub const fn kind(&self) -> &ReturnKind {
192        &self.kind
193    }
194
195    /// Returns the numeric return value.
196    #[must_use]
197    pub const fn value(&self) -> f64 {
198        self.value
199    }
200}
201
202/// Errors returned by return value construction and price-to-return helpers.
203#[derive(Clone, Copy, Debug, Eq, PartialEq)]
204pub enum ReturnError {
205    /// Return values must be finite.
206    NonFiniteReturn,
207    /// Price inputs must be finite.
208    NonFinitePrice { name: &'static str },
209    /// Start prices must be strictly positive.
210    NonPositiveStartPrice,
211    /// End prices must not be negative for simple returns.
212    NegativeEndPrice,
213    /// End prices must be strictly positive for log returns.
214    NonPositiveEndPrice,
215}
216
217impl fmt::Display for ReturnError {
218    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
219        match self {
220            Self::NonFiniteReturn => formatter.write_str("return value must be finite"),
221            Self::NonFinitePrice { name } => write!(formatter, "{name} price must be finite"),
222            Self::NonPositiveStartPrice => formatter.write_str("start price must be positive"),
223            Self::NegativeEndPrice => {
224                formatter.write_str("end price cannot be negative for simple return")
225            },
226            Self::NonPositiveEndPrice => {
227                formatter.write_str("end price must be positive for log return")
228            },
229        }
230    }
231}
232
233impl Error for ReturnError {}
234
235const fn validate_return(value: f64) -> Result<f64, ReturnError> {
236    if value.is_finite() {
237        Ok(value)
238    } else {
239        Err(ReturnError::NonFiniteReturn)
240    }
241}
242
243fn validate_start_price(value: f64) -> Result<(), ReturnError> {
244    if !value.is_finite() {
245        return Err(ReturnError::NonFinitePrice { name: "start" });
246    }
247
248    if value <= 0.0 {
249        return Err(ReturnError::NonPositiveStartPrice);
250    }
251
252    Ok(())
253}
254
255fn validate_end_price_for_simple_return(value: f64) -> Result<(), ReturnError> {
256    if !value.is_finite() {
257        return Err(ReturnError::NonFinitePrice { name: "end" });
258    }
259
260    if value < 0.0 {
261        return Err(ReturnError::NegativeEndPrice);
262    }
263
264    Ok(())
265}
266
267fn validate_end_price_for_log_return(value: f64) -> Result<(), ReturnError> {
268    if !value.is_finite() {
269        return Err(ReturnError::NonFinitePrice { name: "end" });
270    }
271
272    if value <= 0.0 {
273        return Err(ReturnError::NonPositiveEndPrice);
274    }
275
276    Ok(())
277}
278
279fn normalized_token(value: &str) -> String {
280    value
281        .trim()
282        .chars()
283        .map(|character| match character {
284            '_' | ' ' => '-',
285            other => other.to_ascii_lowercase(),
286        })
287        .collect()
288}
289
290#[cfg(test)]
291mod tests {
292    use super::{LogReturn, ReturnError, ReturnKind, SimpleReturn};
293
294    fn assert_close(left: f64, right: f64) {
295        assert!((left - right).abs() < 1.0e-12, "left={left}, right={right}");
296    }
297
298    #[test]
299    fn computes_simple_return() {
300        let value = SimpleReturn::from_prices(100.0, 105.0).expect("return should compute");
301
302        assert_close(value.value(), 0.05);
303    }
304
305    #[test]
306    fn computes_log_return() {
307        let value = LogReturn::from_prices(100.0, 105.0).expect("return should compute");
308
309        assert_close(value.value(), 1.05_f64.ln());
310    }
311
312    #[test]
313    fn rejects_zero_start_price() {
314        assert_eq!(
315            SimpleReturn::from_prices(0.0, 105.0),
316            Err(ReturnError::NonPositiveStartPrice)
317        );
318    }
319
320    #[test]
321    fn displays_and_parses_return_kind() {
322        let kind: ReturnKind = "gross".parse().expect("kind should parse");
323
324        assert_eq!(kind, ReturnKind::Gross);
325        assert_eq!(kind.to_string(), "gross");
326    }
327
328    #[test]
329    fn supports_custom_return_kind() {
330        let kind: ReturnKind = "after-fee".parse().expect("kind should parse");
331
332        assert_eq!(kind, ReturnKind::Custom("after-fee".to_string()));
333    }
334}