fixed_num_helper/
lib.rs

1use std::error::Error;
2use std::fmt::Display;
3
4// ==============
5// === Consts ===
6// ==============
7
8/// The number of digits after the dot.
9pub const FRAC_PLACES: u32 = 19;
10
11/// Scale that moves [`FRAC_PLACES`] fractional digits into the integer part when multiplied.
12pub const FRAC_SCALE_U128: u128 = 10_u128.pow(FRAC_PLACES);
13pub const FRAC_SCALE_I128: i128 = FRAC_SCALE_U128 as i128;
14
15// ======================
16// === ParseF128Error ===
17// ======================
18
19#[derive(Debug, Eq, PartialEq)]
20pub enum ParseDec19x19Error {
21    ParseIntError(std::num::ParseIntError),
22    OutOfBounds,
23    TooPrecise,
24    InvalidChar { char: char, pos: usize },
25}
26
27impl From<std::num::ParseIntError> for ParseDec19x19Error {
28    fn from(err: std::num::ParseIntError) -> Self {
29        Self::ParseIntError(err)
30    }
31}
32
33impl Error for ParseDec19x19Error {}
34impl Display for ParseDec19x19Error {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            Self::ParseIntError(err) => Display::fmt(err, f),
38            Self::OutOfBounds => write!(f, "Value out of bounds"),
39            Self::TooPrecise => write!(f, "Value too precise"),
40            Self::InvalidChar { char, pos } =>
41                write!(f, "Invalid character `{char}` at position {pos}"),
42        }
43    }
44}
45
46// ===============
47// === Parsing ===
48// ===============
49
50/// Shifts digits between the integer and fractional part strings based on the given exponent.
51///
52/// # Examples
53///
54/// ```
55/// # use fixed_num_helper::*;
56/// fn test(inp: (&str, &str, i128), out: (&str, &str)) {
57///     assert_eq!(shift_decimal(inp.0, inp.1, inp.2), (out.0.to_string(), out.1.to_string()));
58/// }
59/// test(("123", "456", -5), ("0", "00123456"));
60/// test(("123", "456", -4), ("0", "0123456"));
61/// test(("123", "456", -3), ("0", "123456"));
62/// test(("123", "456", -2), ("1", "23456"));
63/// test(("123", "456", -1), ("12", "3456"));
64/// test(("123", "456",  0), ("123", "456"));
65/// test(("123", "456",  1), ("1234", "56"));
66/// test(("123", "456",  2), ("12345", "6"));
67/// test(("123", "456",  3), ("123456", "0"));
68/// test(("123", "456",  4), ("1234560", "0"));
69/// test(("123", "456",  5), ("12345600", "0"));
70///
71/// test(("100", "",  -1), ("10", "0"));
72/// test(("100", "",  -2), ("1", "0"));
73/// test(("100", "",  -3), ("0", "1"));
74/// test(("100", "",  -4), ("0", "01"));
75///
76/// test(("", "001",  1), ("0", "01"));
77/// test(("", "001",  2), ("0", "1"));
78/// test(("", "001",  3), ("1", "0"));
79/// test(("", "001",  4), ("10", "0"));
80/// ```
81pub fn shift_decimal(
82    int_part: &str,
83    frac_part: &str,
84    exp: i128,
85) -> (String, String) {
86    let mut int_part = int_part.to_string();
87    let mut frac_part = frac_part.to_string();
88
89    #[expect(clippy::comparison_chain)]
90    if exp > 0 {
91        let exp = exp as usize;
92        let move_count = exp.min(frac_part.len());
93        int_part.push_str(&frac_part[..move_count]);
94        frac_part = frac_part[move_count..].to_string();
95        if exp > move_count {
96            int_part.push_str(&"0".repeat(exp - move_count));
97        }
98    } else if exp < 0 {
99        let exp = (-exp) as usize;
100        let move_count = exp.min(int_part.len());
101        let moved = &int_part[int_part.len() - move_count..];
102        frac_part = format!("{moved}{frac_part}");
103        int_part.truncate(int_part.len() - move_count);
104        if exp > move_count {
105            frac_part = format!("{}{frac_part}", "0".repeat(exp - move_count));
106        }
107    }
108
109    let mut int_part = int_part.trim_start_matches('0').to_string();
110    let mut frac_part = frac_part.trim_end_matches('0').to_string();
111
112    if int_part.is_empty() {
113        int_part = "0".to_string();
114    }
115    if frac_part.is_empty() {
116        frac_part = "0".to_string();
117    }
118
119    (int_part, frac_part)
120}
121
122pub fn parse_dec19x19_internal(s: &str) -> Result<i128, ParseDec19x19Error> {
123    // let debug_pfx = "debug";
124    // let (s, debug) = if s.starts_with(debug_pfx) {
125    //     (&s[debug_pfx.len()..], true)
126    // } else {
127    //     (s, false)
128    // };
129    let clean = s.replace(['_', ' '], "");
130    let trimmed = clean.trim();
131    let is_negative = trimmed.starts_with('-');
132    let e_parts: Vec<&str> = trimmed.split('e').collect();
133    if e_parts.len() > 2 {
134        let pos = e_parts[0].len() + e_parts[1].len() + 1;
135        return Err(ParseDec19x19Error::InvalidChar { char: 'e', pos })
136    }
137    let exp: i128 = e_parts.get(1).map_or(Ok(0), |t| t.parse())?;
138    let parts: Vec<&str> = e_parts[0].split('.').collect();
139    let parts_count = parts.len();
140    if parts_count > 2 {
141        let pos = parts[0].len() + parts[1].len() + 1;
142        return Err(ParseDec19x19Error::InvalidChar { char: '.', pos })
143    }
144    let int_part_str = parts[0].to_string();
145    let frac_part_str = parts.get(1).map(|t| t.to_string()).unwrap_or_default();
146    let (int_part_str2, frac_part_str2) = shift_decimal(&int_part_str, &frac_part_str, exp);
147    let int_part: i128 = int_part_str2.parse()?;
148    let frac_part: i128 = {
149        if frac_part_str2.len() > FRAC_PLACES as usize {
150            return Err(ParseDec19x19Error::TooPrecise);
151        }
152        let mut buffer = [b'0'; FRAC_PLACES as usize];
153        let frac_bytes = frac_part_str2.as_bytes();
154        buffer[..frac_bytes.len()].copy_from_slice(frac_bytes);
155        #[allow(clippy::unwrap_used)]
156        let padded = std::str::from_utf8(&buffer).unwrap();
157        padded.parse()?
158    };
159    let scaled = int_part.checked_mul(FRAC_SCALE_I128).ok_or(ParseDec19x19Error::OutOfBounds)?;
160    let repr = if is_negative {
161        scaled.checked_sub(frac_part)
162    } else {
163        scaled.checked_add(frac_part)
164    }.ok_or(ParseDec19x19Error::OutOfBounds)?;
165    Ok(repr)
166}
167
168// ====================
169// === FmtSeparated ===
170// ====================
171
172#[derive(Debug, Clone, Copy)]
173pub struct Formatter {
174    pub separator: Option<char>,
175    pub precision: Option<usize>,
176    pub width: Option<usize>,
177    pub align: Option<std::fmt::Alignment>,
178    pub fill: char,
179    pub sign_plus: bool
180}
181
182pub trait Format {
183    fn format(&self, f: &mut Formatter) -> String;
184}
185
186// ============
187// === Rand ===
188// ============
189
190pub trait Rand {
191    fn rand(seed: u64, int: impl IntoRandRange, frac: impl IntoRandRange) -> Self;
192}
193
194pub type RandRange = std::ops::RangeInclusive<u32>;
195
196pub trait IntoRandRange {
197    fn into_rand_range(self) -> RandRange;
198}
199
200impl IntoRandRange for RandRange {
201    fn into_rand_range(self) -> RandRange {
202        self
203    }
204}
205
206impl IntoRandRange for u32 {
207    fn into_rand_range(self) -> RandRange {
208        self ..= self
209    }
210}