1/*
    2Author: Conrado M. Rodriguez <Conrado.Rgz@gmail.com> https://github.com/crgz
    3*/
    4:- module(abbreviated_dates,
    5[
    6  parse/3,      % +Context, +Expression, ?Dates
    7  parse/4,      % +Context, +Expression, ?Dates, ?Syntax
    8  parse/5,      % +Context, +Expression, ?Dates, ?Syntax, ?Language
    9  parse/6,      % +Context, +Expression, ?Dates, ?Syntax, ?Language, ?Country
   10  single_day/7  % Esported for testing purpose. (See: abbreviated_dates.plt)
   11]).   12
   13:- [library(dcg/basics)].   14:- use_module(library(date_time)).   15:- use_module(facts/languages).  % Facts about languages
   16:- use_module(facts/country_language).   17:- use_module(facts/country_date_endianness).
 parse(+Context, +Expression, ?Dates)
True if Dates can be parsed from Expression and Dates is greater than the Reference date.
parse(date(29,02,2020), 'saturday, 23 april', Dates, Syntax, Language).
   27parse(Context, Expression, Dates) :-
   28  parse(Context, Expression, Dates, _, _, _).
   29
   30parse(Context, Expression, Dates, Syntax) :-
   31  parse(Context, Expression, Dates, Syntax, _, _).
   32
   33parse(Context, Expression, Dates, Syntax, Language) :-
   34  parse(Context, Expression, Dates, Syntax, Language, _).
   35
   36parse(Context, Expression, Dates, Syntax, Language, Country) :-
   37  atom_codes(Expression, Codes), phrase(multiple_days([Context], Dates, Language, Syntax, Country), Codes).
   38
   39%-----------------------------------------------------------
   40% Grammar
   41%
   42multiple_days(_, [], _, [], _) --> [].
   43multiple_days([LastKnownDate|Other], [SingleDay|MultipleDays], Language, [S1|S2], Country) -->
   44  single_day([LastKnownDate|Other], SingleDay, Language, S1, Country),
   45  (" - " | eos),
   46  multiple_days([SingleDay, LastKnownDate|Other], MultipleDays, Language, S2, Country).
   47
   48% DATES HINTING WEEKDAY NAME, DAY NUMBER AND MONTH NAME
   49
   50% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax, Country), `Wednesday, 1 July`).
   51single_day([Context|_], date(Year,MonthNumber,Day), Language, Syntax, _) -->
   52  string_without(",", WeekDay), ",", b, date_number(Day), b, string(Month),
   53  {
   54    factor_month(Month, implicit, Language, MonthNumber, MonthFormat),
   55    factor_week_day(WeekDay, WeekDayNumber, Language, WeekDaySyntax),
   56    possible_year(Context, Year),
   57    week_dayn(date(Year,MonthNumber,Day), WeekDayNumber),
   58    date_compare(date(Year,MonthNumber,Day), >=, Context),
   59    atomic_list_concat([WeekDaySyntax, ', %d ', MonthFormat], Syntax)
   60  }.
   61
   62% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax, Country), `Wednesday, 1 Jun.`).
   63single_day([Context|_], date(Year,MonthNumber,Day), Language, Syntax, _) -->
   64  string_without(",", WeekDay), ",", b, date_number(Day), b, string(Month), ".",
   65  {
   66    factor_month(Month, explicit, Language, MonthNumber, MonthFormat),
   67    factor_week_day(WeekDay, WeekDayNumber, Language, WeekDaySyntax),
   68    possible_year(Context, Year),
   69    week_dayn(date(Year,MonthNumber,Day), WeekDayNumber),
   70    date_compare(date(Year,MonthNumber,Day), >=, Context),
   71    atomic_list_concat([WeekDaySyntax, ', %d ', MonthFormat], Syntax)
   72  }.
   73
   74% DATES HINTING WEEKDAY NAMES AND TWO NUMBERS
   75
   76% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax), `Pirm. 06-20`).
   77single_day([Context|_], Date, Language, Syntax, Country) --> % Explicit abbreviation
   78  string(WeekDayCodes), ".", b, date_number(First), separator, date_number(Second),
   79  {
   80    solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country),
   81    atomic_list_concat([WeekDaySyntax, DayMonthSyntax], ' ', Syntax)
   82  }.
   83% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax), `Pirm, 06-20`).
   84single_day([Context|_], Date, Language, Syntax, Country) -->
   85  string(WeekDayCodes), ",", b, date_number(First), separator, date_number(Second),
   86  {
   87    solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country),
   88    atomic_list_concat([WeekDaySyntax, DayMonthSyntax], ' ', Syntax)
   89  }.
   90% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax), `Pirm 06-20`).
   91single_day([Context|_], Date, Language, Syntax, Country) -->
   92  string(WeekDayCodes), b, date_number(First), separator, date_number(Second),
   93  {
   94    solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country),
   95    atomic_list_concat([WeekDaySyntax, DayMonthSyntax], ' ', Syntax)
   96  }.
   97% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax), `Th., 06-20`).
   98single_day([Context|_], Date, Language, Syntax, Country) -->
   99  string(WeekDayCodes), ".", ",", b, integer(First), "-", integer(Second),
  100  {
  101    solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country),
  102    atomic_list_concat([WeekDaySyntax, '., ', DayMonthSyntax], Syntax)
  103  }.
  104
  105% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax), `Th., 16.06.`).
  106single_day([Context|_], Date, Language, Syntax, Country) -->
  107  string(WeekDayCodes), ".", ",", b, integer(First), ".", integer(Second), ".",
  108  {
  109    solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country),
  110    atomic_list_concat([WeekDaySyntax, '., ', DayMonthSyntax], Syntax)
  111  }.
  112
  113% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax), `Th., 16.06`).
  114single_day([Context|_], Date, Language, Syntax, Country) -->
  115  string(WeekDayCodes), ".", ",", b, integer(First), ".", integer(Second),
  116  {
  117    solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country),
  118    atomic_list_concat([WeekDaySyntax, '., ', DayMonthSyntax], Syntax)
  119  }.
  120
  121% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax, Country), `Th., 16. 06`).
  122single_day([Context|_], Date, Language, Syntax, Country) -->
  123  string(WeekDayCodes), ".", ",", b, integer(First), ".", b, integer(Second),
  124  {
  125    solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country),
  126    atomic_list_concat([WeekDaySyntax, '., ', DayMonthSyntax], Syntax)
  127  }.
  128
  129% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax, Country), `Th., 16/06`).
  130single_day([Context|_], Date, Language, Syntax, Country) -->
  131  string(WeekDayCodes), ".", ",", b, integer(First), "/", integer(Second),
  132  {
  133    solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country),
  134    atomic_list_concat([WeekDaySyntax, '., ', DayMonthSyntax], Syntax)
  135  }.
  136
  137% DATES HINTING WEEKDAY NAMES AND ONE NUMBER
  138
  139% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax), `saturday, 23`).
  140single_day([Context|_], Date, Language, Syntax, _) -->
  141  string(WeekDayCodes), ",", b, month_day(Day),
  142  {
  143    factor_week_day(WeekDayCodes, WeekDayNumber, Language, WeekDaySyntax),
  144    possible_day(Context, Day, Date),
  145    week_dayn(Date, WeekDayNumber),
  146    atomic_list_concat([WeekDaySyntax, ' %d'], Syntax)
  147  }.
  148
  149% DATES HINTING TWO NUMBERS AND WEEKDAY NAMES
  150
  151% phrase(abbreviated_dates:single_day([date(2022, 2, 28)], Date, Language, Syntax), `06-20, Pirm`).
  152single_day([Context|_], Date, Language, Syntax, Country) -->
  153  date_number(First), separator, date_number(Second), ",", b, string(WeekDayCodes),
  154  {
  155    solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country),
  156    atomic_list_concat([DayMonthSyntax, WeekDaySyntax], ' ', Syntax)
  157  }.
  158
  159% DATES HINTING ONE NUMBER AND A MONTH NAME
  160
  161% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax), `4 July`).
  162single_day([Context|_], Date, Language, Syntax, _) -->
  163  month_day(Day), b, string(Month),
  164  {factor_month_day(Context, Day, Month, implicit, Date, Language, MonthFormat),atom_concat('%d ', MonthFormat, Syntax)}.
  165
  166% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax), `23 Sep.`).
  167single_day([Context|_], Date, Language, Syntax, _) --> % Explicit abbreviation
  168  month_day(Day), b, string(Month), ".", 
  169  {factor_month_day(Context, Day, Month, explicit, Date, Language, MonthFormat),atom_concat('%d ', MonthFormat, Syntax)}.
  170
  171% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax), `23. Sep`).
  172single_day([Context|_], Date, Language, Syntax, _) --> % Explicit abbreviation
  173  month_day(Day), ".", b, string(Month), 
  174  {factor_month_day(Context, Day, Month, implicit, Date, Language, MonthFormat),atom_concat('%d ', MonthFormat, Syntax)}.
  175
  176% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax), `23. Sep.`).
  177single_day([Context|_], Date, Language, Syntax, _) --> % Explicit abbreviation
  178  month_day(Day), ".", b, string(Month), ".", 
  179  {factor_month_day(Context, Day, Month, implicit, Date, Language, MonthFormat),atom_concat('%d ', MonthFormat, Syntax)}.
  180
  181% DATES HINTING A MONTH NAME AND ONE NUMBER 
  182
  183% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax), `Jan. 1`).
  184single_day([Context|_], Date, Language, Syntax, _) -->
  185  string(Month), b, month_day(Day),
  186  {factor_month_day(Context, Day, Month, implicit, Date, Language, MonthFormat), atom_concat(MonthFormat,' %d', Syntax)}.
  187
  188% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax), `Jan. 1`).
  189single_day([Context|_], Date, Language, Syntax, _) -->
  190  string(Month), ".", b, month_day(Day),
  191  {factor_month_day(Context, Day, Month, explicit, Date, Language, MonthFormat), atom_concat(MonthFormat,' %d', Syntax)}.
  192  
  193% DATES HINTING JUST DAYS
  194
  195% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax), `31`).
  196single_day([Context|_], Date, Language, '%d', _) -->
  197  month_day(D),
  198  {future_date(Context, D, Date), language(Language)}.
  199
  200% phrase(abbreviated_dates:single_day([date(2020, 2, 28)], Date, Language, Syntax), `31.`).
  201single_day([Context|_], Date, Language, '%d', _) -->
  202  month_day(D), ".",
  203  {future_date(Context, D, Date), language(Language)}.
  204
  205% DATES HINTING RELATIVE DAYS
  206
  207% phrase(abbreviated_dates:single_day([date(2020, 2, 29)], Date, Language, Syntax), `Tomorrow`).
  208single_day([Context|_], Date, Language, Syntax, _) -->
  209  nonblanks(Codes),
  210  {atom_codes(Adverb, Codes), adverb(Language, Adverb, Context, Date, Syntax)}.
  211
  212
  213month_day(Day) --> integer(Day), {between(1, 31, Day)}.
  214date_number(N) --> integer(N).
  215date_number(N) --> integer(N), ".".
  216separator --> "-"|"."|"/"|" ".
  217b --> white.
  218
  219%-----------------------------------------------------------
  220% Grammar supporting predicates
  221%
  222
  223solve_date_numbers(Context, WeekDayCodes, First, Second, Date, Language, DayMonthSyntax, WeekDaySyntax, Country):-
  224  factor_week_day(WeekDayCodes, WeekDayNumber, Language, WeekDaySyntax),
  225  possible_year(Context, Year),
  226  factor_country_endianness(Language, First, Second, date(Year,Month,Day), DayMonthSyntax, Country),
  227  week_dayn(date(Year,Month,Day), WeekDayNumber),
  228  Date = date(Year,Month,Day).
  229
  230factor_month_day(Context, Day, Month, Style, date(Year,MonthNumber,Day), Language, MonthFormat):-
  231  factor_month(Month, Style, Language, MonthNumber, MonthFormat),
  232  possible_year(Context, Year),
  233  date_compare(date(Year,MonthNumber,Day), >=, Context).
  234
  235factor_week_day(InputCodes, WeekDayNumber, Language, Format):-
  236  atom_codes(InputAtom, InputCodes),
  237  downcase_atom(InputAtom, LowerCaseInputAtom),
  238  week_day_name(Language, WeekDayNumber, WeekDayName),
  239  downcase_atom(WeekDayName, LowerCaseWeekDayName),
  240  abbreviation(LowerCaseWeekDayName, LowerCaseInputAtom, IsAbbreviated),
  241  (IsAbbreviated -> Format = '%a'; Format = '%A').
  242
  243factor_month(Month, Style, Language, MonthNumber, Syntax):-
  244  month_name(Language, MonthNumber, MonthName),
  245  abbreviation(MonthName, Abbreviation, IsAbbreviated),
  246  atom_codes(MaybeAbbreviation, Month),
  247  capitalize_sentence(MaybeAbbreviation, Abbreviation),
  248  (IsAbbreviated-> MonthFormat = '%b'; MonthFormat = '%B'),
  249  (IsAbbreviated, Style = explicit -> Suffix = '.'; Suffix = ''),
  250  atomic_list_concat([MonthFormat, Suffix], Syntax).
  251
  252factor_country_endianness(Language, First, Second, date(Year,Month,Day), Syntax, Country):-
  253  top_endianness(Country, Endianness),     % Find a country with defined endianness
  254  top_country_language(Country, Language), % Check if the language is spoken there
  255  day_month_order(Endianness, First, Second, Day, Month, Syntax),
  256  valid(date(Year,Month,Day)).
  257
  258day_month_order(little, Day,   Month, Day, Month, '%d %m'). % day is first number in little endian dates
  259day_month_order(middle, Month, Day,   Day, Month, '%m %d'). % day is second number in little middle dates
  260day_month_order(big,    Month, Day,   Day, Month, '%m %d'). % day is second number in big middle dates
  261
  262valid(date(Year,Month,Day)):- Month =< 12, date_month_days(Month,Year,MD), Day =< MD.
  263
  264possible_year(Context, Year):-
  265  date_extract(Context, years(Y)),
  266  Max is Y + 6,
  267  between(Y, Max, Year).
  268
  269possible_day(Context, Day, Date):-
  270  date_extract(Context, years(Year)),
  271  date_extract(Context, months(M)),
  272  Max is M + 24,
  273  between(M, Max, Month),
  274  future_date(date(Year,Month,Day), Date).
  275
  276% Find optional abbreviations ordering by length
  277abbreviation(Atom, Abbreviation, IsAbbreviated):-
  278  abbreviation_all_letters(Atom, Abbreviation, IsAbbreviated);
  279  abbreviation_consonant(Atom, Abbreviation, IsAbbreviated).
  280
  281abbreviation_all_letters(Atom, Abbreviation, IsAbbreviated):-
  282  order_by([desc(L)], (sub_atom(Atom, 0, _, After, Abbreviation), atom_length(Abbreviation,L))),
  283  L > 0,
  284  (After = 0 -> IsAbbreviated = false; IsAbbreviated = true).
  285
  286abbreviation_consonant(Atom, Abbreviation, IsAbbreviated):-
  287  atom_remove_vowels(Atom, AtomConsonants),
  288  abbreviation_all_letters(AtomConsonants, Abbreviation, IsAbbreviated).
  289
  290atom_remove_vowels(Atom, AtomConsonants):-
  291  atom_chars(Atom, Chars),
  292  subtract(Chars, [a, ą, e, ę, i, o, ó, u], Consonants),
  293  remove_duplicates(Consonants, Uniques),
  294  atom_chars(AtomConsonants, Uniques).
  295
  296future_date(date(Y, M, D), Day, date(YY,MM,DD)):-
  297  date_month_days(M,Y,MD),
  298  D =< Day, Day =< MD, % Day in current month
  299  !,
  300  future_date(date(Y,M,Day), date(YY,MM,DD)).
  301future_date(date(Y, M, D), Day, date(YY,MM,DD)):-
  302  date_month_days(M,Y,MD),
  303  (Day =< D; MD =< Day),  % Day out of current month
  304  !,
  305  M2 is M + 1,
  306  future_date(date(Y,M2,Day), date(YY,MM,DD)).
  307future_date(date(Y, M, D), date(YY,MM,DD)):-
  308  M > 12,
  309  !,
  310  M2 is M - 12,
  311  Y2 is Y + 1,
  312  future_date(date(Y2,M2,D), date(YY,MM,DD)).
  313future_date(date(Y,M,D), date(Y,M,D)).
  314
  315date_month_days(0,_,31).
  316date_month_days(1,_,31).
  317date_month_days(2,Y,29) :- date_leap_year(Y), !.
  318date_month_days(2,_,28).
  319date_month_days(3,_,31).
  320date_month_days(4,_,30).
  321date_month_days(5,_,31).
  322date_month_days(6,_,30).
  323date_month_days(7,_,31).
  324date_month_days(8,_,31).
  325date_month_days(9,_,30).
  326date_month_days(10,_,31).
  327date_month_days(11,_,30).
  328date_month_days(12,_,31).
  329date_month_days(13,_,31).
  330
  331date_leap_year(Y) :-
  332   ( ( 0 =:= Y mod 100, 0 =:= Y mod 400 ) ;
  333     ( 0 =\= Y mod 100, 0 =:= Y mod 4 ) ).
  334
  335%
  336% Utility predicates
  337%
  338remove_duplicates(List, Uniques):- remove_duplicates(List, Uniques, []).
  339remove_duplicates([], [], _).
  340remove_duplicates([Head|Tail], Result, Seen) :-
  341  (  member(Head, Seen)
  342  -> (Result = Uniques, NewSeen = Seen)
  343  ;  (Result = [Head|Uniques], NewSeen = [Head])
  344  ),
  345  remove_duplicates(Tail, Uniques, NewSeen).
  346
  347capitalize_sentence(LowerCase, UpperCase):-
  348  atom_chars(LowerCase, [Lower|Tail]),
  349  char_type(Lower, to_lower(Upper)),
  350  atom_chars(UpperCase, [Upper|Tail])