/* ---- This file is part of SECONDO. Copyright (C) 2004-2008, University in Hagen, Faculty of Mathematics and Computer Science, Database Systems for New Applications. SECONDO is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. SECONDO is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with SECONDO; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ---- 1 Auxiliary Predicates This file contains the pretty-printing predicate ~pretty\_print~ and various auxiliary predicates for ~pretty\_print~ and a ~secondo~ predicate which uses just one argument (the command) and pretty-prints the result. 1.1 Predicate ~pretty\_print~ Predicate ~pretty\_print~ prints a list L which is assumed to be a PROLOG representation of a Secondo nested list. That is the case e.g. if L is output by the ~secondo~ predicate. If L is a relation, a special output format is used which makes reading the output more comfortable. That output format closely resembles the output format used by SecondoTTY. 1.1.1 Predicates Auxiliary to Predicate ~pretty\_print~ */ is_atomic_list([]). is_atomic_list([Head | Tail]) :- atomic(Head), is_atomic_list(Tail). write_spaces(0). write_spaces(N) :- N > 0, write(' '), N1 is N - 1, write_spaces(N1). write_tabs(N) :- N1 is 2 * N , write_spaces(N1). write_atoms([X]) :- !, write(X). write_atoms([X | Rest]) :- write(X), write(', '), write_atoms(Rest). write_element(X, N) :- atomic(X), write_tabs(N), write(X). write_element(X, N) :- is_atomic_list(X), !, write_tabs(N), write('['), write_atoms(X), write(']'). write_element(X, N) :- is_list(X), N1 is N + 1, write_tabs(N), write('['), nl, write_elements(X, N1), write(']'). write_elements([], _). write_elements([X], N) :- !, write_element(X, N). write_elements([X | L], N) :- write_element(X, N), write(','), nl, write_elements(L, N). max_attr_length([], 0). max_attr_length([[Name, _] | AttrDescription], M) :- max_attr_length(AttrDescription, M1), atom_length(Name, M2), M is max(M1, M2). write_tuple([], [], _). write_tuple([[Name, _] | RestOfAttr], [AttrValue | RestOfValues], M) :- write(Name), atom_length(Name, NLength), PadLength is M - NLength, write_spaces(PadLength), write(' : '), write(AttrValue), nl, write_tuple(RestOfAttr, RestOfValues, M). write_tuples(_, [], _). write_tuples(AttrDescription, [Tuple], M) :- !, write_tuple(AttrDescription, Tuple, M). write_tuples(AttrDescription, [Tuple | TupleList], M) :- write_tuple(AttrDescription, Tuple, M), nl, write_tuples(AttrDescription, TupleList, M). /* 1.1.2 Predicate ~pretty\_print~ */ pretty_print([[RelType, [tuple, AttrDescription]], Tuples]) :- (RelType = rel ; RelType = trel), !, nl, max_attr_length(AttrDescription, AttrLength), write_tuples(AttrDescription, Tuples, AttrLength). % NVK ADDED NR: Support for nested relations. pretty_print([[nrel, [tuple, AttrDescription]], Tuples]) :- nr_pretty_print([[nrel, [tuple, AttrDescription]], Tuples]), !. % NVK ADDED NR END pretty_print(L) :- write_element(L, 0). /* 1.1.2 Predicate ~show~ */ show([Type, Value]) :- !, display(Type, Value). show(Y) :- write(Y), pretty_print(Y), nl. /* 1.1.3 Predicate ~display~ ---- display(Type, Value) :- ---- Display the value according to its type description. To be extended when new type constructors are added to Secondo. */ % Section:Start:display_2_b % Section:End:display_2_b display(int, N) :- !, write(N). display(real, N) :- !, write(N). display(bool, N) :- !, write(N). display(string, N) :- !, term_to_atom(String, N), displayString(String). display(date, N) :- !, term_to_atom(String, N), displayString(String). display(instant, N) :- !, term_to_atom(String, N), displayString(String). display(text, N) :- !, write_elements([N], 0). display(rect, [L, R, B, T]) :- !, write('rectangle xl = '), write(L), write(', xr = '), write(R), write(', yb = '), write(B), write(', yt = '), write(T). display([Rel, [tuple, Attrs]], Tuples) :- (Rel = rel ; Rel = trel), !, nl, max_attr_length(Attrs, AttrLength), displayTuples(Attrs, Tuples, AttrLength). % NVK ADDED NR: Support for nested relations. display([Rel, [tuple, Attrs]], Tuples) :- nr_display([Rel, [tuple, Attrs]], Tuples), !. % Just write the terms to stdout to avoid the 'There is no specific display' % message. display(upoint, UPoint) :- write_term(UPoint, []), !. display(mpoint, MPoint) :- write_element(MPoint, 0), !. % NVK ADDED END display(duration, [0, MSec]) :- MSec > 3600000, !, Hour is round(float_integer_part(MSec / 3600000.0)), write(Hour), write('h '), Rest is MSec - (Hour * 3600000), display(duration, [0, Rest]). display(duration, [0, MSec]) :- MSec > 60000, !, Min is round(float_integer_part(MSec / 60000.0)), write(Min), write('min '), Rest is MSec - (Min * 60000), display(duration, [0, Rest]). display(duration, [0, MSec]) :- MSec > 1000, !, Sec is round(float_integer_part(MSec / 1000.0)), write(Sec), write('s '), Rest is MSec - (Sec * 1000), display(duration, [0, Rest]). display(duration, [0, MSec]) :- !, MS is round(MSec), write(MS), write('ms'). display(duration, [Days, MSec]) :- !, write(Days), write('d '), display(duration, [0, MSec]). display(Type, Value) :- write('There is no specific display function for type '), write(Type), write('. '), nl, write('Generic display used. '), nl, pretty_print(Value), nl. displayString([]). displayString([Char | Rest]) :- put(Char), displayString(Rest). displayTuples(_, [], _). displayTuples(Attrs, [Tuple | Rest], AttrLength) :- displayTuple(Attrs, Tuple, AttrLength), nl, displayTuples(Attrs, Rest, AttrLength). displayTuple([], _, _). displayTuple([[Name, Type] | Attrs], [Value | Values], AttrNameLength) :- atom_length(Name, NLength), PadLength is AttrNameLength - NLength, write_spaces(PadLength), write(Name), write(' : '), display(Type, Value), nl, displayTuple(Attrs, Values, AttrNameLength). /* 1.2 Predicate ~secondo~ Predicate ~secondo~ expects its argument to be a string atom or a nested list, representing a query to the SECONDO system. The query is executed and the result pretty-printed. If the query fails, the error code and error message are printed. ---- secondo(+SecondoExecutableExpression) ---- Send ~SecondoExecutableExpression~ to the Secondo kernel (using secondo/2) and print the returned result onto the screen. Keep the optimizer informed by tracking 'open', 'close', 'restore', 'let' and 'delete' commands within ~SecondoExecutableExpression~. ---- secondo_direct(+SecondoExecutableExpression) ---- Send ~SecondoExecutableExpression~ to the Secondo kernel (using secondo/2) and print the returned result onto the screen. Do NOT keep the optimizer informed by tracking 'open', 'close', 'rstore', 'let' or 'delete' commands within ~SecondoExecutableExpression~. This variant is useful within automaically triggered queries to secondo, e.g. deleting or creating objects used by the optimizer (like samples, indexes, small objects). This command should never be used by the user! */ % Such facts are used to mark a file has been considered by some predicate: :- dynamic(tempConsideredFile/1). % atom_postfix(+Atom, +PrefixLength, ?Post) % succeeds iff Post is a postfix of Atom starting after PrefixLength atom_postfix(Atom, PrefixLength, Post) :- atom_length(Atom, Length), PostLength is Length - PrefixLength, sub_atom(Atom, PrefixLength, PostLength, 0, Post). /* Some facts which store important state information ---- databaseName(X) ---- When a database X gets openend, a fact databaseName(X) gets asserted. If no such fact exists, no database is opened. The fact is used to get the name of the currently opened database. */ :- assert(helpLine(secondo,1, [[+,'CommandString', 'Command to send as a string (enclose in single quotes).']], 'Send a command to the DBMS kernel.')). :- dynamic databaseName/1. promptSecondoResultSucceeded(Result) :- write('Command succeeded, result:'), nl, nl, show(Result), !. promptSecondoResultFailed :- secondo_error_info(ErrorCode, ErrorString), write('Command failed with error code : '), write(ErrorCode), nl, write('and error message : '), nl, write(ErrorString), nl, !. secondo(X) :- sub_atom(X,0,_,_,'open '), atom_postfix(X, 14, DB1), downcase_atom(DB1, DB), !, ( secondo(X, Y) *-> ( promptSecondoResultSucceeded(Y), !, retractall(databaseName(_)), assert(databaseName(DB)), readSystemIdentifiers, readSecondoTypeSizes, refreshSecondoCatalogInfo, checkObjectNamingConvention, updateCatalog, updateCatalog % ,ensureSmallObjectsExist ) ; ( promptSecondoResultFailed, fail ) ), !. secondo(X) :- sub_atom(X,0,_,_,'close'), !, ( secondo(X, Y) *-> ( promptSecondoResultSucceeded(Y), retractall(databaseName(_)) ) ; ( promptSecondoResultFailed, fail ) ), !. secondo(X) :- sub_atom(X,0,6,_,'update '), isDatabaseOpen, !, ( secondo(X, Y) *-> promptSecondoResultSucceeded(Y) ; ( promptSecondoResultFailed, fail ) ), !. secondo(X) :- member(Command,['let ', 'derive ', 'delete ']), sub_atom(X,0,CommandLength,_,Command), ( notIsDatabaseOpen -> ( write('\nCannot execute command, because no database is open.\n'), !, fail ) ; true ), ( member(Command,['let ', 'derive ']) % if this is a let/derive command, -> ( % we need to test, whether the (downcased) objectname is not % already used in the DB and the name is a valid itentifier sub_atom(X,PosEq,1,_,'='), Namelength is PosEq - CommandLength, !, sub_atom(X,CommandLength,Namelength,_,ExtObjNameA), removeWhitespace(ExtObjNameA,ExtObjName), dcName2externalName(DCobjName,ExtObjName), ( secondoCatalogInfo(DCobjName,ExtOther,_,_) % already used? -> ( write_list(['\nERROR:\tCannot create object \'', ExtObjName,'\'.\n','--->\tThere already ', 'exits an object named \'',ExtOther,'\'!\n']), nl, !, fail ) ; ( validIdentifier(ExtObjName) % identifier valid? -> true ; ( write_list(['\nERROR:\tCannot create object \'', ExtObjName,'\'.\n', '--->\tInvalid identifier!\n']), nl, !, fail ) ) ) ) ; true % if this is a delete command ), ( secondo(X, Y) *-> ( promptSecondoResultSucceeded(Y), updateCatalog, updateCatalog % ,ensureSmallObjectsExist % ,ensureSamplesExist ) ; ( promptSecondoResultFailed, fail ) ), !. secondo(X) :- sub_atom(X,0,_,_,'create '), not(my_concat_atom([create, database, _], ' ', X)), isDatabaseOpen, ( secondo(X, Y) *-> promptSecondoResultSucceeded(Y) ; ( promptSecondoResultFailed, fail ) ), !. % restore database % One should consider wiping out the knowledge base on the target database! secondo(X) :- my_concat_atom([restore, database, DB1, from, _], ' ', X), ( notIsDatabaseOpen -> ( downcase_atom(DB1, DB), !, ( secondo(X, Y) *-> ( promptSecondoResultSucceeded(Y), retractall(databaseName(_)), assert(databaseName(DB)), readSystemIdentifiers ) ; ( promptSecondoResultFailed, fail ) ) ) ; write('\nERROR:\tCannot restore database, because a database is open.\n') ), updateCatalog, updateCatalog, % ensureSmallObjectsExist, !. % restore object secondo(X) :- my_concat_atom([restore, _, from, _], ' ', X), ( isDatabaseOpen -> write('\nERROR:\tCannot restore object, because no database is open.\n') ; ( secondo(X, Y) *-> ( promptSecondoResultSucceeded(Y) ) ; ( promptSecondoResultFailed, fail ) ) ), updateCatalog, updateCatalog, % ensureSmallObjectsExist, !. secondo(X) :- my_concat_atom([list, objects],' ',X), isDatabaseOpen, secondo(X, Y), promptSecondoResultSucceeded(Y), !. secondo(X) :- sub_atom(X,0,_,_,'query'), isDatabaseOpen, !, ( secondo(X, Y) *-> ( promptSecondoResultSucceeded(Y), ! ) ; ( promptSecondoResultFailed, fail ) ), !. % fallback-case (all other commands) secondo(X) :- ( secondo(X, Y) *-> ( promptSecondoResultSucceeded(Y), !) ; ( promptSecondoResultFailed, fail ) ), !. % Variant of execulting a command on the Secondo kernel % without updating the internal optimizer knowledge base. % Not to be used by the user! secondo_direct(X) :- ( secondo(X, Y) *-> ( promptSecondoResultSucceeded(Y), !) ; ( promptSecondoResultFailed, fail ) ), !. /* 1.4 Operators ~query~, ~update~, ~let~, ~create~, ~open~, ~restore~ and ~delete~ The purpose of these operators is to make using the PROLOG interface similar to using SecondoTTY. A SecondoTTY query ---- query ten ---- can be issued as ---- query 'ten'. ---- in the PROLOG interface via the ~query~ operator. The operators ~delete~, ~let~, ~create~, ~open~, and ~update~ work the same way. */ :- assert(helpLine(let,1, [[+,'ExecQuery','Partial command in Secondo syntax.']], 'Prepend \'query \' and send to DBMS-kernel.')). :- assert(helpLine(query,1, [[+,'ExecQuery','Partial command in Secondo syntax.']], 'Prepend \'query \' and send to DBMS-kernel.')). :- assert(helpLine(derive,1, [[+,'ExecQuery','Partial command in Secondo syntax.']], 'Prepend \'derive \' and send to DBMS-kernel.')). :- assert(helpLine(create,1, [[+,'ExecQuery','Partial command in Secondo syntax.']], 'Prepend \'create \' and send to DBMS-kernel.')). :- assert(helpLine(update,1, [[+,'ExecQuery','Partial command in Secondo syntax.']], 'Prepend \'update \' and send to DBMS-kernel.')). :- assert(helpLine(delete,1, [[+,'ExecQuery','Partial command in Secondo syntax.']], 'Prepend \'delete \' and send to DBMS-kernel.')). :- assert(helpLine(open,1, [[+,'ExecQuery','Partial command in Secondo syntax.']], 'Prepend \'open \' and send to DBMS-kernel.')). :- assert(helpLine(restore,1, [[+,'ExecQuery','Partial command in Secondo syntax.']], 'Prepend \'restore \' and send to DBMS-kernel.')). :- op(993, fx, open). :- op(993, fx, restore). :- op(959, fx, database). %:- op(960, xfx, from). % already defined in file optimizer.pl :- op(993, fx, query). :- op(993, fx, delete). :- op(993, fx, let). :- op(993, fx, create). :- op(993, fx, derive). :- op(993, fx, update). isDatabaseOpen :- ( databaseName(_) -> true ; ( write('No database open.'), nl, fail) ), !. notIsDatabaseOpen :- not(databaseName(_)), !. query(Query) :- query(Query, _). query(Query, Time) :- isDatabaseOpen, atom(Query), atom_concat('query ', Query, QueryText), !, getTime( secondo(QueryText), Time). let(Query) :- isDatabaseOpen, atom(Query), my_concat_atom(['let', Query], ' ', QueryText), !, secondo(QueryText). derive(Query) :- isDatabaseOpen, atom(Query), atom_concat('derive ', Query, QueryText), !, secondo(QueryText). create(Query) :- atom(Query), atom_concat('create ', Query, QueryText), !, secondo(QueryText). update(Query) :- isDatabaseOpen, atom(Query), atom_concat('update ', Query, QueryText), !, secondo(QueryText). delete(Query) :- atom(Query), atom_concat('delete ', Query, QueryText), !, secondo(QueryText). % more comfortable (without quoting) open(Query) :- notIsDatabaseOpen, Query =.. [database, DB], atom(DB), atom_concat('open database ', DB, QueryText), !, secondo(QueryText), dropTempRels. % the hard way... open(Query) :- notIsDatabaseOpen, atom(Query), atom_concat('open ', Query, QueryText), !, secondo(QueryText), dropTempRels. % the hard way... restore(Query) :- notIsDatabaseOpen, atom(Query), atom_concat('restore ', Query, QueryText), !, secondo(QueryText). % more comfortable (without quoting) restore(Query) :- Query =.. [from,Obj,Source], ( ( atom(Source), Obj =.. [database, DB], atom(DB) ) % restore database -> ( notIsDatabaseOpen -> my_concat_atom(['restore database',DB,'from',Source],' ',QueryText) ; ( write_list(['\nERROR:\tCannot restore a database, \n', '--->\twhile a database is open!']), nl, !, fail ) ) ; ( ( atom(Obj), term_to_atom(Source, SourceA) ) % restore object -> ( notIsDatabaseOpen -> (write_list(['\nERROR:\tCannot restore object.\n', '--->\tNo database open.']), nl, !, fail) ; my_concat_atom(['restore ',Obj,'from',SourceA],' ',QueryText) ) ; ( write_list(['\nERROR:\tInvalid restore command.\n', '\tThe correct syntax is \'restore from .\'', '\n\tor \'restore database from .\'.']), nl, !, fail ) ) ), secondo(QueryText). /* The keywords open and close are already assigned with operations on streams. Hence we provide ~openDB~ and closeDB to close a database. */ :-assert(helpLine(cdb,0,[],'Close current database.')). :-assert(helpLine(closeDB,0,[],'Close current database.')). cdb :- closeDB. closedb :- closeDB. closeDB :- secondo('close database'). odb(Name) :- openDB(Name). openDB(Name) :- atom_concat('open database ', Name, Cmd), secondo(Cmd), dropTempRels. :-assert(helpLine(ldb,0,[],'List available databases.')). :-assert(helpLine(listDB,0,[],'List available databases.')). ldb :- listDB. listDB :- secondo('list databases'). :-assert(helpLine(lo,0,[],'Send \'list objects\'.')). :-assert(helpLine(listObj,0,[],'Send \'list objects\'.')). lo :- listObj. listObj :- secondo('list objects'). % search for a description of operator O :- assert(helpLine(findop,1, [[+,'OperatorName','The name of the operator to query for.']], 'Show help on a given Secondo operator.')). findop(O) :- my_concat_atom([ 'query SEC2OPERATORINFO feed filter[.Name contains "', O, '"] consume' ], Q), runQuery(Q). % dump the command history to a given file name :- assert(helpLine(cmdHist2File,1, [[+,'FileName','The file used for storing the history.']], 'Dump the command history to a file.')). cmdHist2File(Name) :- my_concat_atom(['query SEC2COMMANDS feed '], Q), dumpQueryResult2File(Q, Name, Q2), runQuery(Q2). % dump the result of a secondo query to a CSV file dumpQueryResult2File(Q, File, Q2) :- my_concat_atom([Q, ' dumpstream["', File, '","|"] tconsume'], Q2). % removing whitespace (tab, space) from an atomic removeWhitespace(ExtObjNameA,ExtObjName) :- atom_chars(ExtObjNameA,CharListDirty), delete(CharListDirty,' ',CharListDirty1), delete(CharListDirty1,'\t',CharListCleaned), atom_chars(ExtObjName,CharListCleaned), !. /* Useful for debugging */ showValue(Name, Var) :- write(Name), write(': '), write(Var), nl, nl. mark(X) :- write('(* Mark '), write(X), write(' *)'), nl. /* 2.1 Generic display for printing formatted tables Sometimes we want to collect some information and to print it as a table, e.g. predicate ~writeSizes/0~. Below there are some predicates which display lists of well defined tuples as specified by a header list. If the data is presented in a prolog list as [t1, t2, ... tn] and ti are itself prolog lists of fixed length each value will be printed in a separate column. Example application: ---- findall(X, createMyTuples(_, X), L ), TupFormat = [ ['Size', 'c'], ['Time', 'l'], ['Error', 'l'] ], showTuples(L, TupFormat). ---- */ showTuples(L, TupFormat) :- nl, nl, showHeader(TupFormat, WriteSpec), showTuplesRec(L, WriteSpec). showTuplesRec([], _). showTuplesRec([H|T], WriteSpec) :- showTupleRec(H, WriteSpec), nl, %format('~w~t~+~w~t~+~w~t~+~w~n',H), showTuplesRec(T, WriteSpec). showTupleRec([], _). showTupleRec([H|T], [Wh|Wt]) :- %showValue('Wh', Wh), writef(Wh, [H]), %format('~w~t ~+~w~t ~+~w~t~ +~w',[H]), showTupleRec(T, Wt). showHeader(L, WriteSpec) :- showHeaderRec(L, [], HeadList, [], WriteSpec, 0, Len), showTuplesRec([HeadList], WriteSpec), Len2 is Len + 2, writef('%r', ['-', Len2]), nl. showHeaderRec([], L1, L1, L2, L2, N, N). showHeaderRec([H|T], Tmp1, Res1, Tmp2, Res2, Tmp3, Res3 ) :- H = [Attr, Adjust], atom_length(Attr, Len), FieldLen is Len + 4, TotalLen is Tmp3 + FieldLen, my_concat_atom([' %', FieldLen, Adjust], WriteSpec), append(Tmp1, [Attr], L1), append(Tmp2, [WriteSpec], L2), showHeaderRec(T, L1, Res1, L2, Res2, TotalLen, Res3 ). /* Write a list of tuples to a file. The members of an inner list are separated by a comma. */ writeElem(FD, []) :- write(FD, '\n'). writeElem(FD, [H | T]) :- write(FD, H), ( length(T, 0) -> write(FD, ' ') ; write(FD, ', ') ), writeElem(FD, T). dumpTuples2File(Name, T, Format) :- project(1, Format, Fp), append([Fp], T, L), showValue('L:', L), open(Name, write, FD), checklist( writeElem(FD), L), close(FD). /* Extract the N-th element out of all tuples given in list L and unify them with R. */ project(N, L, R) :- maplist(nth1(N), L, R). /* A predicate which translates the first letter of an atom to lower case. */ downcase_first(Atom, Res) :- atom_chars(Atom, [H | T]), downcase_atom(H, Hdown), append([Hdown], T, L), name(Res, L). /* 2.3 ~subList/3~ ---- subList(+L, +N, ?Res) ---- Unifies the first ~N~ elements of List ~L~ with ~Res~. */ subList(L, N, Res) :- subListRec(L, N, 1, Res). subListRec([H|_], N, N, [H]). subListRec([H|T], N, Pos, [H|T2]) :- NewPos is Pos + 1, subListRec(T, N, NewPos, T2). /* 2.4 Runtime Flags the dynamic predicate ~flag~ a tool for setting and querying options. This is useful to set global options. */ :- dynamic flag/2, clearflags. clearflags :- retractall( flag(_,_) ). setflag(F) :- assert( flag(F, on) ). clearflag(F) :- retractall( flag(F, _) ). showflag( [F, Val], _ ) :- nl, write(F), write(' --> '), write(Val), nl. showflags :- findall( [X, Y], flag(X, Y), L), maplist( showflag, L, _). /* 2.5 Running Queries The clause ~runQuery~ can be used to execute or just print queries depending on the flag ~printQueryOnlyMode~. */ showQuery(Q) :- nl, write(Q), nl. runQuery(Q) :- ( flag(printQueryOnlyMode, on) -> ( showQuery(Q) ) ; ( nl, write('Executing '), write(Q), write(' ...'), nl, secondo(Q) ) ), !. runQuery(Q, Res) :- ( flag(printQueryOnlyMode, on) -> ( showQuery(Q), Res = 999, nl, write('Dummy-Result: '), write(Res), nl ) ; ( nl, write('Executing '), write(Q), write(' ...'), nl, secondo(Q, [_, Res]), nl, write('Result: '), write(Res), nl ) ), !. /* 3.0 Stop Watch Predicate ---- time(+Clause) ---- This predicate stops the time required to answer query ~Clause~ and prints the needed time on screen. */ time(Clause) :- get_time(Time1), (call(Clause) ; true), get_time(Time2), Time is Time2 - Time1, my_convert_time(Time, _, _, _, _, Minute, Sec, MilliSec), MSs is Minute *60000 + Sec*1000 + MilliSec, write('Elapsed Time: '), write(MSs), write(' ms'),nl. /* End of file ~auxiliary.pl~ */