Class: Tabledata::Table

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/tabledata/table.rb

Overview

Table represents tabular data and provides various ways to create one, read from it and represent it in a different format.

Direct Known Subclasses

CustomTable

Constant Summary collapse

ValidOptions =

A list of options which are valid to be passed to some of the constructors.

[:has_headers, :has_footer, :file_type, :name, :table_class, :table_classes, :accessors, :data, :header, :body, :footer]
InvalidFromFileOptions =

Options which are invalid to be passed to Tabledata::Table.from_file.

[:data, :header, :body, :footer]
DefaultTableName =

The default name for unnamed tables.

'Unnamed Table'
DefaultAccessors =

The default accessor list.

[]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options = nil) ⇒ Table

Create a new table.

Parameters:

  • options (Hash) (defaults to: nil)

    A list of options. Mostly identical to #initialize's options hash, but with the additional options :file_type and :table_class.

Options Hash (options):

  • :name (String)

    The name of the table

  • :accessors (Array<Symbol>, Hash<Symbol => Integer>, nil)

    A list of accessors for the columns. Allows accessing columns by that accessor.

  • :data (Symbol)

    An array of arrays with the table data. Mutually exclusive with :header, :body and :footer.

  • :header (Symbol)

    An array with the header values. To be used together with :body and :footer.
    Mutually exclusive with :data.
    Automatically sets :has_headers to true.

  • :body (Symbol)

    An array with the header values. To be used together with :header and :footer.
    Mutually exclusive with :data.
    Automatically sets :has_headers to false if :header is not also present.
    Automatically sets :has_footer to false if :footer is not also present.

  • :footer (Symbol)

    An array with the header values. To be used together with :header and :body.
    Mutually exclusive with :data.
    Automatically sets :has_footer to true.

  • :has_headers (true, false)

    Whether the table has a header, defaults to true

  • :has_footer (true, false)

    Whether the table has a footer, defaults to false

Raises:

  • (ArgumentError)

153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
# File 'lib/tabledata/table.rb', line 153

def initialize(options=nil)
  options           = options ? options.dup : {}
  raise ArgumentError, "Invalid options: #{(options.keys-ValidOptions).inspect[1..-2]}" unless (options.keys-ValidOptions).empty?

  if options.has_key?(:data)
    raise "Must not mix :data with :header, :body or :footer" if options.has_key?(:header) || options.has_key?(:body) || options.has_key?(:footer)
    data = options.delete(:data)
  else
    data = []
    if options.has_key?(:header)
      data << options.delete(:header)
      options[:has_headers] = true
    elsif !options.has_key?(:has_headers)
      options[:has_headers] = !options.has_key?(:body)
    end
    data.concat(options.delete(:body)) if options.has_key?(:body)
    if options.has_key?(:footer)
      data << options.delete(:footer)
      options[:has_footer] = true
    end
  end

  column_count      = data.first ? data.first.size : 0
  @name             = options.delete(:name) || DefaultTableName
  @has_headers      = options.fetch(:has_headers, true) ? true : false
  @has_footer       = options.fetch(:has_footer, false) ? true : false
  @data             = data
  @header_columns   = nil                        # used for cell access by header name, e.g. table[0]["Some Cellname"]
  self.accessors    = options.delete(:accessors) # used for cell access by accessor, e.g. table[0][:some_cell_accessor]
  @rows             = data.map.with_index { |row, index|
    raise InvalidColumnCount.new(index, row.size, column_count) if index > 0 && row.size != column_count
    raise ArgumentError, "Row must be provided as Array, but got #{row.class} in row #{index}" unless row.is_a?(Array)

    Row.new(self, index, row)
  }
end

Instance Attribute Details

#accessor_columnsHash<Symbol => Integer> (readonly)

Returns A hash mapping column accessor names to the column index.

Returns:

  • (Hash<Symbol => Integer>)

    A hash mapping column accessor names to the column index


110
111
112
# File 'lib/tabledata/table.rb', line 110

def accessor_columns
  @accessor_columns
end

#accessorsArray<Symbol>

Returns An array of all named accessors.

Returns:

  • (Array<Symbol>)

    An array of all named accessors


107
108
109
# File 'lib/tabledata/table.rb', line 107

def accessors
  @accessors
end

#column_accessorsHash<Integer => Symbol> (readonly)

Returns A hash mapping column index to the column accessor names.

Returns:

  • (Hash<Integer => Symbol>)

    A hash mapping column index to the column accessor names


113
114
115
# File 'lib/tabledata/table.rb', line 113

def column_accessors
  @column_accessors
end

#nameString? (readonly)

Returns The name of the table.

Returns:

  • (String, nil)

    The name of the table


120
121
122
# File 'lib/tabledata/table.rb', line 120

def name
  @name
end

Class Method Details

.from_data(data, options = nil) ⇒ Object

Create a table from a datastructure.

Parameters:

  • data (Array<Array>)

    An array of arrays, representing rows -> cells.

  • options (Hash) (defaults to: nil)

    A list of options. Many are identical to #initialize's options hash, but with the additional options :file_type and :table_class. The :data option is used by from_file and thus overridden and can't be used. For the same reason, :header, :footer and :body can't be used.

Options Hash (options):

  • :file_type (Symbol)

    The file type. Nil for auto-detection (which uses the extension of the filename), or one of :csv, :xls or :xlsx

  • :table_class (Symbol)

    The class to use for this table. Defaults to self (Tabledata::Table)

  • :name (String)

    The name of the table

  • :accessors (Array<Symbol>, Hash<Symbol => Integer>, nil)

    A list of accessors for the columns. Allows accessing columns by that accessor.

  • :has_headers (true, false)

    Whether the table has a header, defaults to true

  • :has_footer (true, false)

    Whether the table has a footer, defaults to false


102
103
104
# File 'lib/tabledata/table.rb', line 102

def self.from_data(data, options=nil)
  new(options ? options.merge(data: data) : {data: data})
end

.from_file(path, options = nil) ⇒ Tabledata::Table

Create a table from a file.
See Tabledata docs for a list of supported file types.

Parameters:

  • path (String)

    The path to a file in one of the supported formats.

  • options (Hash) (defaults to: nil)

    A list of options. Many are identical to #initialize's options hash, but with the additional options :file_type and :table_class. The :data option is used by from_file and thus overridden and can't be used. For the same reason, :header, :footer and :body can't be used.

Options Hash (options):

  • :file_type (Symbol)

    The file type. Nil for auto-detection (which uses the extension of the filename), or one of :csv, :xls or :xlsx

  • :name (String)

    The name of the table. Defaults to the basename of the file without the suffix.

  • :accessors (Array<Symbol>, Hash<Symbol => Integer>, nil)

    A list of accessors for the columns. Allows accessing columns by that accessor.

  • :has_headers (true, false)

    Whether the table has a header, defaults to true

  • :has_footer (true, false)

    Whether the table has a footer, defaults to false

Returns:


57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/tabledata/table.rb', line 57

def self.from_file(path, options=nil)
  options ||= {}

  unless (options.keys & InvalidFromFileOptions).empty?
    raise ArgumentError, "Must not pass #{(options.keys & InvalidFromFileOptions).inspect[1..-2]}"
  end

  options[:table_class] ||= self
  options[:file_type]   ||= Detection.file_type_from_path(path)
  options[:name]        ||= File.basename(path).sub(/\.(?:csv|xlsx?)\z/, '')

  case options[:file_type]
    when :csv then Parser.parse_csv(path, options)
    when :xls then Parser.table_from_xls(path, options)
    when :xlsx then Parser.table_from_xlsx(path, options)
    else raise InvalidFileType, "Unknown file format #{options[:file_type].inspect}"
  end
end

Instance Method Details

#<<(row) ⇒ self

Append a row to the table.

Parameters:

  • row (Array, #to_ary)

    The row to append to the table

Returns:

  • (self)

Raises:


406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# File 'lib/tabledata/table.rb', line 406

def <<(row)
  index  = @data.size
  begin
    row = row.to_ary
  rescue NoMethodError
    raise ArgumentError, "Row must be provided as Array or respond to `to_ary`, but got #{row.class} in row #{index}" unless row.respond_to?(:to_ary)
    raise
  end
  raise InvalidColumnCount.new(index, row.size, column_count) if @data.first && row.size != @data.first.size

  @data << row
  @rows << Row.new(self, index, row)

  self
end

#==(other) ⇒ true, false

Returns Whether this table has the same content as other. Uses the #data method to get the contents. If other has no #data method, the comparison is false.

Returns:

  • (true, false)

    Whether this table has the same content as other. Uses the #data method to get the contents. If other has no #data method, the comparison is false.


488
489
490
491
492
# File 'lib/tabledata/table.rb', line 488

def ==(other)
  @data == other.data
rescue NoMethodError
  false
end

#[](*args) ⇒ Array<Tabledata::Row>

Array#[] like access to the rows in the body (excluding headers and footer) of the table.

Returns:


243
244
245
# File 'lib/tabledata/table.rb', line 243

def [](*args)
  body[*args]
end

#accessors?true, false

Returns Whether accessors have been defined for this table.

Returns:

  • (true, false)

    Whether accessors have been defined for this table


358
359
360
# File 'lib/tabledata/table.rb', line 358

def accessors?
  !@accessors.empty?
end

#accessors_from_headers!Object

Note:

The actual transformation algorithm might change in the future.

Automatically create accessors from the headers of the table. It does that by downcasing the headers, replace everything which is not in [a-z0-9_] with an _, replace all repeated occurrences of _ with a single _.


196
197
198
199
200
# File 'lib/tabledata/table.rb', line 196

def accessors_from_headers!
  raise "Can't define accessors from headers in a table without headers" unless @has_headers

  self.accessors = headers.map { |val| (val && !val.empty?) ? val.to_s.downcase.tr('^a-z0-9_', '_').squeeze('_').gsub(/\A_|_\z/, '').to_sym : nil }
end

#bodyArray<Tabledata::Row>

Returns All rows except header and footer.

Returns:


390
391
392
393
394
395
396
397
398
# File 'lib/tabledata/table.rb', line 390

def body
  end_offset = footer? ? -2 : -1

  if headers?
    @rows.empty? ? [] : @rows[1..end_offset]
  else
    @rows[0..end_offset]
  end
end

#column(index) ⇒ Tabledata::Column

Returns The column at the given index, accessor or name.

Returns:


324
325
326
# File 'lib/tabledata/table.rb', line 324

def column(index)
  Column.new(self, index_for_column(index))
end

#column_accessor(index) ⇒ Symbol?

Returns The accessor defined for the given column index.

Returns:

  • (Symbol, nil)

    The accessor defined for the given column index.


301
302
303
# File 'lib/tabledata/table.rb', line 301

def column_accessor(index)
  @column_accessors[index]
end

#column_countInteger?

Returns The number of columns. Nil if no rows are present.

Returns:

  • (Integer, nil)

    The number of columns. Nil if no rows are present.


235
236
237
# File 'lib/tabledata/table.rb', line 235

def column_count
  @data.first ? @data.first.size : nil
end

#column_name(index) ⇒ String? Also known as: column_header

The name defined for the given column index.
The name is determined by the column's header. Returns nil if the table has no headers.

Returns:

  • (String, nil)

    The name defined for the given column index.
    The name is determined by the column's header. Returns nil if the table has no headers.


309
310
311
312
313
# File 'lib/tabledata/table.rb', line 309

def column_name(index)
  h = headers

  h && h.at(index)
end

#columnsArray<Tabledata::Column>

Returns An array with the columns of this table.

Returns:


318
319
320
# File 'lib/tabledata/table.rb', line 318

def columns
  Array.new(column_count) { |col| column(col) }
end

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

Iterate over all rows in the body

Yields:

Yield Parameters:

Returns:

  • (self)

See Also:


430
431
432
433
434
435
436
# File 'lib/tabledata/table.rb', line 430

def each(&block)
  return enum_for(__method__) unless block

  body.each(&block)

  self
end

#each_column {|column| ... } ⇒ self

Iterate over all columns

Yields:

Yield Parameters:

Returns:

  • (self)

460
461
462
463
464
465
466
467
468
# File 'lib/tabledata/table.rb', line 460

def each_column
  return enum_for(__method__) unless block_given?

  column_count.times do |i|
    yield column(i)
  end

  self
end

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

Iterate over all rows, header and body

Yields:

Yield Parameters:

Returns:

  • (self)

See Also:


446
447
448
449
450
451
452
# File 'lib/tabledata/table.rb', line 446

def each_row(&block)
  return enum_for(__method__) unless block

  @data.each(&block)

  self
end

#eql?(other) ⇒ true, false

Returns Whether this table has the same content and properties (name, has_headers, has_footer, accessors) as another table.

Returns:

  • (true, false)

    Whether this table has the same content and properties (name, has_headers, has_footer, accessors) as another table


497
498
499
500
501
502
503
504
505
506
# File 'lib/tabledata/table.rb', line 497

def eql?(other)
  (
    other.is_a?(Tabledata::Table) &&
    other.name      == @name &&
    other.headers?  == @has_headers &&
    other.footer?   == @has_footer &&
    other.accessors == @accessors &&
    other.data      == @data
  )
end

#fetch_cell(row, column, *default_value, &default_block) ⇒ Object

The cell value at the given row and column number (zero based). Includes headers and footer. Returns the given default value or invokes the default block if the desired cell does not exist.

Returns:

  • (Object)

    The cell value at the given row and column number (zero based). Includes headers and footer. Returns the given default value or invokes the default block if the desired cell does not exist.

Raises:

  • (KeyError)

    If the cell was not found and neither a default value nor a default block were given.


283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
# File 'lib/tabledata/table.rb', line 283

def fetch_cell(row, column, *default_value, &default_block)
  raise ArgumentError, "Must only provide at max one default value or one default block" if default_value.size > (block_given? ? 0 : 1)

  row_data = row(row)

  if row_data
    row_data.fetch(column, *default_value, &default_block)
  elsif block_given?
    yield(self, row, column)
  elsif default_value.empty?
    raise IndexError, "Row not found: #{row.inspect}, #{column.inspect}"
  else
    default_value.first
  end
end

#fetch_row(row, *default) ⇒ Tabledata::Row

The row at the given row number (zero based). Includes headers and footer. Returns the given default value or invokes the default block if the desired row does not exist.

Returns:

  • (Tabledata::Row)

    The row at the given row number (zero based). Includes headers and footer. Returns the given default value or invokes the default block if the desired row does not exist.

Raises:

  • (KeyError)

    If the row was not found and neither a default value nor a default block were given.


254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
# File 'lib/tabledata/table.rb', line 254

def fetch_row(row, *default)
  raise ArgumentError, "Must only provide at max one default value or one default block" if default.size > (block_given? ? 0 : 1)

  row_data = row(row)

  if row_data
    row_data
  elsif block_given?
    yield(self, row)
  elsif default.empty?
    raise KeyError, "Row not found: #{row.inspect}"
  else
    default.first
  end
end

Returns The header row, if the table has a footer and the footer row is present. Nil otherwise.

Returns:

  • (Tabledata::Row, nil)

    The header row, if the table has a footer and the footer row is present. Nil otherwise.


384
385
386
# File 'lib/tabledata/table.rb', line 384

def footer
  !footer? || (headers? && @rows.size < 2) ? nil : @rows.last
end

#footer?true, false

Returns Whether this table has a footer.

Returns:

  • (true, false)

    Whether this table has a footer


377
378
379
# File 'lib/tabledata/table.rb', line 377

def footer?
  @has_footer
end

#format(format_id, options = nil) ⇒ Tabledata::Presenter


514
515
516
# File 'lib/tabledata/table.rb', line 514

def format(format_id, options=nil)
  Presenter.present(self, format_id, options)
end

#hash(other) ⇒ Object

See Object#hash


509
510
511
# File 'lib/tabledata/table.rb', line 509

def hash(other)
  [Tabledata::Table, @name, @has_headers, @has_footer, @accessors, @data].hash
end

#headersTabledata::Row?

Returns The header row, if the table has headers and the header row is present. Nil otherwise.

Returns:

  • (Tabledata::Row, nil)

    The header row, if the table has headers and the header row is present. Nil otherwise.


371
372
373
# File 'lib/tabledata/table.rb', line 371

def headers
  headers? ? @rows.first : nil
end

#headers?true, false

Returns Whether this table has headers.

Returns:

  • (true, false)

    Whether this table has headers


364
365
366
# File 'lib/tabledata/table.rb', line 364

def headers?
  @has_headers
end

#index_for_accessor(name) ⇒ Integer?

Returns The index for the given column-accessor.

Returns:

  • (Integer, nil)

    The index for the given column-accessor.


341
342
343
# File 'lib/tabledata/table.rb', line 341

def index_for_accessor(name)
  @accessor_columns[name.to_sym]
end

#index_for_column(column) ⇒ Integer?

Returns The index for the given column-index, -accessor or -name.

Returns:

  • (Integer, nil)

    The index for the given column-index, -accessor or -name.


330
331
332
333
334
335
336
337
# File 'lib/tabledata/table.rb', line 330

def index_for_column(column)
  case column
    when Integer then column
    when Symbol  then index_for_accessor(column)
    when String  then index_for_header(column)
    else raise InvalidColumnSpecifier, "Invalid index type, expected Symbol, String or Integer, but got #{column.class}"
  end
end

#index_for_header(name) ⇒ Integer?

Returns The index for the given column-name.

Returns:

  • (Integer, nil)

    The index for the given column-name.


347
348
349
350
351
352
353
354
# File 'lib/tabledata/table.rb', line 347

def index_for_header(name)
  if @has_headers && @data.first then
    @header_columns ||= Hash[@data.first.each_with_index.to_a]
    @header_columns[name]
  else
    nil
  end
end

#inspectObject

See Object#inspect


519
520
521
522
523
524
525
526
# File 'lib/tabledata/table.rb', line 519

def inspect
  sprintf "#<%s headers: %p, footer: %p, cols: %s, rows: %d>",
    self.class,
    headers?,
    footer?,
    column_count || '-',
    size
end

#row(row) ⇒ Tabledata::Row

Returns The row at the given row number (zero based). Includes headers and footer.

Returns:

  • (Tabledata::Row)

    The row at the given row number (zero based). Includes headers and footer.


272
273
274
# File 'lib/tabledata/table.rb', line 272

def row(row)
  @rows[row]
end

#sizeInteger Also known as: length

Returns The number of rows, excluding headers and footer.

Returns:

  • (Integer)

    The number of rows, excluding headers and footer


227
228
229
230
231
# File 'lib/tabledata/table.rb', line 227

def size
  result = @data.size - (@has_headers ? 1 : 0) - (@has_footer ? 1 : 0)

  result < 0 ? 0 : result
end

#to_aArray<Array>

Note:

Only rows and columns are copied, the individual values are not. Example:

Returns a deep copy of the tables internal datastructure.

Returns:

  • (Array<Array>)

    A deep copy of the tables internal datastructure, an Array of row array.

See Also:

  • Table#data returns the actual, uncopied, internal datastructure

480
481
482
# File 'lib/tabledata/table.rb', line 480

def to_a
  @data.map(&:dup)
end