【问题标题】:Alternatives to (Measure-Object -sum).Sum(Measure-Object -sum).Sum 的替代方案
【发布时间】:2019-03-17 09:39:45
【问题描述】:

我陷入了以下情况: 我必须从 CSV 文件中获取信息。我使用 Import-Csv 导入了 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.Sum属性只能返回一个Nullable<Double>实例作为属性值。

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

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

提前致谢!

【问题讨论】:

  • 请更具体一点。在示例 CSV 中,也向我们展示了标题。您的代码中的$Imported_CSV_Unique.Sender 来自哪里?你不能简单地做类似[decimal]$sum = 0; $yourData | ForEach-Object { $sum += $_.Points} 的事情吗?

标签: powershell csv sum measure-object


【解决方案1】:

tl;dr

如果您需要控制用于求和数字特定数值数据类型

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

  • 改为使用 LINQ Sum method(可在 PSv3+ 中访问)和 cast 到所需的数字类型

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

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

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

你说得对:

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

确实:Measure-Object -Sum:

  • 总是使用[double] 值对输入求和。
  • 强制输入[double]s,如果可能的话——即使它们不是数字。
    • 如果无法将输入强制转换为 [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 [...] 好像我正在产生“双”溢出

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

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

然而,您得到的是 rounding 错误,这是由于 [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 * 1028)。

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

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

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

    • 没有它,[decimal[]] 转换将导致 $null,并且 PowerShell 将无法找到 .Sum() 方法的 [decimal[]] 类型的重载并报告错误,“发现多个模糊重载对于“总和”和参数计数:1”。
  • 上述命令总是要求将所有匹配的 CSV 行(表示为自定义对象)作为一个整体放入内存,而 Measure-Object - 与 PowerShell 管道中的大多数 cmdlet 一样 - 会处理它们一个接一个,它只需要恒定的内存量(但速度较慢)。

如果不能一次将所有匹配的行加载到内存中,请使用 ForEach-Object (foreach) cmdlet,但请注意,这只有在您将实际的 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 }

【讨论】:

    【解决方案2】:

    我首先将所有发件人地址组合在一起,然后将它们单独相加:

    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
        }
    }
    

    【讨论】:

    • 使用Group-Object (+1) 的好主意;但是请注意,在第二个命令中,当您调用 Measure-Object 时,您将再次失去 [decimal] 精度。
    【解决方案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
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-08-10
      • 2022-11-18
      • 1970-01-01
      • 2017-08-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多