module Eco class CSV class Table < ::CSV::Table # @param input [Array, Array, Eco::CSV::Table, ::CSV::Table] # - when `Array` => all `rows` as arrays where first array is the **header** def initialize(input) super(to_rows_array(input)) end # @return [Hash] where keys are the groups and the values a `Eco::CSV::Table` def group_by(&block) rows.group_by(&block).transform_values do |rows| self.class.new(rows) end end # It allows to rename the header names # @return [Eco::CSV::Table] def transform_headers header = self.headers cols = self.columns cols.each do |col| col[0] = yield(col.first) end columns_to_table(cols) end # When there are headers with the same name, it merges those columns # @note it also offers a way to resolve merge conflicts # @return [Eco::CSV::Table] def merge_same_header_names dups = self.duplicated_header_names out_rows = self.map do |row| row.to_a.each_with_object({}) do |(name, value), out| if dups.include?(name) && out.key?(name) if block_given? yield(value, out[name], name) else # resolve value || out[name] end elsif out.key?(name) out[name] else value end.tap do |final_value| out[name] = final_value end end end self.class.new(out_rows) end # @return [Array] list of duplicated header names def duplicated_header_names header = self.headers header.select {|e| header.count(e) > 1}.uniq end # @return [Eco::CSV::Table] def transform_values transformed_rows = rows.map do |row| res = yield(row) case res when Array ::CSV::Row.new(row.headers, res) when ::CSV::Row res end end self.class.new(transformed_rows) end # Slices the selected rows # @return [Eco::CSV::Table] def slice(*index) case index.first when Range, Numeric self.class.new(rows.slice(index.first)) else self end end # @return [Eco::CSV::Table] def slice_columns(*index) case index.first when Range, Numeric columns_to_table(columns.slice(index.first)) when String csv_cols = columns csv_cols = index.each_with_object([]) do |name, cols| col = csv_cols.find {|col| col.first == name} cols << col if col end columns_to_table(csv_cols) else self end end # @return [Eco::CSV::Table] def delete_column(i) csv_cols = columns csv_cols.delete(i) columns_to_table(csv_cols) end # Adds a new column at the end # @param header_name [String] header of the new column # @return [Eco::CSV::Table] with a new empty column def add_column(header_name) new_col = Array.new(length).unshift(header_name) columns_to_table(columns.push(new_col)) end # @return [Array<::CSV::Row>] def rows [].tap do |out| self.each {|row| out << row} end end # It removes all rows where all columns' values are the same def delete_duplicates! unique_rows = [] self.by_row!.delete_if do |row| unique_rows.any? {|done| equal_rows?(row, done)}.tap do |found| unique_rows << row unless found end end end # @param row1 [CSV:Row] row to be compared # @param row2 [CSV:Row] row to be compared # @param [Boolean] `true` if all values of `row1` are as of `row2` def equal_rows?(row1, row2) row1.fields.zip(row2.fields).all? do |(v1, v2)| v1 == v2 end end # @return [Integer] total number of rows not including the header def length to_a.length - 1 end def empty? length < 1 end # @return [Array] each array is the column header followed by its values def columns to_a.transpose end # Creates a single `Hash` where each key, value is a column (header + values) # @note it will override columns with same header name # @return [Hash] keys are headers, values are arrays def columns_hash columns.map do |col| [col.first, col[1..-1]] end.to_h end # Returns an array of row hashes # @note it will override columns with same header def to_a_h rows.map(&:to_h) end # @see #to_a_h def to_array_of_hashes to_a_h end private def columns_to_table(columns_array) data = to_rows_array(columns_array.transpose) self.class.new(data) end def to_rows_array(data) case data when ::CSV::Table to_rows_array(data.to_a) when Hash # hash of columns header as key and column array as value rows_arrays = [a.keys].concat(a.values.first.zip(*a.values[1..-1])) to_rows_array(data.keys) when Enumerable data = data.dup.compact return data unless data.count > 0 sample = data.first case sample when ::CSV::Row data when Array headers = data.shift data.map do |arr_row| ::CSV::Row.new(headers, arr_row) end.compact when Hash headers = sample.keys headers_str = headers.map(&:to_s) data.map do |hash| ::CSV::Row.new(headers_str, hash.values_at(*headers)) end.compact else raise "Expected data that can be transformed into Array<::CSV::Row>. Given 'Enumerable' of '#{sample.class}'" end else raise "Input type not supported. Given: #{data.class}" end end end end end