How to access index value of array in conditional test using Ruby - ruby

Background: I am trying to write a simple function that generates a list of calendar days, which I have mostly working except for one if/else loop.
The relevant variables and their initial declaration values:
monthsOfYear = %w[January February March April May June July August September October November December]
currentMonthName = "" # empty string
daysInMonth = 0 # integer
The relevant loop:
monthsOfYear.each do |month| #loop through each month
# first get the current month name
currentMonthName = "#{month}" # reads month name from monthsOfYear array
if ??month == 3 || 5 || 8 || 10 ?? # April, June, September, November
daysInMonth = 30
elsif ??month == 1?? # February
if isLeapYear
daysInMonth = 29
else
daysInMonth = 28
end
else # All the rest
daysInMonth = 31
end
I've marked the part that I'm having trouble with between ?? ??
Basically, I am trying to figure out how to access the numerical value of the index as it loops and test whether that index number matches the few specific cases. I have searched the documentation extensively trying to find a method that returns the index number value (NOT the value stored at x index), in other words I want to be able to read x in Array[x], not what's stored at Array[x]
Perhaps in this specific case it would be better to test whether month == "April" || "June" || "September" || "November" rather than trying to build the case from parsing the array index number?
But in general, what method can be called to find out the index number value? Or is that even possible?

Joel's answer is a better implementation but to keep with your code and to answer your question, Enumerable has an an each_with_index method (Enumberable#each_with_index):
monthsOfYear.each_with_index do |month, index|
then you can use index in your if/else conditionals. Note that arrays are zero based so January is actually going to be 0.

To get the index of an array item, use the index method:
monthsOfYear = [ "January", "February", "March", ... ]
monthsOfYear.index("February") #=> 1
If you're looking for date calculations specifically,
Ruby has a built-in way:
Date.new(date.year, date.month, -1).mday #=> the number of days in the month
If you're looking to iterate with the month and index, Anthony's answer is correct.
monthsOfYear.each_with_index do |month, index| {
...
# The first loop: month = "January", index = 0
...
}
If you're looking for a way to improve your code, use a case statement:
case month
when "April", "June", "September", "November"
daysInMonth = 30
when "February"
if isLeapYear
daysInMonth = 29
else
daysInMonth = 28
end
else
daysInMonth = 31
end
In Ruby, you can set anything equal to the result of a case statement, and also a case statement can match numbers, so it's possible to write:
daysInMonth = case monthsOfYear.index(currentMonthName)
when 3, 5, 8, 10
30
when 1
isLeapYear ? 29 : 28
else
31
end

Related

Comparison using Ruby's .days.ago seems to use reverse logic

Can anyone help me make sense of this, please?
I am getting a very weird behaviour (reverse logic), when I am trying to use the following code.
require 'active_support/all'
c = {
id: 5,
years_of_experience: 4,
github_points: 293,
languages: ['C', 'Ruby', 'Python', 'Clojure'],
date_applied: 5.days.ago.to_date,
age: 26
}
c["date_applied"] > 15.days.ago.to_date - #works
c["date_applied"] < 15.days.ago.to_date - #doesnt work
c["date_applied"] gives a date value stored in a hash.
The latter makes more logical sense, but the first returns the right answer.
The code's behavior is correct, but I think I understand the confusion.
You're reading
c["date_applied"] > 15.days.ago
as:
Is the date applied more than 15 days ago?
and
c["date_applied"] < 15.days.ago
as:
Is the date applied less than 15 days ago?
and it's giving you the reverse of the answer you expect, right?
If that's the case, you should take a moment to understand how time comparisons operate. When you type date1 > date2, you're actually saying,
If I plot date1 and date2 on a number line with time increasing from left to right,
is date1 to the right of date2?
This is the same as when you type 2 > 1. It means,
If I plot 1 and 2 on a number line with the numbers increasing from left to right,
is 2 to the right of 1?
Given that this is how time comparisons operate, let's reexamine your code.
require 'active_support/all'
c = { date_applied: 5.days.ago.to_date }
c[:date_applied] > 15.days.ago.to_date
Correctly interpreted, this says
Is the date 5 days ago further rightward on a left-to-right timeline than the date 15 days ago?
and the answer is yes, or true.
If, on the other hand, you were to incorrectly interpret this as
Is 5 days ago more than 15 days ago?
you would get (or expect to get) the mistaken answer of no, or false.
The correct way to think about the task in English is to reframe the question of
Is date d more than n days ago?
and instead think of it as
Is date d earlier than the date n days ago?
and the correct code becomes apparent:
d.to_date < n.days.ago.to_date
If I understood your question correctly, this should explain it.
irb ## ruby-1.9.3-p448
require 'active_support/time'
c = {
id: 5,
years_of_experience: 4,
github_points: 293,
languages: ['C', 'Ruby', 'Python', 'Clojure'],
date_applied: 5.days.ago.to_date,
age: 26
}
(c[:date_applied] > 15.days.ago.to_date) - #true
(c[:date_applied] < 15.days.ago.to_date) - #false
###or you can try it by adding your own private methods###
class Fixnum
def days
self * 60 * 60 * 24 # we store seconds in a day
end
def ago
Time.now - self
end
end

Find the closest date from string

I'm trying to convert strings like "Sep 11, Oct 31, Feb 28" into DateTime instances as part of a screen-scraper. Using DateTime.parse() works fine apart from when the data goes across years, and it naively (and probably correctly) returns a date in the current year.
For example the following test case.
test "dateA convert next year" do
TimeService.stubs(:now).returns(Time.new(2013, 12, 30, 9, 30))
assert_equal(Date.new(2014, 1, 2), Extraction.dateA("Jan 2"))
end
I updated my method to look at what would be date with year + 1, and return the closest to 'now' - this works fine. However it feels a bit ugly, and I'm looking for a more elegant solution.
def Extraction.dateA(content)
return_value = DateTime.parse(content)
next_year = return_value.change(:year => return_value.year + 1)
now = TimeService.now.to_i
if (next_year.to_i - now).abs < (return_value.to_i - now).abs then
return_value = next_year
end
return_value
end
TimeService.now is just a utility to return current time to help with stubbing.
Excuse my ruby, I'm new to it.
I think this works as intended, allowing for closest date in previous year as well:
module Extraction
def Extraction.date_a(content)
parsed_date = DateTime.parse(content)
now = DateTime.now
dates = [ parsed_date, parsed_date.next_year, parsed_date.prev_year ]
dates.min_by { |d| ( d - now ).abs }
end
end
A few points:
Changed method name to date_a, just a Ruby convention that differs from Java.
I made use of some built-in methods next_year and prev_year on DateTime
I used a time difference metric and selected date with the minimal value of it from three candidate dates (this is what min_by does). This is simpler code than the conditional switching, especially with three dates to consider.
I forgot about min_by originally, I don't use it often, but it's a very good fit for this problem.
Note there is a pathological case - "29 Feb". If it appears correctly in text, by its nature it will define which year is valid, and it won't parse if current year is e.g. 2015.

Sum of values between start and end date in MATLAB

If, in MATLAB, I have a set of start and end dates that represent a contiguous period of time, how would I take a separate daily time series and accumulate any values from said time series over each start/end pair?
Is this something that can be done with accumarray()? It wasn't clear to me whether it would be efficient to construct the vector that groups each element of the time series by start/end pair.
Inputs
Start Date End Date
01/01/12 01/31/12
02/01/12 02/28/12
...
Date Value
01/01/12 23
01/02/12 87
01/03/12 5
01/04/12 12
...
02/01/12 4
Output
Start Date End Date Value
01/01/12 01/31/12 127
02/01/12 02/28/12 4
...
For consecutive periods of time, the following approach might work. Note that the strings containing dates are cellstrings and, for consecutive data, only the first column of your start date /end date matrix is necesssary.
Furthermore, note that I separated your time series data into two variables for the sake of clarity.
dateBins = {...
'01/01/12';
'02/01/12';
'03/01/12';
'04/01/12'};
dates = {
'01/01/12'
'01/02/12'
'01/03/12'
'01/04/12'
'02/01/12' };
values = [
23
87
5
12
4];
With these variables, the following code
[thrash, idx] = histc(datenum(dates), datenum(dateBins))
accumVal = accumarray(idx, values);
result = zeros(length(dateBins), 1);
result(1:length(accumVal),1) = accumVal;
will result in:
result =
127
4
0
0
Assuming you have already got two vectors with the start dates and end dates in a format that you can use to compare and you just want to count how many occurrences there are in each cateogory, then it is quite straightforward:
% Your data
Dates = 25*rand(10,1);
StartDate = [1 10 20];
EndDate = [9 19 29];
% The Calculation
Result = zeros(size(StartDate)); %Initialization
for d = 1:length(startdate)
idx = dates >= StartDate & dates <= EndDate;
Result(d) = sum(idx);
end
Note that this will require that you store your dates in a comparable format.
I would iterate over each pair of start/end dates. Then pick out the index start/stop pairs and sum them. If you use datestrs, you can make the following less brittle, while allowing for more flexibility in how you represent times that cross years, etc.
start_date = {'01/01/12'};
end_date={'01/31/12'};
datevec={'01/01/12','01/02/12','01/03/12','01/31/12'};
values=[23,87,5,12];
for i=1:numel(start_date)
is = find(ismember(datevec,start_date{i})==1);
ie = find(ismember(datevec,end_date{i})==1);
sum(values(is:ie))
end

How to loop numerically + month and day over the past X years?

I need to loop through all of the days and months for the past couple decades numerically as well as to have the name of the month and day for each date. Obviously a few series of loops can accomplish this, but I wanted to know the most concise ruby-like way to accomplish this.
Essentially I'd need output like this for each day over the past X years:
3 January 2011 and 1/3/2011
What's the cleanest approach?
Dates can work as a range, so it's fairly easy to iterate over a range. The only real trick is how to output them as a formatted string, which can be found in the Date#strftime method, which is documented here.
from_date = Date.new(2011, 1, 1)
to_date = Date.new(2011, 1, 10)
(from_date..to_date).each { |d| puts d.strftime("%-d %B %Y and %-m/%-d/%Y") }
# => 1 January 2011 and 1/1/2011
# => 2 January 2011 and 1/2/2011
# => ...
# => 9 January 2011 and 1/9/2011
# => 10 January 2011 and 1/10/2011
(Note: I recall having some bad luck a ways back with unpadded percent formats like %-d in Windows, but if the above doesn't work and you want them unpadded in that environment you can remove the dash and employ your own workarounds.)
Given start_date & end_date:
(start_date..end_date).each do |date|
# do things with date
end
as David said, this is possible because of Date#succ. You can use Date#strftime to get the date in any format you'd like.
See if you can construct a Range where the min and max are Date objects, then call .each on the range. If the Date object supports the succ method this should work.

Is there a more concise way to combine many comparisons in Ruby?

I currently have this if statement:
if Time.now.day == 1 and Time.now.hour == 0 and Time.now.minute == 0 and Time.now.second == 0
Is there a more concise way to do this?
You can use Time#to_a to convert your time to an array (sec, min, hour, day, month, year, wday, yday, isdst, zone):
Time.now.to_a # => [20, 57, 16, 30, 11, 2010, 2, 334, false, "CET"]
then slice off just the part you want to match against:
Time.now.to_a[0,4] # => [20, 57, 16, 30]
Your particular check can then be made as concise as this:
if Time.now.to_a[0,4] == [0,0,0,1]
[:day,:hour,:minute,:second].map{|s|Time.now.send s}==[1,0,0,0]
The standard Ruby Time library is pretty spare, so I would simply add my own method to make this easier:
class Time
def beginning_of_month
Time.local(self.year, self.month, 1)
end
def beginning_of_month?
self == beginning_of_month
end
end
so you could then write:
if Time.now.beginning_of_month?
How about?
Time.now.strftime('%d %H:%M:%S') == '01 00:00:00'
I like this because it's very self-documenting; It's obvious you're comparing to a certain day at midnight.
Explanation: The strftime() method makes it easy to output a custom date and/or time string. This format is outputting the day of the month, hour, minute and second.
Time.now.strftime('%d %H:%M:%S') #=> "01 16:54:24"
You can compare it to a time you created:
if Time.now == Time.at(0)
or
if Time.now == Time.utc(2000,"jan",1,20,15,1)
I guess that you are trying to make something similar to cron, i.e. you are in an endless loop and checking whether time has come to perform certain action.
If that is the case, I don't think you should rely that your timestamp check will fall exactly on the first second of the day. What would happen if is delayed (for any reason), and first it fires on 23:59:59, and the next cycle happens on 00:00:01 instead of :00? You will fail to perform desired action whatsoever.
Also, you would want to include some sleep, so that your empty loop wouldn't consume all you resources while waiting.
What you could do instead, is keep the last action timestamp, and compare now with the next action timestamp, performing the action if now >= next_timestamp. Something like:
last_action_on = Time.at(0)
loop do
now = Time.now
next_action_on = Time.local(now.year, now.month, 1) # beginning of the current day
if last_action_on < next_action_on && now >= next_action_on
last_action_on = now
do_action
end
sleep 1
end
%w(day==1 hour==0 minute==0 second==0).all? { |e| eval "Time.now.#{e}" }

Resources