Parse a date with too many days in month - ruby-on-rails

Is there any way to correctly parse a date that has too many days in the month? E.g.
'2016-01-32' = '2016-02-01'
'2015-12-32' = '2016-01-01'
This is to support easy date manipulation on some of my JS based front-end.
This is with Ruby 2.3.0 and Rails 4.2.5. I think this was working in an earlier release - perhaps the parsing rules were tightened?
I'd rather rely on a standard library and it's knowledge of leap years etc, rather than implementing this myself.

You can use Date::_parse (or Date::_iso8601) to parse a date string without validation:
require 'date'
h = Date._parse('2016-01-32')
#=> {:mday=>32, :year=>2016, :mon=>1}
And create a date instance using Simone Carletti's suggestion:
Date.new(h[:year], h[:mon]).next_day(h[:mday] - 1)
#=> #<Date: 2016-02-01 ...>
If both, days and months can be out of range, you could use:
h = Date._parse('2016-14-32')
#=> {:mday=>32, :year=>2016, :mon=>14}
Date.new(h[:year]).next_month(h[:mon] - 1).next_day(h[:mday] - 1)
#=> #<Date: 2017-03-04 ...>
next_day and next_month are equivalent to + and << respectively, so the last line can also be written as:
Date.new(h[:year]) << (h[:mon] - 1) + (h[:mday] - 1)
or:
Date.new(h[:year]) << h[:mon].pred + h[:mday].pred

require 'date'
y, m, d = '2016-01-32'.split('-').map(&:to_i)
# => [2016, 01, 32]
last_day_of_month = Date.civil(y, m, -1)
# => #<Date: 2016-01-31 ((2457419j,0s,0n),+0s,2299161j)>
date = last_day_of_month + (d - last_day_of_month.day)
# => #<Date: 2016-02-01 ((2457420j,0s,0n),+0s,2299161j)>

Trying to parse a date like the one you mentioned will raise an error
DateTime.parse('2016-01-32')
ArgumentError: invalid date
One possible solution is to create a custom function/class for date parsing. You forward the parse to the DateTime object, if it fails with that error, you can retry by stripping the day number and replacing it with 1.
If it succeeds, you add the remaining days to the date.
date = DateTime.parse('2016-01-1')
=> Fri, 01 Jan 2016 00:00:00 +0000
date + (32-1).days
=> Mon, 01 Feb 2016 00:00:00 +0000
No existing core or Rails library will do that automatically for you. Hence you need a custom function for that.
You should be able to easily write the method following the instructions and the example above.

Here is one way to do it:
require 'date'
date_string = '2016-01-50'
year,month,day = date_string.split('-').map(&:to_i)
date = Date.new(year,month)
(day-1).times{date = date.next}
p date #=> 2016-02-19

Related

Rails - Convert time to string

This is the time format I want to convert
time = Time.parse('2020-07-02 03:59:59.999 UTC')
#=> 2020-07-02 03:59:59 UTC
I want to convert to string in this format.
"2020-07-02T03:59:59.999Z"
I have tried.
time.strftime("%Y-%m-%dT%H:%M:%S.999Z")
Is this correct? Any better way?
You can just use Time#iso8601 with the desired number of fraction digits as an argument:
time = Time.current.end_of_hour
time.iso8601(3) #=> "2020-07-01T10:59:59.999Z"
If you want to handle the output format explicitly via strftime, there are some things to keep in mind:
Instead of hard-coding 999, you should use %L to get the actual milliseconds:
time = Time.parse('2020-07-02 03:59:59.999 UTC')
#=> 2020-07-02 03:59:59 UTC
time.strftime('%Y-%m-%dT%H:%M:%S.%LZ')
#=> "2020-07-02T03:59:59.999Z"
Use combinations for common formats, e.g. %F for %Y-%m-%d and %T for %H:%M:%S:
time.strftime('%FT%T.%LZ')
#=> "2020-07-02T03:59:59.999Z"
If you are dealing with time zones other than UTC (maybe your machine's local time zone), make sure to convert your time instance to utc first:
time = Time.parse('2020-07-02 05:59:59.999+02:00')
#=> 2020-07-02 05:59:59 +0200
time.utc
#=> 2020-07-02 03:59:59 UTC
time.strftime('%FT%T.%LZ')
#=> "2020-07-02T03:59:59.999Z"
or to use %z / %:z to append the actual time zone offset:
time = Time.parse('2020-07-02 05:59:59.999+02:00')
time.strftime('%FT%T.%L%:z')
#=> "2020-07-02T05:59:59.999+02:00"
For APIs you should use utc.iso8601:
> timestamp = Time.now.utc.iso8601
=> "2015-07-04T21:53:23Z"
See:
https://thoughtbot.com/blog/its-about-time-zones#working-with-apis

Rails absolute time from reference + french "ago" string

I need to reimport some data that was exported using the "ago" stringification helper, in French.
I have a reference Time/DateTime date at which the import was done, and from there I need to substract this "time ago" difference to find the absolute time.
I need to code the parse_relative_time method below
Some sample input/output of what I'm trying to achieve
IMPORT_DATE = Time.parse('Sat, 11 Jun 2016 15:15:19 CEST +02:00')
sample_ago_day = 'Il y a 5j' # Note : 'Il y a 5j" = "5d ago"
parse_relative_time(from: IMPORT_DATE, ago: sample_ago_day)
# => Should output sthing like Sat, 6 Jun 2016 (DateTime object)
sample_ago_month = 'Il y a 1 mois' # Note : 'Il y a 5j" = "1 month ago"
parse_relative_time(from: IMPORT_DATE, ago: sample_ago_month)
# => 11 May 2016 (it's not big deal if it's 10 or 11 or 12 because of months with odd numbers, just need something approximate)
EDIT
Range of values
"il y a xj" -> x belongs to (1..31)
"il y a y mois" -> y belongs to (2..10) and "un"
(for one)
Let's divide the problem into 2 sub-tasks:
Parse the 'ago' string
Since there is no reversible way in ruby to parse an 'ago' string, lets use regular expressions to extract the data as seconds:
def parse_ago(value)
# If the current value matches 'il y a Xj'
if match = /^il y a (.*?)j$/i.match(value)
# Convert the matched data to an integer
value = match[1].to_i
# Validate the numeric value (between 1 and 31)
raise 'Invalid days value!' unless (1..31).include? value
# Convert to seconds with `days` rails helper
value.days
# If the current value matches 'il y a X mois'
elsif match = /^il y a (.*?) mois$/i.match(value)
# If the matched value is 'un', then use 1. Otherwise, use the matched value
value = match[1] == 'un' ? 1 : match[1].to_i
# Validate the numeric value (between 1 and 10)
raise 'Invalid months value!' unless (1..10).include? value
# Convert to seconds with `months` rails helper
value.months
# Otherwise, something is wrong (or not implemented)
else
raise "Invalid 'ago' value!"
end
end
Substract from current time
This is pretty straightforward; once we have the seconds from the 'ago' string; just call the ago method on the seconds extracted from the 'ago' string. An example of usage of this method for Ruby on Rails:
5.months.ago # "Tue, 12 Jan 2016 15:21:59 UTC +00:00"
The thing is, you are substracting it from IMPORT_DATE, and not from current time. For your code, you need to specify the current time to IMPORT_DATE:
parse_ago('Il y a 5j').ago(IMPORT_DATE)
Hope this helps!

Converting time between timezone without changing the time?

Our system currently defaults to Sydney time. I'm attempting to localize some reports to another timezone without changing the daily interval of the reports (i.e. 00:00:00 to 23:59:59).
Any time I attempt to convert the time object to another time zone it converts the hour/minutes/seconds along with the zone.
[16] pry(main)> t = Time.now
=> 2015-09-07 14:41:33 +1000
[17] pry(main)> t.in_time_zone("Darwin")
=> Mon, 07 Sep 2015 14:11:33 ACST +09:30
Can someone please point me in the right direction?
Thanks!
You can use Time::use_zone to override the time zone inside a given block:
t = Time.parse('2015-09-07 14:41:33 +1000')
#=> 2015-09-07 14:41:33 +1000
Time.use_zone('Darwin') { Time.zone.local(t.year, t.month, t.day, t.hour, t.min, t.sec) }
#=> Mon, 07 Sep 2015 14:41:33 ACST +09:30
Or the shortcut:
Time.use_zone('Darwin') { Time.zone.local(*t) }
#=> Mon, 07 Sep 2015 14:41:33 ACST +09:30
*t converts t to an array (using Time#to_a) and passes its elements as arguments:
The ten elements can be passed directly to ::utc or ::local to create a new Time object.
Maybe with regex is the most straightforward way:
t = Time.now
#=> 2015-09-07 05:53:23 +0000
Time.parse t.to_s.sub(/[+-]\d+$/, '+0930')
#=> 2015-09-07 05:53:23 +0930
I've found a solution.. Please feel free to leave an any answers that would be better as I am still a lowly junior.
I created the following method:
def export_in_timezone(time, zone)
Time.use_zone(zone) { time.to_datetime.change(offset: Time.zone.now.strftime("%z")) }
end
and pass in the time and timezone for each reporting being created.
Thanks!

ArgumentError creating instance of Time class on Ruby version 1.8.6

I'm trying to get the time value using datetime_select from the view, return values obtained are:
"scheduletime"=>{"scheduletime(1i)"=>"2014", "scheduletime(2i)"=>"1", "scheduletime(3i)"=>"9", "scheduletime(4i)"=>"10", "scheduletime(5i)"=>"33"}
In controller I'm trying to create the instance of Time class using the return values of datetime_select. In following manner.
schedule_time = Time.new(params[:scheduletime]["scheduletime(1i)"], params[:scheduletime]["scheduletime(2i)"], params[:scheduletime]["scheduletime(3i)"], params[:scheduletime]["scheduletime(4i)"], params[:scheduletime]["scheduletime(5i)"],0, "+09:00")
Trying this I'm getting following error.
ArgumentError (wrong number of arguments (7 for 0))
The version of ruby I'm using is 1.8.6 . Can any one suggest me where I'm going wrong.
Time.new in Ruby 1.8.6 did not allowed any param. See the official documentation.
a = Time.new #=> Wed Apr 09 08:56:03 CDT 2003
b = Time.new #=> Wed Apr 09 08:56:03 CDT 2003
a == b #=> false
"%.6f" % a.to_f #=> "1049896563.230740"
"%.6f" % b.to_f #=> "1049896563.231466"
My suggestion is to use Time.utc in order to create a date with given params.
You can rewrite the code using Hash#valutes_at
Time.utc(*params[:scheduletime].values_at(%w( scheduletime(1i) scheduletime(2i) scheduletime(3i) scheduletime(4i) scheduletime(5i) )))
Note that Time.utc also accepts an optional time zone (given I see you are passing one).
As a side note, you definitely need to upgrade your Ruby version.
The documentation seems to point that you can't pass arguments to the Time.new method.
Try this instead (will use your current TimeZone):
schedule_time = Time.local(params[:scheduletime]["scheduletime(1i)"], params[:scheduletime]["scheduletime(2i)"], params[:scheduletime]["scheduletime(3i)"], params[:scheduletime]["scheduletime(4i)"], params[:scheduletime]["scheduletime(5i)"],0)
# some variants, using standard TimeZone:
Time.utc(2000,"jan",1,20,15,1) #=> Sat Jan 01 20:15:01 UTC 2000
Time.gm(2000,"jan",1,20,15,1) #=> Sat Jan 01 20:15:01 UTC 2000
Time.local(2000,"jan",1,20,15,1) #=> Sat Jan 01 20:15:01 CST 2000
You might want to set the TimeZone to a different one after that.

Using distance_of_time_in_words in Rails

I have a start month (3), start year (2004), and I have an end year (2008). I want to calculate the time in words between the start and end dates. This is what I'm trying and it's not working..
# first want to piece the start dates together to make an actual date
# I don't have a day, so I'm using 01, couldn't work around not using a day
st = (start_year + "/" + start_month + "/01").to_date
ed = (end_year + "/01/01").to_date
# the above gives me the date March 1st, 2004
# now I go about using the method
distance_of_time_in_words(st, ed)
..this throws an error, "string can't me coerced into fixnum". Anyone seen this error?
You can't just concatenate strings and numbers in Ruby. You should either convert numbers to strings as mliebelt suggested or use string interpolation like that:
st = "#{start_year}/#{start_month}/01".to_date
But for your particular case I think there is no need for strings at all. You can do it like that:
st = Date.new(start_year, start_month, 1)
ed = Date.new(end_year, 1, 1)
distance_of_time_in_words(st, ed)
or even like that:
st = Date.new(start_year, start_month)
ed = Date.new(end_year)
distance_of_time_in_words(st, ed)
See Date class docs for more information.
Given that the context in which you are calling the method is one that knows the methods from ActionView::Helpers::DateHelper, you should change the following:
# first want to piece the start dates together to make an actual date
# I don't have a day, so I'm using 01, couldn't work around not using a day
st = (start_year.to_s + "/" + start_month.to_s + "/01").to_date
ed = (end_year.to_s + "/01/01").to_date
# the above gives me the date March 1st, 2004
# now I go about using the method
distance_of_time_in_words(st, ed)
=> "almost 3 years"
So I have added calls to to_s for the numbers, to ensure that the operation + is working. There may be more efficient ways to construct a date, but yours is sufficient.

Resources