如何在 F# 中使用 sprintf 构建格式字符串?

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

我正在尝试从:

sprintf "%3.1f" myNumber

至:

sprintf myFormatter myNumber

这是不可能的

我遇到数字精度取决于某些设置的情况,因此我希望能够创建自己的格式化程序字符串。

我知道可以用 String.Format 来完成,但我很好奇是否有使用 sprintf 或 ksprinf 的 F# 方式;可以吗?

简单回答

编辑:F# Slack 上的 Diego Esmerio 向我展示了一种更简单的方法,老实说,我在得出下面的答案时从未想过。诀窍是直接使用PrintfFormat,如下所示。

// 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

输出:

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"

这种方法可以说比下面基于 Expr 的方法简单得多。对于这两种方法,请注意格式化字符串,因为它可以正常编译,但如果无效则在运行时中断。

原始答案(复杂)

这不是一件容易的事,因为像 sprintfprintfn 这样的函数是编译时的特殊情况函数,可以将您的字符串参数转换为函数(在这种情况下是类型float -> string).

您可以使用 kprintf 做一些事情,但它不允许格式化参数成为动态值,因为编译器仍然需要进行类型检查。

然而,使用引号我们可以自己构建这样的函数。简单的方法是从您的表达式创建引用并更改我们需要更改的部分。

起点是这样的:

> <@ 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)))
...

这看起来可能一团糟,但由于我们只需要改变一点点,我们可以相当简单地做到这一点:

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)

要使用它,我们现在可以简单地这样做:

> 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"

或者像这样:

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

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

格式字符串现在在编译期间是否为固定字符串并不重要。但是,如果这用于性能敏感区域,您可能希望缓存结果函数,因为重新编译表达式树的成本适中。