Search code examples
ruby-on-railsrubydatetimerubygemsstrftime

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?


Solution

  • 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.