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::{MarketPrice, MarketPriceError, PriceKind, PriceKindParseError, PriceQuote};
10}
11
12#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
17pub struct MarketPrice {
18 value: f64,
19}
20
21impl MarketPrice {
22 pub fn new(value: f64) -> Result<Self, MarketPriceError> {
29 if !value.is_finite() {
30 return Err(MarketPriceError::NonFinite);
31 }
32
33 if value < 0.0 {
34 return Err(MarketPriceError::Negative);
35 }
36
37 Ok(Self { value })
38 }
39
40 #[must_use]
42 pub const fn value(self) -> f64 {
43 self.value
44 }
45
46 #[must_use]
48 pub fn is_positive(self) -> bool {
49 self.value > 0.0
50 }
51}
52
53impl TryFrom<f64> for MarketPrice {
54 type Error = MarketPriceError;
55
56 fn try_from(value: f64) -> Result<Self, Self::Error> {
57 Self::new(value)
58 }
59}
60
61impl fmt::Display for MarketPrice {
62 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
63 self.value.fmt(formatter)
64 }
65}
66
67#[derive(Clone, Copy, Debug, Eq, PartialEq)]
69pub enum MarketPriceError {
70 NonFinite,
72 Negative,
74}
75
76impl fmt::Display for MarketPriceError {
77 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::NonFinite => formatter.write_str("market price must be finite"),
80 Self::Negative => formatter.write_str("market price cannot be negative"),
81 }
82 }
83}
84
85impl Error for MarketPriceError {}
86
87#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
89pub enum PriceKind {
90 Open,
92 High,
94 Low,
96 Close,
98 Last,
100 Bid,
102 Ask,
104 Mid,
106 Settlement,
108 AdjustedClose,
110 Unknown,
112 Custom(String),
114}
115
116impl fmt::Display for PriceKind {
117 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
118 formatter.write_str(match self {
119 Self::Open => "open",
120 Self::High => "high",
121 Self::Low => "low",
122 Self::Close => "close",
123 Self::Last => "last",
124 Self::Bid => "bid",
125 Self::Ask => "ask",
126 Self::Mid => "mid",
127 Self::Settlement => "settlement",
128 Self::AdjustedClose => "adjusted-close",
129 Self::Unknown => "unknown",
130 Self::Custom(value) => value.as_str(),
131 })
132 }
133}
134
135impl FromStr for PriceKind {
136 type Err = PriceKindParseError;
137
138 fn from_str(value: &str) -> Result<Self, Self::Err> {
139 let trimmed = value.trim();
140 if trimmed.is_empty() {
141 return Err(PriceKindParseError::Empty);
142 }
143
144 match normalized_token(trimmed).as_str() {
145 "open" => Ok(Self::Open),
146 "high" => Ok(Self::High),
147 "low" => Ok(Self::Low),
148 "close" => Ok(Self::Close),
149 "last" => Ok(Self::Last),
150 "bid" => Ok(Self::Bid),
151 "ask" => Ok(Self::Ask),
152 "mid" => Ok(Self::Mid),
153 "settlement" => Ok(Self::Settlement),
154 "adjusted-close" => Ok(Self::AdjustedClose),
155 "unknown" => Ok(Self::Unknown),
156 _ => Ok(Self::Custom(trimmed.to_string())),
157 }
158 }
159}
160
161#[derive(Clone, Copy, Debug, Eq, PartialEq)]
163pub enum PriceKindParseError {
164 Empty,
166}
167
168impl fmt::Display for PriceKindParseError {
169 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
170 match self {
171 Self::Empty => formatter.write_str("price kind cannot be empty"),
172 }
173 }
174}
175
176impl Error for PriceKindParseError {}
177
178#[derive(Clone, Debug, PartialEq)]
180pub struct PriceQuote {
181 kind: PriceKind,
182 price: MarketPrice,
183}
184
185impl PriceQuote {
186 #[must_use]
188 pub const fn new(kind: PriceKind, price: MarketPrice) -> Self {
189 Self { kind, price }
190 }
191
192 #[must_use]
194 pub const fn kind(&self) -> &PriceKind {
195 &self.kind
196 }
197
198 #[must_use]
200 pub const fn price(&self) -> MarketPrice {
201 self.price
202 }
203}
204
205fn normalized_token(value: &str) -> String {
206 value
207 .trim()
208 .chars()
209 .map(|character| match character {
210 '_' | ' ' => '-',
211 other => other.to_ascii_lowercase(),
212 })
213 .collect()
214}
215
216#[cfg(test)]
217mod tests {
218 use super::{MarketPrice, MarketPriceError, PriceKind, PriceQuote};
219
220 #[test]
221 fn accepts_valid_positive_price() {
222 let price = MarketPrice::new(101.25).expect("price should be valid");
223
224 assert!((price.value() - 101.25).abs() < f64::EPSILON);
225 assert!(price.is_positive());
226 assert_eq!(price.to_string(), "101.25");
227 }
228
229 #[test]
230 fn rejects_negative_price() {
231 assert_eq!(MarketPrice::new(-0.01), Err(MarketPriceError::Negative));
232 }
233
234 #[test]
235 fn displays_and_parses_price_kind() {
236 let kind: PriceKind = "Adjusted Close".parse().expect("kind should parse");
237
238 assert_eq!(kind, PriceKind::AdjustedClose);
239 assert_eq!(kind.to_string(), "adjusted-close");
240 }
241
242 #[test]
243 fn supports_custom_price_kind() {
244 let kind: PriceKind = "auction-price".parse().expect("kind should parse");
245
246 assert_eq!(kind, PriceKind::Custom("auction-price".to_string()));
247 assert_eq!(kind.to_string(), "auction-price");
248 }
249
250 #[test]
251 fn constructs_quote() {
252 let price = MarketPrice::new(100.0).expect("price should be valid");
253 let quote = PriceQuote::new(PriceKind::Close, price);
254
255 assert_eq!(quote.kind(), &PriceKind::Close);
256 assert_eq!(quote.price(), price);
257 }
258}