Class: Philiprehberger::CsvBuilder::Builder

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/csv_builder/builder.rb

Overview

DSL builder for constructing CSV output from records

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(records, delimiter: ',', quote_char: '"', row_sep: "\n", bom: false, encoding: 'UTF-8', empty_value: '') ⇒ Builder

Returns a new instance of Builder.

Parameters:

  • records (Array)

    the source records

  • delimiter (String) (defaults to: ',')

    the column separator (default: “,”)

  • quote_char (String) (defaults to: '"')

    the quote character (default: ‘“’)

  • row_sep (String) (defaults to: "\n")

    the line separator (default: “n”)

  • bom (Boolean) (defaults to: false)

    prepend UTF-8 BOM (default: false)

  • encoding (String) (defaults to: 'UTF-8')

    output encoding name (default: “UTF-8”)

  • empty_value (String) (defaults to: '')

    placeholder for nil/empty values (default: “”)



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/philiprehberger/csv_builder/builder.rb', line 23

def initialize(records, delimiter: ',', quote_char: '"', row_sep: "\n",
               bom: false, encoding: 'UTF-8', empty_value: '')
  @records = records
  @columns = []
  @filters = []
  @validations = []
  @row_number_header = nil
  @delimiter = delimiter
  @quote_char = quote_char
  @row_sep = row_sep
  @sort_by = nil
  @sort_direction = :asc
  @limit_count = nil
  @offset_count = nil
  @footer_block = nil
  @header_transform = nil
  @bom = bom
  @encoding = encoding
  @empty_value = empty_value
end

Instance Attribute Details

#columnsArray<Column> (readonly)

Returns the defined columns.

Returns:

  • (Array<Column>)

    the defined columns



11
12
13
# File 'lib/philiprehberger/csv_builder/builder.rb', line 11

def columns
  @columns
end

#recordsArray (readonly)

Returns the source records.

Returns:

  • (Array)

    the source records



14
15
16
# File 'lib/philiprehberger/csv_builder/builder.rb', line 14

def records
  @records
end

Instance Method Details

#append_to(path) ⇒ void

This method returns an undefined value.

Append data rows (no header, no BOM) to an existing CSV file.

Parameters:

  • path (String)

    the output file path



268
269
270
# File 'lib/philiprehberger/csv_builder/builder.rb', line 268

def append_to(path)
  write_to(path, mode: 'ab')
end

#column(name, header: nil) {|record| ... } ⇒ self

Define a column

Parameters:

  • name (Symbol, String)

    the column name

  • header (String, nil) (defaults to: nil)

    optional custom header label

Yields:

  • (record)

    optional block to transform the value

Yield Parameters:

  • record (Object)

    the source record

Returns:

  • (self)


136
137
138
139
# File 'lib/philiprehberger/csv_builder/builder.rb', line 136

def column(name, header: nil, &block)
  @columns << Column.new(name, header: header, &block)
  self
end

#column_statsHash{Symbol => Hash}

Per-column statistics across filtered records.

Returns:

  • (Hash{Symbol => Hash})

    column name mapped to stats hash with keys :count, :unique, :nil_count, :sample



181
182
183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/philiprehberger/csv_builder/builder.rb', line 181

def column_stats
  recs = filtered_records
  @columns.to_h do |col|
    values = recs.map { |r| col.extract(r, empty_value: @empty_value) }
    nil_count = values.count { |v| v.nil? || v == @empty_value }
    unique_values = values.reject { |v| v.nil? || v == @empty_value }.uniq
    [col.name, {
      count: values.size,
      unique: unique_values.size,
      nil_count: nil_count,
      sample: unique_values.first(3)
    }]
  end
end

#filter {|record| ... } ⇒ self

Add a filter to exclude records

Yields:

  • (record)

    block that returns true to include the record

Yield Parameters:

  • record (Object)

    the source record

Returns:

  • (self)


146
147
148
149
# File 'lib/philiprehberger/csv_builder/builder.rb', line 146

def filter(&block)
  @filters << block
  self
end

#filtered_recordsArray

Return the filtered records

Returns:

  • (Array)


199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/philiprehberger/csv_builder/builder.rb', line 199

def filtered_records
  result = @records
  @filters.each do |f|
    result = result.select(&f)
  end
  if @sort_by
    result = result.sort_by(&@sort_by)
    result = result.reverse if @sort_direction == :desc
  end
  result = result.drop(@offset_count) if @offset_count
  result = result.first(@limit_count) if @limit_count
  result
end

Append a computed footer row after all data rows

Yields:

  • (Array)

    filtered records

Yield Returns:

  • (Array)

    footer row values

Returns:

  • (self)


84
85
86
87
# File 'lib/philiprehberger/csv_builder/builder.rb', line 84

def footer(&block)
  @footer_block = block
  self
end

#headersArray<String>

Return the header row

Returns:

  • (Array<String>)


163
164
165
166
167
# File 'lib/philiprehberger/csv_builder/builder.rb', line 163

def headers
  base = @columns.map(&:header)
  base = base.map { |h| @header_transform.call(h) } if @header_transform
  @row_number_header ? [@row_number_header] + base : base
end

#limit(n) ⇒ self

Limit the number of output rows

Parameters:

  • n (Integer)

    maximum rows

Returns:

  • (self)


65
66
67
68
# File 'lib/philiprehberger/csv_builder/builder.rb', line 65

def limit(n)
  @limit_count = n
  self
end

#offset(n) ⇒ self

Skip the first N filtered/sorted records

Parameters:

  • n (Integer)

    number of rows to skip

Returns:

  • (self)


74
75
76
77
# File 'lib/philiprehberger/csv_builder/builder.rb', line 74

def offset(n)
  @offset_count = n
  self
end

#row_countInteger

Number of data rows the builder will emit (headers and footer excluded). Applies all configured filters, sorts, offsets, and limits.

Returns:

  • (Integer)


173
174
175
# File 'lib/philiprehberger/csv_builder/builder.rb', line 173

def row_count
  filtered_records.size
end

#row_number(header: '#') ⇒ self

Add an auto-incrementing row number as the first column

Parameters:

  • header (String) (defaults to: '#')

    the header label for the row number column

Returns:

  • (self)


155
156
157
158
# File 'lib/philiprehberger/csv_builder/builder.rb', line 155

def row_number(header: '#')
  @row_number_header = header
  self
end

#sort_by(direction: :asc) {|record| ... } ⇒ self

Sort records before CSV output

Parameters:

  • direction (Symbol) (defaults to: :asc)

    :asc (default) or :desc

Yields:

  • (record)

    block returning the sort key

Yield Parameters:

  • record (Object)

    the source record

Returns:

  • (self)

Raises:

  • (Error)

    if direction is not :asc or :desc



51
52
53
54
55
56
57
58
59
# File 'lib/philiprehberger/csv_builder/builder.rb', line 51

def sort_by(direction: :asc, &block)
  raise Error, 'A block is required for sort_by' unless block
  raise Error, "direction must be :asc or :desc (got #{direction.inspect})" unless %i[asc
                                                                                      desc].include?(direction)

  @sort_by = block
  @sort_direction = direction
  self
end

#to_aArray<Array>

Return the CSV as an array of row arrays (headers + data + footer).

Returns:

  • (Array<Array>)

Raises:



293
294
295
296
297
298
299
300
# File 'lib/philiprehberger/csv_builder/builder.rb', line 293

def to_a
  recs = filtered_records
  validate_rows!(recs) unless @validations.empty?
  rows = [headers]
  recs.each_with_index { |record, index| rows << build_row(record, index) }
  rows << @footer_block.call(recs) if @footer_block
  rows
end

#to_csvString

Generate the CSV as a string

Returns:

  • (String)

Raises:



217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/philiprehberger/csv_builder/builder.rb', line 217

def to_csv
  recs = filtered_records
  validate_rows!(recs) unless @validations.empty?
  csv_string = CSV.generate(**csv_options) do |csv|
    csv << headers
    recs.each_with_index do |record, index|
      csv << build_row(record, index)
    end
    csv << @footer_block.call(recs) if @footer_block
  end
  csv_string = csv_string.encode(@encoding) unless @encoding == 'UTF-8'
  @bom ? "\xEF\xBB\xBF#{csv_string}" : csv_string
end

#to_file(path) ⇒ void

This method returns an undefined value.

Write the CSV to a file

Parameters:

  • path (String)

    the output file path



242
243
244
# File 'lib/philiprehberger/csv_builder/builder.rb', line 242

def to_file(path)
  File.binwrite(path, to_csv)
end

#to_io(io) ⇒ void

This method returns an undefined value.

Stream CSV to any IO object

Parameters:

  • io (IO, StringIO)

    the IO object to write to

Raises:



277
278
279
280
281
282
283
284
285
286
287
# File 'lib/philiprehberger/csv_builder/builder.rb', line 277

def to_io(io)
  io.write("\xEF\xBB\xBF") if @bom
  recs = filtered_records
  validate_rows!(recs) unless @validations.empty?
  csv = CSV.new(io, **csv_options)
  csv << headers
  recs.each_with_index do |record, index|
    csv << build_row(record, index)
  end
  csv << @footer_block.call(recs) if @footer_block
end

#to_sString

Alias for #to_csv so instances behave nicely with string interpolation.

Returns:

  • (String)


234
235
236
# File 'lib/philiprehberger/csv_builder/builder.rb', line 234

def to_s
  to_csv
end

#total(column_name) {|values| ... } ⇒ self

Shorthand for adding a footer row with a computed total for the named column

Parameters:

  • column_name (Symbol, String)

    the column to total

Yields:

  • (values)

    optional block to compute the total (receives array of numeric values)

Returns:

  • (self)


114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/philiprehberger/csv_builder/builder.rb', line 114

def total(column_name, &block)
  col_name = column_name.to_sym
  @footer_block = lambda do |recs|
    columns.map do |col|
      if col.name == col_name
        values = recs.map { |r| col.extract(r, empty_value: @empty_value).to_f }
        block ? block.call(values) : values.sum
      else
        ''
      end
    end
  end
  self
end

#transform_header {|name| ... } ⇒ self

Register a proc applied to all column headers during rendering

Yields:

  • (name)

    block that transforms a header name

Yield Parameters:

  • name (String)

    the original header label

Returns:

  • (self)


104
105
106
107
# File 'lib/philiprehberger/csv_builder/builder.rb', line 104

def transform_header(&block)
  @header_transform = block
  self
end

#validate {|row| ... } ⇒ self

Register a validation block for rows

Yields:

  • (row)

    block that validates the row hash

Yield Parameters:

  • row (Hash)

    column-name to value mapping

Returns:

  • (self)


94
95
96
97
# File 'lib/philiprehberger/csv_builder/builder.rb', line 94

def validate(&block)
  @validations << block
  self
end

#write_to(path, mode: 'wb') ⇒ void

This method returns an undefined value.

Write the CSV to a file with an explicit mode. Useful for appending to existing files or combining multiple builders into one file.

When appending (‘mode: ’ab’‘ / `’a’‘), the header row and BOM from subsequent writes are suppressed so the file keeps a single header.

Parameters:

  • path (String)

    the output file path

  • mode (String) (defaults to: 'wb')

    file open mode (default: “wb”)



255
256
257
258
259
260
261
262
# File 'lib/philiprehberger/csv_builder/builder.rb', line 255

def write_to(path, mode: 'wb')
  appending = mode.start_with?('a')
  if appending
    File.open(path, mode) { |f| write_body_rows(f) }
  else
    to_file(path)
  end
end