1:- module(ap_validation,
    2          [ validate_scenario/2,
    3            clamp/4,
    4            safe_number/2,
    5            safe_atom/2,
    6            normalized_scenario/2
    7          ]).    8
    9:- use_module(library(apply)).   10:- use_module(ap_defaults).   11
   12clamp(Min, Max, X, Y) :-
   13    ( X < Min -> Y = Min
   14    ; X > Max -> Y = Max
   15    ; Y = X
   16    ).
   17
   18safe_number(X, N) :- number(X), !, N = X.
   19safe_number(X, N) :- atom(X), atom_number(X, N), !.
   20safe_number(X, N) :- string(X), number_string(N, X), !.
   21
   22safe_atom(X, A) :- atom(X), !, A = X.
   23safe_atom(X, A) :- string(X), !, atom_string(A, X).
   24
   25normalized_scenario(In, Out) :-
   26    ap_defaults:default_scenario(D),
   27    ( is_dict(In) -> put_dict(In, D, Merged) ; Merged = D ),
   28    normalize_numeric_fields(Merged, Out0),
   29    normalize_misc(Out0, Out).
   30
   31normalize_numeric_fields(S0, S) :-
   32    ap_defaults:scenario_numeric_keys(Keys),
   33    foldl(normalize_numeric, Keys, S0, S).
   34
   35normalize_numeric(Key, S0, S) :-
   36    get_dict(Key, S0, Raw),
   37    ( safe_number(Raw, N0) -> true ; N0 = 50 ),
   38    clamp(0, 100, N0, N),
   39    put_dict(Key, S0, N, S).
   40
   41normalize_misc(S0, S) :-
   42    normalize_duration(S0, S1),
   43    normalize_population(S1, S2),
   44    normalize_seed(S2, S3),
   45    normalize_category(scope, S3, S4),
   46    normalize_category(issue, S4, S5),
   47    normalize_category(target, S5, S6),
   48    normalize_category(notified, S6, S).
   49
   50normalize_duration(S0, S) :-
   51    get_dict(duration_days, S0, Raw),
   52    ( safe_number(Raw, N0) -> N1 is round(N0) ; N1 = 1 ),
   53    clamp(1, 365, N1, N),
   54    put_dict(duration_days, S0, N, S).
   55
   56normalize_population(S0, S) :-
   57    get_dict(population_millions, S0, Raw),
   58    ( safe_number(Raw, N0) -> true ; N0 = 2.0 ),
   59    clamp(0.01, 300.0, N0, N),
   60    put_dict(population_millions, S0, N, S).
   61
   62normalize_seed(S0, S) :-
   63    get_dict(seed, S0, Raw),
   64    ( safe_number(Raw, N0) -> N is round(N0) ; N = 1998 ),
   65    put_dict(seed, S0, N, S).
   66
   67normalize_category(Key, S0, S) :-
   68    get_dict(Key, S0, Raw),
   69    ( safe_atom(Raw, A) -> true ; A = unknown ),
   70    put_dict(Key, S0, A, S).
   71
   72validate_scenario(In, Errors) :-
   73    ( is_dict(In) -> E0 = [] ; E0 = ['Skenario harus berupa dict SWI-Prolog'] ),
   74    required_keys(Keys),
   75    findall(Msg,
   76            ( member(K, Keys),
   77              ( get_dict(K, In, _) -> fail
   78              ; format(string(Msg), 'Field wajib hilang: ~w', [K])
   79              )
   80            ), Missing),
   81    append(E0, Missing, Errors).
   82
   83required_keys([name, scope, province, city, issue, subissue, target])