ca9163d9
ca9163d9

Reputation: 29159

Powershell break a long array into a array of array with length of N in one line?

For example, given a list 1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8 and a number 4, it returns a list of list with length of 4, that is (1, 2, 3, 4), (5, 6, 7, 8), (1, 2, 3, 4), (5, 6, 7, 8).

Basically I want to implement the following Python code in Powershell.

s = 1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8
z = zip(*[iter(s)]*4)  # Here N is 4
# z is (1, 2, 3, 4), (5, 6, 7, 8), (1, 2, 3, 4), (5, 6, 7, 8)

The following script returns 17 instead of 5.

$a = 1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,0
$b = 0..($a.Length / 4) | % {  @($a[($_*4)..($_*4 + 4 - 1)]) } 
$b.Length

Upvotes: 14

Views: 29983

Answers (10)

posh
posh

Reputation: 1

$chunk_size = 10
@(1..100) | % { $c = @(); $i = 0 } { if ($i % $chunk_size -eq 0) { $c += ,@() }; $c[-1] += $_; $i++ } 
$c
$chunk_size = 10 # Set desired chunk size
@(1..100) | % `
    { # Initialize loop variables
        $c = @(); # Cumulating array of chunks
        $i = 0;   # Incrementing index
    } {
        if ($i % $chunk_size -eq 0) { # If the index is divisible by chunk size
            $c += ,@() # Add another chunk to the accumulator
        }
        $c[-1] += $_; # Add current element to the last chunk
        $i++          # Increment
    }
$c

Upvotes: 0

Santiago Squarzon
Santiago Squarzon

Reputation: 59822

A simple method using a List<T> to collect input objects elements and output without enumerating using $PSCmdlet.WriteObject. This is compatible with Windows PowerShell 5.1.

function Split-Collection {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object[]] $InputObject,

        [Parameter(Position = 0)]
        [ValidateRange(1, [int]::MaxValue)]
        [int] $ChunkSize = 5
    )

    begin {
        $list = [System.Collections.Generic.List[object]]::new()
    }
    process {
        foreach($item in $InputObject) {
            $list.Add($item)
            if($list.Count -eq $ChunkSize) {
                $PSCmdlet.WriteObject($list.ToArray())
                $list.Clear()
            }
        }
    }
    end {
        if($list.Count) {
            $PSCmdlet.WriteObject($list.ToArray())
        }
    }
}

Usage:

PS ..\pwsh> 0..10 | Split-Collection 3 | ForEach-Object { "[$_]" }

[0 1 2]
[3 4 5]
[6 7 8]
[9 10]


PS ..\pwsh> Split-Collection -InputObject (0..10) 3 | ForEach-Object { "[$_]" }

[0 1 2]
[3 4 5]
[6 7 8]
[9 10]

An even simpler way to do it if using PowerShell 7+ is with Enumerable.Chunk from LINQ.

PS ..\pwsh> [System.Linq.Enumerable]::Chunk([int[]] (0..10), 3) | ForEach-Object { "[$_]" }

[0 1 2]
[3 4 5]
[6 7 8]
[9 10]

Upvotes: 1

Gennadii Saltyshchak
Gennadii Saltyshchak

Reputation: 637

If collection is big enough (e.g. millions of elements), then performance may matter. None of the solutions proposed before satisfied me first of all from performance standpoints. Here is my solution:

Function Chunk {
    param(
        [object[]] $source,
        [int] $size = 1)
    $chunkCount = [Math]::Ceiling($source.Count / $size)
    0 .. ($chunkCount - 1) `
    | ForEach-Object {
        $startIndex = $_ * $size
        $endIndex = [Math]::Min(($_ + 1) * $size, $source.Count)
        ,$source[$startIndex .. ($endIndex - 1)]
    }
}

Example of usage:

Chunk @(1..17) 4 | foreach { $_ -join ',' }

Output:

1,2,3,4
5,6,7,8
9,10,11,12
13,14,15,16
17

Performance measure:

(Measure-Command { Chunk @(1..1000000) 1000 }).TotalMilliseconds

Output: 140.467 (in milliseconds)

Upvotes: 0

Haoshu
Haoshu

Reputation: 914

Providing a solution using select. It doesn't need to worry whether $list.Count can be divided by $chunkSize.

function DivideList {
    param(
        [object[]]$list,
        [int]$chunkSize
    )

    for ($i = 0; $i -lt $list.Count; $i += $chunkSize) {
        , ($list | select -Skip $i -First $chunkSize)
    }
}

DivideList -list @(1..17) -chunkSize 4 | foreach { $_ -join ',' }

Output:

1,2,3,4
5,6,7,8
9,10,11,12
13,14,15,16
17

Upvotes: 6

Chris
Chris

Reputation: 11

Clear-Host

$s = 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18

$count = $s.Length

$split = $count/2

$split --

$b = $s[0..$split]

$split ++

$a = $s[$split..$count]

write-host "first array"

$b

write-host "next array"

$a

#clean up
Get-Variable -Exclude PWD,*Preference | Remove-Variable -EA 0

Upvotes: 1

TheCodingMatt
TheCodingMatt

Reputation: 61

@Shay Levy Answer: if you change the value of a to 1..15 then your solution not working anymore ( Peter Reavy comment )

So this worked for me:

$a = 1..15
$z=for($i=0; $i -lt $a.length; $i+=4){if ($a.length -gt ($i+3)) { ,($a[$i]..$a[$i+3])} else { ,($a[$i]..$a[-1])}}
$z.count

Upvotes: 3

Dave Wyatt
Dave Wyatt

Reputation: 1568

This is a bit old, but I figured I'd throw in the method I use for splitting an array into chunks. You can use Group-Object with a constructed property:

$bigList = 1..1000

$counter = [pscustomobject] @{ Value = 0 }
$groupSize = 100

$groups = $bigList | Group-Object -Property { [math]::Floor($counter.Value++ / $groupSize) }

$groups will be a collection of GroupInfo objects; in this case, each group will have exactly 100 elements (accessible as $groups[0].Group, $groups[1].Group, and so on.) I use an object property for the counter to avoid scoping issues inside the -Property script block, since a simple $i++ doesn't write back to the original variable. Alternatively, you can use $script:counter = 0 and $script:counter++ and get the same effect without a custom object.

Upvotes: 58

ca9163d9
ca9163d9

Reputation: 29159

$a = 1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8,0
$b = 0..([Math]::ceiling($a.Length / 4) - 1) | 
    % {  @(, $a[($_*4)..($_*4 + 4 - 1)]) } 

Don't know why I had to put a comma after (.

Upvotes: 1

Shay Levy
Shay Levy

Reputation: 126722

PS> $a = 1..16
PS> $z=for($i=0; $i -lt $a.length; $i+=4){ ,($a[$i]..$a[$i+3])}
PS> $z.count
4    

PS> $z[0]
1
2
3
4

PS> $z[1]
5
6
7
8

PS> $z[2]
9
10
11
12

PS> $z[3]
13
14
15
16

Upvotes: 7

Doug Finke
Doug Finke

Reputation: 6823

Wrote this in 2009 PowerShell Split-Every Function

Probably can be improved.

Function Split-Every($list, $count=4) {
    $aggregateList = @()

    $blocks = [Math]::Floor($list.Count / $count)
    $leftOver = $list.Count % $count
    for($i=0; $i -lt $blocks; $i++) {
        $end = $count * ($i + 1) - 1

        $aggregateList += @(,$list[$start..$end])
        $start = $end + 1
    }    
    if($leftOver -gt 0) {
        $aggregateList += @(,$list[$start..($end+$leftOver)])
    }

    $aggregateList    
}

$s = 1,2,3,4,5,6,7,8,1,2,3,4,5,6,7,8

$r = Split-Every $s 4

$r[0]
""
$r[1]
""
$r[2]
""
$r[3]

Upvotes: 12

Related Questions