Determining if a bi-weekly schedule matches a given date - ruby

I'm creating multiple Schedule objects, which have a started_at datetime which begins on Mondays.
I have Location objects which have a visit_frequency. Some of these are set to :bi_weekly, in which case I only need to see them every other week.
However, things don't always go according to plan and sometimes Locations are visited more or less often than the need to.
Right now I'm doing
Location.all.each do |location|
...
elsif location.frequency.rate == 'biweekly'
if (#schedule.start_date - location.last_visit_date) > 7
schedule_for_week location
end
The problem is, if I make a Schedule more than 7 days from now, the Location's last_visit_date will ALWAYS be > 7 days. I need to calculate if it falls into a bi-weekly rate.
Example:
Location 1 visit_frequency set to :bi_weekly
Location 1 is visited on Week 1
Week 2 Schedule Generated -- Location 1 is left out because it is within 7 days
Week 3 Schedule Generated -- Location 1 is included because it is within 7 days
Week 4 Schedule Generated -- Location 1 is included because it is within 7 days
The last line should not have happened. Location 1 should not be included because it was visited on Week 1 and scheduled for Week 3.
How can I calculate if a week is within a bi-weekly frequency succintly? I"m guessing I need to use beginning_of_week?

As I understand your question, I believe this would do it:
require 'date'
def schedule?(sched_start_date, last_visit_date)
(sched_start_date - last_visit_date) % 14 > 7
end
sched_start_date = Date.parse("2014-12-29")
#=> #<Date: 2014-12-29 ((2457021j,0s,0n),+0s,2299161j)> a Monday
schedule?(sched_start_date, Date.parse("2014-12-04")) #=> true
schedule?(sched_start_date, Date.parse("2014-12-14")) #=> false
schedule?(sched_start_date, Date.parse("2014-12-20")) #=> true
schedule?(sched_start_date, Date.parse("2014-12-23")) #=> false

Related

console output of the current calendar month in Ruby

I need to output to the console the calendar of the current month in Ruby. The result should be similar to ncal on UNIX-like systems. I found a solution for C ++ but can't adapt for Ruby. So far, I only realized that I need to use nested loops to output the height and width. Tell me in which direction to move?
require 'date'
days = %w[Mun Tue Wed Thu Fri Sat Sun]
puts " #{Date::MONTHNAMES[Date.today.month]} #{Date.today.year}"
i = 0
start_month = (Date.today - Date.today.mday + 1).strftime("%a")
while i < days.size
print days[i]
j = 1
while j <= 31
if days[i] == start_month
print " #{j}"
end
j += 7
end
i += 1
puts
end
I'll take your solution so far, and try to give some specific pointers for how to progress with it - but of course, there are many different ways to approach this problem in general, so this is by no means the only approach!
The first critical issue (as you're aware!) is that you're only printing things for the row starting on the 1st of the month, due to this line:
if days[i] == start_month
Sticking with the current overall design, we know we'll need to print something for every line, so clearly a conditional like this isn't going to work. Let's try removing it.
Firstly, it will be more convenient to know which day of the week the month started on as a number, not a string, so we can easily calculate offsets against another day. Let's do that with:
# e.g. for 1st July 2021 this was a Thursday, so we get `4`.
start_of_month_weekday = (Date.today - Date.today.mday + 1).cwday
Next (and this is the crucial step!), we can use the above information to find out "which day of the month is it, on this day of the week?"
Here a first version of that calculation, incorporated into your solution so far:
require 'date'
days = %w[Mon Tue Wed Thu Fri Sat Sun]
puts " #{Date::MONTHNAMES[Date.today.month]} #{Date.today.year}"
i = 0
# e.g. for 1st July 2021 this was a Thursday, so we get `4`.
start_of_month_weekday = (Date.today - Date.today.mday + 1).cwday
while i < days.size
print days[i]
day_of_month = i - start_of_month_weekday + 2 # !!!
while day_of_month <= 31
print " #{day_of_month}"
day_of_month += 7
end
i += 1
puts
end
This outputs:
July 2021
Mon -2 5 12 19 26
Tue -1 6 13 20 27
Wed 0 7 14 21 28
Thu 1 8 15 22 29
Fri 2 9 16 23 30
Sat 3 10 17 24 31
Sun 4 11 18 25
Not bad! Now we're getting somewhere!
I'll leave you to figure out the rest 😉 .... But here are some clues, for what I'd tackle next:
This code, print " #{day_of_month}", needs to print a "blank space" if the day number is less than 1. This could be done with a simple if statement.
Similarly, since you want this calendar to line up neatly in a grid, you need this code to always print a something two characters wide. sprintf is your friend here! Check out the "Examples of width", about halfway down the page.
You've hardcoded 31 for the number of days in the month. This should be fixed, of course. (Use the Date library!)
It's funny how you used strftime("%a") in one place, yet constructed the calendar title awkwardly in the line above! 😄 Take a look at the documentation for formatting dates; it's extremely flexible. I think you can use: Date.today.strftime("%B %Y").
If you'd like to add some colour (or background colour?) to the current day of the month, consider doing something like this, or use a library to assist.
Using while loops works OK, but is quite un-rubyish. In 99% of cases, ruby has even better tools for the job; it's a very expressive language - iterators are king! (I'm guessing you first learned another language, before ruby? Seeing while loops, and/or for loops, is a dead giveaway that you're more familiar with a different language.) Instead of the outer while loop (while i < days.size), you could use days.each_with_index. And instead of the inner while loop (while j < 31), you could use day_of_month.step(31, 7) (how cool is that!!).
This is one way:
Construct a one-dimensional array, beginning with the daynames (Mon Tue ...).
Figure out a way to determine with how many "blanks" the month starts (these are days from the previous month. wday might help). Attach that amount of empty strings to the array.
Determine how many days the month has (hint Date.new(2021,7,-1), and attach all these daynumbers to the array.
Attach empty strings to the array until the size of the array is divisible by 7 (or better, calculate). Skip this if you're skipping the last bullet.
Convert all elements of this array to right-adjusted strings of size 3 or some-such.
Use each_slice(7) to slice the array into weeks.
If desired, transpose this array of week-slices to mimic the ncal output.
Thank you for your help, literally 10 hours and I figured it out thanks to you. I apologize once again for the initially incorrectly posed question.
With the help of hints, I assembled such a solution.
require 'date'
days = %w[Mon Tue Wed Thu Fri Sat Sun]
p days
blanks = Date.new(2021,7,1).wday - 1
blanks.times do
days.push(' ')
end
days_in_month = Date.new(2021, 7, -1).day
days_in_month
day = 1
while day <= days_in_month
days.push(day)
day += 1
end
unless (days.size % 7) == 0
days.push(' ')
end
days.join(', ')
new_arr = days.each_slice(7).to_a
puts"Массив дней: #{new_arr}"
for i in 0...7
for j in 0...new_arr.size
print " #{new_arr[j][i]}"
end
puts
end
require 'date'
# init
DAYS_ORDER = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
today = Date.today
month = today.month
year = today.year
first_day = Date.new(year, month, 1)
last_day = Date.new(year, month, -1)
hash_days = {}
# get all current months days and add to hash_days
first_day.upto(last_day) { |day| hash_days[day.day] = day.strftime('%a') }
# group by wday
grouped_hash = hash_days.group_by { |day| day.pop }.transform_values { |days| days.flatten }
# sort by wday from DAYS_ORDER
sorted_arr = grouped_hash.sort_by { |k, v| DAYS_ORDER.index(k) }
# rendering current month's calendar with mark current day
## title
print "\x1b[4m#{today.strftime("%B %Y")}\x1b[0m\n"
## calendar
indent = true
sorted_arr.each do |wday, days|
print wday
if days[0] != 1 && indent == true
print " "
else
indent = false
end
days.each do |value|
spaces = " " * (value > 9 ? 1 : 2)
str_day = spaces + value.to_s
current_day = "\x1b[1;31m#{str_day}\x1b[0m"
print value == today.day ? current_day : str_day
end
puts
end
view

Hash key sort week numbers using ruby

I am having an hash whose keys are week numbers and values are attendance scores. I am tying to calculate the average attendance for each month based on the week number i.e.keys.
Below is the example of the hash
weekly_attendance = {31 => 40.0, 32 => 100.00, 33 => 34.00, 34 => 23.78, 35 => 56.79, 36 => 44.50, 37 => 67.00, 38 => 55.00 }
Since a month consists of 4 weeks and the beginning week of the month is divisible by 4, the attendance needs to be sorted as follows
Month 1 attendance consists of weeks 31,32 i.e. (40.00+100.00)/2 =70.0
Month 2 attendance consists of weeks 33,34,35,36
i.e. (34.00+23.78+56.79+44.50)/4 = 39.5
Month 3 attendance consists of weeks 37, 38 i.e. (67.00+55.00)/2 = 69.5
The output should be
monthly_attendance = [70.0,39.5,61]
I had tried each and select approaches and used the modulo operator condition i.e. week % 4 == 0 to add the attendance values. But could not effectively group them based on months
tmp = 0
monthly_attendance = []
weekly_attendance.select do |k,v|
tmp += v
monthly_attendance << tmp if k % 4 == 0
end
I am unable to sort the week number in ranges using the above code.
You can try something like this:
results = weekly_attendance.group_by { |week, value| (week + 3) / 4 }.map do |month, groups|
values = groups.map(&:last)
average = values.inject(0) { |sum, val| sum + val } / values.length
[month, average]
end.to_h
p results # {8=>70.0, 9=>39.7675, 10=>61.0}
But the logic of converting weeks to months is flawed here, it's better to use some calendar function instead of just division by 4.
You can get the real month numbers using:
require 'date'
weekly_attendance.group_by { |week, value| Date.commercial(Time.now.year, week, 1).month }
But the result will not match the result you expect, because for example week 31 is in July, while week 32 is in August (this year), instead of being the same month like you expect.
I assume that if x units are produced in a given week, x/7 units are produced on each day of that week. The code below could be easily changed if this assumption were changed.
First construct a hash whose keys are months (1-12) and whose values are hashes whose keys are weeks and whose values are the numbers of days in the given week for the given month. (Whew!)
require 'date'
def months_to_weeks(year)
day = Date.new(year)
days = day.leap? ? 365 : 364
days.times.with_object(Hash.new { |h,k| h[k] = Hash.new(0) }) do |_,h|
h[day.month][day.cweek] += 1
day = day.next
end
end
The doc for Hash#new provides an explanation of the statement:
Hash.new { |h,k| h[k] = Hash.new(0) }
In brief, this creates an empty hash with a default given by the block. If h is the hash that is created, and h does not have a key k, h[k] will cause the block to be executed, which adds that key to the hash and sets its value to an empty hash with a default value of 0. The latter hash is often referred to as a "counting hash". I realize this is still rather a mouthful for a Ruby newbie.
Let's generate this hash for the current year:
year = 2015
mon_to_wks = months_to_weeks(year)
#=> {1 =>{1 =>4, 2 =>7, 3 =>7, 4 =>7, 5=>6},
# 2 =>{5 =>1, 6 =>7, 7 =>7, 8 =>7, 9=>6},
# 3 =>{9 =>1, 10=>7, 11=>7, 12=>7, 13=>7, 14=>2},
# 4 =>{14=>5, 15=>7, 16=>7, 17=>7, 18=>4},
# 5 =>{18=>3, 19=>7, 20=>7, 21=>7, 22=>7},
# 6 =>{23=>7, 24=>7, 25=>7, 26=>7, 27=>2},
# 7 =>{27=>5, 28=>7, 29=>7, 30=>7, 31=>5},
# 8 =>{31=>2, 32=>7, 33=>7, 34=>7, 35=>7, 36=>1},
# 9 =>{36=>6, 37=>7, 38=>7, 39=>7, 40=>3},
# 10=>{40=>4, 41=>7, 42=>7, 43=>7, 44=>6},
# 11=>{44=>1, 45=>7, 46=>7, 47=>7, 48=>7, 49=>1},
# 12=>{49=>6, 50=>7, 51=>7, 52=>7, 53=>3}}
Because of how Date#cweek is defined, the weeks in this hash begin on Mondays. In January, for example, there 4 days are in week 1. These four days, Jan. 1-4, 2015, would be the first Thursday, Friday, Saturday and Sunday of 2015. (Check your calendar.)
If the first day of each week is to be a day other than Monday (Sunday, for example) the hash calculation would have to be changed slightly.
This shows, for example, that in January of 2015, there are 4 days in week 1, 7 days in weeks 2, 3 and 4 and 6 days in week 5. The remaining day of week 5 is the first day in February.
Once this hash has been constructed, it is a simple matter to compute the averages for each month:
weekly_attendance = {31 => 40.00, 32 => 100.00, 33 => 34.00, 34 => 23.78,
35 => 56.79, 36 => 44.50, 37 => 67.00, 38 => 55.00 }
prod_by_mon = (1..12).each_with_object(Hash.new(0)) do |i,h|
mon_to_wks[i].each do |week, days|
h[i] += (days/7.0)*weekly_attendance[week] if weekly_attendance.key?(week)
end
end
#=> {7=>28.571428571428573, 8=>232.3557142857143, 9=>160.14285714285714}
prod_by_mon.merge(prod_by_mon) { |_,v| v.round(2) }
#=> {7=>28.57, 8=>232.36, 9=>160.14}
This shows that production in month 7 was 27.57, and so on. Note that:
28.57 + 232.36 + 160.14 #=> 421.07
weekly_attendance.values.reduce(:+) #=> 421.07

NEXT_DAY in Crystal Reports

Is there anything like the Oracle "NEXT_DAY" function available in the syntax that Crystal Reports uses?
I'm trying to write a formula to output the following Monday # 9:00am if the datetime tested falls between Friday # 9:00pm and Monday # 9:00am.
So far I have
IF DAYOFWEEK ({DATETIMEFROMMYDB}) IN [7,1]
OR (DAYOFWEEK({DATETIMEFROMMYDB}) = 6 AND TIME({DATETIMEFROMMYDB}) in time(21,00,00) to time(23,59,59))
OR (DAYOFWEEK({DATETIMEFROMMYDB}) = 2 AND TIME({DATETIMEFROMMYDB}) in time(00,00,00) to time(08,59,59))
THEN ...
I know I can write seperate IF statements to do a different amount of DateAdd for each of Fri, Sat, Sun, Mon, but if I can keep it concise by lumping all of these into one I would much prefer it. I'm already going to be adding additional rules for if the datetime falls outside of business hours on the other weekdays so I want to do as much as possible to prevent this from becoming a very overgrown and ugly formula.
Since there is no CR equivalent that I know of, you can just cheat and borrow the NEXT_DAY() function from the Oracle database. You can do this by creating a SQL Expression and then entering something like:
-- SQL Expression {%NextDay}
(SELECT NEXT_DAY("MYTABLE"."MYDATETIME", 'MONDAY')
FROM DUAL)
then you could either use that directly in your formula:
IF DAYOFWEEK ({MYTABLE.MYDATETIME}) IN [7,1]
OR (DAYOFWEEK({MYTABLE.MYDATETIME}) = 6 AND TIME({MYTABLE.MYDATETIME}) in time(21,00,00) to time(23,59,59))
OR (DAYOFWEEK({MYTABLE.MYDATETIME}) = 2 AND TIME({MYTABLE.MYDATETIME) in time(00,00,00) to time(08,59,59))
THEN DateTime(date({%NextDay}),time(09,00,00))
Or, the even better way would be to just stuff ALL of the logic into the SQL Expression and do away with the formula altogether.
Considering Sunday is 1
And the first 7 is the week we want to back
7 = 1 week
14 = 2 weeks
The last Number (1) is 1 for Sunday, 2 for Monday, 3 for Tuestday
Last Sunday 1 week ago
Today - 7 + ( 7 - WEEKDAY(TODAY) )+1
Last Monday 2 weeks ago
Today - 14 + ( 7 - WEEKDAY(TODAY) )+2
So this 2 formulas give me MONDAY LAST WEEK and SUNDAY LAST WEEK.
EvaluateAfter({DATETIMEFROMMYDB}) ;
If DayOfWeek ({DATETIMEFROMMYDB}) In [crFriday,crSaturday,crSunday,crMonday]
then
IF DayOfWeek ({DATETIMEFROMMYDB}) In [crFriday]
AND TIME({DATETIMEFROMMYDB}) >= time(21,00,00)
then //your code here
Else if Not(DayOfWeek ({DATETIMEFROMMYDB}) In [crFriday] )
AND (TIME({DATETIMEFROMMYDB}) >= time(00,00,00) AND TIME({DATETIMEFROMMYDB}) <= time(23,59,59))
then //your code here
Else if DayOfWeek ({DATETIMEFROMMYDB})In [crMonday]
AND TIME({DATETIMEFROMMYDB}) < time(09,00,00)
then //your code here

Get last day of the month in Ruby

I made new object Date.new with args (year, month). After create ruby added 01 number of day to this object by default. Is there any way to add not first day, but last day of month that i passed as arg(e.g. 28 if it will be 02month or 31 if it will be 01month) ?
use Date.civil
With Date.civil(y, m, d) or its alias .new(y, m, d), you can create a new Date object. The values for day (d) and month (m) can be negative in which case they count backwards from the end of the year and the end of the month respectively.
=> Date.civil(2010, 02, -1)
=> Sun, 28 Feb 2010
>> Date.civil(2010, -1, -5)
=> Mon, 27 Dec 2010
To get the end of the month you can also use ActiveSupport's helper end_of_month.
# Require extensions explicitly if you are not in a Rails environment
require 'active_support/core_ext'
p Time.now.utc.end_of_month # => 2013-01-31 23:59:59 UTC
p Date.today.end_of_month # => Thu, 31 Jan 2013
You can find out more on end_of_month in the Rails API Docs.
So I was searching in Google for the same thing here...
I wasn't happy with above so my solution after reading documentation
in RUBY-DOC was:
Example to get 10/31/2014
Date.new(2014,10,1).next_month.prev_day
require "date"
def find_last_day_of_month(_date)
if(_date.instance_of? String)
#end_of_the_month = Date.parse(_date.next_month.strftime("%Y-%m-01")) - 1
else if(_date.instance_of? Date)
#end_of_the_month = _date.next_month.strftime("%Y-%m-01") - 1
end
return #end_of_the_month
end
find_last_day_of_month("2018-01-01")
This is another way to find
You can do something like that:
def last_day_of_month?
(Time.zone.now.month + 1.day) > Time.zone.now.month
end
Time.zone.now.day if last_day-of_month?
This is my Time based solution. I have a personal preference to it compared to Date although the Date solutions proposed above read somehow better.
reference_time ||= Time.now
return (Time.new(reference_time.year, (reference_time.month % 12) + 1) - 1).day
btw for December you can see that year is not flipped. But this is irrelevant for the question because december always has 31 day. And for February year does not need flipping. So if you have another use case that needs year to be correct, then make sure to also change year.
Here is taking the first and third answers to find the last day of the previous month.
today_c = Date.civil(Date.today.prev_month.year, -1, -1)
p today_c

Business date/holiday handling

I've posted this question for C# but I may be working in Ruby instead. So I'm asking the same question about Ruby:
I'm looking for a Ruby class/library/module that works similarly to the Perl module Date::Manip as far as business/holiday dates. Using that module in Perl, I can pass it a date and find out whether it's a business day (ie, Mon-Fri) or a holiday. Holidays are very simple to define in a config file (see Date::Manip::Holidays). You can enter a 'fixed' date that applies to every year like:
12/25 = Christmas
or 'dynamic' dates for every year like:
last Monday in May = Memorial Day
or 'fixed' dates for a given year like:
5/22/2010 = Bob's Wedding
You can also pass in a date and get back the next/previous business day (which is any day that's not a weekend and not a holiday).
Does anyone know of anything like that in the Ruby world?
You may use the holidays-gem.
http://rubygems.org/gems/holidays
Some national (and regional) holidays are already predefined, you may define your own holiday definitions.
The business_time gem should do what you need.
The example at bottom of the README doc is a good starting example:
require 'rubygems'
require 'active_support'
require 'business_time'
# We can adjust the start and end time of our business hours
BusinessTime::Config.beginning_of_workday = "8:30 am"
BusinessTime::Config.end_of_workday = "5:30 pm"
# and we can add holidays that don't count as business days
# July 5 in 2010 is a monday that the U.S. takes off because
# our independence day falls on that Sunday.
three_day_weekend = Date.parse("July 5th, 2010")
BusinessTime::Config.holidays << three_day_weekend
friday_afternoon = Time.parse("July 2nd, 2010, 4:50 pm")
tuesday_morning = 1.business_hour.after(friday_afternoon)
You probably going to need the chronic gem to help you build the holiday dates from your config file. However YMMV because your example last monday in may doesn't work in chronic. Hackaround is do something like this:
# last monday in May (2010)
Chronic.parse('last monday', :now => Time.parse('2010-06-01'))
And look at the tickle gem which works on top of chronic for a way to add recurring events.
/I3az/
You could take a look at my Workpattern gem. It allows you to specify working and resting times. It was aimed at producing a "Calendar" like is used in planning tools such as Microsoft Project and Primavera P6, so you can specify right down to the minute.
Here is a simple example:
Create a new Workpattern mywp=Workpattern.new('My Workpattern',2011,10) This is for 10 years from 2011 but you can make it longer or shorter.
Tell it you want the Weekends to be resting and that you also want to rest during the week so you work between 9 and 12 in the morning and 1 and 6 in the afternoon.
mywp.resting(:days => :weekend)
mywp.resting(:days =>:weekday, :from_time=>Workpattern.clock(0,0),:to_time=>Workpattern.clock(8,59))
mywp.resting(:days =>:weekday, :from_time=>Workpattern.clock(12,0),:to_time=>Workpattern.clock(12,59))
mywp.resting(:days =>:weekday, :from_time=>Workpattern.clock(18,0),:to_time=>Workpattern.clock(23,59))
Now just calculate using minutes
mydate=DateTime.civil(2011,9,1,9,0)
result_date = mywp.calc(mydate,1920) # => 6/9/11#18:00
1920 is 4 days * 8 hours a day * 60 minutes and hour.
I wrote the gem to learn Ruby - only scratched the surface.
Check out the biz gem.
Here's an example configuration:
require 'biz'
Biz.configure do |config|
config.hours = {
mon: {'09:00' => '17:00'},
tue: {'00:00' => '24:00'},
wed: {'09:00' => '17:00'},
thu: {'09:00' => '12:00', '13:00' => '17:00'},
sat: {'10:00' => '14:00'}
}
config.holidays = [Date.new(2014, 1, 1), Date.new(2014, 12, 25)]
config.time_zone = 'America/Los_Angeles'
end
When you use the optional core extensions, it's as easy as the following to find out if a date is a business day:
require 'biz/core_ext'
Date.new(2014, 12, 25).business_day? # => false

Resources