Saturday, November 19, 2016

A Year and Quarter Data Type for PostgreSQL

For some applications, time periods are needed that represent quarters of the year.  Although quarters could be represented by using an exact date data type and constraining the month and day values (i.e., to the first day of each quarter) or by using date ranges in PostgreSQL, these representations do not allow easy computation of the difference between two dates (in quarters) or the addition of specific number of quarters to a date to produce a new date.

The code below shows the implementation of custom composite data type in Postgres that represents quarters of a year. This data type is named "quarter". The implementation supports addition of a number of quarters to a "quarter" date to produce a new "quarter" date, and supports calculation of the difference between two "quarter" dates. Comparison operators are also defined for use in expressions using "quarter" dates and to allow indexing of data tables by "quarter" values.

The Postgres code to define this data type, and arithmetic and comparison operators, is shown below. Details on the creation of a custom data type and operators can be found in the Postgres documentation.

The definition of the "quarter" data type is simply:

create type quarter as (
 year integer,
 quarter integer
 );

To support addition and subtraction of an integer number of quarters to (or from) a "quarter" data type, a function must be defined to carry out this calculation. The arguments to the function are a "quarter" data type and the number of quarters; the second argument may be either positive or negative.

create or replace function qtr_add(
 qtr quarter,
 addqtr integer)
 returns quarter as
$BODY$
DECLARE
 qtr2 quarter;
BEGIN
 -- Adjust the year
 if addqtr >= 0 then
  qtr2.year := qtr.year + div((qtr.quarter + addqtr-1), 4);
 else
  qtr2.year := qtr.year - div(((4 - qtr.quarter) - addqtr), 4);
 end if;
 -- Adjust the quarter
 qtr2.quarter := mod(qtr.quarter + addqtr - 1, 4) +1;
 if qtr2.quarter < 1 then 
  qtr2.quarter := qtr2.quarter + 4;
 end if;
 --
 return qtr2;
END;
$BODY$
 language plpgsql
 immutable leakproof strict;


Subtracting two "quarter" dates can be carried out in a simple expression, but a function is defined to carry out this calculation because it is used for both the mathematical and comparison operators.

create or replace function qtr_diff(
 qtr1 quarter,
 qtr2 quarter)
 returns integer as
$BODY$
BEGIN
 return 4 * (qtr1.year - qtr2.year) + (qtr1.quarter - qtr2.quarter);
END;
$BODY$
 language plpgsql
 immutable leakproof strict;

To support comparison operators (<, <=, =, <>, >=, and >), a function is defined to compare two "quarter" dates. This function returns -1, 0, or 1 depending on whether its first argument is less than, equal to, or greater than its second argument.

create or replace function quarter_comp(
 qtr1 quarter,
 qtr2 quarter)
 returns integer as
$BODY$
DECLARE
 diff integer;
BEGIN
 diff := qtr_diff(qtr1, qtr2);
 if diff = 0 then
  return 0;
 elsif diff < 1 then
  return -1;
 end if;
 return 1;
END;
$BODY$
 language plpgsql
 immutable leakproof strict;


Each comparison operation is carried out by an individual function that uses the 'quarter_comp()' function.

-- --------------------------------------------------------------------------
-- qtr_lt()
-- --------------------------------------------------------------------------
create or replace function qtr_lt(
 qtr1 quarter,
 qtr2 quarter)
 returns boolean as
$BODY$
BEGIN
 return quarter_comp(qtr1, qtr2) < 0;
END;
$BODY$
 language plpgsql
 immutable leakproof strict;


-- --------------------------------------------------------------------------
-- qtr_lte()
-- --------------------------------------------------------------------------
create or replace function qtr_lte(
 qtr1 quarter,
 qtr2 quarter)
 returns boolean as
$BODY$
BEGIN
 return quarter_comp(qtr1, qtr2) <= 0;
END;
$BODY$
 language plpgsql
 immutable leakproof strict;


-- --------------------------------------------------------------------------
-- qtr_eq()
-- --------------------------------------------------------------------------
create or replace function qtr_eq(
 qtr1 quarter,
 qtr2 quarter)
 returns boolean as
$BODY$
BEGIN
 return quarter_comp(qtr1, qtr2) = 0;
END;
$BODY$
 language plpgsql
 immutable leakproof strict;


-- --------------------------------------------------------------------------
-- qtr_ne()
-- --------------------------------------------------------------------------
create or replace function qtr_ne(
 qtr1 quarter,
 qtr2 quarter)
 returns boolean as
$BODY$
BEGIN
 return quarter_comp(qtr1, qtr2) <> 0;
END;
$BODY$
 language plpgsql
 immutable leakproof strict;


-- --------------------------------------------------------------------------
-- qtr_gte()
-- --------------------------------------------------------------------------
create or replace function qtr_gte(
 qtr1 quarter,
 qtr2 quarter)
 returns boolean as
$BODY$
BEGIN
 return quarter_comp(qtr1, qtr2) >= 0;
END;
$BODY$
 language plpgsql
 immutable leakproof strict;



-- --------------------------------------------------------------------------
-- qtr_gt()
-- --------------------------------------------------------------------------
create or replace function qtr_gt(
 qtr1 quarter,
 qtr2 quarter)
 returns boolean as
$BODY$
BEGIN
 return quarter_comp(qtr1, qtr2) > 0;
END;
$BODY$
 language plpgsql
 immutable leakproof strict;


After the functions to carry out the arithmetic and comparison operations have been defined, the operators themselves can be defined.

-- --------------------------------------------------------------------------
-- +
-- --------------------------------------------------------------------------
create operator +(
 procedure = qtr_add,
 leftarg = quarter,
 rightarg = integer,
 commutator = +);

-- --------------------------------------------------------------------------
-- -
-- --------------------------------------------------------------------------
create operator -(
 procedure = qtr_diff,
 leftarg = quarter,
 rightarg = quarter);


-- --------------------------------------------------------------------------
-- <
-- --------------------------------------------------------------------------
create operator <(
 procedure = qtr_lt,
 leftarg = quarter,
 rightarg = quarter,
 commutator = >,
 negator = >=);

-- --------------------------------------------------------------------------
-- <=
-- --------------------------------------------------------------------------
create operator <=(
 procedure = qtr_lte,
 leftarg = quarter,
 rightarg = quarter,
 commutator = >=,
 negator = >);

-- --------------------------------------------------------------------------
-- =
-- --------------------------------------------------------------------------
create operator =(
 procedure = qtr_eq,
 leftarg = quarter,
 rightarg = quarter,
 commutator = =,
 negator = <>);

-- --------------------------------------------------------------------------
-- <>
-- --------------------------------------------------------------------------
create operator <>(
 procedure = qtr_ne,
 leftarg = quarter,
 rightarg = quarter,
 commutator = <>,
 negator = =);

-- --------------------------------------------------------------------------
-- >
-- --------------------------------------------------------------------------
create operator >(
 procedure = qtr_gt,
 leftarg = quarter,
 rightarg = quarter,
 commutator = <,
 negator = <=);

-- --------------------------------------------------------------------------
-- >=
-- --------------------------------------------------------------------------
create operator >=(
 procedure = qtr_gte,
 leftarg = quarter,
 rightarg = quarter,
 commutator = >=,
 negator = <);


To allow "quarter" dates to be used in indexes, an operator class must be defined.

create operator class quarter_ops default
for type quarter using btree as
   operator 1  <,
   operator 2  <=,
   operator 3  =,
   operator 4  >=,
   operator 5  >,
   function 1  quarter_comp(quarter, quarter);

The same approach could be used to define a custom "month" data type.  The code for a "month" data type would be almost identical, with changes needed only to the data type name used and to the functions 'qtr_add()' and 'qtr_diff()'.