lib/io_streams/tabular/parser/fixed.rb in iostreams-1.2.1 vs lib/io_streams/tabular/parser/fixed.rb in iostreams-1.3.0
- old
+ new
@@ -1,68 +1,173 @@
module IOStreams
class Tabular
module Parser
# Parsing and rendering fixed length data
class Fixed < Base
- attr_reader :fixed_layout
+ attr_reader :layout, :truncate
# Returns [IOStreams::Tabular::Parser]
#
# Parameters:
# layout: [Array<Hash>]
# [
- # {key: 'name', size: 23 },
- # {key: 'address', size: 40 },
- # {key: 'zip', size: 5 }
+ # {size: 23, key: "name"},
+ # {size: 40, key: "address"},
+ # {size: 2},
+ # {size: 5, key: "zip"},
+ # {size: 8, key: "age", type: :integer},
+ # {size: 10, key: "weight", type: :float, decimals: 2}
# ]
- def initialize(layout:)
- @fixed_layout = parse_layout(layout)
+ #
+ # Notes:
+ # * Leave out the name of the key to ignore that column during parsing,
+ # and to space fill when rendering. For example as a filler.
+ #
+ # Types:
+ # :string
+ # This is the default type.
+ # Applies space padding and the value is left justified.
+ # Returns value as a String
+ # :integer
+ # Applies zero padding to the left.
+ # Returns value as an Integer
+ # Raises Errors::ValueTooLong when the supplied value cannot be rendered in `size` characters.
+ # :float
+ # Applies zero padding to the left.
+ # Returns value as a float.
+ # The :size is the total size of this field including the `.` and the decimals.
+ # Number of :decimals
+ # Raises Errors::ValueTooLong when the supplied value cannot be rendered in `size` characters.
+ def initialize(layout:, truncate: true)
+ @layout = Layout.new(layout)
+ @truncate = truncate
end
+ # The required line length for every fixed length line
+ def line_length
+ layout.length
+ end
+
# Returns [String] fixed layout values extracted from the supplied hash.
- # String will be encoded to `encoding`
+ #
+ # Notes:
+ # * A nil value is considered an empty string
+ # * When a supplied value exceeds the column size it is truncated.
def render(row, header)
hash = header.to_hash(row)
result = ""
- fixed_layout.each do |map|
- # A nil value is considered an empty string
- value = hash[map.key].to_s
- result << format("%-#{map.size}.#{map.size}s", value)
+ layout.columns.each do |column|
+ value = hash[column.key].to_s
+ if !truncate && (value.length > column.size)
+ raise(Errors::ValueTooLong, "Value: #{value.inspect} is too long to fit into column #{column.key} of size #{column.size}")
+ end
+
+ result << column.render(value)
end
result
end
# Returns [Hash<Symbol, String>] fixed layout values extracted from the supplied line.
# String will be encoded to `encoding`
def parse(line)
unless line.is_a?(String)
- raise(IOStreams::Errors::TypeMismatch, "Format is :fixed. Invalid parse input: #{line.class.name}")
+ raise(Errors::TypeMismatch, "Line must be a String when format is :fixed. Actual: #{line.class.name}")
end
+ if line.length != layout.length
+ raise(Errors::InvalidLineLength, "Expected line length: #{layout.length}, actual line length: #{line.length}")
+ end
+
hash = {}
index = 0
- fixed_layout.each do |map|
- value = line[index..(index + map.size - 1)]
- index += map.size
- hash[map.key] = value.to_s.strip
+ layout.columns.each do |column|
+ # Ignore "columns" that have no keys. E.g. Fillers
+ hash[column.key] = column.parse(line[index, column.size]) if column.key
+ index += column.size
end
hash
end
private
- FixedLayout = Struct.new(:key, :size)
+ class Layout
+ attr_reader :columns, :length
- # Returns [Array<FixedLayout>] the layout for this fixed width file.
- # Also validates values
- def parse_layout(layout)
- layout.collect do |map|
- size = map[:size]
- key = map[:key]
- raise(ArgumentError, "Missing required :key and :size in: #{map.inspect}") unless size && key
+ # Returns [Array<FixedLayout>] the layout for this fixed width file.
+ # Also validates values
+ def initialize(layout)
+ @length = 0
+ @columns = parse_layout(layout)
+ end
- FixedLayout.new(key, size)
+ private
+
+ def parse_layout(layout)
+ @length = 0
+ layout.collect do |hash|
+ raise(Errors::InvalidLayout, "Missing required :size in: #{hash.inspect}") unless hash.key?(:size)
+
+ column = Column.new(**hash)
+ @length += column.size
+ column
+ end
+ end
+ end
+
+ class Column
+ TYPES = %i[string integer float].freeze
+
+ attr_reader :key, :size, :type, :decimals
+
+ def initialize(key: nil, size:, type: :string, decimals: 2)
+ @key = key
+ @size = size.to_i
+ @type = type.to_sym
+ @decimals = decimals
+
+ raise(Errors::InvalidLayout, "Size #{size.inspect} must be positive") unless @size.positive?
+ raise(Errors::InvalidLayout, "Unknown type: #{type.inspect}") unless TYPES.include?(type)
+ end
+
+ def parse(value)
+ return if value.nil?
+
+ stripped_value = value.to_s.strip
+
+ case type
+ when :string
+ stripped_value
+ when :integer
+ stripped_value.length.zero? ? nil : value.to_i
+ when :float
+ stripped_value.length.zero? ? nil : value.to_f
+ else
+ raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
+ end
+ end
+
+ def render(value)
+ case type
+ when :string
+ format("%-#{size}.#{size}s", value.to_s)
+ when :integer
+ formatted = format("%0#{size}d", value.to_i)
+ if formatted.length > size
+ raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
+ end
+
+ formatted
+ when :float
+ formatted = format("%0#{size}.#{decimals}f", value.to_f)
+ if formatted.length > size
+ raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
+ end
+
+ formatted
+ else
+ raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
+ end
end
end
end
end
end