/**
 * CHANGELOG.md:
 *
 * - A function was added to retrieve the number of months within a `tstzrange`:
 *   `whole_months(tstzrange)`,
 */
create function whole_months(tstzrange)
    returns integer
    returns null on null input
    language sql
    immutable
    leakproof
    parallel safe
    return case
        when isempty($1) then 0
        when upper_inf($1) or lower_inf($1) then null
        else (
            select
                count(*)
            from
                generate_series(lower($1), upper($1), '1 month'::interval) as m
            where
                m + '1 month'::interval <= upper($1)
        )
    end;

comment on function whole_months(tstzrange) is
$md$Get the number of whole months that passed within the given `tstzrange` value.

This function takes the month length into account.

Inclusivity vs exclusivity of the `tstzrange` bounds are ignored by this
function.
$md$;

/**
 * CHANGELOG.md:
 *
 *   + including a procedure to test said function:
 *     `test__whole_months_from_tstzrange()`.
 */
create procedure test__whole_months_from_tstzrange()
    set pg_readme.include_this_routine_definition to true
    set plpgsql.check_asserts to true
    language plpgsql
    as $$
declare
    _tstzrange tstzrange;
    _expected_int int;
begin
    for _tstzrange, _expected_int in
        select
            tstzrange(lower_bound::timestamptz, upper_bound::timestamptz)
            ,expected_int
        from
            (
                values
                     ('2025-05-01', '2025-06-01', 1)
                    ,('2025-05-01', '2025-06-30', 1)
                    ,('2025-05-01', '2025-07-01', 2)
                    ,('2025-05-01', '2025-07-31', 2)
                    ,('2025-05-01', '2025-07-02', 2)
            ) as v (lower_bound, upper_bound, expected_int)
    loop
        assert whole_months(_tstzrange) = _expected_int, format(
            'whole_months(%L::tstzrange) = %s ≠ %s'
            ,_tstzrange, whole_months(_tstzrange), _expected_int
        );
    end loop;
end;
$$;


/**
 * CHANGELOG.md:
 *
 * - The readability and assertion failure output of the
 *   `test__whole_months_from_tstzrange()` procedure was improved.
 */
create or replace procedure test__whole_days_from_tstzrange()
    set pg_readme.include_this_routine_definition to true
    set plpgsql.check_asserts to true
    language plpgsql
    as $$
declare
    _tstzrange tstzrange;
    _2nd_arg float;
    _expected_int int;
begin
    -- First, we test the truncating without awareness of bound in/exclusivity.
    for _tstzrange, _expected_int in
        select
            tstzrange(lower_bound::timestamptz, upper_bound::timestamptz, bound_type)
            ,expected_int
        from (
            values
                ('2024-06-15 01:10', '2024-06-16 01:10', 1)
                ,('2021-12-01', '2022-01-01', 31)
                ,('2022-01-01', '2022-01-10', 9)
                ,('2022-01-01', '2022-01-01', 0)
                ,('2022-01-01', '2022-01-01 23:59:59.999999', 0)
        ) as without_bound_types (lower_bound, upper_bound, expected_int)
        cross join (
            values ('[]'), ('[)'), ('(]'), ('()')
        ) as bound_types (bound_type)
    loop
        assert whole_days(_tstzrange) = _expected_int, format(
            'whole_days(%L::tstzrange) = %s ≠ %s'
            ,_tztzrange, whole_days(_tstzrange) _expected_int
        );

        assert whole_days(_tstzrange, 0.0) = _expected_int, format(
            'whole_days(%L::tstzrange, %s) = %s ≠ %s'
            ,_tztzrange, 0.0, whole_days(_tstzrange, 0.0), _expected_int
        );
    end loop;

    -- Then we test treating the ranges as discrete ranges with a certain precision.
    assert whole_days('[2022-01-01, 2022-01-01 23:59:59.999999]'::tstzrange, 1.0) = 0;
    assert whole_days('[2022-01-01, 2022-01-02]'::tstzrange, 1.0) = 1;
    assert whole_days('[2022-01-01, 2022-01-02)'::tstzrange, 1.0) = 0;
    assert whole_days('[2022-01-01, 2022-01-02 00:00:00.000001]'::tstzrange, 0.000001) = 1;
    assert whole_days('[2022-01-01, 2022-01-02 00:00:00.000001)'::tstzrange, 0.000001) = 1;
    assert whole_days('[2022-01-01, 2022-01-02 00:00:00.000001)'::tstzrange, 0.000010) = 0;
    assert whole_days('[2022-01-01, 2022-01-02 00:00:00.000010)'::tstzrange, 0.000010) = 1;

    -- PostgreSQL integers do not support positive or negative infinity; `null` will have to do.
    assert whole_days('[,)'::tstzrange) is null;
    assert whole_days('[2024-06-15,)'::tstzrange) is null;
    assert whole_days('[,2024-06-15)'::tstzrange) is null;
end;
$$;
