messaliberty

about hulor and us

[Rails] use DataTime class with ActiveRecord column

ActiveRecord’s :datetime corresponds to ruby Time class.
But Time class can’t use old timestamps like 0001-01-01 00:00:00.
(ActiveRecord converts 0001-01-01 00:00:00 to 2001:01:01 00:00:00.)

So overwrite ActiveRecord to use DateTime class with :datetime.

Test Environment:

  • Ubuntu 9.04
  • ruby 1.8.7 (2008-08-11 patchlevel 72) [i486-linux]
  • rails 2.3.2 – 2.3.4
  • mysql Ver 14.12 Distrib 5.0.75, for debian-linux-gnu (i486) using readline 5.2

Just put following code active_record_datetime_ext.rb into RAILS_ROOT/config/initializers

# -*- coding: utf-8 -*-

# ActiveRecord column data type :datetime corresponds to ruby Time class.
#
# class CreateTasks < ActiveRecord::Migration
#   def self.up
#     create_table :tasks do |t|
#       t.column :title,       :string,   :null => false
#       t.column :deadline_at, :datetime, :null => false, :default => '0001-01-01 00:00:00'
#     end
#   end
# end
#
# if set '0001-01-01 00:00:00' to datetime column, it'll be '2001-01-01 00:00:00' due to Time class.
# 
# DateTime.parse('0001-01-01 00:00:00').strftime('%Y-%m-%d %H:%M:%S') # => 0001-01-01 00:00:00
# Time.parse('0001-01-01 00:00:00').strftime('%Y-%m-%d %H:%M:%S')     # => 2001-01-01 00:00:00
# 
# So change ActiveRecord's :datetime Time to DateTime.

unless 2 <= Rails::VERSION::MAJOR and 3 <= Rails::VERSION::MINOR # and 2 <= Rails::VERSION::TINY
  raise StandardError "required rails version 2.3.x or later but #{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}.#{Rails::VERSION::TINY}"
end

require 'active_record/connection_adapters/abstract/schema_definitions'

module ActiveRecord
  module ConnectionAdapters
    class Column
      alias :klass_org :klass

      # return DateTime when :datetime, otherwise ActiveRecord default class.
      def klass
        if type == :datetime
          DateTime
        else
          klass_org
        end
      end

      alias :type_cast_org :type_cast

      def type_cast(value)
        if type == :datetime
          self.class.string_to_datetime(value)
        else
          type_cast_org(value)
        end
      end

      alias :type_cast_code_org :type_cast_code

      def type_cast_code(var_name)
        if type == :datetime
          "#{self.class.name}.string_to_datetime(#{var_name})"
        else
          type_cast_code_org(var_name)
        end
      end

      class << self
        def string_to_datetime(string)
          return string unless string.is_a?(String)
          return nil if string.empty?

          fast_string_to_datetime(string) || fallback_string_to_datetime(string)
        end

        protected

        def new_datetime(year, mon, mday, hour, min, sec, microsec)
          # Treat 0000-00-00 00:00:00 as nil.
          offset = Base.default_timezone.to_sym == :local ? local_offset : 0
          ::DateTime.civil(year, mon, mday, hour, min, sec, offset)
        end

        def fast_string_to_datetime(string)
          if string =~ Format::ISO_DATETIME
            microsec = ($7.to_f * 1_000_000).to_i
            new_datetime $1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, microsec
          end
        end

        def fallback_string_to_datetime(string)
          datetime_hash = DateTime._parse(string)
          datetime_hash[:sec_fraction] = microseconds(datetime_hash)

          new_datetime(*datetime_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
        end
      end
    end
  end
end
Share and Enjoy:
  • Digg
  • del.icio.us
  • Facebook
  • Google Bookmarks
  • email
  • Reddit
  • StumbleUpon
  • Technorati
  • MySpace
  • Tumblr
  • Yahoo! Buzz
  • Twitter

Related posts:

  1. Lucky Star on Rails A few months ago, suddenly I got a message with...
  2. [JRuby on Rails on GAE/J] how-to put rubygems into a jar file to get around file limitations GAE (Google App Engine) has a limit number to the...

Related posts brought to you by Yet Another Related Posts Plugin.

Leave a Reply