F#迭代对象序列并根据属性有条件地聚合

时间:2018-11-25 22:18:38

标签: f# aggregate

我能够用C#进行此练习,但是我无法在F#中进行复制。我有以下TransactionFs类型的序列:

    type TransactionFs(Debitor: string, Activity:string, Spend:float, Creditor:string)  = 
        member this.Debitor = Debitor
        member this.Activity = Activity
        member this.Spend = Spend
        member this.Creditor = Creditor

序列:

    [FSI_0003+TransactionFs {Activity = "someActivity1";
                         Creditor = "alessio";
                         Debitor = "luca";
                         Spend = 10.0;};
 FSI_0003+TransactionFs {Activity = "someActivity2";
                         Creditor = "alessio";
                         Debitor = "giulia";
                         Spend = 12.0;};
 FSI_0003+TransactionFs {Activity = "someActivity3";
                         Creditor = "luca";
                         Debitor = "alessio";
                         Spend = 7.0;};

我正在尝试使用以下规则获取TransactionFs的序列。对于每笔交易,请检查DebitorCreditor;在DebitorCreditor被交换的所有对应交易的序列中查找并返回具有TransactionFs属性的单个Spend,该属性是应收账款持有人的总债务最大的Spend(适当减去或相加Spend)。 Spend代表从DebitorCreditor的总债务。

例如,CreditorDebitoralessioluca对的结果应为:

TransactionFs {Activity = "_aggregate_";
                     Creditor = "alessio";
                     Debitor = "luca";
                     Spend = 3.0;};

当然,这样做的一种方法是使用嵌套的for循环,但是由于我正在学习F#,所以我想知道什么是执行此操作的适当功能方式。

1 个答案:

答案 0 :(得分:1)

第一步,我可能会使用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元组的格式为(组,项)。在这里,组本身是(name1,name2)的2元组,因此数据的嵌套结构是((name1,name2),Transactions)。

现在,对于每个交易列表,您都希望将总和相加,其中某些交易被视为“正”交易,而某些交易则被视为“(负)交易”,具体取决于它们与(name1,name2)顺序还是相反。即在第一个交易清单中,将Alessio付给Luca的交易视为正数,将Luca付给Alessio的交易视为负数。将所有这些值相加,如果相差为正,则借方与债权人的关系为“名称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#记录提供了许多有用的功能,这些功能是类所没有的,例如已经为您编写了比较和哈希码功能。 Plus记录一旦创建便是不可变的,因此您不必担心多线程环境中的并发性:如果您引用了一条记录,那么其他任何代码都不会在没有警告的情况下从您的身下将其更改。但是,如果您使用的是类,则用于创建类的语法将有所不同。

注意2:在整个代码中,我只有大约90%的人确定我得到了正确的借方/借方顺序。测试此代码,如果结果证明我交换了它们,然后交换代码的适当部分(如let c, d = ...行)。

我希望该解决方案的分步构建可以帮助您更好地了解代码在做什么以及如何以适当的功能风格进行操作。