Add string constants

* Implement string parsing in tokenizer
 * Implement escape sequences `\"` and `\\`
 * Document changes
 * Update change log

Relates to #35
This commit is contained in:
Sebastian Schmidt 2019-03-30 11:54:19 +01:00
parent e19770b517
commit 67d68cd974
10 changed files with 100 additions and 37 deletions

View File

@ -6,6 +6,8 @@
### Added ### Added
* String constants
### Removed ### Removed
### Changed ### Changed

View File

@ -221,12 +221,12 @@ They return the result as the type it was passed into the function.
### Values ### Values
Operators take values as arguments and produce values as results. Operators take values as arguments and produce values as results.
Values can be boolean, integer or floating point numbers, tuples or the empty type. Values can be boolean, integer or floating point numbers, strings, tuples or the empty type.
Strings are supported as well, but there are no operations defined for them yet.
Values are denoted as displayed in the following table. Values are denoted as displayed in the following table.
| Value type | Example | | Value type | Example |
|------------|---------| |------------|---------|
| `Value::String` | `"abc"`, `""`, `"a\"b\\c"` |
| `Value::Boolean` | `true`, `false` | | `Value::Boolean` | `true`, `false` |
| `Value::Int` | `3`, `-9`, `0`, `135412` | | `Value::Int` | `3`, `-9`, `0`, `135412` |
| `Value::Float` | `3.`, `.35`, `1.00`, `0.5`, `123.554` | | `Value::Float` | `3.`, `.35`, `1.00`, `0.5`, `123.554` |

View File

@ -1,9 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use EvalexprError;
use EvalexprResult;
use function::Function; use function::Function;
use value::value_type::ValueType; use value::value_type::ValueType;
use EvalexprError;
use EvalexprResult;
use crate::value::Value; use crate::value::Value;

View File

@ -82,6 +82,7 @@ impl fmt::Display for EvalexprError {
write!(f, "Error modulating {} % {}", dividend, divisor) write!(f, "Error modulating {} % {}", dividend, divisor)
}, },
ContextNotManipulable => write!(f, "Cannot manipulate context"), ContextNotManipulable => write!(f, "Cannot manipulate context"),
IllegalEscapeSequence(string) => write!(f, "Illegal escape sequence: {}", string),
CustomMessage(message) => write!(f, "Error: {}", message), CustomMessage(message) => write!(f, "Error: {}", message),
} }
} }

View File

@ -6,7 +6,7 @@
//! They are meant as shortcuts to not write the same error checking code everywhere. //! They are meant as shortcuts to not write the same error checking code everywhere.
use token::PartialToken; use token::PartialToken;
use value::{TupleType, value_type::ValueType}; use value::{value_type::ValueType, TupleType};
use crate::value::Value; use crate::value::Value;
@ -163,6 +163,9 @@ pub enum EvalexprError {
/// A modification was attempted on a `Context` that does not allow modifications. /// A modification was attempted on a `Context` that does not allow modifications.
ContextNotManipulable, ContextNotManipulable,
/// An escape sequence within a string literal is illegal.
IllegalEscapeSequence(String),
/// A custom error explained by its message. /// A custom error explained by its message.
CustomMessage(String), CustomMessage(String),
} }

View File

@ -208,12 +208,12 @@
//! ### Values //! ### Values
//! //!
//! Operators take values as arguments and produce values as results. //! Operators take values as arguments and produce values as results.
//! Values can be boolean, integer or floating point numbers, tuples or the empty type. //! Values can be boolean, integer or floating point numbers, strings, tuples or the empty type.
//! Strings are supported as well, but there are no operations defined for them yet.
//! Values are denoted as displayed in the following table. //! Values are denoted as displayed in the following table.
//! //!
//! | Value type | Example | //! | Value type | Example |
//! |------------|---------| //! |------------|---------|
//! | `Value::String` | `"abc"`, `""`, `"a\"b\\c"` |
//! | `Value::Boolean` | `true`, `false` | //! | `Value::Boolean` | `true`, `false` |
//! | `Value::Int` | `3`, `-9`, `0`, `135412` | //! | `Value::Int` | `3`, `-9`, `0`, `135412` |
//! | `Value::Float` | `3.`, `.35`, `1.00`, `0.5`, `123.554` | //! | `Value::Float` | `3.`, `.35`, `1.00`, `0.5`, `123.554` |
@ -361,7 +361,7 @@ pub use function::Function;
pub use interface::*; pub use interface::*;
pub use tree::Node; pub use tree::Node;
pub use value::{ pub use value::{
EMPTY_VALUE, EmptyType, FloatType, IntType, TupleType, Value, value_type::ValueType, value_type::ValueType, EmptyType, FloatType, IntType, TupleType, Value, EMPTY_VALUE,
}; };
mod context; mod context;

View File

@ -38,6 +38,7 @@ impl fmt::Display for Token {
Float(float) => float.fmt(f), Float(float) => float.fmt(f),
Int(int) => int.fmt(f), Int(int) => int.fmt(f),
Boolean(boolean) => boolean.fmt(f), Boolean(boolean) => boolean.fmt(f),
String(string) => fmt::Debug::fmt(string, f),
} }
} }
} }

View File

@ -38,6 +38,7 @@ pub enum Token {
Float(FloatType), Float(FloatType),
Int(IntType), Int(IntType),
Boolean(bool), Boolean(bool),
String(String),
} }
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -63,6 +64,12 @@ fn char_to_partial_token(c: char) -> PartialToken {
'%' => PartialToken::Token(Token::Percent), '%' => PartialToken::Token(Token::Percent),
'^' => PartialToken::Token(Token::Hat), '^' => PartialToken::Token(Token::Hat),
'(' => PartialToken::Token(Token::LBrace),
')' => PartialToken::Token(Token::RBrace),
',' => PartialToken::Token(Token::Comma),
';' => PartialToken::Token(Token::Semicolon),
'=' => PartialToken::Eq, '=' => PartialToken::Eq,
'!' => PartialToken::ExclamationMark, '!' => PartialToken::ExclamationMark,
'>' => PartialToken::Gt, '>' => PartialToken::Gt,
@ -70,12 +77,6 @@ fn char_to_partial_token(c: char) -> PartialToken {
'&' => PartialToken::Ampersand, '&' => PartialToken::Ampersand,
'|' => PartialToken::VerticalBar, '|' => PartialToken::VerticalBar,
'(' => PartialToken::Token(Token::LBrace),
')' => PartialToken::Token(Token::RBrace),
',' => PartialToken::Token(Token::Comma),
';' => PartialToken::Token(Token::Semicolon),
c => { c => {
if c.is_whitespace() { if c.is_whitespace() {
PartialToken::Whitespace PartialToken::Whitespace
@ -118,6 +119,7 @@ impl Token {
Token::Float(_) => true, Token::Float(_) => true,
Token::Int(_) => true, Token::Int(_) => true,
Token::Boolean(_) => true, Token::Boolean(_) => true,
Token::String(_) => true,
} }
} }
@ -152,31 +154,70 @@ impl Token {
Token::Float(_) => true, Token::Float(_) => true,
Token::Int(_) => true, Token::Int(_) => true,
Token::Boolean(_) => true, Token::Boolean(_) => true,
Token::String(_) => true,
} }
} }
} }
/// Converts a string to a vector of partial tokens. /// Parses an escape sequence within a string literal.
fn str_to_tokens(string: &str) -> Vec<PartialToken> { fn parse_escape_sequence<Iter: Iterator<Item = char>>(iter: &mut Iter) -> EvalexprResult<char> {
let mut result = Vec::new(); match iter.next() {
for c in string.chars() { Some('"') => Ok('"'),
let partial_token = char_to_partial_token(c); Some('\\') => Ok('\\'),
Some(c) => Err(EvalexprError::IllegalEscapeSequence(format!("\\{}", c))),
None => Err(EvalexprError::IllegalEscapeSequence(format!("\\"))),
}
}
let if_let_successful = /// Parses a string value from the given character iterator.
if let (Some(PartialToken::Literal(last)), PartialToken::Literal(literal)) = ///
(result.last_mut(), &partial_token) /// The first character from the iterator is interpreted as first character of the string.
{ /// The string is terminated by a double quote `"`.
last.push_str(literal); /// Occurrences of `"` within the string can be escaped with `\`.
true /// The backslash needs to be escaped with another backslash `\`.
} else { fn parse_string_literal<Iter: Iterator<Item = char>>(
false mut iter: &mut Iter,
}; ) -> EvalexprResult<PartialToken> {
let mut result = String::new();
if !if_let_successful { while let Some(c) = iter.next() {
result.push(partial_token); match c {
'"' => break,
'\\' => result.push(parse_escape_sequence(&mut iter)?),
c => result.push(c),
} }
} }
result
Ok(PartialToken::Token(Token::String(result)))
}
/// Converts a string to a vector of partial tokens.
fn str_to_partial_tokens(string: &str) -> EvalexprResult<Vec<PartialToken>> {
let mut result = Vec::new();
let mut iter = string.chars().peekable();
while let Some(c) = iter.next() {
if c == '"' {
result.push(parse_string_literal(&mut iter)?);
} else {
let partial_token = char_to_partial_token(c);
let if_let_successful =
if let (Some(PartialToken::Literal(last)), PartialToken::Literal(literal)) =
(result.last_mut(), &partial_token)
{
last.push_str(literal);
true
} else {
false
};
if !if_let_successful {
result.push(partial_token);
}
}
}
Ok(result)
} }
/// Resolves all partial tokens by converting them to complex tokens. /// Resolves all partial tokens by converting them to complex tokens.
@ -255,5 +296,5 @@ fn partial_tokens_to_tokens(mut tokens: &[PartialToken]) -> EvalexprResult<Vec<T
} }
pub(crate) fn tokenize(string: &str) -> EvalexprResult<Vec<Token>> { pub(crate) fn tokenize(string: &str) -> EvalexprResult<Vec<Token>> {
partial_tokens_to_tokens(&str_to_tokens(string)) partial_tokens_to_tokens(&str_to_partial_tokens(string)?)
} }

View File

@ -1,9 +1,9 @@
use token::Token;
use value::{TupleType, EMPTY_VALUE};
use EmptyContext; use EmptyContext;
use EmptyType; use EmptyType;
use FloatType; use FloatType;
use IntType; use IntType;
use token::Token;
use value::{EMPTY_VALUE, TupleType};
use crate::{ use crate::{
context::Context, context::Context,
@ -397,9 +397,10 @@ pub(crate) fn tokens_to_operator_tree(tokens: Vec<Token>) -> EvalexprResult<Node
} }
result result
}, },
Token::Float(number) => Some(Node::new(Const::new(Value::Float(number)))), Token::Float(float) => Some(Node::new(Const::new(Value::Float(float)))),
Token::Int(number) => Some(Node::new(Const::new(Value::Int(number)))), Token::Int(int) => Some(Node::new(Const::new(Value::Int(int)))),
Token::Boolean(boolean) => Some(Node::new(Const::new(Value::Boolean(boolean)))), Token::Boolean(boolean) => Some(Node::new(Const::new(Value::Boolean(boolean)))),
Token::String(string) => Some(Node::new(Const::new(Value::String(string)))),
}; };
if let Some(node) = node { if let Some(node) = node {

View File

@ -1,6 +1,6 @@
extern crate evalexpr; extern crate evalexpr;
use evalexpr::{*, error::*}; use evalexpr::{error::*, *};
#[test] #[test]
fn test_unary_examples() { fn test_unary_examples() {
@ -539,6 +539,20 @@ fn test_expression_chaining() {
); );
} }
#[test]
fn test_strings() {
let mut context = HashMapContext::new();
assert_eq!(eval("\"string\""), Ok(Value::from("string")));
assert_eq!(
eval_with_context_mut("a = \"a string\"", &mut context),
Ok(Value::Empty)
);
assert_eq!(
eval_boolean_with_context("a == \"a string\"", &context),
Ok(true)
);
}
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
#[test] #[test]
fn test_serde() { fn test_serde() {