Search code examples
ruby-on-railsruby-on-rails-7

normalizes str to decimal in rails 7.1


Is is possible to use the new normalizes in Rails 7.1 to convert types before it is assigned. For example,

create_table "procedures" do |t|
  . . . 
  t.float "duration"
  . . . 
end
class Procedure < ActiveRecord
  normalizes :duration, with: -> (d) {
    match = /^(?:(\d+)h)\s?(?:(\d+)m$/.match(d.to_s)
    if match
      match[1].to_i * 60.0 + match[2].to_i
    else
      d.to_f
    end
  }
  expect(Procedure.new(duration: "3h 30m").duration).to eq(210.0) # Fail: duration == 3

I'm either confused about the intended use of normalizes or, I'm using it correctly but I have a bug somewhere.


Solution

  • Other than normalization, there are a few ways you can do it:

    Simply override duration= method, if you only need to convert it one way:

    class Procedure < ApplicationRecord
      def duration=(value)
        self[:duration] = if /^(?:(\d+)h)\s?(?:(\d+)m)$/ =~ value.to_s
                            $1.to_i * 60 + $2.to_i
                          else
                            value
                          end
      end
    end       
    

    or make a custom attribute type:

    # config/initializers/types.rb
    
    class DurationType < ActiveRecord::Type::Float
      def cast(value)
        if /^(?:(\d+)h)\s?(?:(\d+)m)$/ =~ value.to_s
          value = $1.to_i * 60 + $2.to_i
        end
        super
      end
    end
    
    ActiveRecord::Type.register(:duration, DurationType)
    
    class Procedure < ApplicationRecord
      attribute :duration, :duration
    end
    

    You could serialize if you want to save a float into the database but show a string representation:

    class Procedure < ApplicationRecord
      class Duration
        def self.load(value)
          return unless value
          "%dh %dm" % value.divmod(60)
        end
    
        def self.dump(value)
          if /^(?:(\d+)h)\s?(?:(\d+)m)$/ =~ value.to_s
            $1.to_i * 60.0 + $2.to_i
          else
            value
          end
        end
      end
    
      serialize :duration, coder: Duration
    end
    
    >> Procedure.create!(duration: "3h15m")
      TRANSACTION (0.1ms)  begin transaction
      Procedure Create (0.6ms)  INSERT INTO "procedures" ("duration") VALUES (?) RETURNING "id"  [["duration", 195.0]]
      # -> ----------------------------------------------------------------------------------------------------^^^^^
      TRANSACTION (0.1ms)  commit transaction
    => #<Procedure:0x00007fb6c009bfb8 id: 1, duration: "3h 15m">