Функциональный подход к разбору иерархических CSV - PullRequest
4 голосов
/ 26 февраля 2010

Я пытаюсь создать кусок кода, но не могу заставить его работать. Самый простой пример, который я могу вспомнить, - это анализ файла 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 ]

5 голосов
/ 26 февраля 2010

Насколько я знаю, не существует простого способа сделать это, но это, безусловно, хороший способ практиковать навыки функционального программирования. Если бы вы использовали какое-то иерархическое представление данных (например, 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 )

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

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

1 голос
/ 26 февраля 2010

В общем, когда вы работаете только с массивами, вы заставляете себя использовать изменяемый и обязательный код стиля. Я сделал универсальную функцию 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")|]
0 голосов
/ 26 февраля 2010

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

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

edit: по какой-то причине я видел j #, возможно, он все еще применяется в f # ..

...