What's the best way to round down to nearest whole number in PowerShell?
I am trying [math]::truncate but its not giving me predictable results.
Example:
$bla = 17.2/0.1
[math]::truncate($bla)
outputs 171 instead of the expected 172!
$bla = 172
[math]::truncate($bla)
outputs 172
I just need something that works.... and must always round down (i.e round($myNum + 0.5) won't work due to baker's rounding which may round up if the number has a 0.5 component).
Ah, I see. Looks like the datatype needs to be decimal:
[decimal] $de = 17.2/.1
[double] $db = 17.2/.1
[math]::floor($de)
172
[math]::floor($db)
171
http://msdn.microsoft.com/en-us/library/system.math.floor(v=vs.85).aspx
The Math::Floor function combined with [decimal] declaration should give you the results you want.
[Math]::Floor([decimal](17.27975/0.1))
returns = 172
The issue you are encountering with the original 17.2/0.1 division example is due to inaccuracy in the floating-point representation of the given decimal values (as mentioned in Joey's comment on another answer). You can see this in PowerShell by examining the round-trip representation of the final value:
PS> $bla = 17.2/0.1
PS> $bla.GetType().FullName
System.Double
PS> $bla.ToString()
172
PS> $bla.ToString('r')
171.99999999999997
A simple way to get around this is to declare the result as int, as PowerShell will automatically round to the the result to the nearest integer value:
PS> [int]$bli = 17.2/0.1
PS> $bli.GetType().FullName
System.Int32
PS> $bli.ToString()
172
Note that this uses the default .NET method of MidpointRounding.ToEven (also known as banker's rounding). This has nice statistical properties when tabulating large numbers of numeric values, but can also be changed to the simpler away-from-zero method:
function round( $value, [MidpointRounding]$mode = 'AwayFromZero' ) {
[Math]::Round( $value, $mode )
}
PS> [int]3.5
4
PS> [int]4.5
4
PS> round 3.5
4
PS> round 4.5
5
Another option is to use a more accurate representation for the original values, which will avoid the issue entirely:
PS> $bld = [decimal]17.2/0.1
PS> $bld.GetType().FullName
System.Decimal
PS> $bld.ToString()
172
[Math]::floor($x) is the built-in way to do it.
Just be aware of how it will behave with negative numbers. [Math]::floor(5.5) returns 5, but [Math]::floor(-5.5) returns -6.
If you need the function to return the value closest to zero, you'll need:
If ($x -ge 0) {
[Math]::Floor($x)
} Else {
[Math]::Ceiling($x)
}
Related
I've tried really hard not ask this question, but I keep coming back to it as I'm not sure if I'm doing everything as efficiently as I can or if there might be problems under the hood. Basically, I have a CSV file that contains a number field, but it includes a decimal and values out to the ten-thousandths place, e.g. 15.0000. All I need to do is convert that to a whole number without the decimal place.
I came across a related question here, but the selected answer seems to cast doubt on casting the string representation directly to an integer data type - without explaining why.
Simply casting the string as an int won't work reliably. You need to convert it to an int32.
I've haven't had much luck getting the [System.Convert] method to work, or doing something like $StringNumber.ToInt32(). I realize that once I save the data back to the PSCustomObject they'll be stored as strings, so at the end of the day maybe I'm making this even more complicated than necessary for my use case and I just need to reformat $StringNumber...but even that has caused me some problems.
Any ideas on why casting wouldn't be reliable or better ways to handle this in my case?
Examples of what I've tried:
PS > $StringNumber = '15.0000'
PS > [Convert]::ToInt32($StringNumber)
#MethodInvocationException: Exception calling "ToInt32" with "1" argument(s): "Input string was not in a correct format."
PS > [Convert]::ToInt32($StringNumber, [CultureInfo]::InvariantCulture)
#MethodInvocationException: Exception calling "ToInt32" with "2" argument(s): "Input string was not in a correct format."
PS > $StringNumber.ToInt32()
#MethodException: Cannot find an overload for "ToInt32" and the argument count: "0".
PS > $StringNumber.ToInt32([CultureInfo]::InvariantCulture)
#MethodInvocationException: Exception calling "ToInt32" with "1" argument(s): "Input string was not in a correct format."
PS > $StringNumber.ToString("F0")
#MethodException: Cannot find an overload for "ToString" and the argument count: "1".
PS > $StringNumber.ToString("F0", [CultureInfo]::CurrentCulture)
#MethodException: Cannot find an overload for "ToString" and the argument count: "2".
PS > "New format: {0:F0}" -f $StringNumber
#New format: 15.0000
So basically what I've come up with is:
Someone in 2014 said casting my string to an int wouldn't work reliably, even though it seems like the Cast operator is actually doing a conversion
The ToInt32 methods don't like strings with decimals as the input
Apparently String.ToString Method is useless
Thanks to String.ToString and the processing order of composite formatting, simple "reformatting" of my string representation won't work
In summary: Is there a way to safely cast my $StringNumber into a whole number, and, if so, what's the most efficient way to do it on a large dataset?
Bonus Challenge:
If anyone can make this work using the ForEach magic method then I'll buy you a beer. Here's some pseudo code that doesn't work, but would be awesome if it did. As far as I can figure out, there's no way to reference the current item in the collection when setting the value of a string property
#This code DOES NOT work as written
PS > $CSVData = Import-Csv .\somedata.csv
PS > $CSVData.ForEach('StringNumberField', [int]$_.StringNumberField)
If your string representation can be interpreted as a number, you can cast it to an integer, as long as the specific integer type used is large enough to accommodate (the integer portion of) the value represented (e.g. [int] '15.0000')
A string that can not be interpreted as a number or represents a number that is too large (or small, for negative numbers) for the target type, results in a statement-terminating error; e.g. [int] 'foo' or [int] '444444444444444'
Note that PowerShell's casts and implicit string-to-number conversions use the invariant culture, which means that only ever . is recognized as the decimal mark (and , is effectively ignored, because it is interpreted as the thousands-grouping symbol), irrespective of the culture currently in effect (as reflected in $PSCulture).
As for integer types you can use (all of them - except the open-ended [bigint] type - support ::MinValue and ::MaxValue to determine the range of integers they can accommodate; e.g. [int]::MaxValue)
Signed integer types: [sbyte], [int16], [int] ([int32]), [long] ([int64]), [bigint]
Unsigned integer types: [byte], [uint16], [uint] ([uint32]), [ulong] ([uint64]) - but note that PowerShell itself uses only signed types natively in its calculations.
Casting to an integer type performs half-to-even midpoint rounding, which means that a string representing a value whose fractional part is .5 is rounded to the nearest even integer; e.g. [int] '1.5' and [int] '2.5' both round to 2.
To choose a different midpoint rounding strategy, use [Math]::Round() with a System.MidpointRounding argument; e.g.:
[Math]::Round('2.5', [MidPointRounding]::AwayFromZero) # -> 3
To unconditionally round up or down to the nearest integer, use [Math]::Ceiling(), [Math]::Floor(), or [Math]::Truncate(); e.g.:
[Math]::Ceiling('2.5') # -> 3
[Math]::Floor('2.5') # -> 2
[Math]::Truncate('2.5') # -> 2
#
[Math]::Ceiling('-2.5') # -> -2
[Math]::Floor('-2.5') # -> -3
[Math]::Truncate('-2.5') # -> -2
Note: While the resulting number is conceptually an integer, technically it is a [double] or - with explicit [decimal] or integer-number-literal input - a [decimal].
As for the bonus challenge:
With an integer-type cast:
[int[]] (Import-Csv .\somedata.csv).StringNumberField
Note: (Import-Csv .\somedata.csv).StringNumberField.ForEach([int]) would work too, but offers no advantage here.
With a [Math]::*() call and the .ForEach() array method:
(Import-Csv .\somedata.csv).StringNumberField.ForEach(
{ [Math]::Round($_, [MidPointRounding]::AwayFromZero) }
)
Casting [int] as you explained, is something that would work in most cases, however it is also prone to errors. What if the number is higher than [int]::MaxValue ? The alternative you could use to avoid the exceptions would be to use the -as [int] operator however there is another problem with this, if the value cannot be converted to integer you would be getting $null as a result.
To be safe that the string will be converted and you wouldn't get null as a result first you need to be 100% sure that the data you're feeding is correct or assume the worst and use [math]::Round(..) in combination with -as [decimal] or -as [long] or -as [double] (∞) to round your numbers:
[math]::Round('123.123' -as [decimal]) # => 123
[math]::Round('123.asd' -as [decimal]) # => 0
Note: I'm using round but [math]::Ceiling(..) or [math]::Floor(..) or [math]::Truncate(..) are valid alternatives too, depending on your expected output.
Another alternative is to use [decimal]::TryParse(..) however this would throw if there ever be something that is not a number:
$StringNumber = '15.0000'
$ref = 0
[decimal]::TryParse( $StringNumber, ([ref]$ref) )
[math]::Round($ref) # => 15
Using Hazrelle's advise would work too but again, would throw an exception for invalid input or "Value was either too large or too small for an Int32."
[System.Decimal]::ToInt32('123123123.123') # => 123123123
As for the Bonus Challenge, I don't think it's possible to cast and then set the rounded values to your CSV on just one go using ForEach(type convertToType), and even if it was, it could also bring problems because of what was mentioned before:
$csv = #'
"Col1","Col2"
"val1","15.0000"
"val2","20.123"
"val3","922337203685477.5807"
'# | ConvertFrom-Csv
$csv.Col2.ForEach([int])
Cannot convert argument "item", with value: "922337203685477.5807", for "Add" to type "System.Int32": "Cannot convert value "922337203685477.5807" to type "System.Int32".
Using .foreach(..) array method combined with a script block would work:
$csv.ForEach({
$_.Col2 = [math]::Round($_.Col2 -as [decimal])
})
In case you wonder why not just use [math]::Round(..) over the string and forget about it:
[math]::Round('123.123') # => 123 Works!
But what about:
PS /> [math]::Round([decimal]::MaxValue -as [string])
7.92281625142643E+28
PS /> [math]::Round([decimal]([decimal]::MaxValue -as [string]))
79228162514264337593543950335
I am literally just trying to convert a string with week and day information to numbers and store it as variable, yet something really funky is happening, as of now, I have tested this behavior in 4 PCs, and in Powershell 5 and 7 its happening all over the place.
$UP_Down = "6w0d"
[int]$weeks = if ($Up_Down -match "w"){$Up_Down[$($Up_Down.IndexOf('w')-1)]}Else{0}
[int]$days = if ($Up_Down -match "d"){$Up_Down[$Up_Down.IndexOf('d')-1]}Else{0}
[int]$totaldays = (7 * $weeks) + $days
Now the data from the initial variable is obviously 6 weeks and 0 days to which I have to convert to 42 Total days (this is just an example, its happening regardless of combination)
However the following is the Funky results I get which I have elaborated by Write-Output
Weeks If statement results by itself 6
Weeks variable results 54
days If statement results by itself 0
days variable results 48
totaldays variable results are 426
The problem occurs regardless of what numeric datatype I use
Ironically the Variables have the correct value if i DO NOT assign datatype to them, BUT ,
the moment it hits (7*$weeks) even IF the $weeks is correct the value outputted 426, and remember no [int]etc anywhere
What am I doing wrong?
The problem is, that you are not converting the number inside the string "6" to the number 6, but the character '6' to its value according to the underlying character encoding scheme that is 54 in the case of ASCII. Same with the day: '0' has a value of 48. 7 * 54 + 48 = 426.
See the difference:
PS C:\Users\name> [int]"6"[0]
54
PS C:\Users\name> [int]"6"
6
When extracting an element of the string through indexing with [0] you get a character instead of a string of length 1. A cast to int will then return the ASCII value of this character.
You're indexing ([...]) into string $Up_Down, which means you're returning a single character, i.e. a [char] (System.Char) instance.
Casting a [char] to [int] yields its Unicode code point ("ASCII value"), not the digit that the character happens to represent.
For instance, the character 6 is Unicode character DIGIT SIX with code point U+0036; 0036 is the hexadecimal form of the numeric code point, and the decimal form of hexadecimal 0x36 is 54.
PS> [int] "6w0d"[0]
54 # !! Same as: [int] [char] "6"
To interpret the character as a digit, you need an intermediate [string] cast:
PS> [int] [string] "6w0d"[0]
6 # OK - a string is parsed as expected; same as: [int] "6"
If you cast a string rather than char to [int], PowerShell effectively calls System.Int32.Parse behind the scenes as follows: [int]::Parse($string, [cultureinfo]::InvariantCulture).
Note that PowerShell has no char literals - unlike in C#, '...' quoting also produces strings (verbatim ones), and [int] '6' yields integer 6, just like [int] "6" does.
Conversely, you need an explicit [char] cast to convert a single-character string literal to a [char]; e.g., [char] '6'; a multi-character string would cause the cast to fail.
The solution in the context of your command:
[int]$weeks = if ($Up_Down -match "w"){[string] $Up_Down[$Up_Down.IndexOf('w')-1]} Else {0}
[int]$days = if ($Up_Down -match "d"){[string] $Up_Down[$Up_Down.IndexOf('d')-1]} Else {0}
However, I suggest solving the problem differently:
[int] $totalDays = 0
if ($UP_Down -match '^(?:(?<weeks>\d+)w)?(?:(?<days>\d+)d)?$') {
[int] $weeks, [int] $days = $Matches.weeks, $Matches.days
$totalDays = 7 * $weeks + $days
} # else: string wasn't in expected format.
others have shown you why the problem hit you, so this is just an alternate way to get the total day count. [grin]
what it does ...
fakes reading in a text file of Week/Day codes
when ready to use real data, remove the entire #region/#endregion block and use Get-Content.
iterates thru the list
splits on the w
trims away the trailing d
assigns the resulting strings to the two [int] variables on the left of the =
this forces the two number strings to become number objects.
calcs the total days
displays the week/day code, week count, day count, and total days
the code ...
#region >>> fake reading in a list of week/day codes
# in real life use Get-Content
$WD_List = #'
6w0d
3w3d
0w1d
66w6d
9w1d
'# -split [System.Environment]::NewLine
#endregion >>> fake reading in a list of week/day codes
foreach ($WL_Item in $WD_List)
{
[int]$WeekCount, [int]$DayCount = $WL_Item.Split('w').TrimEnd('d')
$TotalDays = ($WeekCount * 7) + $DayCount
$WL_Item
$WeekCount
$DayCount
$TotalDays
'=' * 20
}
the output ...
6w0d
6
0
42
====================
3w3d
3
3
24
====================
0w1d
0
1
1
====================
66w6d
66
6
468
====================
9w1d
9
1
64
====================
How can I count the scale of a given decimal in Powershell?
$a = 0.0001
$b = 0.000001
Casting $a to a string and returning $a.Length gives a result of 6...I need 4.
I thought there'd be a decimal or math function but I haven't found it and messing with a string seems inelegant.
There's probably a better mathematic way but I'd find the decimal places like this:
$a = 0.0001
$decimalPlaces = ("$a" -split '\.')[-1].TrimEnd('0').Length
Basically, split the string on the . character and get the length of the last string in the array. Wrapping $a in double-quotes implicitly calls .ToString() with an invariant culture (you could expand this as $a.ToString([CultureInfo]::InvariantCulture)), making this method to determine the number of decimal places culture-invariant.
.TrimEnd('0') is used in case $a were sourced from a string, not a proper number type, it's possible that trailing zeroes could be included that should not count as decimal places. However, if you want the scale and not just the used decimal places, leave .TrimEnd('0') off like so:
$decimalPlaces = ("$a" -split '\.')[-1].Length
mclayton helpfully linked to this answer to a related C# question in a comment, and the solution there can indeed be adapted to PowerShell, if working with or conversion to type [decimal] is acceptable:
# Define $a as a [decimal] literal (suffix 'd')
# This internally records the scale (number of decimal places) as specified.
$a = 0.0001d
# [decimal]::GetBits() allows extraction of the scale from the
# the internal representation:
[decimal]::GetBits($a)[-1] -shr 16 -band 0xFF # -> 4, the number of decimal places
The System.Decimal.GetBits method returns an array of internal bit fields whose last element contains the scale in bits 16 - 23 (8 bits, even though the max. scale allowed is 28), which is what the above extracts.
Note: A PowerShell number literal that is a fractional number without the d suffix - e.g., 0.0001 becomes a [double] instance, i.e. a double-precision binary floating-point number.
PowerShell automatically converts [double] to [decimal] values on demand, but do note that there can be rounding errors due to the differing internal representations, and that [double] can store larger numbers than [decimal] can (although not accurately).
A [decimal] literal - one with suffix d (note that C# uses suffix m) - is parsed with a scale exactly as specified, so that applying the above to 0.000d and 0.010d yields 3 in both cases; that is, the trailing zeros are meaningful.
This does not apply if you (implicitly) convert from [double] instances such as 0.000 and 0.010, for which the above yields 0 and 2, respectively.
A string-based solution:
To offer a more concise (also culture-invariant) alternative to Bender The Greatest's helpful answer:
$a = 0.0001
("$a" -replace '.+\.').Length # -> 4, the number of decimal places
Caveat: This solution relies on the default string representation of a [double] number, which need not match the original input format; for instance, .0100, when stringified later, becomes '0.01'; however, as discussed above, you can preserve trailing zeros if you start with a [decimal] literal: .0100d stringifies to '0.0100' (input number of decimals preserved).
"$a", uses an expandable string (PowerShell's string interpolation) to create a culture-invariant string representation of the number so as to ensure that the string representation uses . as the decimal mark.
In effect, PowerShell calls $a.ToString([cultureinfo]::InvariantCulture) behind the scenes.[1].
By contrast, .ToString() (argument-less) applies the rules of the current culture, and in some cultures it is , - not . - that is used as the decimal mark.
Caveat: If you use just $a as the LHS of -replace, $a is implicitly stringified, in which case you - curiously - get culture-sensitive behavior, as with .ToString() - see this GitHub issue.
-replace '.+\.' effectively removes all characters up to and including the decimal point from the input string, and .Length counts the characters in the resulting string - the number of decimal places.
[1] Note that casts from strings in PowerShell too use the invariant culture (effectively, ::Parse($value, [cultureinfo]::InvariantCulture) is called) so that in order to parse a a culture-local string representation you'll need to use the ::Parse() method explicitly; e.g., [double]::Parse('1,2'), not [double] '1,2'.
I am using Powershell and need to extract the digits before the decimal point so that I can evaluate the number extracted
So with $Interest_Rate = 15.5
I have tried the following code .. but they do not work:
$Interest_RatePart1 = "{0:N0}" -f $Interest_Rate
It rounds the value to 16
$Interest_RatePart1 = ($Interest_Rate -split '.')[0].trim()
It returns a blank.
I just want to return 15
Formatting the number will cause rounding away from zero
Use Math.Truncate() - which always rounds towards zero - instead:
$Interest_RatePart1 = [Math]::Truncate($Interest_Rate)
FWIW, the reason your last attempt returns nothing, is because -split defaults to regular expressions, and . means any character in regex.
Either escape the . with \:
$Interest_RatePart1 = ($Interest_Rate -split '\.')[0].Trim()
or specify that it shouldn't use regex:
$Interest_RatePart1 = ($Interest_Rate -split '.', 2, 'SimpleMatch')[0].Trim()
or use the String.Split() method instead:
$Interest_RatePart1 = "$Interest_Rate".Split('.')[0].Trim()
Mathias' [Math]::Truncate is correct - some other options for you though, pay attention to Floor as it is Slightly Different to Truncate when working with negative numbers.
Cast to int (can round up)
[int]$Interest_Rate
Use [Math]::Floor (will always round down, similar to truncate for non-negative numbers)
[Math]::Floor($Interest_Rate)
Use [Math]::Round with 0 decimal places. (can round up)
[Math]::Round($Interest_Rate, 0)
Is there a way to convert hexadecimal fractions (i.e.: numbers with values <1.0, represented in hex) into their decimal equivalents in PowerShell?
Examples:
Hex Dec
0.0858b9da24fb4cac = 0.032603851087498366
0.8c3115559ab0c10b = 0.5476239522928976
Guides I've found for general HEX/DEC conversion say to use [Convert], which is fine for whole numbers, but it seems to completely fail when I throw a decimal point in the string.
Note: Examples were taken from data on another site, where these calculations are routinely done. I haven't fully verified their accuracy - there may be some error in the last few digits. Fortunately, I won't be using that much precision anyway.
I don't know of a builtin .Net way to do it. I wrote this brute-force converter, but your values don't seem to exactly match up with .Net type precision - [double] (64bit) is too low precision, and [decimal] (128bit) is more.
Your examples come out like so:
0.0858b9da24fb4cac = 0.032603851087498366
= 0.0326038510874983682928164128
0.8c3115559ab0c10b = 0.5476239522928976
= 0.5476239522928976344718082711
Code:
<#
.Synopsis
Converts strings containing hexadecimal numbers with fractional components
into base-10 [decimal] numbers.
.EXAMPLE
PS C:\> ConvertFrom-HexFraction '0.0858b9da24fb4cac'
0.0326038510874983682928164128
.Example
PS C:\> '0.0858b9da24fb4cac', '0.8c3115559ab0c10b' | ConvertFrom-HexFraction
0.0326038510874983682928164128
0.5476239522928976344718082711
#>
function ConvertFrom-HexFraction
{
[CmdletBinding()]
[OutputType([decimal])]
Param
(
[Parameter(Mandatory=$true,
ValueFromPipeline=$true)]
[string]$hexValue
)
Process
{
# strip leading hex indicator, setup result placeholder of desired type.
$hexValue = $hexValue -replace '^(0x|&h)'
$result = [decimal]0
# Loop over the digits (hexits?) in the string, skip the (hexa)decimal point '.'
# and calculate (value * base ^ columnNum) for each.
$i = $hexValue.IndexOf('.') - 1
[char[]]$hexValue | Where { $_ -ne '.' } | ForEach {
$result += [convert]::ToInt32($_, 16) * [Math]::Pow(16, $i--)
}
$result #output the result
}
}
ConvertFrom-HexFraction '0.0'
ConvertFrom-HexFraction 'a.a'
ConvertFrom-HexFraction '0.0858b9da24fb4cac'
ConvertFrom-HexFraction '0.8c3115559ab0c10b'
'0.0858b9da24fb4cac', '0.8c3115559ab0c10b' | ConvertFrom-HexFraction
Ok, this one caught my interest. I didn't even know that hexadecimal fractions were a thing, so I went and looked it up. Converting them isn't covered in the [math] or [convert] libraries that I can see, so we'll have to write up a function that takes care of it for us. I wrote this up, and it's relatively accurate, but you have to consider that a fraction as small as you have it there's going to be some rounding... I mean really, for your first example even you rounded. The last digit alone is something like 6.50521303491303E-19 (which is 12*(16^-16)).
So, that function looks like:
Function HexToInt([string]$TextIn){
$Pre,$Post = $TextIn.split('.')
$PostConverted = For($i=1;$i -le $Post.Length;$i++){
[convert]::ToInt32($Post[($i-1)],16) * [math]::pow(16,($i*-1))
}
$PostSum = $PostConverted | Measure-Object -Sum |% Sum
$Int = [convert]::ToInt64($Pre,16)
$Int+$PostSum
}
That should convert your fractions for you.