How is : Date.parse("123 456 789") == Fri, 03 May 2019? - ruby

The title pretty much says it all. I'm trying to assess the validity of dates from a CSV import but if someone chooses a telephone number column for date of birth the Date parsing still passes

How is : Date.parse(“123 456 789”) == Fri, 03 May 2019?
Date._parse (with an underscore) returns the raw data:
Date._parse('123 456 789')
#=> {:yday=>123}
Ruby treats 123 as the day of the year and the 123rd day of the current year is May 3.

The documentation on Date#parse explicitly states:
This method does not function as a validator.
That means, this method is omnivorous, it’ll produce a date from whatever input. You need to use Date#iso8601 instead:
main > Date.iso8601("2019-03-19")
#⇒ #<Date: 2019-03-19 ((2458562j,0s,0n),+0s,2299161j)>
main > Date.iso8601("123 456 789")
#⇒ ArgumentError: invalid date

You (and possibly others) might find the following method useful. I've made the following assumptions:
years must be four digits;
invalid commas can be disregarded and removed (e.g., "Jan, 3 2019"); and
the day of the week can be disregarded and removed, even if it is invalid for the date.
MON_NAMES = Date::MONTHNAMES.drop(1).concat(Date::ABBR_MONTHNAMES.drop(1))
#=> ["January", "February", "March", "April", "May", "June",
# "July", "August", "September", "October", "November", "December",
# "Jan", "Feb", "Mar", "Apr", "May", "Jun",
# "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
MON_REGEX = /\b#{Regexp.union(MON_NAMES)}\b(?!\A)/
# => /\b(?-mix:January|February|...|December|Jan|Feb|...|Dec)\b(?!\A)/
MONTH_STR_TO_NBR = MON_NAMES.each_with_index.map { |mon,i| [mon, " #{1 + (i%12)} "] }.to_h
#=> {"January"=>" 1 ", "February"=>" 2 ", ... , "December"=>" 12 ",
# "Jan"=>" 1 ", "Feb"=>" 2 ", ... , "Dec"=>" 12 "}
DAY_REGEX = /\b#{Regexp.union(Date::DAYNAMES + Date::ABBR_DAYNAMES)}\b,?/
#=> /\b(?-mix:Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sun|Mon|Tue|Wed|Thu|Fri|Sat)\b,?/
def my_parse(date_str, locale = :US)
a = date_str.gsub(MON_REGEX, MONTH_STR_TO_NBR).
gsub(DAY_REGEX, '').
gsub(/(?<!\p{Alpha})(?:st|rd|th|,)(?!\p{Alpha})/, '').
gsub(/[-\/]/, ' ').
strip.
split
return nil if a.size != 3 || a.any? { |s| s.match?(/\D/) }
yr_idx = a.index { |s| s.size == 4 }
return nil if yr_idx.nil? || yr_idx == 1
yr = a.delete_at(yr_idx)
return nil unless a.all? { |s| [1,2].include? s.size }
if yr_idx == 0
mon, day = a
else
mon, day = locale == :US ? a : a.reverse
end
begin
Date.strptime("%s %s %s" % [mon, day, yr], '%m %d %Y')
rescue ArgumentError
nil
end
end
my_parse("Tue, 12th January 2019") #=> #<Date: 2019-01-12 (...)>
my_parse("Tue, 12th January 2019", :UK) #=> #<Date: 2019-12-01 (...)>
my_parse("12/4/2019", :US) #=> #<Date: 2019-04-12 (...)>
my_parse("12/4/2019", :UK) #=> #<Date: 2019-12-04 (...)>
my_parse("Jan 12, 2019") #=> #<Date: 2019-12-01 (...)>
my_parse("2019 Jan 23rd") #=> #<Date: 2019-01-23 (...)>
my_parse("Jan 2019 4") #=> nil
my_parse("1/2019/4") #=> nil
my_parse("1/2019/4") #=> nil
my_parse("Jen 12, 2019") #=> nil
my_parse("3 Jan 12, 2019") #=> nil
I would encourage readers to identify any other assumptions required that I have not mentioned. This could of course be modified as needed. One change that could be made fairly easily would be to confirm that the day of week, if present, is correct for the given date.

Related

Matching a sliced string

I am trying to use the case statement:
week # => "03 – 09 MAR 2019"
first_day = week.slice(0..2) # => 03
last_day = week.slice(5..7) # => 09
month = week.slice(8..11) # => MAR
year = week.slice(12..17) # => 2019
puts month # >> MAR
case month
when 'JAN' then month_num = '01'
when 'FEB' then month_num = '02'
when 'MAR' then month_num = '03'
when 'APR' then month_num = '04'
when 'MAY' then month_num = '05'
when 'JUN' then month_num = '06'
when 'JUL' then month_num = '07'
when 'AGO' then month_num = '08'
when 'SEP' then month_num = '09'
when 'OCT' then month_num = '10'
when 'NOV' then month_num = '11'
when 'DEC' then month_num = '12'
else month_num = 'ERROR'
end
puts month_num # >> ERROR
However, the case statement always goes to the else branch.
Why is the var month_num equal to the string "ERROR" instead of "03"?
You are using puts to examine what you have, and therefore you are missing to observe whitespaces in your results. You actually have:
week.slice(0..2) # => "03 "
week.slice(5..7) # => "09 "
week.slice(8..11) # => "MAR "
week.slice(12..17) # => "2019"
To observe what you have, it is better to use p rather than puts.
You have the wrong range. Actually, there is no reason to use ranges here. It is much easier to use the second argument to specify the length:
week.slice(0, 2) # => "03"
week.slice(5, 2) # => "09"
week.slice(8, 3) # => "MAR"
week.slice(12, 4) # => "2019"
Your month is "MAR "
Try
month = week.slice(8..10)
And makes sense, from 8 to 10 inclusive are three characters. Same for the other parts.
Seems like you want to parse a string containing data in a specific format. Instead of relying on absolute indices, you could use a regular expression to match the date format, e.g:
PATTERN = /
(?<first_day>\d{2}) # 2-digit first day
\s* # optional whitespace
[–-] # delimiter(s)
\s*
(?<last_day>\d{2}) # 2-digit last day
\s*
(?<month>\w{3}) # 3-letter month name
\s*
(?<year>\d{4}) # 4-digit year
/ix
To extract the data:
str = '03 – 09 MAR 2019'
m = str.match(PATTERN)
#=> #<MatchData "03 – 09 MAR 2019" first_day:"03" last_day:"09" month:"MAR" year:"2019">
m[:first_day] #=> "03"
m[:last_day] #=> "09"
m[:month] #=> "MAR"
m[:year] #=> "2019"
The results could further be fed into Date.strptime:
require 'date'
from = m.values_at(:first_day, :month, :year).join(' ') #=> "03 MAR 2019"
to = m.values_at(:first_day, :month, :year).join(' ') #=> "09 MAR 2019"
Date.strptime(from, '%d %b %Y') #=> #<Date: 2019-03-03 ...>
Date.strptime(to, '%d %b %Y') #=> #<Date: 2019-03-09 ...>
Or _strptime if you're just interested in the raw values:
Date._strptime(from, '%d %b %Y')
#=> {:mday=>3, :mon=>3, :year=>2019}

Ruby hash string with dates

Im working on a birthday widget, that shows the name of the person who is the closest to have his birthday.
I currently have the following code;
<%
def closest_birthdate(birthdates)
sorted_dates = birthdates.sort_by do |_, value|
(DateTime.parse(value) - DateTime.now).abs
end
name, birthday = sorted_dates.first
"#{name} has his birthday on #{Date.parse(birthday).strftime('%m-%d')}"
end
%>
<% hash = { 'Bart' => '2017-12-06',
'Thomas' => '2017-10-06',
'William' => '2018-09-05',} %>
<%= closest_birthdate(hash) %>
It returns the following:
Thomas has his birthday on 10-06
Now, after his birthday, I have to change the year from 2017 to 2018.
The dates with names are currently stored as strings.
How can I change the strings to dates?
How can I use their actual birth dates so I dont have to change the year from 2017 to 2018 everytime they had their birthday?
Storing dates
A Date instance can be created via Date::new:
require 'date'
birthdate = Date.new(1978, 12, 6)
#=> #<Date: 1978-12-06 ...>
Calculating the next birthday
The next or upcoming birthday for the above birthdate is 2017-12-06, so we have to combine today's year and the birthdate's month and day:
today = Date.today
#=> #<Date: 2017-10-06 ...>
next_birthday = Date.new(today.year, birthdate.month, birthdate.day)
#=> #<Date: 2017-12-06 ...>
What happens if the birthday already occurred this year?
birthdate = Date.new(1985, 9, 5)
#=> #<Date: 1985-12-06 ...>
next_birthday = Date.new(today.year, birthdate.month, birthdate.day)
#=> #<Date: 2017-09-05 ...>
To actually get the next birthday, we have to add a year in that case: (i.e. if next_birthday happens to be before today)
next_birthday = next_birthday.next_year if next_birthday < today
#=> #<Date: 2018-09-05 ...>
Finding the closest birthday
Let's move the above calculation into a method:
def next_birthday(birthdate, today = Date.today)
date = Date.new(today.year, birthdate.month, birthdate.day)
date < today ? date.next_year : date
end
To find the closest (upcoming) birthday, we can either use sort_by and first:
hash = {
'Bart' => Date.new(1978, 12, 6),
'Thomas' => Date.new(1981, 10, 6),
'William' => Date.new(1985, 9, 5)
}
hash.sort_by { |_name, birthdate| next_birthday(birthdate) }
#=> [
# ["Thomas", #<Date: 1981-10-06 ...>],
# ["Bart", #<Date: 1978-12-06 ...>],
# ["William", #<Date: 1985-09-05 ...>]
# ]
hash.sort_by { |_name, birthdate| next_birthday(birthdate) }.first
#=> ["Thomas", #<Date: 1981-10-06 ...>]
or min_by:
hash.min_by { |_name, birthdate| next_birthday(birthdate) }
#=> ["Thomas", #<Date: 1981-10-06 ...>]
Generating output
name, birthdate = hash.min_by { |_name, birthdate| next_birthday(birthdate) }
puts "#{name}'s birthday is on #{birthdate.strftime('%m-%d')}"
# Thomas's birthday is on 10-06
We could also add the age:
age = next_birthday(birthdate).year - birthdate.year
#=> 36
puts "#{name} is turning #{age} on #{birthdate.strftime('%m-%d')}"
# Thomas is turning 36 on 10-06
Edge case
February 29 only occurs on leap years:
Date.new(1984, 2, 29)
#=> #<Date: 1984-02-29 ...>
Attempting to create a February 29 on a non-leap year results in an error:
Date.new(2017, 2, 29)
#=> ArgumentError: invalid date
You could remove those dates or adjust them unless today's year is leap? (to Feb 28 or Mar 1).
There is no standard object as a (birth)day without specification of the year. So you should just save an array of month and day of month. And it is much better to save them as integers rather than strings.
hash = {
'Bart' => [12, 6],
'Thomas' => [10, 6],
'William' => [9, 5],
}
To convert them to a date with a year specification (let's say year = 2018), do like this:
require "date"
date = Date.new(year, *hash['Bart']) rescue nil
Implementing it fully would be like this:
require "date"
def closest_birthdate(birthdates)
year, month, day = Date.today.year, Date.today.month, Date.today.day
name, (m, d) =
birthdates
.sort_by(&:last)
.bsearch{|_, (m, d)| month <= m and day < d}
return unless Date.new(year, m, d) rescue nil
"#{name} has his birthday on #{year}-#{m}-#{d}"
end
closest_birthdate(hash)
Try this if you don't want to change the structure:
<%
def closest_birthdate(birthdates)
sorted_dates = birthdates.sort_by do |_, value|
array = value.split("-") # you got something like this [2017, 12, 06]
(DateTime.parse("#{DateTime.now.year}-#{array[1]}-#{array[2]}") - DateTime.now).abs
end
name, birthday = sorted_dates.first
"#{name} has his birthday on #{Date.parse(birthday).strftime('%m-%d')}"
end
%>
<% hash = { 'Bart' => '1978-12-06',
'Thomas' => '1972-10-06',
'William' => '1992-09-05',} %>
<%= closest_birthdate(hash) %>
Hope this helps.

Is there a function in ruby to display from date to date in words?

I have the followings dates,
from_date( 11-15-2013) and to_date(11-30-2013)
from_date( 11-30-2013) and to_date(12-15-2013)
Now I wanted to display it in words lets say
Nov. 15 - 30, 2013
Nov. 30 - Dec. 15, 2013
is there existing ruby date class method to do this?
Suppose you have:
date_str = "11-15-2013"
where you know the date format is "mm-dd-yyyy". Then the first step is to convert date_strto date a object, using the class method Date::strptime:
require 'date'
date_obj = Date.strptime(date_str, '%m-%d-%Y')
#=> #<Date: 2013-11-15 ((2456612j,0s,0n),+0s,2299161j)>
We now use various methods in the Date class to extract the information of interest:
month = date_obj.month #=> 11
day = date_obj.day #=> 15
year = date_obj.year #=> 2013
wday = date_obj.wday #=> 5
The date class also provides some useful constants, including:
Date::MONTHNAMES
#=> [nil, "January", "February", "March", "April", "May", "June",
# "July", "August", "September", "October", "November", "December"]
Date::ABBR_MONTHNAMES
#=> [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
# "Sep", "Oct", "Nov", "Dec"]
Date::DAYNAMES
#=> ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
Note that the first elements of Date::MONTHNAMES and Date::ABBT_MONTHNAMES (nil) are never referenced because January is month 1, not month 0.
We can now construct strings such as the following:
"Today is day #{day} of #{Date::MONTHNAMES[month]}, a #{Date::DAYNAMES[wday]}."
#=> "Today is day 15 of November, a Friday."
"Oh, I forgot, the year is #{year}."
#=> "Oh, I forgot, the year is 2013.
So doing what you want to do is a fairly straightforward application of these methods:
require 'date'
def spell_out_date_ranges(*date_ranges)
date_ranges.map { |start_str, end_str|
spell_out_one_range(start_str, end_str) }.join(' ')
end
def spell_out_one_range(start_str, end_str)
sd = date_str_to_hash(start_str)
ed = date_str_to_hash(end_str)
if sd[:month] == ed[:month] && sd[:year] == ed[:year]
"%s. %d - %d, %d" % [sd[:month_name], sd[:day], ed[:day], sd[:year]]
elsif sd[:year] == ed[:year]
"%s. %d - %s. %d, %d" % [sd[:month_name], sd[:day], ed[:month_name],
ed[:day], sd[:year]]
else
"%s. %d, %d - %s. %d, %d" % [sd[:month_name], sd[:day], sd[:year],
sd[:month_name], ed[:day], ed[:year]]
end
end
def date_str_to_hash(date_str)
d = Date.strptime(date_str, '%m-%d-%Y')
{ day: d.day, month: d.month, month_name: Date::ABBR_MONTHNAMES[d.month],
year: d.year }
end
Let's try it:
spell_out_date_ranges(['11-15-2013', '11-30-2013'], ['11-30-2013', '12-15-2013'],
['11-30-2013', '12-15-2014'])
#=> "Nov. 15 - 30, 2013 Nov. 30 - Dec. 15, 2013 Nov. 30, 2013 - Nov. 15, 2014"
Here I've used the method String#% to format the strings. The format syntax is explained in the doc for the method Kernel#sprintf.
No ruby date class method can make it directly, you should do something by yourself. you can use strftime method format the time.
Date.new(2015,12,30).strftime("%b") # get The abbreviated month name
=> "Dec"
Date.new(2015,12,30).strftime("%Y-%m-%d") # get the year, month, day
=> "2015-12-30"
Date.parse("30-12-2015") # convert string to date
=> Wed, 30 Dec 2015
you can make it like this:
def func(date1, date2)
date1, date2 = date2, date1 if date1 > date2
if date1.year == date2.year and date1.month == date2.month
return [date1.strftime("%b.%d"), date2.strftime("%d,%Y")].join('-')
elsif date1.year == date2.year and date1.month != date2.month
return [date1.strftime("%b.%d"), date2.strftime("%b.%d,%Y")].join('-')
elsif date1.year != date2.year
return [date1.strftime("%b.%d,%Y"), date2.strftime("%b.%d,%Y")].join('-')
end
end
func(Date.new(2015,11,15), Date.new(2015,11,30))
=> "Nov.15-30,2015"
func(Date.new(2015,11,15), Date.new(2015,12,30))
=> "Nov.15-Dec.30,2015"
func(Date.new(2014,11,15), Date.new(2015,12,30))
=> "Nov.15,2014-Dec.30,2015"

How to split a string in x equal pieces in ruby

I have a string in ruby like this:
str = "AABBCCDDEEFFGGHHIIJJ01020304050607080910"
# 20 letters and 20 numbers in this case
I want to split this in half, which I can do like this:
str[0, str.length/2]
or
str.split(0, str.length/2)
After that, I need to make arrays with the chars but with length 2 for each element like this:
["AA", "BB", "CC", "DD", "EE", "FF", "GG", "HH", "II", "JJ"],
[01, 02, 03, 04, 05, 06, 07, 08, 09, 10]
The problem is, I can't find a concise way to convert this string. I can do something like this
arr = []
while str.length > 0 do
arr << str[0, 1]
str[0, 1] = ""
end
but I rather want something like str.split(2), and the length of the string may change anytime.
How about this?
str.chars.each_slice(2).map(&:join)
You could use the scan method:
1.9.3p194 :004 > a = 'AABBCCDDEEC'
=> "AABBCCDDEEC"
1.9.3p194 :005 > a.scan(/.{1,2}/)
=> ["AA", "BB", "CC", "DD", "EE", "C"]
2.1.0 :642 > "d852".scan(/../)
=> ["d8", "52"]

Ruby: convert string to date

In Ruby, what's the best way to convert a string of the format: "{ 2009, 4, 15 }" to a Date?
You could also use Date.strptime:
Date.strptime("{ 2009, 4, 15 }", "{ %Y, %m, %d }")
Another way:
s = "{ 2009, 4, 15 }"
d = Date.parse( s.gsub(/, */, '-') )
def parse_date(date)
Date.parse date.gsub(/[{}\s]/, "").gsub(",", ".")
end
date = parse_date("{ 2009, 4, 15 }")
date.day
#=> 15
date.month
#=> 4
date.year
#=> 2009
Another way:
Date.new(*"{ 2009, 04, 15 }".scan(/\d+/).map(&:to_i))

Resources