Writing a small set of date math predicates using scryer prolog, evaluating in emacs using edi-prolog. There's a cut in the first branch of date_end_of_month below, which I intended to mean "If the month is 2 and the year is a leap year, the number of days is 29; otherwise it's the number of days defined in the list months".
But the edi-prolog queries seem to say otherwise - it seems to accept either 28 and 29 as the last day of the month in a leap year, and I don't understand why. Would love some help understanding what's going on here and why the cut isn't having the effect I expected.
:- use_module(library(lists)).
:- use_module(library(clpz)).
months([
january-31,
february-28,
march-31,
april-30,
may-31,
june-30,
july-31,
august-31,
september-30,
october-31,
november-30,
december-31
]).
nth1_month_days(N, Days) :-
months(Ms),
nth1(N, Ms, _-Days).
date_end_of_month(date(Year, 2, _), 29) :-
leap_year(Year), !.
date_end_of_month(date(Year, M, _), Days) :-
nth1_month_days(M, Days).
leap_year(Y) :-
Diff #= Y - 2000,
0 #= Diff rem 4.
date_is_end_of_month(date(Year, Month, Day)) :-
date_end_of_month(date(Year, Month, Day), Day).
% ?- leap_year(2024).
%@ true.
% date's day is 29, and `date_is_end_of_month` succeeds as expected.
% ?- Year = 2024, Date = date(Year, 2, 29), date_month_days(Date, D), date_is_end_of_month(Date).
%@ Year = 2024, Date = date(2024,2,29), D = 29.
# date's day is 28. Expected this query to fail because it isn't e last day of the month, but it also succeeds, correctly unifying D with 29.
% ?- Year = 2024, Date = date(Year, 2, 28), date_end_of_month(Date, D), date_is_end_of_month(Date).
%@ Year = 2024, Date = date(2024,2,28), D = 29.
% day is never the end of the month: fails as expected.
% ?- Year = 2024, Date = date(Year, 2, 27), date_end_of_month(Date, D), date_is_end_of_month(Date).
%@ false.
When I replace the cut with a negation of the leap_year goal in the second definition of date_end_of_month, it works as expected. So I believe it's my use of the cut that is incorrect, but I don't understand why.
date_end_of_month(date(Year, 2, _), 29) :-
leap_year(Year).
date_end_of_month(date(Year, M, _), Days) :-
\+ leap_year(Year),
nth1_month_days(M, Days).
% (rest of the code remains the same)
% correctly fails
% ?- Year = 2024, Date = date(Year, 2, 28), date_end_of_month(Date, D), leap_year(Year), date_is_end_of_month(Date).
%@ false.
% correctly succeeds with an extra backtracking point that fails
% ?- Year = 2024, Date = date(Year, 2, 29), date_end_of_monthggggggb(Date, D), leap_year(Year), date_is_end_of_month(Date).
%@ Year = 2024, Date = date(2024,2,29), D = 29
%@ ; false.
# correctly fails - never the end of Feb.
% ?- Year = 2024, Date = date(Year, 2, 27), date_month_days(Date, D), leap_year(Year), date_is_end_of_month(Date).
%@ false.
Thanks to @TA_intern for the comment above not to mix constraints and cuts. This code for date_end_of_month works as expected, using reif's if_ instead of a cut to implement the if-then-else.
date_end_of_month(date(Y, M, _), EndOfMonth) :-
if_(date_is_leap_month(date(Y, M, _)),
EndOfMonth = 29,
nth1_month_days(M, Days)).
date_is_leap_month(date(Year, 2, _), true) :-
leap_year(Year).
date_is_leap_month(date(_, Month, _), false) :-
Month \= 2.
leap_year(Y) :-
0 #= (Y - 2000) rem 4.