Providers/FreeIPA/Private/ConvertFrom-FreeIPADateTime.ps1

function ConvertFrom-FreeIPADateTime {
    <#
    .SYNOPSIS
        Reads a FreeIPA date value into a DateTimeOffset

    .DESCRIPTION
        FreeIPA's JSON carries a date as an object, {"__datetime__": "20261210024734Z"}, and
        the generalized-time string inside it is always UTC. A value may also arrive as that
        bare string, as a list of one such object - most attributes are multi-valued in LDAP -
        or as nothing at all. Every shape is handled here, once, and anything unreadable is
        $null rather than an exception, because a report row with a blank expiry is more
        useful than a report that stopped.

    .PARAMETER Value
        The value as it came out of ConvertFrom-Json.

    .OUTPUTS
        System.DateTimeOffset, or $null.

    .EXAMPLE
        PS> ConvertFrom-FreeIPADateTime -Value $user.krbpasswordexpiration

        DESCRIPTION: Reads a user's password expiry
        OUTPUT: The expiry as a DateTimeOffset in UTC
        USE CASE: The report

    .NOTES
        Author: Jeffrey Stuhr
        Blog: https://www.techbyjeff.net
        LinkedIn: https://www.linkedin.com/in/jeffrey-stuhr-034214aa/
    #>


    [CmdletBinding()]
    [OutputType([DateTimeOffset])]
    param(
        [Parameter()]
        [AllowNull()]
        [object]$Value
    )

    if ($null -eq $Value) { return $null }
    if ($Value -is [array]) {
        if ($Value.Count -eq 0) { return $null }
        $Value = $Value[0]
    }
    if ($Value -is [DateTimeOffset]) { return $Value }
    if ($Value -is [DateTime]) { return [DateTimeOffset]::new($Value.ToUniversalTime(), [TimeSpan]::Zero) }

    $text = $null
    if ($Value -is [string]) { $text = $Value }
    elseif ($Value.PSObject.Properties['__datetime__']) { $text = [string]$Value.__datetime__ }
    if ([string]::IsNullOrWhiteSpace($text)) { return $null }

    $parsed = [DateTime]::MinValue
    if ([DateTime]::TryParseExact($text, 'yyyyMMddHHmmssZ', [Globalization.CultureInfo]::InvariantCulture,
            [Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal, [ref]$parsed)) {
        return [DateTimeOffset]::new($parsed, [TimeSpan]::Zero)
    }
    try {
        return [DateTimeOffset]::Parse($text, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::AssumeUniversal)
    }
    catch {
        return $null
    }
}