Skip to content

Commit e1b7fcb

Browse files
Add StringFormatted, PrintFormatted, PrintToFormatted.
This PR adds a simple method of formatting strings, strongly inspired by Python's "format" function. These 3 functions mirror Sting, Print and PrintTo.
1 parent 9d0b6fc commit e1b7fcb

4 files changed

Lines changed: 239 additions & 3 deletions

File tree

doc/ref/string.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,6 +515,7 @@ gap> s;
515515
<#Include Label="JoinStringsWithSeparator">
516516
<#Include Label="Chomp">
517517
<#Include Label="StartsWith">
518+
<#Include Label="StringFormatted">
518519

519520
The following two functions convert basic strings to lists of numbers and
520521
vice versa. They are useful for examples of text encryption.

lib/string.gd

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,73 @@ BindGlobal("BHINT", MakeImmutable("\>\<"));
860860

861861
DeclareGlobalFunction("StringOfMemoryAmount");
862862

863+
#############################################################################
864+
##
865+
## <#GAPDoc Label="StringFormatted">
866+
## <ManSection>
867+
## <Func Name="StringFormatted" Arg='string, data...'/>
868+
## <Func Name="PrintFormatted" Arg='string, data...'/>
869+
## <Func Name="PrintToFormatted" Arg='stream, string, data...'/>
870+
##
871+
## <Description>
872+
## These functions format an input string.
873+
## <Ref Func="StringFormatted"/> returns the formatted string,
874+
## <Ref Func="PrintFormatted"/> prints the formatted string and
875+
## <Ref Func="PrintToFormatted"/> appends the formatted string to <A>stream</A>,
876+
## which can be either an output stream or a filename.
877+
## <P/>
878+
## <A>data</A> is a list of values to printed, using the formatting rules below:
879+
## <P/>
880+
## <A>string</A> is treated as a normal string, except for occurrences
881+
## of <C>{</C> and <C>}</C>, which follow special rules, as follows:
882+
##
883+
## The contents of <C>{ }</C> form a small language. The format is <C>{id!format}</C>,
884+
## where both <C>id</C> and <C>format</C> are optional. If the <C>!</C> is ommitted, the bracket
885+
## is treated as <C>{id}</C> with no <C>format</C>.
886+
## <P/>
887+
## <C>id</C> is interpreted as follows:
888+
## <List>
889+
## <Mark>An integer <C>i</C></Mark> <Item>
890+
## Take the <C>i</C>th element of <A>data</A>.
891+
## </Item>
892+
## <Mark>No variable</Mark> <Item>
893+
## Take the <C>j</C>th element of <A>data</A>, where <C>j</C> is the
894+
## number of occurrences of <C>{}</C> in the string, including this one,
895+
## up to this point.
896+
## </Item>
897+
## <Mark>A string <C>str</C></Mark> <Item>
898+
## Take the <C>str</C> member of the first element of <A>data</A>, which
899+
## must be a record.
900+
## </Item>
901+
## </List>
902+
##
903+
## The <C>format</C> decides how the variable is printed. <C>format</C> must be one
904+
## of <C>s</C> (which uses <Ref Oper="String"/>), <C>v</C> (which uses
905+
## <Ref Oper="ViewString"/>) or <C>d</C> (which calls <Ref Oper="DisplayString"/>).
906+
## The default value for <C>format</C> is <C>s</C>.
907+
## </Description>
908+
## </ManSection>
909+
## <Example><![CDATA[
910+
## gap> StringFormatted("I have {} cats and {} dogs", 4, 5);
911+
## "I have 4 cats and 5 dogs"
912+
## gap> StringFormatted("I have {2} cats and {1} dogs", 4, 5);
913+
## "I have 5 cats and 4 dogs"
914+
## gap> StringFormatted("I have {cats} cats and {dogs} dogs", rec(cats:=3, dogs:=2));
915+
## "I have 3 cats and 2 dogs"
916+
## gap> StringFormatted("We use {{ and }} to mark {dogs} dogs", rec(cats:=3, dogs:=2));
917+
## "We use { and } to mark 2 dogs"
918+
## gap> sym3 := SymmetricGroup(3);;
919+
## gap> StringFormatted("String: {1!s}, ViewString: {1!v}", sym3);
920+
## "String: SymmetricGroup( [ 1 .. 3 ] ), ViewString: Sym( [ 1 .. 3 ] )"
921+
## ]]></Example>
922+
## <#/GAPDoc>
923+
924+
925+
DeclareGlobalFunction("StringFormatted");
926+
DeclareGlobalFunction("PrintFormatted");
927+
DeclareGlobalFunction("PrintToFormatted");
928+
929+
863930

864931
#############################################################################
865932
##

lib/string.gi

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,7 +1041,7 @@ InstallGlobalFunction(PrintCSV,function(arg)
10411041
end);
10421042

10431043

1044-
# Format commands
1044+
# StringFormatted commands
10451045
# RLC: alignment
10461046
# M: Math mode
10471047
# MN: Math mode but names, characters are put into mbox
@@ -1258,9 +1258,121 @@ InstallGlobalFunction(StringOfMemoryAmount, function(m)
12581258
Append(s, units[shift+1]);
12591259
return s;
12601260
end);
1261-
1262-
12631261

1262+
InstallGlobalFunction(PrintToFormatted, function(stream, s, args...)
1263+
local pos, len, outstr, nextbrace, endbrace,
1264+
argcounter, var,
1265+
splitBracket, toprint;
1266+
1267+
splitBracket := function(string, startpos, endpos)
1268+
local posbang, type;
1269+
posbang := Position(s, '!', startpos-1);
1270+
# Print(startpos, ":",endpos, ":",posbang,"\n");
1271+
if posbang = fail or posbang > endpos then
1272+
posbang := endpos + 1;
1273+
fi;
1274+
type := string{[posbang + 1 .. endpos]};
1275+
if type = "" then
1276+
type := "s";
1277+
fi;
1278+
return rec(id := string{[startpos..posbang-1]}, type := type);
1279+
end;
1280+
1281+
argcounter := 1;
1282+
len := Length(s);
1283+
pos := 0;
1284+
1285+
if not (IsOutputStream(stream) or IsString(stream)) or not IsString(s) then
1286+
ErrorNoReturn("Usage: PrintToFormatted(<stream>, <string>, <args>...)");
1287+
fi;
1288+
1289+
while pos < len do
1290+
nextbrace := Position(s, '{', pos);
1291+
endbrace := Position(s, '}', pos);
1292+
# Keep checking for '}}' until we find an '{'
1293+
while endbrace <> fail and endbrace < nextbrace do
1294+
if endbrace + 1 <= len and s[endbrace + 1] = '}' then
1295+
AppendTo(stream, s{[pos+1..endbrace]});
1296+
pos := endbrace + 1;
1297+
endbrace := Position(s, '}', pos);
1298+
else
1299+
ErrorNoReturn("Mismatched '}' at position ",endbrace);
1300+
fi;
1301+
od;
1302+
1303+
if nextbrace = fail then
1304+
AppendTo(stream, s{[pos+1..len]});
1305+
return;
1306+
fi;
1307+
1308+
AppendTo(stream, s{[pos+1..nextbrace-1]});
1309+
1310+
# If this is {{, then print a { and call 'continue'
1311+
if nextbrace+1 <= len and s[nextbrace+1] = '{' then
1312+
AppendTo(stream, "{");
1313+
pos := nextbrace + 1;
1314+
continue;
1315+
fi;
1316+
1317+
if endbrace = fail then
1318+
ErrorNoReturn("Invalid format string, no matching '}' at position ", nextbrace);
1319+
fi;
1320+
1321+
toprint := splitBracket(s, nextbrace+1,endbrace-1);
1322+
1323+
1324+
if toprint.id = "" then
1325+
if Length(args) < argcounter then
1326+
ErrorNoReturn("out of bounds in StringFormatted -- used ",argcounter," {} when there are only ",Length(args), " arguments");
1327+
fi;
1328+
var := args[argcounter];
1329+
argcounter := argcounter + 1;
1330+
elif Int(toprint.id) <> fail then
1331+
if Int(toprint.id) < 1 or Int(toprint.id) > Length(args) then
1332+
ErrorNoReturn("out of bounds in StringFormatted -- asked for {",Int(toprint.id),"} when there are only ",Length(args), " arguments");
1333+
fi;
1334+
var := args[Int(toprint.id)];
1335+
else
1336+
if not IsRecord(args[1]) then
1337+
ErrorNoReturn("first data argument must be a record when using {",toprint.id,"}");
1338+
fi;
1339+
if not IsBound(args[1].(toprint.id)) then
1340+
ErrorNoReturn("No record member '",toprint[1].id,"'");
1341+
fi;
1342+
var := args[1].(toprint.id);
1343+
fi;
1344+
pos := endbrace;
1345+
1346+
if toprint.type = "s" then
1347+
if not IsString(var) then
1348+
var := String(var);
1349+
fi;
1350+
AppendTo(stream, var);
1351+
elif toprint.type = "v" then
1352+
AppendTo(stream, ViewString(var));
1353+
elif toprint.type = "d" then
1354+
AppendTo(stream, DisplayString(var));
1355+
else ErrorNoReturn("Invalid formatting option: '", toprint.type, "'");
1356+
fi;
1357+
od;
1358+
end);
1359+
1360+
InstallGlobalFunction(StringFormatted, function(s, args...)
1361+
local str;
1362+
if not IsString(s) then
1363+
ErrorNoReturn("Usage: StringFormatted(<string>, <args>...");
1364+
fi;
1365+
str := "";
1366+
CallFuncList(PrintToFormatted, Concatenation([OutputTextString(str, false), s], args));
1367+
return str;
1368+
end);
1369+
1370+
InstallGlobalFunction(PrintFormatted, function(s, args...)
1371+
if not IsString(s) then
1372+
ErrorNoReturn("Usage: PrintFormatted(<string>, <args>...");
1373+
fi;
1374+
CallFuncList(PrintToFormatted, Concatenation(["*stdout*", s], args));
1375+
end);
12641376

12651377
#############################################################################
12661378
##

tst/testinstall/format.tst

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
gap> START_TEST("format.tst");
2+
gap> StringFormatted("a{}b{}c{}d", 1,(),"xyz");
3+
"a1b()cxyzd"
4+
gap> StringFormatted("{}{}{}", 1,(),"xyz");
5+
"1()xyz"
6+
gap> StringFormatted("{{}}{}}}{{", 0);
7+
"{}0}{"
8+
gap> StringFormatted("{", 1);
9+
Error, Invalid format string, no matching '}' at position 1
10+
gap> StringFormatted("{abc", 1);
11+
Error, Invalid format string, no matching '}' at position 1
12+
gap> StringFormatted("}", 1);
13+
Error, Mismatched '}' at position 1
14+
gap> StringFormatted("{3}{2}{2}{3}{1}", 1,2,3,4);
15+
"32231"
16+
gap> StringFormatted("{3}{}{2}{}{1}", 1,2,3,4);
17+
"31221"
18+
gap> StringFormatted("{a}{b}{a}", rec(a := (1,2), b := "ch"));
19+
"(1,2)ch(1,2)"
20+
gap> StringFormatted("{a}{b}{a}", 1,2);
21+
Error, first data argument must be a record when using {a}
22+
gap> StringFormatted("{}", rec());
23+
"rec( )"
24+
gap> StringFormatted("{1}", rec());
25+
"rec( )"
26+
gap> r1 := SymmetricGroup([3..5]);;
27+
gap> r2 := AlternatingGroup([1,3,5]);;
28+
gap> r3 := AlternatingGroup([11,12,13]);;
29+
gap> StringFormatted("{1!s} {1!v} {1!d}", r1);
30+
"SymmetricGroup( [ 3 .. 5 ] ) Sym( [ 3 .. 5 ] ) <object>\n"
31+
gap> StringFormatted("{!s} {!v} {!d}", r1, r2, r3);
32+
"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) <object>\n"
33+
gap> StringFormatted("{a!s} {b!v} {c!d}", rec(a := r1, b := r2, c := r3));
34+
"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) <object>\n"
35+
gap> StringFormatted("{a!x}", rec(a := r1));
36+
Error, Invalid formatting option: 'x'
37+
gap> StringFormatted("{a!}", rec(a := r1));
38+
"SymmetricGroup( [ 3 .. 5 ] )"
39+
gap> StringFormatted("{!x}", r1);
40+
Error, Invalid formatting option: 'x'
41+
gap> StringFormatted("abc{}def",[1,2]) = "abc[ 1, 2 ]def";
42+
true
43+
gap> StringFormatted([1,2]);
44+
Error, Usage: StringFormatted(<string>, <args>...
45+
gap> PrintFormatted("abc\n");
46+
gap> PrintFormatted("abc{}\n", 2);
47+
gap> PrintToFormatted("*stdout*", "abc{}\n", [1,2]);
48+
gap> PrintFormatted([1,2]);
49+
Error, Usage: PrintFormatted(<string>, <args>...
50+
gap> PrintToFormatted([1,2]);
51+
Error, Function: number of arguments must be at least 2 (not 1)
52+
gap> PrintToFormatted([1,2], "abc");
53+
Error, Usage: PrintToFormatted(<stream>, <string>, <args>...)
54+
gap> PrintToFormatted("*stdout*", [1,2]);
55+
Error, Usage: PrintToFormatted(<stream>, <string>, <args>...)
56+
gap> STOP_TEST("format.tst",1);

0 commit comments

Comments
 (0)