ExcelAnt.psm1

#Region '.\Private\Classes\ExcelColor.ps1' 0

function mangle_colorHexToRgb {
    # oh gosh. terrible hack.
    [OutputType('System.Drawing.Color')]
    param( [string]$HexStr )
    if ($HexStr.Length -eq 8) { write-error '8char wip' }

    $alpha = 0xff
    $strRgb = $HexStr.Substring(0, 6)
    $r, $g, $b = [rgbcolor]::FromRgb( $strRgb ).ToRgb()

    return [System.Drawing.Color]::FromArgb( $alpha, $r, $g, $b)
}
class ExcelColor {
    # future: convert to argument transformation type
    [int]$Red = 0xff
    [int]$Green = 0xff
    [int]$Blue = 0xff
    [int]$Alpha = 0xff
    [System.Drawing.Color]$Color = 'white'


    ExcelColor ( [string]$HexStr ) {
        $this.Color = [ExcelColor]::FromHex( $HexStr )

    }
    ExcelColor ( [int]$Red, [int]$Green, [int]$Blue ) {
        $This.Red = $Red
        $This.Green = $Green
        $This.Blue = $Blue
        $This.Color = [ExcelColor]::FromRGBA( $this.Red, $This.Green, $This.Blue )

    }
    ExcelColor ( [int]$Red, [int]$Green, [int]$Blue, [int]$Alpha ) {
        $This.Red = $Red
        $This.Green = $Green
        $This.Blue = $Blue
        $This.Alpha = $Alpha
        $This.Color = [ExcelColor]::FromRGBA( $this.Red, $This.Green, $This.Blue, $This.Alpha )
    }
    [string] ToString() {
        # outputs: [ExcelColor('#0f232490')]
        return [ExcelColor]::__repr__( $This )
    }
    hidden static [string] __repr__ (  [ExcelColor]$Object ) {
        # outputs: [ExcelColor('#0f232490')]
        return '[ExcelColor(''#{0:x}{1:x}{2:x}{3:x}'')]' -f @(
            $Object.Red.ToString('x')
            $Object.Green.ToString('x')
            $Object.Blue.ToString('x')
            $Object.Alpha.ToString('x')
        )
    }
    [string] ToHexString() {
        # outputs: #0f232490
        return '#{0:x}{1:x}{2:x}{3:x}' -f @(
            $this.Red.ToString('x')
            $this.Green.ToString('x')
            $this.Blue.ToString('x')
            $this.Alpha.ToString('x')
        )
    }

    # static [ExcelColor] FromHex( [string]$HexStr) {
    static [System.Drawing.Color] FromHex( [string]$HexStr) {
        [System.Drawing.Color]$res = _colorHexToRgb -HexStr $HexStr
        return $res
    }
    static [System.Drawing.Color] FromRGB( [int]$Red, [int]$Green, [int]$Blue ) {
        return [System.Drawing.Color]::FromArgb( $Red, $Green, $Blue)
    }
    static [System.Drawing.Color] FromRGBA( [int]$Red, [int]$Green, [int]$Blue, [int]$Alpha ) {
        return [System.Drawing.Color]::FromArgb( $Alpha, $Red, $Green, $Blue)
    }
}

# function mangle_l.NewColor {
# return [ExcelColor]
# }
#EndRegion '.\Private\Classes\ExcelColor.ps1' 80
#Region '.\Public\0.shared.ps1' 0
$script:__xantState ??= @{}
#EndRegion '.\Public\0.shared.ps1' 2
#Region '.\Public\Close-ExcelSession.ps1' 0
function Close-ExcelSession {
    [CmdletBinding()]
    param(
        # instead of closing, return the stored list
        [switch]$PassThru,
        # I'm not sure whether there's a reason benefit
        [switch]$Force
    )
    $state = $script:__xantState.openBookList
    if($PassThru) {
        return $state
    }

    'Books stored: {0}' -f @( $state.Count ) | Write-Information -infa 'Continue'

    foreach ($Book in $state) {
        try {
            Close-ExcelPackage -ExcelPackage $Book
            if ($Force) {
                $book.close()
                $book.dispose()
            }
        }
        catch {
            Write-Error "Failed closing`nnote: `nfuture will alias a label to a session.`ncurrently I never drop a reference explicitly`n$_" -ea 'continue'
        }
    }
}
#EndRegion '.\Public\Close-ExcelSession.ps1' 29
#Region '.\Public\Close-ExcelWindow.ps1' 0
function Close-ExcelWindow {
    <#
    .SYNOPSIS
        politely close all open excel windows. Modified files will prompt for you to save.
    .DESCRIPTION
        Polite version, does not create broken recovery files
    .EXAMPLE
        PS> Close-ExcelWindow
    #>

    [CmdletBinding()]
    [Alias('xL.Window.CloseAll')]

    $Ps = Get-Process *Excel* -ea ignore
    if ($PS) {
        $response = $ps.CloseMainWindow()
    }
    '{0} of {1} excel windows closed' -f @(
        ($response -eq $true).Count
        $ps.count
    ) | write-information -infa 'Continue'
}
#EndRegion '.\Public\Close-ExcelWindow.ps1' 22
#Region '.\Public\Get-ExcelAddress.ps1' 0

function Get-ExcelAddress {
    <#
    .NOTES
        future: allow autocomplete of currently open file
    todo:
        - [ ] arg completer enumerates tables, enumerates worksheets
    #>

    [Alias('xl.Addr.Lookup')]
    [CmdletBinding()]
    [OutputType('string')]
    param(
        # required param Package, worksheet, table name
        [Parameter(Mandatory, Position = 0)]
        [OfficeOpenXml.ExcelPackage]$Package,

        [ArgumentCompletions(
            'PayloSettings',
            'Changes',
            'New_JCUsers',
            'Previous_JCUsers',
            'Metrics',
            'errLog'
        )]
        [Parameter(Mandatory, Position = 1)]
        [string]$Worksheet,

        [ArgumentCompletions(
            'PayloSettings',
            'Changes',
            'New_JCUsers',
            'Previous_JCUsers',
            'Metrics',
            'errLog'
        )]
        [Parameter(Mandatory, Position = 2)]
        [string]$TableName
        # [switch]$TestIsValid # return bool
    )

    'Trying: {0}, {1}' -f @(
        $Worksheet, $TableName
    ) | Write-Verbose

    $sheetExists = -not $null -eq $Package.Workbook.Worksheets[$Worksheet]
    $tableExistsSomewhere = $TableName -in @($package.Workbook.Worksheets.Tables.Name)
    # $TableExistsInSameSheet = -not $null -eq $Package.Workbook.Worksheets[$Worksheet]
    $TableExistsInSameSheet = -not $null -eq $Package.Workbook.Worksheets[$Worksheet].Tables[$TableName]

    # try {
    # I was going to write: $Pkg.Workbook.Worksheets['Changes'].Tables['Changes'].Address.Address

    # write-verbose sheetExists, TableExistsSOmewhere, and TableExistsInSameSheet
    @{
        'SheetExists'            = $sheetExists
        'TableExistsSomewhere'   = $tableExistsSomewhere
        'TableExistsInSameSheet' = $TableExistsInSameSheet
    } | bdgLog -category ModuleEvent -message 'xl.Addr.Lookup' -PassThru
    | write-verbose

    return $Package.Workbook.Worksheets[ $Worksheet ].Tables[ $TableName ].Address.Address
}
#EndRegion '.\Public\Get-ExcelAddress.ps1' 63
#Region '.\Public\Get-ExcelPackageError.ps1' 0
# Import-Module ImportExcel

@'
initial
        15 0.009 cd 'G:\temp\xl'
        16 0.008 $whichXl = gcl |gi
        19 0.031 mkdir temp_exo
        20 0.009 cd .\temp_exo\
        24 0.012 $whichXl | cp -Destination '.'
        25 0.006 ls
        30 0.190 $src = gi JumpCloud_Changes.2023-02-24_12-22-02Z.xlsx
                    $dest = 'dir'
                    Expand-Archive $src -DestinationPath $dest
        31 0.008 ls
        33 1.207 code.cmd --new-window .\dir\
 
sprint
    - [ ] filepath from string or fileinfo
    - [ ] file but as an excel package
    - [ ] allow piping of either
    - [ ] maybe ValuesFromParameterType or ParameterName
        to auto coerce packages cleaner ?
'@
 | write-verbose -verbose

function Get-ExcelPackageError {
    # filepath to an excel file
    # copy to g:\temp\xl\auto'
    # expand archive to subdir
    [Alias('xl.Errors.Inspect')]
    param(
        [string]$ExcelPkg
        # [string]$subdir,
        # [string]$outpath,


    )

    throw 'NYI'
    ''

    # $coerceFilepath = Get-Item $ExcelPkg
    # }
}

# transformation type
#EndRegion '.\Public\Get-ExcelPackageError.ps1' 46
#Region '.\Public\New-ExcelTypeInfo.ps1' 0
function New-ExcelTypeInfo {
    <#
    .SYNOPSIS
        sugar to get different excel types, exposing class and type references
    .examples
        see also:
        PS> find-type -FullName *excel*
    #>

    [Alias('debug.TypeInfo.ExcelColor')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateSet(
            'ExcelColor'
        )][string]$TypeName
    )
    switch($TypeName) {
        'ExcelColor' {
            return [ExcelColor]
        }
        default {
            "UnhandledTypeName: $TypeName"
        }
    }
    return $null
}
#EndRegion '.\Public\New-ExcelTypeInfo.ps1' 27
#Region '.\Public\Start-ExcelSession.ps1' 0
# [Collections.Generic.List[Object]]$script:__xantState.curWorkbook ??= @{}
# $script:__xantState.curWorkbook ??= @{}
[Collections.Generic.List[Object]]$script:__xantState.openBookList ??= @()

function Start-ExcelSession {
    <#
    .SYNOPSIS
        create a new excel package, either replace existing or create a new file
    .DESCRIPTION
        this is called the first time, allowing you to auto rotate the file, once per run. Not new files every export
    .notes
    '
    todo
        - [ ] generates temp name, saves to a new sheet.
        - [ ] keep a reference based on a name or label
            which maps to the filesafetime
    '
    # future: track names to aliases
    #>

    write-warning 'still a WIP'
    $nextPkg = Invoke-SafeFileTimeTemplate -infa 'Continue'
    $state = $script:__xantState.openBookList
    $state.add( $nextPkg )
    'Number of items: {0}' -f @( $state.count )
    | write-verbose -Verbose

    foreach($book in $state) {
        Close-ExcelPackage -ExcelPackage $book -ea 'continue'
        $state.Remove( $book )
    }
    'hardcoded temp behavior: closing existing sessions' | write-debug

    # [Collections.Generic.List[Object]]$script:__xantState.openBookList.add( $nextPkg )
    # throw
    'StartedXlsx => "{0}"' -f @(
        $nextPkg.File.fullName
    )
    | write-information -infa 'Continue'
    return $nextPkg
}
#EndRegion '.\Public\Start-ExcelSession.ps1' 41
#Region '.\Public\transformTypes\coerce.ToExcelPackage.ps1' 0

function coerce.ToExcelPackage { #

    <#
    .synopsis
        convert filepaths or [ExcelPackage]s, resolving to a workbook
 
    - [ ] 1 file already exists, and is fileinfo
    - [ ] 2 file exists, is a string path
    - [ ] 3 file does not exist
    #>

    # [OutputType('System.IO.FileSystemInfo')]
    [CMdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, position = 0)]
        [object]$InputObject,

        #

        # # When creating missing items, the default type is File
        # [Parameter()]
        # [ArgumentCompletions('File', 'Directory', 'SymoblicLink', 'Junction', 'HardLink')]
        # [string]$ItemType = 'File',

        # # create file if not yet existing
        [switch]$CreateIfMissing
        # [switch]$Mandatory

    )
    begin {
        # $null = $InputObject
    }
    process {

        <#
            DirectoryInfo isa System.IO.FileSystemInfo
            FileInfo isa System.IO.FileSystemInfo
        #>

        if($InputObject -is 'OfficeOpenXml.ExcelPackage') {
            write-verbose 'already an ExcelPackage'
            return $InputObject
        }

        throw 'left off here
        1] simple coerce path to package if existing
        create if requested
        commit
        '


        if ($InputObject -is 'string') {
            $alreadyExists = Test-Path $InputObject
            '{0} is string, and exists? {1}' -f @(
                $InputObject | Join-String -double
                $alreadyExists
            ) | write-verbose

            'AlreadyExists? {0}. CreateIfMissing? {1}' -f @(
                $AlreadyExists,
                $CreateIfMissing
            ) | write-verbose

            if ($AlreadyExists) {
                return $InputObject | Get-Item
            }
            'Does not exist. CreateIfMissing? {0} using type: {1}' -f @(
                $CreateIfMissing
                $ItemType
            )| write-verbose


            if($CreateIfMissing) {
                'creating: {0}' -f @(
                    $InputObject | Join-String -double
                ) | write-verbose

                New-Item -ItemType $ItemType -path $InputObject -Force -passThru
                return
            }

            if(-not $AlreadyExists -and -not $CreateIfMssing){
                $PSCmdlet.WriteError(
                    [Management.Automation.ErrorRecord]::new(
                        [ArgumentException]::new(
                            'File does not exist, and CreateIfMissing is not set'
                        ),
                        'FileDoesNotExist',
                        [Management.Automation.ErrorCategory]::InvalidArgument,
                        $InputObject
                    ))
            }
            # elkse, missing with create

        }

        'unhandled type name: [{0}]' -f @(
            $InputObject.GetType().Name
        ) | Write-Error



        # return [IO.FileInfo]::new($InputObject)

        switch ($InputObject) {
            { $_ -is [System.IO.FileInfo] } {
                $InputObject
            }
            { $_ -is [string] } {
                [System.IO.FileInfo]::new($InputObject)
            }
            default {
                [System.IO.FileInfo]::new($InputObject)
            }
        }
    }
    end {}

}
#EndRegion '.\Public\transformTypes\coerce.ToExcelPackage.ps1' 118
#Region '.\Public\transformTypes\coerce.ToFileInfo.ps1' 0




function coerce.ToFileInfo { # build might work if name is coerce-ToFileSystemINfo

    <#
    .SYNOPSIS
        coerce strings, [FileInfo], [ExcelPackage]s to [IO.FileInfo] instances
    .link
        ExcelAnt\coerce.ToFileInfo
    .link
        ExcelAnt\coerce.ToExcelPackage
    .DESCRIPTION
        coerce/resolve data types
    Cases that are* supported
 
    ask for opinion on Write-Error, vs throw, vs CmdletError
    especialy since pipeline is involved
 
    does write-error -ea 'continue' require a return, then ?
        does PSCmdlet Error recordscontinue' require a return too?
 
 
    - [ ] 1 file already exists, and is fileinfo
    - [ ] 2 file exists, is a string path
    - [ ] 3 file does not exist
    #>

    [OutputType('System.IO.FileInfo')]
    [CMdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, position = 0)]
        [object]$InputObject,

        [Parameter()]
        [OfficeOpenXml.ExcelPackage]$ExcelPackage,

        # future: what is the cleaner way to pipe multiple non-objects, does 'ValueFromPipelineByPropertyName' make it worse ?
        # [Parameter(mandatory, ValueFromPipeline, Position=0, ValueFromPipelineByPropertyName=)]

        # When creating missing items, the default type is File
        [Parameter()]
        [ArgumentCompletions('File', 'Directory', 'SymoblicLink', 'Junction', 'HardLink')]
        [string]$ItemType = 'File',

        # create file if not yet existing
        [switch]$CreateIfMissing,
        [switch]$Mandatory

    )
    begin {
        # $null = $InputObject
    }
    process {

        <#
            DirectoryInfo isa System.IO.FileSystemInfo
            FileInfo isa System.IO.FileSystemInfo
        #>


        if ($InputObject -is 'IO.FileSystemInfo') {
            Write-Verbose 'already a FileInfo instance'
            return $InputObject
        }
        if ($InputObject -is 'OfficeOpenXml.ExcelPackage') {
            write-verbose 'WorkBook has a FileSystemInfo'
            return $InputObject.File
        }
        if($ExcelPackage) {
            write-verbose 'WorkBook (from type param) has a FileSystemInfo'
            return $ExcelPackage.File
        }
        if ($InputObject -is 'string') {
            $alreadyExists = Test-Path $InputObject
            '{0} is string, and exists? {1}' -f @(
                $InputObject | Join-String -double
                $alreadyExists
            ) | write-verbose

            'AlreadyExists? {0}. CreateIfMissing? {1}' -f @(
                $AlreadyExists,
                $CreateIfMissing
            ) | write-verbose

            if ($AlreadyExists) {
                return $InputObject | Get-Item
            }
            'Does not exist. CreateIfMissing? {0} using type: {1}' -f @(
                $CreateIfMissing
                $ItemType
            )| write-verbose


            if($CreateIfMissing) {
                'creating: {0}' -f @(
                    $InputObject | Join-String -double
                ) | write-verbose

                New-Item -ItemType $ItemType -path $InputObject -Force -passThru
                return
            }

            if(-not $AlreadyExists -and -not $CreateIfMssing){
                $PSCmdlet.WriteError(
                    [Management.Automation.ErrorRecord]::new(
                        [ArgumentException]::new(
                            'File does not exist, and CreateIfMissing is not set'
                        ),
                        'FileDoesNotExist',
                        [Management.Automation.ErrorCategory]::InvalidArgument,
                        $InputObject
                    ))
            }
            # elkse, missing with create

        }

        'unhandled type name: [{0}]' -f @(
            $InputObject.GetType().Name
        ) | Write-Error



        # return [IO.FileInfo]::new($InputObject)

        switch ($InputObject) {
            { $_ -is [System.IO.FileInfo] } {
                $InputObject
            }
            { $_ -is [string] } {
                [System.IO.FileInfo]::new($InputObject)
            }
            default {
                [System.IO.FileInfo]::new($InputObject)
            }
        }
    }
    end {}

}
#EndRegion '.\Public\transformTypes\coerce.ToFileInfo.ps1' 141
#Region '.\Public\utils\Convert-RotateObjectProperty.ps1' 0
function Convert-RotateObjectProperty {
    <#
    .SYNOPSIS
        pivot into key value pairs
    .example
        Ps> b.rotateProperties $this | to-xl
    #>

    [Alias('xl.Object.RotateProperties')]
    param(
        # takes one PSObject and rotates the properties
        [Parameter(Mandatory, Position = 0)]
        [Object]$InputObject
    )
    $InputObject.PSObject.Properties | sort Name | %{
        $meta = [ordered]@{}
        $meta[ 'Property' ] = $_.Name
        $meta[ 'Value' ] = $_.Value
        [pscustomobject]$meta
    }
}
#EndRegion '.\Public\utils\Convert-RotateObjectProperty.ps1' 21
#Region '.\Public\utils\Enum.MembersFromTypeInfo.ps1' 0
function Enum.MembersFromTypeInfo {
    <#
    .SYNOPSIS
        list properties/keys from a [type] type, no objects yet.
    .example
        PS>
 
        $stuff = (gi .), (gi .\README.md)
        $tinfos = $stuff | % GetType | sort -Unique { $_.FullName }
        $tinfos | Enum.MembersFromTypeInfo | To-Xl
    #>

    [CmdletBinding()]
    [Alias('xl.enumerateKeysOfType')]
    param(
        [Parameter(ValueFromPipeline, Position=0, Mandatory)]
        [object] $InputObject
    )

    process {
        if ($_ -isnot 'type') { Write-Error "Arg is not a type: $_ " }
        $_ | ClassExplorer\Find-Member | ForEach-Object Name | Sort-Object -Unique
    }
}
#EndRegion '.\Public\utils\Enum.MembersFromTypeInfo.ps1' 24
#Region '.\Public\utils\Filter-DropBlankProperties.ps1' 0
# function dropBlankKeys {
    function Filter-DropBlankProperties {
    <#
    .SYNOPSIS
        enumerates to mutate object, by dropping keys with blank values
    .DESCRIPTION
    #>

    [Alias('xl.Object.DropBlankProperties')]
    [CmdletBinding()]
    [OutputType('Hashtable')]
    param(
        [Parameter(mandatory)]
        [hashtable]$InputHashtable,

        [switch]$NoMutate
    )
    $strUserKeyId = '[User={2} <CoId={0}, EmpId={1}>]' -f @(
        $finalObj.companyId
        $finalObj.employeeIdentifier
        $finalObj.userName
    )
    if ($NoMutate) {
        $targetHash = [hashtable]::new( $InputHashtable )
    }
    else {
        $targetHash = $InputHashtable
    }

    $msg = $targetHash.GetEnumerator()
    | Where-Object { [string]::IsNullOrEmpty( $_.Value ) }
    | ForEach-Object Name | Sort-Object -Unique
    | Join-String -sep ', ' -op "dropped blank fields on ${strUserKeyId}: "
    @{
        Message = $msg
    }
    | bdgLog -Category DataIntegrity -Message $msg -PassThru
    | Write-Verbose


    $toDrop = $targetHash.GetEnumerator()
    | Where-Object { [string]::IsNullOrEmpty( $_.Value ) }
    | ForEach-Object Name

    foreach ($k in $toDrop) {
        $targetHash.Remove( $k )
    }
    return $targetHash

}

# label '=== Reached final core_config.ps1 300.' $PSCommandPath
# | write-warning
<# AutoGen: EndOf: C:\Users\cppmo_000\SkyDrive\Documents\2022\client_BDG\self\bdg_lib\src_static\core_config.ps1 #>
<# AutoGen: Begin: C:\Users\cppmo_000\SkyDrive\Documents\2022\client_BDG\self\bdg_lib\src_static\stand_alone_entry.ps1 #>
#EndRegion '.\Public\utils\Filter-DropBlankProperties.ps1' 55
#Region '.\Public\utils\GetRandomColor.ps1' 0

# or import nancy?

function Get-RandomColor {
    <#
    .synopsis
        random 24bit color
    .DESCRIPTION
        Output is raw random, no smoothing.
    .notes
        future: differentiate between Write-Color and Get-Color
 
        colors are objects to mutate
        write is ansi escapes as string
 
        see [pansies.RGBColor] as an idea, but,
        the new behavior of 7.3 gives new posibilities, see about_AnsiTerminal
    .EXAMPLE
        Pwsh> Get-RandomNancyColor
        Pwsh> Get-RandomNancyColor -Count 10
    #>

    [Alias('xl.Rand.AnsiColor')]
    [OutputType('PoshCode.Pansies.RgbColor')]
    [CmdletBinding()]
    param(
        # Return more than one color
        [Alias('Count')][int]$TotalCount = 1
    )

    foreach ($i in 1..$TotalCount) {
        $r, $g, $b = Get-Random -Count 3 -Minimum 0 -Maximum 255
        $PSStyle.Background.FromRgb($r, $g, $b)
        # or: [PoshCode.Pansies.RgbColor]::new($r, $g, $b)
    }
}
#EndRegion '.\Public\utils\GetRandomColor.ps1' 36
#Region '.\Public\utils\Invoke-SafeFileTimeTemplate.ps1' 0
function Invoke-SafeFileTimeTemplate {

    <#
    .SYNOPSIS
        timenow for safe filepaths: "2022-08-17_12-46-47Z"
    .notes
        distinct values to the level of a full second
    #>

    # [Alias('xl.New.Safetime')]
    [OutputType('OfficeOpenXml.ExcelPackage')]
    [Alias(
        'xl.New.SafeTime',
        'New-FileTimeTemplate'
    )]
    [CmdletBinding()]
    param(
        # [parameter(Mandatory, Position = 0)]
        # [string]$Label,

        [Parameter(Position = 0)]
        # [Parameter(Position = 1)]
        [ArgumentCompletions(
            'export-{0}.xlsx',
            './.output/export-{0}.xlsx',
            '{0}.xlsx'
        )]
        [string]$NameTemplate,

        # Root output directory, if not template
        [ArgumentCompletions(
            'G:\temp\xl',
            '([IO.Path]::GetTempPath())'
        )]
        [Parameter(Position=1)]
        # [Parameter(Position=2)]
        $RelativeTo
    )

    $Final_NameTemplate = $NameTemplate ?? '{0}.xlsx'
    # $Final_RelativeTo = $RelativeTo ?? ([IO.Path]::GetTempPath())
    $Final_RelativeTo = $RelativeTo ?? 'g:\temp\xl\.output'
    $Final_fullName = Join-Path $Final_RelativeTo $Final_NameTemplate
    $Render = $Safe -f @( SafeFiletimeString )

    # (Get-Date).ToString('u') -replace '\s+', '_' -replace ':', '-'
    # always new
    if(test-path $render ) {
        'Unexpected, filetime exists. $Render = "{0}"' -f @(
            $render
        ) | Write-Error
    }
    # Ensure full filepath exists, and delete.
    # Then return a new package
    New-Item -itemtype file -path $render -force -ea 'ignore'
    Remove-Item  -path $render -force -ea 'ignore'
    $pkg = Open-ExcelPackage -Path $Render -Create -Verbose
    'Created new filetime template: {0}' -f @(
        $Pkg.File.FullName | Join-String -double
     ) | Write-Information -infa 'Continue'
    return $Pkg
}

#EndRegion '.\Public\utils\Invoke-SafeFileTimeTemplate.ps1' 63
#Region '.\Public\utils\SafeFileTimeString.ps1' 0

function SafeFileTimeString {

    <#
    .SYNOPSIS
        timenow for safe filepaths: "2022-08-17_12-46-47Z"
    .notes
        distinct values to the level of a full second
    #>

    # [Alias('xl.New.Safetime')]
    [CmdletBinding()]
    param(
        # [Parameter(Mandatory, Position = 0)]
    )

    (Get-Date).ToString('u') -replace '\s+', '_' -replace ':', '-'
}
#EndRegion '.\Public\utils\SafeFileTimeString.ps1' 18