PowerShell script efficiency advice - powershell

I have a telephony .csv with compiled data from January 2020 and some days of February, each row has the date and time spent on each status, since someone uses different status over the day the file has one row for each status, my script is supposed to go through the file, find the minimum date and then start saving on new files all the data for the same day, so I'll end with one file for 01-01-2020, 02-01-2020 and so on, but it has 15 hours running and it's still at 1/22.
The column I'm using for the dates is called "DateFull" and this is the script
write-host "opening file"
$AT= import-csv “C:\Users\xxxxxx\Desktop\SignOnOff_20200101_20200204.csv”
write-host "parsing and sorting file"
$go= $AT| ForEach-Object {
$_.DateFull= (Get-Date $_.DateFull).ToString("M/d/yyyy")
$_
}
Write-Host "prep day"
$min = $AT | Measure-Object -Property Datefull -Minimum
Write-Host $min
$dateString = [datetime] $min.Minimum
Write-host $datestring
write-host "Setup dates"
$start = $DateString - $today
$start = $start.Days
For ($i=$start; $i -lt 0; $i++) {
$date = get-date
$loaddate = $date.AddDays($i)
$DateStr = $loadDate.ToString("M/d/yyyy")
$now = Get-Date -Format HH:mm:ss
write-host $datestr " " $now
#Install-Module ImportExcel #optional import if you dont have the module already
$Check = $at | where {$_.'DateFull' -eq $datestr}
write-host $check.count
if ($check.count -eq 0 ){}
else {$AT | where {$_.'DateFull' -eq $datestr} | Export-Csv "C:\Users\xxxxx\Desktop\signonoff\SignOnOff_$(get-date (get-date).addDays($i) -f yyyyMMdd).csv" -NoTypeInformation}
}
$at = ''

The first loop doesn't make much sense. It loops through CSV contents and converts each row's date into different a format. Afterwards, $go is never used.
$go= $AT| ForEach-Object {
$_.DateFull= (Get-Date $_.DateFull).ToString("M/d/yyyy")
$_
}
Later, there is an attempt to calculate a value from uninitialized a variable. $today is never defined.
$start = $DateString - $today
It looks, however, like you'd like to calculate, in days, how old eldest record is.
Then there's a loop that counts from negative days to zero. During each iteration, the whole CSV is searched:
$Check = $at | where {$_.'DateFull' -eq $datestr}
If there are 30 days and 15 000 rows, there are 30*15000 = 450 000 iterations. This has complexity of O(n^2), which means runtime will go sky high for even relative small number of days and rows.
The next part is that the same array is processed again:
else {$AT | where {$_.'DateFull' -eq $datestr
Well, the search condition is exactly the same, but now results are sent to a file. This has a side effect of doubling your work. Still, O(2n^2) => O(n^2), so at least the runtime isn't growing in cubic or worse.
As for how to fix this, there are a few things. If you sort the CSV based on date, it can be processed afterwards in just a single run.
$at = $at | sort -Property datefull
Then, iterate each row. Since the rows are in ascending order, the first is the oldest. For each row, check if date has changed. If not, add it to buffer. If it has, save the old buffer and create a new one.
The sample doesn't convert file names in yyyyMMdd format, and it assumes there are only two columns foo and datefull like so,
$sb = new-object text.stringbuilder
# What's the first date?
$current = $at[0]
# Loop through sorted data
for($i = 0; $i -lt $at.Count; ++$i) {
# Are we on next date?
if ($at[$i].DateFull -gt $current.datefull) {
# Save the buffer
$file = $("c:\temp\OnOff_{0}.csv" -f ($current.datefull -replace '/', '.') )
set-content $file $sb.tostring()
# Pick the current date
$current = $at[$i]
# Create new buffer and save data there
$sb = new-object text.stringbuilder
[void]$sb.AppendLine(("{0},{1}" -f $at[$i].foo, $at[$i].datefull))
} else {
[void]$sb.AppendLine(("{0},{1}" -f $at[$i].foo, $at[$i].datefull))
}
}
# Save the final buffer
$file = $("c:\temp\OnOff_{0}.csv" -f ($current.datefull -replace '/', '.') )
set-content $file $sb.tostring()

Related

how to read time from file and perform minus operation on hours using powershell

I have created a text file that contains id and time[24hr format] that is going to trigger one operation at the mentioned time.
I want to read both the values and minus 2 hrs from mentioned hr.
I tried, but I am not sure how to read input as time format(24hr notation HH:MM) and minus 2 hrs from that and also the scenario like 01:30 - 2hr = 23.30 (previous date)
$lines = Get-Content C:\wamp64\www\schedule_daily_task.txt | Where {$_ -notmatch '^\s+$'}
foreach ($line in $lines) {
$fields = $line -split '\s+'
$id = $fields[0]
$time = $fields[1]
#code here to change the format of $time to time format and perform minus oper
#01:30 - 2hr = 23.30 (previous date)
}
For date and time arithmetics, use .Net's DateTime class. It has method AddHours() which, when passed negative values, subtracts hours. Calculating new datetime to yesterday is quite straight-forward. Like so,
# First, let's get today's date with specific time and minute
$hh = ($fields[1] -split ':')[0]
$mm = ($fields[1] -split ':')[1]
$dd = get-date -date $(get-date).date -hour $hh -minute $mm
# Add -2 hours
$dd.AddHours(-2)
I would use switch -Regex -File for this to read the file line-by-line and get the values from the lines you are after using regex.
Advantage is that this is crazy fast aswell.
$result = switch -regex -File 'C:\wamp64\www\schedule_daily_task.txt' {
'^(\d+)\s+(\d{2}:\d{2})' {
$id = $Matches[1]
$h, $m = $Matches[2] -split ':'
$time = '{0:HH:mm}' -f (Get-Date -Hour $h -Minute $m -Second 0).AddHours(-2)
# output the id and updated time
"$id $time"
}
default {
# output any other line you may have in the file. You can of course
# also ignore these if they are unwanted in the output. In that case
# simply comment out this whole 'default' block
$_
}
}
# output to (new) file and because of '-PassThru' also on screen
$result | Set-Content -Path 'C:\wamp64\www\schedule_daily_task_minus_2.txt' -PassThru
Example input file:
2603961 01:30
1234567 11:59
9876543 23:30
Example output file:
2603961 23:30
1234567 09:59
9876543 21:30

Finding value in a CSV file and comparing with today's date

I'm trying to get a value from a CSV file.
If today's date = DateInCSVFile give the "key" value.
Keys.csv
Guest,Key
1-Jun,OIOMY-ZFILZ
2-Jun,LSSJC-PDEUL
3-Jun,MQNVJ-TETLV
4-Jun,HCJIJ-ECVPY
5-Jun,SPACR-AJSLU
6-Jun,MEURS-UQTVX
Code:
$today = Get-Date -format dd-MMM
$keys = import-csv c:\office\keys.csv -Header #(1..2)
$data = $keys | ? { $_.1 -match $today}
Write-Host $data.2
I tried the foreach and if commands. Nothing worked.
I can think of a couple of options. If you want something quick and dirty, try:
$stuff = Import-Csv -Path .\stuff.csv
foreach ($thing in $stuff) {
if ( $thing.Guest -eq $(Get-date -Format 'd-MMM') ) {
Write-Output $thing.Key
}
}
I import the CSV file's contents to a variable. I iterate over each line. If the day in Guest matches the current day, I output the key
The only problem with your code is your date format, dd-MMM, as LotPings observes:
It creates 0-left-padded numbers for single-digit days such as 6, whereas the dates in the CSV have no such padding.
Thus, changing Get-Date -format dd-MMM to Get-Date -format d-MMM (just d instead of dd) should fix your problem.
However, given that you're reading the entire file into memory anyway, you can optimize the command to (PSv4+):
$today = Get-Date -Format d-MMM
(Import-Csv c:\office\keys.csv).Where({ $_.Guest -eq $today }).Key
Also note that the purpose of -match is to perform regular-expression-based matching, not (case-insensitive) string equality; use -eq for the latter.

Compare current date to date string in a file using powershell

I am writing some PS scripts to log times into a text file, login.txt, using the following code:
$logdir = "C:\FOLDER"
$logfile = "$logdir\LastLogin.txt"
$user = $env:USERNAME
$date = Get-Date -Format "dd-MM-yyyy"
if (!(Test-Path $logdir)){New-Item -ItemType Directory $logdir}else{}
if (!(Test-Path $logfile)){New-Item $logfile}else{}
if (Get-Content $logfile | Select-String $user -Quiet){write-host "exists"}else{"$user - $date" | Add-Content -path $logfile}
(Get-Content $logfile) | Foreach-Object {$_ -replace "$user.+$", "$user - $date"; } | Set-Content $logfile
This creates an entry in the text file like:
UserName - 01-01-1999
Using Powershell, I want to read the text file, compare the date, 01-01-1999, in the text file to the current date and if more than 30 days difference, extract the UserName to a variable to be used later in the script.
I would really appreciate any hints as to how I could do the following:
Compare the date in the text file to the current date.
If difference is more than 30 days, pick up UserName as a variable.
I would really appreciate any advice.
Checking all dates in the file with the help of a RegEx with named capture groups.
$logdir = "C:\FOLDER"
$logfile = Join-Path $logdir "LastLogin.txt"
$Days = -30
$Expires = (Get-Date).AddDays($Days)
Get-Content $logfile | ForEach-Object {
if ($_ -match "(?<User>[^ ]+) - (?<LastLogin>[0-9\-]+)") {
$LastLogin = [datetime]::ParseExact($Matches.LastLogin,"dd-MM-yyyy",$Null)
if ( $Expires -gt $LastLogin ) {
"{0} last login {1} is {2:0} days ago" -F $Matches.User, $Matches.LastLogin,
(New-TimeSpan -Start $LastLogin -End (Get-Date) ).TotalDays
}
}
}
Sample output
username last login 31-12-1999 is 6690 days ago
There is a way of doing that using regex (Regular Expressions). I will assume that the username which you get in your text file is .(dot) separated. For example, username looks like john.doe or jason.smith etc. And the entry in your text file looks like john.doe - 01-01-1999 or jason.smith - 02-02-1999. Keeping these things in mind our approach would be -
Using a regex we would get the username and date entry into a single variable.
Next up, we will split the pattern we have got in step 1 into two parts i.e. the username part and the date part.
Next we take the date part and if the difference is more than 30 days, we would take the other part (username) and store it in a variable.
So the code would look something like this -
$arr = #() #defining an array to store the username with date
$pattern = "[a-z]*[.][a-z]*\s[-]\s[\d]{2}[-][\d]{2}[-][\d]{4}" #Regex pattern to match entires like "john.doe - 01-01-1999"
Get-Content $logfile | Foreach {if ([Regex]::IsMatch($_, $pattern)) {
$arr += [Regex]::Match($_, $pattern)
}
}
$arr | Foreach {$_.Value} #Storing the matched pattern in $arr
$UserNamewithDate = $arr.value -split ('\s[-]\s') #step 2 - Storing the username and date into a variable.
$array = #() #Defining the array that would store the final usernames based on the time difference.
for($i = 1; $i -lt $UserNamewithDate.Length;)
{
$datepart = [Datetime]$UserNamewithDate[$i] #Casting the date part to [datetime] format
$CurrentDate = Get-Date
$diff = $CurrentDate - $datepart
if ($diff.Days -gt 30)
{
$array += $UserNamewithDate[$i -1] #If the difference between current date and the date received from the log is greater than 30 days, then store the corresponding username in $array
}
$i = $i + 2
}
Now you can access the usernames like $array[0], $array[1] and so on. Hope that helps!
NOTE - The regex pattern will change as per the format your usernames are defined. Here is a regex library which might turn out to be helpful.

Importing csv rows to powershell, with date specified as greater than

I have two csv files. One with a report from AD, containing accounts created during last month, second is manually kept database, that should theoretically contain the same information, but from all history of our company, with some additional data needed for accounting. I imported the AD report to powershell, now I need to import specific rows of the database. The rows I need are defined by a value in column "Date added". I need to import only rows, where the date exceeds specific value. I have this code:
$Report = Read-Host "File name" #AD report, last ten chars are date of report creation, in format yyyy-MM-dd
$Date_text = $Report.Substring($Report.get_Length()-10)
$Date = Get-Date -Date $Date_text
$Date_limit = (($Date).AddDays(-$Date.Day)).Date
$Date_start = $Date_limit.AddMonths(-1)
$CSVlicence = Import-Csv $Database -Encoding UTF8 |
where {(Where-Object {![string]::IsNullOrWhiteSpace($_.'Date added')} |
ForEach-Object{$_.'Date added' = $_.'datum Pridani' -as [datetime] $_}) -gt $Date_start}
When run like this, nothing is imported. Without the condition the database is imported successfully, but it's extremely large and the rest of the script takes for ever. So I need to work only with relevant data. I don't care, that when Date_limit is 30th Sep, the Date_start would be 30th Aug instead of 31st Aug. That's just few more rows, but all those 10 years or so really takes for ever, if everything is imported.
So based on your current logic, it filters the PSCustomObject based on your constraints, which is the wrong way to handle it since any item in the object will cause it to be filtered out. You want to filter the source.
$Report = Read-Host -Prompt 'Filename'
## Grabs the datestamp at the end
$Date = Get-Date -Date $Report.Substring($Report.Length - 10)
## Grabs last day of previous month
$Limit = $Date.AddDays(-$Date.Day)
## Grabs last day of two months ago, inaccuracy of one day
$Start = $Limit.AddMonths(-1)
Get-Content -Path $Database -TotalCount 1 | Set-Content -Path 'tempfile'
Get-Content -Path $Database -Encoding 'UTF8' |
## Checks that the entry has a valid date entry within limits
ForEach-Object {
## For m/d/yy or d/m/yy variants, try
## '\d{1,2}\/\d{1,2}\/\d{2,4}'
If ($_ -match '\d{4}-\d{2}-\d{2}')
{
[DateTime]$Date = $Matches[0]
If ($Date -gt $Start -and $Date -lt $Limit) { $_ }
}
} |
Add-Content -Path 'tempfile'
$CSV = Import-Csv -Path 'tempfile' -Encoding 'UTF8'

Piping error with extra line and does not give a header

0001;Third Week;Every Monday 12am-2am
002;Third Week;Every Tuesday 8pm-10pm
003;Third Week;Every Monday 12am-2am
#Get the number of lines in a CSV file
$Lines = (Import-Csv "C:\MM1.csv").count
#Import the CSV file
$a = #(Import-CSV "C:\MM1.csv")
$month = Get-Date -Format MMMM
#loop around the end of the file
for ($i=0; $i -le $lines; $i++) {
$Servername = $a[$i].ServerName
$week = $a[$i].Week
$dayweekString = [String]$a[$i].DayTime
# This will help in getting the Day of the WeekDay String
$dayweekString = ($dayweekString -split "\s+",(3))[(1)]
#This will find the time Ex 2am or 8pm, it can be any time
$DayNew = if ($Day -match "\d{1,2}Travelm") {$Matches[0]}
#Format for Maintenance mode which can be fed into the SCOM MM Script.
$MaintenanceTime = get-date "$DayNew $month.$($_.$dayweekString).$year"
write-host $Servername, $MaintenanceTime
#Store all my data while in a for each loop
New-Object -TypeName PSObject -Property #{
ServerNameNew = $Servername
TimeStamp = $MaintenanceTime
} | Select-Object ServerNameNew,TimeStamp
} | Export-Csv -Path "C:\MM3.csv" -Append
# Error as extra Pipe
I am not able to pipe the output while in a loop with a header. Says Extra pipe and writes a extra line of TimeStamp Variable to the line.
You are trying to pipe the result of the for statement to another command. That's not supported. If you want to do that, you need to use a subexpression around the statement:
$( for(){} ) | ...
(The reason is that pipelines need an expression as their first element. And for is not an expression, it's a statement.)
However, in your case I'd replace the for with a simple pipeline iterating over the array, like this:
Import-CSV "C:\MM1.csv" | ForEach-Object {
$Servername = $_.ServerName
$week = $_.Week
$dayweekString = [String]$_.DayTime
...
}
Generally there is very rarely a reason to use explicit looping constructs in PowerShell. It leads to code that's awkward at best (because it resembles converted C# or VBScript code) and horrible at worst. Just don't do it.