Альтернативы (Measure-Object -sum) .Sum - PullRequest
2 голосов
/ 17 марта 2019

Я застрял в следующей ситуации: я должен получить информацию из файла CSV.Я импортировал 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].Я немного покопался и обнаружил, что свойство GenericMeasureInfo.Sum Powershell может возвращать только экземпляр Nullable<Double> в качестве значения свойства.

Похоже, что я создаю переполнение [double], потому чтоотображаемый номер абсолютно неверен.Я хочу придерживаться десятичного или целого числа, поэтому у меня есть вывод вроде 71123.4 или что-то в этом роде.

Есть ли другой подход для этого, поэтому мне не нужно использовать (Measure-Object -sum).Sum?

Заранее спасибо!

Ответы [ 3 ]

3 голосов
/ 17 марта 2019

Используя группировку, как Матиас уже сделал, вот как вы можете получить сумму без потери десятичной точности, как я прокомментировал ранее:

# 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
3 голосов
/ 17 марта 2019

ТЛ; др

Если вам нужно контролировать определенный числовой тип данных , используемый для суммирования чисел :

  • Избегайте Measure-Object, который неизменно использует [double] вычисления.

  • Вместо этого используйте метод LINQ Sum (доступный в PSv3 +) с cast для нужного числового типа

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

Полезный ответ Матиаса Р. Джессена показывает вам элегантный способ суммировать ваш столбец Points, сгруппированный по строкам, которые имеют один и тот же адрес электронной почты, и Полезный ответ Тео улучшает его путем истинно суммируя баллы как [decimal] значения.

Некоторые общие замечания о Measure-Object с -Sum и типами данных с плавающей точкой :

Вы правильно заявляете:

Свойство [тип данных] изменилось на double [...] я обнаружил, что свойство Powershell GenericMeasureInfo.Sum может возвращать только Nullable<Double> в качестве значения свойства.

Действительно: Measure-Object -Sum:

  • неизменно использует [double] значения для суммирования входных данных.
  • it принудительно вводит значения в [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"

Переполнение будет означать превышение максимального значения, которое может быть сохранено в [double], что является (а) маловероятным ([double]::MaxValue равно 1.79769313486232E+308, т. Е. Больше 10 в степени 308) и (б) будет производить другой симптом; e.g.:

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

Что вы делаете получаете, однако, это округление ошибок из-за внутреннего двоичного представления *1118* типа *, которое не не всегда имеет точное десятичное представление, что может привести к сбивающим с толку результатам вычислений; e.g.:

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] с, вы должны избегать Measure-Object и самостоятельно суммировать .

В контексте вашей исходной команды вы можете использовать метод Sum LINQ:

[Linq.Enumerable]::Sum(
  [decimal[]] @(
    $Imported_CSV | where {$_.Sender -eq $Imported_CSV_Unique.Sender}
  ).Points
)
  • Использование @(...) (оператор подвыражения массива), а не просто (...) вокруг команды конвейера, гарантирует, что общая команда не потерпит неудачу в случае, если конвейер вернет без строк . @(...) превращает невыдачу в пустой массив , для которого .Sum() правильно возвращает 0.

    • Без него приведение [decimal[]] приведет к $null, и PowerShell не сможет найти тип [decimal[]] перегрузки метода .Sum() и сообщить об ошибке "Обнаружено несколько неоднозначных перегрузок" для "суммы" и количества аргументов: 1 ".
  • Приведенная выше команда неизменно требует, чтобы все соответствующие строки CSV (представленные в виде пользовательских объектов) помещались в память в целом , тогда как Measure-Object, как и большинство командлетов в конвейере PowerShell, обрабатывает их один за другим , что требует только постоянного объема памяти (но медленнее).

Если загрузка всех совпадающих строк в память не возможна, используйте командлет ForEach-Object (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 }
2 голосов
/ 17 марта 2019

Я бы начал с группировки всех адресов отправителей, а затем суммировал их по отдельности:

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
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...