pub const SCALE_FACTOR: i64 = 10_000;
pub type Price = u32;
pub type Qty = i64;
#[inline]
pub fn parse_price_str(s: &str) -> Option<Price> {
parse_scaled_unsigned(s, SCALE_FACTOR).and_then(|v| u32::try_from(v).ok())
}
#[inline]
pub fn parse_qty_str(s: &str) -> Option<Qty> {
parse_scaled_signed(s, SCALE_FACTOR)
}
fn parse_scaled_unsigned(s: &str, scale: i64) -> Option<u64> {
let b = s.as_bytes();
if b.is_empty() {
return None;
}
let (neg, digits) = match b[0] {
b'-' => (true, &b[1..]),
b'+' => (false, &b[1..]),
_ => (false, b),
};
if neg || digits.is_empty() {
return None;
}
let signed = parse_scaled_signed_bytes(digits, scale)?;
if signed < 0 {
return None;
}
Some(signed as u64)
}
fn parse_scaled_signed(s: &str, scale: i64) -> Option<i64> {
let b = s.as_bytes();
if b.is_empty() {
return None;
}
let (neg, digits) = match b[0] {
b'-' => (true, &b[1..]),
b'+' => (false, &b[1..]),
_ => (false, b),
};
let magnitude = parse_scaled_signed_bytes(digits, scale)?;
Some(if neg { -magnitude } else { magnitude })
}
fn parse_scaled_signed_bytes(b: &[u8], scale: i64) -> Option<i64> {
if b.is_empty() {
return None;
}
let mut dot = None;
for (i, c) in b.iter().enumerate() {
match c {
b'0'..=b'9' => {}
b'.' if dot.is_none() => dot = Some(i),
_ => return None,
}
}
let (int_part, frac_part) = match dot {
Some(i) => (&b[..i], &b[i + 1..]),
None => (b, &b[..0]),
};
if int_part.is_empty() && frac_part.is_empty() {
return None;
}
let mut acc: i64 = 0;
for &c in int_part {
acc = acc.checked_mul(10)?.checked_add((c - b'0') as i64)?;
}
acc = acc.checked_mul(scale)?;
let mut frac_scale = scale / 10;
for &c in frac_part {
if frac_scale == 0 {
break;
}
acc = acc.checked_add((c - b'0') as i64 * frac_scale)?;
frac_scale /= 10;
}
Some(acc)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_fractional() {
assert_eq!(parse_price_str("0.5432"), Some(5432));
assert_eq!(parse_price_str("0.5"), Some(5000));
assert_eq!(parse_price_str("0.0001"), Some(1));
assert_eq!(parse_price_str("1.0"), Some(10000));
assert_eq!(parse_price_str("0"), Some(0));
}
#[test]
fn parse_integer() {
assert_eq!(parse_price_str("50"), Some(500_000));
assert_eq!(parse_price_str("99"), Some(990_000));
}
#[test]
fn rejects_malformed() {
assert_eq!(parse_price_str(""), None);
assert_eq!(parse_price_str("abc"), None);
assert_eq!(parse_price_str("1.2.3"), None);
assert_eq!(parse_price_str("-1"), None);
}
#[test]
fn qty_handles_signed() {
assert_eq!(parse_qty_str("100.0"), Some(1_000_000));
assert_eq!(parse_qty_str("-5.25"), Some(-52_500));
}
#[test]
fn excess_fraction_truncates_to_tick() {
assert_eq!(parse_price_str("0.54329"), Some(5432));
}
#[test]
fn upper_bound() {
assert_eq!(parse_price_str("1.0"), Some(10000));
assert!(parse_price_str("99999999999999999999").is_none());
}
}