我陷入了以下情况: 我必须从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}}
现在我得到了一个包含该属性作为[decimal]
的Selected.System.Management.Automation.PSCustomObject
类型对象。现在我想总结一下同一电子邮件地址使用的所有要点:
$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
?
提前感谢!
tl;博士:
如果需要控制用于对数字求和的特定数值数据类型:
-
避免
Measure-Object
,它总是使用[double]
计算。 -
请改用LINQ
Sum
方法(可在 PSv3+ 中访问)并强制转换为所需的数值类型:
[Linq.Enumerable]::Sum(
[decimal[]] @(
$Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender}
).Points
)
Mathias R. Jessen 的有用答案向您展示了一种优雅的方式来对Points
列进行求和,这些列按共享相同电子邮件地址的行分组,Theo 的有用答案通过真正将点相加为[decimal]
值来改进它。
关于-Sum
和浮点数据类型Measure-Object
的一些一般要点:
您正确声明:
属性 [数据类型] 更改为
double
[...] 我发现 Powershell 的GenericMeasureInfo.Sum
属性只能返回一个Nullable<Double>
作为属性值。
确实:Measure-Object -Sum
:
- 总是使用
[double]
值来汇总输入。 - 如果可能的话,它会强制输入
[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
[...]似乎我正在产生"双重"的溢出
溢出意味着超过可以存储在[double]
中的最大值,这是(a)不太可能([double]::MaxValue
是1.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 * 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[]]
类型重载并报告错误"发现 'Sum' 和参数计数的多个不明确重载: 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 }
我首先将所有发件人地址分组在一起,然后将它们单独相加:
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
}
}
<</div> div class="one_answers">像 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