如何使时间解析谓词双向工作?

How can I make a time parsing predicate work in both directions?

我使用 SWI-Prolog 制作了这个简单的谓词,它将 hh:mm 格式的时间与时间项相关联。

time_string(time(H,M), String) :-
    number_string(H,Hour),
    number_string(M,Min),
    string_concat(Hour,":",S),
    string_concat(S,Min,String).

虽然谓词只能在一个方向上起作用。

time_string(time(10,30),String).
String = "10:30".      % This is perfect.

很遗憾,此查询失败。

time_string(Time,"10:30").
ERROR: Arguments are not sufficiently instantiated
ERROR: In:
ERROR:   [11] number_string(_8690,_8692)
ERROR:   [10] time_string(time(_8722,_8724),"10:30") at /tmp/prolcompDJBcEE.pl:74
ERROR:    [9] toplevel_call(user:user: ...) at /usr/local/logic/lib/swipl/boot/toplevel.pl:1107

如果我不必编写一个全新的谓词来回答这个查询,那就太好了。我有办法做到这一点吗?

好吧,从结构化术语 time(H,M) 到字符串 String 比从非结构化 String 术语 String 更容易=12=].

您的谓词在“生成”方向起作用。

对于另一个方向,您想要解析 String。在这种情况下,这在计算上很容易,无需 search/backtracking 即可完成,这很好!

使用 Prolog 的 "Definite Clause Grammar" syntax which are "just" a nice way to write predicates that process a "list of stuff". In this case the list of stuff is a list of characters (atoms of length 1). (For the relevant page from SWI-Prolog, see here)

运气好的话,DCG代码可以运行backwards/forwards,但一般情况下不会这样。满足某些效率或因果关系需求的真实代码可能会迫使它在单个谓词的幕后,您首先按“处理方向”分支,然后 运行 通过相当不同的代码结构来交付货物。

所以在这里。代码立即“衰减”到解析中并生成分支。 Prolog 还没有完全表现 constraint-based。你只是先做一些事情。

无论如何,让我们这样做:

:- use_module(library(dcg/basics)).

% ---
% "Generate" direction; note that String may be bound to something
% in which case this clause also verifies whether generating "HH:MM"
% from time(H,M) indeed yields (whatever is denoted by) String.
% ---

process_time(time(H,M),String) :-
   integer(H),                            % Demand that H,M are valid integers inside limits
   integer(M),
   between(0,23,H),
   between(0,59,M),
   !,                                     % Guard passed, commit to this code branch
   phrase(time_g(H,M),Chars,[]),          % Build Codes from time/2 Term
   string_chars(String,Chars).            % Merge Codes into a string, unify with String

% ---
% "Parse" direction. 
% ---

process_time(time(H,M),String) :-
   string(String),                        % Demand that String be a valid string; no demands on H,M  
   !,                                     % Guard passed, commit to this code branch
   string_chars(String,Chars),            % Explode String into characters
   phrase(time_p(H,M),Chars,[]).          % Parse "Codes" into H and M

% ---
% "Generate" DCG
% ---
   
time_g(H,M) --> hour_g(H), [':'], minute_g(M).
hour_g(H)   --> { divmod(H,10,V1,V2), digit_int(D1,V1), digit_int(D2,V2) }, digit(D1), digit(D2).
minute_g(M) --> { divmod(M,10,V1,V2), digit_int(D1,V1), digit_int(D2,V2) }, digit(D1), digit(D2).

% ---
% "Parse" DCG
% ---
   
time_p(H,M) --> hour_p(H), [':'], minute_p(M).
hour_p(H)   --> digit(D1), digit(D2), { digit_int(D1,V1), digit_int(D2,V2), H is V1*10+V2, between(0,23,H) }.
minute_p(M) --> digit(D1), digit(D2), { digit_int(D1,V1), digit_int(D2,V2), M is V1*10+V2, between(0,59,M) }.   
   
% ---
% Do I really have to code this? Oh well!
% ---

digit_int('0',0).
digit_int('1',1).
digit_int('2',2).
digit_int('3',3).
digit_int('4',4).
digit_int('5',5).
digit_int('6',6).
digit_int('7',7).
digit_int('8',8).
digit_int('9',9).

% ---
% Let's add plunit tests!
% ---

:- begin_tests(hhmm).

test("parse 1",    true(T == time(0,0)))   :- process_time(T,"00:00").
test("parse 2",    true(T == time(12,13))) :- process_time(T,"12:13").
test("parse 1",    true(T == time(23,59))) :- process_time(T,"23:59").
test("generate",   true(S == "12:13"))     :- process_time(time(12,13),S).
test("verify",     true)                   :- process_time(time(12,13),"12:13").
test("complete",   true(H == 12))          :- process_time(time(H,13),"12:13").

test("bad parse",    fail)                 :- process_time(_,"66:66").
test("bad generate", fail)                 :- process_time(time(66,66),_).

:- end_tests(hhmm).

代码太多了。

有效吗?

?- run_tests.
% PL-Unit: hhmm ........ done
% All 8 tests passed
true.

鉴于模式的简单性,DCG 可能被认为是矫枉过正,但实际上它为我们提供了一种轻松访问原子成分的途径,我们可以将这些成分输入某些声明性算术库中。例如

:- module(hh_mm_bi,
         [hh_mm_bi/2
         ,hh_mm_bi//1
         ]).

:- use_module(library(dcg/basics)).
:- use_module(library(clpfd)).

hh_mm_bi(T,S) :- phrase(hh_mm_bi(T),S).

hh_mm_bi(time(H,M)) --> n2(H,23),":",n2(M,59).
n2(V,U)             --> d(A),d(B), {V#=A*10+B,V#>=0,V#=<U}.
d(V)                --> digit(D), {V#=D-0'0}.

一些测试

?- hh_mm_bi(T,`23:30`).
T = time(23, 30).

?- hh_mm_bi(T,`24:30`).
false.

?- phrase(hh_mm_bi(T),S).
T = time(0, 0),
S = [48, 48, 58, 48, 48] ;
T = time(0, 1),
S = [48, 48, 58, 48, 49] ;
...

编辑

library(clpfd) 不是我们对声明性算法的唯一选择。这是另一个镜头,使用 library(clpBNR),但它需要您安装适当的包,使用 ?- pack_install(clpBNR). 完成后,另一个功能上与上述解决方案等效的解决方案可能是

:- module(hh_mm_bnr,
         [hh_mm_bnr/2
         ,hh_mm_bnr//1
         ]).

:- use_module(library(dcg/basics)).
:- use_module(library(clpBNR)).

hh_mm_bnr(T,S) :- phrase(hh_mm_bnr(T),S).

hh_mm_bnr(time(H,M)) --> n2(H,23),":",n2(M,59).
n2(V,U)              --> d(A),d(B), {V::integer(0,U),{V==A*10+B}}.
d(V)                 --> digit(D), {{V==D-0'0}}.

编辑

@DavidTonhofer 的评论(现已删除)让我认为可以使用更简单的方法,将 'generation power' 移动到 d//1:


:- module(hh_mm,
         [hh_mm/2
         ,hh_mm//1
         ]).

hh_mm(T,S) :- phrase(hh_mm(T),S).

hh_mm(time(H,M)) --> n2(H,23),":",n2(M,59).
n2(V,U)          --> d(A),d(B), { V is A*10+B, V>=0, V=<U }.
d(V)             --> [C], { member(V,[0,1,2,3,4,5,6,7,8,9]), C is V+0'0 }.
time_string(time(H,M),String)
:-
hour(H) ,
minute(M) ,
number_string(H,Hs) ,
number_string(M,Ms) ,
string_concat(Hs,":",S) ,
string_concat(S,Ms,String)
.

hour(H) :- between(0,11,H) .

minute(M) :- between(0,59,M) .
/*
?- time_string(time(10,30),B).
B = "10:30".

?- time_string(time(H,M),"10:30").
H = 10,
M = 30 ;
false.

?- time_string(time(H,M),S).
H = M, M = 0,
S = "0:0" ;
H = 0,
M = 1,
S = "0:1" ;
H = 0,
M = 2,
S = "0:2" ;
H = 0,
M = 3,
S = "0:3" %etc.
*/

又一个答案,避免 DCG 对这个任务来说是矫枉过正。或者更确切地说,这里涉及两个独立的任务:不是每个关系都可以用一个 Prolog 谓词来表达,尤其是不是每个关系都像 extra-logical 作为 SWI-Prolog 的字符串。

所以这是其中一项任务的解决方案,从时间计算字符串(这是您的代码重命名):

time_string_(time(H,M), String) :-
    number_string(H,Hour),
    number_string(M,Min),
    string_concat(Hour,":",S),
    string_concat(S,Min,String).

例如:

?- time_string_(time(11, 59), String).
String = "11:59".

下面是反向转换的简单实现:

string_time_(String, time(H, M)) :-
    split_string(String, ":", "", [Hour, Minute]),
    number_string(H, Hour),
    number_string(M, Minute).

例如:

?- string_time_("11:59", Time).
Time = time(11, 59).

这里是一个谓词,它根据已知参数选择使用这些转换中的哪一个。确切的条件将取决于您的应用程序中可能发生的情况,但似乎可以合理地说,如果字符串确实是字符串,我们要尝试解析它:

time_string(Time, String) :-
    (   string(String)
    ->  % Try to parse the existing string.
        string_time_(String, Time)
    ;   % Hope that Time is a valid time term.
        time_string_(Time, String) ).

这将双向翻译:

?- time_string(time(11, 59), String).
String = "11:59".

?- time_string(Time, "11:59").
Time = time(11, 59).