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