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

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

Related

Iterating over big arrays with limited memory and time of execution

Iā€™m having trouble using Ruby to pass some tests that make the array too big and return an error.
Solution.rb: failed to allocate memory (NoMemoryError)
I have failed to pass it twice.
The problem is about scheduling meetings. The method receives two parameters in order: a matrix with all the first days that investors can meet in the company, and a matrix with all the last days.
For example:
firstDay = [1,5,10]
lastDay = [4,10,10]
This shows that the first investor will be able to find himself between the days 1..4, the second between the days 5..10 and the last one in 10..10.
I need to return the largest number of investors that the company will serve. In this case, all of them can be attended to, the first one on day 1, the second one on day 5, and the last one on day 10.
So far, the code works normally, but with some hidden tests with at least 1000 investors, the error I mentioned earlier appears.
Is there a best practice in Ruby to handle this?
My current code is:
def countMeetings(firstDay, lastDay)
GC::Profiler.enable
GC::Profiler.clear
first = firstDay.sort.first
last = lastDay.sort.last
available = []
#Construct the available days for meetings
firstDay.each_with_index do |d, i|
available.push((firstDay[i]..lastDay[i]).to_a)
end
available = available.flatten.uniq.sort
investors = {}
attended_day = []
attended_investor = []
#Construct a list of investor based in their first and last days
firstDay.each_index do |i|
investors[i+1] = (firstDay[i]..lastDay[i]).to_a
end
for day in available
investors.each do |key, value|
next if attended_investor.include?(key)
if value.include?(day)
next if attended_day.include?(day)
attended_day.push(day)
attended_investor.push(key)
end
end
end
attended_investor.size
end
Using Lazy as far as I could understand, I escaped the MemoryError, but I started receiving a runtime error:
Your code was not executed on time. Allowed time: 10s
And my code look like this:
def countMeetings(firstDay, lastDay)
loop_size = firstDay.size
first = firstDay.sort.first
last = lastDay.sort.last
daily_attendance = {}
(first..last).each do |day|
for ind in 0...loop_size
(firstDay[ind]..lastDay[ind]).lazy.each do |investor_day|
next if daily_attendance.has_value?(ind)
if investor_day == day
daily_attendance[day] = ind
end
end
end
end
daily_attendance.size
end
And it went through the cases with few investors. I thought about using multi-thread and the code became the following:
def countMeetings(firstDay, lastDay)
loop_size = firstDay.size
first = firstDay.sort.first
last = lastDay.sort.last
threads = []
daily_attendance = {}
(first..last).lazy.each_slice(25000) do |slice|
slice.each do |day|
threads << Thread.new do
for ind in 0...loop_size
(firstDay[ind]..lastDay[ind]).lazy.each do |investor_day|
next if daily_attendance.has_value?(ind)
if investor_day == day
daily_attendance[day] = ind
end
end
end
end
end
end
threads.each{|t| t.join}
daily_attendance.size
end
Unfortunately, it went back to the MemoryError.
This can be done without consuming any more memory than the range of days. The key is to avoid Arrays and keep things as Enumerators as much as possible.
First, rather than the awkward pair of Arrays that need to be converted into Ranges, pass in an Enumerable of Ranges. This both simplifies the method, and it allows it to be Lazy if the list of ranges is very large. It could be read from a file, fetched from a database or an API, or generated by another lazy enumerator. This saves you from requiring big arrays.
Here's an example using an Array of Ranges.
p count_meetings([(1..4), (5..10), (10..10)])
Or to demonstrate transforming your firstDay and lastDay Arrays into a lazy Enumerable of Ranges...
firstDays = [1,5,10]
lastDays = [4,10,10]
p count_meetings(
firstDays.lazy.zip(lastDays).map { |first,last|
(first..last)
}
)
firstDays.lazy makes everything that comes after lazy. .zip(lastDays) iterates through both Arrays in pairs: [1,4], [5,10], and [10,10]. Then we turn them into Ranges. Because it's lazy it will only map them as needed. This avoids making another big Array.
Now that's fixed, all we need to do is iterate over each Range and increment their attendance for the day.
def count_meetings(attendee_ranges)
# Make a Hash whose default values are 0.
daily_attendance = Hash.new(0)
# For each attendee
attendee_ranges.each { |range|
# For each day they will attend, add one to the attendance for that day.
range.each { |day| daily_attendance[day] += 1 }
}
# Get the day/attendance pair with the maximum value, and only return the value.
daily_attendance.max[1]
end
Memory growth is limited to how big the day range is. If the earliest attendee is on day 1 and the last is on day 1000 daily_attendance is just 1000 entries which is a long time for a conference.
And since you've built the whole Hash anyway, why waste it? Write one function that returns the full attendance, and another that extracts the max.
def count_meeting_attendance(attendee_ranges)
daily_attendance = Hash.new(0)
attendee_ranges.each { |range|
range.each { |day| daily_attendance[day] += 1 }
}
return daily_attendance
end
def max_meeting_attendance(*args)
count_meeting_attendance(*args).max[1]
end
Since this is an exercise and you're stuck with the wonky arguments, we can do the same trick and lazily zip firstDays and lastDays together and turn them into Ranges.
def count_meeting_attendance(firstDays, lastDays)
attendee_ranges = firstDays.lazy.zip(lastDays).map { |first,last|
(first..last)
}
daily_attendance = Hash.new(0)
attendee_ranges.each { |range|
range.each { |day| daily_attendance[day] += 1 }
}
return daily_attendance
end

conditional statements to only accept integer as an input Ruby

I wrote a code where it asks multiple individuals questions, and every individuals' response is put into a hash. This is in a loop and looks something like this
arr:[]
(1..n).each do |i|
hash=Hash.new()
puts "Please input a value for day # #{i}"
hash["day1"]=gets.chomp.to_f
puts "Please input a value for day # #{i}"
hash["day2"]=gets.chomp.to_f
arr << hash
end
So to avoid any incorrect input (i.e. entering string instead of an integer/number), I need to place a conditional statement.
I am super lost with how I would do that though since I am assigning the users' input into a hash at the same time I take their input.
Is it even possible to do that or should I take a different route completely.
thanks
You can get day values like below. When a character other than a number is entered, it asks the value of that day again.
puts 'Enter number of days'
days_hash = {}
number_of_days = gets.chomp.to_i
day = 1
while day <= number_of_days
puts "Please enter a value for day # #{day}"
input = gets.chomp
if input !~ /\D/
days_hash["day#{day}"] = input.to_i
day += 1
else
puts "Please enter only number"
next
end
end
p days_hash
#=> { "day1" => 2, "day2" => 4, "day3" => 8 }
days_hash['day2']
#=> 4
Maybe you can consider this general approach for validating user input, note that this example require 'date'.
So, you can check user input to be an integer or a float or a formatted date or anything you could add...
First define an array of questions containing the question text, validation method and way to convert the input, like this:
questions = [
{text: "Enter an integer:", validate: :must_be_an_integer, convert: :my_to_i},
{text: "Enter a float:", validate: :must_be_a_float, convert: :my_to_f},
{text: "Enter a data as d-m-yyyy", validate: :must_be_a_formatted_date, convert: :to_formatted_date}
]
Then define some methods to be called by :validate key, for user input validation:
def must_be_an_integer(num)
Integer(num).class == Integer rescue false
end
def must_be_a_float(num)
Float(num).class == Float && num.include?('.') rescue false
end
def must_be_a_formatted_date(date_str)
date = Date.strptime(date_str, "%d-%m-%Y") rescue false
return false unless date
date.year <= 9999 && date.year >= 1000
end
Define also the methods required by the key :convert (need to pass an argument, that's why my_to_i and my_to_f):
def my_to_i(num)
num.to_i
end
def my_to_f(num)
num.to_f
end
def to_formatted_date(date_str)
DateTime.strptime(date_str, "%d-%m-%y")
end
Finally iterate over the questions:
res = questions.map do |question|
answer = nil
3.times do
puts question[:text]
u_input = gets.chomp
if send question[:validate], u_input
answer = send question[:convert], u_input
break
end
end
if answer.nil?
puts "C'mon man! Check your keyboard!" # after three input errors!!
break
end
{ question: question[:text], answer: answer }
end
Example of a result:
#=> [
{:question=>"Enter an integer:", :answer=>1},
{:question=>"Enter a float:", :answer=>1.1},
{:question=>"Enter a data as d-m-Y", :answer=>#<DateTime: 2020-10-27T00:00:00+00:00 ((2459150j,0s,0n),+0s,2299161j)>}
]

How to get values from hash if I know keys of another hash

I stucked on a trouble when I have instance method and a hash and I want to use attributes of method to iterate over that hash to retrieve value.
For example I have method:
class M
def initialize(time, line)
#time = YAML.load_file(time)
#line = YAML.load_file(line)
end
def sum(from_station:, to_station:)
array = #time['timing']
lines = #line['lines']
line = #line['stations']
from_station_line = line[from_station.to_s]
to_station_line = line[to_station.to_s]
str = from_station
stop = to_station
result = []
result2 = []
result3 = []
time = 0
if from_station_line[0] == to_station_line[0]
loop do
tmp = array.find{|x| x['start'] == str}
break unless tmp
result << tmp
str = tmp['end']
time = result.sum{|b| b['time']}
break if str == stop
end
puts time
else
case array
end
p time, result2
end
end
end
a = M.new("./config/time.yml", "./config/config.yml")
a.sum(from_station: :tokyo, to_station: :milan)
and config.yml stations:
lines:
- :black
- :red
- :yellow
stations:
tokyo:
- :black
chicago:
- :black
amster:
- :black
- :red
prague:
- :black
milan:
- :black
- :red
bayern:
- :black
- :yellow
And here is time.yml
timing:
-
start: :tokyo
end: :chicago
time: 6
price: 3
-
start: :chicago
end: :amster
time: 4
price: 2
-
start: :amster
end: :prague
time: 3.5
price: 3
-
start: :bayern
end: :milan
time: 3.5
price: 3
-
start: :milan
end: :roma
time: 3.5
price: 3
-
And I need to select if from_station: and to_station: on the same branch (black or red or both). Could i make it?
Another words: if user choose to move from station ":tokyo" to station ":milan" I need to know if these two stations on one line (:black, red or yellow). So to know that I need to manage config.yml file and iterate if line of ":tokyo" [black] == line of ":milan [yellow]
Usually in Ruby you won't use loop and break statements. The Enumerable module will do most of that stuff for you. It might take a bit to get used to it, but then it will become naturally and allow you to quickly to lots of stuff. Note that other programming languages employ similar concepts.
In
a = line.select {|k,v| k.to_sym == from_station} #how to implement this condition?
b = line.select {|k,v| k.to_sym == to_station}#
you do typical lookups in a Hash, which can be done by
def sum(from_station, to_station)
from_station_colors = #lines[from_station]
to_station_colors = #lines[to_station]
if (from_station_colors.nil? || from_station_colors.empty?)
# from_station not found
end
# ...
end
The lookup in timing is a bit more complicated:
require 'yaml'
timings = YAML::load_file('timing.yml')['timing']
from_station = :tokyo
to_station = :chicago
# "t" (a timing entry) might be nil, thus the first select
# Note that there are other handy functions to make this more readable
# (left as an exercise)
transit = timings.select{|t| t}.select do |timing|
# if the following line evaluates to "true", select this entry
timing['start'] == from_station && timing['end'] == to_station
end
puts transit # {"start"=>:tokyo, "end"=>:chicago, "time"=>6, "price"=>3}
Your other question (find out if the locations share the same 'color') should probably be asked separately. Somehow, this task sounds like a homework assignment.
And welcome at StackOverflow! Please consider to be a as precise as possible with your next question(s) - while reducing the examples to a "good" minimum.

Ruby: day selection program (select next available day from a list)

I'm tryind to build a helper to select the next available day from a list.
I have a list of days as a reference (those are the day where I want something to happen)
class_list = ["Monday","Saturday","Sunday"]
I need to check the current day and match it in the list.
If it's part of the list I select it, if it isn't I select the next one from the list.
this is what i have so far:
#select current day, get its name value and weekday number value
today = Time.now
today_name = today.strftime("%A")
#not sure which of the 2 following line is better
#today_index = DateTime.parse(today_name).wday
today_index = today.strftime("%u").to_i
Then I do the matching
if class_list.include? today_name
#victory!!!
puts today_name
else
puts "find next day"
class_list.each do |x|
if DateTime.parse(x).wday > today_index
puts "result #{x}"
break
end
end
end
When I run it seems to work fine, but as i'm just learning Ruby i'm always wondering if i'm not overcomplicating things.
Does this code looks alright to you Ruby masters?
require 'date'
def next_date_from(ar)
cur_day = Date.today
cur_day += 1 until ar.include?(cur_day.strftime('%A'))
cur_day
end
puts next_date_from(%w(Monday Saturday Sunday))
#=>2011-10-01
I would better have a map linking a given day to the following one and a default value if the day is not found:
days = {:Monday => :Tuesday, :Tuesday => :Wednesday ...}
days.default = :Monday
When you do days[:Monday] you get :Tuesday
when you try to get a non existing entry, you get the default.
For the part:
if class_list.include? today_name
#victory!!!
puts today_name
else
puts "find next day"
class_list.each do |x|
if DateTime.parse(x).wday > today_index
puts "result #{x}"
break
end
end
end
You could write it like this:
if class_list.include? today_name
#victory!!!
puts today_name
else
puts "find next day"
result = class_list.find {|e| DateTime.parse(e).wday > today_index }
puts "result = #{result}"
end

how to convert 270921sec into days + hours + minutes + sec ? (ruby)

I have a number of seconds. Let's say 270921. How can I display that number saying it is xx days, yy hours, zz minutes, ww seconds?
It can be done pretty concisely using divmod:
t = 270921
mm, ss = t.divmod(60) #=> [4515, 21]
hh, mm = mm.divmod(60) #=> [75, 15]
dd, hh = hh.divmod(24) #=> [3, 3]
puts "%d days, %d hours, %d minutes and %d seconds" % [dd, hh, mm, ss]
#=> 3 days, 3 hours, 15 minutes and 21 seconds
You could probably DRY it further by getting creative with collect, or maybe inject, but when the core logic is three lines it may be overkill.
I was hoping there would be an easier way than using divmod, but this is the most DRY and reusable way I found to do it:
def seconds_to_units(seconds)
'%d days, %d hours, %d minutes, %d seconds' %
# the .reverse lets us put the larger units first for readability
[24,60,60].reverse.inject([seconds]) {|result, unitsize|
result[0,0] = result.shift.divmod(unitsize)
result
}
end
The method is easily adjusted by changing the format string and the first inline array (ie the [24,60,60]).
Enhanced version
class TieredUnitFormatter
# if you set this, '%d' must appear as many times as there are units
attr_accessor :format_string
def initialize(unit_names=%w(days hours minutes seconds), conversion_factors=[24, 60, 60])
#unit_names = unit_names
#factors = conversion_factors
#format_string = unit_names.map {|name| "%d #{name}" }.join(', ')
# the .reverse helps us iterate more effectively
#reversed_factors = #factors.reverse
end
# e.g. seconds
def format(smallest_unit_amount)
parts = split(smallest_unit_amount)
#format_string % parts
end
def split(smallest_unit_amount)
# go from smallest to largest unit
#reversed_factors.inject([smallest_unit_amount]) {|result, unitsize|
# Remove the most significant item (left side), convert it, then
# add the 2-element array to the left side of the result.
result[0,0] = result.shift.divmod(unitsize)
result
}
end
end
Examples:
fmt = TieredUnitFormatter.new
fmt.format(270921) # => "3 days, 3 hours, 15 minutes, 21 seconds"
fmt = TieredUnitFormatter.new(%w(minutes seconds), [60])
fmt.format(5454) # => "90 minutes, 54 seconds"
fmt.format_string = '%d:%d'
fmt.format(5454) # => "90:54"
Note that format_string won't let you change the order of the parts (it's always the most significant value to least). For finer grained control, you can use split and manipulate the values yourself.
Needed a break. Golfed this up:
s = 270921
dhms = [60,60,24].reduce([s]) { |m,o| m.unshift(m.shift.divmod(o)).flatten }
# => [3, 3, 15, 21]
Rails has an helper which converts distance of time in words.
You can look its implementation: distance_of_time_in_words
If you're using Rails, there is an easy way if you don't need the precision:
time_ago_in_words 270921.seconds.from_now
# => 3 days
You can use the simplest method I found for this problem:
def formatted_duration total_seconds
hours = total_seconds / (60 * 60)
minutes = (total_seconds / 60) % 60
seconds = total_seconds % 60
"#{ hours } h #{ minutes } m #{ seconds } s"
end
You can always adjust returned value to your needs.
2.2.2 :062 > formatted_duration 3661
=> "1 h 1 m 1 s"
I modified the answer given by #Mike to add dynamic formatting based on the size of the result
def formatted_duration(total_seconds)
dhms = [60, 60, 24].reduce([total_seconds]) { |m,o| m.unshift(m.shift.divmod(o)).flatten }
return "%d days %d hours %d minutes %d seconds" % dhms unless dhms[0].zero?
return "%d hours %d minutes %d seconds" % dhms[1..3] unless dhms[1].zero?
return "%d minutes %d seconds" % dhms[2..3] unless dhms[2].zero?
"%d seconds" % dhms[3]
end
I just start writing ruby. i guess this is only for 1.9.3
def dateBeautify(t)
cute_date=Array.new
tables=[ ["day", 24*60*60], ["hour", 60*60], ["minute", 60], ["sec", 1] ]
tables.each do |unit, value|
o = t.divmod(value)
p_unit = o[0] > 1 ? unit.pluralize : unit
cute_date.push("#{o[0]} #{unit}") unless o[0] == 0
t = o[1]
end
return cute_date.join(', ')
end
Number of days = 270921/86400 (Number of seconds in day) = 3 days this is the absolute number
seconds remaining (t) = 270921 - 3*86400 = 11721
3.to_s + Time.at(t).utc.strftime(":%H:%M:%S")
Which will produce something like 3:03:15:21
Not a direct answer to the OP but it might help someone who lands here.
I had this string
"Sorry, you cannot change team leader in the last #{freeze_period_time} of a #{competition.kind}"
freeze_period_time resolved to 5 days inside irb, but inside the string, it resolved to time in seconds eg 47200, so the string became something ugly
"Sorry, you cannot change team leader in the last 47200 of a hackathon"
To fix it, I had to use .inspect on the freeze_period_time object.
So the following made it work
"Sorry, you cannot change team leader in the last #{freeze_period_time.inspect} of a #{competition.kind}"
Which returned the correct sentence
"Sorry, you cannot change team leader in the last 5 days of a hackathon"
TLDR
You might need time.inspect - https://www.geeksforgeeks.org/ruby-time-inspect-function/

Resources