17

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
4
  • Nice solution. How would you handle a number of elements not divisible by N? Which is 4 in this case. $a = 1..18 Commented Dec 15, 2012 at 19:44
  • @DougFinke Creating an variable $n = 4, and replace all the 4s with $n should work. Commented Dec 15, 2012 at 20:02
  • Agreed. I guess what I'm asking is. What do you do with the left overs when you have a array of #'s that is not divisible by N? Commented Dec 15, 2012 at 20:33
  • @DougFinke I used 0..($a.Length / 4) instead of 1..($a.Length /4). So basically it added one more group. There will be an empty array if the length is divided by 4. Commented Dec 15, 2012 at 20:44

10 Answers 10

60

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.

Sign up to request clarification or add additional context in comments.

4 Comments

I would like to use this but I need to group by an additional property first. How can I have $counter reset each time the basedomain property changes?
I think this is the most elegant answer as it uses the pipeline.
Thank you @DaveWyatt, I used this sort of logic for an array of strings with a bit of adjusted to this logic you shared: $div = [Math]::Round(($List.Count / 4),0); $counter = [pscustomobject] @{ Value = 0 }; $groups = $List | Group-Object -Property { [math]::Floor($counter.Value++ / $div) }; ($groups | ? {$_.Name -eq 0}).Group; ($groups | ? {$_.Name -eq 1}).Group; ($groups | ? {$_.Name -eq 2}).Group; ($groups | ? {$_.Name -eq 3}).Group;
This is old, but here from 2022 I was able to divide a group of 43K computer names in 8 seconds. Thank you sir!
12

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]

2 Comments

If the length of the list is < $count then $start and $end are used in the $aggregateList code uninitialized (or set to unknown values). Fix is to add $start=$end=0 above the for loop.
I had an issue with this one, it was ripping apart strings when I had one item in my $list, so I my fix was to add if ($list.Count -eq 1) { return @($list) } above the $aggregateList declaration.
7
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

2 Comments

The accepted solution only works for consecutive integers. For instance when $i is 0, $a[$i] is 1 and $a[$i+3] is 4. Hence 1 .. 4 will work. But the correct solution is $z=for($i=0; $i -lt $a.length; $i+=4){ ,($a[$i .. ($i+3)])}
@PeterReavy great answer!
6

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

1 Comment

The advantage of this solution is that it is applicable on an array of strings as well.
3

@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

Comments

1
$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 (.

Comments

1
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

1 Comment

Could you please add more details to this answer? It's just code only, so a bit of explaination would be great
1

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]

Comments

0

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)

Comments

0
$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

1 Comment

please avoid aliases for scripts, even though % has kinda a long history of being there on all versions it makes the code less readable. And the main goal for scripts is readability, as opposed to ad-hoc within the shell.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.