Project: YugabyteDB (App Definition and Development / Database)
File: age.md

title: Function age() returns integer [YSQL] headerTitle: Function age() returns integer linkTitle: Function age() description: The semantics of "function age() returns integer". [YSQL] menu: v2.18: identifier: age parent: miscellaneous weight: 10 type: docs This section defines the semantics of the overload of the function age() with two parameters of data type plain timestamp by implementing the defining rules in PL/pgSQL in the function modeledage(). The rules by which the returned interval value is calculated are the same for the (timestamptz, timestamptz) overload as for the plain (timestamp, timestamp) overload except that, for the with time zone_ overload, the actual timezone component (whether this is specified implicitly or taken from the session environment) and the sensitivity to the reigning timezone have their usual effect. It turns out that to match the behaviour of the built-in age(timestamp, timestamp) function, modeledage(timestamp, timestamp) needs to map a "borrowed" month into a month-specific number of days. (The subsection on this page's parent page introduces the notion of "borrowing".) The function daysprmonth() implements the mapping. Because of the effect of leap years, the function needs both year and month_ parameters. Create the helper function thus: ```plpgsql drop function if exists daysprmonth(int, int) cascade; create function daysprmonth(year in int, month in int) returns int language plpgsql as $body$ begin -- Self-doc. The value of "month" comes from "extract(month from ...)". assert (month between 1 and 12), 'daysprmonth: assert failed'; declare m_next constant int not null := case month when 12 then 1 else month + 1 end; ynext constant int not null := case mnext when 1 then year + 1 else year end; begin -- February needs special treatment 'cos of leap year possibility. -- So may as well use the same method for all months. return makedate(ynext, mnext, 1) - makedate(year, month, 1); end; end; $body$; ``` You can test it with examples like this: ```plpgsql select daysprmonth(2011, 2) as "Feb-2011", daysprmonth(2012, 2) as "Feb-2012", daysprmonth(2013, 4) as "Apr-2013", daysprmonth(2016, 12) as "Dec-2016"; ``` This is the result: ```output Feb-2011 | Feb-2012 | Apr-2013 | Dec-2016 -+-+-+- 28 | 29 | 30 | 31 ``` Because the says nothing of value to describe the semantics that the function age(timestamp, timestamp) implements, the function modeledage(timestamp, timestamp)_ was developed to a large extent by trial and errorin other words, thorough testing with a huge set of input values was key. (See the subsections that describe the functions and below.) The implementation was made simpler by this intuitive, and trivially observable, realization: ```output age(t1, t2) == -age (t2, t1) ``` The following intuition (copied from this page's parent page) is key to the scheme: The function age() extracts the year, month, day, and seconds since midnight for each of the two input moment values. It then subtracts these values pairwise and uses them to create an interval value. But the statement of the semantics must be made more carefully than this to accommodate the fact that the outcomes of the pairwise differences might be negative. So the first step, done conveniently in the declare section, is to calculate the pairwise differences months, days, and secs navelyaccepting that the values might come out negative. However, because the input timestamp values are exchanged, if necessary, so that the local variable tdob (for date-of-birth) is less than or equal to the local variable ttoday., it's guaranteed that the navely computed value of the years pairwise difference cannot be negative. There is, though, a different quirk to accommodate in the years calculation: the year zero (as a matter of arbitrary, but universal, convention) simply does not

This means that the years difference between 1 AD and 1 BC is just one. This explains the use of the case expression in the expression for years. The executable block statement (from begin through its matching end;) revisits the computation of each of secs, days, and months, in that order, by implementing "borrowing" from the immediately next coarser-grained difference value when the initially computed present value is negative. This is where the conversion from one "borrowed" month to a number of days must accommodate the number of days in the "borrowed" monthgiven not only which month it is but also, because of the possibility that this is February in a leap year, which year it is. This pinpoints a key question: Which is the "borrowed" month? Is it (a) the one with [yydob, mmdob]; or (b) the one with [yytoday, mmtoday]; or (c) maybe the month that precedes or follows either of these? Empirical testing shows that the "borrowed" month is given by choice (a). The candidate interval value to return is then evaluated (using the revised values) as: ```output make_interval(years=>years, months=>months, days=>days, secs=>secs); ``` The final step is simply to take account of whether the initial test showed that the input timestamp values should be exchanged. If there was no exchange, then the candidate interval value is returned "as is". But if the input timestamp values were exchanged, then the negated candidate interval value is returned. Create the function thus: ```plpgsql drop function if exists modeled_age(timestamp, timestamp) cascade; create function modeledage(ttodayin in timestamp, tdob_in in timestamp) returns interval language plpgsql as $body$ declare -- Exchange the inputs for negative age. negativeage constant boolean not null := (ttodayin - tdobin) < makeinterval(); ttoday constant timestamp not null := case negativeage when true then tdobin else ttodayin end; tdob constant timestamp not null := case negativeage when true then ttodayin else tdobin end; secsprday constant double precision not null := 246060; monspryear constant int not null := 12; yytoday constant int not null := extract(year from ttoday); mmtoday constant int not null := extract(month from ttoday); ddtoday constant int not null := extract(day from ttoday); sstoday constant double precision not null := extract(epoch from ttoday::time); yydob constant int not null := extract(year from tdob); mmdob constant int not null := extract(month from tdob); dddob constant int not null := extract(day from tdob); ssdob constant double precision not null := extract(epoch from tdob::time); years int not null := case -- Special treatment is needed when yytoday and yydob span AC/BC -- 'cos there's no year zero. when yytoday > 0 and yydob < 0 then yytoday - yydob - 1 else yytoday - yydob end; months int not null := mmtoday - mmdob; days int not null := ddtoday - dddob; secs double precision not null := sstoday - ssdob; begin if secs < 0 then secs := secs + secsprday; days := days - 1; end if; if days < 0 then days := days + daysprmonth(yydob, mmdob); months := months - 1; end if; if months < 0 then months := months + 12; years := years - 1; end if; declare age constant interval not null := make_interval(years=>years, months=>months, days=>days, secs=>secs); begin return case negative_age when true then -age else age end; end; end; $body$; ``` The design of this is straightforward. It evaluates both age() and modeledage() using the actual input timestamp values. Then it compares them for

Notice that it's critical to use the user-defined "strict equals" operator, `==`, for a pair of interval_ values rather than the native equals for this argument pair. The section presents the code that creates the "strict equals" operator. The recommendation, given at the start of the section, is to download the '.zip' file to create the reusable code that supports the pedagogy of the overall date-time major section and then to execute the kit's "one-click" install script. The section explains why you must use the "strict equals" operator. The function returns a text value that starts with the text typecasts of the actual input timestamp values followed by the text typecast of the result given by the built-in age() function. Only if the function modeledagevsage() gives a result that is not strictly equal to the built-in age() function's result, is the modeled result appended to the returned text value. The idea here is to reduce the visual noise in the output to make it easy to spot when (at least while modeledage() was under development) the results from the built-in and the modeled functions disagree. Create the comparison function thus: ```plpgsql drop function if exists modeledagevs_age(timestamp, timestamp) cascade; create function modeledagevsage(ttoday in timestamp, t_dob in timestamp) returns text language plpgsql as $body$ declare input constant text not null := lpad(ttoday::text, 31)||' '||lpad(tdob::text, 31)||' '; m constant interval not null := modeledage(ttoday, t_dob); a constant interval not null := age(ttoday, tdob); begin return case (m == a) when true then input||lpad(a::text, 42) else input||lpad(a::text, 42)||' ! '||lpad(m::text, 42) end; end; $body$; ``` Now exercise the comparison function with a few manually composed input values. The results are easiest to read if you use a table function encapsulation: ```plpgsql drop function if exists manualtestreportformodeled_age() cascade; create function manualtestreportformodeled_age() returns table(z text) language plpgsql as $body$ begin z := lpad('ttoday', 31 )||' '||lpad('tdob', 31 )||' '||lpad('age()', 42 ); return next; z := lpad('-', 31, '-')||' '||lpad('-', 31, '-')||' '||lpad('-', 42, '-'); return next; -- Sanity test: zero age. z := modeledagevs_age('2019-12-21', '2019-12-21' ); return next; -- Positive ages. z := modeledagevs_age('2001-04-10', '1957-06-13' ); return next; z := modeledagevs_age('2001-04-10 11:19:17', '1957-06-13 15:31:42' ); return next; z := modeledagevs_age('0007-06-13 15:31:42.123456 BC', '2001-04-10 11:19:17.654321 BC'); return next; -- Negative age. z := modeledagevs_age('1957-06-13 15:31:42', '2001-04-10 11:19:17' ); return next; -- ttoday and tdob span the BC/AD transition. z := modeledagevs_age('0001-01-01 11:19:17', '0001-01-01 15:31:42 BC' ); return next; z := modeledagevs_age('0001-01-01 15:31:42 BC', '0001-01-01 11:19:17' ); return next; end; $body$; select z from manualtestreportformodeled_age(); ``` This is the result: ```output ttoday tdob age() - 2019-12-21 00:00:00 2019-12-21 00:00:00 00:00:00 2001-04-10 00:00:00 1957-06-13 00:00:00 43 years 9 mons 27 days 2001-04-10 11:19:17 1957-06-13 15:31:42 43 years 9 mons 26 days 19:47:35 0007-06-13 15:31:42.123456 BC 2001-04-10 11:19:17.654321 BC 1994 years 2 mons 3 days 04:12:24.469135 1957-06-13 15:31:42 2001-04-10 11:19:17 -43 years -9 mons -26 days -19:47:35 0001-01-01 11:19:17 0001-01-01 15:31:42 BC 11 mons 30 days 19:47:35 0001-01-01 15:31:42 BC 0001-01-01 11:19:17 -11 mons -30 days -19:47:35 ``` There is no fourth results columnin other words, the comparison of the result from the user-defined modeledagevsage(timestamp, timestamp) and the result from the built-in age()_ passes the test for each tested pair of inputs. Notice that the first two "Positive ages" tests use the same values as do the examples on this page's parent page in the section Because the implementation of modeledage(timestamp, timestamp) function was designed using intuition and iterative refinement in response to trial-and-error, it's critically important to test the comparison of the result from this and the result from the built-in age() function with a huge number of distinct input pairs. The only way to do this is to generate these pairs

There is no available suitable random-number generator. But the function genrandombytes() comes to the rescue. This is not, strictly speaking, a built-in. Rather, it comes when you install the _ extension. {{< tip title="Always make the 'pgcrypto' extension centrally available in every database." >}} Not only does installing the extension bring the function genrandombytes(); also, it brings genrandomuuid(). This is commonly used to populate a surrogate primary key column. Yugabyte therefore recommends that you adopt the practice routinely to install pgcrypto (this must be done by a superuser) in a central "utilities" schema in every database that you create. By granting appropriate privileges and by including this schema in, for example, the second position in every regular user's search path, you can make genrandombytes(), genrandomuuid(), and all sorts of other useful utilities immediately available to all users with no further fuss. You might also like to install the extension as part of your standard set of central utilities. This does bring a random-number generator function, normalrand(). However, this generates a normally distributed set of double precision values. This functionality isn't appropriate for testing modeledage(); but it is appropriate for many other testing purposes. {{< /tip >}} First, you need a helper function to convert the bytea value that genrandombytes() returns to a number value. Create and test the helper thus: ```plpgsql drop function if exists byteatonum(bytea) cascade; create function byteatonum(b bytea) returns numeric language plpgsql as $body$ declare n numeric := 0; begin for j in 0..(length(b) - 1) loop n := n*256+get_byte(b, j); end loop; return n; end; $body$; ``` Now create the function randomtimestamp(). This invokes maketimestamp() with values from six successive invocations of genrandombytes(), using byteatonum() to convert the returned values first to numeric values and then to suitably constrained int values for each of the year, month, mday, hour, and min actual arguments and to a suitably constrained double precision value for the sec actual argument. Create it thus: ```plpgsql drop function if exists random_timestamp() cascade; create function random_timestamp() returns timestamp language plpgsql as $body$ declare year constant int not null := greatest(1::int, mod(byteatonum(genrandombytes(2))::int, 4700)); month constant int not null := greatest(1::int, mod(byteatonum(genrandombytes(2))::int, 12)); mday constant int not null := greatest(1::int, mod(byteatonum(genrandombytes(2))::int, 28)); hour constant int not null := mod(byteatonum(genrandombytes(2))::int, 23); min constant int not null := mod(byteatonum(genrandombytes(2))::int, 59); sec constant numeric not null := mod(byteatonum(genrandombytes(3)), 58.987654::numeric); ts constant timestamp not null := make_timestamp(year, month, mday, hour, min, sec::double precision); begin return case when (mod(byteatonum(genrandombytes(3))::int, 2) = 1) then ts else (ts::text||' BC')::timestamp end; end; $body$; ``` Test it like this: ```plpgsql select random_timestamp() as "ts-1", random_timestamp() as "ts-2"; ``` Repeat this select time and again. Each time, you'll see a pair of different values. Sometimes, they both have AD dates; sometimes they both have BC dates; sometimes one has an AD date and one has a BC date; sometimes "ts-1" is earlier than "ts-2"; and sometimes "ts-2" is earlier than "ts-1". The value 4700 was chosen to constrain the year because 4713 BC is the earliest legal timestamp value. (See the table in the subsection on the major sections' main page.) It's sufficient for the present testing purpose that the generated timestamp values are between 4700 BC and 4700 AD. This function invokes randomtimestamp() to generate two new distinct values and then uses these to invoke modeledage() and the built-in age(). It compares the values that they return using the `==` operator. Only if they differ, does it invoke modeledagevsage() to show the differing values. And if this happens, it notes that at least one difference has been

The expectation is that there will be no differences to report so that the final report will show simply "No failures" and therefore be maximally easily understood. As a bonus, the report shows the minimum and the maximum generated values returned by randomtimestamp(). {{< note title="Notice how the special manifest constants '-infinity' and 'infinity' are used." >}} Notice how the '-infinity' and 'infinity' are used to set the starting values for, respectively, maxts and mints. Without this text-book pattern, the loop would need to be coded more elaborately by treating the first iteration as a special case that establishes maxts and mints as the values returned by the invocation of randomtimestamp() this time; only then could the second and subsequent iterations be coded using "maxts := greatest(maxts, greatest(ts1, ts2));" and "mints := least(mints, least(ts1, ts2));"_. {{< /note >}} Create the function thus: ```plpgsql drop function if exists randomtestreportformodeled_age(int) cascade; create function randomtestreportformodeledage(noof_attempts in int) returns table(z text) language plpgsql as $body$ declare no_failures boolean not null := true; max_ts timestamp not null := '-infinity'; min_ts timestamp not null := 'infinity'; begin for j in 1..noofattempts loop declare ts1 constant timestamp not null := random_timestamp(); ts2 constant timestamp not null := random_timestamp(); m constant interval not null := modeled_age(ts1, ts2); a constant interval not null := age(ts1, ts2); begin maxts := greatest(maxts, greatest(ts1, ts2)); mints := least(mints, least(ts1, ts2)); if m == a then null; else no_failures := false; z := modeledagevs_age(ts1, ts2); return next; end if; end; end loop; z := ''; return next; z := rpad('-', 120, '-'); return next; if no_failures then z := 'No failures.'; return next; end if; z := 'maxts: '||maxts::text||' | mints: '||mints::text; return next; end; $body$; ``` Test it first with a modest number of attempts: ```plpgsql select z from randomtestreportformodeled_age(1000); ``` Then increase the number of attempts to, say, one million. This takes about a minute. (No thought was given to make the test run faster. Its speed is uninteresting.) You'll see that "No failures" is reported and that the range of randomly generated timestamp values spans close to the maximum that randomtimestamp() can produce (4700 BC through 4700 AD). This should give you a very high confidence indeed that the function modeledage()_ lives up to its name. The effect of age(t) is identical to the effect of age(\<midnight today\>, t). Here's a demonstration of the semantics. The expression datetrunc('day', clocktimestamp()) is copied from the definition of today() in the subsection . Do this to test this assertion for the timestamptz overloads: ```plpgsql drop procedure if exists assertoneparameteroverloadofagesemantics(timestamptz) cascade; create procedure assertoneparameteroverloadofagesemantics(t in timestamptz) language plpgsql as $body$ declare age_1 constant interval not null := age(t); age2 constant interval not null := age(datetrunc('day', clock_timestamp()), t); begin assert age1 = age2, 'Assert failed'; end; $body$; set timezone = 'UTC'; call assertoneparameteroverloadofagesemantics('2007-06-24'); call assertoneparameteroverloadofagesemantics('2051-07-19 Europe/Helsinki'); call assertoneparameteroverloadofagesemantics('2007-02-01 13:42:19.12345'); call assertoneparameteroverloadofagesemantics(clock_timestamp()); set timezone = 'America/Los_Angeles'; call assertoneparameteroverloadofagesemantics('2007-06-24'); call assertoneparameteroverloadofagesemantics('2051-07-19 Europe/Helsinki'); call assertoneparameteroverloadofagesemantics('2007-02-01 13:42:19.12345'); call assertoneparameteroverloadofagesemantics(clock_timestamp()); ``` Each call statement finishes without error, showing that the assertion holds for every test Do this to test this assertion for the plain timestamp overloads: ```plpgsql drop procedure if exists assertoneparameteroverloadofagesemantics(timestamp) cascade; create procedure assertoneparameteroverloadofagesemantics(t in timestamp) language plpgsql as $body$ declare age_1 constant interval not null := age(t); age2 constant interval not null := age(datetrunc('day', localtimestamp), t); begin assert age1 = age2, 'Assert failed'; end; $body$; set timezone = 'UTC'; call assertoneparameteroverloadofagesemantics('2007-02-01 13:42:19.12345'); call assertoneparameteroverloadofagesemantics('2007-06-24'); call assertoneparameteroverloadofagesemantics('2051-07-19'); call assertoneparameteroverloadofagesemantics(clock_timestamp()); set timezone = 'America/Los_Angeles'; call assertoneparameteroverloadofagesemantics('2007-02-01 13:42:19.12345'); call assertoneparameteroverloadofagesemantics('2007-06-24'); call assertoneparameteroverloadofagesemantics('2051-07-19'); call assertoneparameteroverloadofagesemantics(clock_timestamp()); ``` Each call statement finishes without error, showing that the assertion holds for every test.