Thomas
Thomas

Reputation: 12107

how can I build a format string with sprintf, in F#?

I'm trying to go from:

sprintf "%3.1f" myNumber

to:

sprintf myFormatter myNumber

which is not possible

I have a situation where number precision depends on some settings, so I would like to be able to create my own formatter string.

I know it can be done with String.Format, but I am curious if there is a F# way with sprintf, or ksprinf; can it be done?

Upvotes: 3

Views: 848

Answers (1)

Abel
Abel

Reputation: 57159

Simple answer

EDIT: Diego Esmerio on F# Slack showed me a simpler way that I honestly never thought of while working out the answer below. The trick is to use PrintfFormat directly, like as follows.

// Credit: Diego. This 
let formatPrec precision = 
    PrintfFormat<float -> string,unit,string,string>(sprintf "%%1.%if" precision)

let x = 15.234

let a = sprintf (formatPrec 0) x
let b = sprintf (formatPrec 1) x
let c = sprintf (formatPrec 3) x

Output:

val formatPrec : precision:int -> PrintfFormat<(float -> string),unit,string,string>
val x : float = 15.234
val a : string = "15"
val b : string = "15.2"
val c : string = "15.234"

This approach is arguably much simpler than the Expr-based approach below. For both approaches, be careful with the formatting string, as it will compile just fine, but break at runtime if it is invalid.

Original answer (complex)

This isn't trivial to do, because functions like sprintf and printfn are compile-time special-case functions that turn your string-argument into a function (in this case of type float -> string).

There are some things you can do with kprintf, but it won't allow the formatting-argument to become a dynamic value, since the compiler still wants to type-check that.

However, using quotations we can build such function ourselves. The easy way is to create quotation from your expression and to change the parts we need to change.

The starting point is this:

> <@ sprintf "%3.1f" @>
val it : Expr<(float -> string)> =
  Let (clo1,
     Call (None, PrintFormatToString,
           [Coerce (NewObject (PrintfFormat`5, Value ("%3.1f")), PrintfFormat`4)]),
     Lambda (arg10, Application (clo1, arg10)))
...

That may look like a whole lot of mess, but since we only need to change one tiny bit, we can do this rather simply:

open Microsoft.FSharp.Quotations   // part of F#
open Microsoft.FSharp.Quotations.Patterns  // part of F#
open FSharp.Quotations.Evaluator    // NuGet package (with same name)

// this is the function that in turn will create a function dynamically
let withFormat format = 
    let expr =
        match <@ sprintf "%3.1f" @> with
        | Let(var, expr1, expr2) ->
            match expr1 with
            | Call(None, methodInfo, [Coerce(NewObject(ctor, [Value _]), mprintFormat)]) ->
                Expr.Let(var, Expr.Call(methodInfo, [Expr.Coerce(Expr.NewObject(ctor, [Expr.Value format]), mprintFormat)]), expr2)

            | _ -> failwith "oops"  // won't happen

        | _ -> failwith "oops"  // won't happen

    expr.CompileUntyped() :?> (float -> string)

To use this, we can now simply do this:

> withFormat "%1.2f" 123.4567899112233445566;;
val it : string = "123.46"

> withFormat "%1.5f" 123.4567899112233445566;;
val it : string = "123.45679"

> withFormat "%1.12f" 123.4567899112233445566;;
val it : string = "123.456789911223"

Or like this:

> let format = "%0.4ef";;
val format : string = "%0.4ef"

> withFormat format 123.4567899112233445566;;
val it : string = "1.2346e+002f"

It doesn't matter whether the format string is now a fixed string during compile time. However, if this is used in performance sensitive area, you may want to cache the resulting functions, as recompiling an expression tree is moderately expensive.

Upvotes: 5

Related Questions