Skip to content

Commit a29a129

Browse files
ChrisJeffersonfingolfin
authored andcommitted
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 6cb64f6 commit a29a129

4 files changed

Lines changed: 295 additions & 1 deletion

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: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,6 +860,86 @@ 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 perform a string formatting operation.
873+
## They accept a format string, which can contain replacement fields
874+
## which are delimited by braces {}.
875+
## Each replacement field contains a numeric or positional argument,
876+
## describing the element of <A>data</A> to replace the braces with.
877+
## <P/>
878+
## There are three formatting functions, which differ only in how they
879+
## output the formatted string.
880+
## <Ref Func="StringFormatted"/> returns the formatted string,
881+
## <Ref Func="PrintFormatted"/> prints the formatted string and
882+
## <Ref Func="PrintToFormatted"/> appends the formatted string to <A>stream</A>,
883+
## which can be either an output stream or a filename.
884+
## <P/>
885+
## The arguments after <A>string</A> form a list <A>data</A> of values used to
886+
## substitute the replacement fields in <A>string</A>, using the following
887+
## formatting rules:
888+
## <P/>
889+
## <A>string</A> is treated as a normal string, except for occurrences
890+
## of <C>{</C> and <C>}</C>, which follow special rules, as follows:
891+
## <P/>
892+
## The contents of <C>{ }</C> is split by a <C>!</C> into <C>{id!format}</C>,
893+
## where both <C>id</C> and <C>format</C> are optional. If the <C>!</C> is
894+
## ommitted, the bracket is treated as <C>{id}</C> with no <C>format</C>.
895+
## <P/>
896+
## <C>id</C> is interpreted as follows:
897+
## <List>
898+
## <Mark>An integer <C>i</C></Mark> <Item>
899+
## Take the <C>i</C>th element of <A>data</A>.
900+
## </Item>
901+
## <Mark>A string <C>str</C></Mark> <Item>
902+
## If this is used, the first element of <A>data</A> must be a record <C>r</C>.
903+
## In this case, the value <C>r.(str)</C> is taken.
904+
## </Item>
905+
## <Mark>No id given</Mark> <Item>
906+
## Take the <C>j</C>th element of <A>data</A>, where <C>j</C> is the
907+
## number of replacement fields with no id in the format string so far.
908+
## If any replacement field has no id, then all replacement fields must
909+
## have no id.
910+
## </Item>
911+
## </List>
912+
##
913+
## A single brace can be outputted by doubling, so <C>{{</C> in the format string
914+
## produces <C>{</C> and <C>}}</C> produces <C>}</C>.
915+
## <P/>
916+
## The <C>format</C> decides how the variable is printed. <C>format</C> must be one
917+
## of <C>s</C> (which uses <Ref Oper="String"/>), <C>v</C> (which uses
918+
## <Ref Oper="ViewString"/>) or <C>d</C> (which calls <Ref Oper="DisplayString"/>).
919+
## The default value for <C>format</C> is <C>s</C>.
920+
## </Description>
921+
## </ManSection>
922+
## <Example><![CDATA[
923+
## gap> StringFormatted("I have {} cats and {} dogs", 4, 5);
924+
## "I have 4 cats and 5 dogs"
925+
## gap> StringFormatted("I have {2} cats and {1} dogs", 4, 5);
926+
## "I have 5 cats and 4 dogs"
927+
## gap> StringFormatted("I have {cats} cats and {dogs} dogs", rec(cats:=3, dogs:=2));
928+
## "I have 3 cats and 2 dogs"
929+
## gap> StringFormatted("We use {{ and }} to mark {dogs} dogs", rec(cats:=3, dogs:=2));
930+
## "We use { and } to mark 2 dogs"
931+
## gap> sym3 := SymmetricGroup(3);;
932+
## gap> StringFormatted("String: {1!s}, ViewString: {1!v}", sym3);
933+
## "String: SymmetricGroup( [ 1 .. 3 ] ), ViewString: Sym( [ 1 .. 3 ] )"
934+
## ]]></Example>
935+
## <#/GAPDoc>
936+
937+
938+
DeclareGlobalFunction("StringFormatted");
939+
DeclareGlobalFunction("PrintFormatted");
940+
DeclareGlobalFunction("PrintToFormatted");
941+
942+
863943

864944
#############################################################################
865945
##

lib/string.gi

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1259,8 +1259,139 @@ InstallGlobalFunction(StringOfMemoryAmount, function(m)
12591259
return s;
12601260
end);
12611261

1262-
1262+
InstallGlobalFunction(PrintToFormatted, function(stream, s, data...)
1263+
local pos, len, outstr, nextbrace, endbrace,
1264+
argcounter, var,
1265+
splitReplacementField, toprint, namedIdUsed;
1266+
1267+
# Set to true if we ever use a named id in a replacement field
1268+
namedIdUsed := false;
1269+
1270+
# Split a replacement field {..} at [startpos..endpos]
1271+
splitReplacementField := function(startpos, endpos)
1272+
local posbang, format;
1273+
posbang := Position(s, '!', startpos-1);
1274+
if posbang = fail or posbang > endpos then
1275+
posbang := endpos + 1;
1276+
fi;
1277+
format := s{[posbang + 1 .. endpos]};
1278+
# If no format, default to "s"
1279+
if format = "" then
1280+
format := "s";
1281+
fi;
1282+
return rec(id := s{[startpos..posbang-1]}, format := format);
1283+
end;
1284+
1285+
argcounter := 1;
1286+
len := Length(s);
1287+
pos := 0;
1288+
1289+
if not (IsOutputStream(stream) or IsString(stream)) or not IsString(s) then
1290+
ErrorNoReturn("Usage: PrintToFormatted(<stream>, <string>, <data>...)");
1291+
fi;
1292+
1293+
while pos < len do
1294+
nextbrace := Position(s, '{', pos);
1295+
endbrace := Position(s, '}', pos);
1296+
# Scan until we find an '{'.
1297+
# Produce an error if we find '}', unless it is part of '}}'.
1298+
while IsInt(endbrace) and (nextbrace = fail or endbrace < nextbrace) do
1299+
if endbrace + 1 <= len and s[endbrace + 1] = '}' then
1300+
# Found }} with no { before it, insert everything up to
1301+
# including the first }, skipping the second.
1302+
AppendTo(stream, s{[pos+1..endbrace]});
1303+
pos := endbrace + 1;
1304+
endbrace := Position(s, '}', pos);
1305+
else
1306+
ErrorNoReturn("Mismatched '}' at position ",endbrace);
1307+
fi;
1308+
od;
1309+
1310+
if nextbrace = fail then
1311+
# In this case, endbrace = fail, or we would not have left
1312+
# previous while loop
1313+
AppendTo(stream, s{[pos+1..len]});
1314+
return;
1315+
fi;
1316+
1317+
AppendTo(stream, s{[pos+1..nextbrace-1]});
1318+
1319+
# If this is {{, then print a { and call 'continue'
1320+
if nextbrace+1 <= len and s[nextbrace+1] = '{' then
1321+
AppendTo(stream, "{");
1322+
pos := nextbrace + 1;
1323+
continue;
1324+
fi;
1325+
1326+
if endbrace = fail then
1327+
ErrorNoReturn("Invalid format string, no matching '}' at position ", nextbrace);
1328+
fi;
1329+
1330+
toprint := splitReplacementField(nextbrace+1,endbrace-1);
1331+
1332+
# Check if we are mixing giving id, and not giving id.
1333+
if (argcounter > 1 and toprint.id <> "") or (namedIdUsed and toprint.id = "") then
1334+
ErrorNoReturn("replacement field must either all have an id, or all have no id");
1335+
fi;
1336+
1337+
if toprint.id = "" then
1338+
if Length(data) < argcounter then
1339+
ErrorNoReturn("out of bounds -- used ",argcounter," replacement fields without id when there are only ",Length(data), " arguments");
1340+
fi;
1341+
var := data[argcounter];
1342+
argcounter := argcounter + 1;
1343+
elif Int(toprint.id) <> fail then
1344+
namedIdUsed := true;
1345+
if Int(toprint.id) < 1 or Int(toprint.id) > Length(data) then
1346+
ErrorNoReturn("out of bounds -- asked for {",Int(toprint.id),"} when there are only ",Length(data), " arguments");
1347+
fi;
1348+
var := data[Int(toprint.id)];
1349+
else
1350+
namedIdUsed := true;
1351+
if not IsRecord(data[1]) then
1352+
ErrorNoReturn("first data argument must be a record when using {",toprint.id,"}");
1353+
fi;
1354+
if not IsBound(data[1].(toprint.id)) then
1355+
ErrorNoReturn("no record member '",toprint[1].id,"'");
1356+
fi;
1357+
var := data[1].(toprint.id);
1358+
fi;
1359+
pos := endbrace;
1360+
1361+
if toprint.format = "s" then
1362+
if not IsString(var) then
1363+
var := String(var);
1364+
fi;
1365+
AppendTo(stream, var);
1366+
elif toprint.format = "v" then
1367+
AppendTo(stream, ViewString(var));
1368+
elif toprint.format = "d" then
1369+
AppendTo(stream, DisplayString(var));
1370+
else ErrorNoReturn("Invalid format: '", toprint.format, "'");
1371+
fi;
1372+
od;
1373+
end);
1374+
1375+
InstallGlobalFunction(StringFormatted, function(s, data...)
1376+
local str;
1377+
if not IsString(s) then
1378+
ErrorNoReturn("Usage: StringFormatted(<string>, <data>...)");
1379+
fi;
1380+
str := "";
1381+
CallFuncList(PrintToFormatted, Concatenation([OutputTextString(str, false), s], data));
1382+
return str;
1383+
end);
1384+
1385+
InstallGlobalFunction(PrintFormatted, function(args...)
1386+
# Do some very baic argument checking
1387+
if not Length(args) > 1 and IsString(args[1]) then
1388+
ErrorNoReturn("Usage: PrintFormatted(<string>, <data>...)");
1389+
fi;
12631390

1391+
# We can't use PrintTo, as we do not know where Print is currently
1392+
# directed
1393+
Print(CallFuncList(StringFormatted, args));
1394+
end);
12641395

12651396
#############################################################################
12661397
##

tst/testinstall/format.tst

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
gap> START_TEST("format.tst");
2+
3+
# Some variables we will use for testing printing
4+
gap> r1 := SymmetricGroup([3..5]);;
5+
gap> r2 := AlternatingGroup([1,3,5]);;
6+
gap> r3 := AlternatingGroup([11,12,13]);;
7+
8+
# Start with simple examples
9+
gap> StringFormatted("a{}b{}c{}d", 1,(),"xyz");
10+
"a1b()cxyzd"
11+
gap> StringFormatted("{}{}{}", 1,(),"xyz");
12+
"1()xyz"
13+
14+
# Check id types
15+
gap> StringFormatted("{3}{2}{2}{3}{1}", 1,2,3,4);
16+
"32231"
17+
gap> StringFormatted("{a}{b}{a}", rec(a := (1,2), b := "ch"));
18+
"(1,2)ch(1,2)"
19+
gap> StringFormatted("{}", rec());
20+
"rec( )"
21+
gap> StringFormatted("{1}", rec());
22+
"rec( )"
23+
24+
# Check double bracket matching
25+
gap> StringFormatted("{{}}{}}}{{", 0);
26+
"{}0}{"
27+
28+
# Error cases
29+
gap> StringFormatted("{", 1);
30+
Error, Invalid format string, no matching '}' at position 1
31+
gap> StringFormatted("{abc", 1);
32+
Error, Invalid format string, no matching '}' at position 1
33+
gap> StringFormatted("}", 1);
34+
Error, Mismatched '}' at position 1
35+
gap> StringFormatted("{}{1}", 1,2,3,4);
36+
Error, replacement field must either all have an id, or all have no id
37+
gap> StringFormatted("{1}{}", 1,2,3,4);
38+
Error, replacement field must either all have an id, or all have no id
39+
gap> StringFormatted("{}{a}", rec(a := 1) );
40+
Error, replacement field must either all have an id, or all have no id
41+
gap> StringFormatted("{a}{}", rec(a := 1) );
42+
Error, replacement field must either all have an id, or all have no id
43+
gap> StringFormatted("{a}{b}{a}", 1,2);
44+
Error, first data argument must be a record when using {a}
45+
gap> StringFormatted("{a!x}", rec(a := r1));
46+
Error, Invalid format: 'x'
47+
gap> StringFormatted("{!x}", r1);
48+
Error, Invalid format: 'x'
49+
gap> StringFormatted([1,2]);
50+
Error, Usage: StringFormatted(<string>, <data>...)
51+
52+
# Check format options
53+
gap> StringFormatted("{1!s} {1!v} {1!d}", r1);
54+
"SymmetricGroup( [ 3 .. 5 ] ) Sym( [ 3 .. 5 ] ) <object>\n"
55+
gap> StringFormatted("{!s} {!v} {!d}", r1, r2, r3);
56+
"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) <object>\n"
57+
gap> StringFormatted("{a!s} {b!v} {c!d}", rec(a := r1, b := r2, c := r3));
58+
"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) <object>\n"
59+
gap> StringFormatted("{a!}", rec(a := r1));
60+
"SymmetricGroup( [ 3 .. 5 ] )"
61+
gap> StringFormatted("abc{}def",[1,2]) = "abc[ 1, 2 ]def";
62+
true
63+
64+
# Test alternative functions
65+
gap> PrintFormatted("abc\n\n");
66+
Error, Usage: PrintFormatted(<string>, <data>...)
67+
gap> PrintFormatted("abc{}\n", 2);
68+
abc2
69+
gap> str := "";
70+
""
71+
gap> PrintToFormatted(OutputTextString(str, false), "abc{}\n", [1,2]);
72+
gap> Print(str);
73+
abc[ 1, 2 ]
74+
gap> PrintFormatted([1,2]);
75+
Error, Usage: StringFormatted(<string>, <data>...)
76+
gap> PrintToFormatted([1,2]);
77+
Error, Function: number of arguments must be at least 2 (not 1)
78+
gap> PrintToFormatted([1,2], "abc");
79+
Error, Usage: PrintToFormatted(<stream>, <string>, <data>...)
80+
gap> PrintToFormatted("*stdout*", [1,2]);
81+
Error, Usage: PrintToFormatted(<stream>, <string>, <data>...)
82+
gap> STOP_TEST("format.tst",1);

0 commit comments

Comments
 (0)