Private/Resolve-VmPowerTimeZone.ps1

function Resolve-VmPowerTimeZone {
    <#
    .SYNOPSIS
    A TimeZoneInfo for an id written in either form, whatever platform is asking.

    .DESCRIPTION
    Windows ids are the portable form, and that is not a preference - it is the only shape that
    works in both places a schedule lives.

    Measured with a probe runbook on 2026-09-10. The Azure Automation PowerShell 7.2 sandbox is
    Windows Server 2019, build 17763, which is version 1809 - older than the 1903 that first shipped
    ICU with Windows. .NET there runs in NLS mode (`GlobalizationMode.UseNls` is `True`), so the CLDR
    data is absent and **both** conversion directions return false:

        IANA->Windows Europe/Zurich ok=False
        Windows->IANA W. Europe Standard Time ok=False

    So `Europe/Zurich` cannot be translated inside a sandbox, only rejected. A Windows id resolves
    there, and also resolves on macOS and Linux, where .NET ships ICU and accepts both forms. That
    asymmetry decides the storage format: schedules keep Windows ids.

    This function still tries a translation, because on a machine with ICU it turns an IANA id into
    something the sandbox can use, which is what makes `New-VmPowerSchedule -TimeZone 'Europe/Zurich'`
    work at all.

    .PARAMETER Id
    A time zone id in the Windows form ('W. Europe Standard Time') or the IANA form
    ('Europe/Zurich').
    #>

    [CmdletBinding()]
    [OutputType([System.TimeZoneInfo])]
    param(
        [Parameter(Mandatory)]
        [string]$Id
    )

    try { return [System.TimeZoneInfo]::FindSystemTimeZoneById($Id) }
    catch { Write-Verbose "Time zone '$Id' is not known to this platform as written; trying the other form." }

    # Not resolvable as written. Try the other form - IANA to Windows in a sandbox, Windows to IANA
    # on a laptop - and say which translation worked, because a silent one is hard to debug later.
    $translated = ''
    if ([System.TimeZoneInfo]::TryConvertIanaIdToWindowsId($Id, [ref]$translated) -and $translated) {
        try {
            $zone = [System.TimeZoneInfo]::FindSystemTimeZoneById($translated)
            Write-Verbose "Time zone '$Id' resolved as '$translated' on this platform."
            return $zone
        }
        catch { Write-Verbose "'$translated' is not known here either." }
    }

    $translated = ''
    if ([System.TimeZoneInfo]::TryConvertWindowsIdToIanaId($Id, [ref]$translated) -and $translated) {
        try {
            $zone = [System.TimeZoneInfo]::FindSystemTimeZoneById($translated)
            Write-Verbose "Time zone '$Id' resolved as '$translated' on this platform."
            return $zone
        }
        catch { Write-Verbose "'$translated' is not known here either." }
    }

    $hint = if ($Id -match '/') {
        "'$Id' is an IANA id. This runtime has no CLDR data to translate it - Azure Automation runs " +
        'on Windows Server 2019, where .NET falls back to NLS. Store the Windows form instead: ' +
        "New-VmPowerSchedule normalises it for you when it runs on a machine that can, so re-create " +
        'the schedule from a laptop rather than editing the variable by hand.'
    }
    else {
        "'$Id' is not a time zone this runtime knows, in either form."
    }
    throw "Time zone: $hint (platform: $([System.Environment]::OSVersion.VersionString))"
}