Introduce fish_wcstoi_partial

fish_wcstoi_partial is like fish_wcstoi: it converts from a string to an
int optionally inferring the radix. fish_wcstoi_partial also returns the
number of characters consumed.
This commit is contained in:
ridiculousfish 2023-03-05 19:52:12 -08:00
parent 7729d3206a
commit dc8aab3f52
7 changed files with 122 additions and 38 deletions

View file

@ -99,12 +99,12 @@ pub fn bg(parser: &mut parser_t, streams: &mut io_streams_t, args: &mut [&wstr])
let mut retval = STATUS_CMD_OK; let mut retval = STATUS_CMD_OK;
let pids: Vec<i64> = args[opts.optind..] let pids: Vec<i64> = args[opts.optind..]
.iter() .iter()
.map(|arg| { .map(|&arg| {
fish_wcstoi(arg.chars()).unwrap_or_else(|_| { fish_wcstoi(arg).unwrap_or_else(|_| {
streams.err.append(wgettext_fmt!( streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a valid job specifier\n", "%ls: '%ls' is not a valid job specifier\n",
cmd, cmd,
*arg arg
)); ));
retval = STATUS_INVALID_ARGS; retval = STATUS_INVALID_ARGS;
0 0

View file

@ -73,7 +73,7 @@ pub fn random(
cmd: &wstr, cmd: &wstr,
num: &wstr, num: &wstr,
) -> Result<T, wutil::Error> { ) -> Result<T, wutil::Error> {
let res = fish_wcstoi_radix_all(num.chars(), None, true); let res = fish_wcstoi_radix_all(num, None, true);
if res.is_err() { if res.is_err() {
streams streams
.err .err

View file

@ -117,7 +117,7 @@ pub fn parse_return_value(
if optind == args.len() { if optind == args.len() {
Ok(parser.get_last_status().into()) Ok(parser.get_last_status().into())
} else { } else {
match fish_wcstoi(args[optind].chars()) { match fish_wcstoi(args[optind]) {
Ok(i) => Ok(i), Ok(i) => Ok(i),
Err(_e) => { Err(_e) => {
streams streams

View file

@ -196,7 +196,7 @@ pub fn wait(
for i in w.woptind..argc { for i in w.woptind..argc {
if iswnumeric(argv[i]) { if iswnumeric(argv[i]) {
// argument is pid // argument is pid
let mpid: Result<pid_t, wutil::Error> = fish_wcstoi(argv[i].chars()); let mpid: Result<pid_t, wutil::Error> = fish_wcstoi(argv[i]);
if mpid.is_err() || mpid.unwrap() <= 0 { if mpid.is_err() || mpid.unwrap() <= 0 {
streams.err.append(wgettext_fmt!( streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a valid process id\n", "%ls: '%ls' is not a valid process id\n",

View file

@ -112,7 +112,7 @@ impl RedirectionSpec {
/// Attempt to parse target as an fd. /// Attempt to parse target as an fd.
pub fn get_target_as_fd(&self) -> Option<RawFd> { pub fn get_target_as_fd(&self) -> Option<RawFd> {
fish_wcstoi(self.target.as_char_slice().iter().copied()).ok() fish_wcstoi(&self.target).ok()
} }
fn get_target_as_fd_ffi(&self) -> SharedPtr<i32> { fn get_target_as_fd_ffi(&self) -> SharedPtr<i32> {
match self.get_target_as_fd() { match self.get_target_as_fd() {

View file

@ -116,6 +116,22 @@ impl<'a> IntoCharIter for &'a WString {
} }
} }
// Also support `str.chars()` itself.
impl<'a> IntoCharIter for std::str::Chars<'a> {
type Iter = Self;
fn chars(self) -> Self::Iter {
self
}
}
// Also support `wstr.chars()` itself.
impl<'a> IntoCharIter for CharsUtf32<'a> {
type Iter = Self;
fn chars(self) -> Self::Iter {
self
}
}
/// \return true if \p prefix is a prefix of \p contents. /// \return true if \p prefix is a prefix of \p contents.
fn iter_prefixes_iter<Prefix, Contents>(prefix: Prefix, mut contents: Contents) -> bool fn iter_prefixes_iter<Prefix, Contents>(prefix: Prefix, mut contents: Contents) -> bool
where where

View file

@ -1,22 +1,39 @@
pub use super::errors::Error; pub use super::errors::Error;
use crate::wchar::IntoCharIter;
use num_traits::{NumCast, PrimInt}; use num_traits::{NumCast, PrimInt};
use std::iter::Peekable; use std::iter::{Fuse, Peekable};
use std::result::Result; use std::result::Result;
struct ParseResult { struct ParseResult {
result: u64, result: u64,
negative: bool, negative: bool,
consumed_all: bool, consumed_all: bool,
consumed: usize,
} }
/// Helper to get the current char, or \0. struct CharsIterator<Iter: Iterator<Item = char>> {
fn current<Chars>(chars: &mut Peekable<Chars>) -> char chars: Peekable<Fuse<Iter>>,
where consumed: usize,
Chars: Iterator<Item = char>, }
{
match chars.peek() { impl<Iter: Iterator<Item = char>> CharsIterator<Iter> {
Some(c) => *c, /// Get the current char, or \0.
None => '\0', fn current(&mut self) -> char {
self.peek().unwrap_or('\0')
}
/// Get the current char, or None.
fn peek(&mut self) -> Option<char> {
self.chars.peek().copied()
}
/// Get the next char, incrementing self.consumed.
fn next(&mut self) -> Option<char> {
let res = self.chars.next();
if res.is_some() {
self.consumed += 1;
}
res
} }
} }
@ -26,17 +43,22 @@ where
/// - Leading 0 means 8. /// - Leading 0 means 8.
/// - Otherwise 10. /// - Otherwise 10.
/// The parse result contains the number as a u64, and whether it was negative. /// The parse result contains the number as a u64, and whether it was negative.
fn fish_parse_radix<Chars>(ichars: Chars, mradix: Option<u32>) -> Result<ParseResult, Error> fn fish_parse_radix<Iter: Iterator<Item = char>>(
where iter: Iter,
Chars: Iterator<Item = char>, mradix: Option<u32>,
{ ) -> Result<ParseResult, Error> {
if let Some(r) = mradix { if let Some(r) = mradix {
assert!((2..=36).contains(&r), "fish_parse_radix: invalid radix {r}"); assert!((2..=36).contains(&r), "fish_parse_radix: invalid radix {r}");
} }
let chars = &mut ichars.peekable();
// Construct a CharsIterator to keep track of how many we consume.
let mut chars = CharsIterator {
chars: iter.fuse().peekable(),
consumed: 0,
};
// Skip leading whitespace. // Skip leading whitespace.
while current(chars).is_whitespace() { while chars.current().is_whitespace() {
chars.next(); chars.next();
} }
@ -46,9 +68,9 @@ where
// Consume leading +/-. // Consume leading +/-.
let mut negative; let mut negative;
match current(chars) { match chars.current() {
'-' | '+' => { '-' | '+' => {
negative = current(chars) == '-'; negative = chars.current() == '-';
chars.next(); chars.next();
} }
_ => negative = false, _ => negative = false,
@ -57,9 +79,9 @@ where
// Determine the radix. // Determine the radix.
let radix = if let Some(radix) = mradix { let radix = if let Some(radix) = mradix {
radix radix
} else if current(chars) == '0' { } else if chars.current() == '0' {
chars.next(); chars.next();
match current(chars) { match chars.current() {
'x' | 'X' => { 'x' | 'X' => {
chars.next(); chars.next();
16 16
@ -71,6 +93,7 @@ where
result: 0, result: 0,
negative: false, negative: false,
consumed_all: chars.peek().is_none(), consumed_all: chars.peek().is_none(),
consumed: chars.consumed,
}); });
} }
} }
@ -79,19 +102,19 @@ where
}; };
// Compute as u64. // Compute as u64.
let mut consumed1 = false; let start_consumed = chars.consumed;
let mut result: u64 = 0; let mut result: u64 = 0;
while let Some(digit) = current(chars).to_digit(radix) { while let Some(digit) = chars.current().to_digit(radix) {
result = result result = result
.checked_mul(radix as u64) .checked_mul(radix as u64)
.and_then(|r| r.checked_add(digit as u64)) .and_then(|r| r.checked_add(digit as u64))
.ok_or(Error::Overflow)?; .ok_or(Error::Overflow)?;
chars.next(); chars.next();
consumed1 = true;
} }
// Did we consume at least one char? // Did we consume at least one char after the prefix?
if !consumed1 { let consumed = chars.consumed;
if consumed == start_consumed {
return Err(Error::InvalidChar); return Err(Error::InvalidChar);
} }
@ -104,6 +127,7 @@ where
result, result,
negative, negative,
consumed_all, consumed_all,
consumed,
}) })
} }
@ -112,6 +136,7 @@ fn fish_wcstoi_impl<Int, Chars>(
src: Chars, src: Chars,
mradix: Option<u32>, mradix: Option<u32>,
consume_all: bool, consume_all: bool,
out_consumed: &mut usize,
) -> Result<Int, Error> ) -> Result<Int, Error>
where where
Chars: Iterator<Item = char>, Chars: Iterator<Item = char>,
@ -125,8 +150,9 @@ where
result, result,
negative, negative,
consumed_all, consumed_all,
.. consumed,
} = fish_parse_radix(src, mradix)?; } = fish_parse_radix(src, mradix)?;
*out_consumed = consumed;
if !signed && negative { if !signed && negative {
Err(Error::InvalidChar) Err(Error::InvalidChar)
@ -158,20 +184,20 @@ where
/// - Leading + is supported. /// - Leading + is supported.
pub fn fish_wcstoi<Int, Chars>(src: Chars) -> Result<Int, Error> pub fn fish_wcstoi<Int, Chars>(src: Chars) -> Result<Int, Error>
where where
Chars: Iterator<Item = char>, Chars: IntoCharIter,
Int: PrimInt, Int: PrimInt,
{ {
fish_wcstoi_impl(src, None, false) fish_wcstoi_impl(src.chars(), None, false, &mut 0)
} }
/// Convert the given wide string to an integer using the given radix. /// Convert the given wide string to an integer using the given radix.
/// Leading whitespace is skipped. /// Leading whitespace is skipped.
pub fn fish_wcstoi_radix<Int, Chars>(src: Chars, radix: u32) -> Result<Int, Error> pub fn fish_wcstoi_radix<Int, Chars>(src: Chars, radix: u32) -> Result<Int, Error>
where where
Chars: Iterator<Item = char>, Chars: IntoCharIter,
Int: PrimInt, Int: PrimInt,
{ {
fish_wcstoi_impl(src, Some(radix), false) fish_wcstoi_impl(src.chars(), Some(radix), false, &mut 0)
} }
pub fn fish_wcstoi_radix_all<Int, Chars>( pub fn fish_wcstoi_radix_all<Int, Chars>(
@ -180,10 +206,24 @@ pub fn fish_wcstoi_radix_all<Int, Chars>(
consume_all: bool, consume_all: bool,
) -> Result<Int, Error> ) -> Result<Int, Error>
where where
Chars: Iterator<Item = char>, Chars: IntoCharIter,
Int: PrimInt, Int: PrimInt,
{ {
fish_wcstoi_impl(src, radix, consume_all) fish_wcstoi_impl(src.chars(), radix, consume_all, &mut 0)
}
/// Convert the given wide string to an integer.
/// The semantics here match wcstol():
/// - Leading whitespace is skipped.
/// - 0 means octal, 0x means hex
/// - Leading + is supported.
/// The number of consumed characters is returned in out_consumed.
pub fn fish_wcstoi_partial<Int, Chars>(src: Chars, out_consumed: &mut usize) -> Result<Int, Error>
where
Chars: IntoCharIter,
Int: PrimInt,
{
fish_wcstoi_impl(src.chars(), None, false, out_consumed)
} }
#[cfg(test)] #[cfg(test)]
@ -205,8 +245,14 @@ mod tests {
assert_eq!(run1("0"), Ok(0)); assert_eq!(run1("0"), Ok(0));
assert_eq!(run1("-0"), Ok(0)); assert_eq!(run1("-0"), Ok(0));
assert_eq!(run1("+0"), Ok(0)); assert_eq!(run1("+0"), Ok(0));
assert_eq!(run1("+00"), Ok(0));
assert_eq!(run1("-00"), Ok(0));
assert_eq!(run1("+0x00"), Ok(0));
assert_eq!(run1("-0x00"), Ok(0));
assert_eq!(run1("+-0"), Err(Error::InvalidChar)); assert_eq!(run1("+-0"), Err(Error::InvalidChar));
assert_eq!(run1("-+0"), Err(Error::InvalidChar)); assert_eq!(run1("-+0"), Err(Error::InvalidChar));
assert_eq!(run1("5"), Ok(5));
assert_eq!(run1("-5"), Ok(-5));
assert_eq!(run1("123"), Ok(123)); assert_eq!(run1("123"), Ok(123));
assert_eq!(run1("+123"), Ok(123)); assert_eq!(run1("+123"), Ok(123));
assert_eq!(run1("-123"), Ok(-123)); assert_eq!(run1("-123"), Ok(-123));
@ -236,4 +282,26 @@ mod tests {
test_min_max(std::u32::MIN, std::u32::MAX); test_min_max(std::u32::MIN, std::u32::MAX);
test_min_max(std::u64::MIN, std::u64::MAX); test_min_max(std::u64::MIN, std::u64::MAX);
} }
#[test]
fn test_partial() {
let run1 = |s: &str| -> (i32, usize) {
let mut consumed = 0;
let res =
fish_wcstoi_partial(s.chars(), &mut consumed).expect("Should have parsed an int");
(res, consumed)
};
assert_eq!(run1("0"), (0, 1));
assert_eq!(run1("-0"), (0, 2));
assert_eq!(run1(" -1 "), (-1, 3));
assert_eq!(run1(" +1 "), (1, 3));
assert_eq!(run1(" 345 "), (345, 5));
assert_eq!(run1(" -345 "), (-345, 5));
assert_eq!(run1(" 0345 "), (229, 6));
assert_eq!(run1(" +0345 "), (229, 7));
assert_eq!(run1(" -0345 "), (-229, 7));
assert_eq!(run1(" 0x345 "), (0x345, 6));
assert_eq!(run1(" -0x345 "), (-0x345, 7));
}
} }