ruby: self changes from one class to another - ruby

The code below helps calculate business hours for completing tasks. In the calculate_deadline method, in the BusinesHours Class, the two puts statements reveal this
puts self
#<BusinessHours:0x000001008cbb70>
puts self[start_time.to_date].inspect
#<TimeRange:0x000001008cbb48 #range=2010-06-10 09:00:00 -0700..2010-06-10 15:00:00 -0700>
I don't understand why putting start_time.to_date beside 'self' in calculate_deadline changes self from BusinessHours class to TimeRange class, as the two puts statements suggest, especially since the 'to_date' method is part of class Time. Can you explain how this is happening?
require 'time'
require 'date'
class Time
def to_date
Date.new(year, month, day)
end
end
class BusinessHours
def initialize(time_in, time_out)
#default_range = TimeRange.new(time_in, time_out)
#modified_days = {}
end
def update(day, time_in, time_out)
key = day.is_a?(Symbol) ? day : Date.parse(day)
#modified_days.merge!({key => TimeRange.new(time_in, time_out)})
end
def closed(*days)
days.each {|day| update(day, '0:00', '0:00')}
end
def [](date)
day_of_week = date.strftime("%a").downcase.to_sym
range = #modified_days[date] || #modified_days[day_of_week] || #default_range
# reset time range dates to match date param
range.reset_date(date)
range
end
def calculate_deadline(seconds, start_time)
start_time = Time.parse(start_time)
puts self
puts self[start_time.to_date].inspect
range = self[start_time.to_date]
if range.applies?(start_time)
start_time = [start_time, range.start].max
available_seconds = range.stop - start_time
return start_time + seconds if available_seconds > seconds
seconds -= available_seconds
end
calculate_deadline(seconds, (start_time.to_date + 1).to_s)
end
end
class TimeRange
def initialize(time_in, time_out)
#range = Time.parse(time_in)..Time.parse(time_out)
end
def reset_date(date)
#range = Time.local(date.year, date.month, date.day, start.hour, start.min)..
Time.local(date.year, date.month, date.day, stop.hour, stop.min)
end
def applies?(time)
stop > time
end
def stop
#range.end
end
def start
#range.begin
end
end
k = BusinessHours.new("9:00 AM", "3:00 PM")
k.calculate_deadline(20*60*60, "Jun 7, 2010 10:45 AM")

#default_range = TimeRange.new(time_in, time_out)
From that line, we can see that #default_range is a TimeRange instance, and that #modified_days is an empty hash (therefore #modified_days[anything] will be nil, i.e. falsey).
range = #modified_days[date] || #modified_days[day_of_week] || #default_range
Since #modified_days[anything] is falsey, range ends up being #default_range, which, as we see above, is a TimeRange object. Your [] method on BusinessHours returns the range variable, which is a TimeRange object.
Therefore, with self being a BusinessHours object, when you call the [] method (self[argument]), you will get a TimeRange object.

It does not change your self. You are inspecting a different object.
self.inspect # inspecting 'self'.
self[date].inspect # inspecting 'self[date]'
# which returns a `TimeRange` object `range`.
# Hence, you are inspecting `range`.

Related

ruby trie implementation reference issue

I am trying to implement a trie in Ruby but can't figure out what the problem is with my print + collect methods.
I just implemented the same in JS and working fine. I guess the issue could be that Ruby is passed by reference (unlike JS) and how variable assignment works in Ruby.
So if I run the code with string.clone as argument when I recursively call the collect function then I get:
["peter", "peter", "petera", "pdanny", "pdjane", "pdjanck"]
and if I pass string then:
["peterradannyjaneck", "peterradannyjaneck", "peterradannyjaneck", "peterradannyjaneck", "peterradannyjaneck", "peterradannyjaneck"]
Any ideas how to fix this?
the code:
class Node
attr_accessor :hash, :end_node, :data
def initialize
#hash = {}
#end_node = false
#data = data
end
def end_node?
end_node
end
end
class Trie
def initialize
#root = Node.new
#words = []
end
def add(input, data, node = #root)
if input.empty?
node.data = data
node.end_node = true
elsif node.hash.keys.include?(input[0])
add(input[1..-1], data, node.hash[input[0]])
else
node.hash[input[0]] = Node.new
add(input[1..-1], data, node.hash[input[0]])
end
end
def print(node = #root)
collect(node, '')
#words
end
private
def collect(node, string)
if node.hash.size > 0
for letter in node.hash.keys
string = string.concat(letter)
collect(node.hash[letter], string.clone)
end
#words << string if node.end_node?
else
string.length > 0 ? #words << string : nil
end
end
end
trie = Trie.new
trie.add('peter', date: '1988-02-26')
trie.add('petra', date: '1977-02-12')
trie.add('danny', date: '1998-04-21')
trie.add('jane', date: '1985-05-08')
trie.add('jack', date: '1994-11-04')
trie.add('pete', date: '1977-12-18')
print trie.print
Ruby's string concat mutates the string and doesn't return a new string. You may want the + operator instead. So basically change the 2 lines inside collect's for-loop as per below:
stringn = string + letter
collect(node.hash[letter], stringn)
Also, you probably want to either always initialize #words to empty in print before calling collect, or make it a local variable in print and pass it to collect.

Loop through an array, and check each item

I have an array: #costumer_request = ['regular', '12/03/2013', '14/03/2013'].
I need to verify if the first item is 'regular' or 'rewards', then verify if each date of the rest of the array is a weekend.
I did something like this:
#costumer_request.each_with_index do |item, index|
if index[0] == 'regular:'
if DateTime.parse(index).to_date.saturday? or DateTime.parse(index).to_date.sunday?
print "It's a weekend"
else
print "It's not a weekend"
end
end
end
require 'date'
module HotelReservation
class Hotel
HOTELS = {
:RIDGEWOOD => 'RidgeWood',
:LAKEWOOD => 'LakeWood',
:BRIDGEWOOD => 'BridgeWood'
}
def weekend?(date)
datetime = DateTime.parse(date)
datetime.saturday? || datetime.sunday?
end
def find_the_cheapest_hotel(text_file)
#weekends_for_regular = 0
#weekdays_for_regular = 0
#weekends_for_rewards = 0
#weekdays_for_rewards = 0
File.open(text_file).each_line do |line|
#costumer_request = line.delete!(':').split
#costumer_request = line.delete!(',').split
#Here I want to process something in each array
#but if I do something like bellow, it will
#store the result of the two arrays in the same variable
#I want to store the result of the first array, process something
#and then do another thing with the second one, and so on.
if(#costumer_request.first == 'regular')
#costumer_request[1..-1].each do |date|
if (weekend?(date))
#weekends_for_regular +=1
else
#weekdays_for_regular +=1
end
end
else
if(#costumer_request.first == 'rewards')
#costumer_request[1..-1].each do |date|
if (weekend?(date))
#weekends_for_rewards +=1
else
#weekdays_for_rewards +=1
end
end
end
end
end
end
end
end
The find_the_cheapest_hotel method should output the cheapest hotel based on the given data.
require 'time'
require 'date'
#costumer_request = ['regular', '28/03/2013', '14/03/2013']
if #costumer_request.first == 'regular'
if #costumer_request[1...-1].all?{|item| Time.local(item).saturday? || Time.local(item).sunday? }
print "It's a weekend"
else
print "It's not a weekend"
end
end
output:
It's a weekend
Here's a really clean way of doing it:
def is_weekend?(dt)
dt.saturday? || dt.sunday?
end
type, *dates = #costumer_request
if(type == 'regular')
dates.each do |date|
if(weekend?(Date.parse(date))
#do processing here
end
end
end
require 'time'
def weekend?(date)
datetime = DateTime.parse(date)
datetime.saturday? || datetime.sunday?
end
#costumer_request = ['regular', '28/03/2013', '14/03/2013']
type = #costumer_request.shift
if type == 'regular'
#costumer_request.each do |date|
if weekend?(date)
puts "#{date} a weekend"
else
puts "#{date} not a weekend"
end
end
end
You may as well add weekend? directly to DateTime:
class DateTime
def weekend?
saturday? || sunday?
end
end
Now you can convert all but the first element to DateTime objects, then check that all of them are on the weekend.
if #customer_request.first == 'regular'
dates = #customer_request[1..-1].map { |date_string| DateTime.parse(date_string) }
if dates.all?(&:weekend?)
puts 'all dates on a weekend'
else
puts 'at least one date is not on a weekend'
end
else
puts 'not a regular customer request'
end
#customer_request = ['regular', '12/03/2013', '14/03/2013']
def val
#customer_request.first == 'regular' &&
#customer_request.drop(1).inject(true) do |m, e|
wd = Time.local(*e.split('/').reverse.map(&:to_i))
m &&= (wd.saturday? || wd.sunday?)
end
end
p val
p val ? 'all are weekends' : 'something isn\'t a weekend'

Sort Array by Popularity and Time in Ruby

I am a Ruby Rails newbie.
Is there a way to know the popularity of elements in an Array over time?
For example lets say for the last 15 min..
The array has like ["abc", "ab", "abc", "a", "abc", "ab"........] being pushed into the array.. can we get "abc" and "ab" as the most popular ones.. just for the last 15 minutes?
If you take for an entire hour.. typical for the entire hour.."abcd" is the most popular.. it should return "abcd" as the most popular element in an array..
Is there a way to achieve this?
Create your own class which inherits from Array, or delegates all its functionality to an Array. For example:
class TimestampedArray
def initialize
#items = []
end
def <<(obj)
#items << [Time.now,obj]
end
# get all the items which were added in the last "seconds" seconds
# assumes that items are kept in order of add time
def all_from_last(seconds)
go_back_to = Time.now - seconds
result = []
#items.reverse_each do |(time,item)|
break if time < go_back_to
result.unshift(item)
end
result
end
end
If you have an old version of Ruby, which doesn't have reverse_each:
def all_from_last(seconds)
go_back_to = Time.now - seconds
result = []
(#items.length-1).downto(0) do |i|
time,item = #items[i]
break if time < go_back_to
result.unshift(item)
end
result
end
Then you need something to find the "most popular" item. I often use this utility function:
module Enumerable
def to_histogram
result = Hash.new(0)
each { |x| result[x] += 1 }
result
end
end
On which you could base:
module Enumerable
def most_popular
h = self.to_histogram
max_by { |x| h[x] }
end
end
So then you get:
timestamped_array.all_from_last(3600).most_popular # "most popular" in last 1 hour

How to create an infinite enumerable of Times?

I want to be able to have an object extend Enumerable in Ruby to be an infinite list of Mondays (for example).
So it would yield: March 29, April 5, April 12...... etc
How can I implement this in Ruby?
In 1.9 (and probably previous versions using backports), you can easily create enumerator:
require 'date'
def ndays_from(from, step=7)
Enumerator.new {|y|
loop {
y.yield from
from += step
}
}
end
e = ndays_from(Date.today)
p e.take(5)
#=> [#<Date: 2010-03-25 (4910561/2,0,2299161)>, #<Date: 2010-04-01 (4910575/2,0,2299161)>, #<Date: 2010-04-08 (4910589/2,0,2299161)>, #<Date: 2010-04-15 (4910603/2,0,2299161)>, #<Date: 2010-04-22 (4910617/2,0,2299161)>]
Store a Date as instance variable, initialized to a Monday. You would implement an each method which increments the stored date by 7 days using date += 7.
You could do something by extending Date...
#!/usr/bin/ruby
require 'date'
class Date
def current_monday
self - self.wday + 1
end
def next_monday
self.current_monday + 7
end
end
todays_date = Date.today
current_monday = todays_date.current_monday
3.times do |i|
puts current_monday.to_s
current_monday = current_monday.next_monday
end
2010-03-22
2010-03-29
2010-04-05
2010-04-12
...with the usual warnings about extending base classes of course.
You can extend Date class with nw method mondays
class Date
def self.mondays(start_date=Date.today, count=10)
monday = start_date.wday > 1 ? start_date - start_date.wday + 8 : start_date - start_date.wday + 1
mondays = []
count.times { |i| mondays << monday + i*7}
mondays
end
end
Date.mondays will return by default Array of mondays with 10 elements from closest monday to Date.today. You can pass parameters:
Date.mondays(start_date:Date, count:Integer)
start_date - start point to find closest monday
count - number of mondays you are looking
IE:
Date.mondays(Date.parse('11.3.2002'))
Date.mondays(Date.parse('11.3.2002'), 30)
module LazyEnumerable
extend Enumerable
def select(&block)
lazily_enumerate { |enum, value| enum.yield(value) if
block.call(value) }
end
def map(&block)
lazily_enumerate {|enum, value| enum.yield(block.call(value))}
end
def collect(&block)
map(&block)
end
private
def lazily_enumerate(&block)
Enumerator.new do |enum|
self.each do |value|
block.call(enum, value)
end
end
end
end
...........
class LazyInfiniteDays
include LazyEnumerable
attr_reader :day
def self.day_of_week
dow = { :sundays => 0, :mondays => 1, :tuesdays => 2, :wednesdays =>
3, :thursdays => 4, :fridays => 5, :saturdays => 6, :sundays => 7 }
dow.default = -10
dow
end
DAY_OF_WEEK = day_of_week()
def advance_to_midnight_of_next_specified_day(day_sym)
year = DateTime.now.year
month = DateTime.now.month
day_of_month = DateTime.now.day
output_day = DateTime.civil(year, month, day_of_month)
output_day += 1 until output_day.wday == DAY_OF_WEEK[day_sym]
output_day
end
def initialize(day_sym)
#day = advance_to_midnight_of_next_specified_day(day_sym)
end
def each
day = #day.dup
loop {
yield day
day += 7
}
end
def ==(other)
return false unless other.kind_of? LazyInfiniteDays
#day.wday == other.day.wday
end
end
Ruby 2.7 introduced Enumerator#produce for creating an infinite enumerator from any block, which results in a very elegant, very functional way of implementing the original problem:
irb(main):001:0> require 'date'
=> true
irb(main):002:0> puts Date.today
2022-09-23
=> nil
irb(main):003:0> Date.today.friday?
=> true
irb(main):004:0> future_mondays = Enumerator.produce { |date|
date = (date || Date.today).succ
date = date.succ until date.monday?
date
}
=> #<Enumerator: #<Enumerator::Producer:0x00007fa4300b3070>:each>
irb(main):005:0> puts future_mondays.first(5)
2022-09-26
2022-10-03
2022-10-10
2022-10-17
2022-10-24
=> nil
irb(main):006:0> _

What's a better way to sort by day date?

I have an Array of Events and I want to divide them into a 2-dimensional Array based on event_date (which returns a Time object). But I want it to be by the day part of the date. Here's what I have so far:
def divide_events_by_day(events)
# Sort, so we can start with the first day.
events = events.sort { |x,y| x.event_date <=> y.event_date }
days = []
current_date = events[0].event_date # A Time object.
# With meaningful hours and minutes.
while events.length > 0 do
# Find events for the current date.
day_events = events.select { |e|
(e.event_date.year == current_date.year) &&
(e.event_date.yday == current_date.yday)
}
# Move them to their subarray.
days << day_events
events = events - day_events
# Advance the current_date by one day.
current_date = (current_date + (60 * 60 * 24))
end
days
end
Any suggestions to make it simpler or faster?
For a start, you could try something like this:
def divide_events_by_day(events)
days = [[]]
events.sort_by { |x| x.event_date }.each { |e|
if days.last.empty? or days.last.event_date_date == e.event_date.date
days.last << e
else
days << [e]
end
}
days
end
It doesn't produce empty days, but other than that follows yours pretty closely.
I have a Time#date method on my machine, but I'm not seeing it in a clean distribution build (1.8.7), so you may need to add something like:
class Time
def date
to_i/(60*60*24)
end
end
This should be faster:
def divide(events)
hash = {}
events.each do |event|
key = "#{event.event_date.year} #{event.event_date.yday}"
hash[key] ||= []
hash[key] << event
end
hash.to_a.sort do|pair1,pair2|
pair1[0] <=> pair2[0]
end.map do |pair|
pair[1].sort{|x,y| x.event_date <=> y.event_date}
end
end

Resources