blob: 79fb2d59bee7af0862c2d30acd853b7c58037111 [file] [log] [blame]
/*
This code implements one part of functonality of
free available library PL/Vision. Please look www.quest.com
This library isn't optimalized for big numbers, for working
with n days (n > 10000), can be slow (on my P4 31ms).
Original author: Steven Feuerstein, 1996 - 2002
PostgreSQL implementation author: Pavel Stehule, 2006
This module is under BSD Licence
History:
1.0. first public version 13. March 2006
*/
#define PLVDATE_VERSION "PostgreSQL PLVdate, version 1.0, March 2006"
#include "postgres.h"
#include "utils/date.h"
#include "utils/builtins.h"
#include "utils/nabstime.h"
#include <sys/time.h>
#include <stdlib.h>
#include "orafunc.h"
/*
* External (defined in PgSQL datetime.c (timestamp utils))
*/
extern PGDLLIMPORT char *days[];
Datum plvdate_add_bizdays (PG_FUNCTION_ARGS);
Datum plvdate_nearest_bizday (PG_FUNCTION_ARGS);
Datum plvdate_next_bizday (PG_FUNCTION_ARGS);
Datum plvdate_bizdays_between (PG_FUNCTION_ARGS);
Datum plvdate_prev_bizday (PG_FUNCTION_ARGS);
Datum plvdate_isbizday (PG_FUNCTION_ARGS);
Datum plvdate_set_nonbizday_dow (PG_FUNCTION_ARGS);
Datum plvdate_unset_nonbizday_dow (PG_FUNCTION_ARGS);
Datum plvdate_set_nonbizday_day (PG_FUNCTION_ARGS);
Datum plvdate_unset_nonbizday_day (PG_FUNCTION_ARGS);
Datum plvdate_use_easter (PG_FUNCTION_ARGS);
Datum plvdate_using_easter (PG_FUNCTION_ARGS);
Datum plvdate_include_start (PG_FUNCTION_ARGS);
Datum plvdate_including_start (PG_FUNCTION_ARGS);
Datum plvdate_default_holidays (PG_FUNCTION_ARGS);
Datum plvdate_version (PG_FUNCTION_ARGS);
Datum plvdate_days_inmonth (PG_FUNCTION_ARGS);
Datum plvdate_isleapyear (PG_FUNCTION_ARGS);
PG_FUNCTION_INFO_V1(plvdate_add_bizdays);
PG_FUNCTION_INFO_V1(plvdate_nearest_bizday);
PG_FUNCTION_INFO_V1(plvdate_next_bizday);
PG_FUNCTION_INFO_V1(plvdate_bizdays_between);
PG_FUNCTION_INFO_V1(plvdate_prev_bizday);
PG_FUNCTION_INFO_V1(plvdate_isbizday);
PG_FUNCTION_INFO_V1(plvdate_set_nonbizday_dow);
PG_FUNCTION_INFO_V1(plvdate_unset_nonbizday_dow);
PG_FUNCTION_INFO_V1(plvdate_set_nonbizday_day);
PG_FUNCTION_INFO_V1(plvdate_unset_nonbizday_day);
PG_FUNCTION_INFO_V1(plvdate_use_easter);
PG_FUNCTION_INFO_V1(plvdate_using_easter);
PG_FUNCTION_INFO_V1(plvdate_include_start);
PG_FUNCTION_INFO_V1(plvdate_including_start);
PG_FUNCTION_INFO_V1(plvdate_default_holidays);
PG_FUNCTION_INFO_V1(plvdate_version);
PG_FUNCTION_INFO_V1(plvdate_days_inmonth);
PG_FUNCTION_INFO_V1(plvdate_isleapyear);
#define CHECK_SEQ_SEARCH(_l, _s) \
do { \
if ((_l) < 0) { \
ereport(ERROR, \
(errcode(ERRCODE_INVALID_DATETIME_FORMAT), \
errmsg("invalid value for %s", (_s)))); \
} \
} while (0)
extern int ora_seq_search(const char *name, /*const*/ char **array, int max);
#define SUNDAY (1 << 0)
#define SATURDAY (1 << 6)
static unsigned char nonbizdays = SUNDAY | SATURDAY;
static bool use_easter = true;
static bool include_start = true;
#define MAX_holidays 30
#define MAX_EXCEPTIONS 50
typedef struct {
char day;
char month;
} holiday_desc;
typedef struct {
unsigned char nonbizdays;
bool use_easter;
holiday_desc *holidays;
int holidays_c;
} cultural_info;
static holiday_desc holidays[MAX_holidays]; /* sorted array */
static DateADT exceptions[MAX_EXCEPTIONS]; /* sorted array */
static int holidays_c = 0;
static int exceptions_c = 0;
static holiday_desc czech_holidays[] = {
{1,1}, // Novy rok
{1,5}, // Svatek prace
{8,5}, // Den osvobozeni
{5,7}, // Den slovanskych verozvestu
{6,7}, // Den upaleni mistra Jana Husa
{28,9}, // Den ceske statnosti
{28,10}, // Den vzniku samostatneho ceskoslovenskeho statu
{17,11}, // Den boje za svobodu a demokracii
{24,12}, // Stedry den
{25,12}, // 1. svatek vanocni
{26,12} // 2. svatek vanocni
};
static holiday_desc germany_holidays[] = {
{1,1},{1,5},{25,5},{4,6},{5,6},
{15,8},{3,10},{25,12},{26,12}
};
static holiday_desc poland_holidays[] = {
{1,1},{1,5},{3,5},{15,6},{15,8},
{1,11},{11,11},{25,12},{26,12}
};
static holiday_desc austria_holidays[] = {
{1,1},{6,1},{1,5},{25,5},{4,6},
{5,6},{15,6},{15,8},{26,10},{1,11},
{8,12},{25,12},{26,12}
};
static holiday_desc slovakia_holidays[] = {
{1,1},{6,1},{1,5},{8,5},{5,7},
{29,8},{1,9},{15,9},{1,11},{17,11},
{24,12},{25,12},{26,12}
};
static holiday_desc russian_holidays[] = {
{1,1},{2,1},{3,1},{4,1},{5,1},
{7,1},{23,2},{8,3},{1,5},{9,5},
{12,6}, {4,11}
};
static holiday_desc england_holidays[] = {
{1,1},{2,1},{1,5},{29,5},{28,8},
{25,12},{26,12}
};
static holiday_desc usa_holidays[] = {
{1,1},{16,1},{20,2},{29,5},{4,7},
{4,9},{9,10},{11,11},{23,11},{25,12}
};
cultural_info defaults_ci[] = {
{SUNDAY | SATURDAY, true, czech_holidays, 11},
{SUNDAY | SATURDAY, true, germany_holidays, 9},
{SUNDAY | SATURDAY, true, poland_holidays, 9},
{SUNDAY | SATURDAY, true, austria_holidays, 13},
{SUNDAY | SATURDAY, true, slovakia_holidays, 13},
{SUNDAY | SATURDAY, false, russian_holidays, 12},
{SUNDAY | SATURDAY, true, england_holidays, 7},
{SUNDAY | SATURDAY, false, usa_holidays, 10}
};
static char *states[] = {
"Czech", "Germany", "Poland",
"Austria", "Slovakia", "Russia",
"Gb", "Usa",
NULL,
};
static int
dateadt_comp(const void* a, const void* b)
{
DateADT *_a = (DateADT*)a;
DateADT *_b = (DateADT*)b;
return *_a - *_b;
}
static int
holiday_desc_comp(const void* a, const void* b)
{
int result;
if (0 == (result = ((holiday_desc*)a)->month - ((holiday_desc*)b)->month))
result = ((holiday_desc*)a)->day - ((holiday_desc*)b)->day;
return result;
}
static void
easter_sunday(int year, int* dd, int* mm)
{
int b, d, e, q;
if (year < 1900 || year > 2099)
ereport(ERROR,
(errcode(ERRCODE_DATETIME_VALUE_OUT_OF_RANGE),
errmsg("date is out of range"),
errdetail("Easter is defined only for years between 1900 and 2099")));
b = 255 - 11 * (year % 19);
d = ((b - 21) % 30) + 21;
if (d > 38) d -= 1;
e = (year + year/4 + d + 1) % 7;
q = d + 7 - e;
if (q < 32)
{
*dd = q; *mm = 3;
}
else
{
*dd = q - 31; *mm = 4;
}
}
static Datum
ora_add_bizdays(DateADT day, int days)
{
int d, dx;
int y, m, auxd;
holiday_desc hd;
d = j2day(day+POSTGRES_EPOCH_JDATE);
dx = days > 0? 1 : -1;
while (days != 0)
{
d = (d+dx) % 7;
d = (d < 0) ? 6:d;
day += dx;
if ((1 << d) & nonbizdays)
continue;
if (NULL != bsearch(&day, exceptions, exceptions_c,
sizeof(DateADT), dateadt_comp))
continue;
j2date(day + POSTGRES_EPOCH_JDATE, &y, &m, &auxd);
hd.day = (char) auxd;
hd.month = (char) m;
if (use_easter && (m == 3 || m == 4))
{
easter_sunday(y, &auxd, &m);
if (m == hd.month && (auxd == hd.day || d+1 == hd.day))
continue;
}
if (NULL != bsearch(&hd, holidays, holidays_c,
sizeof(holiday_desc), holiday_desc_comp))
continue;
days -= dx;
}
return day;
}
static int
ora_diff_bizdays(DateADT day1, DateADT day2)
{
int d, days;
int y, m, auxd;
holiday_desc hd;
int cycle_c = 0;
bool start_is_bizday = false;
DateADT aux_day;
if (day1 > day2)
{
aux_day = day1;
day1 = day2; day2 = aux_day;
}
d = j2day(day1+POSTGRES_EPOCH_JDATE);
days = 0;
while (day1 <= day2)
{
++ cycle_c;
d = (d+1) % 7;
d = (d < 0) ? 6:d;
day1 += 1;
if ((1 << d) & nonbizdays)
continue;
if (NULL != bsearch(&day1, exceptions, exceptions_c,
sizeof(DateADT), dateadt_comp))
continue;
j2date(day1 + POSTGRES_EPOCH_JDATE, &y, &m, &auxd);
hd.day = (char) auxd;
hd.month = (char) m;
if (use_easter && (m == 3 || m == 4))
{
easter_sunday(y, &auxd, &m);
if (m == hd.month && (auxd == hd.day || d+1 == hd.day))
continue;
}
if (NULL != bsearch(&hd, holidays, holidays_c,
sizeof(holiday_desc), holiday_desc_comp))
continue;
days += 1;
if (cycle_c == 1)
start_is_bizday = true;
}
if (include_start && start_is_bizday && days >= 1)
days -= 1;
return days;
}
/****************************************************************
* PLVdate.add_bizdays
*
* Syntax:
* FUNCTION add_bizdays(IN dt DATE, IN days int) RETURNS DATE;
*
* Purpouse:
* Get the date created by adding <n> business days to a date
*
****************************************************************/
Datum
plvdate_add_bizdays (PG_FUNCTION_ARGS)
{
DateADT day = PG_GETARG_DATEADT(0);
int days = PG_GETARG_INT32(1);
PG_RETURN_DATEADT(ora_add_bizdays(day,days));
}
/****************************************************************
* PLVdate.nearest_bizday
*
* Syntax:
* FUNCTION nearest_bizday(IN dt DATE) RETURNS DATE;
*
* Purpouse:
* Get the nearest business date to a given date, user defined
*
****************************************************************/
Datum
plvdate_nearest_bizday (PG_FUNCTION_ARGS)
{
DateADT dt = PG_GETARG_DATEADT(0);
DateADT d1, d2, res;
d1 = ora_add_bizdays(dt, -1);
d2 = ora_add_bizdays(dt, 1);
if ((dt - d1) > (d2 - dt))
res = d2;
else
res = d1;
PG_RETURN_DATEADT(res);
}
/****************************************************************
* PLVdate.next_bizday
*
* Syntax:
* FUNCTION next_bizday(IN dt DATE) RETURNS DATE;
*
* Purpouse:
* Get the next business date from a given date, user defined
*
****************************************************************/
Datum
plvdate_next_bizday (PG_FUNCTION_ARGS)
{
DateADT day = PG_GETARG_DATEADT(0);
PG_RETURN_DATEADT(ora_add_bizdays(day,1));
}
/****************************************************************
* PLVdate.bizdays_between
*
* Syntax:
* FUNCTION bizdays_between(IN dt1 DATE, IN dt2 DATE)
* RETURNS int;
*
* Purpouse:
* Get the number of business days between two dates
*
****************************************************************/
Datum
plvdate_bizdays_between (PG_FUNCTION_ARGS)
{
DateADT day1 = PG_GETARG_DATEADT(0);
DateADT day2 = PG_GETARG_DATEADT(1);
PG_RETURN_INT32(ora_diff_bizdays(day1,day2));
}
/****************************************************************
* PLVdate.prev_bizday
*
* Syntax:
* FUNCTION prev_bizday(IN dt DATE) RETURNS date;
*
* Purpouse:
* Get the previous business date from a given date, user
* defined
*
****************************************************************/
Datum
plvdate_prev_bizday (PG_FUNCTION_ARGS)
{
DateADT day = PG_GETARG_DATEADT(0);
PG_RETURN_DATEADT(ora_add_bizdays(day,-1));
}
/****************************************************************
* PLVdate.isbizday
*
* Syntax:
* FUNCTION isbizday(IN dt DATE) RETURNS bool;
*
* Purpouse:
* Call this function to determine if a date is a business day
*
****************************************************************/
Datum
plvdate_isbizday (PG_FUNCTION_ARGS)
{
DateADT day = PG_GETARG_DATEADT(0);
int y, m, d;
holiday_desc hd;
if (0 != ((1 << j2day(day+POSTGRES_EPOCH_JDATE)) & nonbizdays))
return false;
if (NULL != bsearch(&day, exceptions, exceptions_c,
sizeof(DateADT), dateadt_comp))
return false;
j2date(day + POSTGRES_EPOCH_JDATE, &y, &m, &d);
hd.month = m; hd.day = d;
if (use_easter && (m == 3 || m == 4))
{
easter_sunday(y, &d, &m);
if (m == hd.month && (d == hd.day || d+1 == hd.day))
return false;
}
PG_RETURN_BOOL (NULL == bsearch(&hd, holidays, holidays_c,
sizeof(holiday_desc), holiday_desc_comp));
}
/****************************************************************
* PLVdate.set_nonbizday
*
* Syntax:
* FUNCTION set_nonbizday(IN dow VARCHAR) RETURNS void;
*
* Purpouse:
* Set day of week as non bussines day
*
****************************************************************/
Datum
plvdate_set_nonbizday_dow (PG_FUNCTION_ARGS)
{
unsigned char check;
text *day_txt = PG_GETARG_TEXT_PP(0);
int d = ora_seq_search(VARDATA_ANY(day_txt), days, VARSIZE_ANY_EXHDR(day_txt));
CHECK_SEQ_SEARCH(d, "DAY/Day/day");
check = nonbizdays | (1 << d);
if (check == 0x7f)
ereport(ERROR,
(errcode(ERRCODE_DATA_EXCEPTION),
errmsg("nonbizday registeration error"),
errdetail("Constraint violation."),
errhint("One day in week have to be bizday.")));
nonbizdays = nonbizdays | (1 << d);
PG_RETURN_VOID();
}
/****************************************************************
* PLVdate.unset_nonbizday
*
* Syntax:
* FUNCTION unset_nonbizday(IN dow VARCHAR) RETURNS void;
*
* Purpouse:
* Unset day of week as non bussines day
*
****************************************************************/
Datum
plvdate_unset_nonbizday_dow (PG_FUNCTION_ARGS)
{
text *day_txt = PG_GETARG_TEXT_PP(0);
int d = ora_seq_search(VARDATA_ANY(day_txt), days, VARSIZE_ANY_EXHDR(day_txt));
CHECK_SEQ_SEARCH(d, "DAY/Day/day");
nonbizdays = (nonbizdays | (1 << d)) ^ (1 << d);
PG_RETURN_VOID();
}
/****************************************************************
* PLVdate.set_nonbizday
*
* Syntax:
* FUNCTION set_nonbizday(IN day DATE) RETURNS void;
* FUNCTION set_nonbizday(IN day DATE, IN repeat := false BOOL) RETURNS void;
*
* Purpouse:
* Set day as non bussines day, second arg specify year's
* periodicity
*
****************************************************************/
Datum
plvdate_set_nonbizday_day (PG_FUNCTION_ARGS)
{
DateADT arg1 = PG_GETARG_DATEADT(0);
bool arg2 = PG_GETARG_BOOL(1);
int y, m, d;
holiday_desc hd;
if (arg2)
{
if (holidays_c == MAX_holidays)
ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("nonbizday registeration error"),
errdetail("Too much registered nonbizdays."),
errhint("Increase MAX_holidays in 'plvdate.c'.")));
j2date(arg1 + POSTGRES_EPOCH_JDATE, &y, &m, &d);
hd.month = m; hd.day = d;
if (NULL != bsearch(&hd, holidays, holidays_c, sizeof(holiday_desc), holiday_desc_comp))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
errmsg("nonbizday registeration error"),
errdetail("Date is registered.")));
holidays[holidays_c].month = m;
holidays[holidays_c].day = d;
holidays_c += 1;
qsort(holidays, holidays_c, sizeof(holiday_desc), holiday_desc_comp);
}
else
{
if (exceptions_c == MAX_EXCEPTIONS)
ereport(ERROR,
(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
errmsg("nonbizday registeration error"),
errdetail("Too much registered nonrepeated nonbizdays."),
errhint("Increase MAX_EXCEPTIONS in 'plvdate.c'.")));
if (NULL != bsearch(&arg1, exceptions, exceptions_c, sizeof(DateADT), dateadt_comp))
ereport(ERROR,
(errcode(ERRCODE_DUPLICATE_OBJECT),
errmsg("nonbizday registeration error"),
errdetail("Date is registered.")));
exceptions[exceptions_c++] = arg1;
qsort(exceptions, exceptions_c, sizeof(DateADT), dateadt_comp);
}
PG_RETURN_VOID();
}
/****************************************************************
* PLVdate.unset_nonbizday
*
* Syntax:
* FUNCTION unset_nonbizday(IN day DATE) RETURNS void;
* FUNCTION unset_nonbizday(IN day DATE, IN repeat := false BOOL) RETURNS void;
*
* Purpouse:
* Unset day as non bussines day, second arg specify year's
* periodicity
*
****************************************************************/
Datum
plvdate_unset_nonbizday_day (PG_FUNCTION_ARGS)
{
DateADT arg1 = PG_GETARG_DATEADT(0);
bool arg2 = PG_GETARG_BOOL(1);
int y, m, d;
bool found = false;
int i;
if (arg2)
{
j2date(arg1 + POSTGRES_EPOCH_JDATE, &y, &m, &d);
for (i = 0; i < holidays_c; i++)
{
if (!found && holidays[i].month == m && holidays[i].day == d)
found = true;
else if (found)
{
holidays[i-1].month = holidays[i].month;
holidays[i-1].day = holidays[i].day;
}
}
if (found)
holidays_c -= 1;
}
else
{
for (i = 0; i < exceptions_c; i++)
if (!found && exceptions[i] == arg1)
found = true;
else if (found)
exceptions[i-1] = exceptions[i];
if (found)
exceptions_c -= 1;
}
if (!found)
ereport(ERROR,
(errcode(ERRCODE_UNDEFINED_OBJECT),
errmsg("nonbizday unregisteration error"),
errdetail("Nonbizday not found.")));
PG_RETURN_VOID();
}
/****************************************************************
* PLVdate.use_easter
*
* Syntax:
* FUNCTION unuse_easter() RETURNS void;
* FUNCTION use_easter() RETURNS void;
* FUNCTION use_easter(IN bool) RETURNS void
*
* Purpouse:
* Have to use easter as nonbizday?
*
****************************************************************/
Datum
plvdate_use_easter (PG_FUNCTION_ARGS)
{
use_easter = PG_GETARG_BOOL(0);
PG_RETURN_VOID();
}
/****************************************************************
* PLVdate.using_easter
*
* Syntax:
* FUNCTION using_easter() RETURNS bool
*
* Purpouse:
* Use it easter as nonbizday?
*
****************************************************************/
Datum
plvdate_using_easter (PG_FUNCTION_ARGS)
{
PG_RETURN_BOOL(use_easter);
}
/****************************************************************
* PLVdate.include_start
*
* Syntax:
* FUNCTION include_start() RETURNS void;
* FUNCTION noinclude_start() RETURNS void;
* FUNCTION include_start(IN bool) RETURNS void
*
* Purpouse:
* Have to include current day in bizdays_between calculation?
*
****************************************************************/
Datum
plvdate_include_start (PG_FUNCTION_ARGS)
{
include_start = PG_GETARG_BOOL(0);
PG_RETURN_VOID();
}
/****************************************************************
* PLVdate.including_start
*
* Syntax:
* FUNCTION including_start() RETURNS bool
*
* Purpouse:
* include current day in bizdays_between calculation?
*
****************************************************************/
Datum
plvdate_including_start (PG_FUNCTION_ARGS)
{
PG_RETURN_BOOL(include_start);
}
/*
* Load some national configurations
*
*/
Datum
plvdate_default_holidays (PG_FUNCTION_ARGS)
{
text *country = PG_GETARG_TEXT_PP(0);
int c = ora_seq_search(VARDATA_ANY(country), states, VARSIZE_ANY_EXHDR(country));
CHECK_SEQ_SEARCH(c, "STATE/State/state");
nonbizdays = defaults_ci[c].nonbizdays;
use_easter = defaults_ci[c].use_easter;
exceptions_c = 0;
holidays_c = defaults_ci[c].holidays_c;
memcpy(holidays, defaults_ci[c].holidays, holidays_c*sizeof(holiday_desc));
PG_RETURN_VOID();
}
/*
* helper maintaince functions
*/
Datum
plvdate_version (PG_FUNCTION_ARGS)
{
PG_RETURN_CSTRING(PLVDATE_VERSION);
}
/****************************************************************
* PLVdate.days_inmonth
*
* Syntax:
* FUNCTION days_inmonth(date) RETURNS integer
*
* Purpouse:
* Returns month's length
*
****************************************************************/
Datum
plvdate_days_inmonth(PG_FUNCTION_ARGS)
{
DateADT day = PG_GETARG_DATEADT(0);
int result;
int y, m, d;
j2date(day + POSTGRES_EPOCH_JDATE, &y, &m, &d);
result = date2j(y, m+1, 1) - date2j(y, m, 1);
PG_RETURN_INT32(result);
}
/****************************************************************
* PLVdate.isleapyear
*
* Syntax:
* FUNCTION isleapyear() RETURNS bool
*
* Purpouse:
* Returns true, if year is leap
*
****************************************************************/
Datum
plvdate_isleapyear(PG_FUNCTION_ARGS)
{
DateADT day = PG_GETARG_DATEADT(0);
int y, m, d;
bool result;
j2date(day + POSTGRES_EPOCH_JDATE, &y, &m, &d);
result = ((( y % 4) == 0) && ((y % 100) != 0)) || ((y / 400) == 0);
PG_RETURN_BOOL(result);
}
/****************************************************************
* PLVdate.set_nonbizdays
*
* Syntax:
* FUNCTION set_nonbizdays(IN dow bool[7]) RETURNS void;
*
* Purpouse:
* Set pattern bussines/nonbussines days in week
*
****************************************************************/
/****************************************************************
* PLVdate.set_nonbizday
*
* Syntax:
* FUNCTION set_nonbizdays(IN days DATE[]) RETURNS void;
* FUNCTION set_nonbizdays(IN days DATE[], IN repeat := false BOOL) RETURNS void;
*
* Purpouse:
* Set days as non bussines day, second arg specify year's
* periodicity
*
****************************************************************/
/****************************************************************
* PLVdate.display
*
* Syntax:
* FUNCTION display() RETURNS void;
*
* Purpouse:
* Show current calendar
*
****************************************************************/