Q: | |
19-17. | It’s a bit more complex than it sounds at first. You need to use NEXT_DAY to obtain the nearest earlier date and nearest later date. Then, perform date arithmetic to see how far away each is from the specified date. Finally, compare the “distances” and return the appropriate date. Here is one implementation:
/* Filename on web page: nearday.sf */
CREATE OR REPLACE FUNCTION nearestday (
yourdate IN DATE, dayname IN VARCHAR2)
RETURN DATE
IS
before_date DATE := NEXT_DAY (yourdate-7, dayname);
after_date DATE := NEXT_DAY (yourdate, dayname);
before_diff NUMBER;
after_diff NUMBER;
BEGIN
before_diff := yourdate - before_date;
after_diff := yourdate - after_date;
IF before_diff < after_diff
THEN
RETURN before_date;
ELSE
RETURN after_date;
END IF;
END;
/ |
Q: | |
19-18. | You can take one of two approaches: Compute the number of Saturdays and Sundays between the two dates and subtract that from the total. I’ll call this the “brute-force” method. Execute a loop from start date to end date and keep count, ignoring the weekend. I’ll call this the “smart” method.
Here is a solution following the brute-force method:
/* Filename on web page: bizbetwn2.sf */
CREATE OR REPLACE FUNCTION bizdays_betwn (
ld_date1 DATE,
ld_date2 DATE)
RETURN NUMBER AS
ln_diff NUMBER;
ln_bus_days NUMBER;
ld_date DATE;
BEGIN
ln_diff := ABS (TO_NUMBER (ld_date2 - ld_date1)) + 1;
IF ld_date1 < ld_date2
THEN
ld_date := ld_date1;
ELSE
ld_date := ld_date2;
END IF;
ln_bus_days := ln_diff;
FOR i IN 1 .. ln_diff
LOOP
IF RTRIM (UPPER (TO_CHAR (ld_date + i - 1, 'DAY'))) IN
('SATURDAY', 'SUNDAY')
THEN
ln_bus_days := ln_bus_days - 1;
END IF;
END LOOP;
RETURN ln_bus_days;
END;
/ The following function implements the smart method; it is the more concise and efficient of the two and was created by Solomon Yakobson:
/* Filename on web page: bizbetwn.sf */
/* Provided by Solomon Yakobson */
CREATE OR REPLACE FUNCTION bizdays_between (
start_date IN DATE, end_date IN DATE)
RETURN INTEGER
IS
v_sundays INTEGER :=
NEXT_DAY (end_date - 7, 'SUNDAY') -
NEXT_DAY (start_date - 1, 'SUNDAY'),
v_saturdays INTEGER :=
NEXT_DAY (end_date - 7, 'SATURDAY') -
NEXT_DAY (start_date - 1, 'SATURDAY'),
BEGIN
RETURN (
end_date -
start_date -
(v_sundays + v_saturdays)/7 - 1);
END;
/ |
Q: | |
19-19. | Here is one possible implementation, building upon the smart method (in this version, I have also isolated the common “count for day” logic into its own function):
/* Filename on web page: bizbetwn3.sf */
CREATE TABLE holiday (dt DATE);
CREATE OR REPLACE FUNCTION daycount (
start_date IN DATE, end_date IN DATE, dayname IN VARCHAR2)
RETURN INTEGER
IS
BEGIN
RETURN (
NEXT_DAY (end_date - 7, dayname) -
NEXT_DAY (start_date - 1, dayname)
);
END;
/
CREATE OR REPLACE FUNCTION bizdays_between (
start_date IN DATE, end_date IN DATE)
RETURN INTEGER
IS
holiday_count INTEGER;
BEGIN
SELECT COUNT(*) INTO holiday_count
FROM holiday
WHERE dt BETWEEN start_date AND end_date;
RETURN (
end_date -
start_date -
(daycount (start_date, end_date, 'SUNDAY') +
daycount (start_date, end_date, 'SATURDAY'))/7 - 1 - holiday_count);
END;
/ |
Q: | |
19-20. | One possible implementation is shown next. Here’s one complication to keep in mind: if you start on a Monday and ask NEXT_DAY to find the next Monday, it finds it one week in the future:
/* Filename on web page: nthday.sf */
CREATE OR REPLACE FUNCTION nthday (
yourdate IN DATE, dayname IN VARCHAR2, nthday IN INTEGER := 1)
RETURN DATE
IS
/* Start with last day of previous month. */
retval DATE := TRUNC (yourdate, 'MONTH') - 1;
BEGIN
/* Start search in "nth week". */
RETURN NEXT_DAY (retval + (nthday - 1) * 7, dayname);
END;
/ |
Q: | |
19-21. | Here is a brute force implementation I threw together:
/* Filename on web page: maxdt1.sql */
DECLARE
old_date DATE;
curr_date DATE := SYSDATE;
BEGIN
LOOP
old_date := curr_date;
curr_date := curr_date + 1;
END LOOP;
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE (SQLERRM);
DBMS_OUTPUT.PUT_LINE ('Last date: ' ||
TO_CHAR (old_Date, 'MM-DD-YYYY'));
END;
/ A number of my readers have pointed out that this is a rather slow implementation and offered dramatically more efficient versions. Here is one:
/* Filename on web page: maxdt2.sql */
DECLARE
/* Initial implementation by Eva Blinder, modified by SF. */
curr_date DATE := SYSDATE;
last_date DATE;
step PLS_INTEGER := 1000;
BEGIN
LOOP
BEGIN
last_date := curr_date;
curr_date := curr_date + step;
EXCEPTION
WHEN OTHERS
THEN
IF step = 1
THEN
DBMS_OUTPUT.put_line (
'Last date: ' ||
TO_CHAR (
last_date,
'Month DD, YYYY'
)
);
EXIT;
ELSE
step := step / 2;
curr_date := last_date;
END IF;
END;
END LOOP;
END;
/ In this algorithm, the potential last date is moved forward by 1,000 days at a time. When you hit an error, you go back to the last good date and step forward by half that number of days. Keep doing that (reducing your step forward by half) until the step is 1, and that means you’ve gone as far forward as possible. |
Q: | |
19-22. | Here is one possible implementation:
/* Filename on web page: addmths.sf */
CREATE OR REPLACE FUNCTION new_add_months (
date_in IN DATE, months_shift IN NUMBER)
RETURN DATE
IS
retval DATE;
BEGIN
retval := ADD_MONTHS (date_in, months_shift);
/* Is original date the last day of its month? */
IF date_in = LAST_DAY (date_in)
THEN
retval :=
LEAST (retval,
TO_DATE (
TO_CHAR (date_in, 'DD') ||
TO_CHAR (retval, 'MMYYYY'),
'DDMMYYYY'));
END IF;
/* Return the shifted date */
RETURN retval;
EXCEPTION
WHEN OTHERS THEN RETURN retval;
END new_add_months;
/ The core logic here says that if the date you supplied is the last day of its month, construct a “physical” last day in the resulting month. If that day number exceeds the last actual day in the month, let the exception section trap the error and return the ADD_MONTHS standard value. Otherwise, return the earliest of the ADD_MONTHS value (the last day in the month) and the date with the matching day number. When you run the same lastday.sql script (saved to lastday2) using new_add_months instead of ADD_MONTHS, here are the results:
SQL> @lastday2
30-JAN-99 -> 28-FEB-99
27-FEB-99 -> 27-MAR-99
31-JAN-99 -> 28-FEB-99
28-FEB-99 -> 28-MAR-99 As you can see, 28-FEB-99 now shifts to 28-MAR-99, which is the desired behavior. |
Q: | |
19-23. | One possible implementation, relying on an index-by table of formats, is in the datemgr.pkg file on the book’s web page. You could also store the various formats in a database table and then use a query to loop through the valid format masks. This is more flexible (change the formats through SQL or a GUI interface: no need to change the code), but much slower. A middle-ground approach is to store the formats in a table, but read them into an index-by table in the package initialization section. |