Project: YugabyteDB (App Definition and Development / Database)
File: justfy-and-extract-epoch.md

title: The justify() and extract(epoch ...) functions for intervals [YSQL] headerTitle: The justify() and extract(epoch ...) functions for interval values linkTitle: Justify() and extract(epoch...) description: Describes the functions justifyhours(interval), justifydays(interval), justifyinterval(interval), and extract(epoch from intervalvalue). [YSQL] menu: v2.18: identifier: justfy-and-extract-epoch parent: type-interval weight: 35 type: docs {{< tip title="Download and install the date-time utilities code." >}} The code on this page depends on the code presented in the section . This is included in the larger that includes all of the reusable code that the overall section describes and uses. {{< /tip >}} Consider these interval values: ```output i1: '5 days 1 hour '::interval i2: '4 days 25 hours' ::interval i3: '5 months 1 day' ::interval i4: '4 months 31 days ' ::interval ``` The values i2 and i4 are noteworthy: because 25 hours is more than the number of hours in at least a typical 1 day period (but notice that 1 day will be 23 hours or 25 hours at the Daylight Savings Time "spring forward" and "fall back" transitions); and because 31 days is more than the number of days in at least some 1 month durations. The justify built-in functions have a single interval formal in parameter and return an interval value. They normalize the fields of the internal representation of the input by decreasing the value of a finer-granularity time unit field and correspondingly increasing the value of its greater granularity neighbor. justifyhours() returns an interval whose ss value doesn't exceed the number of seconds in one day (24\60\60)_. justifydays() returns an interval whose dd value doesn't exceed the nominal number of days in one month (30)_. justifyinterval() returns an interval both whose ss value doesn't exceed the number of seconds in one day and whose dd_ value doesn't exceed the nominal number of days in one month. In general, justifying an interval value changes the semantics of adding or subtracting the result to a plain timestamp value or a timestamptz value with respect to using the input, unjustified, value. In general, too, justifying a pure interval value will produce a hybrid interval value. (A pure value is one where only one of the three fields of the internal representation is non-zero; And a hybrid value has two or more of these fields non-zero.) The justifyhours() built-in function "normalizes" the value of the ss field of the internal representation by subtracting an appropriate integral number of 24 hour periods so that the resulting ss value is less than 24 hours (but not less than zero). The subtracted 24 hour periods are converted to days, using the rule that one 24 hour period is always the same as 1 day, and added to the value of the dd_ field. (Daylight Savings Time regimes are ignored by the implementation of this rule of thumb.) Try this: ```plpgsql select justify_hours('4 days 25 hours'::interval); ``` This is the result: ```output 5 days 01:00:00 ``` In general, justifyhours() changes the semantics of the interval-timestamptz_ overloads of the `+` and `-` operators. Try this: ```plpgsql set timezone = 'America/Los_Angeles'; with c as ( select '2021-03-13 19:00:00 America/Los_Angeles'::timestamptz as d, '25 hours'::interval as i) select d + i as "d + i", d + justifyhours(i) as "d + justifyhours(i)" from c; ``` This is the result: ```output d + i | d + justify_hours(i) 2021-03-14 21:00:00-07 | 2021-03-14 20:00:00-07 ``` Notice that the result of adding the interval value i "as is" is changed (it becomes one hour earlier) when justifyhours(i)_ is used. See the section

In a corresponding way, the justifydays() built-in function "normalizes" the value of the dd field of the internal representation by subtracting an appropriate integral number of 30 day periods so that the resulting dd value is less than 30 days (but not less than zero). The subtracted 30 day periods are converted to months, using the rule that one 30 day period is the same as 1 month, and added to the value of the mm_ field. Try this: ```plpgsql select justify_days('4 months 31 days'::interval); ``` This is the result: ```output 5 mons 1 day ``` In general, justifydays() changes the semantics of the interval-timestamp and the interval-timestamptz overloads of the `+` and `-` operators. This has nothing to do with Daylight Savings Time and how the reigning timezone specifies the relevant rules. Rather, it has simply to do with the _ convention for the meaning of adding one month: it aims to take you to the same date in the next month. Try this: ```plpgsql -- Here, the effect is seen even with plain timestamp (of course). with c as ( select '2021-02-20 12:00:00'::timestamp as d, '33 days'::interval as i) select d + i as "d + i", d + justifydays(i) as "d + justifydays(i)" from c; ``` This is the result: ```output d + i | d + justify_days(i) 2021-03-25 12:00:00 | 2021-03-23 12:00:00 ``` Notice that the result of adding the interval value i "as is" is changed (it becomes two days earlier) when justifydays(i)_ is used. The rules are explained in the section. The justifyinterval() built-in function simply applies first the justifyhours() function and then the justifydays() function to produce what the YSQL documentation refers to as a "normalized interval value". (The PostgreSQL documentation doesn't define such a term.) A normalized interval value is one where the extracted hours value is less than 24 (but not less than zero) and the extracted days value is less than 30_ (but not less than zero). Try this: ```plpgsql select justify_interval('4 months 31 days 25 hours'::interval); ``` This is the result: ```output 5 mons 2 days 01:00:00 ``` Of course, justifyinterval() affects the semantics of moment-interval arithmetic too by combining the semantic effects of both justifyhours() and justifydays()_. First, tests these properties of the justify functions: ```output justifydays(justifyhours(i)) == justify_interval(i) # user-defined "strict" equals operator ``` and: ```output justifyhours(justifydays (i)) = justify_interval(i) # natve equals operator not (justifyhours(justifydays (i)) == justify_interval(i)) # "strict" equals operator ``` Then implements the algorithms that the three justify functions use (as described in prose above) and tests that each produces the same result as the native implementation that it models. The helper defines an _interval_ value so that this can be used in both the implementation of and to invoke without cluttering the interesting code. Create function i() and inspect the value that it returns: ```plpgsql drop function if exists i() cascade; create function i() returns interval language plpgsql as $body$ begin return make_interval( years => 516, months => 317, days => 977, hours => 473, mins => 853, secs => 417.5); end; $body$; select i(); ``` This is the result: ```output 542 years 5 mons 977 days 487:19:57.5 ``` Demonstrate the outcomes of justifyhours(), justifydays(), and justifyinterval(). And test the fact the effect of justifyinterval() is the same as the effect of justifyhours() followed by justifydays()and different from the effect of justifydays() followed by

```plpgsql drop function if exists justify_outcomes() cascade; create function justify_outcomes() returns table(z text) language plpgsql as $body$ declare i constant interval not null := i(); i1 constant interval not null := justifyhours(justifydays (i)); i2 constant interval not null := justifydays (justifyhours(i)); i3 constant interval not null := justify_interval(i); begin -- Native equality test. assert i2 = i1, 'Assert #1 failed'; -- Strict equality tests. assert not(i2 == i1), 'Assert #2 failed'; assert i3 == i2, 'Assert #3 failed'; z := 'i: '||i ::text; return next; z := 'i1: '||i1::text; return next; z := 'i2: '||i2::text; return next; end; $body$; select z from justify_outcomes(); ``` Notice that the function justifyoutcomes() uses both the native interval-interval equality operator, `=`, and the user-defined so-called strict interval-interval equality operator, `==`. See the section . Briefly, the native `=` operator uses a loose definition that judges two interval values to be equal if their internal representations, after applying justifyinterval() to each, are identical; but the strict `==` operator judges two interval values to be equal only if the internal representations of the two "as is" values are identical. This is the result: ```output i: 542 years 5 mons 977 days 487:19:57.5 i1: 545 years 1 mon 37 days 07:19:57.5 i2: 545 years 2 mons 7 days 07:19:57.5 ``` (Whitespace was added manually to improve the readability by vertically align the corresponding fields.) ```plpgsql drop procedure if exists testjustifymodel(interval) cascade; create procedure testjustifymodel(i in interval) language plpgsql as $body$ declare secsprday constant double precision not null := 246060; daysprmonth constant int not null := 30; begin -- justify_hours() declare rnative constant intervalmmddsst not null := intervalmmddss(justify_hours(i)); r constant intervalmmddsst not null := intervalmmdd_ss(i); ss constant double precision not null := mod(r.ss::numeric, secsprday::numeric); dd constant int not null := r.dd + trunc(r.ss/secsprday); rmodel constant intervalmmddss_t not null := (r.mm, dd, ss); begin assert rmodel = rnative, 'Assert #1 failed'; end; -- justify_days() declare rnative constant intervalmmddsst not null := intervalmmddss(justify_days(i)); r constant intervalmmddsst not null := intervalmmdd_ss(i); dd constant int not null := mod(r.dd, daysprmonth); mm constant int not null := r.mm + trunc(r.dd/daysprmonth); rmodel constant intervalmmddss_t not null := (mm, dd, r.ss); begin assert rmodel = rnative, 'Assert #2 failed'; end; -- justify_interval() declare rnative constant intervalmmddsst not null := intervalmmddss(justify_interval(i)); r constant intervalmmddsst not null := intervalmmdd_ss(i); ss constant double precision not null := mod(r.ss::numeric, secsprday::numeric); dd1 constant int not null := r.dd + trunc(r.ss/secsprday); dd constant int not null := mod(dd1, daysprmonth); mm constant int not null := r.mm + trunc(dd1/daysprmonth); rmodel constant intervalmmddss_t not null := (mm, dd, ss); begin assert rmodel = rnative, 'Assert #3 failed'; end; end; $body$; ``` Test it thus: ```plpgsql call testjustifymodel(i()); ``` The procedure finishes without error showing that the conditions tested by the assert statements hold. The section relies on this function to model the implementation of the comparison algorithm. It's therefore included in the section. Here's a simple test: ```plpgsql drop function if exists justifiedsecondsdemo() cascade; create function justifiedsecondsdemo() returns table(z text) language plpgsql as $body$ begin z := 'secs => 86000: '||justifiedseconds(makeinterval(secs => 86000)); return next; z := 'secs => 31234567891: '||justifiedseconds(makeinterval(secs => 31234567891)); return next; z := 'hours => 2147483647: '||justifiedseconds(makeinterval(hours => 2147483647)); return next; z := 'days => 2147483647: '||justifiedseconds(makeinterval(days => 2147483647)); return next; end; $body$; select z from justifiedsecondsdemo(); ``` The function finishes without error, showing that, for the interval values used for the test, the assertion holds. This is the result: ```output secs => 86000: 86000 secs => 31234567891: 31234567891 hours => 2147483647: 7730941129200 days => 2147483647: 185542587100800 ``` The specifies the semantics of extract(epoch from intervalvalue)_ thus: [the function returns] the total number of seconds in the interval [value]. However, it does not explain what this

The discussion and demonstration of the user-defined function shows that the notion of the total number of seconds in an interval value can be defined only by using a rule of thumb. Try this: ```plpgsql drop function if exists secondsintwo_years() cascade; create function secondsintwo_years() returns table(z text) language plpgsql as $body$ declare e_1999 constant double precision not null := extract(epoch from '1999-07-15 12:00:00'::timestamptz); e_2001 constant double precision not null := extract(epoch from '2001-07-15 12:00:00'::timestamptz); e_2003 constant double precision not null := extract(epoch from '2003-07-15 12:00:00'::timestamptz); s1 constant double precision not null := e2001 - e1999; s2 constant double precision not null := e2003 - e2001; s3 constant double precision not null := justifiedseconds(makeinterval(years=>2)); s4 constant double precision not null := extract(epoch from make_interval(years=>2)); begin z := 's1: '||s1::text; return next; z := 's2: '||s2::text; return next; z := 's3: '||s3::text; return next; z := 's4: '||s4::text; return next; end; $body$; select z from secondsintwo_years(); ``` The number of seconds in a nominal two year period is defined in four different ways: s1 ::= from noon on Midsummer's Day 1999 to noon on Midsummer's Day 2001. s2 ::= from noon on Midsummer's Day 2001 to noon on Midsummer's Day 2003. s3 ::= justifiedseconds('2 years'::interval)_. s4 ::= the total number of seconds in '2 years'::interval (quoting the text from the PostgreSQL documentation) This is the result: ```output s1: 63158400 s2: 63072000 s3: 62208000 s4: 63115200 ``` Notice that each reported number of seconds differs from the others. It's unremarkable that the duration from Midsummer 1999 to Midsummer 2001 is longer than that from Midsummer 2001 to Midsummer 2003 because the former includes a leap year and the latter does not. The difference (63158400 - 63072000) is exactly equal to the number of seconds in one day (24\*60\*60). It's easy to see why justifiedseconds('2 years'::interval) is less than both 63158400 and 63072000: it's because it simply uses the rule that twelve months is (12\30)_ daysfive days less than a non-leap year. So _(63072000 - 62208000)_ is equal to _(2\5\24\60\*60)_. But why is the fourth result, from extract(epoch from intervalvalue)_ different from the third result? It turns out that the result from extract(epoch from intervalvalue) aims to give a sensible number of seconds for durations of many years. So it uses the (semantics of the) trunc() and mod() built-in functions to transform the value of the mm field of the interval representation to years, yy, and a months remainder, mm. Then the yy value is multiplied by the number of days in a Julian year. This is greater than 12\*30_. {{< note title="How many days are there in a year?" >}} Internet search quickly finds lots of articles on this topicfor example, in Wikipedia. Two subtly different answers are in common use. The _Julian year_ is defined to be _365.25_ days. This is calculated using the fact that over a four year period, there are usually three normal years and one leap yearso the average number of days per year is 365.25. The uses the Julian year to define the size of the . The _Gregorian year_ is defined to be _365.2425_ daysjust a little shorter than the Julian year. This is because the Julian calendar assumed incorrectly that the average solar year is exactly 365.25 days longan overestimate of a little under one day per century. The Gregorian reform shortened the average (calendar) year by 0.0075 days to stop the drift of the calendar with respect to the equinoxes. It uses the following rule to say when leap years occur in order to produce the required shorter average over a sufficiently long period: a year that is evenly divisible by 100 is a leap year only if it is also evenly divisible by

{{< /note >}} This function models the rule described in prose above. And it uses an assertstatement to confirm that the model produces the same result as the native built-in function. Notice that it uses these scaling factors: secsprday secsprmonth secspryear (calculated as secsprday\*avgdayspryear where avgdayspryear is the length of the Julian year) Create it thus. ```plpgsql drop function if exists modeledextractepochfrominterval(interval) cascade; create function modeledextractepochfrominterval(i in interval) returns double precision language plpgsql as $body$ declare e constant double precision not null := extract(epoch from i); -- Scaling factors secsprday constant double precision not null := 246060; secsprmonth constant double precision not null := secsprday*30; avgdayspr_year constant double precision not null := 365.25; secspryear constant double precision not null := secsprday*avgdayspr_year; r constant intervalmmddsst not null := intervalmmdd_ss(i); mm constant double precision not null := mod(r.mm, 12::int); yy constant double precision not null := trunc(r.mm::numeric/12::int); modeled_e constant double precision not null := r.ss + r.dd*secsprday + mm*secsprmonth + yy*secspryear; begin assert (extract(epoch from makeinterval(days =>1)) = secspr_day) and (extract(epoch from makeinterval(months=>1)) = secspr_month) and (extract(epoch from makeinterval(years =>1)) = secspr_year) , 'Assert scale factors OK failed'; assert e = modelede, 'Assert "e = modelede" failed'; return e; end; $body$; ``` This function invokes modeledextractepochfrominterval() using the same set of input interval values as was used (above) to demonstrate the user-defined function. ```plpgsql drop function if exists modeledextractepochfrominterval_demo() cascade; create function modeledextractepochfrominterval_demo() returns table(z text) language plpgsql as $body$ begin z := 'secs => 86000: '||modeledextractepochfrominterval(make_interval(secs => 86000)); return next; z := 'secs => 31234567891: '||modeledextractepochfrominterval(make_interval(secs => 31234567891)); return next; z := 'hours => 2147483647: '||modeledextractepochfrominterval(make_interval(hours => 2147483647)); return next; z := 'days => 2147483647: '||modeledextractepochfrominterval(make_interval(days => 2147483647)); return next; end; $body$; select z from modeledextractepochfrominterval_demo(); ``` This is the result: ```output secs => 86000: 86000 secs => 31234567891: 31234567891 hours => 2147483647: 7730941129200 days => 2147483647: 185542587100800 ``` Compare this with the results from invoking the justifiedsecondsdemo() function (above): ```output secs => 86000: 86000 secs => 31234567891: 31234567891 hours => 2147483647: 7730941129200 days => 2147483647: 185542587100800 ``` They happen to be identical. But this is just a fortuitous outcome due to how the input values happen to be defined as pure seconds and pure days interval values. Try a test that uses a hybrid interval value: ```plpgsql with c as ( select make_interval(months=>987, days=>876, hours=>765) as i) select justifiedseconds(i) as "justifiedseconds()", modeledextractepochfrominterval(i) as "extracted epoch" from c; ``` This is the result: ```output justified_seconds() | extracted epoch +-- 2636744400 | 2673939600 ``` Now the results from justifiedseconds() and extract(epoch ...)_ are different. This is the rule for the behavior difference: The results from justifiedseconds() and extract(epoch ...) are the same for a pure interval valuei.e. one where only one of the three fields of the internal representation is non-zero. (The section shows how to ensure declaratively that an interval value is pure_ in this way.) The results from the two approaches will differ for a hybrid interval value when the value's duration is long enough that the scaling of years to seconds, in extract(epoch ...), uses the Julian year definition. As a consequence, the total number of seconds in two interval values i1 and i2, (using the PostgreSQL documentation's wording) will differ when: ```output (i1 = i2) and not (i1 == i2) ``` In other words, the extracted seconds will differ when the native equality comparison shows i1 and i2 to be the same but the user-defined strict equality comparison shows i1 and i2 to be different. Of course, therefore, extract(epoch from intervalvalue) is no help for understanding the semantics of the native interval_ value equality and inequality comparisons.