lib/fat_table/column.rb in fat_table-0.4.2 vs lib/fat_table/column.rb in fat_table-0.5.1

- old
+ new

@@ -141,11 +141,11 @@ # :category: Attributes # Force the column to have String type and then convert all items to # strings. - def force_to_string_type + def force_string! # msg = "Can only force an empty column to String type" # raise UserError, msg unless empty? @type = 'String' unless empty? @items = items.map(&:to_s) @@ -173,70 +173,96 @@ # :category: Aggregates # The names of the known aggregate operations that can be performed on a # Column. - VALID_AGGREGATES = %s(first last rng - sum count min max avg var dev + VALID_AGGREGATES = %s(first last range + sum count min max + avg var pvar dev pdev any? all? none? one?) # :category: Aggregates # Return the first non-nil item in the Column. Works with any Column type. def first - items.compact.first + if type == 'String' + items.reject(&:blank?).first + else + items.compact.first + end end # :category: Aggregates # Return the last non-nil item in the Column. Works with any Column type. def last - items.compact.last + if type == 'String' + items.reject(&:blank?).last + else + items.compact.last + end end # :category: Aggregates - # Return a string of the #first and #last non-nil values in the Column. - # Works with any Column type. - def rng - "#{first}..#{last}" + # Return a count of the non-nil items in the Column. Works with any Column + # type. + def count + if type == 'String' + items.reject(&:blank?).count.to_d + else + items.compact.count.to_d + end end # :category: Aggregates - # Return the sum of the non-nil items in the Column. Works with numeric and - # string Columns. For a string Column, it will return the concatenation of - # the non-nil items. - def sum - only_with('sum', 'Numeric', 'String') - items.compact.sum + # Return the smallest non-nil, non-blank item in the Column. Works with + # numeric, string, and datetime Columns. + def min + only_with('min', 'NilClass', 'Numeric', 'String', 'DateTime') + if type == 'String' + items.reject(&:blank?).min + else + items.compact.min + end end # :category: Aggregates - # Return a count of the non-nil items in the Column. Works with any Column - # type. - def count - items.compact.count.to_d + # Return the largest non-nil, non-blank item in the Column. Works with + # numeric, string, and datetime Columns. + def max + only_with('max', 'NilClass', 'Numeric', 'String', 'DateTime') + if type == 'String' + items.reject(&:blank?).max + else + items.compact.max + end end # :category: Aggregates - # Return the smallest non-nil item in the Column. Works with numeric, - # string, and datetime Columns. - def min - only_with('min', 'NilClass', 'Numeric', 'String', 'DateTime') - items.compact.min + # Return a Range object for the smallest to largest value in the column. + # Works with numeric, string, and datetime Columns. + def range + only_with('range', 'NilClass', 'Numeric', 'String', 'DateTime') + Range.new(min, max) end # :category: Aggregates - # Return the largest non-nil item in the Column. Works with numeric, - # string, and datetime Columns. - def max - only_with('max', 'NilClass', 'Numeric', 'String', 'DateTime') - items.compact.max + # Return the sum of the non-nil items in the Column. Works with numeric and + # string Columns. For a string Column, it will return the concatenation of + # the non-nil items. + def sum + only_with('sum', 'Numeric', 'String') + if type == 'String' + items.reject(&:blank?).join(' ') + else + items.compact.sum + end end # :category: Aggregates # Return the average value of the non-nil items in the Column. Works with @@ -390,189 +416,26 @@ Column.new(header: header, items: items + other.items) end private - # Convert val to the type of key, a ruby class constant, such as Date, - # Numeric, etc. If type is NilClass, the type is open, and a non-blank val - # will attempt conversion to one of the allowed types, typing it as a String - # if no other type is recognized. If the val is blank, and the type is nil, - # the Column type remains open. If the val is nil or a blank and the type is - # already determined, the val is set to nil, and should be filtered from any - # Column computations. If the val is non-blank and the Column type - # determined, raise an error if the val cannot be converted to the Column - # type. Otherwise, returns the converted val as an object of the correct - # class. def convert_to_type(val) - case type - when 'NilClass' - if val != false && val.blank? - # Leave the type of the Column open. Unfortunately, false counts as - # blank and we don't want it to. It should be classified as a boolean. - new_val = nil - else - # Only non-blank values are allowed to set the type of the Column - bool_val = convert_to_boolean(val) - new_val = - if bool_val.nil? - convert_to_date_time(val) || - convert_to_numeric(val) || - convert_to_string(val) - else - bool_val - end - @type = - if [true, false].include?(new_val) - 'Boolean' - elsif new_val.is_a?(Date) || new_val.is_a?(DateTime) - 'DateTime' - elsif new_val.is_a?(Numeric) - 'Numeric' - elsif new_val.is_a?(String) - 'String' - else - msg = "can't add #{val} of type #{new_val.class.name} to a column" - raise UserError, msg - end - end - new_val - when 'Boolean' - if val.is_a?(String) && val.blank? || val.nil? - nil - else - new_val = convert_to_boolean(val) - if new_val.nil? - msg = "attempt to add '#{val}' to a column already typed as #{type}" + new_val = Convert.convert_to_type(val, type) + if new_val && type == 'NilClass' + @type = + if [true, false].include?(new_val) + 'Boolean' + elsif new_val.is_a?(Date) || new_val.is_a?(DateTime) + 'DateTime' + elsif new_val.is_a?(Numeric) + 'Numeric' + elsif new_val.is_a?(String) + 'String' + else + msg = "can't add value '#{val}' of type #{new_val.class.name} to a column" raise UserError, msg end - new_val - end - when 'DateTime' - if val.blank? - nil - else - new_val = convert_to_date_time(val) - if new_val.nil? - msg = "attempt to add '#{val}' to a column already typed as #{type}" - raise UserError, msg - end - new_val - end - when 'Numeric' - if val.blank? - nil - else - new_val = convert_to_numeric(val) - if new_val.nil? - msg = "attempt to add '#{val}' to a column already typed as #{type}" - raise UserError, msg - end - new_val - end - when 'String' - if val.nil? - nil - else - new_val = convert_to_string(val) - if new_val.nil? - msg = "attempt to add '#{val}' to a column already typed as #{type}" - raise UserError, msg - end - new_val - end - else - raise UserError, "Mysteriously, column has unknown type '#{type}'" end - end - - # Convert the val to a boolean if it looks like one, otherwise return nil. - # Any boolean or a string of t, f, true, false, y, n, yes, or no, regardless - # of case is assumed to be a boolean. - def convert_to_boolean(val) - return val if val.is_a?(TrueClass) || val.is_a?(FalseClass) - val = val.to_s.clean - return nil if val.blank? - if val.match?(/\A(false|f|n|no)\z/i) - false - elsif val.match?(/\A(true|t|y|yes)\z/i) - true - end - end - - ISO_DATE_RE = %r{(?<yr>\d\d\d\d)[-\/] - (?<mo>\d\d?)[-\/] - (?<dy>\d\d?)\s* - (T?\s*\d\d:\d\d(:\d\d)? - ([-+](\d\d?)(:\d\d?))?)?}x - - AMR_DATE_RE = %r{(?<dy>\d\d?)[-/](?<mo>\d\d?)[-/](?<yr>\d\d\d\d)\s* - (?<tm>T\d\d:\d\d:\d\d(\+\d\d:\d\d)?)?}x - - # A Date like 'Tue, 01 Nov 2016' or 'Tue 01 Nov 2016' or '01 Nov 2016'. - # These are emitted by Postgresql, so it makes from_sql constructor - # possible without special formatting of the dates. - INV_DATE_RE = %r{((mon|tue|wed|thu|fri|sat|sun)[a-zA-z]*,?)?\s+ # looks like dow - (?<dy>\d\d?)\s+ # one or two-digit day - (?<mo_name>[jfmasondJFMASOND][A-Za-z]{2,})\s+ # looks like a month name - (?<yr>\d\d\d\d) # and a 4-digit year - }xi - - # Convert the val to a DateTime if it is either a DateTime, a Date, a Time, or a - # String that can be parsed as a DateTime, otherwise return nil. It only - # recognizes strings that contain a something like '2016-01-14' or '2/12/1985' - # within them, otherwise DateTime.parse would treat many bare numbers as dates, - # such as '2841381', which it would recognize as a valid date, but the user - # probably does not intend it to be so treated. - def convert_to_date_time(val) - return val if val.is_a?(DateTime) - return val if val.is_a?(Date) - return val.to_datetime if val.is_a?(Time) - begin - str = val.to_s.clean - return nil if str.blank? - - if str.match(ISO_DATE_RE) - date = DateTime.parse(val) - elsif str =~ AMR_DATE_RE - date = DateTime.new(Regexp.last_match[:yr].to_i, - Regexp.last_match[:mo].to_i, - Regexp.last_match[:dy].to_i) - elsif str =~ INV_DATE_RE - mo = Date.mo_name_to_num(last_match[:mo_name]) - date = DateTime.new(Regexp.last_match[:yr].to_i, mo, - Regexp.last_match[:dy].to_i) - else - return nil - end - # val = val.to_date if - date.seconds_since_midnight.zero? ? date.to_date : date - rescue ArgumentError - nil - end - end - - # Convert the val to a Numeric if is already a Numeric or is a String that - # looks like one. Any Float is promoted to a BigDecimal. Otherwise return - # nil. - def convert_to_numeric(val) - return BigDecimal(val, Float::DIG) if val.is_a?(Float) - return val if val.is_a?(Numeric) - # Eliminate any commas, $'s (or other currency symbol), or _'s. - cursym = Regexp.quote(FatTable.currency_symbol) - clean_re = /[,_#{cursym}]/ - val = val.to_s.clean.gsub(clean_re, '') - return nil if val.blank? - case val - when /(\A[-+]?\d+\.\d*\z)|(\A[-+]?\d*\.\d+\z)/ - BigDecimal(val.to_s.clean) - when /\A[-+]?[\d]+\z/ - val.to_i - when %r{\A(?<nm>[-+]?\d+)\s*[:/]\s*(?<dn>[-+]?\d+)\z} - Rational(Regexp.last_match[:nm], Regexp.last_match[:dn]) - end - end - - def convert_to_string(val) - val.to_s + new_val end end end