in rails i need to get all records based on a date range, ignoring the year - sql

i am trying to get all records within a date range in rails that fall after a given start date and before a given end date, ignoring the year. the start date will just be a month. the end of the date range is a month and date.
the following example is to get users with a hire date month equal to january or greater, but less than 2(february) / 28(day).
i was trying this but it doesnt work:
users.where('extract(month from hire_date) >= ?', 1).where('extract(month from hire_date) <= ? AND extract(day from hire_date) <= ?', 2, 28)
the reason this doesnt work is it will exclude users, for example who fall on 1/29, because their day in january is not less than 28.
is there a good way to make this work, ignoring the year on the date field?

I would use to_char to transform the dates into strings including just the month and the day. For example 2021-12-31 could be translated into "12-31". That string can then be compared to the range of date strings you are interested in.
users.where("to_char(hire_date, 'MM-DD') BETWEEN '01-01' AND '02-29'")
Note that this, in theory, this would also match invalid date strings like '01-40'. But I guess it is safe to assume that to_char with a valid date will never return such invalid date strings.

Let me start by saying that I'm not familiar with ruby-on-rails, sql, or activerecord but this question really piqued my interest because it contains a number of complexities that I've never had to deal with. Most notably:
How to accommodate leap years when the test range and target date may
have differing leap year statuses.
How to deal with a hire date of February 29th. This may seem to be
the same as the first item on my list, but it has its own unique
nuances.
How to deal with ranges that started in one year and then overlap into the next.
I did some searching myself and there seems to be very little information out there on the subject, particularly any that seems to properly deal with the various complexities listed above, so I decided to see if I could come up with a logical approach of my own. It seems like it may be a novel approach, but ultimately I decided that converting month and day to float values would allow an easy way to address the leap year issue as well as the issue with February 29th, and that testing the date ranges using an if/else statement along with opposing boolean operations would solve the ranges that overlap years.
You may have to extrapolate and reconfigure for your specific needs, but here's the general idea written out in pure ruby. It's rather verbose, but I did so just to try and make it more clear what I'm doing. I could be made much more compact:
hire_date = Time.new(2004, 1, 22)
provided_start_date = Time.new(2008, 12, 22)
day_value = (60*60*24) #60 seconds * 60 minutes * 24 hours = 1 day
num_days = 30
calculated_end_date = provided_start_date + num_days*day_value
start_range = provided_start_date.month + provided_start_date.day/100.0
end_range = calculated_end_date.month + calculated_end_date.day/100.0
day_float = hire_date.month + hire_date.day/100.0
if start_range < end_range
result = day_float >= start_range and day_float <= end_range
else
result = day_float >= start_range or day_float <= end_range
end
result

So i figured out something that worked for this.
given the following:
start_date = 01 Jan 2021
end_date = 02 Feb 2021
Users.where("(extract(month from hire_date) * 100 + extract(day from hire_date)) BETWEEN ? AND ?", ((start_date.month * 100) + start_date.day), ((end_date.month * 100) + end_date.day))
start date and end date can be any dates and this should work to get all users between, since SQL between is inclusive, it will also get users whos end and start dates fall on the start / end date, ignoring year.

Related

Select records when month and day are less than current date

I am using SQL Server Mgmt Studio. I need to create a formula that will only select records when the month and day of a date/time field, LAW_TAEEMASTER.MASTR_ENTRY, is less than or equal to the current month and day. For example, If LAW_TAEEMASTER.MASTR_ENTRY = 8/20/2015 and current date = 7/6/2017, exclude. If LAW_TAEEMASTER.MASTR_ENTRY = 12/21/2014 and current date = 1/15/2017, include. Date formatting is my weakness. Here is what I have tried:
select LAW_TAEEMASTER.MASTR_ENTRY
from LAW_TAEEMASTER
WHERE day(LAW_TAEEMASTER.MASTR_ENTRY) <= DAY(GETDATE()) and MONTH(LAW_TAEEMASTER.MASTR_ENTRY) <= MONTH(getdate())
and
select LAW_TAEEMASTER.MASTR_ENTRY
from LAW_TAEEMASTER
WHERE AND DATEPART(DAY,LAW_TAEEMASTER.MASTR_ENTRY) <= DATEPART(DAY,GETDATE()) AND DATEPART(MONTH,LAW_TAEEMASTER.MASTR_ENTRY) <= DATEPART(MONTH,GETDATE())
but these just evaluate the month and day separately and as a number, greater than or less than, so it's excluding records. How can I get SQL to recognize these as part of a date and just ignore the year?
I would do this using month() and day():
where month(LAW_TAEEMASTER.MASTR_ENTRY) * 100 + day(LAW_TAEEMASTER.MASTR_ENTRY) < month(getdate()) * 100 + day(getdate())
If performance is an issue, I would recommend a computed column with an index.
Explain the logic and code:
Use CONVERT(varchar(5),<your_date> ,110)). The 110 arguement means USA standard time and have an ouput like mm-dd-yyyy. Then we only take the first five character which is the month and day of the date.
110 = mm-dd-yyyy
read more about cast and convert here
my sample table:
MASTER_ENTRY
2017-08-30
2017-07-05
2017-07-04
Then I executed this query
SELECT LAW_TAEEMASTER.MASTR_ENTRY
FROM LAW_TAEEMASTER
WHERE CONVERT(varchar(5),LAW_TAEEMASTER.MASTR_ENTRY ,110) <= CONVERT(varchar(5),GETDATE() ,110)
result:where month and day equal or earlier current month and day
MASTER_ENTRY
2017-07-05
2017-07-04
I hope this helps and welcome to StackOverflow. If you find this answer or any other answer solves your problem please mark it as the solution. This will help the community and fellow programmers who run into the same problem in the future. Thanks.

SQL ORACLE Get week numbers from multiple datetime rows

I have 70.000 rows of data, including a date time column (YYYY-MM-DD HH24-MM-SS.).
I want to split this data into 3 separate columns; Hour, day and Week number.
The date time column name is 'REGISTRATIONDATE' from the table 'CONTRACTS'.
This is what I have so far for the day and hour columns:
SELECT substr(REGISTRATIONDATE, 0, 10) AS "Date",
substr(REGISTRATIONDATE, 11, 9) AS "Hour"
FROM CONTRACTS;
I have seen the options to get a week number for specific dates, this assignment concerns 70.000 dates so this is not an option.
You (the OP) still have to explain what week number to assign to the first few days in a year, until the first Monday of the year. Do you assign a week number for the prior calendar year? In a Comment I asked about January 1, 2017, as an example; that was a Sunday. The week from January 2 to January 8 of 2017 is "week 1" according to your definition; what week number do you assign to Sunday, January 1, 2017?
The straightforward calculation below assigns to it week number 0. Other than that, the computation is trivial.
Notes: To find the Monday of the week for any given date dt, we can use trunc(dt, 'iw'). iw stands for ISO Week, standard week which starts on Monday and ends on Sunday.
Then: To find the first Monday of the year, we can start with the date January 7 and ask for the Monday of the week in which January 7 falls. (I won't explain that one - it's easy logic and it has nothing to do with programming.)
To input a fixed date, the best way is with the date literal syntax: date '2017-01-07' for January 7. Please check the Oracle documentation for "date literals" if you are not familiar with it.
So: to find the week number for any date dt, compute
1 + ( trunc(dt, 'iw') - trunc(date '2017-01-07', 'iw') ) / 7
This formula finds the Monday of the ISO Week of dt and subtracts the first Monday of the year - using Oracle date arithmetic, where the difference between two dates is the number of days between them. So to find the number of weeks we divide by 7; and to have the first Monday be assigned the number 1, instead of 0, we need to add 1 to the result of dividing by 7.
The other issue you will have to address is to convert your strings into dates. The best solution would be to fix the data model itself (change the data type of the column so that it is DATE instead of VARCHAR2); then all the bits of data you need could be extracted more easily, you would make sure you don't have dates like '2017-02-29 12:30:00' in your data (currently, if you do, you will have a very hard time making any date calculations work), queries will be a lot faster, etc. Anyway, that's an entirely different issue so I'll leave it out of this discussion.
Assuming your REGISTRATIONDATE if formatted as 'MM/DD/YYYY'
the simples (and the faster ) query is based ond to to_char(to_date(REGISTRATIONDATE,'MM/DD/YYYY'),'WW')
(otherwise convert you column in a proper date and perform the conversio to week number)
SELECT substr(REGISTRATIONDATE, 0, 10) AS "Date",
substr(REGISTRATIONDATE, 11, 9) AS "Hour",
to_char(to_date(REGISTRATIONDATE,'MM/DD/YYYY'),'WW') as "Week"
FROM CONTRACTS;
This is messy, but it looks like it works:
to_char(
to_date(RegistrationDate,'YYYY-MM-DD HH24-MI-SS') +
to_number(to_char(trunc(to_date(RegistrationDate,'YYYY-MM-DD HH24-MI-SS'),'YEAR'),'D'))
- 2,
'WW')
On the outside you have the solution previous given by others but using the correct date format. In the middle there is an adjustment of a certain number of days to adjust for where the 1st Jan falls. The trunc part gets the first of Jan from the date, the 'D' gets the weekday of 1st Jan. Since 1 represents Sunday, we have to use -2 to get what we need.
EDIT: I may delete this answer later, but it looks to me that the one from #mathguy is the best. See also the comments on that answer for how to extend to a general solution.
But first you need to:
Decide what to do dates in Jan before the first Monday, and
Resolve the underlying problems in the date which prevent it being converted to dates.
On point 1, if assigning week 0 is not acceptable (you want week 52/53) it gets a bit more complicated, but we'll still be able to help.
As I see it, on point 2, either there is something systematically wrong (perhaps they are timestamps and include fractions of a second) or there are isolated cases of invalid data.
Either the length, or the format, or the specific values don't compute. The error message you got suggests that at least some of the data is "too long", and the code in my comment should help you locate that.

SQL computing and reusing fiscal year calculation in sql query

I have a condition in my SQL query, using Oracle 11g database, that depends on a plan starting or ending with in a fiscal year:
(BUSPLAN.START_DATE BETWEEN (:YEAR || '-04-01') AND (:YEAR+1 || '-03-31')) OR
(BUSPLAN.END_DATE BETWEEN (:YEAR || '-04-01') AND (:YEAR+1 || '-03-31'))
For now, I am passing in YEAR as a parameter. It can be computed as (pseudocode):
IF CURRENT MONTH IN (JAN, FEB, MAR):
USE CURRENT YEAR // e.g. 2015
ELSE:
USE CURRENT YEAR + 1 // e.g. 2016
Is there a way I could computer the :YEAR parameter within in an SQL query and reuse it for the :YEAR parameter?
CTEs are easy, you can make little tables on the fly. With a 1 row table you just cross join it and then you have that value available every row:
WITH getyear as
(
SELECT
CASE WHEN to_char(sysdate,'mm') in ('01','02','03') THEN
EXTRACT(YEAR FROM sysdate)
ELSE
EXTRACT(YEAR FROM sysdate) + 1
END as ynum from dual
), mydates as
(
SELECT getyear.ynum || '-04-01' as startdate,
getyear.ynum+1 || '-03-31' as enddate
from getyear
)
select
-- your code here
from BUSPLAN, mydates -- this is a cross join
where
(BUSPLAN.START_DATE BETWEEN mydates.startdate AND mydates.enddate) OR
(BUSPLAN.END_DATE BETWEEN mydates.startdate AND mydates.enddate)
note, values statement is probably better if Oracle has values then the first CTE would look like this:
VALUES(CASE WHEN to_char(sysdate,'mm') in ('01','02','03') THEN
EXTRACT(YEAR FROM sysdate)
ELSE
EXTRACT(YEAR FROM sysdate) + 1)
I don't have access to Oracle so I might have bugs typos etc since I didn't test.
In the code you shared there is a problem and a potential problem.
Problem, implicit conversion to date without format string.
In (BUSPLAN.START_DATE BETWEEN (:YEAR || '-04-01') AND (:YEAR+1 || '-03-31')) two strings are being formed and then converted to dates. The conversion to date is going to change depending on the value of NLS_DATE_FORMAT. To insure that the string is converted correctly to_date(:YEAR || '-04-01', 'YYYY-MM-DD').
Potential problem, boundary at the end of the year when time <> midnight.
Oracle's date type holds both date and time. A test like someDate between startDate and endDate will miss all records that happened after midnight on endDate. One simple fix that precludes use of indexes on someDate is trunc(someDate) between startDate and endDate.
A more general approach is to define date ranges and closed open intervals. lowerBound <= aDate < upperBound where lowerBound is the same asstartDateabove andupperBoundisendDate` plus one day.
Note: Some applications used Oracle date columns as dates and always store midnight, if your application is of that sort, then this is not a problem. And check constraints like check (trunc(dateColumn) = dateColumn) would make sure it stays that way.
And now, to answer the question actually asked.
Using subquery factoring (Oracle's terminology) / common table expression (SQL Server's terminology) one can avoid repetition within a query.
Instead of figuring out the proper year, and then using strings to put together dates, the code below starts by getting January 1 at Midnight of the current calendar year, trunc(sysdate, 'YEAR')). Then it adds an offset in months. When the months are Jan, Feb, Mar, the current fiscal year started last year on 4/1, or nine months before the start of this year. The offset is -9. Else the current fiscal year started 4/1 of this calendar year, start of this year plus three months.
Instead of end date, an upper bound is calculated, similar to lower bound, but with the offsets being 12 greater than lower bound to get 4/1 the following year.
with current_fiscal_year as (select add_months(trunc(sysdate, 'YEAR')
, case when extract(month from sysdate) <= 3 then -9 else 3 end) as LowerBound
, add_months(trunc(sysdate, 'YEAR')
, case when extract(month from sysdate) <= 3 then 3 else 15 end) as UpperBound
from dual)
select *
from busplan
cross join current_fiscal_year CFY
where (CFY.LowerBound <= busplan.start_date and busplan.start_date < CFY.UpperBound)
or (CFY.LowerBound <= busplan.end_date and busplan.end_date < CFY.UpperBound)
And yet more unsolicited advise.
The times I've had to deal with fiscal year stuff, avoiding repetition within a query was low hanging fruit. Having the fiscal year calculations consistent and correct among many queries, that was the essence of the work. So I'd recommend a developing PL/SQL package that centralizes fiscal calculations. It might include a function like:
create or replace function GetFiscalYearStart(v_Date in date default sysdate)
return date
as begin
return add_months(trunc(v_Date, 'YEAR')
, case when extract(month from v_Date) <= 3 then -9 else 3 end);
end GetFiscalYearStart;
Then the query above becomes:
select *
from busplan
where (GetFiscalYearStart() <= busplan.start_date
and busplan.start_date < add_months(GetFiscalYearStart(), 12))
or (GetFiscalYearStart() <= busplan.end_date
and busplan.end_date < add_months(GetFiscalYearStart(), 12))

Calculating remaining time to a given benchmark (in weeks) in Oracle

Here's what I'm up to: I have some data that represents a goal that students are supposed to reach by a given benchmark (in this case, June 15th). I'm looking at a transactional table and running some rollup queries that attempt to figure out how many students were 'on track' at a given point in time, using the techniques described in 'Filling Gaps in Data' and 'Filling Gaps in an Inventory Table' to calculate running/cumulative totals and densify the data.
http://download.oracle.com/docs/cd/B28359_01/server.111/b28313/analysis.htm#i1014934
To express student 'progress to the goal' at each row in the table, I'm trying to figure out fractionally how much of the year is left until June 15th, allowing me to compare the running total to a pro-rated version of the goal.
My data looks like:
Measure Goal Year Week Date (YYYY-WW)
106141 400000 2011 42 2011-42
128886 400000 2011 43 2011-43
145449 400000 2011 44 2011-44
156921 400000 2011 45 2011-45
I have a workaround, but it's really hacky and awful.
,case
when year = 2012 then 24 - week
when year = 2011 then (52 - week) + 24
else null
end as weeks_to_june_15
(June 15th is the 24th week of 2012). Is there a more elegant even marginally correct way to do this? Every time I attempt the calculations using date functions I get illegal number or literal does not match format string errors.
It seemed like someone accomplished something somewhat related to what I was trying to do here: https://forums.oracle.com/forums/thread.jspa?threadID=633028
but I'm not sure if rebuilding that date using IYYY and IW would get me where I want to go.
Using Oracle 11gR2 Express and want to do this in the database, if at all possible.
Thanks!
This is going to depend a lot on your definition of a week. The IW and IYYY formats use the ISO standard which state that a week always begins on Monday and lasts 7 days. This means that, by the standard, 1/1/2011 was part of the 52nd week of 2010. If this doesn't work for you, then you need to start by defining exactly what you mean by a week.
Because the ISO standard is that well-defined, you can treat the first day of the week as equivalent to the week itself, which will allow you to use date math on it accurately. Below is a function that does just that. You could do it all in SQL as well, but PL/SQL makes this sort of thing a little simpler and easier to read.
CREATE OR REPLACE FUNCTION week_diff(p_year NUMBER,
p_week NUMBER,
p_end_date DATE)
RETURN NUMBER AS
v_end_date DATE;
v_start_date DATE;
v_weeks NUMBER;
v_last_week DATE;
BEGIN
v_end_date := TRUNC(TO_DATE(p_end_date, 'mm/dd/yyyy'), 'IW');
v_start_date := TRUNC(TO_DATE('2/1/' || p_year, 'mm/dd/yyyy'), 'IYYY')
+ (p_week * 7);
IF TRUNC(v_end_date, 'IYYY') = TRUNC(v_start_date, 'IYYY') THEN
v_weeks := (v_end_date - v_start_date) / 7;
ELSE
v_last_week := TRUNC(TO_DATE('12/31/' || TO_CHAR(v_start_date, 'yyyy'),
'mm/dd/yyyy'), 'IW');
v_weeks := (v_last_week - v_start_date) / 7
+ TO_NUMBER(TO_DATE(v_end_date, 'IW'));
END IF;
RETURN v_weeks;
END week_diff;
trunc (to_date('15-jun-2012') - sysdate)
will give you the number of calendar days between now and next June 15th.
That should get you started.
If you have a stored date/time data item, this expression:
trunc (stored,'DAY')
yields midnight on the preceding Sunday (see http://www.techonthenet.com/oracle/functions/trunc_date.php), so for example,
select count(*) events_per_week,
trunc(event_time,'DAY') week_starting
from event
group by trunc(event_time,'DAY')
will tell you how many events per week you have in the event table. This is a valuable way of summarizing data week by week, because it works over both short and multi-year time periods.
If your business rules require your week to start on Monday, do this instead.
1+trunc(stored-1,'DAY')
If you want a date/time data item from something like '2011-43' then do
to_date('2011-43','YYYY-WW')
or
to_date('2011-43','YYYY-IW')
depending on whether you use ISO week numbers. See http://www.techonthenet.com/oracle/functions/to_date.php.
I much prefer the Sunday-midnight way of identifying a week; it's quite intuitive to read ("Week beginning 13-Nov-2011") and handles end-of-year rollovers in a straightforward way.

SQL that list all birthdays within the next and previous 14 days

I have a MySQL member table, with a DOB field which stores all members' dates of birth in DATE format (Notice: it has the "Year" part)
I'm trying to find the correct SQL to:
List all birthdays within the next 14 days
and another query to:
List all birthdays within the previous 14 days
Directly comparing the current date by:
(DATEDIFF(DOB, now()) <= 14 and DATEDIFF(DOB, now()) >= 0)
will fetch nothing since the current year and the DOB year is different.
However, transforming the DOB to 'this year' won't work at all, because today could be Jan 1 and the candidate could have a DOB of Dec 31 (or vice versa)
It will be great if you can give a hand to help, many thanks! :)
#Eli had a good response, but hardcoding 351 makes it a little confusing and gets off by 1 during leap years.
This checks if birthday (dob) is within next 14 days. First check is if in same year. Second check is if its say Dec 27, you'll want to include Jan dates too.
With DAYOFYEAR( CONCAT(YEAR(NOW()),'-12-31') ), we are deciding whether to use 365 or 366 based on the current year (for leap year).
SELECT dob
FROM birthdays
WHERE DAYOFYEAR(dob) - DAYOFYEAR(NOW()) BETWEEN 0 AND 14
OR
DAYOFYEAR( CONCAT(YEAR(NOW()),'-12-31') ) - ( DAYOFYEAR(NOW()) - DAYOFYEAR(dob) ) BETWEEN 0 AND 14
Here's the simplest code to get the upcoming birthdays for the next x days and previous x days
this query is also not affected by leap-years
SELECT name, date_of_birty
FROM users
WHERE DATE(CONCAT(YEAR(CURDATE()), RIGHT(date_of_birty, 6)))
BETWEEN
DATE_SUB(CURDATE(), INTERVAL 14 DAY)
AND
DATE_ADD(CURDATE(), INTERVAL 14 DAY)
My first thought was it would be easy to just to use DAYOFYEAR and take the difference, but that actually gets kinda trick near the start/end of a yeay. However:
WHERE
DAYOFYEAR(NOW()) - DAYOFYEAR(dob) BETWEEN 0 AND 14
OR DAYOFYEAR(dob) - DAYOFYEAR(NOW()) > 351
Should work, depending on how much you care about leap years. A "better" answer would probably be to extract the DAY() and MONTH() from the dob and use MAKEDATE() to build a date in the current (or potential past/following) year and compare to that.
Easy,
We can obtain the nearer birthday (ie the birthday of this year) by this code:
dateadd(year,datediff(year,dob,getdate()),DOB)
use this in your compares ! it will work.
There are a number of options, I would first try to transform by number of years between current year and row's year (i.e. Add their age).
Another option is day number within the year (but then you have still to worry about the rollover arithmetic or modulo).
This is my query for the 30 days before check:
select id from users where
((TO_DAYS(concat(DATE_FORMAT(NOW(),'%Y'), '-', DATE_FORMAT(date_of_birth, '%m-%d')))-TO_DAYS(NOW()))>=-30
AND (TO_DAYS(concat(DATE_FORMAT(NOW(),'%Y'), '-', DATE_FORMAT(date_of_birth, '%m-%d')))-TO_DAYS(NOW()))<=0)
OR (TO_DAYS(concat(DATE_FORMAT(NOW(),'%Y'), '-', DATE_FORMAT(date_of_birth, '%m-%d')))-TO_DAYS(NOW()))>=(365-31)
and 30 days after:
select id from users where
((TO_DAYS(NOW())-TO_DAYS(concat(DATE_FORMAT(NOW(),'%Y'), '-', DATE_FORMAT(date_of_birth, '%m-%d'))))>=-31
AND (TO_DAYS(NOW())-TO_DAYS(concat(DATE_FORMAT(NOW(),'%Y'), '-', DATE_FORMAT(date_of_birth, '%m-%d'))))<=0)
OR (TO_DAYS(NOW())-TO_DAYS(concat(DATE_FORMAT(NOW(),'%Y'), '-', DATE_FORMAT(date_of_birth, '%m-%d'))))>=(365-30)
My solution is as follow:
select cm.id from users cm where
date(concat(
year(curdate()) - (year(subdate(curdate(), 14)) < year(curdate())
and month(curdate()) < month(cm.birthday)) + (year(adddate(curdate(), 14)) > year(curdate())
and month(curdate()) > month(cm.birthday)), date_format(cm.birthday, '-%m-%d'))) between subdate(curdate(), 14)
and adddate(curdate(), 14);
It looks like it works fine when the period captures the current and next year or the current and previous year