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 ] ) \n" +gap> Formatted("{!s} {!v} {!d}", r1, r2, r3); +"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) \n" +gap> Formatted("{a!s} {b!v} {c!d}", rec(a := r1, b := r2, c := r3)); +"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) \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);