In Powershell how can I convert a string with a trailing 'sign' to a number? - powershell

I need to convert strings with optional trailing signs into actual numbers using Powershell.
Possible strings are:
1000-
323+
456
I'm trying to use System.Int.TryParse with a NumberStyles of AllowTrailingSign, but I can't work out how to make System.Globalization.NumberStyles available to Powershell.

EDIT: as per Halr9000's suggestion
$foo = "300-";
$bar = 0;
$numberStyles = [System.Globalization.NumberStyles];
$cultureInfo = [System.Globalization.CultureInfo];
[int]::TryParse($foo, $numberStyles::AllowTrailingSign, $cultureInfo::CurrentCulture, [ref]$bar);

[System.Globalization.NumberStyles]::AllowTrailingSign
I should also point out, that when I'm dealing with enums in general, sometimes I can get by typing a string. E.g. in this case, just put
"AllowTrailingSign"
Final note, when quizzing an Enum for all possible values, use the line:
[System.Globalization.NumberStyles] | gm -static

Here's a better way to get the enum values:
$type = [System.Globalization.NumberStyles]
[enum]::GetValues($type)

If you are sure that the signs could be - or +, String.Replace could help.
If you mean that 323- should return -323, checking for the sign and multiplying it by -1 would help.

Related

PowerShell: Converting String Representations of Numbers to Integers

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

What is the fastest/easiest way to increase a number in a string variable in Powershell?

I have the following Powershell variable
$var = "AB-0045"
I would like to increase the number in the string to become "AB-0046".
I can do:
$newNumber = [int]$var.Substring($var.length -4,4) + 1
Which will give me the desired number 46, but then I have to append that 46 as a string to a new string "AB-00".
Is there a better way to do that?
Now that you have the integer, you'll have to convert back to string formatted in the way you'd like and concatenate.
I'd recommend adding to "AB-" rather than "AB-00" in case your number goes over 100.
To pad leading zeros, you can use the -f operator.
e.g. "{0:d4}" -f 45
You'll still need to get the integer first (45 in the example) from your original string.
I tested with regex class Replace() method and string class Split() method with string formatter. Split() seems faster provided your string is always in the same format. The Replace() method does not care what happens before the last 4 numbers:
# Replace Method
[regex]::Replace($var,'\d{4}$',{([int]$args[0].Value+1).ToString('0000')})
# Split method
$a,[int]$b = $var.split('-'); "{0}-{1:0000}" -f $a,++$b

String.Trim() not removing characters in a string

I need to create a String from double the use String.Trim() to remove the full stop, but it doesn't remove it. I think there is also a way to do this numerically but I'd like to do it with the string. Is there a reason it won't remove it? The output from the code is 5.5
$MyDouble = 5.5
[String]$MyDouble2 = $MyDouble
$MyDouble2.Trim(".")
$MyDouble2
String.Trim() only trims from the beginning and end of strings, so it has no effect in your command, because the . only occurs inside your input string.
If you truly want to remove just the . and keep the post-decimal-point digits, use the -replace operator:
$MyDouble2 -replace '\.' # -> '55'
Note:
* -replace takes a regex (regular expression) as the search operand, hence the need to escape regex metacharacter . as \.
* The above is short for $MyDouble2 -replace '\.', ''. Since the replacement string is the empty string in this case, it can be omitted.
If you only want to extract the integer portion, use either 4c74356b41's .Split()-based answer, or adapt the regex passed to -replace to match everything from the . through the end of the string.
$MyDouble2 -replace '\..*' # -> '5'
#Matt mentions the following alternatives:
For removing the . only: Using String.Replace() to perform literal substring replacement (note how . therefore does not need \-escaping, as it did with -replace, and that specifying the replacement string is mandatory):
$MyDouble2.Replace('.', '') # -> '55'
For removing the fractional part of the number (extracting the integer part only), using a numerical operation directly on $MyDouble (as opposed to via the string representation stored in $MyDouble2), via Math.Floor():
[math]::Floor($MyDouble) # -> 5 (still a [double])
Looking at some documentation for .Trim([char[]]) you will see that
Removes all leading and trailing occurrences of a set of characters specified in an array from the current String object.
That does not cover the middle of strings, so using the .Replace() method would accomplish that.
I think there is also a way to do this numerically but I'd like to do it with the string.
Just wanted to mention that converting numbers to strings to then drop decimals via string manipulation is a poor approach. Assuming your example is what you are actually trying to do, I suggest using a static method from the [math] class instead.
$MyDouble = 5.5
[math]::Floor($MyDouble)
$MyDouble = 5.5
[String]$MyDouble2 = $MyDouble
$MyDouble2.Replace(".", "")
Well, why would it trim not the last (or first) character? It wouldn't, what you need (probably) is:
$MyDouble = 5.5
[String]$MyDouble2 = $MyDouble
$MyDouble2.Split(".")[0]
$MyDouble = 5.5
[String]$MyDouble2 = $MyDouble
$res=$MyDouble2 -split "\."
$res[0..($res.Count-1)] -join ""

Concatenate elements of a char array and strings in powershell

I'm probably over thinking this, but this is not coming out the way I expect. I've searched google, I've searched stackoverflow. Please Help.
Here are some variables for testing, and the invocation of a function:
$SQL_FirstName = "PowerShell";
$SQL_LastName = "CreateUser";
$SQL_Office = "TEST";
$SQL_IsAdmin = $true;
Create_User($SQL_FirstName.ToLower(), $SQL_LastName.ToLower(), $SQL_Office, $SQL_IsAdmin);
Here is the function, not much there yet:
Function Create_User([string]$FirstName, [string]$LastName, $Office, $IsAdmin)
{
$FirstNameCharArray = [char[]]$FirstName;
$UserName = [string]$FirstNameCharArray[0] + $LastName;
Write-Host $UserName;
}
Now I expect the output to be "pcreateuser". But it's not. I have tried casting different things, I have tried surrounding my variables with $(). I have tried using the + symbol and not using the + symbol. I have tried smashing the variables right up against each other. Every single time it just outputs "p".
Any help would be appreciated. Thanks!
It's because of how you are calling the function. You are not supposed to use brackets for function calls nor use commas to separate the parameters (unless you are sending array values on purpose or subexpressions). You have passed it a single array of those elements.
Create_User $SQL_FirstName.ToLower() $SQL_LastName.ToLower() $SQL_Office $SQL_IsAdmin
In your function call your sent an array to $firstname which was casted as a string "powershell createuser TEST True". The other parameters would have been blank. Hence your output.
They work just the same as cmdlet calls. Just use spaces to separate the parameters and their values.
Get-ChildItem -Filter "*.txt" -Path "C:\temp"
String to char array
For what it is worth you don't need to cast the string as a char array. You can just use array notation directly on the string.
PS C:\Users\Matt> [string]"Bagels"[0]
B
Heck you don't even need to cast it "Bagels"[0]

Powershell - Capture text in a var from a specific character

I want to grab the first char of a var string and the first char of the following caracter
Example:
$var1 = "Jean-Martin"
I want a way to grab the first letter "J" then I want to take the first char following the "-" (dash) which is "M".
Something like this?
$initial1 = $var1[0]
$initial2 = $var1.Split('-')[1][0]
Strings in Powershell use the System.String class from the .Net framework. As such, they are indexable to retrieve individual characters and have many methods available such as the Split method used above.
See the documentation here.
$var1 = "Jean-Martin"
To get the first character:
$var1[0]
To get the first character after the dash:
$characterToSeek = '-'
$var1[$var1.IndexOf($characterToSeek)+1]
Another option using regex:
PS> $var1 -replace '^(.)[^-]+-(.).+$','$1$2'
JM