diff --git a/doc/ref/string.xml b/doc/ref/string.xml
index b16667854bd..6dae08defd9 100644
--- a/doc/ref/string.xml
+++ b/doc/ref/string.xml
@@ -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.
diff --git a/lib/string.gd b/lib/string.gd
index a9abaa3d3cb..b9ba0374a33 100644
--- a/lib/string.gd
+++ b/lib/string.gd
@@ -860,6 +860,70 @@ BindGlobal("BHINT", MakeImmutable("\>\<"));
DeclareGlobalFunction("StringOfMemoryAmount");
+#############################################################################
+##
+## <#GAPDoc Label="Formatted">
+##
+##
+##
+##
+##
+## These function performs a string formatting operation using a list or
+## record. returns the formatted string, while
+## appends the formatted string to stream,
+## which can be either an output stream or a filename.
+##
+## data is a list of values to printed, using the formatting rules below:
+##
+## string is treated as a normal string, except for occurrences
+## of { and }, which follow special rules, as follows:
+##
+## The contents of { } form a small language. The format is {id!format},
+## where both id and format are optional. If the ! is ommitted, the bracket
+## is treated as {id} with no format.
+##
+## id is interpreted as follows:
+##
+## An integer i
+## Take the ith element of data.
+##
+## No variable
+## Take the jth element of data, where j is the
+## number of occurrences of {} in the string, including this one,
+## up to this point.
+##
+## A string str
+## Take the str member of the first element of data, which
+## must be a record.
+##
+##
+##
+## The format decides how the variable is printed. format must be one
+## of s (which uses ), v (which uses
+## ) or d (which calls ).
+## The default value for format is s.
+##
+##
+## 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: {0!s}, ViewString: {0!v}", sym3);
+## "String: SymmetricGroup( [ 1 .. 3 ] ), ViewString: Sym( [ 1 .. 3 ] )"
+## ]]>
+## <#/GAPDoc>
+
+
+DeclareGlobalFunction("Formatted");
+DeclareGlobalFunction("FormattedTo");
+
+
#############################################################################
##
diff --git a/lib/string.gi b/lib/string.gi
index ed570e0e224..4c2fce07860 100644
--- a/lib/string.gi
+++ b/lib/string.gi
@@ -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
@@ -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(, , ...)");
+ 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(, ...");
+ fi;
+ str := "";
+ CallFuncList(FormattedTo, Concatenation([OutputTextString(str, false), s], args));
+ return str;
+end);
#############################################################################
##
diff --git a/tst/testinstall/format.tst b/tst/testinstall/format.tst
new file mode 100644
index 00000000000..0ff3f1771e8
--- /dev/null
+++ b/tst/testinstall/format.tst
@@ -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 ] )