fcla 0.1.0

Command line argument parsing
Documentation
use super::{Decimal, Digit, Digits, Sign, UnsignedDecimal};
use num::{BigInt, BigUint, Zero as _};

pub type Parent = String;

pub fn from_arg(mut arg: Parent) -> Option<Decimal> {
    use UnsignedDecimal::*;

    let mut exponent = take_exponent(&mut arg)?;
    let mut arg = arg.into_bytes();
    let sign = take_sign(&mut arg);
    let shift = take_radix(&mut arg);
    if arg.is_empty() {
        return None;
    }
    let trailing = remove_trailing_zeros(&mut arg);
    let magnitude = if arg.is_empty() {
        Zero
    } else {
        remove_leading_zeros(&mut arg);
        let adjust = trailing as isize - shift as isize;
        exponent += adjust;
        let digits = into_digits_inner(arg.into_boxed_slice())?;
        NonZero {
            base: Digits(digits),
            exponent,
        }
    };
    Some(Decimal { sign, magnitude })
}

fn take_exponent(arg: &mut String) -> Option<BigInt> {
    let (left, right) = match arg.split_once(['E', 'e']) {
        None => return Some(BigInt::zero()),
        Some(parts) => parts,
    };
    let exponent = parse_exponent(right)?;
    arg.truncate(left.len());
    Some(exponent)
}

fn parse_exponent(exponent: &str) -> Option<BigInt> {
    use num::bigint::Sign::*;

    let sign = if exponent.starts_with('-') {
        Minus
    } else {
        Plus
    };
    let magnitude = exponent.strip_prefix(['+', '-']).unwrap_or(exponent);
    if magnitude.is_empty() {
        return None;
    }
    let mut sum = BigUint::zero();
    for byte in magnitude.bytes() {
        let digit = match byte {
            b'0'..=b'9' => byte - b'0',
            _ => return None,
        };
        sum = 10u32 * sum + digit;
    }
    let magnitude = sum;
    let exponent = BigInt::from_biguint(sign, magnitude);
    Some(exponent)
}

fn take_sign(arg: &mut Vec<u8>) -> Sign {
    use Sign::*;

    let sign = match **arg {
        [b'+', ..] => Some(Positive),
        [b'-', ..] => Some(Negative),
        _ => None,
    };
    if sign.is_some() {
        arg.remove(0);
    }
    sign.unwrap_or(Positive)
}

fn take_radix(arg: &mut Vec<u8>) -> usize {
    let radix = match arg.iter().position(|byte| *byte == b'.') {
        None => return 0,
        Some(index) => index,
    };
    arg.remove(radix);
    arg.len() - radix
}

fn remove_trailing_zeros(arg: &mut Vec<u8>) -> usize {
    let length = arg.len();
    let last = arg
        .iter()
        .rposition(|byte| *byte != b'0')
        .map(|index| index + 1)
        .unwrap_or(0);
    arg.truncate(last);
    length - last
}

fn remove_leading_zeros(arg: &mut Vec<u8>) {
    let first = arg.iter().position(|byte| *byte != b'0').unwrap_or(0);
    let truncate = arg[first..].len();
    arg.copy_within(first.., 0);
    arg.truncate(truncate);
}

fn into_digits_inner(mut arg: Box<[u8]>) -> Option<Box<[Digit]>> {
    for byte in arg.iter_mut() {
        match byte {
            b'0'..=b'9' => *byte -= b'0',
            _ => return None,
        }
    }
    Some(unsafe { Box::from_raw(Box::into_raw(arg) as *mut [Digit]) })
}

#[cfg(test)]
mod tests {
    use super::*;
    use Digit::*;

    pub fn parse<A: ToString, const LEN: usize>(args: [A; LEN]) -> crate::ParseResult<Decimal> {
        crate::internal::parse(args)
    }

    #[test]
    fn empty() {
        parse([""]).unwrap_err();
    }

    #[test]
    fn plus() {
        parse(["+"]).unwrap_err();
    }

    #[test]
    fn minus() {
        parse(["-"]).unwrap_err();
    }

    #[test]
    fn period() {
        parse(["."]).unwrap_err();
    }

    #[test]
    fn plus_period() {
        parse(["+."]).unwrap_err();
    }

    #[test]
    fn minus_period() {
        parse(["-."]).unwrap_err();
    }

    #[test]
    fn e() {
        parse(["e"]).unwrap_err();
    }

    #[test]
    fn zero() {
        let Decimal { sign, magnitude } = parse(["0"]).unwrap();
        assert!(sign.is_positive());
        match magnitude {
            UnsignedDecimal::Zero => (),
            UnsignedDecimal::NonZero { .. } => panic!("`magnitude` should be zero"),
        }
    }

    #[test]
    fn plus_one() {
        let Decimal { sign, magnitude } = parse(["+1"]).unwrap();
        assert!(sign.is_positive());
        let (base, exponent) = match magnitude {
            UnsignedDecimal::Zero => panic!("`magnitude` should be non-zero"),
            UnsignedDecimal::NonZero { base, exponent } => (base, exponent),
        };
        assert_eq!(*base.into_inner(), [D1]);
        assert_eq!(exponent, BigInt::from(0));
    }

    #[test]
    fn minus_one() {
        let Decimal { sign, magnitude } = parse(["-1"]).unwrap();
        assert!(sign.is_negative());
        let (base, exponent) = match magnitude {
            UnsignedDecimal::Zero => panic!("`magnitude` should be non-zero"),
            UnsignedDecimal::NonZero { base, exponent } => (base, exponent),
        };
        assert_eq!(*base.into_inner(), [D1]);
        assert_eq!(exponent, BigInt::from(0));
    }

    #[test]
    fn radix_one() {
        let Decimal { sign, magnitude } = parse([".1"]).unwrap();
        assert!(sign.is_positive());
        let (base, exponent) = match magnitude {
            UnsignedDecimal::Zero => panic!("`magnitude` should be non-zero"),
            UnsignedDecimal::NonZero { base, exponent } => (base, exponent),
        };
        assert_eq!(*base.into_inner(), [D1]);
        assert_eq!(exponent, BigInt::from(-1));
    }

    #[test]
    fn radix_plus_one() {
        parse([".+1"]).unwrap_err();
    }

    #[test]
    fn radix_minus_one() {
        parse([".-1"]).unwrap_err();
    }

    #[test]
    fn one_e_one() {
        let Decimal { sign, magnitude } = parse(["1e1"]).unwrap();
        assert!(sign.is_positive());
        let (base, exponent) = match magnitude {
            UnsignedDecimal::Zero => panic!("`magnitude` should be non-zero"),
            UnsignedDecimal::NonZero { base, exponent } => (base, exponent),
        };
        assert_eq!(*base.into_inner(), [D1]);
        assert_eq!(exponent, BigInt::from(1));
    }

    #[test]
    fn one_e_plus_one() {
        let Decimal { sign, magnitude } = parse(["1e+1"]).unwrap();
        assert!(sign.is_positive());
        let (base, exponent) = match magnitude {
            UnsignedDecimal::Zero => panic!("`magnitude` should be non-zero"),
            UnsignedDecimal::NonZero { base, exponent } => (base, exponent),
        };
        assert_eq!(*base.into_inner(), [D1]);
        assert_eq!(exponent, BigInt::from(1));
    }

    #[test]
    fn one_e_minus_one() {
        let Decimal { sign, magnitude } = parse(["1e-1"]).unwrap();
        assert!(sign.is_positive());
        let (base, exponent) = match magnitude {
            UnsignedDecimal::Zero => panic!("`magnitude` should be non-zero"),
            UnsignedDecimal::NonZero { base, exponent } => (base, exponent),
        };
        assert_eq!(*base.into_inner(), [D1]);
        assert_eq!(exponent, BigInt::from(-1));
    }

    #[test]
    fn leading_zeros() {
        let Decimal { sign, magnitude } = parse(["+0001"]).unwrap();
        assert!(sign.is_positive());
        let (base, exponent) = match magnitude {
            UnsignedDecimal::Zero => panic!("`magnitude` should be non-zero"),
            UnsignedDecimal::NonZero { base, exponent } => (base, exponent),
        };
        assert_eq!(*base.into_inner(), [D1]);
        assert_eq!(exponent, BigInt::from(0));
    }

    #[test]
    fn trailing_zeros() {
        let Decimal { sign, magnitude } = parse(["1000.000e000"]).unwrap();
        assert!(sign.is_positive());
        let (base, exponent) = match magnitude {
            UnsignedDecimal::Zero => panic!("`magnitude` should be non-zero"),
            UnsignedDecimal::NonZero { base, exponent } => (base, exponent),
        };
        assert_eq!(*base.into_inner(), [D1]);
        assert_eq!(exponent, BigInt::from(3));
    }

    #[test]
    fn typical() {
        let Decimal { sign, magnitude } = parse(["123.456e789"]).unwrap();
        assert!(sign.is_positive());
        let (base, exponent) = match magnitude {
            UnsignedDecimal::Zero => panic!("`magnitude` should be non-zero"),
            UnsignedDecimal::NonZero { base, exponent } => (base, exponent),
        };
        assert_eq!(*base.into_inner(), [D1, D2, D3, D4, D5, D6]);
        assert_eq!(exponent, BigInt::from(786));
    }
}