use std::num::ParseFloatError;
use std::num::ParseIntError;
pub fn parse_float(s: &str) -> Result<f64, ParseFloatError> {
s.trim().parse()
}
pub fn parse_int<T: std::str::FromStr<Err = ParseIntError>>(s: &str) -> Result<T, ParseIntError> {
s.trim().parse()
}
pub(crate) fn decode_hybrid36(field: &str, width: u32) -> Result<i32, crate::PdbError> {
let invalid = || crate::PdbError::ParseError(format!("Invalid hybrid-36 number: {field:?}"));
let first = field.chars().next().ok_or_else(invalid)?;
if first == ' ' || first == '-' || first == '+' || first.is_ascii_digit() {
return field.trim().parse::<i32>().map_err(|_| invalid());
}
if field.len() != width as usize {
return Err(invalid());
}
let uppercase = first.is_ascii_uppercase();
let mut value: i64 = 0;
for c in field.chars() {
let digit = match c {
'0'..='9' => c as i64 - '0' as i64,
'A'..='Z' if uppercase => c as i64 - 'A' as i64 + 10,
'a'..='z' if !uppercase => c as i64 - 'a' as i64 + 10,
_ => return Err(invalid()),
};
value = value * 36 + digit;
}
let power36 = 36_i64.pow(width - 1);
let decimal_limit = 10_i64.pow(width);
let decoded = if uppercase {
value - 10 * power36 + decimal_limit
} else {
value + 16 * power36 + decimal_limit
};
i32::try_from(decoded).map_err(|_| invalid())
}
pub(crate) fn encode_hybrid36(value: i32, width: u32) -> Option<String> {
let field_width = width as usize;
let value = i64::from(value);
let decimal_limit = 10_i64.pow(width);
if value > -10_i64.pow(width - 1) && value < decimal_limit {
return Some(format!("{value:>field_width$}"));
}
if value < 0 {
return None;
}
let block = 26 * 36_i64.pow(width - 1);
let mut offset = value - decimal_limit;
let digits: &[u8] = if offset < block {
b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
} else {
offset -= block;
if offset >= block {
return None;
}
b"0123456789abcdefghijklmnopqrstuvwxyz"
};
let mut n = offset + 10 * 36_i64.pow(width - 1);
let mut encoded = vec![b'0'; field_width];
for slot in encoded.iter_mut().rev() {
*slot = digits[(n % 36) as usize];
n /= 36;
}
Some(encoded.iter().map(|&b| b as char).collect())
}
#[cfg(test)]
mod tests {
use super::{decode_hybrid36, encode_hybrid36};
use proptest::prelude::*;
#[test]
fn numbers_in_the_decimal_range_are_encoded_in_decimal() {
for (value, width, expected) in [
(1, 5, " 1"),
(99_999, 5, "99999"),
(-9_999, 5, "-9999"),
(1, 4, " 1"),
(9_999, 4, "9999"),
(-999, 4, "-999"),
] {
assert_eq!(encode_hybrid36(value, width).as_deref(), Some(expected));
}
}
#[test]
fn larger_numbers_are_encoded_in_hybrid36() {
for (value, width, expected) in [
(100_000, 5, "A0000"),
(100_035, 5, "A000Z"),
(100_036, 5, "A0010"),
(43_770_015, 5, "ZZZZZ"),
(43_770_016, 5, "a0000"),
(87_440_031, 5, "zzzzz"),
(10_000, 4, "A000"),
(1_223_055, 4, "ZZZZ"),
(1_223_056, 4, "a000"),
(2_436_111, 4, "zzzz"),
] {
assert_eq!(encode_hybrid36(value, width).as_deref(), Some(expected));
}
}
#[test]
fn numbers_outside_the_hybrid36_range_are_not_encoded() {
for (value, width) in [(87_440_032, 5), (-10_000, 5), (2_436_112, 4), (-1_000, 4)] {
assert_eq!(encode_hybrid36(value, width), None, "{value}");
}
}
proptest! {
#[test]
fn encoded_numbers_decode_to_the_original(value in -999..2_436_112i32, wide in any::<bool>()) {
let width = if wide { 5 } else { 4 };
let field = encode_hybrid36(value, width).unwrap();
prop_assert_eq!(field.len(), width as usize);
prop_assert_eq!(decode_hybrid36(&field, width).unwrap(), value);
}
}
#[test]
fn decimal_numbers_decode_unchanged() {
for (field, width, expected) in [
(" 1", 5, 1),
("99999", 5, 99_999),
("-9999", 5, -9_999),
("+1234", 5, 1_234),
(" 1", 4, 1),
("9999", 4, 9_999),
("-999", 4, -999),
] {
assert_eq!(
decode_hybrid36(field, width).unwrap(),
expected,
"{field:?}"
);
}
}
#[test]
fn hybrid36_numbers_continue_after_the_decimal_range() {
for (field, width, expected) in [
("A0000", 5, 100_000),
("A0001", 5, 100_001),
("A000Z", 5, 100_035),
("A0010", 5, 100_036),
("ZZZZZ", 5, 43_770_015),
("a0000", 5, 43_770_016),
("A000", 4, 10_000),
("ZZZZ", 4, 1_223_055),
("a000", 4, 1_223_056),
] {
assert_eq!(
decode_hybrid36(field, width).unwrap(),
expected,
"{field:?}"
);
}
}
#[test]
fn invalid_fields_are_rejected() {
for (field, width) in [("A00*", 4), ("Aa000", 5), ("", 5), ("A000", 5), ("**", 4)] {
assert!(decode_hybrid36(field, width).is_err(), "{field:?}");
}
}
}