1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
9 pub use crate::{
10 LogReturn, ReturnError, ReturnKind, ReturnKindParseError, ReturnValue, SimpleReturn,
11 };
12}
13
14#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
16pub struct SimpleReturn {
17 value: f64,
18}
19
20impl SimpleReturn {
21 pub fn new(value: f64) -> Result<Self, ReturnError> {
27 validate_return(value).map(|value| Self { value })
28 }
29
30 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 #[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#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
58pub struct LogReturn {
59 value: f64,
60}
61
62impl LogReturn {
63 pub fn new(value: f64) -> Result<Self, ReturnError> {
69 validate_return(value).map(|value| Self { value })
70 }
71
72 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 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
100pub enum ReturnKind {
101 Simple,
103 Log,
105 Gross,
107 Net,
109 Excess,
111 Unknown,
113 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#[derive(Clone, Copy, Debug, Eq, PartialEq)]
154pub enum ReturnKindParseError {
155 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#[derive(Clone, Debug, PartialEq)]
171pub struct ReturnValue {
172 kind: ReturnKind,
173 value: f64,
174}
175
176impl ReturnValue {
177 pub fn new(kind: ReturnKind, value: f64) -> Result<Self, ReturnError> {
183 Ok(Self {
184 kind,
185 value: validate_return(value)?,
186 })
187 }
188
189 #[must_use]
191 pub const fn kind(&self) -> &ReturnKind {
192 &self.kind
193 }
194
195 #[must_use]
197 pub const fn value(&self) -> f64 {
198 self.value
199 }
200}
201
202#[derive(Clone, Copy, Debug, Eq, PartialEq)]
204pub enum ReturnError {
205 NonFiniteReturn,
207 NonFinitePrice { name: &'static str },
209 NonPositiveStartPrice,
211 NegativeEndPrice,
213 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}