2010-02-25 43 views
4

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

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")|] 

其中第一个数字是小节范围的开始线索引,第二个是结束线索引。我怎么做?我正在考虑使用折叠功能,但无法创建任何东西。

回答

5

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

无论如何,解决这个问题的一个好方法是认识到你需要对数据做一些更一般的操作 - 你需要对数组的相邻元素进行分组,开始一个新的组,当你找到一行值在第一列。

我将通过添加行号到数组开始,然后将其转换为列表(通常更容易在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)。一旦你有一个很好的可重用函数集,大部分的问题都很容易:-)。

+0

非常好!这就是我需要的。谢谢。 – Max 2010-02-26 00:40:10

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")|]