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

3

I'm trying to go from:

sprintf "%3.1f" myNumber

to:

sprintf myFormatter myNumber

这是不可能的。

我的情况是数字精度取决于一些设置,因此我想能够创建自己的格式化字符串。

我知道可以使用String.Format来实现,但我想知道是否有F#的方式来使用sprintf或ksprinf来完成,是否可行?

1个回答

5

简短回答

编辑:在 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"

无论格式字符串在编译时是否为固定字符串,都没有关系。但是,如果在性能敏感的区域使用它,则需要缓存生成的函数,因为重新编译表达式树的代价是适度昂贵的。

1
我完全不知道引号还有这个用法!我会阅读相关资料来理解你的回答!谢谢你展示给我!与此同时,我会使用你编辑的简化解决方案。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接