<commit_before># encoding: utf-8

require 'tabledata/parser'
require 'tabledata/row'
require 'tabledata/column'
require 'tabledata/detection'
require 'tabledata/exceptions'
require 'tabledata/presenter'

module TableData

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

    include Enumerable

    # A list of options which are valid to be passed to some of the constructors.
    ValidOptions           = [:has_headers, :has_footer, :file_type, :name, :table_class, :accessors, :data, :header, :body, :footer]

    # Options which are invalid to be passed to TableData::Table.from_file.
    InvalidFromFileOptions = [:data, :header, :body, :footer]

    # The default name for unnamed tables.
    DefaultTableName       = 'Unnamed Table'

    # The default accessor list.
    DefaultAccessors       = []


    # Create a table from a file.  
    # See {TableData} docs for a list of supported file types.
    #
    # @param [String] path
    #   The path to a file in one of the supported formats.
    #   
    # @param [Hash] options
    #   A list of options. Many are identical to {Table#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.
    #   
    # @option options [Symbol] :file_type
    #   The file type. Nil for auto-detection (which uses the extension of the
    #   filename), or one of :csv, :xls or :xlsx
    # @option options [String] :name
    #   The name of the table. Defaults to the basename of the file without the suffix.
    # @option options [Array<Symbol>, Hash<Symbol => Integer>, nil] :accessors
    #   A list of accessors for the columns. Allows accessing columns by that accessor.
    # @option options [true, false] :has_headers
    #   Whether the table has a header, defaults to true
    # @option options [true, false] :has_footer
    #   Whether the table has a footer, defaults to false
    #
    # @return [TableData::Table]
    #
    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

    # Create a table from a datastructure.
    #
    # @param [Array<Array>] data
    #   An array of arrays, representing rows -> cells.
    #
    # @param [Hash] options
    #   A list of options. Many are identical to {Table#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.
    #   
    # @option options [Symbol] :file_type
    #   The file type. Nil for auto-detection (which uses the extension of the
    #   filename), or one of :csv, :xls or :xlsx
    # @option options [Symbol] :table_class
    #   The class to use for this table. Defaults to self (TableData::Table)
    # @option options [String] :name
    #   The name of the table
    # @option options [Array<Symbol>, Hash<Symbol => Integer>, nil] :accessors
    #   A list of accessors for the columns. Allows accessing columns by that accessor.
    # @option options [true, false] :has_headers
    #   Whether the table has a header, defaults to true
    # @option options [true, false] :has_footer
    #   Whether the table has a footer, defaults to false
    #
    #
    def self.from_data(data, options=nil)
      new(options ? options.merge(data: data) : {data: data})
    end

    # @return [Array<Symbol>] An array of all named accessors
    attr_reader :accessors

    # @return [Hash<Symbol => Integer>] A hash mapping column accessor names to the column index
    attr_reader :accessor_columns

    # @return [Hash<Integer => Symbol>] A hash mapping column index to the column accessor names
    attr_reader :column_accessors

    # @private
    # The internal data structure. Do not modify.
    attr_reader :data

    # @return [String, nil] The name of the table
    attr_reader :name

    # Create a new table.
    #
    # @param [Hash] options
    #   A list of options. Mostly identical to {Table#initialize}'s options
    #   hash, but with the additional options :file_type and :table_class.
    # 
    # @option options [String] :name
    #   The name of the table
    # @option options [Array<Symbol>, Hash<Symbol => Integer>, nil] :accessors
    #   A list of accessors for the columns. Allows accessing columns by that accessor.
    # @option options [Symbol] :data
    #   An array of arrays with the table data. Mutually exclusive with
    #   :header, :body and :footer.
    # @option options [Symbol] :header
    #   An array with the header values. To be used together with :body and :footer.  
    #   Mutually exclusive with :data.  
    #   Automatically sets :has_headers to true.
    # @option options [Symbol] :body
    #   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.  
    # @option options [Symbol] :footer
    #   An array with the header values. To be used together with :header and :body.  
    #   Mutually exclusive with :data.  
    #   Automatically sets :has_footer to true.
    # @option options [true, false] :has_headers
    #   Whether the table has a header, defaults to true
    # @option options [true, false] :has_footer
    #   Whether the table has a footer, defaults to false
    #
    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] = false
        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<commit_msg>Bugfix in TableData::Table#initialize. Default :has_headers properly to true if no :body and no :data is given.<commit_after># encoding: utf-8

require 'tabledata/parser'
require 'tabledata/row'
require 'tabledata/column'
require 'tabledata/detection'
require 'tabledata/exceptions'
require 'tabledata/presenter'

module TableData

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

    include Enumerable

    # A list of options which are valid to be passed to some of the constructors.
    ValidOptions           = [:has_headers, :has_footer, :file_type, :name, :table_class, :accessors, :data, :header, :body, :footer]

    # Options which are invalid to be passed to TableData::Table.from_file.
    InvalidFromFileOptions = [:data, :header, :body, :footer]

    # The default name for unnamed tables.
    DefaultTableName       = 'Unnamed Table'

    # The default accessor list.
    DefaultAccessors       = []


    # Create a table from a file.  
    # See {TableData} docs for a list of supported file types.
    #
    # @param [String] path
    #   The path to a file in one of the supported formats.
    #   
    # @param [Hash] options
    #   A list of options. Many are identical to {Table#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.
    #   
    # @option options [Symbol] :file_type
    #   The file type. Nil for auto-detection (which uses the extension of the
    #   filename), or one of :csv, :xls or :xlsx
    # @option options [String] :name
    #   The name of the table. Defaults to the basename of the file without the suffix.
    # @option options [Array<Symbol>, Hash<Symbol => Integer>, nil] :accessors
    #   A list of accessors for the columns. Allows accessing columns by that accessor.
    # @option options [true, false] :has_headers
    #   Whether the table has a header, defaults to true
    # @option options [true, false] :has_footer
    #   Whether the table has a footer, defaults to false
    #
    # @return [TableData::Table]
    #
    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

    # Create a table from a datastructure.
    #
    # @param [Array<Array>] data
    #   An array of arrays, representing rows -> cells.
    #
    # @param [Hash] options
    #   A list of options. Many are identical to {Table#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.
    #   
    # @option options [Symbol] :file_type
    #   The file type. Nil for auto-detection (which uses the extension of the
    #   filename), or one of :csv, :xls or :xlsx
    # @option options [Symbol] :table_class
    #   The class to use for this table. Defaults to self (TableData::Table)
    # @option options [String] :name
    #   The name of the table
    # @option options [Array<Symbol>, Hash<Symbol => Integer>, nil] :accessors
    #   A list of accessors for the columns. Allows accessing columns by that accessor.
    # @option options [true, false] :has_headers
    #   Whether the table has a header, defaults to true
    # @option options [true, false] :has_footer
    #   Whether the table has a footer, defaults to false
    #
    #
    def self.from_data(data, options=nil)
      new(options ? options.merge(data: data) : {data: data})
    end

    # @return [Array<Symbol>] An array of all named accessors
    attr_reader :accessors

    # @return [Hash<Symbol => Integer>] A hash mapping column accessor names to the column index
    attr_reader :accessor_columns

    # @return [Hash<Integer => Symbol>] A hash mapping column index to the column accessor names
    attr_reader :column_accessors

    # @private
    # The internal data structure. Do not modify.
    attr_reader :data

    # @return [String, nil] The name of the table
    attr_reader :name

    # Create a new table.
    #
    # @param [Hash] options
    #   A list of options. Mostly identical to {Table#initialize}'s options
    #   hash, but with the additional options :file_type and :table_class.
    # 
    # @option options [String] :name
    #   The name of the table
    # @option options [Array<Symbol>, Hash<Symbol => Integer>, nil] :accessors
    #   A list of accessors for the columns. Allows accessing columns by that accessor.
    # @option options [Symbol] :data
    #   An array of arrays with the table data. Mutually exclusive with
    #   :header, :body and :footer.
    # @option options [Symbol] :header
    #   An array with the header values. To be used together with :body and :footer.  
    #   Mutually exclusive with :data.  
    #   Automatically sets :has_headers to true.
    # @option options [Symbol] :body
    #   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.  
    # @option options [Symbol] :footer
    #   An array with the header values. To be used together with :header and :body.  
    #   Mutually exclusive with :data.  
    #   Automatically sets :has_footer to true.
    # @option options [true, false] :has_headers
    #   Whether the table has a header, defaults to true
    # @option options [true, false] :has_footer
    #   Whether the table has a footer, defaults to false
    #
    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
{'fraction_non_alphanumeric': 0.09574540736187939, 'fraction_numerical': 0.0006829201666325207, 'mean_word_length': 2.866913123844732, 'pattern_counts': {'":': 0, '<': 31, '<?xml version=': 0, '>': 35, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1269315', 'n_tokens_mistral': 4312, 'n_tokens_neox': 4218, 'n_words': 1602}