%---------------------------------------------------------------------------%
% vim: ft=mercury ts=4 sw=4 et
%---------------------------------------------------------------------------%
% Copyright (C) 1997-1998, 2003-2006 The University of Melbourne.
% Copyright (C) 2014-2016, 2018-2019 The Mercury team.
% This file is distributed under the terms specified in COPYING.LIB.
%---------------------------------------------------------------------------%
%
% File: rational.m.
% Authors: aet Apr 1998. (with plagiarism from rat.m)
% Stability: high.
%
% Implements a rational number type and a set of basic operations on
% rational numbers.
%
%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%

:- module rational.
:- interface.

:- import_module integer.

%---------------------------------------------------------------------------%

:- type rational.

:- func numer(rational) = integer.

:- func denom(rational) = integer.

:- func zero = rational.

:- func one = rational.

:- pred '<'(rational::in, rational::in) is semidet.

:- pred '>'(rational::in, rational::in) is semidet.

:- pred '=<'(rational::in, rational::in) is semidet.

:- pred '>='(rational::in, rational::in) is semidet.

:- func rational(int) = rational.

:- func rational(int, int) = rational.

:- func from_integer(integer) = rational.

:- func from_integers(integer, integer) = rational.

% :- func float(rational) = float.

:- func '+'(rational) = rational.

:- func '-'(rational) = rational.

:- func rational + rational = rational.

:- func rational - rational = rational.

:- func rational * rational = rational.

:- func rational / rational = rational.

:- func reciprocal(rational) = rational.

:- func abs(rational) = rational.

%---------------------------------------------------------------------------%
%---------------------------------------------------------------------------%

:- implementation.

:- import_module require.

    % The normal form of a rational number has the following properties:
    %
    % - numerator and denominator have no common factors.
    % - denominator is positive.
    % - denominator is not zero.
    % - if numerator is zero, then denominator is one.
    %
    % These invariants must be preserved by any rational number
    % constructed using this module, since the equality predicate
    % on rationals is simply Mercury's default unification
    % predicate =/2. If the invariants were not maintained,
    % we would have pathologies like r(-1,2) \= r(1,-2).
    %
    % The rational_norm/2 function generates rationals in this normal form.
    %
:- type rational
    --->    r(integer, integer).

%---------------------------------------------------------------------------%

numer(r(Num, _)) = Num.

denom(r(_, Den)) = Den.

zero = r(integer.zero, integer.one).

one = r(integer.one, integer.one).

'<'(R1, R2) :-
    Cmp = cmp(R1, R2),
    Cmp = (<).

'>'(R1, R2) :-
    Cmp = cmp(R1, R2),
    Cmp = (>).

'=<'(R1, R2) :-
    Cmp = cmp(R1, R2),
    (Cmp = (<) ; Cmp = (=)).

'>='(R1, R2) :-
    Cmp = cmp(R1, R2),
    (Cmp = (>) ; Cmp = (=)).

rational(Int) = rational_norm(integer(Int), integer.one).

rational(Num, Den) = rational_norm(integer(Num), integer(Den)).

from_integer(Integer) = rational_norm(Integer, integer.one).

from_integers(Num, Den) = rational_norm(Num, Den).

%% XXX: There are ways to do this in some cases even if the
%% float conversions would overflow.
% float(r(Num, Den)) =
%   float:'/'(integer.float(Num), integer.float(Den)).

'+'(Rat) = Rat.

'-'(r(Num, Den)) = r(-Num, Den).

r(An, Ad) + r(Bn, Bd) = rational_norm(Numer, M) :-
    M = lcm(Ad, Bd),
    CA = M // Ad,
    CB = M // Bd,
    Numer = An * CA + Bn * CB.

R1 - R2 = R1 + (-R2).

    % XXX: need we call rational_norm here?
r(An, Ad) * r(Bn, Bd) = rational_norm(Numer, Denom) :-
    G1 = gcd(An, Bd),
    G2 = gcd(Ad, Bn),
    Numer = (An // G1) * (Bn // G2),
    Denom = (Ad // G2) * (Bd // G1).

R1 / R2 = R1 * reciprocal(R2).

reciprocal(r(Num, Den)) =
    ( if Num = integer.zero then
        func_error($pred, "division by zero")
    else
        r(signum(Num) * Den, integer.abs(Num))
    ).

abs(r(Num, Den)) = r(integer.abs(Num), Den).

:- func rational_norm(integer, integer) = rational.

rational_norm(Num, Den) = Rat :-
    ( if Den = integer.zero then
        error("rational.rational_norm: division by zero")
    else if Num = integer.zero then
        Rat = r(integer.zero, integer.one)
    else
        G    = gcd(Num, Den),
        Num2 = Num * signum(Den),
        Den2 = integer.abs(Den),
        Rat  = r(Num2 // G, Den2 // G)
    ).

:- func gcd(integer, integer) = integer.

gcd(A, B) = gcd_2(integer.abs(A), integer.abs(B)).

:- func gcd_2(integer, integer) = integer.

gcd_2(A, B) = ( if B = integer.zero then A else gcd_2(B, A rem B) ).

:- func lcm(integer, integer) = integer.

lcm(A, B) =
    ( if A = integer.zero then
        integer.zero
    else if B = integer.zero then
        integer.zero
    else
        integer.abs((A // gcd(A, B)) * B)
    ).

:- func signum(integer) = integer.

signum(N) =
    ( if N = integer.zero then
        integer.zero
    else if N < integer.zero then
        -integer.one
    else
        integer.one
    ).

:- func cmp(rational, rational) = comparison_result.

cmp(R1, R2) = Cmp :-
    Diff = R1 - R2,
    ( if is_zero(Diff) then
        Cmp = (=)
    else if is_negative(Diff) then
        Cmp = (<)
    else
        Cmp = (>)
    ).

:- pred is_zero(rational::in) is semidet.

is_zero(r(integer.zero, _)).

:- pred is_negative(rational::in) is semidet.

is_negative(r(Num, _)) :-
    Num < integer.zero.

%---------------------------------------------------------------------------%
:- end_module rational.
%---------------------------------------------------------------------------%
