Reputation: 1024
I am fairly new to functional programming and have some problems with a list processing task. I have a collection of records like the following:
type TestRec = {
Id : string
Amount : int }
Now I want to remove all items in the list that build a pair with each other. For example if there are two items with the Amount
of 7
and -7
both items should be removed from the list. If there is a third element with Amount = 7
it should stay in the list.
I hope you guys can understand what I am trying to do. This is what I came up with so far (but it does not work correctly yet):
let removeDoubles items =
items
|> Seq.groupBy (fun i -> Math.Abs(i.Amount))
|> Seq.map snd
|> Seq.filter (fun i -> Seq.length i = 1)
Edit:
The function that determines if two elements match with each other might be more complex than described above (Math.Abs
). I thought it would be a good example with the Amount
values but it could be any predicate function.
Edit 2: In order to clarify some more I want to give a more realistic description of a possible related problem. You can imagine the calculation of an invoice where the list consists of all invoice positions. Now you want to remove all pairs of invoice positions that have for example the same 'article number', 'currency' and the prices evaluate to zero.
Maybe this example helps to explain my problem. I just thought there might be a more 'functional way' of solving this problem than having two loops running over the list and removing the elements like I would do in an imperative language.
Upvotes: 4
Views: 1213
Reputation: 47904
(Updated, based on your comment)
If you want to remove consecutive negating pairs, you can simply do:
let removePairs f items =
let rec loop acc = function
| a :: b :: t when f a b -> loop acc t
| h :: t -> loop (h::acc) t
| [] -> List.rev acc
loop [] (List.ofSeq items)
items |> removePairs (fun {Amount=amtA} {Amount=amtB} -> amtA + amtB = 0)
Upvotes: 0
Reputation: 13862
To abstract the idea of opposing pairs away, I define two functions. One for relational equality (related) and one to define cancellations (opposite).
The function works by first grouping the related objects together and then partitioning them into opposing arrays. These resulting arrays are then sliced according to the number of required cancellations. Finally everything is concated together.
type TestRec = {
Id : string;
Amount : int;
}
let removeDoubles items related opposite =
items
|> Seq.groupBy related
|> Seq.map (fun (key, values) ->
let t, f = values |> Seq.toArray |> Array.partition opposite
if t.Length > f.Length then
t.[.. t.Length - f.Length - 1]
else
f.[.. f.Length - t.Length - 1]
)
|> Seq.concat
let items = [
{Id="first";Amount=7};
{Id="seconds";Amount=7};
{Id="we";Amount=4};
{Id="negative";Amount= -7}
]
let test = removeDoubles
items
(fun x -> abs x.Amount)
(fun x -> x.Amount > 0)
printf "%A" test
System.Console.ReadLine() |> ignore
Outputs
seq [{Id = "first";
Amount = 7;}; {Id = "we";
Amount = 4;}]
Upvotes: 1
Reputation: 16782
Another option, probably not so functional as other proposals, but should be efficient:
type R = {
Id : int
Amount : int
}
let mkR id amount = {Id = id; Amount = amount}
open System.Collections.Generic
let removePairs s : seq<R> =
// stores mapping: key -> corresponding nodes in result list
let seen = Dictionary<_, LinkedList<LinkedListNode<R>>>()
// accumulates result
let list = LinkedList<R>()
// check if paired element was already met
// if yes - remove its first appearance from the result list
let tryEliminate ({Amount = a} as el) =
// paired element will have different sign
let key = -a
match seen.TryGetValue key with
| true, existing ->
list.Remove(existing.First.Value) // Remove(LinkedListNode) works in O(1)
existing.RemoveFirst()
if existing.Count = 0 then seen.Remove key |> ignore
true
| _ ->
false
let append ({Amount = a} as el) =
let newNode = list.AddLast(el)
let l =
match seen.TryGetValue a with
| true, existing -> existing
| false, _ ->
let nodes = LinkedList()
seen.Add(a, nodes)
nodes
l.AddLast(newNode) |> ignore
for el in s do
if not (tryEliminate el) then append el
list :> _
let check source expected =
let result =
source
|> List.mapi (fun i x -> {Id = i; Amount = x})
|> removePairs
|> List.ofSeq
if (result <> expected) then failwithf "Expected: %A, actual %A" expected result
check [1;1;-1;2] [mkR 1 1; mkR 3 2]
check [1;1;-1;-1] []
check [7;7;4] [mkR 0 7; mkR 1 7; mkR 2 4]
Upvotes: 0