nautilus_core/
parsing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3//  https://poseitrader.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Core parsing functions.
17
18/// Returns the decimal precision inferred from the given string.
19///
20/// # Panics
21///
22/// Panics if the input string is not a valid decimal or scientific notation format.
23#[must_use]
24#[allow(clippy::cast_possible_truncation)]
25pub fn precision_from_str(s: &str) -> u8 {
26    let s = s.trim().to_ascii_lowercase();
27
28    // Check for scientific notation
29    if s.contains("e-") {
30        return s.split("e-").last().unwrap().parse::<u8>().unwrap();
31    }
32
33    // Check for decimal precision
34    if let Some((_, decimal_part)) = s.split_once('.') {
35        // Clamp decimal precision to u8::MAX for very long decimal strings
36        decimal_part.len().min(u8::MAX as usize) as u8
37    } else {
38        0
39    }
40}
41
42/// Returns the minimum increment precision inferred from the given string,
43/// ignoring trailing zeros.
44/// Returns the minimum increment precision inferred from the given string,
45/// ignoring trailing zeros.
46#[must_use]
47#[allow(clippy::cast_possible_truncation)]
48pub fn min_increment_precision_from_str(s: &str) -> u8 {
49    let s = s.trim().to_ascii_lowercase();
50
51    // Check for scientific notation
52    if let Some(pos) = s.find('e') {
53        if s[pos + 1..].starts_with('-') {
54            return s[pos + 2..].parse::<u8>().unwrap_or(0);
55        }
56    }
57
58    // Check for decimal precision
59    if let Some(dot_pos) = s.find('.') {
60        let decimal_part = &s[dot_pos + 1..];
61        if decimal_part.chars().any(|c| c != '0') {
62            let trimmed_len = decimal_part.trim_end_matches('0').len();
63            return trimmed_len.min(u8::MAX as usize) as u8;
64        }
65        return decimal_part.len().min(u8::MAX as usize) as u8;
66    }
67
68    0
69}
70
71/// Returns a `usize` from the given bytes.
72///
73/// # Errors
74///
75/// Returns an error if there are not enough bytes to represent a `usize`.
76pub fn bytes_to_usize(bytes: &[u8]) -> anyhow::Result<usize> {
77    // Check bytes width
78    if bytes.len() >= std::mem::size_of::<usize>() {
79        let mut buffer = [0u8; std::mem::size_of::<usize>()];
80        buffer.copy_from_slice(&bytes[..std::mem::size_of::<usize>()]);
81
82        Ok(usize::from_le_bytes(buffer))
83    } else {
84        anyhow::bail!("Not enough bytes to represent a `usize`");
85    }
86}
87
88////////////////////////////////////////////////////////////////////////////////
89// Tests
90////////////////////////////////////////////////////////////////////////////////
91#[cfg(test)]
92mod tests {
93    use rstest::rstest;
94
95    use super::*;
96
97    #[rstest]
98    #[case("", 0)]
99    #[case("0", 0)]
100    #[case("1.0", 1)]
101    #[case("1.00", 2)]
102    #[case("1.23456789", 8)]
103    #[case("123456.789101112", 9)]
104    #[case("0.000000001", 9)]
105    #[case("1e-1", 1)]
106    #[case("1e-2", 2)]
107    #[case("1e-3", 3)]
108    #[case("1e8", 0)]
109    #[case("-1.23", 2)]
110    #[case("-1e-2", 2)]
111    #[case("1E-2", 2)]
112    #[case("  1.23", 2)]
113    #[case("1.23  ", 2)]
114    fn test_precision_from_str(#[case] s: &str, #[case] expected: u8) {
115        let result = precision_from_str(s);
116        assert_eq!(result, expected);
117    }
118
119    #[rstest]
120    #[case("", 0)]
121    #[case("0", 0)]
122    #[case("1.0", 1)]
123    #[case("1.00", 2)]
124    #[case("1.23456789", 8)]
125    #[case("123456.789101112", 9)]
126    #[case("0.000000001", 9)]
127    #[case("1e-1", 1)]
128    #[case("1e-2", 2)]
129    #[case("1e-3", 3)]
130    #[case("1e8", 0)]
131    #[case("-1.23", 2)]
132    #[case("-1e-2", 2)]
133    #[case("1E-2", 2)]
134    #[case("  1.23", 2)]
135    #[case("1.23  ", 2)]
136    #[case("1.010", 2)]
137    #[case("1.00100", 3)]
138    #[case("0.0001000", 4)]
139    #[case("1.000000000", 9)]
140    fn test_min_increment_precision_from_str(#[case] s: &str, #[case] expected: u8) {
141        let result = min_increment_precision_from_str(s);
142        assert_eq!(result, expected);
143    }
144
145    #[rstest]
146    fn test_bytes_to_usize_empty() {
147        let payload: Vec<u8> = vec![];
148        let result = bytes_to_usize(&payload);
149        assert!(result.is_err());
150        assert_eq!(
151            result.err().unwrap().to_string(),
152            "Not enough bytes to represent a `usize`"
153        );
154    }
155
156    #[rstest]
157    fn test_bytes_to_usize_invalid() {
158        let payload: Vec<u8> = vec![0x01, 0x02, 0x03];
159        let result = bytes_to_usize(&payload);
160        assert!(result.is_err());
161        assert_eq!(
162            result.err().unwrap().to_string(),
163            "Not enough bytes to represent a `usize`"
164        );
165    }
166
167    #[rstest]
168    fn test_bytes_to_usize_valid() {
169        let payload: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
170        let result = bytes_to_usize(&payload).unwrap();
171        assert_eq!(result, 0x0807_0605_0403_0201);
172        assert_eq!(result, 578_437_695_752_307_201);
173    }
174}