Putting Date and 1.month.ago together? - ruby-on-rails

How would I put Date and 1.month.ago together when I have a date attribute called :purchase_date and want to put it inside a class method?
def self.last_month # Show only products of last month.
where(:purchase_date => Date.today.1.month.ago.end_of_month..Date.1.month.ago.beginning_of_month)
end
console gives a syntax error and taking it away Date.today gives me blank results compared to my other method:
def self.this_month # Show only products of this month.
where(:purchase_date => Date.today.beginning_of_month..Date.today.end_of_month)
end

Just 1.month.ago is enough, you don't need to prepend Date.today to 1.month.ago because 1.month.ago starts from today

You have mistake in your Date syntax, you might want to use something like this:
def self.last_month # Show only products of last month.
where(:purchase_date => 1.month.ago.beginning_of_month..1.month.ago.end_of_month)
end
def self.this_month # Show only products of this month.
where(:purchase_date => Date.today.beginning_of_month..Date.today.end_of_month)
end

Maybe:
def self.this_month
where(:purchase_date =>(Date.today - 1.month)..Date.today
end

If the future time needs to be farther out, like in the case of planned subscription orders, remember to use .since
def self.next_quarter # Show only product order in the next 3 months
where(:purchase_date => Date.today.beginning_of_month..3.months.since)
end

Related

Elegant way of safely changing the day of ruby Date?

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.

Fetching Data based date and time

I am trying to find results from today onwards but also want to include the yesterdays plans if the time is between 12:00am-5:00am
Right now i have the following
def self.current
where(
"plan_date >= :today",
today: Date.current,
)
end
Is there a way i can know the time of the day based on the users timezone which am setting as bellow in the app controller and make sure that if its before 6:am the next day i want to include the previous days results as well.
def set_time_zone(&block)
if current_user
Time.use_zone(current_user.time_zone_name, &block)
else
yield
end
end
Try this:
def self.current
where(
"plan_date >= :today",
today: (Time.zone.now.in_time_zone(get_user_time_zone) - 6.hours).beginning_of_day,
)
end
...where get_user_time_zone returns the time zone for the user (E.G.: America/New_York). I'm using - 6.hours because you wanted it to be "before 6am" local time.

Rails - Update attributes using rake task. Need help troubleshooting model method code

I am trying to use a rake task that will run every night (using Heroku Scheduler) and update some attributes if some certain criteria is met.
A little logic about the application:
The application allows users to "take the challenge" and read a book a week over the course of a year. It's pretty simple: users sign up, create the first book that they will read for their first week, then can enter what they're going to read next week. After they've "queued" up next week's book, that form is then hidden until it's been 7 days since their first book was created. At that point, the book that was queued up gets moved to the top of the list and marked as 'currently reading', while the previous 'currently reading' book moves down to the second position in the list.
And IF a user doesn't 'queue' a book, the system will automatically create one if it's been 7 days since the latest 'currently reading' book was created.
Where I need some advice
The place I'm currently stuck is getting the books to update attributes if it's been 7 days since the last 'currently reading' book was created. Below is my book model and the method update_queue is what gets called during the rake task. Running the rake task currently gives no errors and properly loops through the code, but it just doesn't change any attribute values. So I'm sure the code in the update_queue method is not correct somewhere along the lines and I would love your help troubleshooting the reason why. And how I'm testing this is by adding a book then manually changing my system's date to 8 days ahead. Pretty barbaric, but I don't have a test suite written for this application & it's the easiest way to do it for me :)
class Book < ActiveRecord::Base
attr_accessible :author, :date, :order, :title, :user_id, :status, :queued, :reading
belongs_to :user
scope :reading_books, lambda {
{:conditions => {:reading => 1}}
}
scope :latest_first, lambda {
{:order => "created_at DESC"}
}
def move_from_queue_to_reading
self.update_attributes(:queued => false, :reading => 1);
end
def move_from_reading_to_list
self.update_attributes(:reading => 0);
end
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
# If been 7 days since last 'currently reading' book created
if days_gone >= 7
# If there's a queued book, move it to 'currently reading'
if Book.my_books(user_id).where(:queued => true)
new_book = Book.my_books(user_id).latest_first.where(:queued => true).last
new_book.move_from_queue_to_reading
Book.my_books(user_id).reading_books.move_from_reading_to_list
# Otherwise, create a new one
else
Book.my_books(user_id).create(:title => "Sample book", :reading => 1)
end
end
end
My rake task looks like this (scheduler.rake placed in lib/tasks):
task :queue => :environment do
puts "Updating feed..."
#books = Book.all
#books.each do |book|
book.update_queue
end
puts "done."
end
I would move the update_queue logic to the User model and modify the Book model somewhat, and do something like this:
# in book.rb
# change :reading Boolean field to :reading_at Timestamp
scope :queued, where(:queued => true)
scope :earliest_first, order("books.created_at")
scope :reading_books, where("books.reading_at IS NOT NULL")
def move_from_queue_to_reading
self.update_attributes(:queued => false, :reading_at => Time.current);
end
def move_from_reading_to_list
self.update_attributes(:reading_at => nil);
end
# in user.rb
def update_queue
reading_book = books.reading_books.first
# there is an edge-case where reading_book can't be found
# for the moment we will simply exit and not address it
return unless reading_book
days_gone = Date.today - reading_book.reading_at.to_date
# If less than 7 days since last 'currently reading' book created then exit
return if days_gone < 7
# wrap modifications in a transaction so they can be rolled back together
# if an error occurs
transaction do
# First deal with the 'currently reading' book if there is one
reading_book.move_from_reading_to_list
# If there's a queued book, move it to 'currently reading'
if books.queued.exists?
books.queued.earliest_first.first.move_from_queue_to_reading
# Otherwise, create a new one
else
books.create(:title => "Sample book", :reading_at => Time.current)
end
end
end
Now you can have the Heroku scheduler run something like this once a day:
User.all.each(&:update_queue)
Modify User.all to only return active users if you need to.
Oh, and you can use the timecop gem to manipulate times and dates when testing.
Probably the reason is that, the program flow is not going inside the if days_gone >= 7 condition.
you could check this in two ways
1 - Simple and easy way (but not a very good way)
use p statements every were with some meaning full text
Ex:
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
p "days gone : #{days_gone}"
# If been 7 days since last 'currently reading' book created
if days_gone >= 7
p "inside days_gone >= 7"
etc...
2 - Use ruby debugger and use debug points
in Gem file add
gem 'debugger'
and insert break points where ever needed
def update_queue
days_gone = (Date.today - Date.parse(Book.where(:reading => 1).last.created_at.to_s)).to_i
debugger
more help
HTH

Sum / grouping with date constraints?

i've written the following sum/group methods in my 'StatementSales' model and want to be able to constrain the results by date, at the moment it's just producing totals for all valid db entries. In my views I want to provide links to 'One Week, One Month, Three Months, One Year' etc and ideally pass these to the methods below. How should I approach this?
def self.total_units
sum(:units)
end
def self.units_by_store
group(:store).sum(:units)
end
def self.units_by_territory
group(:territory).sum(:units)
end
def self.units_by_upc
group(:upc).sum(:units)
end
Many thanks in advance!
You could use scopes
Add this to your class
scope :between_dates, lambda { |start_date, end_date| where("date < #{end_date} AND date >= #{start_date}") }
scope :one_week, between_dates(Date.today, Date.today + 7.days)
Then you can do
def self.total_units
self.one_week.sum(:units)
end

Rails: Find tasks that were created on a certain day?

I have a list of tasks (name, starts_at) and I'm trying to display them in a daily view (just like iCal).
def todays_tasks(day)
Task.find(:all, :conditions => ["starts_at between ? and ?", day.beginning, day.ending]
end
I can't figure out how to convert Time.now such as "2009-04-12 10:00:00" into the beginning (and end) of the day dynamically so I can make the comparison.
def todays_tasks(now = Time.now)
Task.find(:all, :conditions => ["starts_at > ? AND < ?", now.at_beginning_of_day, now.at_end_of_day])
end
Voila!
Expanding on augustl's answer a little bit. In your Task model you could define a method to fetch these tasks for you.
def self.todays_tasks(t = Time.now)
Task.all(:conditions => ["created_at > ? AND created_at < ?", t.at_beginning_of_day, t.tomorrow.at_beginning_of_day])
end
This helps keep your controller skinny. I also compare the end of the day to the beginning of tomorrow. This makes sure that if a user creates a task at 11:59:59 PM it remains in the correct day.
Wouldn't it be:
Task.all(:conditions => ["created_at >= ? AND created_at < ?", Time.now.at_beginning_of_day, Time.now.tomorrow.at_beginning_of_day] )
Or else you would never check for 00:00:00 on the day of the tasks and 11:59:59 is the last value at the end of the day of tasks.
def todays_tasks(day)
Task.where(:created_at => day.beginning_of_day .. day.end_of_day)
end

Resources