Project: YugabyteDB (App Definition and Development / Database)
File: interval-number-multiplication.md

title: Interval-number multiplication [YSQL] headerTitle: Multiplying or dividing an interval value by a number linkTitle: Interval-number multiplication description: Explains the semantics of multiplying or dividing an interval value by a real or integral number. [YSQL] menu: v2.18: identifier: interval-number-multiplication parent: interval-arithmetic weight: 30 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 >}} This section presents a PL/pgSQL implementation of the model that explains how multiplying or dividing an interval value by a number works. Make sure that you have read the section before reading this section. Create a table function to display the model in action: ```plpgsql drop function if exists intervalmultiplicationresult(interval, double precision) cascade; create function intervalmultiplicationresult(i in interval, f in double precision) returns table(z text) language plpgsql as $body$ declare mmddssin constant intervalmmddsst not null := intervalmmddss(i); mmmodel constant double precision not null := mmddssin.mm*f; ddmodel constant double precision not null := mmddssin.dd*f; ssmodel constant double precision not null := mmddssin.ss*f; i_model constant interval not null := interval_value( interval_parameterization( mm=>mmmodel, dd=>ddmodel, ss=>ss_model)); mmddssmodel constant intervalmmddsst not null := intervalmmddss(i_model); i_actual constant interval not null := i*f; mmddssactual constant intervalmmddsst not null := intervalmmddss(i_actual); begin z := 'input mmddss: '|| tochar(mmddssin.mm, '999999990.9999')||' months' || tochar(mmddssin.dd, '999999990.9999')||' days' || tochar(mmddssin.ss, '999999990.9999')||' seconds' ; return next; z := 'intermediate model mmddss: '|| tochar(mmmodel, '999999990.9999')||' months' || tochar(ddmodel, '999999990.9999')||' days' || tochar(ssmodel, '999999990.9999')||' seconds' ; return next; z := 'ultimate model mmddss: '|| tochar(mmddssmodel.mm, '999999990.9999')||' months' || tochar(mmddssmodel.dd, '999999990.9999')||' days' || tochar(mmddssmodel.ss, '999999990.9999')||' seconds' ; return next; z := 'actual mmddss: '|| tochar(mmddssactual.mm, '999999990.9999')||' months' || tochar(mmddssactual.dd, '999999990.9999')||' days' || tochar(mmddssactual.ss, '999999990.9999')||' seconds' ; return next; z := ''; return next; z := 'ultimate model result: '||i_model::text; return next; end; $body$; ``` Test it first for multiplication and then for division: ```plpgsql select z from intervalmultiplicationresult('2 months'::interval, 1.2345678); select z from intervalmultiplicationresult('2 months'::interval, 0.9876543); ``` This is the multiplication result: ```output input mmddss: 2.0000 months 0.0000 days 0.0000 seconds intermediate model mmddss: 2.4691 months 0.0000 days 0.0000 seconds ultimate model mmddss: 2.0000 months 14.0000 days 6399.4752 seconds actual mmddss: 2.0000 months 14.0000 days 6399.4752 seconds ultimate model result: 2 mons 14 days 01:46:39.4752 ``` And this is the division result: ```output input mmddss: 2.0000 months 0.0000 days 0.0000 seconds intermediate model mmddss: 1.9753 months 0.0000 days 0.0000 seconds ultimate model mmddss: 1.0000 months 29.0000 days 22399.8912 seconds actual mmddss: 1.0000 months 29.0000 days 22399.8912 seconds ultimate model result: 1 mon 29 days 06:13:19.8912 ``` Notice that because the internal representation for the input has a non-zero value for only the mm field. Then, after both multiplication and division, the intermediate result, of course, still has a non-zero value for only the mm field. However, this non-zero value is now a real number and not an integer. This means that when the ultimate result is constructed, the hard-to-grasp spill-down rules kick in. See the section . This explains the fact that the ultimate result has non-zero values for the dd and ss fields as well as for the mm

This shows that a practice that the user might adopt to use only interval values that have just a single non-zero internal representation field can easily be thwarted by interval multiplication or division. The section shows how you can guarantee that you avoid this problem in a way that is consistent with what you can reasonably expect interval multiplication and division to mean. The procedure assertintervalmultiplicationmodelok() is a mechanical re-write of the function intervalmultiplicationresult() that produces no output. Instead, it uses an assert statement to check that the model produces the same result as the native implementation. This makes it suitable for testing the model with a wide range of input values. Create it thus: ```plpgsql drop procedure if exists assertintervalmultiplicationmodelok(interval, double precision); create procedure assertintervalmultiplicationmodelok(i in interval, f in double precision) language plpgsql as $body$ declare mmddssin constant intervalmmddsst not null := intervalmmddss(i); mmmodel constant double precision not null := mmddssin.mm*f; ddmodel constant double precision not null := mmddssin.dd*f; ssmodel constant double precision not null := mmddssin.ss*f; i_model constant interval not null := interval_value( interval_parameterization( mm=>mmmodel, dd=>ddmodel, ss=>ss_model)); i_actual constant interval not null := i*f; begin -- Notice the use of the user-defined "strict equals" operator. assert iactual == imodel, 'assert failed'; end; $body$; ``` Notice the use of the . It's essential to use this, and not the native `=`, because two interval values that compare as true with the native `=` operator but as false with the strict `==` operator can produce different results when added to a timestamptz valuesee the section . Use the assert procedure thus: ```plpgsql call assertintervalmultiplicationmodelok('2 months'::interval, 1.2345678); call assertintervalmultiplicationmodelok('2 months'::interval, 0.9876543); call assertintervalmultiplicationmodelok('2 months 2 days 2 hours'::interval, 3); call assertintervalmultiplicationmodelok('2 months 2 days 2 hours'::interval, 0.5); call assertintervalmultiplicationmodelok('2 months 2 days 2 hours'::interval, 3.5); call assertintervalmultiplicationmodelok('2 months 2 days 2 hours'::interval, 0.25); call assertintervalmultiplicationmodelok('2 months 2 days 2 hours'::interval, 1.1); call assertintervalmultiplicationmodelok('2 months 2 days 2 hours'::interval, 0.9); call assertintervalmultiplicationmodelok('2 months 2 days 2 hours'::interval, 1.11); /* call assertintervalmultiplicationmodelok('2 months 2 days 2 hours'::interval, 0.99); */; call assertintervalmultiplicationmodelok( ' -9.123456 years, 18.123456 months, -700.123456 days, 97.123456 hours, -86.123456 minutes, 75.123456 seconds '::interval, 1.2345); /* call assertintervalmultiplicationmodelok( ' -9.123456 years, 18.123456 months, -700.123456 days, 97.123456 hours, -86.123456 minutes, 75.123456 seconds '::interval, 0.0009); */; ``` Notice that two of the tests are commented out. The remaining tests finish silently. So the hypothesised model has been disproved. It's easy to see why if the two failing tests are re-cast to use the intervalmultiplicationresult() function. Try this first: ```plpgsql select z from intervalmultiplicationresult('2 months 2 days 2 hours'::interval, 0.96); ``` This is the result: ```output input mmddss: 2.0000 months 2.0000 days 7200.0000 seconds intermediate model mmddss: 1.9200 months 1.9200 days 6912.0000 seconds ultimate model mmddss: 1.0000 months 28.0000 days 138240.0000 seconds actual mmddss: 1.0000 months 29.0000 days 51840.0000 seconds ultimate model result: 1 mon 28 days 38:24:00 ``` Clearly, the result the model differs in the strict sense from the actual result. But compare the results with the native `=` operator: ```plpgsql select ( '1 month 28 days 138240 seconds'::interval = '1 month 29 days 51840 seconds'::interval )::text; ``` This result is true. (Of course, with the user-defined strict `==` operator, the result is false.) The explanation is just the same with the other failing

You look at more failing cases most effectively by using a stripped re-write of the function intervalmultiplicationresult() that simply returns the interval result without trace output: ```postgresql drop function if exists intervalmultiplicationresult(interval, double precision) cascade; create function intervalmultiplicationresult(i in interval, f in double precision) returns interval language plpgsql as $body$ declare mmddssin constant intervalmmddsst not null := intervalmmddss(i); mmmodel constant double precision not null := mmddssin.mm*f; ddmodel constant double precision not null := mmddssin.dd*f; ssmodel constant double precision not null := mmddssin.ss*f; i_model constant interval not null := interval_value( interval_parameterization( mm=>mmmodel, dd=>ddmodel, ss=>ss_model)); begin return i_model; end; $body$; drop operator if exists (interval, double precision) cascade; create operator ( leftarg = interval, rightarg = double precision, procedure = intervalmultiplicationresult ); ``` The user-defined operator allows the test to be terser and therefore easier to read. Now try two simple tests. First, a test where the model result agrees with the actual result: ```plpgsql select '2 months 2 days'::interval * 0.95 as "actual", '2 months 2 days'::interval 0.95 as "model"; ``` This is the result: ```output actual | model 1 mon 28 days 21:36:00 | 1 mon 28 days 21:36:00 ``` And next, a test where the model result disagrees with the actual result. (The only change from the previous test is that 0.95 is replaced by 0.96.) ```plpgsql select '2 months 2 days'::interval * 0.96 as "actual", '2 months 2 days'::interval 0.96 as "model"; ``` This is the result: ```output actual | model 1 mon 29 days 12:28:48 | 1 mon 28 days 36:28:48 ``` This prompts the obvious question: which of the two alternatives, PostgreSQL's C native code or the PL/pgSQL function intervalmultiplicationresult() implements the more sensible requirements specification? Here's a good way to choose the winner. Consider this ordinary principle of algebra and arithmetic: ```output (a + b)x = ax +b*x ``` Test whether the native interval-number `*` overload satisfies this rule: ```plpgsql select ('2 months'::interval + '2 days'::interval) * 0.96 as "add then multiply", ('2 months'::interval 0.96) + ('2 days'::interval 0.96) as "multiply then add"; ``` This is the result: ```output add then multiply | multiply then add 1 mon 29 days 12:28:48 | 1 mon 28 days 36:28:48 ``` So the native interval-number `` overload fails to satisfy the basic rule. Repeat the test using the user-defined `` operator instead of the native `` operator: ```plpgsql select ('2 months'::interval + '2 days'::interval) 0.96 as "add then multiply", ('2 months'::interval 0.96) + ('2 days'::interval 0.96) as "multiply then add"; ``` This is the result: ```output add then multiply | multiply then add 1 mon 28 days 36:28:48 | 1 mon 28 days 36:28:48 ``` So the user-defined interval-number `` overload does satisfy the basic rule. The conclusion is inescapable: the native PostgreSQL implementation of interval-number multiplication is buggy to the extent that, when edge-case input values are presented, the result is not strictly equal to what a sensible specification of the operation's semantics requires. (It's impossible to imagine a specification that would require the observed behavior.) However, when the native, non-strict implementation of the interval-interval overload of the `=` operator is used to judge the fidelity of the native interval-number multiplication behavior, the results do accord with what the assumed specification requires. {{< tip title="Avoid the problems of native 'interval' multiplacaton by using custom 'interval' domain types." >}} Yugabyte recommends that you simply program defensively to avoid the quirks of the native implementation of interval multiplication by following the approach that the section describes. {{< /tip >}}