F # Группировать или агрегировать последовательность / коллекцию записей по заданным критериям

#f# #functional-programming #grouping #aggregate

#f# #функциональное программирование #группировка #агрегировать

Вопрос:

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

У меня есть последовательность типов записей, скажем, как:

 type Invoice = {
    GrpText : string
    GrpRef : int
    Article : int
    Wkz : int
    Text : string
    Price : decimal
    InvoiceRef : int
}
  

Теперь я хочу сгруппировать или агрегировать последовательность Invoices по заданным критериям и, т.Е. суммировать их цены. Invoices то, что не соответствует критериям, не должно группироваться и просто возвращаться как есть.

Функция критериев может выглядеть следующим образом:

 /// determines whether to group the two given Invoice items or not
let criteria item toCompareWith =
    (item.GrpRef > 0 amp;amp; item.Article = toCompareWith.Article
        amp;amp; item.InvoiceRef = toCompareWith.InvoiceRef) ||
    (item.Wkz <> 0 amp;amp; item.Text = toCompareWith.Text)
  

Агрегирование или группировка могут выглядеть следующим образом:

 /// aggregate the given Invoice items
let combineInvoices item1 item2 =
    {item1 with Price = item1.Price   item2.Price; Wkz = 0}
  

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

Редактировать:

Я просто изменил criteria функцию, чтобы лучше показать, что она может быть немного сложнее, чем группировка по одному или нескольким полям.

Ответ №1:

Если я чего-то не упустил, есть два шага: группировать и сокращать. Самый простой способ сгруппировать Seq.groupBy . Поскольку вы хотите использовать пользовательское равенство, вам нужно либо применить [<CustomEquality>] атрибут к вашему типу и переопределить Equals и GetHashCode , либо создать собственную функцию генерации ключей, которая использует вашу концепцию равенства. Вот пример последнего.

 //custom key generator
let genKeyWith compare =
  let lookup = ResizeArray()
  fun item ->
    match Seq.tryFindIndex (compare item) lookup with
    | Some idx -> idx
    | None ->
      lookup.Add(item)
      lookup.Count - 1
  

Использование

 let getKey = genKeyWith criteria

let invoices = Seq.init 10 (fun _ -> Unchecked.defaultof<Invoice>)

invoices 
|> Seq.groupBy getKey
|> Seq.map (fun (_, items) -> Seq.reduce combineInvoices items)
  

Ответ №2:

Что-то вроде этого, где Defaultinvoice — это своего рода счет-фактура «0»

 input 
|> Seq.groupBy (fun t -> t.Article) 
|> Seq.map (fun (a,b) -> a, (b |> List.fold (fun (c,d) -> combineInvoices c d) Defaultinvoice)
  

РЕДАКТИРОВАТЬ — для более сложной функции объединения.

Поэтому, если ваша функция объединения сложнее, лучшим подходом, вероятно, является использование рекурсии, и я думаю, что будет трудно избежать решения O (n ^ 2). Я бы выбрал что-то вроде

 let rec overallfunc input =
    let rec func input (valid:ResizeArray<_>) =
        match input with
        |[] -> valid //return list
        |h::t -> 
            match valid.tryfindIndex (fun elem -> criteria h elem) with //see if we can combine with something
            |Some(index) -> valid.[index] <- combineInvoices (valid.[index]) h //very non-functional here
            |None -> valid.Add(h)
            func t valid //recurse here
    func input (ResizeArray<_>())
  

Это решение является крайне нефункциональным и, вероятно, очень медленным, но оно должно работать для произвольно сложных комбинированных функций

Комментарии:

1. Я думаю, что это идет в правильном направлении, но я думаю, что осталась одна проблема. Вы просто группируете по Article , и я не совсем уверен, где и как я могу интегрировать эту criteria функцию или еще более сложную.

2. @kongo2002 что, если вы сделаете это type comparison = {InvoiceRef:int;Article:int} , а затем добавите member x.Comp() = {InvoiceRef=x.InvoceRef;Article=x.Article в свой элемент счета-фактуры — тогда вы можете сделать groupBy (fun t -> t.Comp()) — хотя вы не можете получить сравнение groupref — возможно, для этого используйте фильтр?

3. Хорошо, тогда это будет основной вопрос, как применить фильтр к вашей описанной конструкции? 🙂 Кстати, я только что отредактировал свой первоначальный пост, чтобы лучше показать возможный характер / сложность criteria функции.