Powershell Year of Graduation Calculation Breaks When Subtracting -2 - powershell

I have to extrapolate student year of graduation based on their grade number. Their student ID number has the 2-digit year of graduation in it, however this has proven to be inaccurate sometimes due to students who have been held back and other ID number based anomalies.
I wrote a powershell script to do this for me as part of a larger script to take a CSV exported from powerschool and check their location in active directory based on their graduation year. The portion of the script I'm having trouble with is as follows:
function GradeToYog($grade){ # converts grade number to year of grad because IDNUM is not a reliable indicator of grad year
if($grade -ne $null){
# get current date
$date = Get-Date
# account for time of year, add 1yr offset to grad year if september or later
$offset = 0
if($date.month -ge 9){
$offset = 1
}
# Account for placement of extended-stay students such as SPED kids
if($grade -gt 12){
$grade = 12
}
return ($date.year + 12 + $offset - $grade)
} else {
return "Invalid entry."
}
}
This works for grades -1 (pre-K) through 12, however it breaks when I test it against -2:
PS C:\> GradeToYog 12
2022
PS C:\> GradeToYog 9
2025
PS C:\> GradeToYog 6
2028
PS C:\> GradeToYog 3
2031
PS C:\> GradeToYog 1
2033
PS C:\> GradeToYog 0
2034
PS C:\> GradeToYog -1
2035
PS C:\> GradeToYog -2
2022
The weird thing is that if I test the expected values against the same logic in powershell it works:
PS C:\> 2022 + 12 + 0 - -2
2036
I searched for similar questions but either I'm not searching for it correctly or there doesn't seem to be an instance of powershell incorrectly subtracting '-2'

Note: I can't reproduce your specific symptom, but the following advice is still worth heeding.
Type-constrain your $grade parameter to ensure that it is a number:
# Make sure that $grade is an [int] (System.Int32) value
function GradeToYog([int] $grade) { # ...
# Alternative syntax, with a param(...) block,
# which makes it easier to use validation attributes.
function GradeToYog {
param(
[ValidateRange(-2, 15)] # Constrain the range of allowable numbers.
[int] $grade
)
# ...
}
An unconstrained parameter is effectively the same as [object] $grade, i.e. it can receive an object of any type.
PowerShell infers certain types from unquoted arguments such as 2 and -2 and, perhaps surprisingly, 2 becomes an [int], whereas -2 becomes a [string] (to force the latter to be interpreted as an [int] ad hoc, pass it as (-2)).
Many PowerShell operators, such as -eq / ne and -lt / -le / -gt / ge can operate on both strings and numbers, and it is usually the type of the LHS operand that determines the data type of the operation overall.
Here's an example of where this would make a difference:
2 -lt 10 # $true
'2' -lt 10 # !! $false, due to *lexical* comparison

Related

how use pipeline to get info meeting criteria, but also comparing hex numbers

I have a pipeline I've used to get info taken from an excel spreadsheet for other device models, but for this device model, the value is hex, and hex is unusual, in that 0x10 = 0x00010, so I need to compare those values in the pipeline.
This is my pipeline I've used for non-hex values after the spreadsheet content is returned:
$deviceErrDescMap = Process_ErrorDescMap -errorCodeListFilePath $errorCodeListFile #excel is returned
$deviceErrDescRow = $deviceErrDescMap | Where-Object 'Value' -eq $sdkNum
In this, $deviceErrDescMap is holding spreadsheet values like this:
Name Value Description
A 0x00000010 Check Material A
B 0x00000100 Check Interlock
C 0x00000020 Check Material C
This is how I get excel contents, in case it matters:
Function Process_ErrorDescMap{
[cmdletbinding()]
Param ([string]$errorCodeListFilePath)
Process
{
if(Test-Path $errorCodeListFilePath)
{
#Excel method
#Install-Module -Name ImportExcel -Scope CurrentUser -Force (dependency - 1. Close all instances of PowerShell (console, ISE, VSCode), 2. Move the PackageManagement folder from OneDrive, 3. Open a PowerShell Console (Run As Administrator), 4. Run Install-Module ImportExcel)
if(($errorCodeListFilePath -match "DeviceA") -or ($errorCodeListFilePath -match "DeviceB"))
{
$startRow = 1
}
else
{
$startRow = 2
}
$importedExcel = Import-Excel -Path $errorCodeListFilePath -StartRow $startRow
return $importedExcel #list of error desc
}
else {
Write-Host "Invalid path given: $($errorCodeListFilePath)"
return "***Invalid Path $($errorCodeListFilePath)"
}
} #end Process
}# End of Function Process_ErrorDescMap
The spreadsheet's first line, with Value 0x00000010 should compare with $sdkNum=0x10, which is the first one. So 0x10 (or 0x010) needs to match, or be equal to this spreadsheet value of 0x0000010 and grab it from the map. I'm at a bit of a loss as to how to accomplish this. I'm not sure how to convert 'Value' to hex, and compare it with the hex value of $sdkNum in this pipeline. I was thinking of using a regex to get the 10 from $sdkNum, and use match to get any rows containing 10 from the spreadsheet content, and then further compare. I feel like there's an easier way, plus I'm not sure how I'd get just the non-zero number and 0's to the right of that out of the hex string.
If you are confused by this hex comparison, feel free to use a hex to decimal conversion web page, and you will see 0x10 = 0x000010. I thought it was strange too. it's the 0's after the 1 that matter.
This is with PowerShell 5.1 and VSCode.
PowerShell will natively parse a valid hexadecimal numeral when you convert a string to an integral type:
PS ~> [int]'0x10'
16
Since all of PowerShell's overloaded comparison operators (-eq/-ne/-gt/-ge/-lt/-le) automatically converts the right-hand side operand to the type of the left-hand side operand, all you need to do is make sure the expression you provide as the first operand is already an [int]:
$sdkNum = 0x10 # notice no quotation marks
# Option 1: cast the hex string to `[int]` explicitly
... |Where-Object { [int]$_.Value -eq $sdkNum }
# Option 2: $sdkNum is already an [int], PowerShell automatically converts hex string to int
... |Where-Object { $sdkNum -eq $_.Value }

Incorrect Operator Result in PowerShell [duplicate]

This question already has answers here:
Why do integers in PowerShell compare by digits?
(4 answers)
Closed 6 years ago.
So this seems really simple, and it's really easy to guess the result, but I seem to be getting a really wierd result in powershell.
So essentially I'm building an array with an unknown number of objects in there, then running an operator against the .Count property.
Example:
$a = New-Object System.Collections.ArrayList
$n = 0
while ($n -ne 27) {$n++; $a.Add("Test line")}
# Array built, the .Count property should be 27
$bool = $false
$number = 2
if ($number -gt $a.Count) {$bool = $true}
# This correctly gives me $bool as $false
$number = 3
if ($number -gt $a.Count) {$bool = $true}
# This incorrectly gives me $bool as $true, and does so when $number is
# greater than 3.
Any ideas on this? I've never seen this before. Above is a simplified example, but essentially I'm pulling objects into an array, getting user input with Read-Host and I want to compare if the user input is greater than (-gt) the total count of the array.
Found the answer here: Why do integers in PowerShell compare by digits?
Added [int] markers to the variables and now works perfectly. Thanks for the suggestions.

PowerShell Suppress Output

I've checked and tried a few of the suggestions on StackOverflow but none of them seem to work. I put together an example of what I am trying to accomplish.
[System.Random] $rand = New-Object System.Random
$randomNumbers = New-Object int[] 10;
[int[]] $randomNumbers;
for($i = 0; $i -lt $randomNumbers.Length; $i++)
{
($randomNumbers[$i] = $rand.Next(256)) 2>&1 | Out-Null;
}
I've tried the
> $Null
|Out-Null
2>&1
But none of them seem to suppress the output. It's showing 10 zero's in a row. One for each assignment. How can I suppress that output?
Remove int[]] $randomNumbers;. It is not the assignment that is printed, but the empty array.
other solution for replace your code ;)
[int[]] $randomNumbers=1..10 | %{ Get-Random -maximum 256 }
To complement Andrey Marchuk's effective answer:
[int[]] $randomNumbers looks like a type-bound PowerShell variable declaration, but, since no value is assigned, it is merely a cast: the preexisting value of $randomNumbers - a 10-element array of 0 values - is simply cast to [int[]] (a no-op in this case), and then output - yielding 10 lines with a 0 on each in this case.
A true type-bound assignment that is the (inefficient) equivalent of your New-Object int[] 10 statement is [int[]] $randomNumbers = #( 0 ) * 10.
Note that it is the presence of = <value> that makes this statement an assignment that implicitly creates the variable.
PowerShell has no variable declarations in the conventional sense, it creates variables on demand when you assign to them.
You can, however, use the New-Variable cmdlet to explicitly create variables, which allows you to control additional aspects, such as the variable's scope.
Variable assignments in PowerShell do NOT output anything by default, so there's no need to suppress any output (with | Out-Null, >$null, ...).
That said, you can force a variable assignment to output the assigned value by enclosing the assignment in (...).
$v = 'foo' # no output
($v = 'foo') # enclosed in () -> 'foo' is output
As you've discovered, actively suppressing the output in ($randomNumbers[$i] = $rand.Next(256)) 2>&1 | Out-Null; is unnecessary, because simply omitting the parentheses makes the statement quiet: $randomNumbers[$i] = $rand.Next(256)
Finally, you could simplify your code using the Get-Random cmdlet:
[int[]] $randomNumbers = 1..10 | % { Get-Random -Maximum 256 }
This single pipeline does everything your code does (not sure about performance, but it may not matter).

Powershell Golf: Next business day

How to find next business day with powershell ?
Well, my phone allows me to set which days are business days, but Windows/.NET won't, so I assume Monday through Friday.
Note: As the question includes "golf" I am golfing this one, that is trying to use as few bytes for the script as possible. The code is not necessarily readable as a result.
The easiest and most straightforward way to do is would be to start with today, add a day and look whether it is in the wanted range:
PS> $d = [DateTime]::Now.AddDays(1); while ($d.DayOfWeek -eq "Saturday" -or $d.DayOfWeek -eq "Sunday") { $d = $d.AddDays(1) }; $d
Montag, 22. Juni 2009 19:50:27
We can shorten that a little, though:
PS> $d=(Get-Date)+"1";for(;6,0-contains$d.DayOfWeek){$d+="1"}$d
Montag, 22. Juni 2009 19:52:31
But we can also try it differently, using the pipeline. The next business day is at least one and at most three days away, so we can generate a list of possible dates and filter them accordingly and at last, select the first one:
PS> #(1..3|%{(Get-Date).AddDays($_)}|?{$_.DayOfWeek -ge "Monday" -and $_.DayOfWeek -le "Friday"})[0]
Montag, 22. Juni 2009 22:11:19
or shorter:
PS> #(1..3|%{(Get-Date)+"$_"}|?{1..5-contains$_.DayOfWeek})[0]
Montag, 22. Juni 2009 19:55:53
By letting the range go to 4 we can guarantee that it always returns at least two workdays and save the # operator to force an array:
PS> (1..4|%{(Get-Date)+"$_"}|?{1..5-contains$_.DayOfWeek})[0]
Montag, 22. Juni 2009 20:24:06
This is pretty short too (but uses aliases):
,(1,2-eq7-(date).dayofweek)|%{(date)+"$(1+$_[0])"}
In one single statement:
(date)+"$(1+$(#(1,2-eq7-(date).dayofweek)))"
A few notes about this approach:
In Powershell (v1 at least), comparisons with collections return items where the condition is true, for example:
PS> 1,2 -eq 1
PS> 1
I'm taking advantage of the fact that the actual exceptions to the rule today + 1 to calculate the next business day are only Friday (+3 days) and Saturday (+2 days).
Here is another pipline way:
(#(1..4) | foreach { if (([datetime]::Now.AddDays($_)).DayOfWeek -ne "Sunday" -and ([datetime]::Now.AddDays($_)).DayOfWeek -ne "Saturday") {[datetime]::Now.AddDays($_); }})[0]
Not sure why I have to use (1..4) instead of (1..3) however.
I found the sample code in the first answer to be really difficult to follow so I rewrote it to be a bit easier to see what was happening. I'm still using the -eq behavior where the -eq test will return the matching value.
$date = get-date "2013 Apr 24"
write-host "Based on your date"
$date
write-host "next business day (skipping saturday and sunday)"
$Date.AddDays(1 + $(1,2 -eq 7 - [int]$date.dayofweek) )
write-host "Next week monday"
$Date.AddDays(1 + $(0,1,2,3,4,5,6,7 -eq 7 - [int]$date.dayofweek) )

What are some of the most useful yet little known features in the PowerShell language [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
A while back I was reading about multi-variable assignments in PowerShell. This lets you do things like this
64 > $a,$b,$c,$d = "A four word string".split()
65 > $a
A
66 > $b
four
Or you can swap variables in a single statement
$a,$b = $b,$a
What little known nuggets of PowerShell have you come across that you think may not be as well known as they should be?
The $$ command. I often have to do repeated operations on the same file path. For instance check out a file and then open it up in VIM. The $$ feature makes this trivial
PS> tf edit some\really\long\file\path.cpp
PS> gvim $$
It's short and simple but it saves a lot of time.
By far the most powerful feature of PowerShell is its ScriptBlock support. The fact that you can so concisely pass around what are effectively anonymous methods without any type constraints are about as powerful as C++ function pointers and as easy as C# or F# lambdas.
I mean how cool is it that using ScriptBlocks you can implement a "using" statement (which PowerShell doesn't have inherently). Or, pre-v2 you could even implement try-catch-finally.
function Using([Object]$Resource,[ScriptBlock]$Script) {
try {
&$Script
}
finally {
if ($Resource -is [IDisposable]) { $Resource.Dispose() }
}
}
Using ($File = [IO.File]::CreateText("$PWD\blah.txt")) {
$File.WriteLine(...)
}
How cool is that!
A feature that I find is often overlooked is the ability to pass a file to a switch statement.
Switch will iterate through the lines and match against strings (or regular expressions with the -regex parameter), content of variables, numbers, or the line can be passed into an expression to be evaluated as $true or $false
switch -file 'C:\test.txt'
{
'sometext' {Do-Something}
$pwd {Do-SomethingElse}
42 {Write-Host "That's the answer."}
{Test-Path $_} {Do-AThirdThing}
default {'Nothing else matched'}
}
$OFS - output field separator. A handy way to specify how array elements are separated when rendered to a string:
PS> $OFS = ', '
PS> "$(1..5)"
1, 2, 3, 4, 5
PS> $OFS = ';'
PS> "$(1..5)"
1;2;3;4;5
PS> $OFS = $null # set back to default
PS> "$(1..5)"
1 2 3 4 5
Always guaranteeing you get an array result. Consider this code:
PS> $files = dir *.iMayNotExist
PS> $files.length
$files in this case may be $null, a scalar value or an array of values. $files.length isn't going to give you the number of files found for $null or for a single file. In the single file case, you will get the file's size!! Whenever I'm not sure how much data I'll get back I always enclose the command in an array subexpression like so:
PS> $files = #(dir *.iMayNotExist)
PS> $files.length # always returns number of files in array
Then $files will always be an array. It may be empty or have only a single element in it but it will be an array. This makes reasoning with the result much simpler.
Array covariance support:
PS> $arr = '127.0.0.1','192.168.1.100','192.168.1.101'
PS> $ips = [system.net.ipaddress[]]$arr
PS> $ips | ft IPAddressToString, AddressFamily -auto
IPAddressToString AddressFamily
----------------- -------------
127.0.0.1 InterNetwork
192.168.1.100 InterNetwork
192.168.1.101 InterNetwork
Comparing arrays using Compare-Object:
PS> $preamble = [System.Text.Encoding]::UTF8.GetPreamble()
PS> $preamble | foreach {"0x{0:X2}" -f $_}
0xEF
0xBB
0xBF
PS> $fileHeader = Get-Content Utf8File.txt -Enc byte -Total 3
PS> $fileheader | foreach {"0x{0:X2}" -f $_}
0xEF
0xBB
0xBF
PS> #(Compare-Object $preamble $fileHeader -sync 0).Length -eq 0
True
Fore more stuff like this, check out my free eBook - Effective PowerShell.
Along the lines of multi-variable assignments.
$list = 1,2,3,4
While($list) {
$head, $list = $list
$head
}
1
2
3
4
I've been using this:
if (!$?) { # if previous command was not successful
Do some stuff
}
and I also use $_ (current pipeline object) quite a bit, but these might be more known than other stuff.
The fact that many operators work on arrays as well and return the elements where a comparison is true or operate on each element of the array independently:
1..1000 -lt 800 -gt 400 -like "?[5-9]0" -replace 0 -as "int[]" -as "char[]" -notmatch "\d"
This is faster than Where-Object.
Not a language feature but super helpful
f8 -- Takes the text you have put in already and searches for a command that starts with that text.
Tab-search through your history with #
Example:
PS> Get-Process explorer
PS> "Ford Explorer"
PS> "Magellan" | Add-Content "great explorers.txt"
PS> type "great explorers.txt"
PS> #expl <-- Hit the <tab> key to cycle through history entries that have the term "expl"
Love this thread. I could list a ton of things after reading Windows Powershell in Action. There's a disconnect between that book and the documentation. I actually tried to list them all somewhere else here, but got put on hold for "not being a question".
I'll start with foreach with three script blocks (begin/process/end):
Get-ChildItem | ForEach-Object {$sum=0} {$sum++} {$sum}
Speaking of swapping two variables, here's swapping two files:
${c:file1.txt},${c:file2.txt} = ${c:file2.txt},${c:file1.txt}
Search and replace a file:
${c:file.txt} = ${c:file.txt} -replace 'oldstring','newstring'
Using assembly and using namespace statements:
using assembly System.Windows.Forms
using namespace System.Windows.Forms
[messagebox]::show('hello world')
A shorter version of foreach, with properties and methods
ps | foreach name
'hi.there' | Foreach Split .
Use $() operator outside of strings to combine two statements:
$( echo hi; echo there ) | measure
Get-content/Set-content with variables:
$a = ''
get-content variable:a | set-content -value there
Anonymous functions:
1..5 | & {process{$_ * 2}}
Give the anonymous function a name:
$function:timestwo = {process{$_ * 2}}
Anonymous function with parameters:
& {param($x,$y) $x+$y} 2 5
You can stream from foreach () with these, where normally you can't:
& { foreach ($i in 1..10) {$i; sleep 1} } | out-gridview
Run processes in background like unix '&', and then wait for them:
$a = start-process -NoNewWindow powershell {timeout 10; 'done a'} -PassThru
$b = start-process -NoNewWindow powershell {timeout 10; 'done b'} -PassThru
$c = start-process -NoNewWindow powershell {timeout 10; 'done c'} -PassThru
$a,$b,$c | wait-process
Or foreach -parallel in workflows:
workflow work {
foreach -parallel ($i in 1..3) {
sleep 5
"$i done"
}
}
work
Or a workflow parallel block where you can run different things:
function sleepfor($time) { sleep $time; "sleepfor $time done"}
workflow work {
parallel {
sleepfor 3
sleepfor 2
sleepfor 1
}
'hi'
}
work
Three parallel commands in three more runspaces with the api:
$a = [PowerShell]::Create().AddScript{sleep 5;'a done'}
$b = [PowerShell]::Create().AddScript{sleep 5;'b done'}
$c = [PowerShell]::Create().AddScript{sleep 5;'c done'}
$r1,$r2,$r3 = ($a,$b,$c).begininvoke()
$a.EndInvoke($r1); $b.EndInvoke($r2); $c.EndInvoke($r3) # wait
($a,$b,$c).Streams.Error # check for errors
($a,$b,$c).dispose() # cleanup
Parallel processes with invoke-command, but you have to be at an elevated prompt with remote powershell working:
invoke-command localhost,localhost,localhost { sleep 5; 'hi' }
An assignment is an expression:
if ($a = 1) { $a }
$a = $b = 2
Get last array element with -1:
(1,2,3)[-1]
Discard output with [void]:
[void] (echo discard me)
Switch on arrays and $_ on either side:
switch(1,2,3,4,5,6) {
{$_ % 2} {"Odd $_"; continue}
4 {'FOUR'}
default {"Even $_"}
}
Get and set variables in a module:
'$script:count = 0
$script:increment = 1
function Get-Count { return $script:count += $increment }' > counter.psm1 # creating file
import-module .\counter.psm1
$m = get-module counter
& $m Get-Variable count
& $m Set-Variable count 33
See module function definition:
& $m Get-Item function:Get-Count | foreach definition
Run a command with a commandinfo object and the call operator:
$d = get-command get-date
& $d
Dynamic modules:
$m = New-Module {
function foo {"In foo x is $x"}
$x=2
Export-ModuleMember -func foo -var x
}
flags enum:
[flags()] enum bits {one = 1; two = 2; three = 4; four = 8; five = 16}
[bits]31
Little known codes for the -replace operator:
$number Substitutes the last submatch matched by group number.
${name} Substitutes the last submatch matched by a named capture of the form (?).
$$ Substitutes a single "$" literal.
$& Substitutes a copy of the entire match itself.
$` Substitutes all the text from the argument string before the matching portion.
$' Substitutes all the text of the argument string after the matching portion.
$+ Substitutes the last submatch captured.
$_ Substitutes the entire argument string.
Demo of workflows surviving interruptions using checkpoints. Kill the window or reboot. Then start PS again. Use get-job and resume-job to resume the job.
workflow test1 {
foreach ($b in 1..1000) {
$b
Checkpoint-Workflow
}
}
test1 -AsJob -JobName bootjob
Emacs edit mode. Pressing tab completion lists all the options at once. Very useful.
Set-PSReadLineOption -EditMode Emacs
Any command that begins with "get-", you can leave off the "get-":
date
help
End parsing --% and end of parameters -- operators.
write-output --% -inputobject
write-output -- -inputobject
Tab completion on wildcards:
cd \pro*iles # press tab
Compile and import a C# module with a cmdlet inside, even in Osx:
Add-Type -Path ExampleModule.cs -OutputAssembly ExampleModule.dll
Import-Module ./ExampleModule.dll
Iterate backwards over a sequence just use the len of the sequence with a 1 on the other side of the range:
foreach( x in seq.length..1) { Do-Something seq[x] }