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 pids: Vec<i64> = args[opts.optind..]
.iter()
.map(|arg| {
fish_wcstoi(arg.chars()).unwrap_or_else(|_| {
.map(|&arg| {
fish_wcstoi(arg).unwrap_or_else(|_| {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a valid job specifier\n",
cmd,
*arg
arg
));
retval = STATUS_INVALID_ARGS;
0

View file

@ -73,7 +73,7 @@ pub fn random(
cmd: &wstr,
num: &wstr,
) -> 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() {
streams
.err

View file

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

View file

@ -196,7 +196,7 @@ pub fn wait(
for i in w.woptind..argc {
if iswnumeric(argv[i]) {
// 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 {
streams.err.append(wgettext_fmt!(
"%ls: '%ls' is not a valid process id\n",

View file

@ -112,7 +112,7 @@ impl RedirectionSpec {
/// Attempt to parse target as an fd.
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> {
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.
fn iter_prefixes_iter<Prefix, Contents>(prefix: Prefix, mut contents: Contents) -> bool
where

View file

@ -1,22 +1,39 @@
pub use super::errors::Error;
use crate::wchar::IntoCharIter;
use num_traits::{NumCast, PrimInt};
use std::iter::Peekable;
use std::iter::{Fuse, Peekable};
use std::result::Result;
struct ParseResult {
result: u64,
negative: bool,
consumed_all: bool,
consumed: usize,
}
/// Helper to get the current char, or \0.
fn current<Chars>(chars: &mut Peekable<Chars>) -> char
where
Chars: Iterator<Item = char>,
{
match chars.peek() {
Some(c) => *c,
None => '\0',
struct CharsIterator<Iter: Iterator<Item = char>> {
chars: Peekable<Fuse<Iter>>,
consumed: usize,
}
impl<Iter: Iterator<Item = char>> CharsIterator<Iter> {
/// Get the current char, or \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.
/// - Otherwise 10.
/// 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>
where
Chars: Iterator<Item = char>,
{
fn fish_parse_radix<Iter: Iterator<Item = char>>(
iter: Iter,
mradix: Option<u32>,
) -> Result<ParseResult, Error> {
if let Some(r) = mradix {
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.
while current(chars).is_whitespace() {
while chars.current().is_whitespace() {
chars.next();
}
@ -46,9 +68,9 @@ where
// Consume leading +/-.
let mut negative;
match current(chars) {
match chars.current() {
'-' | '+' => {
negative = current(chars) == '-';
negative = chars.current() == '-';
chars.next();
}
_ => negative = false,
@ -57,9 +79,9 @@ where
// Determine the radix.
let radix = if let Some(radix) = mradix {
radix
} else if current(chars) == '0' {
} else if chars.current() == '0' {
chars.next();
match current(chars) {
match chars.current() {
'x' | 'X' => {
chars.next();
16
@ -71,6 +93,7 @@ where
result: 0,
negative: false,
consumed_all: chars.peek().is_none(),
consumed: chars.consumed,
});
}
}
@ -79,19 +102,19 @@ where
};
// Compute as u64.
let mut consumed1 = false;
let start_consumed = chars.consumed;
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
.checked_mul(radix as u64)
.and_then(|r| r.checked_add(digit as u64))
.ok_or(Error::Overflow)?;
chars.next();
consumed1 = true;
}
// Did we consume at least one char?
if !consumed1 {
// Did we consume at least one char after the prefix?
let consumed = chars.consumed;
if consumed == start_consumed {
return Err(Error::InvalidChar);
}
@ -104,6 +127,7 @@ where
result,
negative,
consumed_all,
consumed,
})
}
@ -112,6 +136,7 @@ fn fish_wcstoi_impl<Int, Chars>(
src: Chars,
mradix: Option<u32>,
consume_all: bool,
out_consumed: &mut usize,
) -> Result<Int, Error>
where
Chars: Iterator<Item = char>,
@ -125,8 +150,9 @@ where
result,
negative,
consumed_all,
..
consumed,
} = fish_parse_radix(src, mradix)?;
*out_consumed = consumed;
if !signed && negative {
Err(Error::InvalidChar)
@ -158,20 +184,20 @@ where
/// - Leading + is supported.
pub fn fish_wcstoi<Int, Chars>(src: Chars) -> Result<Int, Error>
where
Chars: Iterator<Item = char>,
Chars: IntoCharIter,
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.
/// Leading whitespace is skipped.
pub fn fish_wcstoi_radix<Int, Chars>(src: Chars, radix: u32) -> Result<Int, Error>
where
Chars: Iterator<Item = char>,
Chars: IntoCharIter,
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>(
@ -180,10 +206,24 @@ pub fn fish_wcstoi_radix_all<Int, Chars>(
consume_all: bool,
) -> Result<Int, Error>
where
Chars: Iterator<Item = char>,
Chars: IntoCharIter,
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)]
@ -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("+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("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));
@ -236,4 +282,26 @@ mod tests {
test_min_max(std::u32::MIN, std::u32::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));
}
}