I'm writing an s-expression tokenizer in Rust.
I have the following code written, but it is not idiomatic Rust - it is quite ugly. TokenizerI
is simply an interface with a body that is really of no import here other than to note that _s
is unused (hence the underscore).
extern crate regex;
use regex::Regex;
use std::cmp;
use tokenize::api::TokenizerI;
pub struct SExprTokenizer {
strict: bool,
open_paren: &'static str,
close_paren: &'static str,
paren_regexp: regex::Regex
}
impl TokenizerI for SExprTokenizer {
fn tokenize<'a>(&'a self, _s: &'a str) -> Result<Vec<&str>, String> {
let mut result = Vec::new();
let mut pos = 0;
let mut depth = 0;
for cap in self.paren_regexp.captures_iter(_s) {
let paren = cap.at(0).unwrap();
let (start, end) = cap.pos(0).unwrap();
if depth == 0 {
for token in _s[pos..start].split(|c: char| c.is_whitespace())
.filter(|s| !s.is_empty()) {
result.push(token);
}
pos = start;
}
if paren == self.open_paren {
depth = depth + 1;
}
if paren == self.close_paren {
if self.strict && depth == 0 {
return Err(String::from(format!("Unmatched open token at {}", pos)));
}
depth = cmp::max(0, depth-1);
if depth == 0 {
result.push(&_s[pos..end]);
pos = end;
}
}
}
Ok(result)
}
}
It is based on the following Python:
import re from nltk.tokenize.api import TokenizerI class SExprTokenizer(TokenizerI): """ A tokenizer that divides strings into s-expressions. An s-expresion can be either: - a parenthesized expression, including any nested parenthesized expressions, or - a sequence of non-whitespace non-parenthesis characters. For example, the string ``(a (b c)) d e (f)`` consists of four s-expressions: ``(a (b c))``, ``d``, ``e``, and ``(f)``. By default, the characters ``(`` and ``)`` are treated as open and close parentheses, but alternative strings may be specified. :param parens: A two-element sequence specifying the open and close parentheses that should be used to find sexprs. This will typically be either a two-character string, or a list of two strings. :type parens: str or list :param strict: If true, then raise an exception when tokenizing an ill-formed sexpr. """ def __init__(self, parens='()', strict=True): if len(parens) != 2: raise ValueError('parens must contain exactly two strings') self._strict = strict self._open_paren = parens[0] self._close_paren = parens[1] self._paren_regexp = re.compile('%s|%s' % (re.escape(parens[0]), re.escape(parens[1]))) def tokenize(self, text): """ Return a list of s-expressions extracted from *text*. For example: >>> SExprTokenizer().tokenize('(a b (c d)) e f (g)') ['(a b (c d))', 'e', 'f', '(g)'] All parentheses are assumed to mark s-expressions. (No special processing is done to exclude parentheses that occur inside strings, or following backslash characters.) If the given expression contains non-matching parentheses, then the behavior of the tokenizer depends on the ``strict`` parameter to the constructor. If ``strict`` is ``True``, then raise a ``ValueError``. If ``strict`` is ``False``, then any unmatched close parentheses will be listed as their own s-expression; and the last partial s-expression with unmatched open parentheses will be listed as its own s-expression: >>> SExprTokenizer(strict=False).tokenize('c) d) e (f (g') ['c', ')', 'd', ')', 'e', '(f (g'] :param text: the string to be tokenized :type text: str or iter(str) :rtype: iter(str) """ result = [] pos = 0 depth = 0 for m in self._paren_regexp.finditer(text): paren = m.group() if depth == 0: result += text[pos:m.start()].split() pos = m.start() if paren == self._open_paren: depth += 1 if paren == self._close_paren: if self._strict and depth == 0: raise ValueError('Un-matched close paren at char %d' % m.start()) depth = max(0, depth-1) if depth == 0: result.append(text[pos:m.end()]) pos = m.end() if self._strict and depth > 0: raise ValueError('Un-matched open paren at char %d' % pos) if pos < len(text): result.append(text[pos:]) return result
The Python explains use and provides a few tests. I have the following written, which pass:
#[cfg(test)]
mod test_sexpr {
use regex::Regex;
use tokenize::api::TokenizerI;
use super::SExprTokenizer;
#[test]
fn passing_strict_parens_test() {
let strict = true;
let open_paren = "(";
let close_paren = ")";
let paren_regexp = Regex::new(
&format!("\\{}|\\{}", open_paren, close_paren)
).unwrap();
let tokenizer = SExprTokenizer {
strict: strict,
open_paren: open_paren,
close_paren: close_paren,
paren_regexp: paren_regexp
};
let text = "(a b (c d)) e f (g)";
let expected = vec!["(a b (c d))", "e", "f", "(g)"];
let result = tokenizer.tokenize(text).unwrap();
assert_eq!(expected, result);
}
#[test]
fn passing_strict_braces_test() {
let strict = true;
let open_paren = "{";
let close_paren = "}";
let paren_regexp = Regex::new(
&format!("\\{}|\\{}", open_paren, close_paren)
).unwrap();
let tokenizer = SExprTokenizer {
strict: strict,
open_paren: open_paren,
close_paren: close_paren,
paren_regexp: paren_regexp
};
let text = "{a b {c d}} e f {g}";
let expected = vec!["{a b {c d}}", "e", "f", "{g}"];
let result = tokenizer.tokenize(text).unwrap();
assert_eq!(expected, result);
}
#[test]
#[should_panic(expected = "Unmatched open token at 20")]
fn failing_strict_braces_test() {
let strict = true;
let open_paren = "{";
let close_paren = "}";
let paren_regexp = Regex::new(
&format!("\\{}|\\{}", open_paren, close_paren)
).unwrap();
let tokenizer = SExprTokenizer {
strict: strict,
open_paren: open_paren,
close_paren: close_paren,
paren_regexp: paren_regexp
};
let text = "{a b {c d}} e f {g} }";
let _result = tokenizer.tokenize(text).unwrap();
// Tests expectedly fails - no need to assert.
}
}