In Ruby, is it possible to do arithmetic with symbols? - ruby

In Ruby, I'd like to be able to do something like this:
days_of_week = %i(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
today = :Sunday
today = today + 1
if today > :Saturday
today = :Sunday
end
This gives me
'undefined method '+' for :Sunday:Symbol (NoMethodError)
Can I define a method somehow?
I've looked at various stack overflow questions on enums, and didn't see what I'm looking for, but it's a large volume of information to sort through.

This code, as it is, does not work in ruby (as you noticed). Symbol :Sunday does not know that it's placed in an array, so adding a 1 to it does not make any sense.
You can compare symbols like that, but the results will surprise you. :Wednesday is greater than :Thursday, for example.
Why not just operate on indexes in the array. With integer indexes you can increment them and compare with expected results. Could look like this, for example:
today = days_of_week.index(:Friday)
puts "tomorrow is #{days_of_week[today + 1]}" # >> tomorrow is Saturday

This gives me undefined method `+' for :Sunday:Symbol (NoMethodError)
Can I define a method somehow?
In Ruby, you can open existing classes and add methods to them. Although it's not recommended to patch classes you don't own – let alone core classes – it's absolutely possible:
class Symbol
WEEKDAYS = %i(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
def +(offset)
wday = WEEKDAYS.index(self)
return unless wday
WEEKDAYS[(wday + offset) % WEEKDAYS.length]
end
end
:Sunday + 2
#=> :Tuesday
A less invasive approach is to define a custom class:
class WeekDay
attr_reader :name, :wday
def initialize(name, wday)
#name = name
#wday = wday
end
alias to_s name
alias inspect name
end
In addition to a name, each instance also knows its wday (0-6) which will simplify the lookup later on (so we don't have to scan the array to find the day's index).
For each day of the week I'd define a constant and also put all days in an array: (you could also use a loop and define them via const_set, I prefer to set them explicitly)
class WeekDay
module DayConstants
Sunday = WeekDay.new('Sunday', 0)
Monday = WeekDay.new('Monday', 1)
Tuesday = WeekDay.new('Tuesday', 2)
Wednesday = WeekDay.new('Wednesday', 3)
Thursday = WeekDay.new('Thursday', 4)
Friday = WeekDay.new('Friday', 5)
Saturday = WeekDay.new('Saturday', 6)
end
include DayConstants
WEEKDAYS = [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday].freeze
def +(offset)
WEEKDAYS[(wday + offset) % WEEKDAYS.length]
end
end
You might have wondered why I put the day constants in a separate module. It's so you can easily include them in your current namespace: (this is mostly cosmetic – you could just as well omit the include and write out today = WeekDay::Sunday)
include WeekDay::DayConstants
today = Sunday
today += 1
today #=> Monday
today += 4
today #=> Friday
today += 2
today #=> Sunday
To add comparison operators, you can include Comparable and implement <=>:
class WeekDay
include Comparable
def <=>(other)
wday <=> other.wday if other.is_a?(WeekDay)
end
# ...
end
Friday > Wednesday
#=> true
Wednesday.between?(Monday, Friday)
#=> true

I think I would create an index with as many entries as necessary to describe what you are trying to do.
Here is a hash of hashes to hold the index and next/prev logical day:
days_of_week = %i(Sunday Monday Tuesday Wednesday Thursday Friday Saturday)
idx=Hash.new {|h, k| h[k] = {} }
days_of_week.each_with_index{ |e,i|
idx[e][:idx]=i
idx[e][:next_day]=
(i+1)==days_of_week.length ? days_of_week[0] : days_of_week[i+1]
idx[e][:prev_day]=
i==0 ? days_of_week[-1] : days_of_week[i-1]
}
# {:Sunday=>{:idx=>0, :next_day=>:Monday, :prev_day=>:Saturday}, :Monday=>{:idx=>1, :next_day=>:Tuesday, :prev_day=>:Sunday}, :Tuesday=>{:idx=>2, :next_day=>:Wednesday, :prev_day=>:Monday}, :Wednesday=>{:idx=>3, :next_day=>:Thursday, :prev_day=>:Tuesday}, :Thursday=>{:idx=>4, :next_day=>:Friday, :prev_day=>:Wednesday}, :Friday=>{:idx=>5, :next_day=>:Saturday, :prev_day=>:Thursday}, :Saturday=>{:idx=>6, :next_day=>:Sunday, :prev_day=>:Friday}}
Then you can get the next day or previous day:
idx[:Tuesday][:next_day]
# Wednesday
idx[:Saturday][:next_day]
# Wraps around to Sunday
You can create a compare:
def cmp(x,y, idx)
idx[x][:idx]<=>idx[y][:idx]
end
So that :Wednesday is properly less than :Thursday
cmp(:Wednesday, :Thursday, idx)
-1
Or sort another array of symbols into the order of the index:
%i(Saturday Sunday Tuesday Thursday Wednesday Friday Monday).
sort_by{ |day| idx[day][:idx] }
# [:Sunday, :Monday, :Tuesday, :Wednesday, :Thursday, :Friday, :Saturday]
Those types of function can be added to a class to represent the type of symbols you want to deal with.

Related

Ruby - How do you add data into a nested array?

I'm relatively new to Ruby and I'm trying to design a code that will help me with work.
What I would like the code to do is allow the user to input the "invoice numbers" for each day of the week starting on Monday, then when the user has finished entering the invoice numbers, the program should ask how many hours were worked for each day. Then, I would like for the program to divide the amount of hours worked by the amount of invoice numbers inputted for each respective day and output the "billable time" in a format like this:
say Monday worked 10 hours and you inputted invoice #123 and #124
The program should output the following -
Monday
#123 - 5 Hours
#124 - 5 Hours
but I would like for this to happen for every day of the week. I'm assuming I'll need to use a nested Array but I'm just confused as to how to go about adding the entries from the user and having the program know when to "move" to the next day to add the next set of entries.
Here is the code I have so far:
days = ["Mon","Tue","Wed","Thurs","Fri","Sat","Sun"]
entry = Array.new
days.each do |day|
while entry != "ok"
puts "Enter PR # for " + day
puts "/n Type 'ok' when finished"
entry.each do |input|
input << gets.to_s.chomp
end
end
end
Essentially I would just like for the program to recognize that the user is done inputting entries for that day by typing "ok" or something so that it can move from Monday to Tuesday, etc.
In a perfect world... at the end of the sequence, I would like for the program to combine the values of all similarly named invoice numbers from each day into one result (i.e. if invoice #123 applied on Monday and Tuesday, the program would add the sums of the billable hours from those two days and output the result as one total billable amount.)
Thank you in advance for any assistance with this as the knowledge that comes with it will be deeply valued!!
You might collect the needed information as follows.
DAYS = ["Mon", "Tue", "Wed", "Thurs", "Fri", "Sat", "Sun"]
def invoices_by_day
DAYS.each_with_object({}) do |day, inv_by_day|
prs = []
loop do
puts "Enter PR # for #{day}. Press ENTER when finished"
pr = gets.chomp
break if pr.empty?
prs << pr
end
hours =
if prs.any?
puts "Enter hours worked on #{day}"
gets.to_f
else
0
end
inv_by_day[day] = { prs: prs, hours: hours }
end
end
Suppose
h = invoices_by_day
#=> { "Mon"=>{ prs: ["123", "124"], hours: 21.05 },
# "Tue"=>{ prs: ["125"], hours: 31.42 },
# "Wed"=>{ prs: ["126", "127"], hours: 68.42 },
# "Thu"=>{ prs: ["128"], hours: 31.05 },
# "Fri"=>{ prs: [], hours: 0 },
# "Sat"=>{ prs: ["129", "130"], hours: 16.71 }
# "Sun"=>{ prs: ["131"], hours: 55.92 } }
Then you could display this information in various ways, such as the following.
h.each do |day, g|
puts day
if g[:prs].empty?
puts " No invoices"
else
avg = (g[:hours]/g[:prs].size).round(2)
g[:prs].each { |pr| puts " ##{pr}: #{avg}" }
end
end
Mon
#123: 10.53
#124: 10.53
Tue
#125: 31.42
Wed
#126: 34.21
#127: 34.21
Thurs
#128: 31.05
Fri
No invoices
Sat
#129: 8.36
#130: 8.36
Sun
#131: 55.92
As a rule it is good practice to separate the data collection from the manipulation of the data and the presentation of the results. That makes it easier to change either at a later date.
I use Kernel#loop with break for virtually all looping. One advantage of loop is that employs a block (unlike while and until), confining the scope of local variables created within the block to the block. Another advantage is that it handles StopIteration exceptions by breaking out of the loop. That exception is raised by an enumerator when it attempts to generate an element beyond its last value. For example, if e = 3.times #=> #<Enumerator: 3:times>, then e.next #=> 0, e.next #=> 1 e.next #=> 2, e.next #=> StopIteration.
Hey – here you go –> https://gist.github.com/Oluwadamilareolusakin/4f147e2149aa97266cfbb17c5c118fbf
Made a gist for you that may help, let me know!
NOTE: Be careful with the while true so you don't run into an infinite loop
Here's it is for easy reference:
# frozen_string_literal: true
def display_billable_hours(entries)
entries.each do |key, value|
puts "#{key}:"
puts value
end
end
def handle_input
days = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"]
entries = {}
days.each do |day|
loop do
p "Enter your invoice number for #{day}:"
invoice_number = gets.chomp
break if invoice_number.length > 0
end
loop do
p "How many hours did you work on #{day}?:"
hours_worked = gets.chomp
break if hours_worked.length > 0
end
entries[day] = "##{invoice_number} - #{hours_worked} Hours"
end
entries
end
def do_audit
entries = handle_input
display_billable_hours(entries)
end
do_audit

Get the last Sunday of the Month

I'm using Chronic to get the last Sunday of the month of any given year. It will gladly give me the n‌th Sunday, but not the last.
This works, but is not what I need:
Chronic.parse('4th sunday in march', :now => Time.local(2015,1,1))
This is what I need, but doesn't work:
Chronic.parse('last sunday in march', :now => Time.local(2015,1,1))
Is there any way around this apparent limitation?
UPDATE: I'm upvoting the two answers below because they're both good, but I've already implemented this in "pure Ruby" (in 2 lines of code, besides the require 'date' line), but I'm trying to demonstrate to management that Ruby is the right language to use to replace a Java codebase that is going away (and which had dozens of lines of code to compute this), and I told one manager that I probably could do it in one line of Ruby, and it would be readable and easy to maintain.
I am not sure about Chronic (I haven't heared about it before), but we can implement this in pure ruby :)
##
# returns a Date object being the last sunday of the given month/year
# month: integer between 1 and 12
def last_sunday(month,year)
# get the last day of the month
date = Date.new year, month, -1
#subtract number of days we are ahead of sunday
date -= date.wday
end
The last_sunday method can be used like this:
last_sunday 07, 2013
#=> #<Date: 2013-07-28 ((2456502j,0s,0n),+0s,2299161j)>
Reading the update in your question, I tried to come up with another answer using only a single line of ruby code (without using gems). How about this one?
##
# returns a Date object being the last sunday of the given month/year
# month: integer between 1 and 12
def last_sunday(month,year)
# get the last day of the month, go back until we have a sunday
Date.new(year, month, -1).downto(0).find(&:sunday?)
end
last_sunday 07, 2013
#=> #<Date: 2013-07-28 ((2456502j,0s,0n),+0s,2299161j)>
What about
Chronic.parse('last sunday', now: Chronic.parse('last day of march'))
This works, and is as readable as I can get:
Chronic.parse('last sunday', now: Date.new(year,3,31))
Thanks to Ismael Abreu for the idea to just parse 'last sunday' and control the rest via the :now option.
UPDATE: Please also upvote Ismael's answer.
It's a bit ugly, but you could simply try the 5th or 4th in that order:
d = [5,4].each do |i|
try = Chronic.parse("#{i}th sunday in march", :now => Time.local(2015,1,1))
break try unless try.nil?
end
=> Sun Mar 29 12:30:00 +0100 2015
d = [5,4].each do |i|
try = Chronic.parse("#{i}th sunday in april", :now => Time.local(2015,1,1))
break try unless try.nil?
end
=> Sun Apr 26 12:00:00 +0100 2015

Time-of-day range in Ruby?

I want to know if a time belongs to an schedule or another.
In my case is for calculate if the time is in night schedule or normal schedule.
I have arrived to this solution:
NIGHT = ["21:00", "06:00"]
def night?( date )
date_str = date.strftime( "%H:%M" )
date_str > NIGHT[0] || date_str < NIGHT[1]
end
But I think is not very elegant and also only works for this concrete case and not every time range.
(I've found several similar question is SO but all of them make reference to Date ranges no Time ranges)
Updated
Solution has to work for random time ranges not only for this concrete one. Let's say:
"05:00"-"10:00"
"23:00"-"01:00"
"01:00"-"01:10"
This is actually more or less how I would do it, except maybe a bit more concise:
def night?( date )
!("06:00"..."21:00").include?(date.strftime("%H:%M"))
end
or, if your schedule boundaries can remain on the hour:
def night?(date)
!((6...21).include? date.hour)
end
Note the ... - that means, basically, "day time is hour 6 to hour 21 but not including hour 21".
edit: here is a generic (and sadly much less pithy) solution:
class TimeRange
private
def coerce(time)
time.is_a? String and return time
return time.strftime("%H:%M")
end
public
def initialize(start,finish)
#start = coerce(start)
#finish = coerce(finish)
end
def include?(time)
time = coerce(time)
#start < #finish and return (#start..#finish).include?(time)
return !(#finish..#start).include?(time)
end
end
You can use it almost like a normal Range:
irb(main):013:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,02,30))
=> true
irb(main):014:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,01,30))
=> false
irb(main):015:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,01,30))
=> true
irb(main):016:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,02,30))
=> false
Note, the above class is ignorant about time zones.
In Rails 3.2 it has added Time.all_day and similars as a way of generating date ranges. I think you must see how it works. It may be useful.

Finding the date for a given week number

I am trying to do some date math based on the week number of a given year. For example:
date = Date.today # Monday, March 5, 2012
puts date.cwyear # 2012
puts date.cweek # 10 (10th week of 2012)
Now that I know what the current week is, I want to figure out what the next week and previous week are. I need to take the year (2012) and the week number (10) and turn it back into a date object so I can calculate the value for the next/previous week. How can I do this?
You want Date.commercial:
require 'date'
now = Date.today #=> 2012-03-05
monday_next_week = Date.commercial(now.cwyear,now.cweek+1) #=> 2012-03-12
next_sunday_or_today = monday_next_week - 1 #=> 2012-03-11
Note that weeks start on Monday, so if you are on a Sunday and ask for next monday - 1 you'll get the same day.
Note also that if you don't want Mondays you can also specify the day number in the method:
thursday_next_week = Date.commercial(now.cwyear,now.cweek+1,4) #=> 2012-03-15
Calculating on a day basis is pretty simple with Date objects. If you just want to get the previous / next week from a given Date object use the following:
date = Date.today
previous_week = (date - 7).cweek
next_week = (date + 7).cweek
In ActiveSupport you have helper to convert Fixnum to time http://as.rubyonrails.org/classes/ActiveSupport/CoreExtensions/Numeric/Time.html use:
date = Date.today
week_ago = date - 1.week
next_week = date + 1.week
I have created some methods to get week number of a given date
something like this:
def self.get_week(date)
year = date.year
first_monday_of_the_year = self.get_first_monday_of_the_year(year)
# The first days of January may belong to the previous year!
if date < first_monday_of_the_year
year -= 1
first_monday_of_the_year = self.get_first_monday_of_the_year(year)
end
day_difference = date - first_monday_of_the_year
week = (day_difference / 7).to_i + 1
week
end
def self.get_monday_of_year_week(year, week)
d = self.get_first_monday_of_the_year(year)
d + ((week - 1) * 7).days
end
def self.get_first_monday_of_the_year(year)
d = Date.new(year, 1, 7) # 7 Jan is always in the first week
self.get_monday_of_week(d)
end
def self.get_monday_of_week(date)
wday = (date.wday + 6) % 7
date - wday.days
end
Assuming you mean "a given week number in the current year", you can do the following:
2.weeks.since(Time.gm(Time.now.year))
=> Fri Jan 15 00:00:00 UTC 2010
Substitute (week_number - 1) for the 1 in the above, and you'll get a date in the desired week.

How to get the number of days in a given month in Ruby, accounting for year?

I'm sure there's a good simple elegant one-liner in Ruby to give you the number of days in a given month, accounting for year, such as "February 1997". What is it?
If you're working in Rails, chances are you'll get hamstrung eventually if you switch among Time, Date, and DateTime, especially when it comes to dealing with UTC/time zones, daylight savings, and the like. My experience has been it's best to use Time, and stick with it everywhere.
So, assuming you're using Rails's Time class, there are two good options, depending on context:
If you have a month m and year y, use the class method on Time:
days = Time.days_in_month(m, y)
If you have a Time object t, cleaner to ask the day number of the last day of the month:
days = t.end_of_month.day
require 'date'
def days_in_month(year, month)
Date.new(year, month, -1).day
end
# print number of days in February 2012
puts days_in_month(2012, 2)
This is the implementation from ActiveSupport (a little adapted):
COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
def days_in_month(month, year = Time.now.year)
return 29 if month == 2 && Date.gregorian_leap?(year)
COMMON_YEAR_DAYS_IN_MONTH[month]
end
How about:
require 'date'
def days_in_month(year, month)
(Date.new(year, 12, 31) << (12-month)).day
end
# print number of days in Feburary 2009
puts days_in_month(2009, 2)
You may also want to look at Time::days_in_month in Ruby on Rails.
In Rails project for current date
Time.days_in_month(Time.now.month, Time.now.year)
For any date t which is instance of Time
Time.days_in_month(t.month, t.year)
or
t.end_of_month.day
.
If you have UTC seconds, you need to get an instance of Time first
Time.at(seconds).end_of_month.day
For a given Date object I feel like the easiest is:
Date.today.all_month.count
Use Time.days_in_month(month) where month = 1 for January, 2 for Feb, etc.
revise the input for other format
def days_in_a_month(date = "2012-02-01")
date.to_date.end_of_month.day
end
as of rails 3.2... there's a built in version:
http://api.rubyonrails.org/classes/Time.html#method-c-days_in_month
(alas, it shows up after this answer, which takes folks on a long hike)
Time.now.end_of_month.day - for current month
Date.parse("2014-07-01").end_of_month.day - use date of first day in month.
Depends on ActiveSupport
A simple way using Date:
def days_of_month(month, year)
Date.new(year, month, -1).day
end
I think it's the simplest way to get it
def get_number_of_days(date = Date.today)
Date.new(date.year, date.month, -1).mday
end
Since the time is irrelevant for this purpose then you just need a date object:
[*Date.today.all_month].size

Resources