2009-06-12

Tokenizer de rapidito, segunda versión

Como les conté hace un par de posts, estoy haciendo un wiki y blogueando sobre el tokenizer
Hoy les cuento de la segunda versión del tokenizer. A esta versión (que no es compatible con la anterior) le agregué la posibilidad de asociar un tipo (kind) a cada regexp que define un token. Por lo tanto, el método next_match ahora devuelve un par [match, kind]. Los matches de texto común (no delimitadores) tienen kind :text. Los matches de los delimitadores vienen con la MatchData entera, para poder usarla si interesan partes del match a la hora de procesar el token. También dejé de usar =~ para matchear regexps, ya que el
Me quedaron un par de cosas con dudas después de implementar esto.

  1. ¿Se puede hacer en ruby que busque un match de una regexp en un string a partir de una posición? Hacer una regexp del tipo /.{34,}(regex de verdad)/ no vale.
  2. ¿Puedo forzar a la unión de expresiones regulares a buscar el match más largo (en vez del primero que matchee)?
A pesar de las dudas, igual tengo una versión que creo que es significativamente mejor que la versión anterior.
Así que como la otra vez dejo el código:
module Rapidito
class Tokenizer

attr_reader :source

def initialize( delimiters )
@delimiter_list = [[/\Z/, :finish]] +
delimiters.to_a.map { |k,arr| arr.map { |re| [re, k] } }.inject([]) { |ac,ps| ac + ps }
@match_cache = nil
end

def source=(s)
@match_cache = nil
@source = s
end

def has_next?
!@source.empty? || valid_cache?
end

def valid_cache?
(!@match_cache.nil?) && (@match_cache[0].to_s.length > 0)
end

def next_match
@delimiter_list.map {|p| [p[0].match(@source), p[1]]}.reject {|p| p[0].nil?}.inject do
|better,new|
better_pos = better[0].pre_match.length
new_pos = new[0].pre_match.length

if better_pos < new_pos
better
elsif new_pos < better_pos
new
elsif better[0].to_s.length > new[0].to_s.length
better
else
new
end
end
end

def next_token
if @match_cache #cached delimiter
rv = @match_cache
@match_cache = nil
return rv
end

match = next_match
p = match[0].pre_match.length
@source = @source[p + match[0].to_s.length, @source.length]

if p == 0 #delimiter
[match[0].to_s, match[1]]
else #text
@match_cache = match
[match[0].pre_match, :text]
end
end

def all_tokens
tokens = []
while has_next?
tokens << next_token
end
tokens
end
end
end

Y los tests de unidad:
require 'test/unit'
require 'rapidito/tokenizer'

include Rapidito

class TokenizerTest < Test::Unit::TestCase

def test_no_token
tok = Tokenizer.new( {} )
tok.source = "aaaa"
assert_equal true, tok.has_next?
assert_equal ["aaaa", :text], tok.next_token
assert_equal false, tok.has_next?
end

def assert_all_tokens( expected, tokenizer )
assert_equal expected,
tokenizer.all_tokens.map { |token, kind| [token.to_s, kind] }
end

def test_two_delimiters
tok = Tokenizer.new(
:a_kind => [/\|/, /;;/]
)

tok.source = "aa|bbb;;;;cccc"
assert_all_tokens \
[ ["aa", :text], ["|", :a_kind], ["bbb", :text],
[";;", :a_kind], [";;", :a_kind], ["cccc", :text] ],
tok

tok.source = "aa;;bbb||cccc"
assert_all_tokens \
[ ["aa", :text], [";;", :a_kind], ["bbb", :text],
["|", :a_kind], ["|", :a_kind], ["cccc", :text] ],
tok
end

def test_choose_longest_match
tok = Tokenizer.new(
:a_kind => [/aa/, /aaa/]
)
tok.source = "aaaa"
assert_equal [ ["aaa", :a_kind], ["a", :text ] ], tok.all_tokens
end

def test_reset_precache
tok = Tokenizer.new(
:a_kind => [/\|/, /,/]
)
tok.source = "original start|original end"
tok.next_token
tok.source = "new start,new end"
assert_equal ["new start", :text], tok.next_token
end

def test_almost_finished
tok = Tokenizer.new( :a_kind => [/!/] )
tok.source = "bang!"
tok.next_token
assert_equal true, tok.has_next?
tok.next_token
assert_equal false, tok.has_next?
end
end

Happy hacking,
Aureliano.

No hay comentarios.: