解析分层CSV的功能方法

时间:2010-02-25 23:02:34

标签: f# functional-programming

我正在尝试创建一段代码,但无法使其正常工作。我能想到的最简单的例子是解析一些CSV文件。 假设我们有一个CVS文件,但数据以某种层次结构组织在其中。像这样:

Section1;
        ;Section1.1
        ;Section1.2
        ;Section1.3
Section2;
        ;Section2.1
        ;Section2.2
        ;Section2.3
        ;Section2.4

我这样做了:

let input = 
"a;
;a1
;a2
;a3
b;
;b1
;b2
;b3
;b4
;b5
c;
;c1"

let lines = input.Split('\n') 
let data = lines |> Array.map (fun l -> l.Split(';'))

let sections = 
  data 
  |> Array.mapi (fun i l -> (i, l.[0])) 
  |> Array.filter (fun (i, s) -> s <> "")

我得到了

val sections : (int * string) [] = [|(0, "a"); (4, "b"); (10, "c")|]

现在我想为每个部分创建一个行索引范围列表,如下所示:

[|(1, 3, "a"); (5, 9, "b"); (11, 11, "c")|]

第一个数字是子部分范围的起始行索引,第二个数字是结束行索引。我怎么做?我正在考虑使用折叠功能,但无法创建任何东西。

3 个答案:

答案 0 :(得分:5)

据我所知,没有简单的方法可以做到这一点,但这绝对是练习函数式编程技巧的好方法。如果你使用了一些数据的层次表示(例如XML或JSON),情况会容易得多,因为你不必将数据结构从线性(例如列表/数组)转换为层次结构(在这种情况下,列表清单。)

无论如何,解决问题的一个好方法是意识到你需要对数据进行更一般的操作 - 你需要对数组的相邻元素进行分组,当你找到一个有值的行时启动一个新的组在第一栏。

我首先在数组中添加一个行号,然后将其转换为list(通常在F#中更容易使用):

let data = lines |> Array.mapi (fun i l -> 
  i, l.Split(';')) |> List.ofSeq

现在,我们可以编写一个可重用的函数,该函数对列表的相邻元素进行分组,并在每次指定的谓词f返回true时启动一个新组:

let adjacentGroups f list =
  // Utility function that accumulates the elements of the current 
  // group in 'current' and stores all groups in 'all'. The parameter
  // 'list' is the remainder of the list to be processed
  let rec adjacentGroupsUtil current all list =
    match list with
    // Finished processing - return all groups
    | [] -> List.rev (current::all) 
    // Start a new group, add current to the list
    | x::xs when f(x) -> 
      adjacentGroupsUtil [x] (current::all) xs
    // Add element to the current group
    | x::xs ->
      adjacentGroupsUtil (x::current) all xs

  // Call utility function, drop all empty groups and
  // reverse elements of each group (because they are
  // collected in a reversed order)
  adjacentGroupsUtil [] [] list
    |> List.filter (fun l -> l <> [])
    |> List.map List.rev

现在,实现您的特定算法相对容易。我们首先需要对元素进行分组,每次第一列都有一些值时启动一个新组:

let groups = data |> adjacentGroups (fun (ln, cells) -> cells.[0] <> "")

在第二步中,我们需要为每个组进行一些处理。我们采用它的第一个元素(并选择组的标题),然后在剩余的元素中找到最小和最大的行号:

groups |> List.map (fun ((_, firstCols)::lines) ->
  let lineNums = lines |> List.map fst
  firstCols.[0], List.min lineNums, List.max lineNums )

请注意,lambda函数中的模式匹配会给出警告,但我们可以放心地忽略它,因为该组将始终为非空。

摘要:这个答案表明,如果您想编写优雅的代码,您可以实现可重用的高阶函数(例如adjacentGroups),因为并非F#中的所有内容都可用核心图书馆。如果使用函数列表,则可以使用递归来实现它(对于数组,您将使用命令式编程,如 gradbot 的答案)。一旦你有了一套好的可重用函数,大多数问题都很简单: - )。

答案 1 :(得分:1)

通常,当您只使用数组时,您会强迫自己使用可变和命令式样式代码。我制作了一个通用的Array.splitBy函数来将不同的部分组合在一起。如果您要编写自己的解析器,那么我建议使用List和其他高级构造。

module Question
open System

let splitArrayBy f (array:_[]) =
    [|
        let i = ref 0
        let start = ref 0
        let last = ref [||]

        while !i < array.Length do
            if f array.[!i] then
                yield !last, array.[!start .. !i - 1]
                last := array.[!i]
                start := !i + 1

            i := !i + 1

        if !start <> !i then
            yield !last, array.[!start .. !i - 1]
    |]

let input = "a;\n;a1\n;a2\n;a3\nb;\n;b1\n;b2\n;b3\n;b4\n;b5\nc;\n;c1"
let lines = input.Split('\n') 
let data = lines |> Array.map (fun l -> l.Split(';'))
let result = data |> splitArrayBy (fun s -> s.[0] <> "")

Array.iter (printfn "%A") result

将输出以下内容。

([||], [||])
([|"a"; ""|], [|[|""; "a1"|]; [|""; "a2"|]; [|""; "a3"|]|])
([|"b"; ""|], [|[|""; "b1"|]; [|""; "b2"|]; [|""; "b3"|]; [|""; "b4"|]; [|""; "b5"|]|])
([|"c"; ""|], [|[|""; "c1"|]|])

以上是对上面的一个小修改,以产生示例输出。

let splitArrayBy f (array:_[][]) =
    [|
        let i = ref 0
        let start = ref 0
        let last = ref ""
        while !i < array.Length do
            if f array.[!i] then
                if !i <> 0 then
                    yield !start, !i - 1, !last
                last := array.[!i].[0]
                start := !i + 1
            i := !i + 1
        if !start <> !i then
            yield !start, !i - 1, !last
    |]

let input = "a;\n;a1\n;a2\n;a3\nb;\n;b1\n;b2\n;b3\n;b4\n;b5\nc;\n;c1"
let lines = input.Split('\n') 
let data = lines |> Array.map (fun l -> l.Split(';'))
let result = data |> splitArrayBy (fun s -> s.[0] <> "")

(printfn "%A") result

输出

[|(1, 3, "a"); (5, 9, "b"); (11, 11, "c")|]

答案 2 :(得分:0)

JSON结构似乎是您的理想选择;解析器和转换器已经可用。

在此处阅读:http://msdn.microsoft.com/en-us/library/bb299886.aspx

编辑:由于某种原因,我看到了j#,也许它仍适用于f#..