warder57 warder57 - 6 months ago 34
Ruby Question

Ruby: List DateTime Format Options

I am importing data from a CSV file and I want to allow my user to specify the format for a DateTime column. Rather than allowing them to type in a format string and deal with the pain of validating the input, I was planning on giving them a select box that lists all of the format options (e.g.

%m/%d/%Y
, etc...). Someone mentioned that a gem already exists which will list all of the reasonable DateTime formats, but I have been unable to find it. How can I get get a list of the available DateTime formats for display?

Answer

You might be interested in another alternative. I had to do something similar not too long ago, and support almost arbitrary date formats. In the end, we detected 29 distinct formats (all domestic US and Canada) from all of the data sources (CSV files) that we supported.

I came up with this class to parse and cache the result:

class DateParser
  @@date_cache = {}

  def self.is_date?(date)
    return self.match_digital_date?(date) || self.match_instance_date?(date)
  end

  def self.parse_date(date)
    return nil if date.blank?
    return date if date.instance_of?(Date)

    date = date.to_s
    cached_date = @@date_cache[date]

    return cached_date if !cached_date.nil?

    match = self.match_instance_date?(date)

    if !match.nil?
      month_str = match[1].upcase
      day = match[2].to_i
      year = match[3].to_i
      year += (year < 20) ? 2000 : 1900 if year < 1600

      month = ['JAN','FEB','MAR','APR','MAY','JUN','JUL','AUG','SEP','OCT','NOV','DEC'].find_index {|mon| mon == month_str }
      cached_date = Date.new(year, month+1, day) if !month.nil?
    else
      scrubbed_date = date.gsub(/^([0-9]+)[\/-]+([0-9]+)[\/-]+00$/, '\\1/\\2/2000')
      begin
        parsed = Chronic.parse(scrubbed_date)
      rescue
        parsed = nil
      end
      return nil if parsed.nil?

      cached_date = parsed.to_date
    end

    @@date_cache[date] = cached_date
    return cached_date
  end

private

  # Matches dates in these formats
  #  : 2015-01-13
  #  : 01-13-2015
  #  : 01-13-15
  #  : 13-01-15
  #  : 01/13/2015
  #  : 01/13/15
  #  : 13/01/15
  def self.match_digital_date?(date)
    return /^([0-9]+)[\/-]+([0-9]+)[\/-]+([0-9]+)$/.match(date)
  end

  def self.match_instance_date?(date)
    return /^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d+)\/(\d+)/i.match(date)
  end
end

This uses the Chronic gem to do some of the date parsing, based on an initial regex match to choose the parse path, as well as some necessary fixups that we encountered along the way. Correctness and speed were the primary goals of this approach. Caching the results helped speed up the processing of dates from the CSV file by 5X or more in most cases.

If you want to skip asking the user which format they used, and just get on with consuming any dates that you choose, this should give you what you need.