4.2.1 加载和解析数据
作为第一步,我们将实现一个函数 convertDataRow,从 CSV 文件中取一行作为字符串,把这一行拆成两部分,以元组形式返回。函数实现后,就可以立即进行测试,输入一个样本(字符串"Test reading,1234”),应该能够正确解析。清单 4.2 是函数的代码和测试结果。
清单 4.2 解析 CSV 文件的一行 (F# Interactive)
> open System;;
> let convertDataRow(csvLine:string) = [1]
let cells = List.ofseq(csvLine.Split(',')) [2]
match cells with
| title::number::_ –> <-- 可能有两个或更多单元
let parsedNumber = Int32.Parse(number)
(title, parsedNumber)
| _ -> failwith "Incorrect dataformat!" <-- 报告错误
;;
val convertDataRow : string -> string *int
> convertDataRow("Testingreading,1234");; [3]
val it : string * int = ("Testingreading", 1234)
启动 F# Interactive 以后,我们需要从 System 命名空间导入功能;只有打开这个命名空间,才能在代码中要使用 Int32.Parse 方法。这必须显式导入,但是,F# 核心库的函数,比如,List.ofseq,默认就是可用的。
函数 convertDataRow [1]取一个字符串作为参数,以逗号作为分隔符,把值拆分成列表,我们用.NET 中标准的 Split 方法执行此操作。当调用值的实例方法时,F# 编译器需要提前知道值的类型。不幸的是,在这里,类型推断没有任何其他方式来推断出类型,因此,我们要使用类型批注,显式声明 csvLine 的类型是字符串[2]。
Split 方法的声明使用 C# 的params 关键字,参数为数量可变的字符,我们只指定一个分隔符:逗号。这个方法的结果是一个字符串数组,但我们想要使用的是列表,所以,要将结果转换为列表,我们使用 F # List 模块的 ofseq 函数。我们将在第十和第十二章讨论有关数组和其他集合类型。
有了列表以后,我们就可以使用 match 结构,检查格式是否正确。如果包含两个或更多的值,将匹配第一种情况(title::number:: _),标题指定给 title、数值指定给 number,其余列(如果有)被忽略。在这个分支中,我们使用 Int32.Parse 把字符串转换为整数,返回的元组包含标题和这个值;第二个分支触发标准的 .NET 异常。
如果看到这个函数的签名,就能明白,取字符串参数,返回一个元组,第一个值是字符串,第二个值是整数。这是完全符合我们的预期:标题返回作为字符串,第二列的数值转换为整数。下一行演示了在 F# Interactive 中测试函数是如何的方便[3]。示例调用的结果是元组,包含“Testing reading” 作为标题,"1234"作为数值。
F# 处理 .NET 字符串
在 F# 中处理 .NET 字符串,一般会使用通常的 .NET 方法。我们现在就来看看如何在 F# 中使用,先选几个 String 类中的静态方法,这些方法就好像是普通的 F# 函数(使用 String 类名)。这些函数的参数值必须在括号内指定,就像是以逗号分隔的元组。在类型签名中,元组写成星号:
■ String.Concat (重载):接受可变数量的字符串或对象类型的参数值,并返回一个把所有的这些都连接起来的字符串:
> String.Concat("1 + 3", 3);;
val it : string = "1 + 33"
■ String.Join (sep:string * strs:string[]) : string:把字符串数组,即参数strs,使用指定的分隔符,即 sep 参数连接起来,可以使用 [| ... |] 语法来构建数组:
> String.Join(", ", [|"1"; "2"; "3" |]);;
val it : string = "1, 2, 3"
在.NET 中,字符串也是对象,也有实例成员,在 F# 中使用典型的点表示法。这一点,在前面的示例中,我们已经看到过,当拆分字符串时,使用了 str.Split。下面的示例假定我们有一个字符串 str 包含"Hello World!":
■ str.Length:返回字符串长度的属性,在 F# 中,访问属性的方法与 C# 相同,所以读这个属性不需要括号:
> str.Length;;
val it : int = 12
■ str.[index:int]:指定字符串的索引,写在方括号内,返回由 index 指定位置的字符。注意,在方括号的前面仍需要点,与 C# 中不同:
> str.[str.Length - 1];;
val it : char = '!'
我们还可以使用 FSharp.PowerPack.dll 库中的函数;F# 中大多数字符串处理代码可以使用 .NET 方法来实现。
在前面的清单中,我们实现了 convertDataRow 函数,取 CSV 文件中的一行字符串作为参数,返回元组,包含标签和数值。下一步,我们将实现一个函数,参数为字符串列表,使用 convertDataRow,将每个字符串转换为元组,清单 4.3 就是这个函数;然后,立即进行测试,解析一个字符串列表样本。
清单 4.3 解析输入文件的多行 (F# Interactive)
> let rec processLines(lines) =
matchlines with
| [] -> [] [1]
|currentLine::remaining -> [2]
letparsedLine = convertDataRow(currentLine) <-- 处理列表头
letparsedRest = processLines(remaining) <-- 递归处理列表尾
parsedLine :: parsedRest
;;
val processLines : string list ->(string * int) list
> let testData = processLines["Test1,123"; "Test2,456"];; [3]
val testData : (string * int) list =[("Test1", 123); ("Test2", 456)]
这个函数在许多方面与我们在前一章实现的列表处理函数相似。可以看到,声明这个函数使用了 let rec 关键字,因此,它是递归的。它的参数(lines)为字符串列表,使用模式匹配,检查列表是空列表,还是cons cell。对于空列表,直接返回元组的空列表[1];如果模式匹配执行的分支是针对 cons cell 的[2],会把列表中的第一个元素的值指定给 currentLine,列表的其余元素指定给 remainning。此分支的代码首先处理一行,使用清单 4.2 的 convertDataRow 函数,然后,以递归方式处理列表中的其余部分。最后,代码构造一个新的cons cell,包含:处理过的行作为列表头,递归处理的列表其余部分作为列表尾。这样,函数对列表中的每个字符串,执行 convertDataRow,把结果收集成新的列表。
为更好地理解 processLines 函数做了什么,我们还可以通过 F# Interactive,查看输出的类型签名,函数的参数为字符串列表(string list 类型),返回为包含类型为元组(strings * int)的列表。这正是解析一行的函数所返回的类型,因此,函数做的很正确。我们通过调用样本列表作为参数值[3],进行验证。可以看到,由 F# Interactive 输出的结果:是包含两个元组的列表,元组有一个字符串和一个数字,所以,函数运行良好。
现在,我们有了一个函数,将字符串列表转换为将在图表绘制应用程序中使用的数据结构。我们在实现关键的数据处理部分之前,还需要看一个简单的工具程序。