Public/Split-Array.ps1

function Split-Array {
<#
.SYNOPSIS
    Splits an array into sub-arrays (chunks) using a configurable distribution strategy.

.DESCRIPTION
    Split-Array accepts an array — either as a direct argument or from the pipeline —
    and returns a list of sub-arrays.

    Two modes are available:

      -ChunkSize Specifies the maximum number of elements per chunk.

      -MaxChunk Specifies the desired number of chunks.

    For each mode, the -Distribution parameter controls how elements are arranged:

      Greedy Fill each chunk as much as possible; the last chunk may be smaller.
                  This is the default for -ChunkSize.

      Even Spread any remainder across chunks so sizes differ by at most one.
                  This is the default for -MaxChunk.

    Example — 1..10 with -MaxChunk 4:

      Greedy → (1,2,3), (4,5,6), (7,8,9), (10) chunks: 3,3,3,1
      Even → (1,2,3), (4,5,6), (7,8), (9,10) chunks: 3,3,2,2

    Example — 1..10 with -ChunkSize 3:

      Greedy → (1,2,3), (4,5,6), (7,8,9), (10) chunks: 3,3,3,1
      Even → (1,2,3), (4,5,6), (7,8), (9,10) chunks: 3,3,2,2

    Optionally, -Pad fills the last chunk to match the size of the first chunk.

.PARAMETER InputObject
    The array or individual elements to split.
    Accepts pipeline input.

    $null handling: $null values sent through the pipeline are silently ignored
    and do not appear in the output. Arrays containing $null passed via
    -InputObject do preserve $null elements, because the IEnumerable iteration
    inside the function adds them directly to the buffer.

    Nested arrays: non-string IEnumerable elements (e.g. inner arrays) received
    via the pipeline are flattened one level, because PowerShell unrolls the
    outer array before invoking the process block, and the function then iterates
    each inner array in turn. When passing nested arrays via -InputObject, the
    outer array is iterated once and inner arrays are kept as atomic elements.

.PARAMETER ChunkSize
    Maximum number of elements per chunk (ParameterSet: BySize).
    Must be greater than 0.
    If ChunkSize >= element count, a single chunk containing all elements is returned.
    Default distribution: Greedy.

.PARAMETER MaxChunk
    Desired number of output chunks (ParameterSet: ByMaxChunk).
    Must be greater than 0.
    If MaxChunk = 1, a single chunk is always returned.
    If element count <= MaxChunk, each element is returned in its own chunk.
    Default distribution: Even.

.PARAMETER Distribution
    Controls how elements are placed into chunks. Applies to both -ChunkSize and -MaxChunk.

      Greedy Fill each chunk as much as possible; the last chunk absorbs any remainder.
              Default when using -ChunkSize.

              Note: when used with -MaxChunk, Greedy may produce fewer than MaxChunk chunks.
              Example: 1..6 with -MaxChunk 4 -Distribution Greedy yields 3 chunks of 2,
              because ceil(6/4)=2 divides 6 evenly into 3 full chunks.
              Use -Distribution Even to always get exactly MaxChunk chunks.

      Even Spread the remainder one element at a time across the first chunks,
              so chunk sizes differ by at most one. Always produces exactly MaxChunk chunks
              (when Count > MaxChunk).
              Default when using -MaxChunk.

.PARAMETER Pad
    Pads the last chunk with the specified value until it matches the size of the first chunk.
    Accepts any value including $null (use -Pad $null to pad with null).
    Only applies when the last chunk is smaller than the first chunk.
    Has no effect when all chunks are already the same size.

.INPUTS
    System.Object
    Individual elements or arrays can be passed via the pipeline.

.OUTPUTS
    System.Object[][]
    An array of arrays. Each element of the outer array is one chunk.

.EXAMPLE
    Split-Array -InputObject 1..10 -ChunkSize 3

    Greedy (default): four chunks of 3, 3, 3, 1.
    Returns: (1,2,3), (4,5,6), (7,8,9), (10)

.EXAMPLE
    Split-Array -InputObject 1..10 -ChunkSize 3 -Distribution Even

    Even: four chunks of 3, 3, 2, 2 — no chunk is more than one element larger than another.
    Returns: (1,2,3), (4,5,6), (7,8), (9,10)

.EXAMPLE
    Split-Array -InputObject 1..10 -ChunkSize 3 -Pad $null

    Greedy with padding: last chunk is padded to match ChunkSize.
    Returns: (1,2,3), (4,5,6), (7,8,9), (10,$null,$null)

.EXAMPLE
    Split-Array -InputObject 1..7 -ChunkSize 4 -Pad 0

    Greedy with a custom pad value.
    Returns: (1,2,3,4), (5,6,7,0)

.EXAMPLE
    Split-Array -InputObject 1..10 -MaxChunk 4

    Even (default): four chunks of 3, 3, 2, 2.
    Returns: (1,2,3), (4,5,6), (7,8), (9,10)

.EXAMPLE
    Split-Array -InputObject 1..10 -MaxChunk 4 -Distribution Greedy

    Greedy: four chunks of 3, 3, 3, 1 — chunks filled first, remainder in the last.
    Returns: (1,2,3), (4,5,6), (7,8,9), (10)

.EXAMPLE
    Split-Array -InputObject 1..10 -MaxChunk 4 -Distribution Greedy -Pad $null

    Greedy with padding: last chunk padded to match first chunk size.
    Returns: (1,2,3), (4,5,6), (7,8,9), (10,$null,$null)

.EXAMPLE
    1..10 | Split-Array -ChunkSize 4

    Pipeline variant with Greedy (default). Returns: (1,2,3,4), (5,6,7,8), (9,10).

.EXAMPLE
    $chunks = Split-Array -InputObject 1..7 -MaxChunk 3
    $chunks | ForEach-Object { $_ -join ',' }

    Processes each chunk individually. Output: "1,2,3" / "4,5" / "6,7"

.EXAMPLE
    Split-Array -InputObject 1..10 -MaxChunk 4 -Verbose

    Verbose output showing how 10 elements are distributed across 4 chunks (Even mode):

      VERBOSE: Input count: 10
      VERBOSE: Mode: MaxChunk
      VERBOSE: MaxChunk: 4
      VERBOSE: Distribution: Even
      VERBOSE: Base size: 2
      VERBOSE: Remainder: 2
      VERBOSE: Chunks created: 4
      VERBOSE: Chunk sizes: 3, 3, 2, 2

.EXAMPLE
    Split-Array -InputObject 1..10 -MaxChunk 4 -Distribution Greedy -Verbose

    Verbose output for Greedy mode:

      VERBOSE: Input count: 10
      VERBOSE: Mode: MaxChunk
      VERBOSE: MaxChunk: 4
      VERBOSE: Distribution: Greedy
      VERBOSE: Base size: 3
      VERBOSE: Chunks created: 4
      VERBOSE: Chunk sizes: 3, 3, 3, 1

.EXAMPLE
    Split-Array -InputObject 1..10 -ChunkSize 3 -Distribution Even -Verbose

    Verbose output for ChunkSize + Even mode. Number of chunks is derived from ChunkSize,
    then elements are distributed evenly:

      VERBOSE: Input count: 10
      VERBOSE: Mode: ChunkSize
      VERBOSE: ChunkSize: 3
      VERBOSE: Distribution: Even
      VERBOSE: Number of chunks: 4
      VERBOSE: Base size: 2
      VERBOSE: Remainder: 2
      VERBOSE: Chunks created: 4
      VERBOSE: Chunk sizes: 3, 3, 2, 2

.EXAMPLE
    Split-Array -InputObject 1..10 -ChunkSize 3 -Pad $null -Verbose

    Verbose output when padding is applied:

      VERBOSE: Input count: 10
      VERBOSE: Mode: ChunkSize
      VERBOSE: ChunkSize: 3
      VERBOSE: Distribution: Greedy
      VERBOSE: Chunks created: 4
      VERBOSE: Chunk sizes: 3, 3, 3, 1
      VERBOSE: Pad value: ''
      VERBOSE: Last chunk padded from 1 to 3 elements

.EXAMPLE
    $null | Split-Array -ChunkSize 2

    $null sent via the pipeline is silently ignored; the function returns one empty chunk.
    To preserve $null as an element, pass the array via -InputObject instead:
    Split-Array -InputObject @(1, $null, 2) -ChunkSize 2
    Returns: (1,$null), (2)

.EXAMPLE
    @(1,2), @(3,4) | Split-Array -ChunkSize 1

    PowerShell unrolls the outer array in the pipeline; the function then iterates
    each inner array, flattening them. Returns four single-element chunks: (1),(2),(3),(4).
    To treat each inner array as one element, pass via -InputObject:
    Split-Array -InputObject @(@(1,2), @(3,4)) -ChunkSize 1
    Returns: ((1,2)), ((3,4))

.NOTES
    The function always returns an array of arrays, even when the result contains
    only a single chunk. This ensures a consistent return type that can always
    be iterated without type-checking the output.

    Use -Verbose to trace the splitting logic: input count, active mode, distribution
    strategy, chunk distribution, and final chunk sizes are all reported.
#>

    [CmdletBinding(DefaultParameterSetName = 'BySize')]
    [OutputType([object[]])]
    param(
        [Parameter(ValueFromPipeline = $true, Position = 0)]
        [object]$InputObject,

        [Parameter(ParameterSetName = 'BySize', Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$ChunkSize,

        [Parameter(ParameterSetName = 'ByMaxChunk', Mandatory = $true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$MaxChunk,

        [Parameter(ParameterSetName = 'BySize')]
        [Parameter(ParameterSetName = 'ByMaxChunk')]
        [ValidateSet('Greedy', 'Even')]
        [string]$Distribution,

        [Parameter(ParameterSetName = 'BySize')]
        [Parameter(ParameterSetName = 'ByMaxChunk')]
        [object]$Pad
    )

    begin {
        $buffer = [System.Collections.Generic.List[object]]::new()
        $doPad  = $PSBoundParameters.ContainsKey('Pad')
    }

    process {
        if ($null -ne $InputObject) {
            if ($InputObject -is [System.Collections.IEnumerable] -and $InputObject -isnot [string]) {
                foreach ($item in $InputObject) { $buffer.Add($item) }
            } else {
                $buffer.Add($InputObject)
            }
        }
    }

    end {

        $items = $buffer.ToArray()
        $count = $items.Count

        Write-Verbose "Input count: $count"

        if ($count -eq 0) {
            Write-Verbose "Input empty — returning empty array."
            return ,@(,@())
        }

        $bySize = $PSCmdlet.ParameterSetName -eq 'BySize'

        # Apply per-mode default when Distribution is not specified
        $effectiveDistribution = if ($Distribution) {
            $Distribution
        } elseif ($bySize) {
            'Greedy'
        } else {
            'Even'
        }

        if ($bySize) {
            Write-Verbose "Mode: ChunkSize"
            Write-Verbose "ChunkSize: $ChunkSize"
        } else {
            Write-Verbose "Mode: MaxChunk"
            Write-Verbose "MaxChunk: $MaxChunk"
        }
        Write-Verbose "Distribution: $effectiveDistribution"

        # Trivial cases that need no distribution logic
        if ($bySize -and $ChunkSize -ge $count) {
            Write-Verbose "ChunkSize >= count, returning a single chunk."
            Write-Verbose "Chunk sizes: $count"
            return ,@(,$items)
        }

        if (-not $bySize) {
            if ($MaxChunk -eq 1) {
                Write-Verbose "MaxChunk = 1 → Returning a single chunk."
                Write-Verbose "Chunk sizes: $count"
                return ,@(,$items)
            }

            if ($count -le $MaxChunk) {
                Write-Verbose "Count <= MaxChunk → Returning $count single-item chunks."
                $singles = [System.Collections.Generic.List[object]]::new()
                foreach ($item in $items) { $singles.Add(@($item)) }
                return ,$singles.ToArray()
            }
        }

        $chunkList = [System.Collections.Generic.List[object]]::new()

        if ($effectiveDistribution -eq 'Greedy') {
            # Chunk width: ChunkSize directly, or ceil(count / MaxChunk)
            $stride = if ($bySize) {
                $ChunkSize
            } else {
                $s = [int][Math]::Ceiling($count / $MaxChunk)
                Write-Verbose "Base size: $s"
                $s
            }
            for ($index = 0; $index -lt $count; $index += $stride) {
                $last = [Math]::Min($index + $stride - 1, $count - 1)
                $chunkList.Add($items[$index..$last])
            }
        } else {
            # Even: number of chunks from ChunkSize, or MaxChunk directly;
            # spread the remainder one element at a time across the first chunks
            $numChunks = if ($bySize) {
                $n = [int][Math]::Ceiling($count / $ChunkSize)
                Write-Verbose "Number of chunks: $n"
                $n
            } else {
                $MaxChunk
            }
            $baseSize  = [int][Math]::Floor($count / $numChunks)
            $remainder = $count % $numChunks
            Write-Verbose "Base size: $baseSize"
            Write-Verbose "Remainder: $remainder"
            $index = 0
            for ($i = 1; $i -le $numChunks; $i++) {
                $size = $baseSize
                if ($remainder -gt 0) { $size++; $remainder-- }
                $chunkList.Add($items[$index..($index + $size - 1)])
                $index += $size
            }
        }

        $chunks = $chunkList.ToArray()

        Write-Verbose "Chunks created: $($chunks.Count)"
        Write-Verbose "Chunk sizes: $(($chunks | ForEach-Object { $_.Count }) -join ', ')"

        if ($doPad) { $chunks = Add-PadToLastChunk -Chunks $chunks -PadValue $Pad }

        return ,$chunks
    }
}