lib/io_streams/tabular/parser/fixed.rb in iostreams-1.4.0 vs lib/io_streams/tabular/parser/fixed.rb in iostreams-1.5.0
- old
+ new
@@ -35,10 +35,26 @@
# 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.
+ #
+ # In some circumstances the length of the last column is variable.
+ # layout: [Array<Hash>]
+ # [
+ # {size: 23, key: "name"},
+ # {size: :remainder, key: "rest"}
+ # ]
+ # By setting a size of `:remainder` it will take the rest of the line as the value for that column.
+ #
+ # A size of `:remainder` and no `:key` will discard the remainder of the line without validating the length.
+ # layout: [Array<Hash>]
+ # [
+ # {size: 23, key: "name"},
+ # {size: :remainder}
+ # ]
+ #
def initialize(layout:, truncate: true)
@layout = Layout.new(layout)
@truncate = truncate
end
@@ -55,16 +71,11 @@
def render(row, header)
hash = header.to_hash(row)
result = ""
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)
+ result << column.render(hash[column.key], truncate)
end
result
end
# Returns [Hash<Symbol, String>] fixed layout values extracted from the supplied line.
@@ -72,17 +83,22 @@
def parse(line)
unless line.is_a?(String)
raise(Errors::TypeMismatch, "Line must be a String when format is :fixed. Actual: #{line.class.name}")
end
- if line.length != layout.length
+ if layout.length.positive? && (line.length != layout.length)
raise(Errors::InvalidLineLength, "Expected line length: #{layout.length}, actual line length: #{line.length}")
end
hash = {}
index = 0
layout.columns.each do |column|
+ if column.size == -1
+ hash[column.key] = column.parse(line[index..-1]) if column.key
+ break
+ end
+
# 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
@@ -91,12 +107,10 @@
# The header is required as an argument and cannot be supplied in the file itself.
def requires_header?
false
end
- private
-
class Layout
attr_reader :columns, :length
# Returns [Array<FixedLayout>] the layout for this fixed width file.
# Also validates values
@@ -111,11 +125,19 @@
@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
+ if column.size == -1
+ if @length == -1
+ raise(Errors::InvalidLayout, "Only the last :size can be '-1' or :remainder in: #{hash.inspect}")
+ end
+
+ @length = -1
+ else
+ @length += column.size
+ end
column
end
end
end
@@ -124,15 +146,17 @@
attr_reader :key, :size, :type, :decimals
def initialize(key: nil, size:, type: :string, decimals: 2)
@key = key
- @size = size.to_i
+ @size = size == :remainder ? -1 : size.to_i
@type = type.to_sym
@decimals = decimals
- raise(Errors::InvalidLayout, "Size #{size.inspect} must be positive") unless @size.positive?
+ unless @size.positive? || (@size == -1)
+ raise(Errors::InvalidLayout, "Size #{size.inspect} must be positive or :remainder")
+ end
raise(Errors::InvalidLayout, "Unknown type: #{type.inspect}") unless TYPES.include?(type)
end
def parse(value)
return if value.nil?
@@ -149,30 +173,36 @@
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
+ def render(value, truncate)
+ formatted =
+ case type
+ when :string
+ value = value.to_s
+ return value if size == -1
- 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}")
+ format(truncate ? "%-#{size}.#{size}s" : "%-#{size}s", value)
+ when :integer
+ return value.to_i.to_s if size == -1
+
+ truncate = false
+ format("%0#{size}d", value.to_i)
+ when :float
+ return value.to_f.to_s if size == -1
+
+ truncate = false
+ format("%0#{size}.#{decimals}f", value.to_f)
+ else
+ raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
end
- formatted
- else
- raise(Errors::InvalidLayout, "Unsupported type: #{type.inspect}")
+ if !truncate && formatted.length > size
+ raise(Errors::ValueTooLong, "Value: #{value} is too large to fit into column:#{key} of size:#{size}")
end
+
+ formatted
end
end
end
end
end