在 Powershell 中通过 LinQ 对数据进行分组

Grouping data via LinQ in Powershell

我有一个数据数组,我需要将它们按 2 个属性分组,然后计算每组第三个属性的总和。我想通过 Linq 尽可能快地完成此操作。

到目前为止,这是我的演示代码:

class costs {
    [string] $first;
    [string] $last;
    [int]    $price;
    costs([string]$first, [string]$last, [int] $price){
        $this.first = $first
        $this.last  = $last
        $this.price = $price
    }
}

[costs[]]$costs = @(
    [costs]::new('peter', 'parker', 1),
    [costs]::new('peter', 'parker', 2),
    [costs]::new('paul',  'summer', 3),
    [costs]::new('paul',  'winter', 4),
    [costs]::new('mary',  'winter', 5)
)

# group by full name:
$groupBy = [Func[Object,string]] {$args[0].first + $args[0].last}
$groupResult = [Linq.Enumerable]::GroupBy($costs, $groupBy)

# sum the costs per group:
$selectFunc   = [Func[Object,int]] {$sum=0; foreach($p in $args[0].price){$sum += $p};$sum}
$selectResult = [Linq.Enumerable]::Select($groupResult, $selectFunc)

$selectResult

selectResult 向我显示了每个用户的正确费用总和。 但我正在努力从初始数组中获得两个用户属性的总和。 我也不确定,如果我可以将两个 Linq 调用组合在一个调用中以使其更快。 在这里欢迎任何输入(除了“为什么使用 Linq?”)。

更新

根据答案,我更新了代码:

class costs {
    [string] $first;
    [string] $last;
    [int]    $price;
    costs([string]$first, [string]$last, [int] $price){
        $this.first = $first
        $this.last  = $last
        $this.price = $price
    }
}

[costs[]]$costs = @(
    [costs]::new('peter', 'parker', 1),
    [costs]::new('peter', 'parker', 2),
    [costs]::new('paul',  'summer', 3),
    [costs]::new('paul',  'winter', 4),
    [costs]::new('mary',  'winter', 5)
)
foreach($doubler in 0..15){$costs += $costs}

cls
write-host "processing $($costs.count) elements."

(measure-command {
    # group by full name:
    $groupBy = [Func[Object,string]] {$args[0].first + $args[0].last}
    $groupResult = [Linq.Enumerable]::GroupBy($costs, $groupBy)

    # sum the costs per group:
    $selectFunc = [Func[Object,Object]]{
        $sum=0
        foreach($p in $args[0].price){
            $sum += $p
        }
        foreach($a in $args[0]) {
            [costs]::new($a.first, $a.last, $sum)
            break
        }
    }
    $selectResult = [Linq.Enumerable]::Select($groupResult, $selectFunc)
    $result = [Linq.Enumerable]::ToArray($selectResult)
}).TotalSeconds

$result

# and for the books the same procedure with a dataTable (slower):

$table = [System.Data.DataTable]::new('table')
[void]$table.Columns.Add('first', [string])
[void]$table.Columns.Add('last',  [string])
[void]$table.Columns.Add('price', [int])
$resultTable = $table.Clone()

# fill table with above test-data:
foreach($c in $costs){
    $null = $table.rows.Add($c.first, $c.last, $c.price)
}

(measure-command {
    $groupBy = [Func[System.Data.DataRow,string]] {$args[0].first + $args[0].last}
    $groupResult = [Linq.Enumerable]::GroupBy([System.Data.DataRow[]]$table.Rows, $groupBy)

    # sum the costs per group:
    $selectFunc = [Func[object,System.Data.DataRow]]{
        $sum=0
        foreach($p in $args[0].price){
            $sum += $p
        }
        foreach($a in $args[0]) {
            $resultTable.rows.Add($a.first, $a.last, $sum)
            break
        }
    }
    $selectResult = [Linq.Enumerable]::Select($groupResult, $selectFunc)
    $null = [Linq.Enumerable]::ToList($selectResult)
}).TotalSeconds
$resultTable

超过 300000 个元素的运行时间约为 2.5 秒。没有那么糟糕。直到现在,如果不切换到嵌入式 C# 代码,我找不到更快的方法。

$selectFunc 定义更改为 return [psobject][object],然后从现有分组值创建结果对象:

$selectFunc   = [Func[Object,psobject]]{
    $sum=0
    foreach($p in $args[0].price){
      $sum += $p
    }

    # Output new object with first+last based on input object + sum
    $args[0] |Select first,last,@{Name='sum';Expression={$sum}} -First 1
}

I want to do this via Linq to be as fast as possible.

我强烈建议您实际 测试 这是否比使用 Group-Object 或用于计算的简单哈希表更快 - 很多开销这会使 PowerShell 变慢(尤其是参数绑定),但仍将适用于您的代码,因此差异可能并不显着 - 但脚本的 可读性 可能会受到很大影响。

我个人的偏好是只使用 Group-Object cmdlet:

$costs |Group-Object first,last |ForEach-Object {
  $sum = ($_.Group |Measure price -Sum).Sum
  $_.Group |Select -Property first,last,@{N='Sum';E={$sum}} -First 1
}