Private/ConvertFrom-VersionRange.ps1

function ConvertFrom-VersionRange {
    <#
    .SYNOPSIS
    Parse a NuGet version range string into a structured bounds object.
 
    .DESCRIPTION
    PSDepend carries version ranges in the Version field using NuGet range
    syntax. A string is only treated as a range when it contains a range
    delimiter ('[', ']', '(', ')', ','); a bare version (e.g. 3.2.1) is returned
    as an exact match so existing requirements files keep their meaning.
 
    Returns a [PSCustomObject] with these properties:
        IsExact - $true when the request is a single exact version
        Exact - the exact version string (only when IsExact)
        Min - lower bound version string, or $null for no lower bound
        Max - upper bound version string, or $null for no upper bound
        MinInclusive - $true when the lower bound is inclusive ('[')
        MaxInclusive - $true when the upper bound is inclusive (']')
 
    Brackets denote inclusive bounds, parentheses exclusive. A bracketed single
    value ([1.0]) is an exact match. An empty side means that bound is open.
    Malformed ranges produce a non-terminating error and return nothing.
 
    .PARAMETER Version
    The version or NuGet range string to parse.
 
    .EXAMPLE
    ConvertFrom-VersionRange -Version '3.2.1'
 
    Returns an object with IsExact = $true and Exact = '3.2.1'.
 
    .EXAMPLE
    ConvertFrom-VersionRange -Version '[2.2.3,3.0)'
 
    Returns Min = '2.2.3' (inclusive), Max = '3.0' (exclusive).
 
    .EXAMPLE
    ConvertFrom-VersionRange -Version '(,3.0)'
 
    Returns Min = $null, Max = '3.0' (exclusive) - an upper-bound-only range.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [string]$Version
    )

    $exactResult = {
        param($Value)
        [PSCustomObject]@{
            IsExact      = $true
            Exact        = $Value
            Min          = $null
            Max          = $null
            MinInclusive = $true
            MaxInclusive = $true
        }
    }

    # No range delimiter: a bare version is an exact match.
    if ($Version -notmatch '[\[\](),]') {
        return & $exactResult $Version
    }

    # Bracketed/parenthesised range: first and last char carry inclusivity.
    $open = $Version[0]
    $close = $Version[-1]
    if ($open -notin '[', '(' -or $close -notin ']', ')') {
        Write-Error "Invalid version range [$Version]: must start with '[' or '(' and end with ']' or ')'."
        return
    }

    $minInclusive = $open -eq '['
    $maxInclusive = $close -eq ']'
    $inner = $Version.Substring(1, $Version.Length - 2)

    # A single value with no comma (e.g. [1.0]) is an exact match.
    if ($inner -notmatch ',') {
        $single = $inner.Trim()
        if ([string]::IsNullOrEmpty($single)) {
            Write-Error "Invalid version range [$Version]: no version specified."
            return
        }
        if (-not $minInclusive -or -not $maxInclusive) {
            Write-Error "Invalid version range [$Version]: a single version must be bracketed as [version]."
            return
        }
        return & $exactResult $single
    }

    $parts = $inner -split ',', 2
    $min = $parts[0].Trim()
    $max = $parts[1].Trim()

    if ([string]::IsNullOrEmpty($min) -and [string]::IsNullOrEmpty($max)) {
        Write-Error "Invalid version range [$Version]: at least one bound is required."
        return
    }

    [PSCustomObject]@{
        IsExact      = $false
        Exact        = $null
        Min          = if ([string]::IsNullOrEmpty($min)) { $null } else { $min }
        Max          = if ([string]::IsNullOrEmpty($max)) { $null } else { $max }
        MinInclusive = $minInclusive
        MaxInclusive = $maxInclusive
    }
}