如何在parslet中定义固定宽度约束

时间:2011-04-04 14:55:11

标签: ruby parsing parslet

我正在研究parslet来编写很多数据导入代码。总体而言,图书馆看起来不错,但我正在努力做一件事。我们的输入文件很多都是固定的宽度,格式之间的宽度不同,即使实际的字段没有。例如,我们可能会得到一个9字符货币的文件,另一个有11个字符(或其他)的文件。有谁知道如何在parslet原子上定义固定宽度约束?

理想情况下,我希望能够定义一个理解货币的原子(带有可选的美元符号,千位分隔符等等)然后我可以在运行中创建一个基于的新原子除了它正好解析N个字符之外,它是完全等价的旧的。

这样的组合器是否存在于parslet中?如果没有,是否有可能/很难自己写一个?

3 个答案:

答案 0 :(得分:1)

解析器类中的方法基本上是用于parslet原子的生成器。这些方法最简单的形式是'规则',每次调用它们时返回相同原子的方法。创建自己的发电机同样容易,而不是那么简单的野兽。请查看http://kschiess.github.com/parslet/tricks.html以了解此技巧的说明(匹配字符串不区分大小写)。

在我看来,您的货币解析器是一个只有少量参数的解析器,您可以创建一个方法(def ... end),返回根据您的喜好定制的货币解析器。甚至可以使用initialize和constructor参数? (即:MoneyParser.new(4,5))

如需更多帮助,请将您的问题发送到邮件列表。如果用代码说明这些问题通常更容易回答。

答案 1 :(得分:1)

也许我的部分解决方案有助于澄清我在问题中的含义。

假设你有一个非常简单的解析器:

class MyParser < Parslet::Parser
    rule(:dollars) {
        match('[0-9]').repeat(1).as(:dollars)
    }
    rule(:comma_separated_dollars) {
        match('[0-9]').repeat(1, 3).as(:dollars) >> ( match(',') >> match('[0-9]').repeat(3, 3).as(:dollars) ).repeat(1)
    }
    rule(:cents) {
        match('[0-9]').repeat(2, 2).as(:cents)
    }
    rule(:currency) {
        (str('$') >> (comma_separated_dollars | dollars) >> str('.') >> cents).as(:currency)
        # order is important in (comma_separated_dollars | dollars)
    }
end

现在,如果我们要解析固定宽度的货币字符串;这不是最简单的事情。当然,你可以弄清楚如何用最终宽度来表达重复表达式,但它实际上变得非常棘手,特别是在逗号分隔的情况下。另外,在我的用例中,货币实际上只是一个例子。我希望能够轻松地为地址,邮政编码等提供固定宽度的定义....

这似乎是PEG应该可以处理的事情。我设法编写了一个原型版本,使用Lookahead作为模板:

class FixedWidth < Parslet::Atoms::Base
    attr_reader :bound_parslet
    attr_reader :width

    def initialize(width, bound_parslet) # :nodoc:
        super()

        @width = width
        @bound_parslet = bound_parslet
        @error_msgs = {
            :premature => "Premature end of input (expected #{width} characters)",
            :failed => "Failed fixed width",
        }
    end

    def try(source, context) # :nodoc:
        pos = source.pos
        teststring = source.read(width).to_s
        if (not teststring) || teststring.size != width
            return error(source, @error_msgs[:premature]) #if not teststring && teststring.size == width
        end
        fakesource = Parslet::Source.new(teststring)
        value = bound_parslet.apply(fakesource, context)
        return value if not value.error?

        source.pos = pos
        return error(source, @error_msgs[:failed])
    end

    def to_s_inner(prec) # :nodoc:
        "FIXED-WIDTH(#{width}, #{bound_parslet.to_s(prec)})"
    end

    def error_tree # :nodoc:
        Parslet::ErrorTree.new(self, bound_parslet.error_tree)
    end
end

# now we can easily define a fixed-width currency rule:
class SHPParser
    rule(:currency15) {
        FixedWidth.new(15, currency >> str(' ').repeat)
    }
end

当然,这是一个非常黑客的解决方案。除此之外,行数和错误消息在固定宽度约束内不好。我希望看到这个想法以更好的方式实施。

答案 2 :(得分:1)

这样的事情......

class MyParser < Parslet::Parser
    def initialize(widths)
        @widths = widths
        super
    end

    rule(:currency)  {...}
    rule(:fixed_c)   {currency.fixed(@widths[:currency])}


    rule(:fixed_str) {str("bob").fixed(4)}
end 

puts MyParser.new.fixed_str.parse("bob").inspect

这将失败:

"Expected 'bob' to be 4 long at line 1 char 1"

这是你如何做到的:

require 'parslet'

class Parslet::Atoms::FixedLength < Parslet::Atoms::Base  
  attr_reader :len, :parslet
  def initialize(parslet, len, tag=:length)
    super()

    raise ArgumentError, 
      "Asking for zero length of a parslet. (#{parslet.inspect} length #{len})" \
      if len == 0

    @parslet = parslet
    @len = len
    @tag = tag
    @error_msgs = {
      :lenrep  => "Expected #{parslet.inspect} to be #{len} long", 
      :unconsumed => "Extra input after last repetition"
    }
  end

  def try(source, context, consume_all)
    start_pos = source.pos

    success, value = parslet.apply(source, context, false)

    return succ(value) if success && value.str.length == @len

    context.err_at(
      self, 
      source, 
      @error_msgs[:lenrep], 
      start_pos, 
      [value]) 
  end

  precedence REPETITION
  def to_s_inner(prec)
    parslet.to_s(prec) + "{len:#{@len}}"
  end
end

module Parslet::Atoms::DSL
  def fixed(len)
    Parslet::Atoms::FixedLength.new(self, len)
  end
end