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.

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)


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

def accessor_columns
  @accessor_columns
end

#accessorsArray<Symbol>


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

def accessors
  @accessors
end

#column_accessorsHash<Integer => Symbol> (readonly)


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

def column_accessors
  @column_accessors
end

#nameString? (readonly)


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.

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.

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


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.

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


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.


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

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

#accessors?true, false


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>


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


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?


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

def column_accessor(index)
  @column_accessors[index]
end

#column_countInteger?


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


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>


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


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:


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:

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


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.

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.

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

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

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

#footer?true, false


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?


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

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

#headers?true, false


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

def headers?
  @has_headers
end

#index_for_accessor(name) ⇒ Integer?


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?


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?


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


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

def row(row)
  @rows[row]
end

#sizeInteger Also known as: length


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.

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