Skip to content

Commit

Permalink
Add StringFormatted, PrintFormatted, PrintToFormatted.
Browse files Browse the repository at this point in the history
This PR adds a simple method of formatting strings, strongly
inspired by Python's "format" function. These 3 functions
mirror Sting, Print and PrintTo.
  • Loading branch information
ChrisJefferson committed Mar 29, 2018
1 parent 6633b56 commit 917e1e3
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 1 deletion.
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
# 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);

0 comments on commit 917e1e3

Please sign in to comment.