2010-02-25 74 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")|]