pub fn str_is_numeric_bytes(input: &[u8]) -> bool {
let Ok(s) = std::str::from_utf8(input) else { return false };
str_is_numeric(s)
}
pub fn str_is_numeric(input: &str) -> bool {
let mut maybe_numeric = input.trim();
if maybe_numeric.is_empty() {
return false;
}
if maybe_numeric.starts_with('+') || maybe_numeric.starts_with('-') {
maybe_numeric = &maybe_numeric[1..];
if maybe_numeric.is_empty() {
return false;
}
}
maybe_numeric = maybe_numeric.trim_start_matches('0');
if maybe_numeric.is_empty() {
return true;
}
maybe_numeric.parse::<f64>().is_ok()
}
pub fn string_to_int(input: &[u8]) -> i64 {
let input = input.trim_ascii_start();
let mut offset = 0;
if matches!(input.first(), Some(b'+' | b'-')) {
offset += 1;
}
let mut has_digits = false;
while input.get(offset).is_some_and(u8::is_ascii_digit) {
has_digits = true;
offset += 1;
}
if input.get(offset) == Some(&b'.') {
offset += 1;
while input.get(offset).is_some_and(u8::is_ascii_digit) {
has_digits = true;
offset += 1;
}
}
if !has_digits {
return 0;
}
if matches!(input.get(offset), Some(b'e' | b'E')) {
let exponent_start = offset;
offset += 1;
if matches!(input.get(offset), Some(b'+' | b'-')) {
offset += 1;
}
let digits_start = offset;
while input.get(offset).is_some_and(u8::is_ascii_digit) {
offset += 1;
}
if offset == digits_start {
offset = exponent_start;
}
}
let Ok(number) = std::str::from_utf8(&input[..offset]) else {
return 0;
};
number.parse::<i64>().unwrap_or_else(|_| number.parse::<f64>().map_or(0, |number| number as i64))
}
pub fn numeric_string_equals_int(input: &[u8], expected: i64) -> bool {
let Ok(input) = std::str::from_utf8(input.trim_ascii()) else {
return false;
};
input.parse::<i64>().map_or_else(
|_| input.parse::<f64>().is_ok_and(|number| number == expected as f64),
|number| number == expected,
)
}
pub fn str_increment_bytes(input: &[u8]) -> Option<String> {
str_increment(std::str::from_utf8(input).ok()?)
}
pub fn str_increment(input: &str) -> Option<String> {
if input.is_empty() {
return None;
}
let input_bytes = input.as_bytes();
let is_alnum = input_bytes.iter().all(|&b| b.is_ascii_alphanumeric());
if !is_alnum {
return None;
}
let mut bytes = input_bytes.to_vec();
let len = bytes.len();
let mut current_idx = len;
loop {
if current_idx == 0 {
let first_char_of_original_input = input_bytes[0];
let char_to_prepend = match first_char_of_original_input {
b'z' => b'a',
b'Z' => b'A',
b'9' => b'1',
_ => {
#[allow(clippy::unreachable)]
{
unreachable!("unexpected character for carry-over: {first_char_of_original_input}");
}
}
};
let mut new_bytes = Vec::with_capacity(len + 1);
new_bytes.push(char_to_prepend);
new_bytes.extend_from_slice(&bytes);
return Some(unsafe { String::from_utf8_unchecked(new_bytes) });
}
current_idx -= 1;
let current_byte = bytes[current_idx];
match current_byte {
b'a'..=b'y' | b'A'..=b'Y' | b'0'..=b'8' => {
bytes[current_idx] = current_byte + 1;
return Some(unsafe { String::from_utf8_unchecked(bytes) });
}
b'z' => {
bytes[current_idx] = b'a';
}
b'Z' => {
bytes[current_idx] = b'A';
}
b'9' => {
bytes[current_idx] = b'0';
}
_ => {
#[allow(clippy::unreachable)]
{
unreachable!("non-alphanumeric character found post-validation");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_numeric() {
assert!(str_is_numeric("123"));
assert!(str_is_numeric("0"));
assert!(str_is_numeric("-123"));
assert!(str_is_numeric("+123"));
assert!(!str_is_numeric("abc"));
assert!(str_is_numeric("12.34"));
assert!(str_is_numeric("12e3"));
assert!(!str_is_numeric(""));
assert!(!str_is_numeric(" "));
}
#[test]
fn test_string_to_int() {
assert_eq!(string_to_int(b"abc"), 0);
assert_eq!(string_to_int(b""), 0);
assert_eq!(string_to_int(b"42"), 42);
assert_eq!(string_to_int(b"042"), 42);
assert_eq!(string_to_int(b"42.9"), 42);
assert_eq!(string_to_int(b"4.2e1"), 42);
assert_eq!(string_to_int(b" +42.9foo"), 42);
assert_eq!(string_to_int(b"-.5x"), 0);
assert_eq!(string_to_int(b"1_000"), 1);
assert_eq!(string_to_int(b"9223372036854775808"), i64::MAX);
assert_eq!(string_to_int(b"-9223372036854775809"), i64::MIN);
}
#[test]
fn test_numeric_string_equals_int() {
assert!(numeric_string_equals_int(b"42", 42));
assert!(numeric_string_equals_int(b"042", 42));
assert!(numeric_string_equals_int(b"42.0", 42));
assert!(numeric_string_equals_int(b"4.2e1", 42));
assert!(numeric_string_equals_int(b" 42 ", 42));
assert!(!numeric_string_equals_int(b"42.5", 42));
assert!(!numeric_string_equals_int(b"abc", 0));
assert!(!numeric_string_equals_int("\u{a0}42".as_bytes(), 42));
}
#[test]
fn test_increment_basic() {
assert_eq!(str_increment("hello"), Some("hellp".to_string()));
assert_eq!(str_increment("PHP"), Some("PHQ".to_string()));
assert_eq!(str_increment("rust"), Some("rusu".to_string()));
assert_eq!(str_increment("abc123"), Some("abc124".to_string()));
}
#[test]
fn test_increment_with_carries() {
assert_eq!(str_increment("hellz"), Some("helma".to_string()));
assert_eq!(str_increment("TESTZ"), Some("TESUA".to_string()));
assert_eq!(str_increment("xyz"), Some("xza".to_string()));
assert_eq!(str_increment("zz"), Some("aaa".to_string()));
assert_eq!(str_increment("ZZ"), Some("AAA".to_string()));
}
#[test]
fn test_increment_with_numeric_carries() {
assert_eq!(str_increment("9"), Some("10".to_string()));
assert_eq!(str_increment("99"), Some("100".to_string()));
assert_eq!(str_increment("999"), Some("1000".to_string()));
assert_eq!(str_increment("abc9"), Some("abd0".to_string()));
assert_eq!(str_increment("abc99"), Some("abd00".to_string()));
}
#[test]
fn test_increment_mixed_alphanumeric() {
assert_eq!(str_increment("a9"), Some("b0".to_string()));
assert_eq!(str_increment("a99z"), Some("b00a".to_string()));
assert_eq!(str_increment("Z9"), Some("AA0".to_string()));
assert_eq!(str_increment("9z"), Some("10a".to_string()));
assert_eq!(str_increment("9Z"), Some("10A".to_string()));
}
#[test]
fn test_increment_at_boundaries() {
assert_eq!(str_increment("z"), Some("aa".to_string()));
assert_eq!(str_increment("Z"), Some("AA".to_string()));
}
#[test]
fn test_increment_failure_cases() {
assert_eq!(str_increment(""), None);
assert_eq!(str_increment("hello!"), None);
assert_eq!(str_increment("test-123"), None);
assert_eq!(str_increment("user@example.com"), None);
assert_eq!(str_increment("русский"), None);
}
}