我有一个类似这样的字符串"3,4\r\n"
,我想将它们转换成一个元组,即(3,4)
。
我们如何在 SML 中实现这一点?
我得到一个字符串值的原因是因为我正在读取一个返回类似字符串的文件。
您需要一个简单的解析器来实现这一点。库中已经提供了一个解析整数的适当函数Int.scan
(以及其他类型的朋友),但您必须自己编写其余的函数。例如:
(* scanLine : (char, 's) StringCvt.reader -> (int * int, 's) StringCvt.reader *)
fun scanLine getc stream =
case Int.scan StringCvt.DEC getc stream
of NONE => NONE
| SOME (x1, stream') =>
case getc stream'
of NONE => NONE
| SOME (c1, stream'') =>
if c1 <> #"," then NONE else
case Int.scan StringCvt.DEC getc stream''
of NONE => NONE
| SOME (x2, stream''') =>
case getc stream'''
of NONE => NONE
| SOME (c2, stream'''') =>
if c2 <> #"\n" then NONE else
SOME ((x1, x2), stream'''')
然后,解析所有行:
(* scanList : ((char, 's) StringCvt.reader -> ('a, 's) StringCvt.reader) -> (char, 's) StringCvt.reader -> ('a list, 's) StringCvt.reader *)
fun scanList scanElem getc stream =
case scanElem getc stream
of NONE => SOME ([], stream)
| SOME (x, stream') =>
case scanList scanElem getc stream'
of NONE => NONE
| SOME (xs, stream'') => SOME (x::xs, stream'')
要使用它,例如:
val test = "4,5\n2,3\n"
val result = StringCvt.scanString (scanList scanLine) test
(* val result : (int * int) list = [(4, 5), (2, 3)] *)
如您所见,代码有点重复。要摆脱选项类型的所有匹配,您可以编写一些基本的解析器组合器:
(* scanCharExpect : char -> (char, 's) StringCvt.reader -> (char, 's) StringCvt.reader *)
fun scanCharExpect expect getc stream =
case getc stream
of NONE => NONE
| SOME (c, stream') =>
if c = expect then SOME (c, stream') else NONE
(* scanSeq : ((char, 's) StringCvt.reader -> ('a, 's) StringCvt.reader) * ((char, 's) StringCvt.reader -> ('b, 's) StringCvt.reader) -> (char, 's) StringCvt.reader -> ('a * 'b, 's) StringCvt.reader *)
fun scanSeq (scan1, scan2) getc stream =
case scan1 getc stream
of NONE => NONE
| SOME (x1, stream') =>
case scan2 getc stream'
of NONE => NONE
| SOME (x2, stream'') => SOME ((x1, x2), stream'')
fun scanSeqL (scan1, scan2) getc stream =
Option.map (fn ((x, _), stream) => (x, stream)) (scanSeq (scan1, scan2) getc stream)
fun scanSeqR (scan1, scan2) getc stream =
Option.map (fn ((_, x), stream) => (x, stream)) (scanSeq (scan1, scan2) getc stream)
(* scanLine : (char, 's) StringCvt.reader -> (int * int, 's) StringCvt.reader *)
fun scanLine getc stream =
scanSeq (
scanSeqL (Int.scan StringCvt.DEC, scanCharExpect #","),
scanSeqL (Int.scan StringCvt.DEC, scanCharExpect #"\n")
) getc stream
您可以沿着这些思路构建更多很酷的抽象,尤其是在定义自己的中缀运算符时。但我会把它留在那里。
您可能还想处理标记之间的空白。阅读器在库StringCvt.skipWS
中很容易获得,只需将其插入正确的位置即可。
以下是如何做到这一点的粗略示例
fun toPair s =
let
val s' = String.substring(s, 0, size s-2)
in
List.mapPartial Int.fromString (String.tokens (fn c => c = #",") s')
end
但是请注意, mapPartial 会丢弃任何无法转换为整数的内容(Int.fromString
返回时NONE
),并且假定字符串始终包含\r\n
,因为通过获取子字符串删除了最后两个字符。
更新
显然,罗斯伯格的答案是正确的做法。然而,根据手头的任务,这仍然可以作为一个快速而愚蠢的方法的例子。
这是从字符串中提取所有无符号整数并将它们返回到列表中的简单方法(将列表转换为元组留给读者练习)。
fun ints_from_str str =
List.mapPartial
Int.fromString
(String.tokens (not o Char.isDigit) str);
ints_from_str " foo 1, bar:22? and 333___ ";
(* val it = [1,22,333] : int list *)
以下应该实现这一点。
exception MyError
fun convert(s) =
case String.explode(s) of
x::','::y::_ => (x,y)
| _ => raise MyError
PS - 在工作中无法使用 SML 解释器。所以可能需要稍作改动。