В качестве первого шага, я бы, вероятно, использовал Seq.groupBy
, чтобы сгруппировать предметы в единицы с той же парой людей, что и кредитором или должником, в любом порядке.Таким образом, вы получите список списков транзакций, но все это делается за один шаг O (N).Т.е.,
let grouped = transactions |> Seq.groupBy (fun t ->
let c, d = t.Creditor, t.Debitor
if c < d then c, d else d, c
)
Теперь у вас есть последовательность, которая выглядит примерно так (в псевдокодовой смеси кода и английского):
[
(("alessio", "luca"), [luca gave alessio 10; alessio gave luca 7])
(("alessio", "giulia"), [alessio gave giulia 12])
]
Вывод Seq.groupBy
равенпоследовательность из 2-х кортежей;формат каждого 2-го кортежа (группа, элементы).Здесь сама группа представляет собой 2-кортеж (name1, name2), поэтому вложенная структура данных ((name1, name2), транзакции).
Теперь для каждого списка переходов выЯ захочу сложить сумму, при этом некоторые транзакции будут считаться «положительными», а некоторые - «отрицательными» в зависимости от того, совпадают ли они с порядком (name1, name2) или наоборот.То есть в первом списке транзакций те, в которых Алессио заплатил Лука, будут считаться положительными, а те, в которых Лука заплатил Алессио, будут считаться отрицательными.Сложите все эти значения, и если разница будет положительной, то отношение дебитор-кредитор будет следующим: «имя1 должно деньги имени2», в противном случае все наоборот.Например:
let result = grouped |> Seq.map (fun ((name1, name2), transactions) ->
let spendTotal = transactions |> Seq.sumBy (fun t ->
let mult = if t.Debitor = name1 then +1.0 else -1.0
t.Spend * mult
)
let c, d = if spendTotal > 0.0 then name1, name2 else name2, name1
{ Activity = "_aggregate_"
Creditor = c
Debitor = d
Spend = spendTotal }
)
Теперь ваша последовательность выглядит примерно так:
[
(("alessio", "luca"), luca gave alessio 3 net)
(("alessio", "giulia"), alessio gave giulia 12 net)
]
Теперь мы хотим отбросить имена групп (пары (name1, name2)) и взять тольковторая часть каждого кортежа в последовательности.(Помните, что всеобъемлющей структурой последовательности является (group, transactions)
. F # имеет вспомогательную функцию, называемую snd
, для получения второго элемента 2-кортежа. Поэтому следующий шаг в цепочке просто:
let finalResult = result |> Seq.map snd
Собрав все части вместе, код будет выглядеть следующим образом, если он расположен в одном конвейере без промежуточных шагов:
let finalResult =
transactions
|> Seq.groupBy (fun t ->
let c, d = t.Creditor, t.Debitor
if c < d then c, d else d, c )
|> Seq.map (fun ((name1, name2), transactions) ->
let spendTotal = transactions |> Seq.sumBy (fun t ->
let mult = if t.Debitor = name1 then +1.0 else -1.0
t.Spend * mult
)
let c, d = if spendTotal > 0.0 then name2, name1 else name1, name2
{ Activity = "_aggregate_"
Creditor = c
Debitor = d
Spend = spendTotal }
|> Seq.map snd
ПРИМЕЧАНИЕ. Так как вы запросили «правильный функциональный способ сделать это»,Я написал это, используя синтаксис записи F # для ваших объектов данных. Записи F # предоставляют множество полезных функциональных возможностей по умолчанию, которые вы не получаете с классами, например, с уже написанными для вас функциями сравнения и хэш-кода. Плюс записи неизменяемыпосле создания, вам никогда не придется беспокоиться о параллелизме в многопоточной среде: если у вас есть ссылка на запись, никакой другой код не изменит ее из-под вас без предупреждения. Однако, если вы используете классы, тогдасинтаксис для создания класса будет другим.
ПРИМЕЧАНИЕ 2. Я толькооколо 90% уверены, что я получил правильный порядок кредитора / дебитора по всему коду.Протестируйте этот код, и если окажется, что я поменял их местами, поменяйте местами соответствующие части (например, строку let c, d = ...
) моего кода.
Надеюсь, что это пошаговое построение решения поможетВы лучше понимаете, что делает код, и как делать вещи в правильном функциональном стиле.