Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/ref/string.xml
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,7 @@ gap> s;
<#Include Label="JoinStringsWithSeparator">
<#Include Label="Chomp">
<#Include Label="StartsWith">
<#Include Label="StringFormatted">

The following two functions convert basic strings to lists of numbers and
vice versa. They are useful for examples of text encryption.
Expand Down
80 changes: 80 additions & 0 deletions lib/string.gd
Original file line number Diff line number Diff line change
Expand Up @@ -860,6 +860,86 @@ BindGlobal("BHINT", MakeImmutable("\>\<"));

DeclareGlobalFunction("StringOfMemoryAmount");

#############################################################################
##
## <#GAPDoc Label="StringFormatted">
## <ManSection>
## <Func Name="StringFormatted" Arg='string, data...'/>
## <Func Name="PrintFormatted" Arg='string, data...'/>
## <Func Name="PrintToFormatted" Arg='stream, string, data...'/>
##
## <Description>
## These functions perform a string formatting operation.
## They accept a format string, which can contain replacement fields
## which are delimited by braces {}.
## Each replacement field contains a numeric or positional argument,
## describing the element of <A>data</A> to replace the braces with.
## <P/>
## There are three formatting functions, which differ only in how they
## output the formatted string.
## <Ref Func="StringFormatted"/> returns the formatted string,
## <Ref Func="PrintFormatted"/> prints the formatted string and
## <Ref Func="PrintToFormatted"/> appends the formatted string to <A>stream</A>,
## which can be either an output stream or a filename.
## <P/>
## The arguments after <A>string</A> form a list <A>data</A> of values used to
## substitute the replacement fields in <A>string</A>, using the following
## formatting rules:
## <P/>
## <A>string</A> is treated as a normal string, except for occurrences
## of <C>{</C> and <C>}</C>, which follow special rules, as follows:
## <P/>
## The contents of <C>{ }</C> is split by a <C>!</C> into <C>{id!format}</C>,
## where both <C>id</C> and <C>format</C> are optional. If the <C>!</C> is
## ommitted, the bracket is treated as <C>{id}</C> with no <C>format</C>.
## <P/>
## <C>id</C> is interpreted as follows:
## <List>
## <Mark>An integer <C>i</C></Mark> <Item>
## Take the <C>i</C>th element of <A>data</A>.
## </Item>
## <Mark>A string <C>str</C></Mark> <Item>
## If this is used, the first element of <A>data</A> must be a record <C>r</C>.
## In this case, the value <C>r.(str)</C> is taken.
## </Item>
## <Mark>No id given</Mark> <Item>
## Take the <C>j</C>th element of <A>data</A>, where <C>j</C> is the
## number of replacement fields with no id in the format string so far.
## If any replacement field has no id, then all replacement fields must
## have no id.
## </Item>
## </List>
##
## A single brace can be outputted by doubling, so <C>{{</C> in the format string
## produces <C>{</C> and <C>}}</C> produces <C>}</C>.
## <P/>
## The <C>format</C> decides how the variable is printed. <C>format</C> must be one
## of <C>s</C> (which uses <Ref Oper="String"/>), <C>v</C> (which uses
## <Ref Oper="ViewString"/>) or <C>d</C> (which calls <Ref Oper="DisplayString"/>).
## The default value for <C>format</C> is <C>s</C>.
## </Description>
## </ManSection>
## <Example><![CDATA[
## gap> StringFormatted("I have {} cats and {} dogs", 4, 5);
## "I have 4 cats and 5 dogs"
## gap> StringFormatted("I have {2} cats and {1} dogs", 4, 5);
## "I have 5 cats and 4 dogs"
## gap> StringFormatted("I have {cats} cats and {dogs} dogs", rec(cats:=3, dogs:=2));
## "I have 3 cats and 2 dogs"
## gap> StringFormatted("We use {{ and }} to mark {dogs} dogs", rec(cats:=3, dogs:=2));
## "We use { and } to mark 2 dogs"
## gap> sym3 := SymmetricGroup(3);;
## gap> StringFormatted("String: {1!s}, ViewString: {1!v}", sym3);
## "String: SymmetricGroup( [ 1 .. 3 ] ), ViewString: Sym( [ 1 .. 3 ] )"
## ]]></Example>
## <#/GAPDoc>


DeclareGlobalFunction("StringFormatted");
DeclareGlobalFunction("PrintFormatted");
DeclareGlobalFunction("PrintToFormatted");



#############################################################################
##
Expand Down
133 changes: 132 additions & 1 deletion lib/string.gi
Original file line number Diff line number Diff line change
Expand Up @@ -1259,8 +1259,139 @@ InstallGlobalFunction(StringOfMemoryAmount, function(m)
return s;
end);


InstallGlobalFunction(PrintToFormatted, function(stream, s, data...)
local pos, len, outstr, nextbrace, endbrace,
argcounter, var,
splitReplacementField, toprint, namedIdUsed;

# Set to true if we ever use a named id in a replacement field
namedIdUsed := false;

# Split a replacement field {..} at [startpos..endpos]
splitReplacementField := function(startpos, endpos)
local posbang, format;
posbang := Position(s, '!', startpos-1);
if posbang = fail or posbang > endpos then
posbang := endpos + 1;
fi;
format := s{[posbang + 1 .. endpos]};
# If no format, default to "s"
if format = "" then
format := "s";
fi;
return rec(id := s{[startpos..posbang-1]}, format := format);
end;

argcounter := 1;
len := Length(s);
pos := 0;

if not (IsOutputStream(stream) or IsString(stream)) or not IsString(s) then
ErrorNoReturn("Usage: PrintToFormatted(<stream>, <string>, <data>...)");
fi;

while pos < len do
nextbrace := Position(s, '{', pos);
endbrace := Position(s, '}', pos);
# Scan until we find an '{'.
# Produce an error if we find '}', unless it is part of '}}'.
while IsInt(endbrace) and (nextbrace = fail or endbrace < nextbrace) do
if endbrace + 1 <= len and s[endbrace + 1] = '}' then
# Found }} with no { before it, insert everything up to
# including the first }, skipping the second.
AppendTo(stream, s{[pos+1..endbrace]});
pos := endbrace + 1;
endbrace := Position(s, '}', pos);
else
ErrorNoReturn("Mismatched '}' at position ",endbrace);
fi;
od;

if nextbrace = fail then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, let's see... if we got here, either endbrace=fail (and then this is correct), or else endbrace is an integer such that endbrace >= nextbrace, but then nextbrace cannot be fail, as fail is larger than any integer. OK, so this is correct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't planning on being this clever, I obviously just didn't think about some cases well enough, then the tests passed.

# In this case, endbrace = fail, or we would not have left
# previous while loop
AppendTo(stream, s{[pos+1..len]});
return;
fi;

AppendTo(stream, s{[pos+1..nextbrace-1]});

# If this is {{, then print a { and call 'continue'
if nextbrace+1 <= len and s[nextbrace+1] = '{' then
AppendTo(stream, "{");
pos := nextbrace + 1;
continue;
fi;

if endbrace = fail then
ErrorNoReturn("Invalid format string, no matching '}' at position ", nextbrace);
fi;

toprint := splitReplacementField(nextbrace+1,endbrace-1);

# Check if we are mixing giving id, and not giving id.
if (argcounter > 1 and toprint.id <> "") or (namedIdUsed and toprint.id = "") then
ErrorNoReturn("replacement field must either all have an id, or all have no id");
fi;

if toprint.id = "" then
if Length(data) < argcounter then
ErrorNoReturn("out of bounds -- used ",argcounter," replacement fields without id when there are only ",Length(data), " arguments");
fi;
var := data[argcounter];
argcounter := argcounter + 1;
elif Int(toprint.id) <> fail then
namedIdUsed := true;
if Int(toprint.id) < 1 or Int(toprint.id) > Length(data) then
ErrorNoReturn("out of bounds -- asked for {",Int(toprint.id),"} when there are only ",Length(data), " arguments");
fi;
var := data[Int(toprint.id)];
else
namedIdUsed := true;
if not IsRecord(data[1]) then
ErrorNoReturn("first data argument must be a record when using {",toprint.id,"}");
fi;
if not IsBound(data[1].(toprint.id)) then
ErrorNoReturn("no record member '",toprint[1].id,"'");
fi;
var := data[1].(toprint.id);
fi;
pos := endbrace;

if toprint.format = "s" then
if not IsString(var) then
var := String(var);
fi;
AppendTo(stream, var);
elif toprint.format = "v" then
AppendTo(stream, ViewString(var));
elif toprint.format = "d" then
AppendTo(stream, DisplayString(var));
else ErrorNoReturn("Invalid format: '", toprint.format, "'");
fi;
od;
end);

InstallGlobalFunction(StringFormatted, function(s, data...)
local str;
if not IsString(s) then
ErrorNoReturn("Usage: StringFormatted(<string>, <data>...)");
fi;
str := "";
CallFuncList(PrintToFormatted, Concatenation([OutputTextString(str, false), s], data));
return str;
end);

InstallGlobalFunction(PrintFormatted, function(args...)
# Do some very baic argument checking
if not Length(args) > 1 and IsString(args[1]) then
ErrorNoReturn("Usage: PrintFormatted(<string>, <data>...)");
fi;

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

#############################################################################
##
Expand Down
82 changes: 82 additions & 0 deletions tst/testinstall/format.tst
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
gap> START_TEST("format.tst");

# Some variables we will use for testing printing
gap> r1 := SymmetricGroup([3..5]);;
gap> r2 := AlternatingGroup([1,3,5]);;
gap> r3 := AlternatingGroup([11,12,13]);;

# Start with simple examples
gap> StringFormatted("a{}b{}c{}d", 1,(),"xyz");
"a1b()cxyzd"
gap> StringFormatted("{}{}{}", 1,(),"xyz");
"1()xyz"

# Check id types
gap> StringFormatted("{3}{2}{2}{3}{1}", 1,2,3,4);
"32231"
gap> StringFormatted("{a}{b}{a}", rec(a := (1,2), b := "ch"));
"(1,2)ch(1,2)"
gap> StringFormatted("{}", rec());
"rec( )"
gap> StringFormatted("{1}", rec());
"rec( )"

# Check double bracket matching
gap> StringFormatted("{{}}{}}}{{", 0);
"{}0}{"

# Error cases
gap> StringFormatted("{", 1);
Error, Invalid format string, no matching '}' at position 1
gap> StringFormatted("{abc", 1);
Error, Invalid format string, no matching '}' at position 1
gap> StringFormatted("}", 1);
Error, Mismatched '}' at position 1
gap> StringFormatted("{}{1}", 1,2,3,4);
Error, replacement field must either all have an id, or all have no id
gap> StringFormatted("{1}{}", 1,2,3,4);
Error, replacement field must either all have an id, or all have no id
gap> StringFormatted("{}{a}", rec(a := 1) );
Error, replacement field must either all have an id, or all have no id
gap> StringFormatted("{a}{}", rec(a := 1) );
Error, replacement field must either all have an id, or all have no id
gap> StringFormatted("{a}{b}{a}", 1,2);
Error, first data argument must be a record when using {a}
gap> StringFormatted("{a!x}", rec(a := r1));
Error, Invalid format: 'x'
gap> StringFormatted("{!x}", r1);
Error, Invalid format: 'x'
gap> StringFormatted([1,2]);
Error, Usage: StringFormatted(<string>, <data>...)

# Check format options
gap> StringFormatted("{1!s} {1!v} {1!d}", r1);
"SymmetricGroup( [ 3 .. 5 ] ) Sym( [ 3 .. 5 ] ) <object>\n"
gap> StringFormatted("{!s} {!v} {!d}", r1, r2, r3);
"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) <object>\n"
gap> StringFormatted("{a!s} {b!v} {c!d}", rec(a := r1, b := r2, c := r3));
"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) <object>\n"
gap> StringFormatted("{a!}", rec(a := r1));
"SymmetricGroup( [ 3 .. 5 ] )"
gap> StringFormatted("abc{}def",[1,2]) = "abc[ 1, 2 ]def";
true

# Test alternative functions
gap> PrintFormatted("abc\n\n");
Error, Usage: PrintFormatted(<string>, <data>...)
gap> PrintFormatted("abc{}\n", 2);
abc2
gap> str := "";
""
gap> PrintToFormatted(OutputTextString(str, false), "abc{}\n", [1,2]);
gap> Print(str);
abc[ 1, 2 ]
gap> PrintFormatted([1,2]);
Error, Usage: StringFormatted(<string>, <data>...)
gap> PrintToFormatted([1,2]);
Error, Function: number of arguments must be at least 2 (not 1)
gap> PrintToFormatted([1,2], "abc");
Error, Usage: PrintToFormatted(<stream>, <string>, <data>...)
gap> PrintToFormatted("*stdout*", [1,2]);
Error, Usage: PrintToFormatted(<stream>, <string>, <data>...)
gap> STOP_TEST("format.tst",1);