Reputation: 176
I have a model that accepts a decimal (SQLite3 development database). I want the user to input a string however, for example "1:15:15" (which can be human-read as 1 hour, 15 minutes and 15 seconds, or, 75 minutes and 15 seconds). In my model I have a before_save that calls a custom private method to convert the string to a number (note that using private or not has the same result):
class LogEntry < ApplicationRecord
belongs_to :user
default_scope -> { order(date: :desc) }
before_save :convert_runtime
def convert_runtime
a = self.runtime.split(':')
#debugger
self[:runtime] = a[0].to_f * 3600 + a[1].to_f * 60 + a[2].to_f
end
end
The split gave me an error so when I inserted the debugger
code and changed the line with the split to only a = self.runtime
(to see what this value actually is) the debugger yields a value of 0.1e1
for the "a" variable (a is for array). So what Rails seems to do is convert the first value in the string coming from the user (before the ":") to a decimal, ignoring the remainder of the string, before it's saved. Is there a way to manipulate the entire string as it's passed from the view to the model, without Rails first converting it to decimal form? My next option is to change the database to save the string. I feel like this is less ideal because if I want to add up years of entries, each string needs to be converted to a number first, so I'd rather store the time value as seconds, and convert upon input.
I've been stuck on this one for awhile.
Upvotes: 0
Views: 626
Reputation: 21110
I would create a custom setter that does not override an existing method. Some would call this a computed attribute.
class LogEntry < ApplicationRecord
belongs_to :user
default_scope -> { order(date: :desc) }
def runtime_string
return unless runtime
hours, minutes = runtime.divmod(3600)
minutes, seconds = minutes.divmod(60)
[hours, minutes, seconds].drop_while(&:zero?).join(':').presence || '0'
end
def runtime_string=(value)
seconds, minutes, hours = value.split(':').map(&:to_i).reverse
self.runtime = (seconds || 0) + (minutes || 0) * 60 + (hours || 0) * 3600
end
end
Then use this new getter/setter in your form.
The reason I reverse the split value is to handle both the strings "10"
, "1:15"
and "1:2:34"
. However if you know the input is always H:M:S
then there is no need to reverse the split output.
Upvotes: 1
Reputation: 176
It turns out this works, and doesn't require the before_save
line:
def runtime=(runtime)
a = runtime.split(":")
self[:runtime] = a[0].to_f * 3600 + a[1].to_f * 60 + a[2].to_f
end
However, it's not private
and I'm not sure if this is a security risk.
Upvotes: 0
Reputation: 230286
You could use a transient accessor for your string representation, so that activerecord's typing helpers don't cast your value. Something like this:
class LogEntry < ApplicationRecord
belongs_to :user
default_scope -> { order(date: :desc) }
attr_accessor :runtime_string
before_save :convert_runtime
def convert_runtime
a = runtime_string.split(':')
#debugger
self[:runtime] = a[0].to_f * 3600 + a[1].to_f * 60 + a[2].to_f
end
end
Naturally, your form must use runtime_string
now
<%= f.text_field :runtime_string %>
Upvotes: 2