Skip to content

Instantly share code, notes, and snippets.

@mklement0
Last active September 10, 2022 21:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mklement0/a700e598b615a1d432cc87e7291cdfec to your computer and use it in GitHub Desktop.
Save mklement0/a700e598b615a1d432cc87e7291cdfec to your computer and use it in GitHub Desktop.
PowerShell function that converts a given integer to another integer type unchecked, i.e. with overflow allowed.
<#
Prerequisites: Windows PowerShell v5.1 (possibly earlier; untested) or PowerShell (Core)
License: MIT
Author: Michael Klement <mklement0@gmail.com>
DOWNLOAD and INSTANT DEFINITION OF THE FUNCTION:
irm https://gist.github.com/mklement0/a700e598b615a1d432cc87e7291cdfec/raw/ConvertTo-IntegerUnchecked.ps1 | iex
The above directly defines the function below in your session and offers guidance for making it available in future
sessions too. To silence the guidance information, append 4>$null
CAVEAT: If you run this command *from a script*, you'll get a spurious warning about dot-sourcing, which you can ignore
and suppress by appending 3>$null. However, it's best to avoid calling this command from scripts, because later versions
of this Gist aren't guaranteed to be backward-compatible; howevever, you can modify the command to lock in a
*specific revision* of this Gist, which is guaranteed not to change: see the instructions at
https://gist.github.com/mklement0/880624fd665073bb439dfff5d71da886?permalink_comment_id=4296669#gistcomment-4296669
DOWNLOAD ONLY:
irm https://gist.github.com/mklement0/a700e598b615a1d432cc87e7291cdfec/raw > ConvertTo-IntegerUnchecked.ps1
The above downloads to the specified file, which you then need to dot-source to make the function available
in the current session (again, use 4>$null to silence the guidance information):
. ./ConvertTo-IntegerUnchecked.ps1
To learn what the function does:
* see the next comment block
* or, once downloaded and defined, invoke the function with -? or pass its name to Get-Help.
To define an ALIAS for the function, (also) add something like the following to your $PROFILE:
Set-Alias ctiu ConvertTo-IntegerUnchecked
#>
function ConvertTo-IntegerUnchecked {
<#
.SYNOPSIS
Converts a given integer to another integer type unchecked.
.DESCRIPTION
Converts a given integer to another integer unchecked, i.e. allowing
overflow to occur and using only as many of the bits as can fit in
the target type, starting with the least significant one.
If the types are of the same size but one is signed and the other unsigned,
conversion between negative and positive numbers can occur.
This command performs the equivalent of an `unchecked` operation in C#; e.g.:
ConvertTo-IntegerUnchecked ([long]::MaxValue) int
is equivalent to C#:
unchecked((int) long.MaxValue)
Note:
Invoke with -Verbose to see conversion details.
.PARAMETER Integer
The input number, which can be of any integer type or
a [double] containing an integer value.
IMPORTANT: Wrap negative literals such as -1 in (...)
.PARAMETER TargetType
The integer type to convert to; defaults to [int] (System.Int32)
.EXAMPLE
ConvertTo-IntegerUnchecked ([long]::MaxValue) int # -> -1
Converts the largest [long] value to an [int] value; the equivalent
of `unchecked((int) long.MaxValue)` in C#.
That is, 0x7fffffffffffffff is truncated to 0xffffffff, which, when
as a signed [int], is -1.
.EXAMPLE
ConvertTo-IntegerUnchecked 257 byte # -> 1
Converts (implied) [int] 257 to [byte]; the equivalent of
`unchecked((byte) 257)` in C#.
That is, 0x101 is truncated to 0x01.
.EXAMPLE
ConvertTo-IntegerUnchecked (-1) uint16 # -> 65535
Note the need to enclose the negative number literal in (...)
Converts (implied) [int] -1 to [uint16]; the equivalent of
`unchecked((UInt16) (-1))` in C#.
That is, 0xffffffff is truncated to 0xffff, which, when intepreted
as unsigned type [uint16], becomes positive value 65535
.EXAMPLE
ConvertTo-IntegerUnchecked ([uint16] 65535) int16 # -> -1
Converts [uint16] 65535 to [int16] -1 to [uint16]; the equivalent of
`unchecked((Int16) 65535)` in C#.
That is, 0xffff is reinterpreted as a *signed* number, and therefore
becomes negative -1.
.NOTES
Also stored in a Gist at https://gist.github.com/mklement0/a700e598b615a1d432cc87e7291cdfec
#>
param(
[Parameter(Mandatory, ValueFromPipeline)]
[object] $Integer, # Supports any integer type, including [bigint], as well as [double] values that contain integer values.
[Type] $TargetType = [int] # Ditto
)
begin {
# Determine the count of hex digits that make up the type. Each hex digit represents 4 bits, so the byte size must be muliplied by 2.
if ($TargetType -ne [bigint]) {
$targetHexDigitCount = try { 2 * [System.Runtime.InteropServices.Marshal]::SizeOf([type] $TargetType) } catch { throw "Unsupported target type: [$($TargetType.FullName)]"; return }
}
$verbose = 'Continue' -eq $VerbosePreference # so we can wrap costly Write-Verbose calls in a simple conditional
$maxSafeInt = [bigint]::Pow(2, 53) - 1
$minSafeInt = [bigint]::Pow(2, 53) - 1
}
process {
# Nothing to do if the input type is the same as the target type.
if ($Integer -is $TargetType) {
if ($verbose) { Write-Verbose "Target type is the same as input type: [$($TargetType.FullName)]" }
return $Integer
}
$originalInputType = $Integer.GetType()
# As a courtesy, we accept [double] values *if they happen to be integers*,
# because - unfortunately - PowerShell unconditionally widens overflowing calculations to [double];
# e.g. ([int]::MaxValue + 1).GetType().FullName yields 'System.Double'
# If the given [double] has a fractional part, the .ToString('x') attempt below will cause the desired error.
if ($Integer -is [double] -and [Math]::Truncate($Integer) -eq $Integer) {
if ($verbose) { Write-Verbose "Courtesy-converting a [double] containing an integer to [bigint]."}
# If the integer value stored in the [double] is outside the range where [double] can accurately represent them, warn.
if ([bigint]::Pow(2, 53) -le $Integer -or -[bigint]::Pow(2, 53) -ge $Integer) {
Write-Warning "A [double] value containing an integer value that cannot be accurately represented as an integer was supplied, possibly due to using the result of a PowerShell expression."
}
# If the [double] is the result of a PowerShell calculation, LOSS OF
$Integer = [bigint] $Integer
}
elseif ($Integer -is [decimal] -and $Integer.Scale -eq 0) { # Accept [decimal] values if they represent integers.
if ($verbose) { Write-Verbose "Converting [decimal] to [bigint] for technical reasons; this shouldn't affect the result."}
$Integer = [bigint] $Integer
}
# Get a hex. string representation.
# This serves as an implicit test whether the input number type is an integer type, as only
# integer types support .ToString('x')
$hexDigits = try { $Integer.ToString('x') } catch { Write-Error "Not an integer: $Integer ([$($Integer.GetType().FullName)]); wrap negative integer literals in (...)"; return }
# Nothing to truncate if the target type is [bigint] - any smaller integer type can fit.
if ($TargetType -is [bigint]) {
[bigint] $Integer
}
else {
# Truncation required.
# Take as many hex digits from the end of the string (each representing 4 bits) as required by the target type.
if ($targetHexDigitCount -lt $hexDigits.Length) {
$targetHexDigits = $hexDigits.Remove(0, $hexDigits.Length - $targetHexDigitCount)
if ($verbose) {
Write-Verbose (
[pscustomobject] @{
InputAsHex = '0x' + $hexDigits;
TargetAsHex = '0x' + $targetHexDigits
InputType = $originalInputType
TargetType = $TargetType
} | Out-String
)
}
}
else {
if ($verbose) {
if ($targetHexDigitCount -eq $hexDigits.Length) { # implies types of same size, but signed vs. unsigned
Write-Verbose "Target type [$($TargetType.FullName)] is the same size as the input type, [$($Integer.GetType().FullName)]; conversion between a negative and a positive number may occur."
}
else {
Write-Verbose "Target type [$($TargetType.FullName)] is larger than the input type, [$($Integer.GetType().FullName)]; no truncation required"
}
}
# Use all input hex digits.
$targetHexDigits = $hexDigits
}
# Prefix the hex digits with '0x' and use the -as operator to convert it to the target type.
# (except for reporting $null if the conversion fails, this is equivalent to casting to a type literal,
# e.g., [sbyte] '0xff' -> -1)
# Note: This -as operation should never return $null.
('0x' + $targetHexDigits) -as $TargetType
}
}
} # end of function
# --------------------------------
# GENERIC INSTALLATION HELPER CODE
# --------------------------------
# Provides guidance for making the function persistently available when
# this script is either directly invoked from the originating Gist or
# dot-sourced after download.
# IMPORTANT:
# * DO NOT USE `exit` in the code below, because it would exit
# the calling shell when Invoke-Expression is used to directly
# execute this script's content from GitHub.
# * Because the typical invocation is DOT-SOURCED (via Invoke-Expression),
# do not define variables or alter the session state via Set-StrictMode, ...
# *except in child scopes*, via & { ... }
if ($MyInvocation.Line -eq '') {
# Most likely, this code is being executed via Invoke-Expression directly
# from gist.github.com
# To simulate for testing with a local script, use the following:
# Note: Be sure to use a path and to use "/" as the separator.
# iex (Get-Content -Raw ./script.ps1)
# Derive the function name from the invocation command, via the enclosing
# script name presumed to be contained in the URL.
# NOTE: Unfortunately, when invoked via Invoke-Expression, $MyInvocation.MyCommand.ScriptBlock
# with the actual script content is NOT available, so we cannot extract
# the function name this way.
& {
param($invocationCmdLine)
# Try to extract the function name from the URL.
$funcName = $invocationCmdLine -replace '^.+/(.+?)(?:\.ps1).*$', '$1'
if ($funcName -eq $invocationCmdLine) {
# Function name could not be extracted, just provide a generic message.
# Note: Hypothetically, we could try to extract the Gist ID from the URL
# and use the REST API to determine the first filename.
Write-Verbose -Verbose "Function is now defined in this session."
}
else {
# Indicate that the function is now defined and also show how to
# add it to the $PROFILE or convert it to a script file.
Write-Verbose -Verbose @"
Function `"$funcName`" is now defined in this session.
* If you want to add this function to your `$PROFILE, run the following:
"``nfunction $funcName {``n`${function:$funcName}``n}" | Add-Content `$PROFILE
* If you want to convert this function into a script file that you can invoke
directly, run:
"`${function:$funcName}" | Set-Content $funcName.ps1 -Encoding $('utf8' + ('', 'bom')[[bool] (Get-Variable -ErrorAction Ignore IsCoreCLR -ValueOnly)])
"@
}
} $MyInvocation.MyCommand.Definition # Pass the original invocation command line to the script block.
}
else {
# Invocation presumably as a local file after manual download,
# either dot-sourced (as it should be) or mistakenly directly.
& {
param($originalInvocation)
# Parse this file to reliably extract the name of the embedded function,
# irrespective of the name of the script file.
$ast = $originalInvocation.MyCommand.ScriptBlock.Ast
$funcName = $ast.Find( { $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $false).Name
if ($originalInvocation.InvocationName -eq '.') {
# Being dot-sourced as a file.
# Provide a hint that the function is now loaded and provide
# guidance for how to add it to the $PROFILE.
Write-Verbose -Verbose @"
Function `"$funcName`" is now defined in this session.
If you want to add this function to your `$PROFILE, run the following:
"``nfunction $funcName {``n`${function:$funcName}``n}" | Add-Content `$PROFILE
"@
}
else {
# Mistakenly directly invoked.
# Issue a warning that the function definition didn't take effect and
# provide guidance for reinvocation and adding to the $PROFILE.
Write-Warning @"
This script contains a definition for function "$funcName", but this definition
only takes effect if you dot-source this script.
To define this function for the current session, run:
. "$($originalInvocation.MyCommand.Path)"
"@
}
} $MyInvocation # Pass the original invocation info to the helper script block.
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment