Repository: rust-bakery/nom (Rust)

Issue:
Add `is_bin_digit` method
## Prerequisites

- Rust version : rustc 1.69.0-nightly (11d96b593 2023-02-01)
- nom version : "7.1.3"

## Question
nom currently provides a set of character test functions like:
[is_digit](https://docs.rs/nom/latest/nom/character/fn.is_digit.html): Tests if byte is ASCII digit: [0-9]
[is_hex_digit](https://docs.rs/nom/latest/nom/character/fn.is_hex_digit.html): Tests if byte is ASCII hex digit: [0-9A-Fa-f]
[is_oct_digit](https://docs.rs/nom/latest/nom/character/fn.is_oct_digit.html): Tests if byte is ASCII octal digit: [0-7]

It appears that there is no method like `is_bin_digit` and `bin_digit0/1` to recognize one or more binary characters: [0-1]. 
At the moment, I am using many1(one_of("01")) to implement this feature, and this will return `Vec<char>`. However, I am not entirely satisfied with this approach. If you have any suggestions for a better method, please let me know. If there are no better solutions, I may consider submitting a pull request at a later time.



Discussion:
I would vote to keep this feature out, I think it's utility is limited and your current solution does not look that painful.



Patch:
diff --git a/doc/choosing_a_combinator.md b/doc/choosing_a_combinator.md
index 6f88f9d9e..4e06019f7 100644
--- a/doc/choosing_a_combinator.md
+++ b/doc/choosing_a_combinator.md
@@ -126,6 +126,7 @@ Use these functions with a combinator like `take_while`:
 - [`is_digit`](https://docs.rs/nom/latest/nom/character/fn.is_digit.html): Tests if byte is ASCII digit: `[0-9]`
 - [`is_hex_digit`](https://docs.rs/nom/latest/nom/character/fn.is_hex_digit.html): Tests if byte is ASCII hex digit: `[0-9A-Fa-f]`
 - [`is_oct_digit`](https://docs.rs/nom/latest/nom/character/fn.is_oct_digit.html): Tests if byte is ASCII octal digit: `[0-7]`
+- [`is_bin_digit`](https://docs.rs/nom/latest/nom/character/fn.is_bin_digit.html): Tests if byte is ASCII binary digit: `[0-1]`
 - [`is_space`](https://docs.rs/nom/latest/nom/character/fn.is_space.html): Tests if byte is ASCII space or tab: `[ \t]`
 - [`is_newline`](https://docs.rs/nom/latest/nom/character/fn.is_newline.html): Tests if byte is ASCII newline: `[\n]`
 
@@ -145,6 +146,7 @@ Alternatively there are ready to use functions:
 - [`newline`](https://docs.rs/nom/latest/nom/character/complete/fn.newline.html): Matches a newline character `\n`
 - [`not_line_ending`](https://docs.rs/nom/latest/nom/character/complete/fn.not_line_ending.html): Recognizes a string of any char except `\r` or `\n`
 - [`oct_digit0`](https://docs.rs/nom/latest/nom/character/complete/fn.oct_digit0.html): Recognizes zero or more octal characters: `[0-7]`. [`oct_digit1`](https://docs.rs/nom/latest/nom/character/complete/fn.oct_digit1.html) does the same but returns at least one character
+- [`bin_digit0`](https://docs.rs/nom/latest/nom/character/complete/fn.bin_digit0.html): Recognizes zero or more binary characters: `[0-1]`. [`bin_digit1`](https://docs.rs/nom/latest/nom/character/complete/fn.bin_digit1.html) does the same but returns at least one character
 - [`rest`](https://docs.rs/nom/latest/nom/combinator/fn.rest.html): Return the remaining input
 - [`space0`](https://docs.rs/nom/latest/nom/character/complete/fn.space0.html): Recognizes zero or more spaces and tabs. [`space1`](https://docs.rs/nom/latest/nom/character/complete/fn.space1.html) does the same but returns at least one character
 - [`tab`](https://docs.rs/nom/latest/nom/character/complete/fn.tab.html): Matches a tab character `\t`
diff --git a/src/character/complete.rs b/src/character/complete.rs
index d6a44cb9c..a61239f97 100644
--- a/src/character/complete.rs
+++ b/src/character/complete.rs
@@ -524,6 +524,56 @@ where
   input.split_at_position1_complete(|item| !item.is_oct_digit(), ErrorKind::OctDigit)
 }
 
+/// Recognizes zero or more binary characters: 0-1
+///
+/// *Complete version*: Will return the whole input if no terminating token is found (a non binary
+/// digit character).
+/// # Example
+///
+/// ```
+/// # use nom::{Err, error::ErrorKind, IResult, Needed};
+/// # use nom::character::complete::bin_digit0;
+/// fn parser(input: &str) -> IResult<&str, &str> {
+///     bin_digit0(input)
+/// }
+///
+/// assert_eq!(parser("013a"), Ok(("3a", "01")));
+/// assert_eq!(parser("a013"), Ok(("a013", "")));
+/// assert_eq!(parser(""), Ok(("", "")));
+/// ```
+pub fn bin_digit0<T, E: ParseError<T>>(input: T) -> IResult<T, T, E>
+where
+  T: Input,
+  <T as Input>::Item: AsChar,
+{
+  input.split_at_position_complete(|item| !item.is_bin_digit())
+}
+
+/// Recognizes one or more binary characters: 0-1
+///
+/// *Complete version*: Will return an error if there's not enough input data,
+/// or the whole input if no terminating token is found (a non binary digit character).
+/// # Example
+///
+/// ```
+/// # use nom::{Err, error::{Error, ErrorKind}, IResult, Needed};
+/// # use nom::character::complete::bin_digit1;
+/// fn parser(input: &str) -> IResult<&str, &str> {
+///     bin_digit1(input)
+/// }
+///
+/// assert_eq!(parser("013a"), Ok(("3a", "01")));
+/// assert_eq!(parser("a013"), Err(Err::Error(Error::new("a013", ErrorKind::BinDigit))));
+/// assert_eq!(parser(""), Err(Err::Error(Error::new("", ErrorKind::BinDigit))));
+/// ```
+pub fn bin_digit1<T, E: ParseError<T>>(input: T) -> IResult<T, T, E>
+where
+  T: Input,
+  <T as Input>::Item: AsChar,
+{
+  input.split_at_position1_complete(|item| !item.is_bin_digit(), ErrorKind::BinDigit)
+}
+
 /// Recognizes zero or more ASCII numerical and alphabetic characters: 0-9, a-z, A-Z
 ///
 /// *Complete version*: Will return the whole input if no terminating token is found (a non
@@ -873,6 +923,13 @@ mod tests {
     assert_eq!(oct_digit1::<_, (_, ErrorKind)>(b), Ok((empty, b)));
     assert_eq!(oct_digit1(c), Err(Err::Error((c, ErrorKind::OctDigit))));
     assert_eq!(oct_digit1(d), Err(Err::Error((d, ErrorKind::OctDigit))));
+    assert_eq!(bin_digit1(a), Err(Err::Error((a, ErrorKind::BinDigit))));
+    assert_eq!(
+      bin_digit1::<_, (_, ErrorKind)>(b), 
+      Ok((&b"234"[..], &b"1"[..]))
+    );
+    assert_eq!(bin_digit1(c), Err(Err::Error((c, ErrorKind::BinDigit))));
+    assert_eq!(bin_digit1(d), Err(Err::Error((d, ErrorKind::BinDigit))));
     assert_eq!(alphanumeric1::<_, (_, ErrorKind)>(a), Ok((empty, a)));
     //assert_eq!(fix_error!(b,(), alphanumeric), Ok((empty, b)));
     assert_eq!(alphanumeric1::<_, (_, ErrorKind)>(c), Ok((empty, c)));
@@ -910,6 +967,13 @@ mod tests {
     assert_eq!(oct_digit1::<_, (_, ErrorKind)>(b), Ok((empty, b)));
     assert_eq!(oct_digit1(c), Err(Err::Error((c, ErrorKind::OctDigit))));
     assert_eq!(oct_digit1(d), Err(Err::Error((d, ErrorKind::OctDigit))));
+    assert_eq!(bin_digit1(a), Err(Err::Error((a, ErrorKind::BinDigit))));
+    assert_eq!(
+      bin_digit1::<_, (_, ErrorKind)>(b), 
+      Ok(("234", "1"))
+    );
+    assert_eq!(bin_digit1(c), Err(Err::Error((c, ErrorKind::BinDigit))));
+    assert_eq!(bin_digit1(d), Err(Err::Error((d, ErrorKind::BinDigit))));
     assert_eq!(alphanumeric1::<_, (_, ErrorKind)>(a), Ok((empty, a)));
     //assert_eq!(fix_error!(b,(), alphanumeric), Ok((empty, b)));
     assert_eq!(alphanumeric1::<_, (_, ErrorKind)>(c), Ok((empty, c)));
@@ -969,6 +1033,12 @@ mod tests {
       }
       _ => panic!("wrong return type in offset test for oct_digit"),
     }
+    match bin_digit1::<_, (_, ErrorKind)>(f) {
+      Ok((i, _)) => {
+        assert_eq!(f.offset(i) + i.len(), f.len());
+      }
+      _ => panic!("wrong return type in offset test for bin_digit"),
+    }
   }
 
   #[test]
@@ -1075,6 +1145,29 @@ mod tests {
     assert!(!crate::character::is_oct_digit(b'\x60'));
   }
 
+  #[test]
+  fn bin_digit_test() {
+    let i = &b"101010;"[..];
+    assert_parse!(bin_digit1(i), Ok((&b";"[..], &i[..i.len() - 1])));
+
+    let i = &b"2"[..];
+    assert_parse!(
+      bin_digit1(i),
+      Err(Err::Error(error_position!(i, ErrorKind::BinDigit)))
+    );
+
+    assert!(crate::character::is_bin_digit(b'0'));
+    assert!(crate::character::is_bin_digit(b'1'));
+    assert!(!crate::character::is_bin_digit(b'8'));
+    assert!(!crate::character::is_bin_digit(b'9'));
+    assert!(!crate::character::is_bin_digit(b'a'));
+    assert!(!crate::character::is_bin_digit(b'A'));
+    assert!(!crate::character::is_bin_digit(b'/'));
+    assert!(!crate::character::is_bin_digit(b':'));
+    assert!(!crate::character::is_bin_digit(b'@'));
+    assert!(!crate::character::is_bin_digit(b'\x60'));
+  }
+
   #[test]
   fn full_line_windows() {
     use crate::sequence::pair;
diff --git a/src/character/mod.rs b/src/character/mod.rs
index 7ed98e92f..ff3228d7f 100644
--- a/src/character/mod.rs
+++ b/src/character/mod.rs
@@ -67,6 +67,22 @@ pub fn is_oct_digit(chr: u8) -> bool {
   matches!(chr, 0x30..=0x37)
 }
 
+/// Tests if byte is ASCII binary digit: 0-1
+///
+/// # Example
+///
+/// ```
+/// # use nom::character::is_bin_digit;
+/// assert_eq!(is_bin_digit(b'a'), false);
+/// assert_eq!(is_bin_digit(b'2'), false);
+/// assert_eq!(is_bin_digit(b'0'), true);
+/// assert_eq!(is_bin_digit(b'1'), true);
+/// ```
+#[inline]
+pub fn is_bin_digit(chr: u8) -> bool {
+  matches!(chr, 0x30..=0x31)
+}
+
 /// Tests if byte is ASCII alphanumeric: A-Z, a-z, 0-9
 ///
 /// # Example
diff --git a/src/character/streaming.rs b/src/character/streaming.rs
index f6a8a17e1..fb49bfc03 100644
--- a/src/character/streaming.rs
+++ b/src/character/streaming.rs
@@ -459,6 +459,48 @@ where
   input.split_at_position1(|item| !item.is_oct_digit(), ErrorKind::OctDigit)
 }
 
+/// Recognizes zero or more binary characters: 0-1
+///
+/// *Streaming version*: Will return `Err(nom::Err::Incomplete(_))` if there's not enough input data,
+/// or if no terminating token is found (a non binary digit character).
+/// # Example
+///
+/// ```
+/// # use nom::{Err, error::ErrorKind, IResult, Needed};
+/// # use nom::character::streaming::bin_digit0;
+/// assert_eq!(bin_digit0::<_, (_, ErrorKind)>("013a"), Ok(("3a", "01")));
+/// assert_eq!(bin_digit0::<_, (_, ErrorKind)>("a013"), Ok(("a013", "")));
+/// assert_eq!(bin_digit0::<_, (_, ErrorKind)>(""), Err(Err::Incomplete(Needed::new(1))));
+/// ```
+pub fn bin_digit0<T, E: ParseError<T>>(input: T) -> IResult<T, T, E>
+where
+  T: Input,
+  <T as Input>::Item: AsChar,
+{
+  input.split_at_position(|item| !item.is_bin_digit())
+}
+
+/// Recognizes one or more binary characters: 0-1
+///
+/// *Streaming version*: Will return `Err(nom::Err::Incomplete(_))` if there's not enough input data,
+/// or if no terminating token is found (a non binary digit character).
+/// # Example
+///
+/// ```
+/// # use nom::{Err, error::ErrorKind, IResult, Needed};
+/// # use nom::character::streaming::bin_digit1;
+/// assert_eq!(bin_digit1::<_, (_, ErrorKind)>("013a"), Ok(("3a", "01")));
+/// assert_eq!(bin_digit1::<_, (_, ErrorKind)>("a013"), Err(Err::Error(("a013", ErrorKind::BinDigit))));
+/// assert_eq!(bin_digit1::<_, (_, ErrorKind)>(""), Err(Err::Incomplete(Needed::new(1))));
+/// ```
+pub fn bin_digit1<T, E: ParseError<T>>(input: T) -> IResult<T, T, E>
+where
+  T: Input,
+  <T as Input>::Item: AsChar,
+{
+  input.split_at_position1(|item| !item.is_bin_digit(), ErrorKind::BinDigit)
+}
+
 /// Recognizes zero or more ASCII numerical and alphabetic characters: 0-9, a-z, A-Z
 ///
 /// *Streaming version*: Will return `Err(nom::Err::Incomplete(_))` if there's not enough input data,
@@ -809,6 +851,17 @@ mod tests {
       alphanumeric1::<_, (_, ErrorKind)>(a),
       Err(Err::Incomplete(Needed::new(1)))
     );
+    assert_eq!(bin_digit1(a), Err(Err::Error((a, ErrorKind::BinDigit))));
+    assert_eq!(
+      bin_digit1::<_, (_, ErrorKind)>(b),
+      Ok((&b"234"[..], &b"1"[..]))
+    );
+    assert_eq!(bin_digit1(c), Err(Err::Error((c, ErrorKind::BinDigit))));
+    assert_eq!(bin_digit1(d), Err(Err::Error((d, ErrorKind::BinDigit))));
+    assert_eq!(
+      alphanumeric1::<_, (_, ErrorKind)>(a),
+      Err(Err::Incomplete(Needed::new(1)))
+    );
     //assert_eq!(fix_error!(b,(), alphanumeric1), Ok((empty, b)));
     assert_eq!(
       alphanumeric1::<_, (_, ErrorKind)>(c),
@@ -868,6 +921,13 @@ mod tests {
     );
     assert_eq!(oct_digit1(c), Err(Err::Error((c, ErrorKind::OctDigit))));
     assert_eq!(oct_digit1(d), Err(Err::Error((d, ErrorKind::OctDigit))));
+    assert_eq!(bin_digit1(a), Err(Err::Error((a, ErrorKind::BinDigit))));
+    assert_eq!(
+      bin_digit1::<_, (_, ErrorKind)>(b),
+      Ok(("234", "1"))
+    );
+    assert_eq!(bin_digit1(c), Err(Err::Error((c, ErrorKind::BinDigit))));
+    assert_eq!(bin_digit1(d), Err(Err::Error((d, ErrorKind::BinDigit))));
     assert_eq!(
       alphanumeric1::<_, (_, ErrorKind)>(a),
       Err(Err::Incomplete(Needed::new(1)))
@@ -936,6 +996,12 @@ mod tests {
       }
       _ => panic!("wrong return type in offset test for oct_digit"),
     }
+    match bin_digit1::<_, (_, ErrorKind)>(f) {
+      Ok((i, _)) => {
+        assert_eq!(f.offset(i) + i.len(), f.len());
+      }
+      _ => panic!("wrong return type in offset test for bin_digit"),
+    }
   }
 
   #[test]
@@ -1048,6 +1114,29 @@ mod tests {
     assert!(!crate::character::is_oct_digit(b'\x60'));
   }
 
+  #[test]
+  fn bin_digit_test() {
+    let i = &b"01;"[..];
+    assert_parse!(bin_digit1(i), Ok((&b";"[..], &i[..i.len() - 1])));
+
+    let i = &b"8"[..];
+    assert_parse!(
+      bin_digit1(i),
+      Err(Err::Error(error_position!(i, ErrorKind::BinDigit)))
+    );
+
+    assert!(crate::character::is_bin_digit(b'0'));
+    assert!(crate::character::is_bin_digit(b'1'));
+    assert!(!crate::character::is_bin_digit(b'8'));
+    assert!(!crate::character::is_bin_digit(b'9'));
+    assert!(!crate::character::is_bin_digit(b'a'));
+    assert!(!crate::character::is_bin_digit(b'A'));
+    assert!(!crate::character::is_bin_digit(b'/'));
+    assert!(!crate::character::is_bin_digit(b':'));
+    assert!(!crate::character::is_bin_digit(b'@'));
+    assert!(!crate::character::is_bin_digit(b'\x60'));
+  }
+
   #[test]
   fn full_line_windows() {
     fn take_full_line(i: &[u8]) -> IResult<&[u8], (&[u8], &[u8])> {
diff --git a/src/error.rs b/src/error.rs
index 22344b5c3..067187d54 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -436,6 +436,7 @@ pub enum ErrorKind {
   Digit,
   HexDigit,
   OctDigit,
+  BinDigit,
   AlphaNumeric,
   Space,
   MultiSpace,
@@ -534,6 +535,7 @@ pub fn error_to_u32(e: &ErrorKind) -> u32 {
     ErrorKind::Fail                      => 75,
     ErrorKind::Many                      => 76,
     ErrorKind::Fold                      => 77,
+    ErrorKind::BinDigit                  => 78,
   }
 }
 
@@ -584,6 +586,7 @@ impl ErrorKind {
       ErrorKind::ManyMN                    => "Many(m, n)",
       ErrorKind::HexDigit                  => "Hexadecimal Digit",
       ErrorKind::OctDigit                  => "Octal digit",
+      ErrorKind::BinDigit                  => "Binary digit",
       ErrorKind::Not                       => "Negation",
       ErrorKind::Permutation               => "Permutation",
       ErrorKind::ManyTill                  => "ManyTill",
diff --git a/src/traits.rs b/src/traits.rs
index c71dc77a6..be3b19d51 100644
--- a/src/traits.rs
+++ b/src/traits.rs
@@ -521,6 +521,8 @@ pub trait AsChar: Copy {
   fn is_hex_digit(self) -> bool;
   /// Tests that self is an octal digit
   fn is_oct_digit(self) -> bool;
+  /// Tests that self is a binary digit
+  fn is_bin_digit(self) -> bool;
   /// Gets the len in bytes for self
   fn len(self) -> usize;
 }
@@ -551,6 +553,10 @@ impl AsChar for u8 {
     matches!(self, 0x30..=0x37)
   }
   #[inline]
+  fn is_bin_digit(self) -> bool {
+      matches!(self, 0x30..=0x31)
+  }
+  #[inline]
   fn len(self) -> usize {
     1
   }
@@ -581,6 +587,10 @@ impl<'a> AsChar for &'a u8 {
     matches!(*self, 0x30..=0x37)
   }
   #[inline]
+  fn is_bin_digit(self) -> bool {
+    matches!(*self, 0x30..=0x31)
+  }
+  #[inline]
   fn len(self) -> usize {
     1
   }
@@ -612,6 +622,10 @@ impl AsChar for char {
     self.is_digit(8)
   }
   #[inline]
+  fn is_bin_digit(self) -> bool {
+    self.is_digit(2)
+  }
+  #[inline]
   fn len(self) -> usize {
     self.len_utf8()
   }
@@ -643,6 +657,10 @@ impl<'a> AsChar for &'a char {
     self.is_digit(8)
   }
   #[inline]
+  fn is_bin_digit(self) -> bool {
+    self.is_digit(2)
+  }
+  #[inline]
   fn len(self) -> usize {
     self.len_utf8()
   }


Test patch:
diff --git a/src/bytes/tests.rs b/src/bytes/tests.rs
index 1d6ef4702..8811be25b 100644
--- a/src/bytes/tests.rs
+++ b/src/bytes/tests.rs
@@ -1,7 +1,7 @@
 use crate::character::is_alphabetic;
 use crate::character::streaming::{
   alpha1 as alpha, alphanumeric1 as alphanumeric, digit1 as digit, hex_digit1 as hex_digit,
-  multispace1 as multispace, oct_digit1 as oct_digit, space1 as space,
+  multispace1 as multispace, oct_digit1 as oct_digit, bin_digit1 as bin_digit, space1 as space,
 };
 use crate::error::ErrorKind;
 use crate::internal::{Err, IResult, Needed};
@@ -331,6 +331,12 @@ fn recognize() {
   let rod = yod(&b"1234567;"[..]);
   assert_eq!(rod, Ok((semicolon, &b"1234567"[..])));
 
+  fn ybd(i: &[u8]) -> IResult<&[u8], &[u8]> {
+    recognize(bin_digit)(i)
+  }
+  let rbd = ybd(&b"101010;"[..]);
+  assert_eq!(rbd, Ok((semicolon, &b"101010"[..])));
+
   fn yan(i: &[u8]) -> IResult<&[u8], &[u8]> {
     recognize(alphanumeric)(i)
   }