This is an answer to this question posted on StackOverflow.
The answer is here because Discourse allows for better formatting and a proper discussion.
Note: The correct way to create a formatted tabular report with SWI-Prolog is to use format/2 with ~|
, ~t
, ~+
, but this answer is specific to the means the OP is seeking.
Repost of original question.
I have list of facts as follows.
items(itemId('P01'),prodName('Pots'),stockQty(50),price(8200)).
items(itemId('P02'),prodName('Pans'),stockQty(50),price(400)).
items(itemId('P03'),prodName('Spoons'),stockQty(50),price(200)).
items(itemId('P04'),prodName('Forks'),stockQty(50),price(120)).
items(itemId('P05'),prodName('Kettles'),stockQty(50),price(500)).
items(itemId('P06'),prodName('Plates'),stockQty(50),price(60)).
How to print on the console something like the following when a command like print_all_products.
is given.
β¦
Available Products
β¦
Name Qty
Pots 60
Pans 50
Spoons 40
β¦
- The Name and Qty must be properly formatted in a tabular structure.
I tried using forall
and foreach
I am unsuccessful in generating what I need.
Answer:
The OP asked for an example using predicates like forall/2 or foreach/2.
This could also be done using DCGs. For something similar using DCGs see this post.
Think of the output as a sequence of Prolog atoms concatenated, then the output would be like
'........................\nAvailable Products\n........................\nName Qty\nPots 60\nPans 50\nSpoons 40\n........................\n'
The head of the output would be
'\n........................\nAvailable Products\n........................\nName Qty\n'
The footer of the output be
'........................\n'
The detail of the output would be
'Pots 60\nPans 50\nSpoons 40\n'
Each detail line would be like
'Pots 60'
Being lines of text, you can also think of them as 'Pots 60'
followed by '\n'
.
Since a fact has more information than is needed, unification can be used to extract the needed term(s), e.g.
Item = items(_,prodName(Name),stockOty(Quantity),_).
?- items(_,prodName(Name),stockOty(Quantity),_).
Name = 'Pots',
Quantity = 50 ;
Name = 'Pans',
Quantity = 50 ;
Name = 'Spoons',
Quantity = 50 ;
Name = 'Forks',
Quantity = 50 ;
Name = 'Kettles',
Quantity = 50 ;
Name = 'Plates',
Quantity = 50.
Now that the code has access to the needed values for the detail line, predicates are needed to convert the detail values into a detail line.
A value will need a variable number of padding spaces on the right.
spaces(Length,Spaces) :-
length(List,Length),
maplist([_,0'\s]>>true,List,Codes),
string_codes(Spaces,Codes).
Example usage:
?- spaces(4,Spaces).
Spaces = " ".
To determine the max width of a field such as name
aggregate_all(max(Width),Width,(items(_,prodName(Name),_,_),string_length(Name,Width)),Max).
Example usage:
?- aggregate_all(max(Width),Width,(items(_,prodName(Name),_,_),string_length(Name,Width)),Max).
Max = 7.
For the next predicate, something not commonly intuitive is used. In SWI-Prolog, strings are also atomic so goals like this work.
?- atomic("abcd").
true.
?- atomic('abcd').
true.
?- string_length('abcd',Length).
Length = 4.
?- string_length("abcd",Length).
Length = 4.
A predicate is needed to take a value and a field width and right pad the value with spaces.
padded_string(String,Width,Padded_string) :-
string_length(String,String_length),
Padding_length is Width - String_length,
spaces(Padding_length,Padding),
atom_concat(String,Padding,Padded_string)
Example run:
?- padded_string('Pots',8,Padded_string).
Padded_string = 'Pots '
A predicate is needed to take an item and convert it into a line.
format_detail_line(item(Name,Quantity),width(Name_width),Formatted_item) :-
padded_string(Name,Name_width,Padded_name),
atom_concat(Padded_name,Quantity,Formatted_item).
Example run:
?- format_detail_line(item('Pots',60),width(8),Formatted_item).
Formatted_item = 'Pots 60'.
To build all of the detail lines foldl/4 is used. foldl/4 needs a goal.
add_detail_line(width(Name_Width),Item,Lines0,Lines) :-
format_detail_line(Item,width(Name_Width),Formatted_item),
atomic_list_concat([Lines0,Formatted_item,"\n"], Lines).
Example runs:
?- add_detail_line(width(8),item('Pots',60),"",Lines).
Lines = 'Pots 60\n'.
?- add_detail_line(width(8),item('Pans',50),'Pots 60\n',Lines).
Lines = 'Pots 60\nPans 50\n'.
The predicate that converts all the items in the detail using foldl/4. Remember the detail is just a concatenated sequence of atomic values.
items_detail(Detail) :-
findall(item(Name,Quantity),items(_,prodName(Name),stockOty(Quantity),_),Items),
aggregate_all(max(Width),Width,(items(_,prodName(Name),_,_),string_length(Name,Width)),Name_Width),
Name_field_width is Name_Width + 1,
foldl(add_detail_line(width(Name_field_width)),Items,"",Detail).
Example run:
?- items_detail(Detail).
Detail = 'Pots 50\nPans 50\nSpoons 50\nForks 50\nKettles 50\nPlates 50\n'.
The predicate to generate the report.
print_all_products(Report) :-
header(Header),
items_detail(Detail),
footer(Footer),
atomic_list_concat([Header,Detail,Footer], Report).
Now for a proper test case.
:- begin_tests(formatted_report).
test(1) :-
print_all_products(Report),
with_output_to(atom(Atom),write(Report)),
assertion( Atom == '\n........................\nAvailable Products\n........................\nName Qty\nPots 50\nPans 50\nSpoons 50\nForks 50\nKettles 50\nPlates 50\n........................\n' ).
:- end_tests(formatted_report).
Example run:
?- run_tests.
% PL-Unit: formatted_report . done
% test passed
true.
To print just the report.
print_all_products :-
print_all_products(Report),
write(Report).
Example run:
?- print_all_products.
........................
Available Products
........................
Name Qty
Pots 50
Pans 50
Spoons 50
Forks 50
Kettles 50
Plates 50
........................
true.
All the code.
items(itemId('P01'),prodName('Pots'),stockOty(50),price(8200)).
items(itemId('P02'),prodName('Pans'),stockOty(50),price(400)).
items(itemId('P03'),prodName('Spoons'),stockOty(50),price(200)).
items(itemId('P04'),prodName('Forks'),stockOty(50),price(120)).
items(itemId('P05'),prodName('Kettles'),stockOty(50),price(500)).
items(itemId('P06'),prodName('Plates'),stockOty(50),price(60)).
header("\n........................\nAvailable Products\n........................\nName Qty\n").
footer("........................\n").
spaces(Length,Spaces) :-
length(List,Length),
maplist([_,0'\s]>>true,List,Codes),
string_codes(Spaces,Codes).
padded_string(String,Width,Padded_string) :-
string_length(String,String_length),
Padding_length is Width - String_length,
spaces(Padding_length,Padding),
atom_concat(String,Padding,Padded_string).
format_detail_line(item(Name,Quantity),width(Name_width),Formatted_item) :-
padded_string(Name,Name_width,Padded_name),
atom_concat(Padded_name,Quantity,Formatted_item).
add_detail_line(width(Name_Width),Item,Lines0,Lines) :-
format_detail_line(Item,width(Name_Width),Formatted_item),
atomic_list_concat([Lines0,Formatted_item,"\n"], Lines).
items_detail(Detail) :-
findall(item(Name,Quantity),items(_,prodName(Name),stockOty(Quantity),_),Items),
aggregate_all(max(Width),Width,(items(_,prodName(Name),_,_),string_length(Name,Width)),Name_Width),
Name_field_width is Name_Width + 1,
foldl(add_detail_line(width(Name_field_width)),Items,"",Detail).
print_all_products(Report) :-
header(Header),
items_detail(Detail),
footer(Footer),
atomic_list_concat([Header,Detail,Footer], Report).
print_all_products :-
print_all_products(Report),
write(Report).
:- begin_tests(formatted_report).
test(1) :-
print_all_products(Report),
with_output_to(atom(Atom),write(Report)),
assertion( Atom == '\n........................\nAvailable Products\n........................\nName Qty\nPots 50\nPans 50\nSpoons 50\nForks 50\nKettles 50\nPlates 50\n........................\n' ).
:- end_tests(formatted_report).
HTH