working with UTC and datamapper


  # stick this in the before_app_loads block in init.rb
  # if this maybe finds its way into dm-more, this wouldn't be necessary anymore

  module DataMapper
    module Types

      class UtcDateTime < DataMapper::Type

        primitive DateTime

        def self.load(v, property)
          if v.nil?
            nil
          elsif v.is_a?(DateTime)
            ::DateTime.new(v.year, v.month, v.day, v.hour, v.min, v.sec, 0)
          else
            raise ArgumentError.new("+value+ must be nil or a DateTime")
          end
        end

        def self.dump(v, property)
          if v.nil?
            nil
          elsif v.is_a?(String)
            Time.parse(v).utc.to_datetime
          elsif v.is_a?(DateTime)
            Time.parse(v.to_s).utc.to_datetime
          else
            raise ArgumentError.new("+value+ must be nil or a String or a DateTime")
          end
        end

      end # class UtcDateTime

      UTCDateTime = UtcDateTime

    end # module Types
  end # module DataMapper

  module DataMapper
    module Timestamp

      def set_timestamps
        return unless dirty? || new_record?
        TIMESTAMP_PROPERTIES.each do |name,(_type,proc)|
          if model.properties.has_property?(name)
            self.send("#{name}=", proc.call(self, model.properties[name]))
          end
        end
      end

      def utc_timestamped?
        self.class.utc_timestamped?
      end

      module ClassMethods

        def timestamps(*names)
          raise ArgumentError, 'You need to pass at least one argument' if names.empty?

          # if the last element in names is a Hash:
          # extract this hash and look for a :utc key
          opts = names.last.is_a?(Hash) ? names.pop : nil
          utc_possible = names.include?(:created_at) || names.include?(:updated_at)
          @utc = opts && opts[:utc] && utc_possible

          names.each do |name|
            case name
              when *TIMESTAMP_PROPERTIES.keys
                type = TIMESTAMP_PROPERTIES[name].first
                property name, type, :nullable => false, :auto_validation => false

                if type == DateTime && @utc # UTC makes no sense for Date
                  define_method "#{name}=", UTC::PROPERTY_WRITER.call(name, type)
                  define_method "#{name}",  UTC::PROPERTY_READER.call(name, type)
                end

              when :at
                timestamps(:created_at, :updated_at, :utc => @utc)
              when :on
                timestamps(:created_on, :updated_on) # UTC makes no sense for Date
              else
                raise InvalidTimestampName, "Invalid timestamp property name '#{name}'"
            end
          end
        end

        def utc_timestamped?
          !!@utc
        end

      end

      module UTC
        PROPERTY_WRITER = lambda { |name, type|
          typecast = "to_#{type.name.downcase}"
          lambda { |dt| attribute_set(name, Time.parse(dt.to_s).utc.send(typecast)) }
        }
        PROPERTY_READER = lambda { |name, type|
          lambda {
            return nil unless dt = attribute_get(name)
            if type == Date
              Date.new(dt.year, dt.month, dt.day, 0)
            elsif type == DateTime
              DateTime.new(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec, 0)
            else
              nil
            end
          }
        }
      end

    end
  end


3 Responses (Add Your Comment)

  1. Thank you for this! I was just in the process of writing something similar myself to make DM work with my rails app smoothly

  2. Oh hey, you have a typo on line #26. “value” should just be “v”

  3. Hey,

    Glad you find it useful! I’m not sure if it’s the best way to tackle the problem of different timezones and DST, but it did the trick for me. I just came back from a nice long holiday and only finished it one or two days before. I’ll maybe ask some datamapper core dev if this is maybe worth including in dm-types.

    Oh, and thx for pointing out the “typo”! (it’s actually a bug, shame on me, tests should have caught it ;-)

Leave a Reply

Formatting: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>