ako 0.0.3

Ako is a Rust crate that offers a practical and human-friendly approach to creating, manipulating, formatting and converting dates, times and timestamps.
Documentation
//! Low-level read/write methods used throughout `fmt` to implement
//! the higher-level routines.

use alloc::str::from_utf8_unchecked;
use alloc::string::String;

use crate::error::ErrorKind;

#[derive(Default)]
pub struct Padding {
    pub byte: u8,
    pub count: usize,
}

impl From<usize> for Padding {
    #[inline]
    fn from(value: usize) -> Self {
        Self {
            byte: b'0',
            count: value,
        }
    }
}

impl From<Option<usize>> for Padding {
    #[inline]
    fn from(value: Option<usize>) -> Self {
        value.map_or_else(Self::default, Into::into)
    }
}

/// Fast-path optimization for writing a known 2-digit number.
/// With `0` padding if the number is smaller than 10.
pub fn write_two_digit_i32(out: &mut String, value: u8) {
    debug_assert!(value <= 99);

    out.push((b'0' + value / 10) as char);
    out.push((b'0' + value % 10) as char);
}

#[allow(unsafe_code)]
pub fn write_i32(out: &mut String, padding: impl Into<Padding>, mut value: i32) {
    let padding = padding.into();

    let mut buffer = [padding.byte; 10];
    let mut index = 9;
    let mut count = 0;

    while value != 0 {
        buffer[index] = b'0' + ((value % 10) as u8);
        value /= 10;
        index -= 1;
        count += 1;
    }

    let s = &buffer[10 - count.max(padding.count)..];

    // SAFETY: `buffer` is known to contain only ASCII values
    let s = unsafe { from_utf8_unchecked(s) };

    out.push_str(s);
}

/// Reads and discards a specific byte.
/// Ensures that the specific byte was actually read and errors if it wasn't.
pub fn read_ensure_u8(input: &mut &[u8], expected: u8) -> crate::Result<()> {
    read_ensure_one_of_u8(input, &[expected])
}

/// Reads and discards a specific byte.
/// Ensures that one of the specific bytes was actually read and errors if it wasn't.
pub fn read_ensure_one_of_u8(input: &mut &[u8], expected: &[u8]) -> crate::Result<()> {
    match input.first().copied() {
        Some(byte) if expected.contains(&byte) => {
            // we read the one byte, we saw we got it
            *input = &input[1..];
            Ok(())
        }

        _ => Err(ErrorKind::ParseInvalid.into()),
    }
}

#[derive(Debug, Copy, Clone)]
pub enum ReadDigits {
    Exactly(usize),
    AtLeast(usize),
}

impl From<usize> for ReadDigits {
    fn from(value: usize) -> Self {
        Self::Exactly(value)
    }
}

impl From<Option<Self>> for ReadDigits {
    fn from(value: Option<Self>) -> Self {
        value.unwrap_or(Self::AtLeast(1))
    }
}

/// Reads an `i32`, stopping at the end of `input` or when reaching a non-numeric character.
pub fn read_i32(
    input: &mut &[u8],
    digits: impl Into<ReadDigits>,
) -> crate::Result<Option<(i32, usize)>> {
    let digits = digits.into();

    let mut len = 0;
    let mut value: i32 = 0;

    while let Some(byte) = input.get(len) {
        if !byte.is_ascii_digit() {
            if len > 0 {
                break;
            }

            return Err(ErrorKind::ParseInvalid.into());
        }

        let digit = (byte - b'0') as i32;

        if len > 0 {
            value = if let Some(value) = value.checked_mul(10) {
                value
            } else {
                // overflow
                return Ok(None);
            };
        }

        value += digit;
        len += 1;
    }

    // handle validation of whether we read enough digits
    // depends on the value of the `digits` param
    match digits {
        ReadDigits::Exactly(digits) => {
            if len != digits {
                return Err(ErrorKind::ParseInvalid.into());
            }
        }

        ReadDigits::AtLeast(digits) => {
            if len < digits {
                if len >= input.len() {
                    // ran out of input, need more
                    return Err(ErrorKind::ParseNotEnough.into());
                }

                // did not run out of input, saw something that wasn't a digit
                // but need more digits
                return Err(ErrorKind::ParseInvalid.into());
            }
        }
    }

    // advance input buffer by consumed characters
    *input = &input[len..];

    Ok(Some((value, len)))
}

pub fn read_i32_in_range(
    input: &mut &[u8],
    digits: impl Into<ReadDigits>,
    min: i32,
    max: i32,
) -> crate::Result<(i32, usize)> {
    match read_i32(input, digits)? {
        Some((number, len)) if (min..=max).contains(&number) => Ok((number, len)),

        _ => Err(ErrorKind::OutOfRange.into()),
    }
}

pub fn read_sign_is_negative(input: &mut &[u8], required: bool) -> crate::Result<bool> {
    let negative = input.first().copied().and_then(|maybe_sign| {
        let sign = match maybe_sign {
            b'+' => false,
            b'-' => true,

            _ => {
                return None;
            }
        };

        *input = &input[1..];
        Some(sign)
    });

    match negative {
        Some(negative) => Ok(negative),

        None if required => Err((if input.is_empty() {
            ErrorKind::ParseNotEnough
        } else {
            ErrorKind::ParseInvalid
        })
        .into()),

        None => Ok(false),
    }
}

#[cfg(test)]
mod tests {
    use alloc::string::String;

    use test_case::test_case;

    use crate::fmt::rw::{read_i32, write_i32, write_two_digit_i32};

    #[test_case("00", 0)]
    #[test_case("05", 5)]
    #[test_case("10", 10)]
    #[test_case("49", 49)]
    fn expect_write_two_digit_i32(text: &str, value: i32) -> crate::Result<()> {
        let mut output = String::with_capacity(2);
        write_two_digit_i32(&mut output, value as u8);

        assert_eq!(output, text);

        Ok(())
    }

    #[test_case("5", 5)]
    #[test_case("10", 10)]
    #[test_case("3278", 3278)]
    fn expect_i32(text: &str, expected: i32) -> crate::Result<()> {
        let mut output = String::with_capacity(4);
        let mut bytes = text.as_bytes();
        let (value, _) = read_i32(&mut bytes, None)?.unwrap();
        write_i32(&mut output, None, value);

        assert_eq!(value, expected);
        assert_eq!(output, text);

        Ok(())
    }

    #[test_case("005", 3, 5)]
    #[test_case("010", 3, 10)]
    #[test_case("3278", 3, 3278)]
    fn expect_write_i32_padding(text: &str, padding: usize, expected: i32) -> crate::Result<()> {
        let mut output = String::with_capacity(4);
        let mut bytes = text.as_bytes();
        let (value, _) = read_i32(&mut bytes, None)?.unwrap();
        write_i32(&mut output, Some(padding), expected);

        assert_eq!(output, text);
        assert_eq!(value, expected);

        Ok(())
    }
}