Swift CLFormat

Platforms: macOS, iOS, Linux Language: Swift 5.7 IDE: Xcode 14 Package managers: SwiftPM, Carthage License: Apache

该框架从头开始在 Swift 中实现了 Common Lisp 的 format 过程format 是一个使用类似于 printf 的格式字符串生成格式化文本的过程。 与 printf 相比,这种格式化形式更加富有表现力。 它允许用户以各种格式(例如十六进制、二进制、八进制、罗马数字、自然语言)显示数字,应用条件格式化,以表格格式输出文本,迭代数据结构,甚至递归应用 format 来处理包含其自身首选格式化字符串的数据。

该框架的文档包括

以下是一些示例,可快速了解clformat:

clformat("~D message~:P received. Average latency: ~,2Fms.", args: 17, 4.2567)
 "17 messages received. Average latency: 4.26ms."
clformat("~D file~:P ~A. Average latency: ~,2Fms.", args: 1, "stored", 68.1)
 "1 file stored. Average latency: 68.10ms."

API

clformat 和 clprintf

框架 CLFormat 提供的主要格式化过程是 clformat。 它具有以下签名

func clformat(_ control: String,
              config: CLControlParserConfig? = CLControlParserConfig.default,
              locale: Locale? = nil,
              tabsize: Int = 4,
              linewidth: Int = 80,
              args: Any?...) throws -> String

control 是格式化字符串。 它使用下一节中描述的格式化语言来定义输出的格式。 config 指的是格式配置,它决定了如何解析和解释控制字符串和参数。 除非用户想要定义自己的控制格式化语言,否则通常会省略此参数。 locale 指的是一个 Locale 对象,用于执行特定于语言环境的指令。 tabsize 定义了单个制表符对应的最大空格字符数。 linewidth 指定每行字符数(仅由对齐指令使用)。 最后,args 指的是为格式化过程提供的参数序列。 控制字符串确定如何将这些参数注入到函数 clformat 返回的最终输出中。 这是一个例子

try clformat("~A is ~D year~:P old.", args: "John", 32)
 "John is 32 years old."
try clformat("~A is ~D year~:P old.", args: "Vicky", 1)
 "Vicky is 1 year old."

还有一个 重载变体clformat,它支持作为数组提供的参数。 它在其他方面与第一个变体等效。

func clformat(_ control: String,
              config: CLControlParserConfig? = CLControlParserConfig.default,
              locale: Locale? = nil,
              tabsize: Int = 4,
              linewidth: Int = 80,
              arguments: [Any?]) throws -> String

最后,还有一个 重载函数 clprintf,它将格式化字符串直接打印到标准输出端口。 通过 terminator 参数,可以控制是否自动添加换行符。

func clprintf(_ control: String,
              config: CLControlParserConfig? = CLControlParserConfig.default,
              locale: Locale? = nil,
              tabsize: Int = 4,
              linewidth: Int = 80,
              args: Any?...,
              terminator: String = "\n") throws
func clprintf(_ control: String,
              config: CLControlParserConfig? = CLControlParserConfig.default,
              locale: Locale? = nil,
              tabsize: Int = 4,
              linewidth: Int = 80,
              arguments: [Any?],
              terminator: String = "\n") throws

请注意,默认情况下,clformatclprintf 都使用 CLControlParserConfig.default 指定的格式化指令。 这是一个可变的解析器配置,可用于影响所有不提供自己解析器配置的 clformatclprintf 的调用。

字符串扩展

类似于 printf 如何集成到 Swift 的 String API 中,框架 CLFormat 提供了两个新的 String 初始化器,它们利用了 CLFormat 格式化机制。 它们可以与 clformat 互换使用,以实现更面向对象的风格,而不是 clformat 的过程性质。

extension String {
  init(control: String,
       config: CLControlParserConfig? = nil,
       locale: Locale? = nil,
       tabsize: Int = 4,
       linewidth: Int = 80,
       args: Any?...) throws
  init(control: String,
       config: CLControlParserConfig? = nil,
       locale: Locale? = nil,
       tabsize: Int = 4,
       linewidth: Int = 80,
       arguments: [Any?]) throws
}

优化重复格式化

每次调用 clformat 时,控制语言解析器都会将控制字符串转换为更易于处理的中间格式。 如果程序反复使用控制字符串,则最好只将控制字符串转换为中间格式一次,并在每次应用新的参数列表时重复使用它。 以下代码展示了如何做到这一点。 结构体 CLControl 的值表示给定控制字符串的中间格式。

let control = try CLControl(string: "~A = ~,2F (time: ~4,1,,,'0Fms)")
let values: [[Any?]] = [["Stage 1", 317.452, 12.7],
                        ["Stage 2", 570.159, 41.2],
                        ["Stage 3", 123.745, 9.4]]
for args in values {
  print(try control.format(arguments: args))
}

这是生成的输出

Stage 1 = 317.45 (time: 12.7ms)
Stage 2 = 570.16 (time: 41.2ms)
Stage 3 = 123.74 (time: 09.4ms)

格式化语言

clformat 和相关函数及构造函数使用的控制字符串由逐字复制到输出中的字符以及 格式化指令 组成。 所有格式化指令都以波浪号 (~) 开头,并以标识指令类型的单个字符结尾。 指令可以带有前缀参数,紧跟在波浪号字符之后,以逗号分隔。 整数和字符都允许作为参数。 它们后面可以跟格式化修饰符 :@+。 这是格式化指令的一般格式

~param1,param2,...mX

where m = (potentially empty) sequence of modifier characters ":", "@", and "+"
      X = character identifying the directive type

此语法以 BNF 形式正式描述了指令的语法

<directive>  ::= "~" <modifiers> <char>
               | "~" <parameters> <modifiers> <char>
<modifiers>  ::= <empty>
               | ":" <modifiers>
               | "@" <modifiers>
               | "+" <modifiers>
<parameters> ::= <parameter>
               | <parameter> "," <parameters>
<parameter>  ::= <empty>
               | "#"
               | "v"
               | <number>
               | "-" <number>
               | <character>
<number>     ::= <digit>
               | <digit> <number>
<digit>      ::= "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
<character>  ::= "'" <char>

以下部分介绍了一些指令,并解释了如何组合指令以构建定义富有表现力的格式化指令的控制字符串。

简单指令

这是一个简单的控制字符串,它通过指令 ~A 注入参数的可读描述

"I received ~A as a response"

指令 ~A 指的是编译格式化输出时提供给 clformat下一个参数

clformat("I received ~A as a response", args: "nothing")
 "I received nothing as a response"
clformat("I received ~A as a response", args: "a long email")
 "I received a long email as a response"

指令 ~A 可以给出参数来影响格式化输出。 ~A 指令的第一个参数定义了最小长度。 如果下一个参数的文本表示形式的长度小于最小长度,则会插入填充字符

clformat("|Name: ~10A|Location: ~13A|", args: "Smith", "New York")
 "|Name: Smith     |Location: New York     |"
clformat("|Name: ~10A|Location: ~13A|", args: "Williams", "San Francisco")
 "|Name: Williams  |Location: San Francisco|"
clformat("|Name: ~10,,,'_@A|Location: ~13,,,'-A|", args: "Garcia", "Los Angeles")
 "|Name: ____Garcia|Location: Los Angeles--|"

上面的第三个示例使用了多个参数,并且在一种情况下,包含了一个 @ 修饰符。 指令 ~13,,,'-A 定义了第一个和第四个参数。 第二个和第三个参数被省略,因此使用默认值。 第四个参数定义了填充字符。 如果在参数列表中使用字符文字,则它们以引号 ' 为前缀。 指令 ~10,,,'_@A 包括一个 @ 修饰符,这将导致输出在左侧填充。

可以从参数列表中注入参数。 以下示例展示了如何使用参数 v 来为具有可配置小数位数的浮点数设置格式。

clformat("length = ~,vF", args: 2, Double.pi)
 "length = 3.14"
clformat("length = ~,vF", args: 4, Double.pi)
 "length = 3.1416"

这里 v 用作固定浮点指令 ~F 的第二个参数,指示小数位数。 它指的是下一个提供的参数(在上面的示例中为 2 或 4)。

复合指令

下一个示例展示了如何通过使用 # 作为参数值来引用格式化过程中尚未消耗的参数总数。

clformat("~A left for formatting: ~#[none~;one~;two~:;many~].",
         args: "Arguments", "eins", 2)
 "Arguments left for formatting: two."
clformat("~A left for formatting: ~#[none~;one~;two~:;many~].",
         args: "Arguments")
 "Arguments left for formatting: none."
clformat("~A left for formatting: ~#[none~;one~;two~:;many~].",
         args: "Arguments", "eins", 2, "drei", "vier")
 "Arguments left for formatting: many."

在这些示例中,使用了条件指令 ~[。 它后面跟着用指令 ~; 分隔的子句,直到到达 ~]。 因此,上面的示例中有四个子句:noneonetwomany~[ 指令前面的参数确定输出哪个子句。 所有其他子句都将被丢弃。 例如,~1[zero~;one~;two~:;many~] 将输出 one,因为选择了子句 1(这是第二个子句,因为编号从零开始)。 最后一个子句很特殊,因为它使用 : 修饰符以 ~; 指令为前缀:这是一个默认子句,当没有其他子句适用时选择它。 因此,~8[zero~;one~;two~:;many~] 输出 many。 这也解释了上面的示例是如何工作的:这里的 # 指的是仍然可用的参数数量,并且这个数字驱动了在此指令中返回的内容:~#[...~]

另一个强大的复合指令是迭代指令 ~{。 通过此指令,可以迭代序列的所有元素。 只要序列中还有剩余元素(作为参数提供),~{~} 之间的控制字符串就会重复。 例如,应用于参数 ["one", "two", "three"]Numbers:~{ ~A~} 会产生输出 Numbers: one two three~{~} 之间的控制字符串也可以消耗序列的多个元素。 因此,应用于参数 ["one", 1, "two", 2]Numbers:~{ ~A=>~A~} 输出 Numbers: one=>1 two=>2

当然,也可以嵌套任意复合指令。 这是一个控制字符串的示例,该控制字符串使用迭代和条件指令的组合来输出以逗号分隔的序列元素:(~{~#[~;~A~:;~A, ~]~})。 当此控制字符串与参数 ["one", "two", "three"] 一起使用时,将生成以下格式化输出:(one, two, three)

格式化指令参考

CLFormat 框架支持的格式化指令基于 Guy L. Steele Jr. 的 Common Lisp the Language, 2nd Edition 中指定的指令。 一些指令已得到扩展,以满足当今的格式化要求(例如,支持本地化)并在 Swift 中实现自然嵌入。 所有扩展的引入方式都不会影响向后兼容性。

命令行工具

swift-clformat框架还包含一个命令行工具,用于试验clformat控制字符串。 它提示用户首先输入控制字符串,然后输入参数列表。 上面描述了控制字符串的语法。 参数列表以类似于 Swift 中字面量语法的语法输入。 以下是与命令行工具交互的示例

═════════╪═══════════════════════════
  CONTROL Done.~^ ~D warning~:P.~^ ~D error~:P.
ARGUMENTS 1, 5
─────────┤
   RESULT Done. 1 warning. 5 errors.
═════════╪═══════════════════════════
  CONTROL ~%;; ~{~<~%;; ~1,30:; ~S~>~^,~}.~%
ARGUMENTS ["first line", "second", "a long third line", "fourth"]
─────────┤
   RESULT 
          ;;  "first line", "second",
          ;;  "a long third line",
          ;;  "fourth".
          
═════════╪═══════════════════════════
  CONTROL ~:{/~S~^ ...~}
ARGUMENTS [["hot", "dog"], ["hamburger"], ["ice", "cream"]]
─────────┤
   RESULT /"hot" .../"hamburger"/"ice" ...
═════════╪═══════════════════════════

要求

构建 CLFormat 框架需要以下技术。 库和命令行工具都可以使用 XcodeSwift Package Manager 构建。

版权

作者:Matthias Zenger (matthias@objecthub.com)
版权所有 © 2023 Matthias Zenger。 保留所有权利。