Reputation: 43214
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
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
The logic for this script block is the exact same as the function demonstrated above.
$toUnroll | & { process { if($_ -is [object[]]) { return $_ | & $MyInvocation.MyCommand.ScriptBlock }; $_ }}
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
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
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
Reputation: 11577
You can use .NET's String.Join method.
[String]::Join("",$array)
Upvotes: 1
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