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::{FactorError, FactorExposure, FactorLoading, FactorModelName, FactorName};
10}
11
12#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
14pub struct FactorName(String);
15
16impl FactorName {
17 pub fn new(value: impl AsRef<str>) -> Result<Self, FactorError> {
23 non_empty_text(value).map(Self)
24 }
25
26 #[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#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
55pub struct FactorModelName(String);
56
57impl FactorModelName {
58 pub fn new(value: impl AsRef<str>) -> Result<Self, FactorError> {
64 non_empty_text(value).map(Self)
65 }
66
67 #[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#[derive(Clone, Debug, PartialEq)]
96pub struct FactorExposure {
97 factor: FactorName,
98 value: f64,
99}
100
101impl FactorExposure {
102 pub fn new(factor: FactorName, value: f64) -> Result<Self, FactorError> {
108 Ok(Self {
109 factor,
110 value: finite_value(value)?,
111 })
112 }
113
114 #[must_use]
116 pub const fn factor(&self) -> &FactorName {
117 &self.factor
118 }
119
120 #[must_use]
122 pub const fn value(&self) -> f64 {
123 self.value
124 }
125}
126
127#[derive(Clone, Debug, PartialEq)]
129pub struct FactorLoading {
130 factor: FactorName,
131 value: f64,
132}
133
134impl FactorLoading {
135 pub fn new(factor: FactorName, value: f64) -> Result<Self, FactorError> {
141 Ok(Self {
142 factor,
143 value: finite_value(value)?,
144 })
145 }
146
147 #[must_use]
149 pub const fn factor(&self) -> &FactorName {
150 &self.factor
151 }
152
153 #[must_use]
155 pub const fn value(&self) -> f64 {
156 self.value
157 }
158}
159
160#[derive(Clone, Copy, Debug, Eq, PartialEq)]
162pub enum FactorError {
163 EmptyName,
165 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}