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.
Related
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.
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.
I am brand new to Oracle. I have figured out most of what I need but one field is driving me absolutely crazy. Seems like it should be simple but I think my brain is fried and I just can't get my head around it. I am trying to produce a Sales report. I am doing all kinds of crazy things based on the Invoice Date. The last thing I need to do is to be able to create a Week Number so I can report on weekly sales year vs year. For purposes of this report my fiscal year starts exactly on December 1 (regardless of day of week it falls on) every year. For example, Dec 1-7 will be week 1, etc. I can get the week number using various functions but all of them are based on either calendar year or ISO weeks. How can I easily generate a field that will give me the number of the week since December 1? Thanks so much for your help.
Forget about the default week number formats as that won't work for this specific requirement. I'd probably subtract the previous 1 December from invoice date and divide that by 7. Round down, add 1 and you should be fine.
select floor(
(
trunc(invoiceDate) -
case
-- if December is current month, than use 1st of this month
when to_char(invoiceDate, 'MM') = '12' then trunc(invoiceDate, 'MM')
-- else, use 1st December of previous year
else add_months(trunc(invoiceDate, 'YYYY'), -1)
end
) / 7
) + 1
from dual;
I need a function which will do interval arithmetic, dealing "correctly" with the different number of days in a month. For my version of "correctly" - see below!
First try
select to_date('31-May-2014') + interval '1' months from dual
This returns an error, because there is no 31st June. I understand that this behaviour is expected due to the ANSI standard.
Second try
select add_months(to_date('31-May-2014'),1) from dual
This correctly (in my use case) returns 30th June 2014, which is great. BUT
select add_months(to_date('28-Feb-2014'),1) from dual
returns 31st March 2014, when I want 28th March 2014.
Background
This has to do with legal deadlines. The deadlines are expressed in law as a number of months (say, 3) from a base date. If the base date is last day of the month and three months later the month is longer, then the deadline does NOT extend to the end of the longer month (as per the add_months function). However, if the base date is last day of the month and three months later the month is shorter, then the deadline expires on the last day of the shorter month.
Question
Is there a function that does what I need?
I have intervals (year to month) stored in a table, so preferably the function would look like:
add_interval_correctly(basedate DATE, intervaltoadd INTERVAL YEAR TO MONTH)
You can write a function, try this one:
CREATE OR REPLACE FUNCTION ADD_LAW_MONTHS(Base IN DATE, Months IN NUMBER)
RETURN DATE DETERMINISTIC IS
DATE_NOT_VALID_FOR_MONTH EXCEPTION;
PRAGMA EXCEPTION_INIT(DATE_NOT_VALID_FOR_MONTH, -1839);
BEGIN
RETURN Base + (Months * INTERVAL '1' MONTH);
EXCEPTION
WHEN DATE_NOT_VALID_FOR_MONTH THEN
RETURN ADD_MONTHS(Base, Months);
END;
SELECT
ADD_LAW_MONTHS(TO_DATE('28-Feb-2014'),1),
ADD_LAW_MONTHS(TO_DATE('31-May-2014'),1)
FROM dual;
Based on Wernfried's answer, above, but accepts INTERVAL data type rather than NUMBER of months.
create or replace FUNCTION ADD_LAW_MONTHS(Base IN DATE, IntervalToAdd IN INTERVAL YEAR TO MONTH)
RETURN DATE DETERMINISTIC IS
DATE_NOT_VALID_FOR_MONTH EXCEPTION;
PRAGMA EXCEPTION_INIT(DATE_NOT_VALID_FOR_MONTH, -1839);
BEGIN
RETURN Base + IntervalToAdd;
EXCEPTION
WHEN DATE_NOT_VALID_FOR_MONTH THEN
RETURN ADD_LAW_MONTHS(Base - 1, IntervalToAdd);
END;
I'm trying to get the next 5 business days, using sysdate in the WHERE (ie.
where trunc(teststart) between trunc(sysdate) and trunc(sysdate+4)),
but if the range includes Friday, it needs to count weekend days, Saturday & Sunday.
What is the command that will tell you the day of the week when looking at the sysdate or sysdate+3 (or any number)? How would you accomplish something like this?
You can't do this calculation because there is no way that oracle know what exactly is a business day. This is because every region has your own hollidays. To do this you will have to create a table calendar and put all days on it marking which ones is business day. Then you can just join your table with this calendar table using a between clause
#JorgeCampos point is well taken about day 1. You indicate with MON 0 that Monday is the start of the week, NLS settings on the DB and your locale determine this.
select to_char(sysdate, 'D'), to_char(sysdate,'DAY') from dual;
gives the result you asked for. We have a table that gets the next year populated in December for the next year. Includes all holidays, etc.
JCAL_JOB_DATE NOT NULL DATE
JCAL_USER_ID VARCHAR2(30)
JCAL_ACTIVITY_DATE DATE
JCAL_BUSINESS_DAY VARCHAR2(2)
JCAL_HOLIDAY VARCHAR2(2)
JCAL_WEEKEND VARCHAR2(2)
This is used to forecast jobs in the future, and some recurring jobs that do not want to run on holidays or weekends for example.
select jcal_job_date from jcal where
JCAL_JOB_DATE > sysdate
and JCAL_BUSINESS_DAY='Y'
and rownum <6;
This is one not-so-good way to get the fifth business day. Fetch You can use a lot of other functions to streamline this.
This kind of table is VERY useful to determine the same information (work, holiday, weekend) in the past.
Where I am we have holidays that fall on the last Thursday of November and extra holidays are added in there. Programming for that is hard when compared to a simple lookup that works great.