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> _
Related
I have a string that looks like this: log/archive/2016-12-21.zip, and I need to extract the date part.
So far I have tried these solutions:
1) ["log/archive/2016-12-21.zip"].map{|i|i[/\d{4}-\d{2}-\d{2}/]}.first
2) "log/archive/2016-12-21.zip".to_date
3) "log/archive/2016-12-21.zip".split("/").last.split(".").first
Is there a better way of doing this?
You can use File.basename passing the extension:
File.basename("log/archive/2016-12-21.zip", ".zip")
# => "2016-12-21"
If you want the value to be a Date, simply use Date.parse to convert the string into a `Date.
require 'date'
Date.parse(File.basename("log/archive/2016-12-21.zip", ".zip"))
require 'date'
def pull_dates(str)
str.split(/[\/.]/).map { |s| Date.strptime(s, '%Y-%m-%d') rescue nil }.compact
end
pull_dates "log/archive/2016-12-21.zip"
#=> [#<Date: 2016-12-21 ((2457744j,0s,0n),+0s,2299161j)>]
pull_dates "log/2016-12-21/archive.zip"
#=> [#<Date: 2016-12-21 ((2457744j,0s,0n),+0s,2299161j)>]
pull_dates "log/2016-12-21/2016-12-22.zip"
#=> [#<Date: 2016-12-21 ((2457744j,0s,0n),+0s,2299161j)>,
# #<Date: 2016-12-22 ((2457745j,0s,0n),+0s,2299161j)>]
pull_dates "log/2016-12-21/2016-12-32.zip"
#=> [#<Date: 2016-12-21 ((2457744j,0s,0n),+0s,2299161j)>]
pull_dates "log/archive/2016A-12-21.zip"
#=> []
pull_dates "log/archive/2016/12/21.zip"
#=> []
If you just want the date string, rather than the date object, change the method as follows.
def pull_dates(str)
str.split(/[\/.]/).
each_with_object([]) { |s,a|
a << s if (Date.strptime(s, '%Y-%m-%d') rescue nil)}
end
pull_dates "log/archive/2016-12-21.zip"
#=> ["2016-12-21"]
This regex should cover most cases. It allows an optional non-digit between year, month and day :
require 'date'
def extract_date(filename)
if filename =~ /((?:19|20)\d{2})\D?(\d{2})\D?(\d{2})/ then
year, month, day = $1.to_i, $2.to_i, $3.to_i
# Do something with year, month, day, or just leave it like this to return an array : [2016, 12, 21]
# Date.new(year, month, day)
end
end
p extract_date("log/archive/2016-12-21.zip")
p extract_date("log/archive/2016.12.21.zip")
p extract_date("log/archive/2016:12:21.zip")
p extract_date("log/archive/2016_12_21.zip")
p extract_date("log/archive/20161221.zip")
p extract_date("log/archive/2016/12/21.zip")
p extract_date("log/archive/2016/12/21")
#=> Every example returns [2016, 12, 21]
Please try this
"log/archive/2016-12-21.zip".scan(/\d{4}-\d{2}-\d{2}/).pop
=> "2016-12-21"
If the date format is invalid, it will return nil.
Example:-
"log/archive/20-12-21.zip".scan(/\d{4}-\d{2}-\d{2}/).pop
^^
=> nil
Hope it helps.
I need to be able to loop through all days of the previous month and do something with the parsed day. This is what I have so far:
def dateTest
d = Date.parse(Time.now.to_s)
from_date = Date.new(d.year, d.month - 1, 1)
to_date = Date.new(d.year, d.month, -1)
from_date..to_date.each do |day|
#Do stuff with day
end
end
My issue is I can't seem to get to_date to equal the last day of the previous month. I've read many similar questions but most of them seem more geared towards Rails, which won't help me in this case.
require 'date'
d = Date.today << 1 # << 1 is one month earlier
(Date.new(d.year, d.month, 1)..Date.new(d.year, d.month,-1)).each{|date| p date}
You also can use ... exclude the end value.
For example:
(1..5).to_a
=> [1, 2, 3, 4, 5]
(1...5).to_a
=> [1, 2, 3, 4]
so you can write like this:
if d.month != 1
from_date = Date.new(d.year, d.month - 1, 1)
else
from_date = Date.new(d.year-1, -1)
end
to_date = Date.new(d.year, d.month)
(from_date...to_date).map { |day| puts day}
output:
2015-04-01
2015-04-02
2015-04-03
......
2015-04-29
2015-04-30
I would do it like this:
require 'date'
d = Date.today << 1
start = Date.new(d.year, d.month)
(start...(start >> 1)).each { |d| p d }
#-> #<Date: 2015-04-01 ((2457114j,0s,0n),+0s,2299161j)>
# #<Date: 2015-04-02 ((2457115j,0s,0n),+0s,2299161j)>
# ...
# #<Date: 2015-04-30 ((2457143j,0s,0n),+0s,2299161j)>
The prev_day function can also be used:
require 'date'
d = Date.today
from_date = Date.new(d.year, d.month - 1, 1)
to_date = Date.new(d.year, d.month, 1).prev_day
from_date..to_date.each do |day|
#Do stuff with day
end
How come this does not work? The CSV is there and has values, and I have 'require "csv" and time at the top, so good there. The problem seems to be with csv.each actually doing anything.
It returns
=> [] is the most common registration hour
=> [] is the most common registration day (Sunday being 0, Mon => 1 ... Sat => 7)
If there is any more info I can provide, please let me know.
#x = CSV.open \
'event_attendees.csv', headers: true, header_converters: :symbol
def time_target
y = []
#x.each do |line|
if line[:regdate].to_s.length > 0
y << DateTime.strptime(line[:regdate], "%m/%d/%y %H:%M").hour
y = y.sort_by {|i| grep(i).length }.last
end
end
puts "#{y} is the most common registration hour"
y = []
#x.each do |line|
if line[:regdate].to_s.length > 0
y << DateTime.strptime(line[:regdate], "%m/%d/%y %H:%M").wday
y = y.sort_by {|i| grep(i).length }.last
end
end
puts "#{y} is the most common registration day \
(Sunday being 0, Mon => 1 ... Sat => 7)"
end
making all the 'y's '#y's has not fixed it.
Here is sample from the CSV I'm using:
,RegDate,first_Name,last_Name,Email_Address,HomePhone,Street,City,State,Zipcode
1,11/12/08
10:47,Allison,Nguyen,arannon#jumpstartlab.com,6154385000,3155 19th St
NW,Washington,DC,20010
2,11/12/08
13:23,SArah,Hankins,pinalevitsky#jumpstartlab.com,414-520-5000,2022
15th Street NW,Washington,DC,20009
3,11/12/08 13:30,Sarah,Xx,lqrm4462#jumpstartlab.com,(941)979-2000,4175
3rd Street North,Saint Petersburg,FL,33703
Try this to load your data:
def database_load(arg='event_attendees.csv')
#contents = CSV.open(arg, headers: true, header_converters: :symbol)
#people = []
#contents.each do |row|
person = {}
person["id"] = row[0]
person["regdate"] = row[:regdate]
person["first_name"] = row[:first_name].downcase.capitalize
person["last_name"] = row[:last_name].downcase.capitalize
person["email_address"] = row[:email_address]
person["homephone"] = PhoneNumber.new(row[:homephone].to_s)
person["street"] = row[:street]
person["city"] = City.new(row[:city]).clean
person["state"] = row[:state]
person["zipcode"] = Zipcode.new(row[:zipcode]).clean
#people << person
end
puts "Loaded #{#people.count} Records from file: '#{arg}'..."
end
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`.
In order to implement auto-vivification of Ruby hash, one can employ the following class
class AutoHash < Hash
def initialize(*args)
super()
#update, #update_index = args[0][:update], args[0][:update_key] unless
args.empty?
end
def [](k)
if self.has_key?k
super(k)
else
AutoHash.new(:update => self, :update_key => k)
end
end
def []=(k, v)
#update[#update_index] = self if #update and #update_index
super
end
def few(n=0)
Array.new(n) { AutoHash.new }
end
end
This class allows to do the following things
a = AutoHash.new
a[:a][:b] = 1
p a[:c] # => {} # key :c has not been created
p a # => {:a=>{:b=>1}} # note, that it does not have key :c
a,b,c = AutoHash.new.few 3
b[:d] = 1
p [a,b,c] # => [{}, {:d=>1}, {}] # hashes are independent
There is a bit more advanced definition of this class proposed by Joshua, which is a bit hard for me to understand.
Problem
There is one situation, where I think the new class can be improved. The following code fails with the error message NoMethodError: undefined method '+' for {}:AutoHash
a = AutoHash.new
5.times { a[:sum] += 10 }
What would you do to handle it? Can one define []+= operator?
Related questions
Is auto-initialization of multi-dimensional hash array possible in Ruby, as it is in PHP?
Multiple initialization of auto-vivifying hashes using a new operator in Ruby
ruby hash initialization r
still open: How to create an operator for deep copy/cloning of objects in Ruby?
There is no way to define a []+= method in ruby. What happens when you type
x[y] += z
is
x[y] = x[y] + z
so both the [] and []= methods are called on x (and + is called on x[y], which in this case is an AutoHash). I think that the best way to handle this problem would be to define a + method on AutoHash, which will just return it's argument. This will make AutoHash.new[:x] += y work for just about any type of y, because the "empty" version of y.class ('' for strings, 0 for numbers, ...) plus y will almost always equal y.
class AutoHash
def +(x); x; end
end
Adding that method will make both of these work:
# Numbers:
a = AutoHash.new
5.times { a[:sum] += 10 }
a[:sum] #=> 50
# Strings:
a = AutoHash.new
5.times { a[:sum] += 'a string ' }
a[:sum] #=> "a string a string a string a string a string "
And by the way, here is a cleaner version of your code:
class AutoHash < Hash
def initialize(args={})
super
#update, #update_index = args[:update], args[:update_key]
end
def [](k)
if has_key? k
super(k)
else
AutoHash.new :update => self, :update_key => k
end
end
def []=(k, v)
#update[#update_index] = self if #update and #update_index
super
end
def +(x); x; end
def self.few(n)
Array.new(n) { AutoHash.new }
end
end
:)
What I think you want is this:
hash = Hash.new { |h, k| h[k] = 0 }
hash['foo'] += 3
# => 3
That will return 3, then 6, etc. without an error, because the the new value is default assigned 0.
require 'xkeys' # on rubygems.org
a = {}.extend XKeys::Hash
a[:a, :b] = 1
p a[:c] # => nil (key :c has not been created)
p a # => { :a => { :b => 1 } }
a.clear
5.times { a[:sum, :else => 0] += 10 }
p a # => { :sum => 50 }