Current section
Files
Jump to
Current section
Files
test/prop_rationals.erl
-module(prop_rationals).
-include_lib("proper/include/proper.hrl").
-export([
prop_normalize_idempotent/0,
prop_normalize_denominator_positive/0,
prop_normalize_reduced/0,
prop_normalize_value_preserved/0,
prop_compare_matches_oracle/0,
prop_compare_antisymmetric/0,
prop_compare_sign_regression/0,
prop_zero_identity/0,
prop_one_identity/0,
prop_negate_additive_inverse/0,
prop_negate_involution/0,
prop_abs_non_negative/0,
prop_abs_idempotent/0,
prop_sign_matches_compare/0,
prop_sign_trichotomy/0,
prop_pow_zero/0,
prop_pow_one/0,
prop_pow_add/0,
prop_min_lower_bound/0,
prop_max_upper_bound/0,
prop_clamp_in_range/0,
prop_dist_symmetric/0,
prop_dist_non_negative/0,
prop_sum_append/0,
prop_product_append/0,
prop_floor_le/0,
prop_ceil_ge/0,
prop_floor_ceil_relation/0,
prop_truncate_between/0,
prop_round_nearest/0,
prop_to_mixed_round_trip/0,
prop_to_mixed_proper/0,
prop_is_integer_consistent/0,
prop_is_proper_consistent/0,
prop_parse_format_round_trip/0,
prop_from_integer_is_integer/0,
prop_parse_integer_string/0,
prop_parse_zero_denominator/0,
prop_cf_round_trip/0,
prop_last_convergent_is_value/0,
prop_mediant_between/0,
prop_is_reduced_after_reduce/0,
prop_lcm_divisible/0,
prop_is_unit_fraction_basic/0,
prop_rationalize_within_tolerance/0,
prop_egyptian_reconstructs/0,
prop_egyptian_all_units/0,
prop_egyptian_distinct/0,
prop_farey_endpoints/0,
prop_farey_ascending/0,
prop_farey_reduced_bounded/0
]).
%%% Generators
non_zero_integer() ->
?SUCHTHAT(D, integer(), D =/= 0).
fraction() ->
?LET({N, D}, {integer(), non_zero_integer()}, rationals:new(N, D)).
fraction_list() ->
list(fraction()).
small_non_neg_integer() ->
range(0, 10).
%%% Oracles — independent of normalize/compare
%% a/b == c/d <=> a*d == c*b. Independent of normalize/compare.
same_value(F1, F2) ->
{N1, D1} = rationals:ratio(F1),
{N2, D2} = rationals:ratio(F2),
N1 * D2 =:= N2 * D1.
sgn(X) when X < 0 -> -1;
sgn(X) when X > 0 -> 1.
%% Sign-aware ordering oracle. Independent of normalize/compare.
expected_compare(F1, F2) ->
{N1, D1} = rationals:ratio(F1),
{N2, D2} = rationals:ratio(F2),
cross_to_order((N1 * D2 - N2 * D1) * sgn(D1) * sgn(D2)).
cross_to_order(Cross) when Cross < 0 -> lt;
cross_to_order(Cross) when Cross > 0 -> gt;
cross_to_order(_) -> eq.
%%% Properties
prop_normalize_idempotent() ->
?FORALL(
F,
fraction(),
rationals:normalize(rationals:normalize(F)) =:=
rationals:normalize(F)
).
prop_normalize_denominator_positive() ->
?FORALL(
F,
fraction(),
rationals:denominator(rationals:normalize(F)) > 0
).
prop_normalize_reduced() ->
?FORALL(
F,
fraction(),
begin
N = rationals:normalize(F),
Num = erlang:abs(rationals:numerator(N)),
Den = rationals:denominator(N),
rationals:gcd(Num, Den) =:= 1 orelse
(Num =:= 0 andalso Den =:= 1)
end
).
prop_normalize_value_preserved() ->
?FORALL(
{N, D},
{integer(), non_zero_integer()},
same_value(
rationals:normalize(rationals:new(N, D)),
rationals:new(N, D)
)
).
prop_compare_matches_oracle() ->
?FORALL(
{F1, F2},
{fraction(), fraction()},
rationals:compare(F1, F2) =:= expected_compare(F1, F2)
).
prop_compare_antisymmetric() ->
?FORALL(
{F1, F2},
{fraction(), fraction()},
begin
C1 = rationals:compare(F1, F2),
C2 = rationals:compare(F2, F1),
Self = rationals:compare(F1, F1),
(Self =:= eq) andalso
case C1 of
gt -> C2 =:= lt;
lt -> C2 =:= gt;
eq -> C2 =:= eq
end
end
).
prop_compare_sign_regression() ->
?FORALL(
{P, Q},
{pos_integer(), pos_integer()},
rationals:compare(rationals:new(1, -P), rationals:new(1, Q)) =:= lt andalso
rationals:compare(rationals:new(1, P), rationals:new(1, -Q)) =:= gt
).
prop_zero_identity() ->
?FORALL(
F,
fraction(),
same_value(rationals:add(F, rationals:zero()), F)
).
prop_one_identity() ->
?FORALL(
F,
fraction(),
same_value(rationals:multiply(F, rationals:one()), F)
).
%%% Phase 1 properties
prop_negate_additive_inverse() ->
?FORALL(
F,
fraction(),
same_value(rationals:add(F, rationals:negate(F)), rationals:zero())
).
prop_negate_involution() ->
?FORALL(
F,
fraction(),
same_value(rationals:negate(rationals:negate(F)), F)
).
prop_abs_non_negative() ->
?FORALL(
F,
fraction(),
not rationals:is_negative(rationals:abs(F))
).
prop_abs_idempotent() ->
?FORALL(
F,
fraction(),
same_value(rationals:abs(rationals:abs(F)), rationals:abs(F))
).
prop_sign_matches_compare() ->
?FORALL(
F,
fraction(),
begin
S = rationals:sign(F),
C = rationals:compare(F, rationals:zero()),
case C of
gt -> S =:= 1;
lt -> S =:= -1;
eq -> S =:= 0
end
end
).
prop_sign_trichotomy() ->
?FORALL(
F,
fraction(),
begin
Z = rationals:is_zero(F),
P = rationals:is_positive(F),
N = rationals:is_negative(F),
[true] =:= [X || X <- [Z, P, N], X]
end
).
prop_pow_zero() ->
?FORALL(
F,
fraction(),
same_value(rationals:pow(F, 0), rationals:one())
).
prop_pow_one() ->
?FORALL(
F,
fraction(),
same_value(rationals:pow(F, 1), F)
).
prop_pow_add() ->
?FORALL(
{F, A, B},
{fraction(), small_non_neg_integer(), small_non_neg_integer()},
same_value(
rationals:pow(F, A + B),
rationals:multiply(rationals:pow(F, A), rationals:pow(F, B))
)
).
prop_min_lower_bound() ->
?FORALL(
{F1, F2},
{fraction(), fraction()},
begin
M = rationals:min(F1, F2),
rationals:lte(M, F1) andalso
rationals:lte(M, F2) andalso
(same_value(M, F1) orelse same_value(M, F2))
end
).
prop_max_upper_bound() ->
?FORALL(
{F1, F2},
{fraction(), fraction()},
begin
M = rationals:max(F1, F2),
rationals:gte(M, F1) andalso
rationals:gte(M, F2) andalso
(same_value(M, F1) orelse same_value(M, F2))
end
).
prop_clamp_in_range() ->
?FORALL(
{F, X, Y},
{fraction(), fraction(), fraction()},
begin
Lo = rationals:min(X, Y),
Hi = rationals:max(X, Y),
rationals:between(rationals:clamp(F, Lo, Hi), Lo, Hi)
end
).
prop_dist_symmetric() ->
?FORALL(
{A, B},
{fraction(), fraction()},
same_value(rationals:dist(A, B), rationals:dist(B, A))
).
prop_dist_non_negative() ->
?FORALL(
{A, B},
{fraction(), fraction()},
not rationals:is_negative(rationals:dist(A, B))
).
prop_sum_append() ->
?FORALL(
{L1, L2},
{fraction_list(), fraction_list()},
same_value(
rationals:sum(L1 ++ L2),
rationals:add(rationals:sum(L1), rationals:sum(L2))
)
).
prop_product_append() ->
?FORALL(
{L1, L2},
{fraction_list(), fraction_list()},
same_value(
rationals:product(L1 ++ L2),
rationals:multiply(rationals:product(L1), rationals:product(L2))
)
).
%%% Phase 2 properties
prop_floor_le() ->
?FORALL(
F,
fraction(),
begin
Fl = rationals:floor(F),
rationals:compare(rationals:new(Fl, 1), F) =/= gt andalso
rationals:compare(F, rationals:new(Fl + 1, 1)) =:= lt
end
).
prop_ceil_ge() ->
?FORALL(
F,
fraction(),
begin
C = rationals:ceil(F),
rationals:compare(rationals:new(C, 1), F) =/= lt andalso
rationals:compare(F, rationals:new(C - 1, 1)) =:= gt
end
).
prop_floor_ceil_relation() ->
?FORALL(
F,
fraction(),
begin
Diff = rationals:ceil(F) - rationals:floor(F),
case rationals:is_integer(F) of
true -> Diff =:= 0;
false -> Diff =:= 1
end
end
).
prop_truncate_between() ->
?FORALL(
F,
fraction(),
begin
T = rationals:truncate(F),
Fl = rationals:floor(F),
C = rationals:ceil(F),
T >= erlang:min(Fl, C) andalso T =< erlang:max(Fl, C) andalso
case rationals:compare(F, rationals:zero()) of
lt -> T =:= C;
_ -> T =:= Fl
end
end
).
prop_round_nearest() ->
?FORALL(
F,
fraction(),
rationals:compare(
rationals:dist(F, rationals:new(rationals:round(F), 1)),
rationals:new(1, 2)
) =/= gt
).
prop_to_mixed_round_trip() ->
?FORALL(
F,
fraction(),
begin
{W, Frac} = rationals:to_mixed(F),
same_value(
rationals:from_mixed(W, rationals:numerator(Frac), rationals:denominator(Frac)),
F
)
end
).
prop_to_mixed_proper() ->
?FORALL(
F,
fraction(),
rationals:is_proper(element(2, rationals:to_mixed(F)))
).
prop_is_integer_consistent() ->
?FORALL(
F,
fraction(),
rationals:is_integer(F) =:=
(rationals:compare(F, rationals:new(rationals:truncate(F), 1)) =:= eq)
).
prop_is_proper_consistent() ->
?FORALL(
F,
fraction(),
rationals:is_proper(F) =:=
(rationals:compare(rationals:abs(F), rationals:one()) =:= lt)
).
%%% Phase 3 properties
prop_parse_format_round_trip() ->
?FORALL(
F,
fraction(),
begin
{ok, G} = rationals:parse(rationals:format(F)),
same_value(G, F)
end
).
prop_from_integer_is_integer() ->
?FORALL(
N,
integer(),
rationals:is_integer(rationals:from_integer(N)) andalso
same_value(rationals:from_integer(N), rationals:new(N, 1))
).
prop_parse_integer_string() ->
?FORALL(
N,
integer(),
begin
{ok, G} = rationals:parse(integer_to_list(N)),
same_value(G, rationals:from_integer(N))
end
).
prop_parse_zero_denominator() ->
?FORALL(
N,
integer(),
rationals:parse(integer_to_list(N) ++ "/0") =:= {error, zero_denominator}
).
%%% Phase 4 properties
prop_cf_round_trip() ->
?FORALL(
F,
fraction(),
same_value(rationals:from_continued_fraction(rationals:continued_fraction(F)), F)
).
prop_last_convergent_is_value() ->
?FORALL(
F,
fraction(),
same_value(lists:last(rationals:convergents(F)), F)
).
prop_mediant_between() ->
?FORALL(
{A, B},
{fraction(), fraction()},
begin
Lo = rationals:min(A, B),
Hi = rationals:max(A, B),
M = rationals:mediant(A, B),
rationals:lte(Lo, M) andalso rationals:lte(M, Hi)
end
).
prop_is_reduced_after_reduce() ->
?FORALL(
F,
fraction(),
rationals:is_reduced(rationals:reduce(F))
).
prop_lcm_divisible() ->
?FORALL(
{A, B},
{non_zero_integer(), non_zero_integer()},
begin
L = rationals:lcm(A, B),
L >= 0 andalso L rem A =:= 0 andalso L rem B =:= 0
end
).
prop_is_unit_fraction_basic() ->
?FORALL(
P,
pos_integer(),
rationals:is_unit_fraction(rationals:new(1, P))
).
prop_rationalize_within_tolerance() ->
?FORALL(
{X, K},
{float(-100.0, 100.0), range(1, 1000)},
begin
Eps = 1.0 / K,
R = rationals:rationalize(X, Eps),
erlang:abs(rationals:to_float(R) - X) =< Eps + 1.0e-9
end
).
%%% Phase 5 properties
proper_fraction() ->
?LET({A, B}, {pos_integer(), pos_integer()}, rationals:new(A, A + B)).
prop_egyptian_reconstructs() ->
?FORALL(
F,
proper_fraction(),
same_value(rationals:sum(rationals:egyptian(F)), F)
).
prop_egyptian_all_units() ->
?FORALL(
F,
proper_fraction(),
lists:all(fun rationals:is_unit_fraction/1, rationals:egyptian(F))
).
prop_egyptian_distinct() ->
?FORALL(
F,
proper_fraction(),
begin
Ratios = [rationals:ratio(E) || E <- rationals:egyptian(F)],
length(Ratios) =:= length(lists:usort(Ratios))
end
).
prop_farey_endpoints() ->
?FORALL(
N,
range(1, 30),
begin
Seq = rationals:farey(N),
same_value(hd(Seq), rationals:zero()) andalso
same_value(lists:last(Seq), rationals:one())
end
).
prop_farey_ascending() ->
?FORALL(
N,
range(1, 30),
begin
Seq = rationals:farey(N),
pairs_ascending(Seq)
end
).
prop_farey_reduced_bounded() ->
?FORALL(
N,
range(1, 30),
begin
Seq = rationals:farey(N),
lists:all(
fun(F) ->
rationals:is_reduced(F) andalso
rationals:denominator(F) =< N
end,
Seq
)
end
).
pairs_ascending([_]) -> true;
pairs_ascending([A, B | Rest]) -> rationals:lt(A, B) andalso pairs_ascending([B | Rest]).