Numbers change inside variables for powershell - powershell

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
====================

Related

Powershell number format

I am creating a script converting a csv file in an another format.
To do so, i need my numbers to have a fixed format to respect column size : 00000000000000000,00 (20 characters, 2 digits after comma)
I have tried to format the number with -f and the method $value.toString("#################.##") without success
Here is an example Input :
4000000
45817,43
400000
570425,02
15864155,69
1068635,69
128586256,9
8901900,04
29393,88
126858346,88
1190011,46
2358411,95
139594,82
13929,74
11516,85
55742,78
96722,57
21408,86
717,01
54930,49
391,13
2118,64
Any hints are welcome :)
Thank you !
tl;dr:
Use 0 instead of # in the format string:
PS> $value = 128586256.9; $value.ToString('00000000000000000000.00')
00000000000128586256.90
Note:
Alternatively, you could construct the format string as an expression:
$value.ToString('0' * 20 + '.00')
The resulting string reflects the current culture with respect to the decimal mark; e.g., with fr-FR (French) in effect, , rather than . would be used; you can pass a specific [cultureinfo] object as the second argument to control what culture is used for formatting; see the docs.
As in your question, I'm assuming that $value already contains a number, which implies that you've already converted the CSV column values - which are invariably strings - to numbers.
To convert a string culture-sensitively to a number, use [double]::Parse('1,2'), for instance (this method too has an overload that allows specifying what culture to use).
Caveat: By contrast, a PowerShell cast (e.g. [double] '1.2') is by design always culture-invariant and only recognizes . as the decimal mark, irrespective of the culture currently in effect.
zerocukor287 has provided the crucial pointer:
To unconditionally represent a digit in a formatted string and default to 0 in the absence of an available digit, use 0, the zero placeholder in a .NET custom numeric format string
By contrast, #, the digit placeholder, represents only digits actually present in the input number.
To illustrate the difference:
PS> (9.1).ToString('.##')
9.1 # only 1 decimal place available, nothing is output for the missing 2nd
PS> (9.1).ToString('.00')
9.10 # only 1 decimal place available, 0 is output for the missing 2nd
Since your input uses commas as decimal point, you can split on the comma and format the whole number and the decimal part separately.
Something like this:
$csv = #'
Item;Price
Item1;4000000
Item2;45817,43
Item3;400000
Item4;570425,02
Item5;15864155,69
Item6;1068635,69
Item7;128586256,9
Item8;8901900,04
Item9;29393,88
Item10;126858346,88
Item11;1190011,46
Item12;2358411,95
Item13;139594,82
Item14;13929,74
Item15;11516,85
Item16;55742,78
Item17;96722,57
Item18;21408,86
Item19;717,01
Item20;54930,49
Item21;391,13
Item22;2118,64
'# | ConvertFrom-Csv -Delimiter ';'
foreach ($item in $csv) {
$num,$dec = $item.Price -split ','
$item.Price = '{0:D20},{1:D2}' -f [int64]$num, [int]$dec
}
# show on screen
$csv
# output to (new) csv file
$csv | Export-Csv -Path 'D:\Test\formatted.csv' -Delimiter ';'
Output in screen:
Item Price
---- -----
Item1 00000000000004000000,00
Item2 00000000000000045817,43
Item3 00000000000000400000,00
Item4 00000000000000570425,02
Item5 00000000000015864155,69
Item6 00000000000001068635,69
Item7 00000000000128586256,09
Item8 00000000000008901900,04
Item9 00000000000000029393,88
Item10 00000000000126858346,88
Item11 00000000000001190011,46
Item12 00000000000002358411,95
Item13 00000000000000139594,82
Item14 00000000000000013929,74
Item15 00000000000000011516,85
Item16 00000000000000055742,78
Item17 00000000000000096722,57
Item18 00000000000000021408,86
Item19 00000000000000000717,01
Item20 00000000000000054930,49
Item21 00000000000000000391,13
Item22 00000000000000002118,64
I do things like this all the time, usually for generating computernames. That custom numeric format string reference will come in handy. If you want a literal period, you have to backslash it.
1..5 | % tostring 00000000000000000000.00
00000000000000000001.00
00000000000000000002.00
00000000000000000003.00
00000000000000000004.00
00000000000000000005.00
Adding commas to long numbers:
psdrive c | % free | % tostring '0,0' # or '#,#'
18,272,501,760
"Per mille" character ‰ :
.00354 | % tostring '#0.##‰'
3.54‰

Convert byte array (hex) to signed Int

I am trying to convert a (variable length) Hex String to Signed Integer (I need either positive or negative values).
[Int16] [int 32] and [int64] seem to work fine with 2,4+ byte length Hex Strings but I'm stuck with 3 byte strings [int24] (no such command in powershell).
Here's what I have now (snippet):
$start = $mftdatarnbh.Substring($DataRunStringsOffset+$LengthBytes*2+2,$StartBytes*2) -split "(..)"
[array]::reverse($start)
$start = -join $start
if($StartBytes*8 -le 16){$startd =[int16]"0x$($start)"}
elseif($StartBytes*8 -in (17..48)){$startd =[int32]"0x$($start)"}
else{$startd =[int64]"0x$($start)"}
With the above code, a $start value of "D35A71" gives '13851249' instead of '-2925967'. I tried to figure out a way to implement two's complement but got lost. Any easy way to do this right?
Thank you in advance
Edit: Basically, I think I need to implement something like this:
int num = (sbyte)array[0] << 16 | array[1] << 8 | array[2];
as seen here.
Just tried this:
$start = "D35A71"
[sbyte]"0x$($start.Substring(0,2))" -shl 16 -bor "0x$($start.Substring(2,2))" -shl 8 -bor "0x$($start.Substring(4,2))"
but doesn't seem to get the correct result :-/
To parse your hex.-number string as a negative number you can use [bigint] (System.Numerics.BigInteger):
# Since the most significant hex digit has a 1 as its most significant bit
# (is >= 0x8), it is parsed as a NEGATIVE number.
# To force unconditional interpretation as a positive number, prepend '0'
# to the input hex string.
PS> [bigint]::Parse('D35A71', 'AllowHexSpecifier')
-2925967
You can cast the resulting [bigint] instance back to an [int] (System.Int32).
Note:
The result is a negative number, because the most significant hex digit of the hex input string is >= 0x8, i.e. has its high bit set.
To force [bigint] to unconditionally interpret a hex. input string as a positive number, prepend 0.
The internal two's complement representation of a resulting negative number is performed at byte boundaries, so that a given hex number with an odd number of digits (i.e. if the first hex digit is a "half byte") has the missing half byte filled with 1 bits.
Therefore, a hex-number string whose most significant digit is >= 0x8 (parses as a negative number) results in the same number as prepending one or more Fs (0xF == 1111) to it; e.g., the following calls all result in -2048:
[bigint]::Parse('800', 'AllowHexSpecifier'),
[bigint]::Parse('F800', 'AllowHexSpecifier'),
[bigint]::Parse('FF800', 'AllowHexSpecifier'), ...
See the docs for details about the parsing logic.
Examples:
# First digit (7) is < 8 (high bit NOT set) -> positive number
[bigint]::Parse('7FF', 'AllowHexSpecifier') # -> 2047
# First digit (8) is >= 8 (high bit IS SET) -> negative number
[bigint]::Parse('800', 'AllowHexSpecifier') # -> -2048
# Prepending additional 'F's to a number that parses as
# a negative number yields the *same* result
[bigint]::Parse('F800', 'AllowHexSpecifier') # -> -2048
[bigint]::Parse('FF800', 'AllowHexSpecifier') # -> -2048
# ...
# Starting the hex-number string with '0'
# *unconditionally* makes the result a *positive* number
[bigint]::Parse('0800', 'AllowHexSpecifier') # -> 2048

How to change each character in a string to the previous character in the alphabet

I am looking for a way to change each character in a string to the previous value in the alphabet. Essentially, I need to create a for each loop that will take every character in a string and increment it backwards by 1. For instance, I would like to change bcd234% to abc123$.
I have tried breaking the string into an array and subtracting 1 from each element.
$myString = "bcd234%"
$myArray = $myString.ToCharArray()
$myArray = $myArray | ForEach-Object { $_ - 1 }
$myArray
-join($myArray)
I would expect that it would iterate the value down 1 and then join all of the new values together.
What I would like to see is the new string:
abc123$
What it is actually doing is creating new values for each of the characters and joining them together instead.
The results I am getting are the new array:
97
98
99
49
50
51
36
And then it joins them together which looks like:
97989949505136
When you're subtracting an integer from a char PowerShell automatically converts the char to an integer for the calculation. Essentially $_ - 1 does the same as ([int]$_) - 1. For getting back a character you can simply cast the result of the operation back to a char.
$myArray = $myArray | ForEach-Object { [char]([int]$_ - 1) }
The [int] is redundant, as mentioned above, but I put it in for good measure, so it's more obvious what is happening there.

Unicode character transformation in SPSS

I have a string variable. I need to convert all non-digit characters to spaces (" "). I have a problem with unicode characters. Unicode characters (the characters outside the basic charset) are converted to some invalid characters. See the code for example.
Is there any other way how to achieve the same result with procedure which would not choke on special unicode characters?
new file.
set unicode = yes.
show unicode.
data list free
/T (a10).
begin data
1234
5678
absd
12as
12(a
12(vi
12(vī
12āčž
end data.
string Z (a10).
comp Z = T.
loop #k = 1 to char.len(Z).
if ~range(char.sub(Z, #k, 1), "0", "9") sub(Z, #k, 1) = " ".
end loop.
comp Z = normalize(Z).
comp len = char.len(Z).
list var = all.
exe.
The result:
T Z len
1234 1234 4
5678 5678 4
absd 0
12as 12 2
12(a 12 2
12(vi 12 2
12(vī 12 � 6
>Warning # 649
>The first argument to the CHAR.SUBSTR function contains invalid characters.
>Command line: 1939 Current case: 8 Current splitfile group: 1
12āčž 12 �ž 7
Number of cases read: 8 Number of cases listed: 8
The substr function should not be used on the left hand side of an expression in Unicode mode, because the replacement character may not be the same number of bytes as the character(s) being replaced. Instead, use the replace function on the right hand side.
The corrupted characters you are seeing are due to this size mismatch.
How about instead of replacing non-numeric characters, you cycle though and pull out the numeric characters and rebuild Z? (Note my version here is pre CHAR. string functions.)
data list free
/T (a10).
begin data
1234
5678
absd
12as
12(a
12(vi
12(vī
12āčž
12as23
end data.
STRING Z (a10).
STRING #temp (A1).
COMPUTE #len = LENGTH(RTRIM(T)).
LOOP #i = 1 to #len.
COMPUTE #temp = SUBSTR(T,#i,1).
DO IF INDEX('0123456789',#temp) > 0.
COMPUTE Z = CONCAT(SUBSTR(Z,1,#i-1),#temp).
ELSE.
COMPUTE Z = CONCAT(SUBSTR(Z,1,#i-1)," ").
END IF.
END LOOP.
EXECUTE.

Powershell - Round down to nearest whole number

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)
}