Calculate days until next birthday in Rails - ruby-on-rails

I have a model with a date column called birthday.
How would I calculate the number of days until the user's next birthday?

Here's a simple way. You'll want to make sure to catch the case where it's already passed this year (and also the one where it hasn't passed yet)
class User < ActiveRecord::Base
attr_accessible :birthday
def days_until_birthday
bday = Date.new(Date.today.year, birthday.month, birthday.day)
bday += 1.year if Date.today >= bday
(bday - Date.today).to_i
end
end
And to prove it! (all I've added is the timecop gem to keep the calculations accurate as of today (2012-10-16)
require 'test_helper'
class UserTest < ActiveSupport::TestCase
setup do
Timecop.travel("2012-10-16".to_date)
end
teardown do
Timecop.return
end
test "already passed" do
user = User.new birthday: "1978-08-24"
assert_equal 313, user.days_until_birthday
end
test "coming soon" do
user = User.new birthday: "1978-10-31"
assert_equal 16, user.days_until_birthday
end
end

Try this
require 'date'
def days_to_next_bday(bday)
d = Date.parse(bday)
next_year = Date.today.year + 1
next_bday = "#{d.day}-#{d.month}-#{next_year}"
(Date.parse(next_bday) - Date.today).to_i
end
puts days_to_next_bday("26-3-1985")

Having a swipe at this:
require 'date'
bday = Date.new(1973,10,8) // substitute your records date here.
this_year = Date.new(Date.today.year, bday.month, bday.day )
if this_year > Date.today
puts this_year - Date.today
else
puts Date.new(Date.today.year + 1, bday.month, bday.day ) - Date.today
end
I'm not sure if Rails gives you anything that makes that much easier.

Here's another way to approach this with lesser-known methods, but they make the code more self-explanatory.
Also, this works with birth dates on a February 29th.
class User < ActiveRecord::Base
attr_accessible :birthday
def next_birthday
options = { year: Date.today.year }
if birthday.month == 2 && birthday.day == 29 && !Date.leap?(Date.today.year)
options[:day] = 28
end
birthday.change(options).tap do |next_birthday|
next_birthday.advance(years: 1) if next_birthday.past?
end
end
end
And of course, the number of days until the next birthday is:
(user.next_birthday - Date.today).to_i

Related

How to test with RSpec on Rails a past date if I cant create an object with past date being prohibited inside the model?

I have a model Appointment that prohibit the object to be created using a past date or update if the field day is in the past.
class Appointment < ApplicationRecord
belongs_to :user
...
validate :not_past, on: [:create, :update]
private
...
def not_past
if day.past?
errors.add(:day, '...')
end
end
end
But I need to make a test file using RSpec to test if it really cannot be edited if the field day is a past date.
require 'rails_helper'
RSpec.describe Appointment, type: :model do
...
it 'Cannot be edited if the date has past' do
#user = User.last
r = Appointment.new
r.day = (Time.now - 2.days).strftime("%d/%m/%Y")
r.hour = "10:00"
r.description = "Some Description"
r.duration = 1.0
r.user = #user
r.save!
x = Appointment.last
x.description = "Other"
expect(x.save).to be_falsey
end
...
end
The trouble is, the test can't be accurate due to an error that prohibit the creation of an Appointment object with the past day.
What should I do to force, or even maybe make a fake object with a past date for I can finally test it?
You can use update_attribute which will skip validations.
it 'Cannot be edited if the date has past' do
#user = User.last
r = Appointment.new
r.day = (Time.now - 2.days).strftime("%d/%m/%Y")
r.hour = "10:00"
r.description = "Some Description"
r.duration = 1.0
r.user = #user
r.save!
x = Appointment.last
x.description = "Other"
r.update_attribute(:day, (Time.now - 2.days).strftime("%d/%m/%Y"))
expect(x.save).to be_falsey
end
Also you have a lot of noise in your test (data which is not asserted) which you should avoid by e.g. creating a helper function or using factories.
it 'Cannot be edited if the date has past' do
appointment = create_appointment
appointment.update_attribute(:day, (Time.now - 2.days).strftime("%d/%m/%Y"))
appointment.description = 'new'
assert(appointment.valid?).to eq false
end
def create_appointment
Appointment.create!(
day: Time.now.strftime("%d/%m/%Y"),
hour: '10:00',
description: 'description',
duration: 1.0,
user: User.last
)
end
Also you test for falsey which will also match nil values. What you want to do in this case is test for false with eq false.

Rails speed up database with code refactor for process remover

I've got worker class which removes InquiryProcess older than x time (default should be set to 6 months). Potentially it will be a large data scale so is there any chance to speed up deletions with code below?
class OldProcessRemover
def initialize(date: 6.months.ago)
#date = date
end
attr_reader :date
def call
remove_loan
remove_checking_account
end
private
def remove_loan
loan_template = InquiryTemplate.find_by(inquiry_process_name: InquiryTemplate::LOAN_APPLICATION_PROCESS_NAME)
loan_template.inquiry_processes.where('created_at <= ?', date).each(&:destroy)
end
def remove_checking_account
checking_account_template = InquiryTemplate.find_by(
inquiry_process_name: InquiryTemplate::CHECKING_ACCOUNT_OPENING_PROCESS_NAME,
)
checking_account_template.inquiry_processes.where('created_at <= ?', date).each(&:destroy)
end
end
Maybe somewhere I could use find_in_batches ?. I don't think these methods are single responsibility, so refactor will helped either.
class OldProcessRemover
def initialize(date: 6.months.ago)
#date = date
end
attr_reader :date
def call
remove_loan
remove_checking_account
end
private
def remove_loan
remove_processes!(InquiryTemplate::LOAN_APPLICATION_PROCESS_NAME)
end
def remove_checking_account
remove_processes!(InquiryTemplate::CHECKING_ACCOUNT_OPENING_PROCESS_NAME)
end
def remove_processes!(process_name)
account_template = InquiryTemplate.find_by(
inquiry_process_name: process_name
)
account_template.inquiry_processes
.where('created_at <= ?', date)
.find_in_batches { |group| group.destroy_all }
end
end
I don't think there is any major difference between using .find_in_batches { |group| group.destroy_all } or .find_each {|record| record.destroy } here.

Rails advance month to february sometimes fails

I am making a payments arrangement, for an order, where a client can choose the day of the month to pay. And can also change it afterwards. For this I used the advance method for date, and it works correctly when creating, but fails when updating. Here is my code:
Creating, if day is 31, it properly changes to last day of month if the month doesn't have a day 31.
months = params[:payment][:months].to_i
pay_day = params[:payment][:pay_day].to_i
today = Date.today
first_date = today.change(months: 1).change(day: pay_day)
months_between = (today.year * 12 + today.month) - (first_date.year * 12 + first_date.month)
quota = (invoice_total)/months
payment_number = 1
months.times do |i|
payment = Payment.new(invoice: current_invoice, payment_type: PaymentType.find(params[:payment][:payment_type_id]), payment_number: payment_number, value: quota, max_payment_date: first_date.advance(months: +(months_between+i+1)))
payment.save
payment_number = payment_number + 1
end
Update code, it fails in, for example, February with "invalid date", but updates january correctly.
pay_day = params[:payment][:pay_day].to_i
#invoice.payments.each do |payment|
if payment.actual_payment_date.nil?
reference_month = payment.max_payment_date.change(months: 1).change(day: pay_day)
months_between = (payment.max_payment_date.year * 12 + payment.max_payment_date.month) - (reference_month.year * 12 + reference_month.month)
payment.update_attributes(max_payment_date: reference_month.advance(months: +(months_between)))
payment.save
end
end
It is basically the same code, does anyone know why in the first part it works correctly, but not the second part?
Thanks
ActiveSupport to the rescue:
def correct_day(day, date = Date.today)
end_of_month = date.end_of_month.day
day < end_of_month ? day : end_of_month
end
Lets test it with [timecop:
Timecop.freeze(Date.new(2000, 02)) do
correct_day(32) # => 29 - 2000 was a leap year.
correct_day(15) # => 15
end
Timecop.freeze(Date.new(2015, 10, 11)) do
correct_day(32) # => 31
correct_day(15) # => 15
end
This is how you would implement it in a model:
class Invoice < ActiveRecord::Base
has_many :payments
# Creates a payment for each month
# #param [Integer] months
# #param [Ingeger] preferred_day - defaults to the last day of the month
def split_payments(months = 12, preferred_day = 31)
(1..months).map do |i|
payments.create(
pay_day: calculate_payment_date(created_at + i.months, preferred_day),
payment_number: i
# ... More attributes
)
end
end
def change_payment_date(preferred_day)
payments.sort_by(:payment_number).map do |payment|
date = created_at + payment.payment_number.months
payment.update_attributes(
pay_day: calculate_payment_date(date, preferred_day)
)
end
end
private
def calculate_payment_date(date, preferred_day)
end_of_month = date.end_of_month
pref = date.beginning_of_month + (preferred_day - 1).days
pref < end_of_month ? pref : end_of_month
end
end

custom validation gives blank field after validation which is not right

I have a custom validation in my model like this:
class Appointment < ActiveRecord::Base
#VIRTUAL ATTRIBUTES
attr_accessor :start_date, :start_time, :duration
#RELATIONSHIPS
belongs_to :task
#VALIDATIONS
before_validation :convert_to_datetime
before_validation :dur
validates :duration, presence: true
validate :is_date_nil
validate :time_collision_validation, if: :is_appointments_not_empty
validate :check_time
after_save :save_start_date
def is_appointments_not_empty
Appointment.all.present?
end
def check_time
start_at = Time.parse("#{#start_date} #{#start_time}")
if start_at < Time.now
errors.add(:start_date, "Cannot input past times")
end
end
def convert_to_datetime
unless #start_date.blank? && #start_time.blank?
self.start_at = Time.parse("#{#start_date} #{#start_time}")
end
end
def dur
if #start_date.present? && #start_time.present? && #duration.present?
self.end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
end
end
def time_collision_validation
appointments = Appointment.all
if #start_date.present? && #start_time.present? && duration == 0.to_s
start_at = Time.parse("#{#start_date} #{#start_time}")
end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
appointments.each do |a|
if start_at <= a.end_at - (2*60*60) && start_at >= a.start_at - (1*60*60)
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{a.start_at.strftime("%I:%M%p")} of #{a.start_at.strftime("%d/%m/%Y")}
to #{a.end_at.strftime("%I:%M%p")} of #{a.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
elsif #start_date.present? && #start_time.present? && duration.present?
start_at = Time.parse("#{#start_date} #{#start_time}")
end_at = Time.parse("#{#start_date} #{#start_time}") + (#duration.to_f*60*60)
appointments.each do |a|
if start_at <= a.end_at - (2*60*60) && a.start_at <= end_at
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{a.start_at.strftime("%I:%M%p")} of #{a.start_at.strftime("%d/%m/%Y")}
to #{a.end_at.strftime("%I:%M%p")} of #{a.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
end
end
def is_date_nil
if #start_date.blank? && #start_time.blank?
errors.add(:start_date, "Start date can't be blank")
errors.add(:start_time, "Start time can't be blank")
end
if #start_date.blank? && #start_time.present?
errors.add(:start_date, "Start date can't be blank")
end
if #start_time.blank? && #start_date.present?
errors.add(:start_time, "Start time can't be blank")
end
end
def start_date=(date)
#start_date = Date.strptime(date, "%d/%m/%Y") if date.present?
end
# def save_start_date
# #start_date = Date.strptime(#start_date, "%d/%m/%Y") if #start_date.present?
# end
# def save_start_date
# #start_date = Date.parse(#start_date).strftime("%d/%m/%Y")if #start_date.present?
# end
def start_time=(time)
#start_time = Time.parse(time).strftime("%H:%M:%S") if time.present?
end
def duration=(duration)
#duration = duration if duration.present?
end
# def start_date
# #start_date.strftime("%d/%m/%Y") if start_at.present? # || start_at.strftime("%d/%m/%Y") if start_at.present?
# end
def start_date
unless #start_date.blank?
#start_date.strftime("%d/%m/%Y")
end
# start_at.strftime("%d/%m/%Y") if start_at.present?
end
def start_time
#start_time || start_at.strftime("%I:%M%p") if start_at.present?
end
# def duration
# #duration || 9
# end
end
After this time_collision_validation executes, the value fields are blank which I don't want because I'm concerned with UX. ie: start_date and start_time fields are blank.
When I checked the value attribute in input in the HTML source code, the value contains a date string. I wonder why it does not show in the field.
Can somebody help me with this and explain what is going on please? >.<
validate :time_collision_validation
def time_collision_validation
appointments = Appointment.all
if self.start_date.present? && self.start_time.present? && duration.present?
start_at = Time.parse("#{self.start_date} #{self.start_time}")
end_at = Time.parse("#{self.start_date} #{self.start_time}") + (self.duration.to_f.hours)
appointments.each do |appointment|
if duration == 0.to_s
duration_ok = start_at >= appointment.start_at - (1.hours)
else
duration_ok = appointment.start_at <= end_at
end
if start_at <= appointment.end_at - (2.hours) && duration_ok
errors.add(:start_time)
errors.add(:start_date, "An appointment already
exists at #{appointment.start_at.strftime("%I:%M%p")} of #{appointment.start_at.strftime("%d/%m/%Y")}
to #{appointment.end_at.strftime("%I:%M%p")} of #{appointment.end_at.strftime("%d/%m/%Y")}.
Please select a different date or time.")
break
end
end
end
end
Notes:
you variously refer to duration and self.duration. For readability i would always use self.duration rather than duration or self.duration as it makes it clear to the reader that you are talking about a method/field of the current object rather than a local variable. I've change all instances of referencing methods/fields of the current object to self.methodname
you had a lot of repetition shared between the two if cases. I've refactored these to avoid repetition.
2*60*60 is like saying 2.hours and the latter is much more readable.
what are start_date and start_time - are they strings or date/time objects?
rather than loading every appointment in your entire database, and cycling through them, it would be much more efficient to just search for a single colliding other appointment. if you find one then you can add the errors. I was tempted to do this here but it's not clear exactly what's going on with your database.

Rails: Activity log by week - filling in blank weeks

I'm creating an activity chart, showing the number of records saved to a user's profile over time (in this case its 'user.notes' i.e. user has_many notes). The json output from the function below feeds nicely into a chart library.
The function below however does not output data for weeks without any activity...I need these 'holes', with correct week dates to be in there... can anyone suggest how I might be able to do this?
I believe the 'has_activity' gem has this functionality, however I would prefer to avoid using a whole gem to do this.
class Case < ActiveRecord::Base
def self.user_activity_profile(user)
array = []
user.notes.group_by{ |u| u.created_at.beginning_of_week }.each do |week, entries|
array << { week: week.strftime("%d %b"), count: entries.count }
end
array.to_json
end
end
I believe this should do what you're looking for. It just takes the first and last note for each user and then steps through the date range by 7 days at a time which forces it not to skip any weeks.
class Case < ActiveRecord::Base
def self.user_activity_profile(user)
array = []
notes = user.notes
first = notes.first.created_at.beginning_of_week
last = notes.last.created_at.beginning_of_week
(first..last).step(7){ |date|
array << [
week:date.strftime("%d %b"),
count: notes.where("created_at BETWEEN ? AND ?", date, date + 1.week).count
]}
array.to_json
end
end
def self.user_activity_profile(user)
from = Time.now - 1.months
to = Time.now
tmp = from
array = []
begin
tmp += 1.week
array << [
week: tmp.strftime("%d %b"),
count: user.notes.where("created_at BETWEEN ? AND ?", tmp, tmp + 1.week).count
]
end while tmp <= to
array.to_json
end

Resources