Скрипт PowerShell для группировки записей путем наложения даты начала и окончания - PullRequest
0 голосов
/ 13 апреля 2020

Я работаю над файлом CSV, у которого есть даты начала и окончания, и требование состоит в том, чтобы группировать записи по датам, когда даты перекрывают друг друга. Например, в приведенной ниже таблице Bill_Number 177835 Start_Date и End_Date перекрываются с 178682,179504, 178990 Start_Date и End_Date, так что все должны быть сгруппированы вместе и так далее для каждой записи.

Bill_Number,Start_Date,End_Date
177835,4/14/20 3:00 AM,4/14/20 7:00 AM
178682,4/14/20 3:00 AM,4/14/20 7:00 AM
179504,4/14/20 3:29 AM,4/14/20 6:29 AM
178662,4/14/20 4:30 AM,4/14/20 5:30 AM
178990,4/14/20 6:00 AM,4/14/20 10:00 AM
178995,4/15/20 6:00 AM,4/15/20 10:00 AM
178998,4/15/20 6:00 AM,4/15/20 10:00 AM

Я пробовал разные комбинация типа «Группировка по» и «для l oop», но не может дать результат. В приведенном выше примере CSV ожидаемый результат:

Group1: 177835,178682,179504, 178990
Group2: 177835,178682,179504, 178662
Group3: 178995, 178998

В настоящее время у меня под кодом ниже. Любая помощь в этом будет оценена, спасибо заранее.

$array = @(‘ab’,’bc’,’cd’,’df’)


for ($y = 0; $y -lt $array.count) {
    for ($x = 0; $x -lt $array.count) {
        if ($array[$y]-ne $array[$x]){
            Write-Host $array[$y],$array[$x]
        }
        $x++ 
    }
    $y++
}

Ответы [ 3 ]

0 голосов
/ 13 апреля 2020

Вы можете сделать что-то вроде следующего. Вероятно, есть более чистое решение, но это может занять много времени.

$csv = Import-Csv file.csv
# Creates all inclusive groups where times overlap
$csvGroups = foreach ($row in $csv) {
    $start = [datetime]$row.Start_Date
    $end = [datetime]$row.End_Date
    ,($csv | where { ($start -ge [datetime]$_.Start_Date -and $start -le [datetime]$_.End_Date) -or ($end -ge [datetime]$_.Start_Date -and $end -le [datetime]$_.End_Date) })
}

# Removes duplicates from $csvGroups
$groups = $csvGroups | Group {$_.Bill_number -join ','} |
    Foreach-Object { ,$_.Group[0] }

# Compares current group against all groups except itself
$output = for ($i = 0; $i -lt $groups.count; $i++) {
    $unique = $true # indicates if the group's bill_numbers are in another group
    $group = $groups[$i]
    $list = $groups -as [system.collections.arraylist]
    $list.RemoveAt($i) # Removes self
    foreach ($innerGroup in $list) {
        # If current group's bill_numbers are in another group, skip to next group
        if ((compare $group.Bill_Number $innergroup.Bill_Number).SideIndicator -notcontains '<=') {
            $unique = $false
            break
        } 
    }
    if ($unique) {
        ,$group
    }
}
$groupCounter = 1
# Output formatting
$output | Foreach-Object { "Group{0}:{1}" -f $groupCounter++,($_.Bill_Number -join ",")}

Объяснение:

Я добавил комментарии, чтобы дать представление о том, что продолжается.

Синтаксис ,$variable использует унарный оператор ,. Он преобразует вывод в массив. Как правило, PowerShell развертывает массив как отдельные элементы. Развертывание становится проблемой здесь, потому что мы хотим, чтобы группы оставались группами (массивами). В противном случае было бы много повторяющихся номеров счетов, и мы бы потеряли связь между группами.

Для $list используется arraylist. Это так, мы можем получить доступ к методу RemoveAt(). Типичный array имеет фиксированный размер и не может управляться таким образом. Это можно эффективно сделать с помощью array, но код другой. Вам нужно либо выбрать диапазоны индекса вокруг элемента, который вы хотите пропустить, либо создать новый массив с помощью некоторого другого условного выражения, которое исключит целевой элемент. arraylist мне проще (личное предпочтение).

0 голосов
/ 18 апреля 2020

@ Шан, я видел твои комментарии, поэтому я хотел ответить дополнительным кодом и обсуждением. Возможно, я ушел за борт, но вы выразили желание учиться, чтобы вы могли поддерживать эти куски кода в будущем. Итак, я уделяю этому много времени.

Я могу также упомянуть некоторые из работ @AdminOfThings. Это не критика, а сотрудничество. Его пример умный и динамичный c с точки зрения выполнения работы и привлечения нужных инструментов, пока он шел к желаемому результату.

Первоначально я обошел вопрос группировки, потому что я не Не похоже на то, чтобы названия / нумерация групп имели какое-то значение. Например: «Группа 1» указывает на то, что все ее участники перекрываются в периоды выставления счетов, но нет указания того, что или когда происходит перекрытие. Может быть, я бросился через это ... Я, возможно, слишком много читал об этом или, возможно, даже позволил моим собственным предубеждениям помешать. Во всяком случае, я решил создать отношения с точки зрения каждого номера счета, и это привело к моему первому ответу.

С тех пор и из-за вашего комментария я приложил усилия к расширению и документированию первого Пример, который я привел. Пересмотренный код будет Пример 1 ниже. Я подробно прокомментировал это, и большинство комментариев будут относиться и к исходному примеру. Существуют некоторые различия, которые были вызваны расширенной функциональностью группировки, но комментарии должны отражать эти ситуации.

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

Пример 1:

Function Get-Overlaps
{

<#
.SYNOPSIS
Given an object (reference object) compare to a collection of other objects of the same 
type.  Return an array of billing numbers for which the billing period overlaps that of
the reference object.

.DESCRIPTION
Given an object (reference object) compare to a collection of other objects of the same 
type.  Return an array of billing numbers for which the billing period overlaps that of
the reference object.

.PARAMETER ReferenceObject
This is the current object you wish to compare to all other objects.

.PARAMETER
The collection of objects you want to compare with the reference object.

.NOTES

> The date time casting could probably have been done by further preparing 
  the objects in the calling code.  However, givin this is for a 
  StackOverflow question I can polish that later.

#>

Param(
    [Parameter(Mandatory = $true)]
    [Object]$ReferenceObject,

    [Parameter( Mandatory = $true )]
    [Object[]]$CompareObjects
) # End Parameter Block

[Collections.ArrayList]$Return = @()

$R_StartDate = [DateTime]$ReferenceObject.Start_Date
$R_EndDate   = [DateTime]$ReferenceObject.End_Date

ForEach($Object in $CompareObjects)
{
    $O_StartDate = [DateTime]$Object.Start_Date
    $O_EndDate   = [DateTime]$Object.End_Date

    # The first if statement skips the reference object's bill_number
    If( !($ReferenceObject.Bill_Number -eq $Object.Bill_Number) )
    {
        # This logic can use some explaining.  So far as I could tell there were 2 cases to look for:
        # 1) Either or both the start and end dates fell inside the the timespan of the comparison
        #    object.  This cases is handle by the first 2 conditions.
        # 2) If the reference objects timespan covers the entire timespan of the comparison object.
        #    Meaning the start date is before and the end date is after, fitting the entire 
        #    comparison timespan is within the bounds of the reference timespan.  I elected to use 
        #    the 3rd condition below to detect that case because once the start date is earlier I
        #    only have to care if the end date is greater than the start date.  It's a little more 
        #    inclusive and partially covered by the previous conditions, but whatever, you gotta
        #    pick something...
        #
        # Note: This was a deceptively difficult thing to comprehend, I missed that last condition
        #       in my first example (later corrected) and I think @AdminOfThings also overlooked it.
        If(
            ( $R_StartDate -ge $O_StartDate -and $R_StartDate -le $O_EndDate   ) -or
            ( $R_EndDate   -ge $O_StartDate -and $R_EndDate   -le $O_EndDate   ) -or
            ( $R_StartDate -le $O_StartDate -and $R_EndDate   -ge $O_StartDate )
          )
            {
                [Void]$Return.Add( $Object.Bill_Number )
            }
    }
}
Return $Return

} # End Get-Overlaps

$Objects = 
Import-Csv 'C:\temp\DateOverlap.CSV' |
ForEach-Object{

    # Consider overlap as a relationship from the perspective of a given Object.
    $Overlaps = [Collections.ArrayList]@(Get-overlaps -ReferenceObject $_ -CompareObjects $Objects)

    # Knowing the overlaps I can infer the group, by  adding the group's bill_number to its group property.    
    If( $Overlaps )
        {   # Don't calculate a group unless you actually have overlaps:
            $Group = $Overlaps.Clone()
            [Void]$Group.Add( $_.Bill_Number ) # Can you do in the above line, but for readability I separated it.
        }
    Else { $Group = $null } # Ensure's not reusing group from a previous iteration of the loop.

    # Create a new PSCustomObject with the data so far.
    [PSCustomObject][Ordered]@{
    Bill_Number = $_.Bill_Number
    Start_Date  = [DateTime]$_.Start_Date
    End_Date    = [DateTime]$_.End_Date
    Overlaps    = $Overlaps
    Group       = $Group | Sort-Object # Sorting will make it a lot easier to get unique lists later.
    }
}

# The reason I recreated the objects from the CSV file instead of using Select-Object as I had 
# previously is that I simply couldn't get Select-Object to maintain type ArrayList that was being
# returned from the function.  I know that's a documented problem or circumstance some where.

# Now I'll add one more property called Group_ID a comma delimited string that we can later use
# to echo the groups according to your original request.
$Objects =
$Objects | 
Select-Object *,@{Name = 'Group_ID'; Expression = { $_.Group -join ', ' } }

# This output is just for the sake of showing the new objects:
$Objects | Format-Table -AutoSize -Wrap

# Now create an array of unique Group_ID strings, this is possible of the sorts and joins done earlier.
$UniqueGroups = $Objects.Group_ID | Select-Object -Unique

$Num = 1
ForEach($UniqueGroup in $UniqueGroups)
{
    "Group $Num : $UniqueGroup"
    ++$Num # Increment the $Num, using convienient unary operator, so next group is echoed properly.
}

# Below is a traditional for loop that does the same thing.  I did that first before deciding the ForEach 
# was cleaner.  Leaving it commented below, because you're on a learning-quest, so just more demo code...

# For($i = 0; $i -lt $UniqueGroups.Count; ++$i)
# {
#     $Num = $i + 1
#     $UniqueGroup = $UniqueGroups[$i]
#     "Group $Num : $UniqueGroup"
# }

Пример 2:

$Objects = 
Import-Csv 'C:\temp\DateOverlap.CSV' |
Select-Object Bill_Number,
    @{ Name = 'Start_Date'; Expression = { [DateTime]$_.Start_Date } },
    @{ Name = 'End_Date';   Expression = { [DateTime]$_.End_Date } }

# The above select statement converts the Start_Date & End_Date properties to [DateTime] objects
# While you had asked to pack everything into the nested loops, that would have resulted in 
# unnecessary recasting of object types to ensure proper comparison.  Often this is a matter of
# preference, but in this case I think it's better.  I did have it  working well without the 
# above select, but the code is more readable / concise with it. So even if you treat  the 
# Select-Object command as a blackbox the rest of the code should be easier to understand.
#
# Of course, and if you couldn't tell from my samples Select-Object is incredibly useful. I 
# recommend taking the time to learn it thoroughly.  The MS documentation can be found here:
# https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/select-object?view=powershell-5.1

:Outer ForEach( $ReferenceObject in $Objects )
{    
    # In other revisions I had assigned these values to some shorter variable names.
    # I took that out. Again since you're learning I wanted the  all the dot referencing
    # to be on full display.
    $ReferenceObject.Start_Date = $ReferenceObject.Start_Date
    $ReferenceObject.End_Date   = $ReferenceObject.End_Date
    [Collections.ArrayList]$TempArrList = @() # Reset this on each iteration of the outer loop.

    :Inner ForEach( $ComparisonObject in $Objects )
    {
        If( $ComparisonObject.Bill_Number -eq $ReferenceObject.Bill_Number )
        {   # Skip the current reference object in the $Objects collection! This prevents the duplication of
            # the current Bill's number within it's group, helping to ensure unique-ification.
            #
            # By now you should have seen across all revision including AdminOfThings demo, that there was some 
            # need skip the current item when searching for overlaps.  And, that there are a number of ways to 
            # accomplish that.  In this case I simply go back to the top of the loop when the current record
            # is encountered, effectively skipping it.

            Continue Inner
        }   

        # The below logic needs some explaining.  So far as I could tell there were 2 cases to look for:
        # 1) Either or both the start and end dates fell inside the the timespan of the comparison
        #    object.  This cases is handle by the first 2 conditions.
        # 2) If the reference object's timespan covers the entire timespan of the comparison object.
        #    Meaning the start date is before and the end date is after, fitting the entire 
        #    comparison timespan is within the bounds of the reference timespan.  I elected to use 
        #    the 3rd condition below to detect that case because once the start date is earlier I
        #    only have to care if the end date is greater than the other start date.  It's a little 
        #    more inclusive and partially covered by the previous conditions, but whatever, you gotta
        #    pick something...
        #
        # Note: This was a deceptively difficult thing to comprehend, I missed that last condition
        #       in my first example (later corrected) and I think @AdminOfThings also overlooked it.

        If(
            ( $ReferenceObject.Start_Date -ge $ComparisonObject.Start_Date -and $ReferenceObject.Start_Date -le $ComparisonObject.End_Date   ) -or
            ( $ReferenceObject.End_Date   -ge $ComparisonObject.Start_Date -and $ReferenceObject.End_Date   -le $ComparisonObject.End_Date   ) -or
            ( $ReferenceObject.Start_Date -le $ComparisonObject.Start_Date -and $ReferenceObject.End_Date   -ge $ComparisonObject.Start_Date )
          )
            {
                [Void]$TempArrList.Add( $ComparisonObject.Bill_Number )
            }
    }

    # Now Add the properties!
    $ReferenceObject | Add-Member -Name Overlaps -MemberType NoteProperty -Value $TempArrList 

    If( $ReferenceObject.Overlaps )
    {           
        [Void]$TempArrList.Add($ReferenceObject.Bill_Number)
        $ReferenceObject | Add-Member -Name Group -MemberType NoteProperty -Value ( $TempArrList | Sort-Object )
        $ReferenceObject | Add-Member -Name Group_ID -MemberType NoteProperty -Value ( $ReferenceObject.Group -join ', ' )

        # Below a script property also works, but I think the above is easier to follow:
        # $ReferenceObject | Add-Member -Name Group_ID -MemberType ScriptProperty -Value { $this.Group -join ', ' }
        }
    Else
    {
        $ReferenceObject | Add-Member -Name Group -MemberType NoteProperty -Value $null
        $ReferenceObject | Add-Member -Name Group_ID -MemberType NoteProperty -Value $null
    }    
}

# This output is just for the sake of showing the new objects:
$Objects | Format-Table -AutoSize -Wrap

# Now create an array of unique Group_ID strings, this is possible of the sorts and joins done earlier.
#
# It's important to point out I chose to sort because I saw the clever solution that AdminOfThings
# used.  There's a need to display only groups that have unique memberships, not necessarily unique
# ordering of the members.  He identified these by doing some additional loops and using the Compare
# -Object cmdlet.  Again, I must say that was very clever, and Compare-Object is another tool very much 
# worth getting to know.  However, the code didn't seem like it cared which of the various orderings it
# ultimately output.  Therefore I could conclude the order wasn't really important, and it's fine if the
# groups are sorted.  With the objects sorted it's much easier to derive the truely unique lists with the
# simple Select-Object command below.

$UniqueGroups = $Objects.Group_ID | Select-Object -Unique

# Finally Loop through the UniqueGroups
$Num = 1
ForEach($UniqueGroup in $UniqueGroups)
{
    "Group $Num : $UniqueGroup"
    ++$Num # Increment the $Num, using convienient unary operator, so next group is echoed properly.
}

Дополнительное обсуждение:

Надеемся, что примеры полезны. Я хотел упомянуть еще несколько моментов:

  1. Использование ArrayLists ([System.Collections.ArrayList]) вместо собственных массивов. Типичная причина для этого - возможность быстро добавлять и удалять элементы. Если вы выполните поиск по inte rnet, вы найдете сотни статей, объясняющих, почему это быстрее. Это так часто, что вы часто найдете опытных пользователей PowerShell, инстинктивно реализующих его. Но главная причина - это скорость и гибкость, позволяющая легко добавлять и удалять элементы.
  2. Вы заметите, что я сильно полагался на возможность добавлять новые свойства к объектам. Есть несколько способов сделать это, Select-Object, Создание ваших собственных объектов, и в Примере 2 выше я использовал Get-Member. Основная причина, по которой я использовал Get-Member, заключалась в том, что я не мог заставить придерживаться типа ArrayList при использовании Select-Object.
  3. Относительно циклов. Это указывается c на ваше желание для вложенных циклов. В моем первом ответе все еще были циклы, за исключением того, что некоторые подразумевались конвейером, а другие были сохранены в вспомогательной функции. Последнее действительно также предпочтение; для удобства чтения иногда полезно оставить некоторый код вне основного тела. Тем не менее, все те же понятия были там с самого начала. Вам должно быть комфортно с подразумеваемой l oop, которая поставляется с возможностью прокладки труб.

Я не думаю, что я могу сказать гораздо больше, не получив избыточности. Я действительно надеюсь, что это было полезно, мне было, конечно, весело работать над этим. Если у вас есть вопросы или пожелания, дайте мне знать. Спасибо.

0 голосов
/ 13 апреля 2020

Так что очень грязный подход. Я думаю, что есть несколько способов определить, есть ли совпадение для конкретного c сравнения, одной записи в другую. Однако вам может понадобиться список номеров счетов, с которыми сталкивается каждый диапазон дат счетов. используя вызов функции в выражении / выражении Select-Object, я добавил свойство ваших столкновений к объектам.

Функция многословна и, возможно, будет улучшена, но суть в том, что для каждой записи она будет сравниваться со всеми другими записывает и сообщает этот номер счета в свойстве коллизии, если дата начала или окончания попадает в диапазон других записей.

Это, конечно, просто демонстрационный код, я уверен, что его можно улучшить для ваших целей , но может быть отправной точкой для вас.

Очевидно, измените путь к файлу CSV.

Function Get-Collisions
{

Param(
    [Parameter(Mandatory = $true)]
    [Object]$ReferenceObject,

    [Parameter( Mandatory = $true )]
    [Object[]]$CompareObjects
) # End Parameter Block

ForEach($Object in $CompareObjects)
{
    If( !($ReferenceObject.Bill_Number -eq $Object.Bill_Number) )
    {
        If(
            ( $ReferenceObject.Start_Date -ge $Objact.StartDate -and  $ReferenceObject.Start_Date -le  $Objact.End_Date ) -or
            ( $ReferenceObject.End_Date -ge $Object.Start_Date -and $ReferenceObject.End_Date -le $Object.End_Date      ) -or
            ( $ReferenceObject.Start_Date -le $Object.Start_Date -and $ReferenceObject.End_Date -ge $Object.Start_Date  )
          )
            {
                 $Object.Bill_Number
            }
    }
}
} # End Get-Collisions

$Objects = Import-Csv 'C:\temp\DateOverlap.CSV' 

$Objects |
ForEach-Object{
     $_.Start_Date = [DateTime]$_.Start_Date
     $_.End_Date   = [DateTime]$_.End_Date
}

$Objects = $Objects | 
Select-object *,@{Name = 'Collisions'; Expression = { Get-Collisions -ReferenceObject $_ -CompareObjects $Objects }}

$Objects | Format-Table -AutoSize

Дайте мне знать, как это происходит. Благодаря.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...