2

我陷入以下情况:我必须从 CSV 文件中获取信息。我使用Import-Csv.

我的原始数据如下所示:

45227;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXX;XXXX;XXX@XX.com;;;3.7;;

其中包含的列3.7是感兴趣的值(“点”)。

这是我的第一个问题-> 使用Import-Csv,powershell 会将这些信息保存在一个[string]属性中。为避免这种情况,我使用了以下行:

| Select @{Name="Points";Expression={[decimal]$_.Points}}

现在我得到一个Selected.System.Management.Automation.PSCustomObject包含该属性的类型对象作为[decimal]. 现在我想总结一下同一个电子邮件地址使用的所有要点:

$Data[$Index].Points += (
  $Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender} | 
    measure Points -sum
).Sum

这似乎工作得很好,但如果我打开$Data[$Index] | gm我得到这个:Points NoteProperty double Points=71301.6000000006

属性更改为[double]。我挖了一下,我发现 Powershell 的GenericMeasureInfo.SumProperty 只能返回一个Nullable<Double>实例作为属性值。

似乎我正在产生溢出[double],因为显示的数字完全错误。我想坚持使用十进制或整数,所以我有类似71123.4或类似的输出。

有没有其他方法,所以我不必使用(Measure-Object -sum).Sum

提前致谢!

4

3 回答 3

3

我首先将所有发件人地址分组在一起,然后将它们单独汇总:

Import-Csv .\data.csv |Group-Object Sender |ForEach-Object {
    [pscustomobject]@{
        Sender = $_.Name
        SumOfPoints = ($_.Group |Measure-Object Points -Sum).Sum
    }
}

Measure-Object将自动将Points字符串转换为[double]- 如果您需要更高的精度,您可以手动转换为[decimal]之前的样子:

Import-Csv .\data.csv |Select-Object Sender,@{Name="Points";Expression={[decimal]$_.Points}} |Group-Object Sender |ForEach-Object {
    [pscustomobject]@{
        Sender = $_.Name
        SumOfPoints = ($_.Group |Measure-Object Points -Sum).Sum
    }
}
于 2019-03-17T12:58:22.763 回答
3

使用像Mathias已经做过的分组,这里是如何在不丢失小数精度的情况下获得总和,正如我之前评论过的:

# faking the Import-Csv here with a here-string.
# in real life, you would use: Import-Csv <yourdata.csv> -Delimiter ';'
$data = @"
Sender;Date;Description;Something;Number;Whatever;DontKnow;Email;Nothing;Zilch;Points;Empty;Nada
45227;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXV;XXXA;XXX@XX.com;;;3.7;;
45227;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXW;XXXB;XXX@XX.com;;;4.7;;
45226;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXX;XXXC;XXX@XX.com;;;4.777779;;
45225;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXY;XXXD;XXX@XX.com;;;4.8;;
45225;01.10.2018 03:24:00;Xxxx Xxxx Xxxxx x XX xxxxxxxxxxxxxx Xxxxx xxx Xxxxxxxxxxxxxxxxxxx;;3;XXXZ;XXXE;XXX@XX.com;;;4.9;;
"@ | ConvertFrom-Csv -Delimiter ';'

#get the two columns you need from the Csv and group them by Sender
$data | Select-Object Sender, Points | Group-Object Sender | ForEach-Object {
    # add the 'Points' values as decimal
    [decimal]$sum = 0
    foreach ($value in $_.Group.Points) { $sum += [decimal]$value }
    [PSCustomObject]@{
        Sender = $_.Name
        Sum    = $sum
    }
}

上面的输出将是:

Sender      Sum
------      ---
45227       8,4
45226  4,777779
45225       9,7
于 2019-03-17T14:23:25.557 回答
3

tl;博士

如果您需要控制用于汇总数字的特定数字数据类型

  • 避免Measure-Object,它总是使用[double]计算。

  • 相反,使用LINQSum方法(可在 PSv3+ 中访问)并强制转换为所需的数字类型

[Linq.Enumerable]::Sum(
  [decimal[]] @(
    $Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender}
  ).Points
)

Mathias R. Jessen 的有用答案向您展示了一种优雅的方式来对Points按共享相同电子邮件地址的行分组的列进行求和,而Theo 的有用答案通过真正将点求和作为[decimal]值来改进它。

关于with和浮点数据类型的一些一般要点Measure-Object-Sum

您正确地说:

属性 [数据类型] 更改为double[...] 我发现 Powershell 的GenericMeasureInfo.Sum属性只能返回一个Nullable<Double>as 属性值。

确实Measure-Object -Sum

  • 总是使用[double]值来总结输入。
  • 如果可能的话,它会强制s的输入[double]——即使它们不是数字。
    • 如果无法将输入强制转换为[double](例如,'foo'),则会发出非终止错误,但对任何剩余输入继续求和。

以上暗示即使是字符串也是可接受的输入Measure-Object -Sum,因为它们将[double]在求和过程中按需转换。这意味着您可以直接Import-Csv使用您的命令,如下例所示(它使用两个实例来模拟的输出):[pscustomobject]Import-Csv

PS> ([pscustomobject] @{ Points = '3.7' }, [pscustomobject] @{ Points = '1.2' } |
      Measure-Object Points -Sum).Sum
4.9  # .Points property values were summed correctly.

71301.6000000006[...]似乎我正在产生“双”溢出

溢出意味着超过可以存储在 a 中的最大值[double],这是(a)不太可能([double]::MaxValue1.79769313486232E+308,大于 10 的 308 次方)和(b)会产生不同的症状;例如:

PS> ([double]::MaxValue, [double]::MaxValue | Measure-Object -Sum).Sum
∞  # represents positive infinity

然而,你得到的由于类型的内部二进制表示导致的舍入错误,它并不总是具有精确的十进制表示,这可能会导致令人困惑的计算结果;例如:[double]

PS> 1.3 - 1.1 -eq 0.2
False # !! With [double]s, 1.3 - 1.1 is NOT exactly equal to 0.2

有关更多信息,请参阅https://floating-point-gui.de/

使用[decimal]值确实解决了这个问题,但请注意,这是以较小的范围为代价的(实际上,您可以获得 28 位精度的十进制数字 - 最大数字的绝对值取决于小数点的位置;作为整数,它是79,228,162,514,264,337,593,543,950,335,即接近 8 * 10 28 )。

如果您确实需要[decimal]s 的精度,则必须避免Measure-Object并自己进行 summing

在原始命令的上下文中,您可以使用SumLINQ 方法:

[Linq.Enumerable]::Sum(
  [decimal[]] @(
    $Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender}
  ).Points
)
  • 使用@(...)(数组子表达式运算符)而不是仅仅围绕管道命令可确保在管道碰巧不返回任何行(...)的情况下整个命令不会失败。 将非输出转换为空数组,并正确返回。@(...).Sum()0

    • 没有它,强制转换[decimal[]]将导致$null,并且 PowerShell 将无法找到该方法的[decimal[]]-typed 重载.Sum()并报告错误“为“Sum”和参数计数找到多个不明确的重载:1”。
  • 上述命令总是需要将所有匹配的 CSV 行(表示为自定义对象)作为一个整体放入内存中,而Measure-Object- 就像 PowerShell 管道中的大多数 cmdlet - 将一个一个地处理它们,这仅需要恒定数量的内存(但速度较慢)。

如果不能一次将所有匹配的行加载到内存中,请使用ForEach-Object( ) cmdlet,但请注意,这只有在您将实际调用替换为已在内存中的数组foreach时才有意义:Import-Csv$Imported_Csv

# Replace $Imported_Csv with the original Import-Csv call to 
# get memory-friendly one-by-one processing.
$Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender} |
  foreach -Begin { [decimal] $sum = 0 } -Process { $sum += $_.Points } -End { $sum }
于 2019-03-17T15:09:19.950 回答