Skip to main content

use_factor/
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 factor primitives.
8pub mod prelude {
9    pub use crate::{FactorError, FactorExposure, FactorLoading, FactorModelName, FactorName};
10}
11
12/// A non-empty factor name.
13#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct FactorName(String);
15
16impl FactorName {
17    /// Creates a factor name from non-empty text.
18    ///
19    /// # Errors
20    ///
21    /// Returns [`FactorError::EmptyName`] when the trimmed value is empty.
22    pub fn new(value: impl AsRef<str>) -> Result<Self, FactorError> {
23        non_empty_text(value).map(Self)
24    }
25
26    /// Returns the factor name.
27    #[must_use]
28    pub fn as_str(&self) -> &str {
29        &self.0
30    }
31}
32
33impl AsRef<str> for FactorName {
34    fn as_ref(&self) -> &str {
35        self.as_str()
36    }
37}
38
39impl fmt::Display for FactorName {
40    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41        formatter.write_str(self.as_str())
42    }
43}
44
45impl FromStr for FactorName {
46    type Err = FactorError;
47
48    fn from_str(value: &str) -> Result<Self, Self::Err> {
49        Self::new(value)
50    }
51}
52
53/// A non-empty factor model name.
54#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
55pub struct FactorModelName(String);
56
57impl FactorModelName {
58    /// Creates a factor model name from non-empty text.
59    ///
60    /// # Errors
61    ///
62    /// Returns [`FactorError::EmptyName`] when the trimmed value is empty.
63    pub fn new(value: impl AsRef<str>) -> Result<Self, FactorError> {
64        non_empty_text(value).map(Self)
65    }
66
67    /// Returns the model name.
68    #[must_use]
69    pub fn as_str(&self) -> &str {
70        &self.0
71    }
72}
73
74impl AsRef<str> for FactorModelName {
75    fn as_ref(&self) -> &str {
76        self.as_str()
77    }
78}
79
80impl fmt::Display for FactorModelName {
81    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
82        formatter.write_str(self.as_str())
83    }
84}
85
86impl FromStr for FactorModelName {
87    type Err = FactorError;
88
89    fn from_str(value: &str) -> Result<Self, Self::Err> {
90        Self::new(value)
91    }
92}
93
94/// A factor exposure value.
95#[derive(Clone, Debug, PartialEq)]
96pub struct FactorExposure {
97    factor: FactorName,
98    value: f64,
99}
100
101impl FactorExposure {
102    /// Creates a factor exposure from a factor and finite value.
103    ///
104    /// # Errors
105    ///
106    /// Returns [`FactorError::NonFiniteValue`] when `value` is not finite.
107    pub fn new(factor: FactorName, value: f64) -> Result<Self, FactorError> {
108        Ok(Self {
109            factor,
110            value: finite_value(value)?,
111        })
112    }
113
114    /// Returns the factor name.
115    #[must_use]
116    pub const fn factor(&self) -> &FactorName {
117        &self.factor
118    }
119
120    /// Returns the exposure value.
121    #[must_use]
122    pub const fn value(&self) -> f64 {
123        self.value
124    }
125}
126
127/// A factor loading value.
128#[derive(Clone, Debug, PartialEq)]
129pub struct FactorLoading {
130    factor: FactorName,
131    value: f64,
132}
133
134impl FactorLoading {
135    /// Creates a factor loading from a factor and finite value.
136    ///
137    /// # Errors
138    ///
139    /// Returns [`FactorError::NonFiniteValue`] when `value` is not finite.
140    pub fn new(factor: FactorName, value: f64) -> Result<Self, FactorError> {
141        Ok(Self {
142            factor,
143            value: finite_value(value)?,
144        })
145    }
146
147    /// Returns the factor name.
148    #[must_use]
149    pub const fn factor(&self) -> &FactorName {
150        &self.factor
151    }
152
153    /// Returns the loading value.
154    #[must_use]
155    pub const fn value(&self) -> f64 {
156        self.value
157    }
158}
159
160/// Errors returned by factor helpers.
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum FactorError {
163    /// Names must be non-empty after trimming whitespace.
164    EmptyName,
165    /// Exposure and loading values must be finite.
166    NonFiniteValue,
167}
168
169impl fmt::Display for FactorError {
170    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
171        match self {
172            Self::EmptyName => formatter.write_str("factor name cannot be empty"),
173            Self::NonFiniteValue => formatter.write_str("factor value must be finite"),
174        }
175    }
176}
177
178impl Error for FactorError {}
179
180fn non_empty_text(value: impl AsRef<str>) -> Result<String, FactorError> {
181    let trimmed = value.as_ref().trim();
182    if trimmed.is_empty() {
183        Err(FactorError::EmptyName)
184    } else {
185        Ok(trimmed.to_string())
186    }
187}
188
189const fn finite_value(value: f64) -> Result<f64, FactorError> {
190    if value.is_finite() {
191        Ok(value)
192    } else {
193        Err(FactorError::NonFiniteValue)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use std::collections::BTreeMap;
200
201    use super::{FactorError, FactorExposure, FactorLoading, FactorName};
202
203    #[test]
204    fn accepts_valid_factor_name() {
205        let name = FactorName::new("momentum").expect("name should be valid");
206
207        assert_eq!(name.as_str(), "momentum");
208    }
209
210    #[test]
211    fn rejects_empty_factor_name() {
212        assert_eq!(FactorName::new(" \t"), Err(FactorError::EmptyName));
213    }
214
215    #[test]
216    fn constructs_exposure() {
217        let exposure = FactorExposure::new(
218            FactorName::new("quality").expect("name should be valid"),
219            0.7,
220        )
221        .expect("exposure should be valid");
222
223        assert!((exposure.value() - 0.7).abs() < f64::EPSILON);
224    }
225
226    #[test]
227    fn constructs_loading() {
228        let loading = FactorLoading::new(
229            FactorName::new("market").expect("name should be valid"),
230            1.2,
231        )
232        .expect("loading should be valid");
233
234        assert!((loading.value() - 1.2).abs() < f64::EPSILON);
235    }
236
237    #[test]
238    fn factor_names_sort_deterministically() {
239        let mut exposures = BTreeMap::new();
240        exposures.insert(FactorName::new("value").expect("name should be valid"), 0.1);
241        exposures.insert(
242            FactorName::new("market").expect("name should be valid"),
243            0.2,
244        );
245
246        let names: Vec<&str> = exposures.keys().map(FactorName::as_str).collect();
247        assert_eq!(names, vec!["market", "value"]);
248    }
249}