Skip to content

Commit

Permalink
Add Formatted
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
  • Loading branch information
ChrisJefferson committed Mar 2, 2018
1 parent 24c69d0 commit 0d7e272
Show file tree
Hide file tree
Showing 4 changed files with 214 additions and 3 deletions.
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="Formatted">

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

DeclareGlobalFunction("StringOfMemoryAmount");

#############################################################################
##
## <#GAPDoc Label="Formatted">
## <ManSection>
## <Func Name="Formatted" Arg='string, data...'/>
## <Func Name="FormattedTo" Arg='stream, string, data...'/>
##
## <Description>
## These function performs a string formatting operation using a list or
## record. <Ref Func="Formatted"/> returns the formatted string, while
## <Ref Func="FormattedTo"/> appends the formatted string to <A>stream</A>,
## which can be either an output stream or a filename.
## <P/>
## <A>data</A> is a list of values to printed, using the formatting rules below:
## <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:
##
## The contents of <C>{ }</C> form a small language. The format is <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>No variable</Mark> <Item>
## Take the <C>j</C>th element of <A>data</A>, where <C>j</C> is the
## number of occurrences of <C>{}</C> in the string, including this one,
## up to this point.
## </Item>
## <Mark>A string <C>str</C></Mark> <Item>
## Take the <C>str</C> member of the first element of <A>data</A>, which
## must be a record.
## </Item>
## </List>
##
## 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> Formatted("I have {} cats and {} dogs", 4, 5);
## "I have 4 cats and 5 dogs"
## gap> Formatted("I have {2} cats and {1} dogs", 4, 5);
## "I have 5 cats and 4 dogs"
## gap> Formatted("I have {cats} cats and {dogs} dogs", rec(cats:=3, dogs:=2));
## "I have 3 cats and 2 dogs"
## gap> Formatted("We use {{ and }} to mark {dogs} dogs", rec(cats:=3, dogs:=2));
## "We use { and } to mark 2 dogs"
## gap> sym3 := SymmetricGroup(3);;
## gap> Formatted("String: {1!s}, ViewString: {1!v}", sym3);
## "String: SymmetricGroup( [ 1 .. 3 ] ), ViewString: Sym( [ 1 .. 3 ] )"
## ]]></Example>
## <#/GAPDoc>


DeclareGlobalFunction("Formatted");
DeclareGlobalFunction("FormattedTo");



#############################################################################
##
Expand Down
111 changes: 108 additions & 3 deletions lib/string.gi
Original file line number Diff line number Diff line change
Expand Up @@ -1041,7 +1041,7 @@ InstallGlobalFunction(PrintCSV,function(arg)
end);


# Format commands
# Formatted commands
# RLC: alignment
# M: Math mode
# MN: Math mode but names, characters are put into mbox
Expand Down Expand Up @@ -1258,9 +1258,114 @@ InstallGlobalFunction(StringOfMemoryAmount, function(m)
Append(s, units[shift+1]);
return s;
end);



InstallGlobalFunction(FormattedTo, function(stream, s, args...)
local pos, len, outstr, nextbrace, endbrace,
argcounter, var,
splitBracket, toprint;

splitBracket := function(string, startpos, endpos)
local posbang, type;
posbang := Position(s, '!', startpos-1);
# Print(startpos, ":",endpos, ":",posbang,"\n");
if posbang = fail or posbang > endpos then
posbang := endpos + 1;
fi;
type := string{[posbang + 1 .. endpos]};
if type = "" then
type := "s";
fi;
return rec(id := string{[startpos..posbang-1]}, type := type);
end;

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

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

while pos < len do
nextbrace := Position(s, '{', pos);
endbrace := Position(s, '}', pos);
# Keep checking for '}}' until we find an '{'
while endbrace <> fail and endbrace < nextbrace do
if endbrace + 1 <= len and s[endbrace + 1] = '}' then
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
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 := splitBracket(s, nextbrace+1,endbrace-1);


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

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

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

#############################################################################
##
Expand Down
41 changes: 41 additions & 0 deletions tst/testinstall/format.tst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
gap> START_TEST("format.tst");
gap> Formatted("a{}b{}c{}d", 1,(),"xyz");
"a1b()cxyzd"
gap> Formatted("{}{}{}", 1,(),"xyz");
"1()xyz"
gap> Formatted("{{}}{}}}{{", 0);
"{}0}{"
gap> Formatted("{", 1);
Error, Invalid format string, no matching '}' at position 1
gap> Formatted("{abc", 1);
Error, Invalid format string, no matching '}' at position 1
gap> Formatted("}", 1);
Error, Mismatched '}' at position 1
gap> Formatted("{3}{2}{2}{3}{1}", 1,2,3,4);
"32231"
gap> Formatted("{3}{}{2}{}{1}", 1,2,3,4);
"31221"
gap> Formatted("{a}{b}{a}", rec(a := (1,2), b := "ch"));
"(1,2)ch(1,2)"
gap> Formatted("{a}{b}{a}", 1,2);
Error, first data argument must be a record when using {a}
gap> Formatted("{}", rec());
"rec( )"
gap> Formatted("{1}", rec());
"rec( )"
gap> r1 := SymmetricGroup([3..5]);;
gap> r2 := AlternatingGroup([1,3,5]);;
gap> r3 := AlternatingGroup([11,12,13]);;
gap> Formatted("{1!s} {1!v} {1!d}", r1);
"SymmetricGroup( [ 3 .. 5 ] ) Sym( [ 3 .. 5 ] ) <object>\n"
gap> Formatted("{!s} {!v} {!d}", r1, r2, r3);
"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) <object>\n"
gap> Formatted("{a!s} {b!v} {c!d}", rec(a := r1, b := r2, c := r3));
"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) <object>\n"
gap> Formatted("{a!x}", rec(a := r1));
Error, Invalid formatting option: 'x'
gap> Formatted("{a!}", rec(a := r1));
"SymmetricGroup( [ 3 .. 5 ] )"
gap> Formatted("{!x}", r1);
Error, Invalid formatting option: 'x'
gap> STOP_TEST("format.tst",1);

0 comments on commit 0d7e272

Please sign in to comment.