Hi,
I'm relatively new to Nim, porting a Python project.
In Python I could use a dictionary (Nim table) to get a string to use as input for formatting strings. I tried something similar in Nim but my tries fail.
I've put together some code to illustrate what I try to do : using various strings to determine at runtime what formatter to use for converting data into a string. It's just a simple example using languages.
importc printf works, but if that is the way to go, I really need sprintf and that did not work (I'm undoubtedly doing it the wrong way). However, I'd rather do it in pure Nim, for printf there's a warning about the garbage collector and it will be used quite a bit in the project.
import strutils, tables, strformat
proc printf(formatstr: cstring) {.importc: "printf", varargs,
header: "<stdio.h>".}
proc sprintf(formatstr: cstring) {.importc: "sprintf", varargs,
header: "<stdio.h>".}
var
name: string
language: string
greeting: string
languages: array[3, string] = ["en", "de", "fr"]
let greetings = {"en": "Hi {name}", "fr": "Bonjour {name}", "de": "Gutentag {name}"}.toTable
let cgreetings = {"en": "Hi %s", "fr": "Bonjour %s", "de": "Gutentag %s"}.toTable
name = "Albert"
for language in languages:
# echo fmt(greetings[language]) # error : only works with string literals
printf(cgreetings[language],name) # works, but I really need it assigned to a string (like sprintf)
#greeting = sprintf(cgreetings[language], name) # error: has no type (or is ambiguous)
echo ""
If I understand the requirements properly the strutils.% operator should work, it allows you to do "some sentence. $1" % "hello" which turns into "some sentence. hello" you can also do "$1 $2" % ["Hello", "World"] to get "Hello World" or finally `"$# $#" which will use arg1 then arg2.
Here is how it'd look with your example
import std/[strutils, tables]
var
name: string
language: string
greeting: string
languages: array[3, string] = ["en", "de", "fr"]
let greetings = {"en": "Hi $1", "fr": "Bonjour $1", "de": "Gutentag $1"}.toTable
let cgreetings = {"en": "Hi $1", "fr": "Bonjour $1", "de": "Gutentag $1"}.toTable
name = "Albert"
for language in languages:
echo cgreetings[language] % name, "\n"
echo "$# $#" % ["Hello", "World"]
Thank you !
Indeed that works, but isn't that limited to strings ?
My example was rather simple, with only strings, but it'll be a mix of types. It's for a speech system where there will be various phrases for saying the same thing, in order not to say things the same way all the time. So there will be ints, floats and strings as arguments for the formatter. And the floats will vary with one, two, three decimals, depending on what variable the text-to-speech system has to announce.
Hope this is what you want:
import strutils, tables
const
languages = ["en", "de", "fr"]
greetings = {"en": "Hi $#", "fr": "Bonjour $#", "de": "Gutentag $#"}.toTable()
name = "Albert"
for language in languages:
let s = greetings[language] % name
echo s
Hi Albert
Gutentag Albert
Bonjour Albert
String interpolation with the `%` proc
Thanks for the prompt replies
Converting them to string first (floats and ints), re-introduces the problem of how to format the floats (depending on situation .0f, .1f .2f .3f) and ints :(
My example was a bit too simple.
It should have been more like :
import strutils, tables, strformat, random
var
name: string
temperature: float
sentence, formatter: string
whichsentence: int
waysToSay: array[3, string] = ["en", "de", "fr"]
let sayTemp: array[3, string] = ["{name} it's {temperature:.1f} degrees", "It is {temperature:.1f} degrees {name}", "The temperature is {temperature:.1f} degrees"]
whichSentence = random[2]
formatter = sayTemperature[whichSentence]
sentence = fmt(formatter)
Obviously this is non-working code, but it shows better what I try to do than the simple en/fr/de language example. Nim would be preferable, because there the format arguments are named {name} {temperature}, which makes it possible to swap them, sprintf would not allow that as far as I know.
ints, floats and strings as arguments for the formatter
yep, that's in strutils:
import strutils
echo "Hi $1 $2 $3".format(3, 5.123, "nine")
And the floats will vary with one, two, three decimals
i know you're looking for a format specifier DSL like in strformat (or sprintf) that parses out the precision. that would be lovely, but i don't think those batteries are included in the stdlib. there is 'formatFloat', of course:
for i in [1,2,3]:
echo 5.123.formatFloat(format = ffDecimal, precision = i)
# 5.1
# 5.12
# 5.123
but some assembly required.
I also found this undocumented (vestigial?) behaviour, which i suppose means you dont need to use a Table
let greetings = ["en","Hi","fr","Salut","it","Ciao"]
assert "$en" % greetings == "Hi"
assert "$fr" % greetings == "Salut"
assert "$it" % greetings == "Ciao"
in any case i can solve your sprintf problem:
proc csnprintf(result: cstring,size:csize_t, formatstr: cstring):cint {.importc: "snprintf", varargs,
header: "<stdio.h>",discardable.}
template sprintf*(formatstr: string,args:varargs[untyped]):string =
let len = csnprintf(nil,0,formatstr,args)+1
var result = newString(len)
csnprintf(result,len.csize_t,formatstr,args)
result
echo sprintf("Hello %s it's %.1f degrees outside","Albert",35.23976)
Thank you shirleyquirk !
I think I'll go with sprintf solution for now, it's closest to the Python code I'm porting.
When I get more proficient at Nim, I may try to see whether it can be implemented with the named format (like {name} {value:.2f}, ...), which has added benefits.
BTW, that undocumented behaviour is interesting, but for a few hundred lines of sentences, it would surely make the code less readable/maintainable than the tables route. But for very small things it may come in handy, so I added it to my snippets.
Thank you, but I'm in my second week of Nim programming. I may look into stuffing more complex things in the tables.
However, cramming it into a string probably will make it easier to translate the app later on, should I wish to do so. It'll be only a list of strings to translate, which can be loaded at program startup. So I believe, indeed, my mileage varies. The sprintf solution shirleyquirk offered is close to the Python implementation, which will also speed up the porting to Nim.
Still wondering why this works :
var name: string = "Albert"
echo fmt("Hi {name}, this is a test")
While the code below throws a compile error "only works with string literals". Isn't the variable format a string literal ? Even when I declare it as const (before the loop), it doesn't work.
var name: string = "Bernard"
let format = "Hi {name}, this is a test"
echo fmt(format) # error : only works with string literals
Oh, it seems an issue marked as TO BE FIXED:
https://scripter.co/notes/nim-fmt/#fmt-and-and-formatting-strings-cannot-be-a-variable
So there is hope yet...
So there is hope yet... if it gets fixed
Nah, there is no bug here to be fixed. Domain specific languages work better when they are not done at runtime, it's disaster for security when done at runtime.
Isn't the variable format a string literal ?
A variable isn't a literal, by definition :) Even though let is an immutable declaration, its contents aren't known at compile time (they can be the result of a runtime computation), contrary to a const. Consts don't work though, no one made it work yet but it's possible.
Domain specific languages work better when they are not done at runtime
Talking specifically about the & macro from strformat, it would be really awesome to specify formatting strings at run time. I see @IvanS opened this issue: https://github.com/nim-lang/Nim/issues/18218 👍
OK, I understand that it's not implemented for security reasons. But as kaushalmodi says, "it would really be awesome". In certain projects it would come in very, very handy.
So it seems it's sprintf for me, although I think it's the same security disaster if it's in my program.
But, for software that is multi lingual, isn't that the road they take ? Some kind of resource file per language with translated strings in it ? I could be very wrong: never did multi lingual software myself.
it's not just a matter of security, it's also plain not possible with the way how Nim currently works. Nim is not a scripting language where every variable is just an entry in a hash table. Things like variable names etc. are lost once compiled, it's not possible to look up a variable by it's name with an arbitrary string at compile time.
So how does fmt work then? It's a macro which operates at compile time while these information are still available. It currently only works with a string literal directly passed to the macro. Theoretically it would be possible to expand this so it could take an arbitrary constant string, though there's probably not much use for that.
The example you give with sprintf is completely different, because in this case the variables which are going to be used for the formatting are not choosen from the string, they're explicitly passed in. The format string only determines when and how they're going to be inserted into the final string.
format/% from the strutils module works just like sprintf, in fact it is a lot better suited for localisation, because it allows the formatted words to be inserted in an arbitrary order and it also allows named operands (see the documentation, which also has been already sent in here before https://nim-lang.org/docs/strutils.html#%25%2Cstring%2CopenArray%5Bstring%5D ).
it's also plain not possible with the way how Nim currently works. Nim is not a scripting language where every variable is just an entry in a hash table. Things like variable names etc. are lost once compiled, it's not possible to look up a variable by it's name with an arbitrary string at runtime time.
except that's not true, I've implemented this, see https://github.com/nim-lang/Nim/issues/18218#issuecomment-861713652
when true:
block: # D20210615T123026
var x = @[10, 2]
let y = 123
doAssert interpIt("{x} and {y}", (x, y)) == "@[10, 2] and 123" # simplest example
doAssert interpIt("{y} times 2 is {y2}", (y, y2: y*2)) == "123 times 2 is 246" # allow introducing variables on the fly
doAssert interpIt("{x} and {y} using locals") == "@[10, 2] and 123 using locals" # using locals()
var s = "foo {x}"
doAssert interpIt(s, (x, )) == "foo @[10, 2]" # works with runtime strings
doAssert interpIt(s) == "foo @[10, 2]" # ditto
doAssertRaises(ValueError): discard interpIt("foo {badvar}")
doAssertRaises(ValueError): discard interpIt("{y} {bad}", (x, )) # # substitution variable not found: bad
doAssert interpIt("{y} {bad} {bad2}", (y, ), it.toUpper) == "123 BAD BAD2" # use arbitrary expression for variables not found (via `it`)
let z = x[0].addr
doAssert interpIt("variable is {z}", (z: $z[])).tap == "variable is 10" # custom $