Resolve-XLRange.ps1


[Regex]$Script:AddressRegex = [Regex]::new(@"
^(?:(?<sheet>[^!]+)!)?
     (?:((?<r1c1>R\d+C\d+(?::R\d+C\d+)?))|
        (?<a1>[A-Z]+\d+(?::[A-Z]+\d+)?))$
"@
, [System.Text.RegularExpressions.RegexOptions]::Compiled -bor [System.Text.RegularExpressions.RegexOptions]::IgnorePatternWhitespace  -bor [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)

function Resolve-XLRange {
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline=$true, ParameterSetName = "Range")]
    [XLRange]$Range,

    [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline=$true, ParameterSetName = "SheetAndRC")]
    [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline=$true, ParameterSetName = "SheetAndName")]
    [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline=$true, ParameterSetName = "SheetAndAddress")]    
    [XLSheet]$Sheet,
    
    [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline=$true, ParameterSetName = "FileAndName")]
    [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline=$true, ParameterSetName = "FileAndAddress")]
    [XLFile]$File,

    [Parameter(Mandatory = $true, Position = 1, ParameterSetName = "SheetAndRC")]
    [Alias("Row")]
    [int]$FromRow,

    [Parameter(ParameterSetName = "SheetAndRC")]
    [int]$ToRow = $FromRow,

    [Parameter(Mandatory = $true, Position = 2, ParameterSetName = "SheetAndRC")]
    [Alias("Column")]
    [int]$FromColumn,

    [Parameter(ParameterSetName = "SheetAndRC")]
    [int]$ToColumn = $FromColumn,
    
    [Parameter(ParameterSetName = "SheetAndName")]
    [XLScope]$Scope = [XLScope]::Any,

    [Parameter(Mandatory = $true, ParameterSetName = "FileAndName")]
    [Parameter(Mandatory = $true, ParameterSetName = "SheetAndName")]
    [string]$Name,
    
    [Parameter(Mandatory = $true, ParameterSetName = "FileAndAddress")]
    [Parameter(Mandatory = $true, ParameterSetName = "SheetAndAddress")]
    [string]$Address
)
    $inputObject = $null
    [XLRange]$xlRange = $null
    if ($PSCmdlet.ParameterSetName -eq 'Range') {
        $xlRange = $Range
        $inputObject = $Range
    } elseif ($PSCmdlet.ParameterSetName -eq 'SheetAndRC') {
        if ($FromRow -gt $ToRow) {
            throw "Invalid range, FromRow ($FromRow) must be less than or equal to ToRow ($ToRow)"
        } elseif ($FromColumn -gt $ToColumn) {
            throw "Invalid range, FromColumn ($FromColumn) must be less than or equal to ToColumn ($ToColumn)"
        }
        
        $xlRange = [XLRange]::new($Sheet.Owner, $Sheet.Worksheet.Cells.Item($FromRow, $FromColumn, $ToRow, $ToColumn))
        $inputObject = $Sheet
    } elseif ($PSCmdlet.ParameterSetName -eq 'SheetAndName') {
        $namedRange = $null
        if ($Scope -band [XLScope]::Sheet -and $Sheet.Worksheet.Names.ContainsKey($Name)) {
            $namedRange = $Sheet.Worksheet.Names[$Name]
        }
        
        if ($namedRange -eq $null -and $Scope -band [XLScope]::File -and $Sheet.Worksheet.Workbook.Names.ContainsKey($Name)) {
            $namedRange = $Sheet.Worksheet.Workbook.Names[$Name]
        }
        if ($namedRange -eq $null) {
            throw "Could not resolve range named '$Name'"
        }
        $xlRange = [XLRange]::new($Sheet.Owner, $namedRange)
        $inputObject = $Sheet
    } elseif ($PSCmdlet.ParameterSetName -eq 'FileAndName') {
        $namedRange = $null
        if ($File.Package.Workbook.Names.ContainsKey($Name)) {
            $namedRange = $File.Package.Workbook.Names[$Name]
        } else {
            throw "Could not resolve range named '$Name'"
        }
        $xlRange = [XLRange]::new($File.Package, $namedRange)
        $inputObject = $File
    } elseif ($PSCmdlet.ParameterSetName.EndsWith('Address')) {
        $match = $Script:AddressRegex.Match($Address)
        if (-not $match.Success) {
            throw "Invalid address: '$Address'"
        }
                
        [OfficeOpenXml.ExcelWorksheet]$targetSheet = $null
        if ($match.Groups['sheet'].Success) {
            $sheetName = $match.Groups['sheet'].Value
            $workbook = $null
            if ($PSCmdlet.ParameterSetName.StartsWith('File')) {
                $workbook = $File.Package.Workbook
            } elseif ($PSCmdlet.ParameterSetName.StartsWith('Sheet')) {
                $workbook = $Sheet.Worksheet.Workbook
            }
            $targetSheet = $workbook.Worksheets[$sheetName]
            if ($targetSheet -eq $null) {
                throw "Sheet not found: '$sheetName'"
            }
        } else {
            if ($PSCmdlet.ParameterSetName.StartsWith('File')) {
                throw "No sheet specified in address: '$Address'"
            } elseif ($PSCmdlet.ParameterSetName.StartsWith('Sheet')) {
                $targetSheet = $Sheet.Worksheet
            }                
        }
        
        $a1 = $null
        if ($match.Groups['r1c1'].Success) {
            $a1 = [OfficeOpenXml.ExcelCellBase]::TranslateFromR1C1($match.Groups['r1c1'].Value, 0, 0)
            if ($a1 -match "#REF") {
                throw "Invalid address: '$($match.Groups['r1c1'].Value)'"
            }
        } else {
            $a1 = $match.Groups['a1'].Value
        }
        
        $owner = $null
        if ($PSCmdlet.ParameterSetName.StartsWith('File')) {
            $owner = $File.Package
            $inputObject = $File
        } elseif ($PSCmdlet.ParameterSetName.StartsWith('Sheet')) {
            $owner = $Sheet.Owner
            $inputObject = $Sheet
        }
        
        if (-not [OfficeOpenXml.FormulaParsing.ExcelUtilities.ExcelAddressUtil]::IsValidAddress($a1)) {
            throw "Invalid address: '$a1'"
        }
        
        # couldn't find a better way to validate
        $a1 -split ':' | ForEach-Object -Process {
            $excelAddress = [OfficeOpenXml.ExcelCellAddress]::new($_)
            if ($excelAddress.Row -eq 0 -or $excelAddress.Column -eq 0) {
                throw "Invalid address: '$a1'"
            }
        }
        
        $excelRange = $targetSheet.Cells[$a1]
        
        $xlRange = [XLRange]::new($owner, $excelRange)
    }
    [pscustomobject][ordered]@{
        Range = $xlRange
        InputObject = $inputObject
    }
}