Ruby hash string with dates - ruby

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.

Related

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"

Use DateTime to print name of day from a date?

I am trying to use DateTime to go through a list of dates, and find the days of week (Monday, Tuesday, Wednesdat, &c.) that are most often associated with each date in the list.
I am trying this:
contents = CSV.open 'event_attendees.csv', headers: true, header_converters: :symbol
contents.each do |row|
times = contents.map { |row| row[:regdate] }
target_days = Hash[times.group_by { |t| DateTime.strptime(t, '%m/%d/%Y %H:%M').wday }.map{|k,v| [k, v.count]}.sort_by{ |k,v| v }.reverse]
puts target_days
I get:
{1=>6, 2=>5, 5=>4, 6=>2, 0=>1}
From what I understand wday will represent each day as 0(sunday), 1(monday), &c. I am stumped on how to convert this into the actual name of the day? Or how can this be converted to the abbreviated name (Sun, Mon, Tue, &c.)?
Also, I'm not positive the above is returning the correct days. Looking at my list, there are six dates for 11/12/2008. November 12, 2008 was a Wednesday — but it looks like it is showing that the most common day, with a count of 6, is Monday. So, I'm not sure that this is really counting the correct day of the week.
Can someone please explain why what I am doing doesn't seem to be counting the correct day of the week, also — how to convert this to the name of the day and abbreviated name?
Thank you!
You can convert the wday integers to full names using Date::DAYNAMES and abbreviated names using Date::ABBR_DAYNAMES:
Date::DAYNAMES[3]
#=> "Wednesday"
Date::ABBR_DAYNAMES[3]
#=> "Wed"
Update
As far as your algorithm goes, it looks right to me:
require "date"
times = [
"4/25/2014 00:00", # Friday
"4/21/2014 00:00", # Monday
"4/22/2014 00:00", # Tuesday
"4/20/2014 00:00", # Sunday
"4/22/2014 00:00", # Tuesday
"4/21/2014 00:00", # Monday
"4/21/2014 00:00", # Monday
"4/19/2014 00:00"] # Saturday
target_days = Hash[times.group_by do |time|
DateTime.strptime(time, "%m/%d/%Y %H:%M").wday
end.map do |key, value|
[Date::ABBR_DAYNAMES[key], value.count]
end.sort_by do |key, value|
value
end.reverse]
puts target_days
#=> {"Mon"=>3, "Tue"=>2, "Sat"=>1, "Sun"=>1, "Fri"=>1}
I would double check the contents of the file, and then step through the algorithm to see what's going wrong.
time = Time.new(2000)
# The full weekday name
puts time.strftime("%A")
# Saturday
# The abbreviated name
puts time.strftime("%a")
# Sat
See Time.strftime for more details. DateTime has the same implementation.

Ruby: convert day as decimal to day as name

Is it possible to convert quickly a strftime("%u") value to a strftime("%A") or do i need to build an equivalence hash like {"Monday" => 1, ......... "Sunday" => 6}
I have an Array with some day as decimal values
class_index=[2,6,7]
and I would like to loop through this array to build and array of days name like this
[nil, "Tuesday", nil, nil, nil, "Saturday", "Sunday"]
so I could do
class_list=[]
class_index.each do |x|
class_list[x-1] = convert x value to day name
end
Is that even possible?
How about:
require "date"
DateTime.parse("Wednesday").wday # => 3
Oh, I now see you've expanded your question. How about:
[2,6,7].inject(Array.new(7)) { |memo,obj| memo[obj-1] = Date::DAYNAMES[obj%7]; memo }
Let me explain that one:
input = [2,6,7]
empty_array = Array.new(7) # => [nil, nil, nil, nil, nil, nil, nil]
input.inject(empty_array) do |memo, obj| # loop through the input, and
# use the empty array as a 'memo'
day_name = Date::DAYNAMES[obj%7] # get the day's name, modulo 7 (Sunday = 0)
memo[obj-1] = day_name # save the day name in the empty array
memo # return the memo for the next iteration
end
The beauty of Ruby.
To go from decimal to weekday:
require 'date'
Date::DAYNAMES[1]
# => "Monday"
So, in your example, you could simply do:
class_list=[]
class_index.each do |x|
class_list[x-1] = Date::DAYNAMES[x-1]
end
Here’s one way that comes to mind:
require "date"
def weekday_index_to_name(index)
date = Date.parse("2011-09-26") # Canonical Monday.
(index - 1).times { date = date.succ }
date.strftime("%A")
end
class_index=[2,6,7]
class_index.map{|day_num| Date::DAYNAMES[day_num%7]}
#=> ["Tuesday", "Saturday", "Sunday"]
note that day names are from 0 to 6, so you can either work from 0 to 6 or have it modulo 7

Group a Ruby Array of Dates by Month and Year into a Hash

Let's say I had a Ruby Array of Dates like:
2011-01-20
2011-01-23
2011-02-01
2011-02-15
2011-03-21
What would be an easy and Ruby-esque way of creating a Hash that groups the Date elements by year and then month, like:
{
2011 => {
1 => [2011-01-20, 2011-01-23],
2 => [2011-02-01, 2011-02-15],
3 => [2011-03-21],
}
}
I can do this by iterating over everything and extracting years, months and so on, then comining them.
Ruby offers so many methods and blocks for Arrays and Hashes, there must be an easier way?
require 'date'
dates = [
'2011-01-20',
'2011-01-23',
'2011-02-01',
'2011-02-15',
'2011-03-21'
].map{|sd| Date.parse(sd)}
Hash[
dates.group_by(&:year).map{|y, items|
[y, items.group_by{|d| d.strftime('%B')}]
}
]
#=> {2011=>{"January"=>[#<Date: 2011-01-20 (4911163/2,0,2299161)>, #<Date: 2011-01-23 (4911169/2,0,2299161)>], "February"=>[#<Date: 2011-02-01 (4911187/2,0,2299161)>, #<Date: 2011-02-15 (4911215/2,0,2299161)>], "March"=>[#<Date: 2011-03-21 (4911283/2,0,2299161)>]}}
I noticed you have changed month names into numbers, so you may want to replace d.strftime('%B') above with d.month or whatever else.
Here's a step-by-step explanation:
You essentially want two-level grouping: first level by year, second by month. Ruby has very useful method group_by, which groups elements by given expression (a block). So: first part is grouping original array by year:
hash_by_year = dates.group_by(&:year)
# => {2011=>[#<Date: 2011-01-20 (4911163/2,0,2299161)>, #<Date: 2011-01-23 (4911169/2,0,2299161)>, #<Date: 2011-02-01 (4911187/2,0,2299161)>, #<Date: 2011-02-15 (4911215/2,0,2299161)>, #<Date: 2011-03-21 (4911283/2,0,2299161)>]}
That gives us first level: keys are years, values arrays of dates with given year. But we still need to group the second level: that's why we map by-year hash - to group its values by month. Let's for start forget strftime and say that we're grouping by d.month:
hash_by_year.map{|year, dates_in_year|
[year, dates_in_year.group_by(&:month)]
}
# => [[2011, {1=>[#<Date: 2011-01-20 (4911163/2,0,2299161)>, #<Date: 2011-01-23 (4911169/2,0,2299161)>], 2=>[#<Date: 2011-02-01 (4911187/2,0,2299161)>, #<Date: 2011-02-15 (4911215/2,0,2299161)>], 3=>[#<Date: 2011-03-21 (4911283/2,0,2299161)>]}]]
That way we got our second level grouping. Instead of array of all dates in a year, now we have hash whose keys are months, and values arrays of dates for a given month.
The only problem we have is that map returns an array and not a hash. Thats why we "surround" whole expression by Hash[], which makes a hash out of array of pairs, in our case pairs [year, hash_of_dates_by_month].
Sorry if the explanation sounds confusing, I found harder to explain functional expressions than imperative, because of the nesting. :(
This gets you pretty close, you just need to change the numerical month number into a textual month name:
dates = %w(
2011-01-20
2011-01-23
2011-02-01
2011-02-15
2011-03-21
)
grouped = dates.inject({}) do |ret, date|
y,m,d = date.split('-')
ret[y] ||= {}
# Change 'm' into month name here
ret[y][m] ||= []
ret[y][m] << date
ret
end
puts grouped.inspect
dates = %w(
2011-01-20
2011-01-23
2011-02-01
2011-02-15
2011-03-21
)
hash = {}
dates.each do |date|
year, month = date.strftime('%Y,%B').split(',')
hash[year] ||= {}
hash[year][month] = hash[year][month].to_a << date
end

Is there an add_days in ruby datetime?

In C#, There is a method AddDays([number of days]) in DateTime class.
Is there any kind of method like this in ruby?
The Date class provides a + operator that does just that.
>> d = Date.today
=> #<Date: 4910149/2,0,2299161>
>> d.to_s
=> "2009-08-31"
>> (d+3).to_s
=> "2009-09-03"
>>
In Rails there are very useful methods of Fixnum class for this (here n is Fixnum. For example: 1,2,3.... ):
Date.today + n.seconds # you can use 1.second
Date.today + n.minutes # you can use 1.minute
Date.today + n.hours # you can use 1.hour
Date.today + n.days # you can use 1.day
Date.today + n.weeks # you can use 1.week
Date.today + n.months # you can use 1.month
Date.today + n.years # you can use 1.year
These are convenient for Time class too.
PS: require Active Support Core Extensions to use these in Ruby
require 'active_support/core_ext'
I think next_day is more readable than + version.
require 'date'
DateTime.new(2016,5,17)
# => #<DateTime: 2016-05-17T00:00:00+00:00 ((2457526j,0s,0n),+0s,2299161j)>
DateTime.new(2016,5,17).next_day(10)
# => #<DateTime: 2016-05-27T00:00:00+00:00 ((2457536j,0s,0n),+0s,2299161j)>
Date.new(2016,5,17)
# => #<Date: 2016-05-17 ((2457526j,0s,0n),+0s,2299161j)>
Date.new(2016,5,17).next_day(10)
# => #<Date: 2016-05-27 ((2457536j,0s,0n),+0s,2299161j)>
http://ruby-doc.org/stdlib-2.3.1/libdoc/date/rdoc/Date.html#method-i-next_day.
From the Date class:
+(n)
Return a new Date object that is n days later than the current one.
n may be a negative value, in which case the new Date is earlier than the current one; however, #-() might be more intuitive.
If n is not a Numeric, a TypeError will be thrown. In particular, two Dates cannot be added to each other.
Date.new(2001,9,01).next_day(30) # 30 - numbers of day
# => #<Date: 2001-10-01 ...
You can also use the advance (https://apidock.com/rails/DateTime/advance) method. I think it's more legible.
date = Date.today
# => Fri, 25 Oct 2019
date.advance(days: 10)
# => Mon, 04 Nov 2019
time = DateTime.now
# => Fri, 25 Oct 2019 14:32:53 +0200
time.advance(months:1, weeks: 2, days: 2, hours: 6, minutes: 6, seconds: 34)
# => Wed, 11 Dec 2019 20:39:27 +0200

Resources