I would want a method that:
def time_between(from, to)
***
end
time_between 10.am.of_today, 3.pm.of_today # => 1pm Time object
time_between 10.am.of_today, 3.pm.of_today # => 3pm Time object
time_between 10.am.of_today, 3.pm.of_today # => 10:30am Time object
# I mean random
There are two questions here: how to implement ***? and how to implement x.pm.of_today?
ruby 1.9.3
require 'rubygems'
require 'active_support/all'
def random_hour(from, to)
(Date.today + rand(from..to).hour + rand(0..60).minutes).to_datetime
end
puts random_hour(10, 15)
This is a first try:
def time_between(from, to)
today = Date.today.beginning_of_day
(today + from.hours)..(today + to.hours).to_a.sample
end
Although it works like:
time_between(10, 15) # => a random time between 10 am and 3 pm
I think is enough, but I'll be open for better solutions.
To get the random time slot you will need to calculate the distance between the two times. Get a random value with that distance span. And finally add it to your from time.
Something like: (but I am not going to test it)
def time_between(from, to)
if from > to
time_between(to, from)
else
from + rand(to - from)
end
end
As for creating a DSL for time. You could look at how Rails does it. But to get something like what you are wanting. Just create a class that represents the hours in the day. Instantiate it with the am or pm call on a Fixnum. Then write the methods for of_today (and any others that you would want).
class Fixnum
def am
TimeWriter.new(self)
end
def pm
TimeWriter.new(self + 12)
end
end
class TimeWriter
MINUTES_IN_HOUR = 60
SECONDS_IN_MINUTE = 60
SECONDS_IN_HOUR = MINUTES_IN_HOUR * SECONDS_IN_MINUTE
def initialize hours
#hours = hours
end
def of_today
start_of_today + (hours * SECONDS_IN_HOUR)
end
private
attr_reader :hours
def start_of_today
now = Time.now
Time.new(now.year, now.month, now.day, 0, 0)
end
end
You should add some error handling for hours more than 24.
This code respects minutes and hours as input.
require 'rubygems'
require 'active_support/all'
def random_time(from, to)
from_arr = from.split(':')
to_arr = to.split(':')
now = Time.now
rand(Time.new(now.year, now.month, now.day, from_arr[0], rom_arr[1])..Time.new(now.year, now.month, now.day, to_arr[0], to_arr[1]))
end
puts random_time('09:15', '18:45')
Another short way to do the same:
require 'rubygems'
require 'active_support/all'
def random_time(from, to)
now = Time.now
rand(Time.parse(now.strftime("%Y-%m-%dT#{from}:00%z"))..Time.parse(now.strftime("%Y-%m-%dT#{to}:00%z")))
end
puts random_time('09:15', '18:45')
Related
I have to create a list of 24 months with the same day amongst them, properly handling the months that do not have day 29, 30 or 31.
What I currently do is:
def dates_list(first_month, assigned_day)
(0...24).map do |period|
begin
(first_month + period.months).change(day: assigned_day)
rescue ArgumentError
(first_month + period.months).end_of_month
end
end
end
I need to rescue from ArgumentError as some cases raise it:
Date.parse('10-Feb-2019').change(day: 30)
# => ArgumentError: invalid date
I am looking for a safe and elegant solution that might already exist in ruby or rails. Something like:
Date.parse('10-Feb-2019').safe_change(day: 30) # => 28-Feb-2019
So I can write:
def dates_list(first_month, assigned_day)
(0...24).map do |period|
(first_month + period.months).safe_change(day: assigned_day)
end
end
Does that exist or I would need to monkey patch Date?
Workarounds (like a method that already creates this list) are very welcome.
UPDATE
The discussion about what to do with negative and 0 days made me realize this function is trying to guess the user's intent. And it also hard codes how many months to generate, and to generate by month.
This got me thinking what is this method doing? It's generates a list of advancing months, of a fixed size, and modifying them in a fixed way, and guessing what the user wants. If your function description includes "and" you probably need multiple functions. We separate generating the list of dates from modifying the list. We replace the hard coded parts with parameters. And instead of guessing what the user wants, we let them tell us with a block.
def date_generator(from, by:, how_many:)
(0...how_many).map do |period|
date = from + period.send(by)
yield date
end
end
The user can be very explicit about what they want to change. No surprises for the user nor the reader.
p date_generator(Date.parse('2019-02-01'), by: :month, how_many: 24) { |month|
month.change(day: month.end_of_month.day)
}
We can take this a step further by turning it into an Enumerator. Then you can have as many as you like and do whatever you like with them using normal Enumerable methods..
INFINITY = 1.0/0.0
def date_iterator(from, by:)
Enumerator.new do |block|
(0..INFINITY).each do |period|
date = from + period.send(by)
block << date
end
end
end
p date_iterator(Date.parse('2019-02-01'), by: :month)
.take(24).map { |date|
date.change(day: date.end_of_month.day)
}
Now you can generate any list of dates, iterating by any field, of any length, with any changes. Rather than being hidden in a method, what's happening is very explicit to the reader. And if you have a special, common case you an wrap this in a method.
And the final step would be to make it a Date method.
class Date
INFINITY = 1.0/0.0
def iterator(by:)
Enumerator.new do |block|
(0..INFINITY).each do |period|
date = self + period.send(by)
block << date
end
end
end
end
Date.parse('2019-02-01')
.iterator(by: :month)
.take(24).map { |date|
date.change(day: date.end_of_month.day)
}
And if you have a special, common case, you can write a special case function for it, give it a descriptive name, and document its special behaviors.
def next_two_years_of_months(date, day:)
if day <= 0
raise ArgumentError, "The day must be positive"
end
date.iterator(by: :month)
.take(24)
.map { |next_date|
next_date.change(day: [day, next_date.end_of_month.day].min)
}
end
PREVIOUS ANSWER
My first refactoring would be to remove the redundant code.
require 'date'
def dates_list(first_month, assigned_day)
(0...24).map do |period|
next_month = first_month + period.months
begin
next_month.change(day: assigned_day)
rescue ArgumentError
next_month.end_of_month
end
end
end
At this point, imo, the function is fine. It's clear what's happening. But you can take it a step further.
def dates_list(first_month, assigned_day)
(0...24).map do |period|
next_month = first_month + period.months
day = [assigned_day, next_month.end_of_month.day].min
next_month.change(day: day)
end
end
I think that's marginally better. It makes the decision a little more explicit and doesn't paper over other possible argument errors.
If you find yourself doing this a lot, you could add it as a Date method.
class Date
def change_day(day)
change(day: [day, end_of_month.day].min)
end
end
I'm not so hot on either change_day nor safe_change. Neither really says "this will use the day or if it's out of bounds the last day of the month" and I'm not sure how to express that.
I would like to calculate the ending date from the starting date.
I have the number of workings days (so without sunday and saturday).
This is my function :
def ending_date(start_date, number_of_working_days)
date = 0
ending_date = start_date
while date > number_of_working_days
next if ending_date.sunday? || ending_date.saturday?
finish = ending_date + 1.day unless date.saturday? or date.sunday?
date += 1
end
finish
end
And I get nil. I don't know why.
If somedoby can help me with this please :)
Enumerator seems to do the job, and it can give you much more information than just a date :
require 'date'
def future_working_days(start_date = Date.today)
date = start_date
Enumerator.new do |yielder|
loop do
date += 1 # NOTE: Should start_date be included?
yielder << date unless date.saturday? || date.sunday?
end
end
end
def ending_date(number_of_working_days, start_date = Date.today)
future_working_days(start_date).find.with_index(1) { |_, i| i == number_of_working_days }
end
# Show the next 10 working days :
puts future_working_days.take(10).map(&:to_s).join('->') # 2016-12-08->2016-12-09->2016-12-12->2016-12-13->2016-12-14->2016-12-15->2016-12-16->2016-12-19->2016-12-20->2016-12-21
# Date after 5 working days :
puts ending_date(5) #=> 2016-12-14
# How many working days until the next Friday the 13th of a leap year
puts future_working_days.find_index{|date| date.friday? && date.day == 13 && Date.leap?(date.year)} #=> 851
Notes :
Parameters of ending_date have been switched to allow default date for start_date
future_working_days is an infinite Enumerator. Don't try future_working_days.to_a! Lazy could be helpful.
Should today be included in future_working_days?
It's also possible to calculate the ending_date directly, without any loop. Every 5 working days comes a week-end! I wrote the code for a Sunday as a start_date, and modified it with start_date.wday for the other days of the week. Saturday is a special case.
def ending_date(number_of_working_days, start_date = Date.today)
if start_date.wday == 6 # Saturday is a special case
start_date + number_of_working_days + (number_of_working_days-1)/5*2 + 1
else
start_date + number_of_working_days + (number_of_working_days-1+start_date.wday)/5*2
end
end
I tested it for many start_dates and many number_of_working_days, and it seems to work fine.
I couldn't find a way to remove the if statement, though.
EDIT: If you work with ActiveSupport::TimeWithZone objects, just call to_date before calling ending_date :
time = Time.zone.local(2016, 10, 15, 15, 30, 45)
puts time.class #=> ActiveSupport::TimeWithZone
puts ending_date(5, time.to_date) #=> 2016-10-21
With minimum changes it should looks like:
def ending_date(start_date, number_of_working_days)
date = 0
while date < number_of_working_days
start_date += 1
unless start_date.saturday? || start_date.sunday?
finish = start_date
date += 1
end
end
finish
end
#> ending_date(Date.today, 5)
#=> #<Date: 2016-12-14 ((2457737j,0s,0n),+0s,2299161j)>
But I prefer to write this method like something this:
def ending_date(date, number_of_working_days)
result = []
while result.size < number_of_working_days
date += 1
unless [6,0].include? date.wday
result << date
end
end
result
end
# To take all the working dates
#> ending_date(Date.today, 5)
#=> [#<Date: 2016-12-08 ((2457731j,0s,0n),+0s,2299161j)>, #<Date: 2016-12-09 ((2457732j,0s,0n),+0s,2299161j)>, #<Date: 2016-12-12 ((2457735j,0s,0n),+0s,2299161j)>, #<Date: 2016-12-13 ((2457736j,0s,0n),+0s,2299161j)>, #<Date: 2016-12-14 ((2457737j,0s,0n),+0s,2299161j)>]
# To take last working date
#> ending_date(Date.today, 5).last
#=> #<Date: 2016-12-14 ((2457737j,0s,0n),+0s,2299161j)>
i am trying to work out how to write a rake tasks that will run daily and find where the days remaining is 0 to update the column amount to zero.
I have the following methods defined in my model, though they don't exactly appear to be working as I am getting the following error in the view
undefined method `-#' for Mon, 27 Jun 2016:Date
def remaining_days
expired? ? 0 : (self.expire_at - Date.today).to_i
end
def expired?
(self.expire_at - Date.today).to_i <= 0
end
def expire_credits
if expired?
self.update(:expire_at => Date.today + 6.months, :amount => 0)
end
end
with the rake tasks i have never written of these and i thought i would be able to call a method of StoreCredit that would expire the points if certain conditions are met but i am not sure how this all works
task :expire_credits => :environment do
puts 'Expiring unused credits...'
StoreCredit.expire_credits
puts "done."
end
# model/store_credit.rb
# get all store_credits that are expired on given date, default to today
scope :expire_on, -> (date = Date.current) { where("expire_at <= ?", date.beginning_of_day) }
class << self
def expire_credits!(date = Date.current)
# find all the expired credits on particular date, and update all together
self.expire_on(date).update_all(amount: 0)
end
end
Since it's a rake task, I think it's more efficient to update all expired ones together
#rake file
result = StoreCredit.expire_credits!
puts "#{result} records updated"
Retrieve Record Count Update
class << self
def expire_credits!(date = Date.current)
# find all the expired credits on particular date, and update all together
records = self.expire_on(date)
records.update_all(amount: 0)
records.length
end
end
You call class method but define instance method. You will need to define class method:
def self.expire_credits
I currently have this file in my models/ folder:
class Show < ActiveRecord::Base
require 'nokogiri'
require 'open-uri'
has_many :user_shows
has_many :users, through: :user_shows
def self.update_all_screenings
Show.all.each do |show|
show.update_attribute(:next_screening, Show.update_next_screening(show.url))
end
end
def self.update_next_screening(url)
nextep = Nokogiri::HTML(open(url))
## Finds the title of the show and extracts the date of the show and converts to string ##
begin
title = nextep.at_css('h1').text
date = nextep.at_css('.next_episode .highlight_date').text[/\d{1,2}\/\d{1,2}\/\d{4}/]
date = date.to_s
## Because if it airs today it won't have a date rather a time this checks whether or not
## there is a date. If there is it will remain, if not it will insert todays date
## plus get the time that the show is airing
if date =~ /\d{1,2}\/\d{1,2}\/\d{4}/
showtime = DateTime.strptime(date, "%m/%d/%Y")
else
date = DateTime.now.strftime("%D")
time = nextep.at_css('.next_episode .highlight_date').text[/\dPM|\dAM/]
time = time.to_s
showtime = date + " " + time
showtime = DateTime.strptime(showtime, "%m/%d/%y %l%p")
end
return showtime
rescue
return nil
end
end
end
However, when I run
Show.update_all_screenings
It takes ages to do. I currently have a very similar script that is a rake file that has to do twice the amount of scraping and manages to do it in about 10 minute where as this one will take 8 hours as is. So I was wondering how I would go about converting this file to a rake task? The whole app I'm building depends on this being able to do it in at most 1 hours.
Here is the other script for reference:
require 'mechanize'
namespace :show do
desc "add tv shows from web into database"
task :scrape => :environment do
puts 'scraping...'
Show.delete_all
agent = Mechanize.new
agent.get 'http://www.tv.com/shows/sort/a_z/'
agent.page.search('//div[#class="alphabet"]//li[not(contains(#class, "selected"))]/a').each do |letter_link|
agent.get letter_link[:href]
letter = letter_link.text.upcase
agent.page.search('//li[#class="show"]/a').map do |show_link|
Show.create(title: show_link.text, url:'http://tv.com' + show_link[:href].to_s + 'episodes/')
end
while next_page_link = agent.page.at('//div[#class="_pagination"]//a[#class="next"]') do
agent.get next_page_link[:href]
agent.page.search('//li[#class="show"]/a').map do |show_link|
Show.create(title: show_link.text, url:'http://tv.com' + show_link[:href].to_s + 'episodes/')
end
end
end
end
end
Rake is no magic bullet - it will not run your code any faster.
What you could do is run your code more efficiently. The main time-consumer in your code is iteratively calling open(url). If you could read all the urls concurrently, the whole process should take fraction of the time it takes now.
You could use typhoeus gem (or some other gem) to handle this for you.
--Danger! Untested code ahead!--
I have no experience using this gem, but your code could look something like this:
require 'nokogiri'
require 'open-uri'
require 'typhoeus'
class Show < ActiveRecord::Base
has_many :user_shows
has_many :users, through: :user_shows
def self.update_all_screenings
hydra = Typhoeus::Hydra.hydra
Show.all.each do |show|
request = Typhoeus::Request.new(show.url, followlocation: true)
request.on_complete do |response|
show.update_attribute(:next_screening, Show.update_next_screening(response.body))
end
hydra.queue(request)
end
hydra.run
end
def self.update_next_screening(body)
nextep = Nokogiri::HTML(body)
## Finds the title of the show and extracts the date of the show and converts to string ##
begin
title = nextep.at_css('h1').text
date = nextep.at_css('.next_episode .highlight_date').text[/\d{1,2}\/\d{1,2}\/\d{4}/]
date = date.to_s
## Because if it airs today it won't have a date rather a time this checks whether or not
## there is a date. If there is it will remain, if not it will insert todays date
## plus get the time that the show is airing
if date =~ /\d{1,2}\/\d{1,2}\/\d{4}/
showtime = DateTime.strptime(date, "%m/%d/%Y")
else
date = DateTime.now.strftime("%D")
time = nextep.at_css('.next_episode .highlight_date').text[/\dPM|\dAM/]
time = time.to_s
showtime = date + " " + time
showtime = DateTime.strptime(showtime, "%m/%d/%y %l%p")
end
return showtime
rescue
return nil
end
end
end
The above should collect all the requests in one queue, and run them concurrently, acting on any response as it comes.
For my Rails application I am trying to build a rake task that will populate the database with 10 invoices for each user:
def make_invoices
User.all.each do |user|
10.times do
date = Date.today
i = user.invoices.create!(:number => user.next_invoice_number,
:date => date += 1.year)
i.save
end
end
end
How can I increment the date by one year?
Change your loop to:
10.times do |t|
puts t + 1 # will puts 1,2,3,4,5,6,7,8,9,10
end
And now you can set your year.
Here it is using Date#next_year:
require 'date'
d = Date.today
d # => #<Date: 2013-09-21 ((2456557j,0s,0n),+0s,2299161j)>
d.next_year # => #<Date: 2014-09-21 ((2456922j,0s,0n),+0s,2299161j)>
def make_invoices
User.all.each do |user|
date = Date.today
10.times do
user.invoices.create!(:number => user.next_invoice_number,
:date => (date += 1.year))
end
end
end
Because you have date = Date.today in the 10.times loop, it will be reseted in each loop. Just move date = Date.today outside the loop.
You can take advantage of Date#next_year, which takes a parameter meaning how many years you want to advance :
def make_invoices
User.all.each do |user|
10.times.with_object(Date.today) do |i, date|
user.invoices.create!(
number: user.next_invoice_number,
date: date.next_year(i)
)
end
end
end
Numeric#times pass an index to block.
Enumerator#with_object allow to pass additional parameters to block, which we here use to avoid setting a local variable outside the block (since we don't need it outside).