alex2k8
alex2k8

Reputation: 43214

Flatten array in PowerShell

Assume we have:

$a = @(1, @(2, @(3)))

I would like to flatten $a to get @(1, 2, 3).

I have found one solution:

@($a | % {$_}).count

But maybe there is a more elegant way?

Upvotes: 38

Views: 20427

Answers (5)

Santiago Squarzon
Santiago Squarzon

Reputation: 60195

There are examples of nested arrays where piping to ForEach-Object simply can't handle them.

For example, given our nested array:

$toUnroll = @(@(0,1),@(2,3),@(@(4,@(5,6)),@(7,8),9),10)

If we attempt to pipe to ForEach-Object, the result would be:

PS /> $toUnroll | ForEach-Object { $_ }

0
1
2
3
4

Length         : 2
LongLength     : 2
Rank           : 1
SyncRoot       : {5, 6}
IsReadOnly     : False
IsFixedSize    : True
IsSynchronized : False
Count          : 2

7
8
9
10

Write-Output is also not able to handle the unrolling:

$toUnroll | Write-Output | ForEach-Object GetType

IsPublic IsSerial Name             BaseType
-------- -------- ----             --------
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Object[]         System.Array
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType
True     True     Int32            System.ValueType

Below we can see some examples on how we can handle the flattening of these nested arrays including a one-liner anonymous function.

This technique unrolls our array in order.

function RecursiveUnroll {
    [cmdletbinding()]
    param(
        [parameter(Mandatory, ValueFromPipeline)]
        [object[]] $Unroll
    )

    process {
        foreach($item in $Unroll) {
            if($item -is [object[]]) {
                RecursiveUnroll -Unroll $item
                continue
            }
            $item
        }
    }
}

RecursiveUnroll -Unroll $toUnroll
# Results in an array from 0 to 10
  • One-liner anonymous function:

The logic for this script block is the exact same as the function demonstrated above.

$toUnroll | & { process { if($_ -is [object[]]) { return $_ | & $MyInvocation.MyCommand.ScriptBlock }; $_ }}
  • Recursive Class Method (can be static or instance)

Same as the recursive function example, we can expect the array to keep it's order. We can add that this technique should be faster than the recursive function approach since repeated function or script block calls is expensive, this is hinted in PowerShell scripting performance considerations.

class Unroller {
    [object[]] $Array

    Unroller() { }
    Unroller([object[]] $Array) {
        $this.Array = $Array
    }

    static [object] Unroll([object[]] $Array) {
        $result = foreach($item in $Array) {
            if($item -is [object[]]) {
                [Unroller]::Unroll($item)
                continue
            }
            $item
        }
        return $result
    }

    [object] Unroll () {
        return [Unroller]::Unroll($this.Array)
    }
}

# Instantiating and using using the instance method of our class:
$instance = [Unroller] $toUnroll
$instance.Unroll()
# Results in an array from 0 to 10

# Using the static method of our class, no need to instantiate:
[Unroller]::Unroll($toUnroll)
# Results in an array from 0 to 10

This technique should be the fastest one, the downside is that we cannot expect an ordered array.

$queue = [System.Collections.Generic.Queue[object]]::new()
$queue.Enqueue($toUnroll)

while($queue.Count) {
    foreach($item in $queue.Dequeue()) {
        if($item -is [object[]]) {
            $queue.Enqueue($item)
            continue
        }
        $item
    }
}

# Using our given nested array as an example we can expect
# a flattened array with the following order:
# 10, 0, 1, 2, 3, 9, 4, 7, 8, 5, 6

Lastly, using a Stack we can ensure that the order is preserved, this technique is also very efficient.

$stack = [System.Collections.Generic.Stack[object]]::new()
$stack.Push($toUnroll)

$result = while($stack.Count) {
    foreach($item in $stack.Pop()) {
        if($item -is [object[]]) {
            [array]::Reverse($item)
            $stack.Push($item)
            continue
        }
        $item
    }
}

[array]::Reverse($result)
$result # Should be array from 0 to 10

Upvotes: 5

Bacon Bits
Bacon Bits

Reputation: 32170

Warning: See edit at the end!

This problem is probably most elegantly resolved with the .ForEach() array method introduced in Powershell v4.0. Performance-wise it has the advantage of not needing to construct a pipeline, so in some cases it might perform better.

> $a.ForEach({$_}).Count
3

If you already have a pipeline, the easiest way to flatten an array is to pipe it through Write-Output:

> $b = $a | Write-Output
> $b.Count
3

--

EDIT: The answer above is not really correct. It doesn't completely flatten arrays that have multiple nested arrays. The answer from @SantiagoSquarzon has an example of a deeply nested array that requires multiple unrolls:

> $toUnroll = @(@(0,1),@(2,3),@(@(4,@(5,6)),@(7,8),9),10) # 11 elements
> $toUnroll.ForEach({$_}).Count
8

> $toUnroll.ForEach({$_}).ForEach({$_}).Count
10

> $toUnroll.ForEach({$_}).ForEach({$_}).ForEach({$_}).Count
11

Or, perhaps more clearly:

> $toUnroll = @(@(0,1),@(2,3),@(@(4,@(5,6)),@(7,8),9),10) # 11 elements
### Unroll 0 times
> $toUnroll.ForEach({$_ | ConvertTo-Json -Compress})
[0,1]
[2,3]
[[4,[5,6]],[7,8],9]
10

### Unroll 1 times
> $toUnroll.ForEach({$_}).ForEach({$_ | ConvertTo-Json -Compress}) 
0
1
2
3
[4,[5,6]]
[7,8]
9
10

### Unroll 2 times
> $toUnroll.ForEach({$_}).ForEach({$_}).ForEach({$_ | ConvertTo-Json -Compress})
0
1
2
3
4
[5,6]
7
8
9
10

### Unroll 3 times
> $toUnroll.ForEach({$_}).ForEach({$_}).ForEach({$_}).ForEach({$_ | ConvertTo-Json -Compress})
0
1
2
3
4
5
6
7
8
9
10

Upvotes: 5

Godeke
Godeke

Reputation: 16281

Piping is the correct way to flatten nested structures, so I'm not sure what would be more "elegant". Yes, the syntax is a bit line-noisy looking, but frankly quite serviceable.

2020 Edit

The recommended syntax these days is to expand % to ForEach-Object. A bit more verbose but definitely more readable:

@($a | ForEach-Object {$_}).count

Upvotes: 28

iraSenthil
iraSenthil

Reputation: 11577

You can use .NET's String.Join method.

[String]::Join("",$array)

Upvotes: 1

alex2k8
alex2k8

Reputation: 43214

Same code, just wrapped in function:

function Flatten($a)
{
    ,@($a | % {$_})
}

Testing:

function AssertLength($expectedLength, $arr)
{
    if($ExpectedLength -eq $arr.length) 
    {
        Write-Host "OK"
    }
    else 
    {
        Write-Host "FAILURE"
    }
}

# Tests
AssertLength 0 (Flatten @())
AssertLength 1 (Flatten 1)
AssertLength 1 (Flatten @(1))
AssertLength 2 (Flatten @(1, 2))
AssertLength 2 (Flatten @(1, @(2)))
AssertLength 3 (Flatten @(1, @(2, @(3))))

Upvotes: 14

Related Questions