TM-DataManipulation.psm1


class Range {
<#
    .SYNOPSIS
    A class to hold the start/end data for a given contiguous integer range.
#>

    [long]$Start
    [long]$End

    # Default New Values
    Range() {
        $this.Start = ([int]::MinValue - 1)
        $this.End   = ([int]::MaxValue + 1)
    }
    # Set Values
    Range([long]$Start, [long]$End) {
        $this.Start = $Start
        $this.End   = $End
    }

    [string]ToString() {
        return (
            if ($this.Start -eq $this.End) {
                $this.Start
            } else {
                "$($this.Start)-$($this.End)"
            }
        )
    }
}


function Get-ContiguousRange {
<#
    .SYNOPSIS
    Finds contiguous ranges of integers within a given integer array.
 
    .DESCRIPTION
    This function takes a list of integers and identifies contiguous ranges within the list.
    The output can be a list of Range objects or a string.
 
    .PARAMETER IntRange
    Array of integers to find contiguous ranges within
 
    .PARAMETER CombineString
    If selected, returns the contiguous ranges as a single comma separated string.
#>

    [CmdletBinding()]
    [OutputType([List[Range]], [string])]
    param (
        [Parameter(
            Position = 0,
            Mandatory,
            HelpMessage = 'Enter one or more integers.'
        )]
        [int[]]$IntRange,

        [Parameter(
            Position = 1,
            Mandatory = $false,
            HelpMessage = 'Outputs contiguous ranges as a string.'
        )]
        [switch]$CombineString
    )

    begin {
        $Return = [List[Range]]::new()
        $RangeObj = [Range]::New()
        [long]$Index = 0
        [long[]]$LongRange = $IntRange | Sort-Object -Unique
    }

    process {
        for ([long]$a = $LongRange[0]; [long]$a -le $LongRange[-1]; [long]$a++) {
            # Set Start value
            if ($RangeObj.Start -eq ([int]::MinValue - 1)) {
                if ($LongRange[$Index] -ne $a) { $a = $LongRange[$Index] }
                $RangeObj.Start = $a
            }

            # Set End Value
            if (
                ($LongRange[$Index] -eq $a) -and
                ($LongRange[($Index + 1)] -ne ([long]$a + 1)) -and
                ($RangeObj.End -eq ([int]::MaxValue + 1))
            ) {
                $RangeObj.End = $a
                $Return.Add($RangeObj)
                $RangeObj = [Range]::New()
            }
            $Index++
        }
    }

    end {
        if ($CombineString) {
            $Strings = $Return | ForEach-Object { $_.ToString() }
            return $Strings -join ', '
        } else {
            return $Return
        }
    }
}


class SemVer {
<#
    .SYNOPSIS
    Translates a version string into a formal semantic versioning pattern.
    {Major}.{Minor}.{Patch}-{pre-releaseTag}+{buildNum}
 
    .DESCRIPTION
    Utilizes regular expressions to verify if a given string adheres to semantic versioning rules.
    For more details: https://semver.org/
 
    Examples of valid semantic versions:
        ex0: 2.1.4
        ex1: 5.12.96-pr1+000a
        ex2: 10.3.2+002
        ex3: 3.1.0-rc3
 
    .PARAMETER Version
    The version string to be parsed into a SemVer object.
 
    .OUTPUTS
    When the provided version does not follow semantic versioning format, the Valid field will be set to false.
    The Major, Minor, and Patch properties will be initialized to 0 and the PreRelease and Build properties will
    contain empty strings.
 
    Conversely, if the version adheres to the semantic versioning format, the Valid field will be set to true.
    The Major, Minor, and Patch properties will contain their respective values as [long]s.
    The PreRelease and Build properties will either be empty if unused, or hold the string value of the relevant fields.
#>

    [boolean]$Valid     = $false
    [long]$Major        = 0
    [long]$Minor        = 0
    [long]$Patch        = 0
    [string]$PreRelease = [string]::Empty
    [string]$Build      = [string]::Empty

    SemVer( [string]$Version ) {
        $SemVerRegex = '^' +
        '(?<Major>0|[1-9]\d*)\.' +
        '(?<Minor>0|[1-9]\d*)\.' +
        '(?<Patch>0|[1-9]\d*)' +
        '(?:-(?<PreRelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))' +
        '?(?:\+(?<Build>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?' +
        '$'
        if ($Version -match $SemVerRegex) {
            $this.Valid      = $true
            $this.Major      = [long]::Parse($Matches['Major'])
            $this.Minor      = [long]::Parse($Matches['Minor'])
            $this.Patch      = [long]::Parse($Matches['Patch'])
            $this.PreRelease = $Matches['PreRelease']
            $this.Build      = $Matches['Build']
        }
    }

    [string] ToString() {
        $result = [string]::Empty
        if ($this.Valid) {
            $result = "$($this.Major).$($this.Minor).$($this.Patch)"
            if ([string]::IsNullOrEmpty($this.PreRelease) -eq $false) {
                $result += "-$($this.PreRelease)"
            }
            if ([string]::IsNullOrEmpty($this.Build) -eq $false) {
                $result += "+$($this.Build)"
            }
        }
        return $result
    }
}


function Test-MatchesSemVer {
<#
    .SYNOPSIS
    Evaluates if the provided version string adheres to semantic versioning rules.
 
    .DESCRIPTION
    This function checks whether the input string follows the semantic versioning format and returns the appropriate
    boolean value or the [SemVer] object if PassThru is selected.
 
    .PARAMETER Version
    The version string to be evaluated against semantic versioning rules.
 
    .PARAMETER PassThru
    An optional switch parameter. When selected, returns the [SemVer] object instead of a boolean.
 
    .OUTPUTS
    If the version string does not adhere to semantic versioning format, the function returns false.
 
    When the version string complies with semantic versioning rules, the function returns true, unless PassThru is
    selected. If PassThru is selected and the version string is valid, it returns the [SemVer] object.
 
    .EXAMPLE
    if (Test-MatchesSemVer -Version $Version) {
        Move-Item -Path $Csproj.FullName -Destination $PackagePath
    }
 
    .EXAMPLE
    $SemVer = Test-MatchesSemVer -Version $Version -PassThru
    if ($SemVer.Valid -and ($SemVer.PreRelease -eq [string]::Empty)) {
        dotnet nuget push $Package.FullName --api-key $ApiKey --source $NugetSource
    }
#>

    [CmdletBinding()]
    [OutputType([Boolean], [SemVer])]
    param(
        [Parameter(Mandatory)]
        [string]$Version,

        [Parameter(Mandatory = $false)]
        [switch]$PassThru
    )

    [SemVer]$SemVer = [SemVer]::new($Version)
    if ($SemVer.Valid) {
        if ($PassThru) { return $SemVer } else { return $true }
    }
    return $false
}


function Write-ReverseString {
<#
    .SYNOPSIS
    Reverses a given string.
 
    .DESCRIPTION
    This function takes a string as input and reverses its characters. The output is the reversed string.
 
    .PARAMETER String
    The string to be reversed.
 
    .PARAMETER Encoding
    The encoding method to use when reversing the string.
    Available options are 'Utf8' and 'Utf16'. The default is 'Utf8'.
#>

    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(
            Position = 0,
            Mandatory,
            ValueFromPipeline
        )]
        [ValidateNotNullOrEmpty()]
        [string]$String,

        [Parameter(
            Position = 1,
            Mandatory = $false
        )]
        [ValidateSet( 'Utf8', 'Utf16')]
        [string]$Encoding = 'Utf8'
    )
    begin { $chars = [List[char]]::new() }
    process {
        $runes = @($String.EnumerateRunes())
        for ($i = ($runes.Count - 1); $i -ge 0; $i--) {
            $rune = $runes[$i]

            switch ($Encoding) {
                'Utf8' {
                    $points = [char[]]::new($rune.Utf8SequenceLength)
                    $rune.EncodeToUtf8($points) | Out-Null
                }
                'Utf16' {
                    $points = [char[]]::new($rune.Utf16SequenceLength)
                    $rune.EncodeToUtf16($points) | Out-Null
                }
                Default { throw "'$Encoding' is not a valid encoding option." }
            }
            $chars.AddRange($points)
        }
    }
    end { return [string]::new($chars) }
}