Ruby中的解析器:#slice! 在里面#each_with_index =缺少元素

让我们说,我想从数组中分离元素的某些组合。 例如

data = %w{ start before rgb 255 255 255 between hex FFFFFF after end } rgb, hex = [], [] data.each_with_index do |v,i| p [i,v] case v.downcase when 'rgb' then rgb = data.slice! i,4 when 'hex' then hex = data.slice! i,2 end end pp [rgb, hex, data] # >> [0, "start"] # >> [1, "before"] # >> [2, "rgb"] # >> [3, "hex"] # >> [4, "end"] # >> [["rgb", "255", "255", "255"], # >> ["hex", "FFFFFF"], # >> ["start", "before", "between", "after", "end"]] 

代码已经完成了正确的提取,但它在提取的集合之后错过了元素。 所以,如果我的数据arrays是

 data = %w{ start before rgb 255 255 255 hex FFFFFF after end } 

然后

 pp [rgb, hex, data] # >> [["rgb", "255", "255", "255"], # >> [], # >> ["start", "before", "hex", "FFFFFF", "after", "end"]] 

为什么会这样? 如何在#each_with_index找到那些遗漏的元素? 或者可能有一个更好的解决方案,假设有更多的集合要提取?

问题是, 您迭代它 ,您正在改变集合。 这不可行。 (在我看来,它不应该。在这种情况下,Ruby应该引发exception,而不是默默地允许不正确的行为。这就是几乎所有其他命令式语言所做的。)

这是我能想到的最好的,同时保持原有的风格:

 require 'pp' data = %w[start before rgb 255 255 255 hex FFFFFF after end] rgb_count = hex_count = 0 rgb, hex, rest = data.reduce([[], [], []]) do |acc, el| acc.tap do |rgb, hex, rest| next (rgb_count = 3 ; rgb << el) if /rgb/i =~ el next (rgb_count -= 1 ; rgb << el) if rgb_count > 0 next (hex_count = 1 ; hex << el) if /hex/i =~ el next (hex_count -= 1 ; hex << el) if hex_count > 0 rest << el end end data.replace(rest) pp rgb, hex, data # ["rgb", "255", "255", "255"] # ["hex", "FFFFFF"] # ["start", "before", "after", "end"] 

但是,你所拥有的是一个解析问题,它应该由解析器真正解决。 一个简单的手动解析器/状态机可能会比上面的代码多一点,但它将更具可读性。

这是一个简单的递归下降解析器,可以解决您的问题:

 class ColorParser def initialize(input) @input = input.dup @rgb, @hex, @data = [], [], [] end def parse parse_element until @input.empty? return @rgb, @hex, @data end private def parse_element parse_color or parse_stop_word end def parse_color parse_rgb or parse_hex end def parse_rgb return unless /rgb/i =~ peek @rgb << consume parse_rgb_values end 

我非常喜欢递归下降解析器,因为它们的结构几乎完全匹配语法:只需保持解析元素直到输入为空。 什么是元素? 嗯,这是一个颜色规范或停止词。 什么是颜色规格? 嗯,它是RGB颜色规格或hex颜色规格。 什么是RGB颜色规格? 好吧,它与Regexp /rgb/i匹配,后跟RGB值。 什么是RGB值? 嗯,这只是三个数字......

  def parse_rgb_values 3.times do @rgb << consume.to_i end end def parse_hex return unless /hex/i =~ peek @hex << consume parse_hex_value end def parse_hex_value @hex << consume.to_i(16) end def parse_stop_word @data << consume unless /rgb|hex/i =~ peek end def consume @input.slice!(0) end def peek @input.first end end 

像这样使用它:

 data = %w[start before rgb 255 255 255 hex FFFFFF after end] rgb, hex, rest = ColorParser.new(data).parse require 'pp' pp rgb, hex, rest # ["rgb", 255, 255, 255] # ["hex", 16777215] # ["start", "before", "after", "end"] 

为了比较,这是语法:

  • S元素 *
  • 元素颜色 |
  • colorrgb | hex
  • rgbrgb rgbvalues
  • rgbvalues令牌 令牌 令牌
  • hexhex hexvalue
  • hexvalue令牌
  • word令牌

因为您正在操作data

当你点击rgb时,循环中的下一个元素将是255 ,但是你要删除这些元素,所以现在它们between rgb所在的位置,所以下一个元素是hex

这样的事情对你来说可能更好:

 when 'rgb' then rgb = data.slice! i+1,3 when 'hex' then hex = data.slice! i+1,1 

这是一个更好的解决方案

 data = %w{ start before rgb 255 255 255 hex FFFFFF hex EEEEEE after end } rest, rgb, hex = [], [], [] until data.empty? case (key = data.shift).downcase when 'rgb' then rgb += [key] + data.shift(3) when 'hex' then hex += [key] + data.shift(1) else rest << key end end p rgb, hex, rest # >> ["rgb", "255", "255", "255"] # >> ["hex", "FFFFFF", "hex", "EEEEEE"] # >> ["start", "before", "after", "end"]