From f35054d5cae6b653cf1af30ff6c32ce4e41499fd Mon Sep 17 00:00:00 2001 From: Valentine Briese Date: Tue, 25 Nov 2025 19:38:41 -0800 Subject: [PATCH] Parse binary and octal integers --- README.md | 2 +- src/lib.rs | 2 +- src/token/mod.rs | 15 +++++++---- src/value/numeric_types.rs | 8 ++++++ .../numeric_types/default_numeric_types.rs | 8 ++++++ tests/integration.rs | 26 +++++++++++++++++++ 6 files changed, 54 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f2e9c4e..d78871d 100644 --- a/README.md +++ b/README.md @@ -437,7 +437,7 @@ Values are denoted as displayed in the following table. |------------|---------| | `Value::String` | `"abc"`, `""`, `"a\"b\\c"` | | `Value::Boolean` | `true`, `false` | -| `Value::Int` | `3`, `-9`, `0`, `135412`, `0xfe02`, `-0x1e` | +| `Value::Int` | `3`, `-9`, `0`, `135412`, `0xfe02`, `-0x1e`, `0b101`, `0o17` | | `Value::Float` | `3.`, `.35`, `1.00`, `0.5`, `123.554`, `23e4`, `-2e-3`, `3.54e+2` | | `Value::Tuple` | `(3, 55.0, false, ())`, `(1, 2)` | | `Value::Empty` | `()` | diff --git a/src/lib.rs b/src/lib.rs index 50136e6..0861e4a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -417,7 +417,7 @@ //! |------------|---------| //! | `Value::String` | `"abc"`, `""`, `"a\"b\\c"` | //! | `Value::Boolean` | `true`, `false` | -//! | `Value::Int` | `3`, `-9`, `0`, `135412`, `0xfe02`, `-0x1e` | +//! | `Value::Int` | `3`, `-9`, `0`, `135412`, `0xfe02`, `-0x1e`, `0b101`, `0o17` | //! | `Value::Float` | `3.`, `.35`, `1.00`, `0.5`, `123.554`, `23e4`, `-2e-3`, `3.54e+2` | //! | `Value::Tuple` | `(3, 55.0, false, ())`, `(1, 2)` | //! | `Value::Empty` | `()` | diff --git a/src/token/mod.rs b/src/token/mod.rs index 998cfda..804be60 100644 --- a/src/token/mod.rs +++ b/src/token/mod.rs @@ -402,7 +402,7 @@ fn partial_tokens_to_tokens( }, PartialToken::Literal(literal) => { cutoff = 1; - if let Ok(number) = parse_dec_or_hex::(&literal) { + if let Ok(number) = parse_integer::(&literal) { Some(Token::Int(number)) } else if let Ok(number) = literal.parse::() { Some(Token::Float(number)) @@ -495,11 +495,16 @@ pub(crate) fn tokenize( partial_tokens_to_tokens(&str_to_partial_tokens(string)?) } -fn parse_dec_or_hex( +/// Can parse decimal (base 10), hexadecimal (base 16), binary (base 2), or octal (base 8). +fn parse_integer( literal: &str, ) -> Result { if let Some(literal) = literal.strip_prefix("0x") { NumericTypes::Int::from_hex_str(literal) + } else if let Some(literal) = literal.strip_prefix("0b") { + NumericTypes::Int::from_binary_str(literal) + } else if let Some(literal) = literal.strip_prefix("0o") { + NumericTypes::Int::from_octal_str(literal) } else { NumericTypes::Int::from_str(literal).map_err(|_| ()) } @@ -546,11 +551,11 @@ mod tests { let token_string = "+ - * / % ^ == != > < >= <= && || ! ( ) = += -= *= /= %= ^= &&= ||= , ; "; - let token_string_with_comments = r"+ - * / % ^ == != > - < >= <= && /* inline comment */ || ! ( ) + let token_string_with_comments = r"+ - * / % ^ == != > + < >= <= && /* inline comment */ || ! ( ) = += -= *= /= %= ^= // line comment - &&= ||= , ; + &&= ||= , ; "; let tokens = tokenize::(token_string_with_comments).unwrap(); diff --git a/src/value/numeric_types.rs b/src/value/numeric_types.rs index 2620450..153fe26 100644 --- a/src/value/numeric_types.rs +++ b/src/value/numeric_types.rs @@ -58,6 +58,14 @@ pub trait EvalexprInt>: #[expect(clippy::result_unit_err)] fn from_hex_str(literal: &str) -> Result; + /// Parse `Self` from a binary string. + #[expect(clippy::result_unit_err)] + fn from_binary_str(literal: &str) -> Result; + + /// Parse `Self` from an octal string. + #[expect(clippy::result_unit_err)] + fn from_octal_str(literal: &str) -> Result; + /// Perform an addition operation, returning an error on overflow. fn checked_add(&self, rhs: &Self) -> EvalexprResult; diff --git a/src/value/numeric_types/default_numeric_types.rs b/src/value/numeric_types/default_numeric_types.rs index 538d96f..841ee2d 100644 --- a/src/value/numeric_types/default_numeric_types.rs +++ b/src/value/numeric_types/default_numeric_types.rs @@ -50,6 +50,14 @@ impl> EvalexprInt f Self::from_str_radix(literal, 16).map_err(|_| ()) } + fn from_binary_str(literal: &str) -> Result { + Self::from_str_radix(literal, 2).map_err(|_| ()) + } + + fn from_octal_str(literal: &str) -> Result { + Self::from_str_radix(literal, 8).map_err(|_| ()) + } + fn checked_add(&self, rhs: &Self) -> EvalexprResult { let result = (*self).checked_add(*rhs); if let Some(result) = result { diff --git a/tests/integration.rs b/tests/integration.rs index cbd0852..f40bc38 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -2426,6 +2426,32 @@ fn test_hex() { ); } +#[test] +fn test_binary() { + assert_eq!(eval("0b11"), Ok(Value::Int(3))); + assert_eq!(eval("0b0101"), Ok(Value::Int(5))); + assert_eq!(eval("0b11111111"), Ok(Value::Int(255))); + assert_eq!(eval("-0b11111111"), Ok(Value::Int(-255))); + assert_eq!( + eval("0b2"), + // See `test_hex`. + Err(EvalexprError::VariableIdentifierNotFound("0b2".into())) + ); +} + +#[test] +fn test_octal() { + assert_eq!(eval("0o12"), Ok(Value::Int(10))); + assert_eq!(eval("0o3"), Ok(Value::Int(3))); + assert_eq!(eval("0o377"), Ok(Value::Int(255))); + assert_eq!(eval("-0o377"), Ok(Value::Int(-255))); + assert_eq!( + eval("0o8"), + // See `test_hex`. + Err(EvalexprError::VariableIdentifierNotFound("0o8".into())) + ); +} + #[test] fn test_broken_string() { assert_eq!(