From 917e1e344078148bdd43a13f91ad85e9132c83cf Mon Sep 17 00:00:00 2001 From: Chris Jefferson Date: Thu, 22 Feb 2018 23:57:23 +0000 Subject: [PATCH] Add StringFormatted, PrintFormatted, PrintToFormatted. This PR adds a simple method of formatting strings, strongly inspired by Python's "format" function. These 3 functions mirror Sting, Print and PrintTo. --- doc/ref/string.xml | 1 + lib/string.gd | 80 ++++++++++++++++++++++ lib/string.gi | 133 ++++++++++++++++++++++++++++++++++++- tst/testinstall/format.tst | 82 +++++++++++++++++++++++ 4 files changed, 295 insertions(+), 1 deletion(-) create mode 100644 tst/testinstall/format.tst diff --git a/doc/ref/string.xml b/doc/ref/string.xml index b16667854b..1cdf36a681 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="StringFormatted"> 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 a9abaa3d3c..81a2c8a303 100644 --- a/lib/string.gd +++ b/lib/string.gd @@ -860,6 +860,86 @@ BindGlobal("BHINT", MakeImmutable("\>\<")); DeclareGlobalFunction("StringOfMemoryAmount"); +############################################################################# +## +## <#GAPDoc Label="StringFormatted"> +## +## +## +## +## +## +## 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 data to replace the braces with. +##

+## There are three formatting functions, which differ only in how they +## output the formatted string. +## returns the formatted string, +## prints the formatted string and +## appends the formatted string to stream, +## which can be either an output stream or a filename. +##

+## The arguments after string form a list data of values used to +## substitute the replacement fields in string, using the following +## formatting rules: +##

+## string is treated as a normal string, except for occurrences +## of { and }, which follow special rules, as follows: +##

+## The contents of { } is split by a ! into {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. +## +## A string str +## If this is used, the first element of data must be a record r. +## In this case, the value r.(str) is taken. +## +## No id given +## Take the jth element of data, where j 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. +## +## +## +## A single brace can be outputted by doubling, so {{ in the format string +## produces { and }} produces }. +##

+## 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. +## +## +## 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 ] )" +## ]]> +## <#/GAPDoc> + + +DeclareGlobalFunction("StringFormatted"); +DeclareGlobalFunction("PrintFormatted"); +DeclareGlobalFunction("PrintToFormatted"); + + ############################################################################# ## diff --git a/lib/string.gi b/lib/string.gi index 116635c678..52aa715202 100644 --- a/lib/string.gi +++ b/lib/string.gi @@ -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(, , ...)"); + 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(, ...)"); + 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(, ...)"); + fi; + # We can't use PrintTo, as we do not know where Print is currently + # directed + Print(CallFuncList(StringFormatted, args)); +end); ############################################################################# ## diff --git a/tst/testinstall/format.tst b/tst/testinstall/format.tst new file mode 100644 index 0000000000..b2bd6e6e37 --- /dev/null +++ b/tst/testinstall/format.tst @@ -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(, ...) + +# Check format options +gap> StringFormatted("{1!s} {1!v} {1!d}", r1); +"SymmetricGroup( [ 3 .. 5 ] ) Sym( [ 3 .. 5 ] ) \n" +gap> StringFormatted("{!s} {!v} {!d}", r1, r2, r3); +"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) \n" +gap> StringFormatted("{a!s} {b!v} {c!d}", rec(a := r1, b := r2, c := r3)); +"SymmetricGroup( [ 3 .. 5 ] ) Alt( [ 1, 3 .. 5 ] ) \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(, ...) +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(, ...) +gap> PrintToFormatted([1,2]); +Error, Function: number of arguments must be at least 2 (not 1) +gap> PrintToFormatted([1,2], "abc"); +Error, Usage: PrintToFormatted(, , ...) +gap> PrintToFormatted("*stdout*", [1,2]); +Error, Usage: PrintToFormatted(, , ...) +gap> STOP_TEST("format.tst",1);