Graceful date-parsing in Ruby
I have two date parameters in a controller action that I would like to fall-back to a default value if they are nil, or parsing fails.
Unfortunately, it seems that DateTime.strptime
throws an exception if parsing fails, which forces me to write this monstrosity:
starting = if params[:starting].present?
begin
DateTime.strptime(params[:starting], "%Y-%m-%d")
rescue
@meeting_range.first
end
else
@meeting_range.f开发者_StackOverflowirst
end
Feels bad man. Is there any way to parse a date with the Ruby stdlib that doesn't require a begin...rescue
block? Chronic feels like overkill for this situation.
In general, I can't agree with the other solution, using rescue
in this way is bad practice. I think it's worth mentioning in case someone else tries to apply the concept to a different implementation.
My concern is that some other exception you might be interested in will be hidden by that rescue
, breaking the early error detection rule.
The following is for Date
not DateTime
but you'll get the idea:
Date.parse(home.build_time) # where build_time does not exist or home is nil
Date.parse(calculated_time) # with any exception in calculated_time
Having to face the same problem I ended up monkey patching Ruby as follows:
# date.rb
class Date
def self.safe_parse(value, default = nil)
Date.parse(value.to_s)
rescue ArgumentError
default
end
end
Any exception in value will be rose before entering the method, and only ArgumentError
is caught (although I'm not aware of any other possible ones).
The only proper use of inline rescue
is something similar to this:
f(x) rescue handle($!)
Update
These days I prefer to not monkey patch Ruby. Instead, I wrap my Date
in a Rich
module, which I put in lib/rich
, I then call it with:
Rich::Date.safe_parse(date)
Why not simply:
starting = DateTime.strptime(params[:starting], '%Y-%m-%d') rescue @meeting_range.first
My preferred approach these days is to use Dry::Types
for type coercions and Dry::Monads
for representing errors.
require "dry/types"
require "dry/monads"
Dry::Types.load_extensions(:monads)
Types = Dry::Types(default: :strict)
Types::Date.try("2021-07-27T12:23:19-05:00")
# => Success(Tue, 27 Jul 2021)
Types::Date.try("foo")
# => Failure(ConstraintError: "foo" violates constraints (type?(Date, "foo"))
All of the existing answers do have rescue
somewhere. However, we can use some "ugly" methods that was available from Ruby version 1.9.3 (it was there before but there is no official description).
The method is ugly because it starts with an underscore. However, it fits the purpose.
With this, the method call in the question can be written
starting = if params[:starting].present?
parsed = DateTime._strptime(params[:starting], "%Y-%m-%d") || {}
if parsed.count==3 && Date.valid_date?(parsed[:year], parsed[:month], parsed[:mday])
@meeting_range.first
end
else
@meeting_range.first
end
- If the date string is matching the input format,
_strptime
will return a hash with all 3 date parts. soparsed.count==3
means all 3 parts exists. - However a further check that three parts forms a valid date in the calendar is still necessary since
_strptime
will not tell you they are not valid.
When you would to get date as object, parsed from string variable, sometimes passed string value may be nil, or empty, or invalid date string. I'd like to wrote safe metods for short:
def safe_date(string_date)
::Date.parse(string_date)
rescue TypeError, ::Date::Error
::Date.today
end
For example - check in irb console:
3.0.2 :001 > safe_date
=> #<Date: 2022-08-29 ((2459821j,0s,0n),+0s,2299161j)>
3.0.2 :001 > safe_date('')
=> #<Date: 2022-08-29 ((2459821j,0s,0n),+0s,2299161j)>
3.0.2 :002 > safe_date('29.12.2022')
=> #<Date: 2022-12-29 ((2459943j,0s,0n),+0s,2299161j)>
3.0.2 :003 > safe_date('29.13.2022')
=> #<Date: 2022-08-29 ((2459821j,0s,0n),+0s,2299161j)>
精彩评论