代码之家  ›  专栏  ›  技术社区  ›  alelom

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

  •  0
  • alelom  · 技术社区  · 6 年前

    我可以在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;};
    

    我想知道 交易行为 按照以下规则。对于每个事务,请检查 Debitor Creditor ;按顺序查找所有相应的事务,其中 借方 债权人 交换并返回一个 交易行为 用一个 Spend 最大持有人的总债务 花费 (减去或求和 花费 适当地)。这个 花费 将代表 借方 债权人 .

    例如, 债权人 借方 夫妇 alessio luca 应该是:

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

    当然,实现这一点的一种方法是使用嵌套for循环,但是由于我正在学习f,我想知道什么是实现这一点的合适的功能方法。

    1 回复  |  直到 6 年前
        1
  •  1
  •   rmunn    6 年前

    作为第一步,我可能会使用 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])
    ]
    

    产量 群组 是2个元组的序列;每个2个元组的格式是(group,items)。在这里,组本身是(name1,name2)的2元组,因此数据的嵌套结构是(name1,name2),事务。

    现在,对于每个交易列表,您都需要加上总和,其中一些交易被认为是“正的”,一些交易被认为是“负的”,这取决于它们是与(name1,name2)顺序相同还是相反。也就是说,在第一个交易列表中,阿莱西奥支付卢卡的交易被认为是正的,卢卡支付阿莱西奥的交易被认为是负的。把所有这些值加起来,如果差额是正数,那么借方和贷方的关系是“name1欠name2钱”,否则是相反的。例如。:

    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 record语法编写了这篇文章。在默认情况下,F记录提供了许多有用的功能,而这些功能是您无法从类中获得的,比如已经为您编写了比较和哈希代码函数。另外,记录一旦创建就不可变,因此您不必担心多线程环境中的并发性:如果您引用了一个记录,那么没有其他代码会在您不发出警告的情况下将其从您的下面更改出来。但是,如果您使用的是类,那么创建类的语法将不同。

    注2:我只有90%的把握我的代码中有正确的债权人/借方顺序。测试这个代码,如果结果是我交换了它们,那么交换适当的部分(如 let c, d = ... 行)。

    我希望这个解决方案的逐步构造能够帮助您更好地理解代码在做什么,以及如何以正确的功能风格做事情。