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

с первым номером, являющимся начальным строковым индексом диапазона подраздела, а вторым - индексом конечной линии. Как мне это сделать? Я думал об использовании функции fold, но ничего не мог создать.

ответ

5

Насколько я знаю, не существует простой способ сделать это, но это, безусловно, хороший способ практиковать функциональные навыки программирования. Если вы использовали некоторое иерархическое представление данных (например, XML или JSON), ситуация была бы намного проще, потому что вам не пришлось бы преобразовывать структуру данных из линейных (например, list/array) в иерархическую (в этом случае список списков).

В любом случае, хороший способ подойти к проблеме состоит в том, чтобы понять, что вам нужно выполнить более общую операцию с данными - вам нужно сгруппировать смежные элементы массива, начиная новую группу, когда вы найдете строку с значение в первом столбце.

Начну, добавив номер строки в массив, а затем преобразовать его в список (который, как правило, легче работать в F #):

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

Теперь мы можем написать многоразовую функцию, группы смежных элементов списка и начинает новую группу каждый раз, когда указанный предикат возвращает ftrue:

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) 

Обратите внимание, что шаблон согласования в лямбда-функции выдаст предупреждение, но мы можем смело игнорировать это, потому что группа всегда будет непустой.

Резюме: Этот ответ показывает, что если вы хотите, чтобы написать элегантный код, вы можете реализовать многоразовую функцию высшего порядка (например, adjacentGroups), потому что не все доступно в # основных библиотеках F. Если вы используете функциональные списки, вы можете реализовать его с помощью рекурсии (для массивов вы должны использовать императивное программирование, как в ответе gradbot). Когда у вас есть хороший набор функций многократного использования, большинство проблем легко :-).

+0

Отлично! Это то, что мне нужно. Благодарю. – Max

0

Структура JSON окажется идеальной для вас; анализаторы и конвертеры уже доступны.

об этом читайте здесь: http://msdn.microsoft.com/en-us/library/bb299886.aspx

редактировать: по какой-то причине я увидел J #, возможно, она по-прежнему применяется в F # ..

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")|] 
Смежные вопросы