Private/ConvertTo-WinClatTimeSpan.ps1

Function ConvertTo-WinClatTimeSpan {

    <#

    .SYNOPSIS
        Converts a netsh lifetime string to a TimeSpan.

    .DESCRIPTION
        Converts a lifetime value from 'netsh.exe interface clat show instance' output, such as the remote prefix remaining lifetime, to a TimeSpan.

        Observed formats: 0s, 18h34s, 17h59m29s, 18h8m3s, 18h11m51s. Components are omitted when zero.
        Pattern: '^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$' (the day component is defensive).
        Returns $null when the value cannot be parsed, is empty, or is too large for a TimeSpan; the caller keeps the raw string.

        This function must also be available in the remote session. See DESIGN.md, sections 4.3 and 5.5.

    .PARAMETER Value
        The lifetime string to convert, for example '17h59m29s'. Leading and trailing whitespace is ignored. An empty string returns $null.

    .INPUTS
        None. This function does not accept pipeline input.

    .OUTPUTS
        System.TimeSpan, or $null when the value cannot be parsed.

    .EXAMPLE
        ConvertTo-WinClatTimeSpan -Value '17h59m29s'

        Returns a TimeSpan of 17 hours, 59 minutes, and 29 seconds.

    .EXAMPLE
        ConvertTo-WinClatTimeSpan -Value 'infinite'

        Returns $null because the value does not match the lifetime format.

    .LINK
        https://learn.microsoft.com/dotnet/api/system.timespan

    .NOTES
        Version: 1.0.0
        Creation Date: October 28, 2026
        Last Updated: October 28, 2026
        Author: Richard Hicks
        Organization: Richard M. Hicks Consulting, Inc.
        Contact: rich@richardhicks.com
        Website: https://www.richardhicks.com/

    #>


    [CmdletBinding()]
    [OutputType([TimeSpan])]

    Param (

        [Parameter(Mandatory)]
        [AllowEmptyString()]
        [string]$Value

    )

    $Match = [regex]::Match($Value.Trim(), '^(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?$')

    # Every component is optional, so an empty string matches. Require at least one component.
    If (-not $Match.Success -or $Match.Length -eq 0) {

        Return $null

    }

    $Multipliers = @(86400, 3600, 60, 1)
    [double]$TotalSeconds = 0

    For ($Index = 0; $Index -lt $Multipliers.Count; $Index++) {

        $Group = $Match.Groups[$Index + 1]

        If ($Group.Success) {

            $TotalSeconds += [double]$Group.Value * $Multipliers[$Index]

        }

    }

    # Values too large for a TimeSpan are treated as unparsable.
    Try {

        [TimeSpan]::FromSeconds($TotalSeconds)

    }

    Catch {

        $null

    }

}