nautilus_core/
parsing.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.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#[must_use]
20pub fn precision_from_str(s: &str) -> u8 {
21    let s = s.trim().to_ascii_lowercase();
22
23    // Check for scientific notation
24    if s.contains("e-") {
25        return s.split("e-").last().unwrap().parse::<u8>().unwrap();
26    }
27
28    // Check for decimal precision
29    if let Some((_, decimal_part)) = s.split_once('.') {
30        decimal_part.len() as u8
31    } else {
32        0
33    }
34}
35
36/// Returns the minimum increment precision inferred from the given string,
37/// ignoring trailing zeros.
38#[must_use]
39pub fn min_increment_precision_from_str(s: &str) -> u8 {
40    let s = s.trim().to_ascii_lowercase();
41
42    // Check for scientific notation
43    if let Some(pos) = s.find('e') {
44        if s[pos + 1..].starts_with('-') {
45            return s[pos + 2..].parse::<u8>().unwrap_or(0);
46        }
47    }
48
49    // Check for decimal precision
50    if let Some(dot_pos) = s.find('.') {
51        let decimal_part = &s[dot_pos + 1..];
52        if decimal_part.chars().any(|c| c != '0') {
53            let trimmed_len = decimal_part.trim_end_matches('0').len();
54            return trimmed_len as u8;
55        } else {
56            return decimal_part.len() as u8;
57        }
58    }
59
60    0
61}
62
63/// Returns a `usize` from the given bytes.
64///
65/// # Errors
66///
67/// Returns an error if there are not enough bytes to represent a `usize`.
68pub fn bytes_to_usize(bytes: &[u8]) -> anyhow::Result<usize> {
69    // Check bytes width
70    if bytes.len() >= std::mem::size_of::<usize>() {
71        let mut buffer = [0u8; std::mem::size_of::<usize>()];
72        buffer.copy_from_slice(&bytes[..std::mem::size_of::<usize>()]);
73
74        Ok(usize::from_le_bytes(buffer))
75    } else {
76        Err(anyhow::anyhow!("Not enough bytes to represent a `usize`"))
77    }
78}
79
80////////////////////////////////////////////////////////////////////////////////
81// Tests
82////////////////////////////////////////////////////////////////////////////////
83#[cfg(test)]
84mod tests {
85    use rstest::rstest;
86
87    use super::*;
88
89    #[rstest]
90    #[case("", 0)]
91    #[case("0", 0)]
92    #[case("1.0", 1)]
93    #[case("1.00", 2)]
94    #[case("1.23456789", 8)]
95    #[case("123456.789101112", 9)]
96    #[case("0.000000001", 9)]
97    #[case("1e-1", 1)]
98    #[case("1e-2", 2)]
99    #[case("1e-3", 3)]
100    #[case("1e8", 0)]
101    #[case("-1.23", 2)]
102    #[case("-1e-2", 2)]
103    #[case("1E-2", 2)]
104    #[case("  1.23", 2)]
105    #[case("1.23  ", 2)]
106    fn test_precision_from_str(#[case] s: &str, #[case] expected: u8) {
107        let result = precision_from_str(s);
108        assert_eq!(result, expected);
109    }
110
111    #[rstest]
112    #[case("", 0)]
113    #[case("0", 0)]
114    #[case("1.0", 1)]
115    #[case("1.00", 2)]
116    #[case("1.23456789", 8)]
117    #[case("123456.789101112", 9)]
118    #[case("0.000000001", 9)]
119    #[case("1e-1", 1)]
120    #[case("1e-2", 2)]
121    #[case("1e-3", 3)]
122    #[case("1e8", 0)]
123    #[case("-1.23", 2)]
124    #[case("-1e-2", 2)]
125    #[case("1E-2", 2)]
126    #[case("  1.23", 2)]
127    #[case("1.23  ", 2)]
128    #[case("1.010", 2)]
129    #[case("1.00100", 3)]
130    #[case("0.0001000", 4)]
131    #[case("1.000000000", 9)]
132    fn test_min_increment_precision_from_str(#[case] s: &str, #[case] expected: u8) {
133        let result = min_increment_precision_from_str(s);
134        assert_eq!(result, expected);
135    }
136
137    #[rstest]
138    fn test_bytes_to_usize_empty() {
139        let payload: Vec<u8> = vec![];
140        let result = bytes_to_usize(&payload);
141        assert!(result.is_err());
142        assert_eq!(
143            result.err().unwrap().to_string(),
144            "Not enough bytes to represent a `usize`"
145        );
146    }
147
148    #[rstest]
149    fn test_bytes_to_usize_invalid() {
150        let payload: Vec<u8> = vec![0x01, 0x02, 0x03];
151        let result = bytes_to_usize(&payload);
152        assert!(result.is_err());
153        assert_eq!(
154            result.err().unwrap().to_string(),
155            "Not enough bytes to represent a `usize`"
156        );
157    }
158
159    #[rstest]
160    fn test_bytes_to_usize_valid() {
161        let payload: Vec<u8> = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
162        let result = bytes_to_usize(&payload).unwrap();
163        assert_eq!(result, 0x0807_0605_0403_0201);
164        assert_eq!(result, 578_437_695_752_307_201);
165    }
166}