Как определить ограничение фиксированной ширины в parslet

Я изучаю parslet, чтобы написать много кода для импорта данных. В целом, библиотека выглядит неплохо, но у меня есть одна проблема. Многие наши входные файлы имеют фиксированную ширину, и ширина различается в зависимости от формата, даже если фактическое поле не имеет такой ширины. Например, мы можем получить файл с 9-символьной валютой и другой с 11-символьной (или любой другой). Кто-нибудь знает, как определить ограничение фиксированной ширины для атома парслета?

В идеале я хотел бы иметь возможность определить атом, который понимает валюту (с необязательными знаками доллара, разделителями тысяч и т. д.). И тогда я мог бы на лету создать новый атом на основе старого. это точно эквивалентно, за исключением того, что он анализирует ровно N символов.

Существует ли такой комбинатор в parslet? Если нет, было бы возможно/трудно написать его самому?


person Jonathan    schedule 04.04.2011    source источник
comment
+1 за отсутствие вызова Ктулху с помощью регулярного выражения.   -  person Andrew Grimm    schedule 14.04.2011


Ответы (3)


А как насчет такого...

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
person Nigel Thorne    schedule 12.02.2014
comment
precedence и to_s_inner, вероятно, нуждаются в исправлении. - person Nigel Thorne; 12.02.2014

Методы в классах синтаксических анализаторов в основном являются генераторами атомов парсеров. Простейшая форма, в которую эти методы входят, - это «правила», методы, которые просто возвращают одни и те же атомы каждый раз, когда они вызываются. Так же легко создавать свои собственные генераторы, которые не являются такими простыми зверями. Пожалуйста, посмотрите http://kschiess.github.com/parslet/tricks.html для иллюстрация этого трюка (сопоставление строк без учета регистра).

Мне кажется, что ваш синтаксический анализатор валют — это синтаксический анализатор с несколькими параметрами, и что вы, вероятно, могли бы создать метод (def ... end), который возвращает парсеры валюты, адаптированные к вашим предпочтениям. Может быть, даже использовать аргументы инициализации и конструктора? (т.е.: MoneyParser.new(4,5))

Для получения дополнительной помощи, пожалуйста, обращайтесь со своими вопросами в список рассылки. На такие вопросы часто легче ответить, если вы проиллюстрируете это кодом.

person kaspar    schedule 08.04.2011
comment
Спасибо за ответ! Я знаю, что вы можете создавать параметризованные правила для конкретных задач. Однако это не совсем то решение, на которое я надеялся. Я хочу что-то, где вы можете определить атом, который не имеет понятия фиксированной ширины, а затем добавить ограничение позже, не изменяя исходный атом. - person Jonathan; 13.04.2011

Возможно, мое частичное решение поможет прояснить, что я имел в виду в вопросе.

Допустим, у вас есть несколько нетривиальный парсер:

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

Конечно, это довольно взломанное решение. Помимо прочего, номера строк и сообщения об ошибках не подходят для ограничения фиксированной ширины. Хотелось бы, чтобы эта идея была реализована более качественно.

person Jonathan    schedule 13.04.2011