How to remove duplicate rows from a DataTable?
I have a Datatable1 & Datatable2. Datatable1 has 5 rows and Datatable2 has 1 row.
When I called $Datatable1.Merge($Datatable2), it is resulting in 7 rows instead of 6 rows.
The duplicate row is from Datatable2.
How can I remove this duplicate one?
Posting the OP's own solution as an actual answer:
function RemoveDups {
Param($dt)
$distinct = New-Object "System.Data.DataTable"
$hash = New-Object "System.Collections.Generic.HashSet[string]"
foreach ($column in $dt.Columns) {
$distinct.Columns.Add($column.ToString()) | Out-Null
}
foreach ($row in $dt.rows) {
$columnsToCheck = [string]($row.COLUMNNAME)
$result = $hash.Add($columnsToCheck)
if ($result -eq $true) {
[void]$distinct.Rows.Add($row.ItemArray)
}
}
return ,$distinct
}
try this:
$Datatable1.Merge($Datatable2)
$Datatable1=$Datatable1.DefaultView.ToTable($true)
Related
In my case i want to use PS script to build WinForm with some elements including DGV contains of 3 columns (#, Page_Name, shrt). First row need to be template row with default values(1;index;NDX)so i get it from csv-file.My code:
$DataGridView1 = New-Object system.Windows.Forms.DataGridView
$DataGridView1.location = New-Object System.Drawing.Point(20,121)
$DataGridView1.Name = "Page-List"
$DataGridView1.AllowUserToAddRowsChanged = $true
$DataGridView1.AllowUserToAddRows = $true
# $DataGridView1.DataBindings
$DataGridView1.width = 363
$DataGridView1.height = 150
$DataGridView1.ColumnCount = 3
$DataGridView1.ColumnHeadersVisible = $true
$DataGridView1.Columns[0].Name = '#'
$DataGridView1.Columns[0].Width = "40"
$DataGridView1.Columns[1].Name = "Page_Name"
$DataGridView1.Columns[1].Width = "205"
$DataGridView1.Columns[2].Name = "shrt"
$DataGridView1.Columns[2].Width = "75"
$DataGridView1.ReadOnly = $false
$DataGridView1.EditMode = "EditOnEnter"
$templateROW = #(Import-Csv -Delimiter ";" "C:\Users\vkons\OneDrive\Документы\PowerShell\Scripts\test\DGV\index.csv" -Header "#", "Page_Name", "shrt" )
$datatable = ($templateROW + $DataGridView1Rows)
$DataGridView1Data = $datatable
foreach ($Row in $DataGridView1Data){
$DataGridView1.Rows.Add($Row.'#', $Row.Page_Name, $Row.shrt)
}
If user will change Page_Name cells value in first row or will fill Page_Name cell`s in the next row (or rowS) - cells value in column "#" and column "shrt" in edited row(s) would get values programmly by this part code:
$DataGridView1.Add_CellValueChanged({autofill})
Function autofill{
$Numbr = $DataGridView1.CurrentRow.Index+1
$DataGridView1.CurrentRow.Cells[0].value = $Numbr
$Name_Page = $DataGridView1.CurrentRow.Cells[1].value
$preshrt = $Name_Page.ToString($Value) -ireplace "[aoueyi]"
$preshrt = $preshrt.ToUpper()
$shrt = $preshrt[0]+$preshrt[1]+$preshrt[2]
$DataGridView1.CurrentRow.Cells[2].value = $shrt
}
My main target is getting the values of all cells in a column Page_Name as a variable(or as array). So I tried to add next string to the function above.
$Pages = $Row.Page_Name+$DataGridView1.CurrentRow.Cells[1].value
But it returns nothing...(Either $Row.Page_Name)
I can get values of all cells in all rows by
$Page_NamesListRows = #($DataGridView1.Rows.Cells.Value)
(Unfortunately) it returns varriable, consist of all existing cells, not rows array.But when i try
$Page_Names = $DataGridView1.Rows.Cells[1].Value
or
$Page_Names = $DataGridView1.Columns[1].Cells.Value
to get only Names of the Pages, it returns error "cannot get value of a null-array" (either in case with #(...) for right part)
Could anybody answer... Is there any way to get values of all existing cells in "Page_Name" Column.Honestly it doesnt matter would the DGVData automaticly edit by changing cells value event or not.
I need to get only column "Page_Name" values.
In the end I want to apologize for my english language. It has rather poor level. And thank the moderator in advance for corrections my mistakes.
I'm afraid you will have to get the array of values by looping over the rows in the "Page_Name" column.
The last row in the DataGridView will always be the "New" row to create by the user, so you need to skip that one.
Either by doing this:
# -1 to skip the always present empty row at the bottom
$Page_Names = for($row = 0; $row -lt $DataGridView1.Rows.Count - 1; $row++) {
$DataGridView1.Rows[$row].Cells.Item("Page_Name").Value
}
Or something like:
$Page_Names = $DataGridView1.Rows | ForEach-Object {
$data = $_.Cells.Item("Page_Name").Value
if ($data) { $data }
}
Or:
$Page_Names = foreach ($row in $DataGridView1.Rows) {
$row.Cells.Item("Page_Name").Value
}
$Page_Names = $Page_Names[0..($Page_Names.Count - 2)]
The last alternative is costly, because it needs to recreate the entire array when removing the last item
P.S.1 Don't forget to call the Dispose() methods on both the $DataGridview1 object and the main form when done with the GUI
P.S.2 I don't see a property called AllowUserToAddRowsChanged on the DataGridView..
EDIT
To hopefully show better what I mean, here's a demo form with a DataGridView control on it.
The initial data comes from a dummy CSV file with this inside:
"1";"Page number 1";"PN1"
"2";"Page number 2";"PN2"
"3";"Page number 3";"PN3"
"4";"Page number 4";"PN4"
"5";"Page number 5";"PN5"
$form = New-Object System.Windows.Forms.Form
$form.ClientSize = New-Object Drawing.Size 580, 505
$form.text = "20/4/2020 v. 0.1 All Right reserved (c) "
$form.TopMost = $true
$DataGridView1 = New-Object system.Windows.Forms.DataGridView
$DataGridView1.Location = New-Object System.Drawing.Point 20,25
$DataGridView1.Width = 363
$DataGridView1.Height = 150
$DataGridView1.AllowUserToAddRows = $true
$DataGridView1.Name = "Page-List"
$DataGridView1.ColumnCount = 3
$DataGridView1.Columns[0].Name = '#'
$DataGridView1.Columns[0].Width = "40"
$DataGridView1.Columns[1].Name = 'Page_Name'
$DataGridView1.Columns[1].Width = "205"
$DataGridView1.Columns[2].Name = "shrt"
$DataGridView1.Columns[2].Width = "75"
$DataGridView1.AllowUserToAddRows = $true
$DataGridView1.ReadOnly = $false
# Populate the DGV with the data from the CSV
$CsvData = Import-Csv -Path 'D:\Test\TEMPLATE_ROW.csv' -Delimiter ";" -Header "#", "Page_Name", "shrt"
foreach ($row in $CsvData) {
[void]$DataGridView1.Rows.Add($row.'#', $row.Page_Name, $row.shrt)
}
# add the DGV to the form
$form.Controls.Add($DataGridView1)
# show the form and capture the result so you can check if the user cancelled or pressed OK
$result = $form.ShowDialog()
# at this point, you can read the data from the DataGridView column of interest
$Page_Names = for($row = 0; $row -lt $DataGridView1.Rows.Count - 1; $row++) {
$DataGridView1.Rows[$row].Cells.Item("Page_Name").Value
}
# cleanup memory by destroying the DGV and the from
$DataGridView1.Dispose()
$form.Dispose()
In variable $Page_Names you will now have the data from the "Page_Name" column in the DataGridView control.
# show on screen
$Page_Names
I have a search text box in a form that uses -match.
A Write-Host shows a positive result when the search matches, but the DataGridView is hidden everything.
I have tried 2 solutions but without success; both returns "MATCH!!!" but all rows become invisible, which is unexpected:
1st approach:
foreach ($row in $DataGridView1.Rows) {
foreach ($cell in $row.Cells) {
if ($cell.Value.ToString() -match ($searchTextBox.Text)) {
$DataGridView1.Rows[$row.Index].Visible = $true
Write-Host "MATCH!!!"
} else {
$DataGridView1.Rows[$row.Index].Visible = $false
}
}
}
2nd approach:
for ($i = 0; $i -lt $DataGridView1.RowCount; $i++) {
for ($j = 0; $j -lt $DataGridView1.ColumnCount; $j++) {
$CurrentCell = $DataGridView1.Rows[$i].Cells[$j]
if ($CurrentCell.Value.ToString() -match ($searchTextBox.Text)) {
$DataGridView1.Rows[$i].Visible = $true
Write-Host "MATCH!!!"
} else {
$DataGridView1.Rows[$i].Visible = $false
}
}
}
Looks like both codes are doing $DataGridView1.Rows[$i].Visible = $false every time.
The inner loops of both of your approaches define the visibility of the row for each cell individually. Unless the last cell in a row produces a match the row will end up hidden, even if a match was found in another cell before.
What you actually want is set the row visible if a match was found in any of the cells in a row, so you need to remember if a match was already found in the current row, and set the visibility after processing all cells of the row.
Change the inner loop to something like this:
$found = $false
foreach ($cell in $row.Cells) {
if ($cell.Value.ToString() -match ($searchTextBox.Text)) {
$found = $true
Write-Host "MATCH!!!"
break
}
}
$DataGridView1.Rows[$row.Index].Visible = $found
and it should do what you want.
The additional break in the condition is a performance optimization. Once a match is found there is no need to test the remaining cells of the row, so you can skip the rest of the loop, set the visibility, and move to the next row.
I am working on a GUI in powershell. I need help writing a foreach loop that looks at the information in a DataGrid and creates 2 variables based on the input in the 2 columns of each row.
something like
for each row
v1 = column1
v2 = column2
do something
then repeat.
Actual code
Function RenameComputers{
foreach ($item in $DataGrid.Items){
$OName = $item.OldName
$NName = $item.NewName
write-host "$OName and $NName"
netdom renamecomputer $OName /newName:$NName /uD:$Username /passwordD:$Password /force /reboot
}
}
foreach ($item in $dataGrid.Items) {
$oldname = $item.oldname
$newname = $item.newname
//Do stuff, you don't even need to create those variables you can just call $item.attribute
}
Then you can call the "columns" with $item.oldname or $item.newname etc.
I have the following function that I provide 3 arrays as variables
$columnHeaders = #('Ticket ID', 'Date Raised', 'Title', 'Status' )
$columnproperties = #('number', 'opened_at', 'short_description', 'state')
$contents
$contents has multiple rows of data matching the columns above, however sometimes may only have 1 row. When the $contents only has 1 row the below function errors out and doesnt print the data.
Using ISE I traced the issue to the $contents.count not showing a value, why is this? is there a way to get around it?
function TableOutput ($columnHeaders, $columnProperties, $contents){
# Number of columns
$columnCount = $columnHeaders.Count
# Create a new table
$docTable = $Word.ActiveDocument.Tables.Add($Word.Selection.Range,$contents.Count,$columnCount)
# Table style
$doctable.Style = "Adapt Table"
# Insert the column headers into the table
for ($col = 0; $col -lt $columnCount; $col++) {
$cell = $docTable.Cell(1,$col+1).Range
$cell.Font.Bold=$true
$cell.InsertAfter($columnHeaders[$col])
}
$doctable.Rows.Add() > Null
# Load the data into the table
$i = 1
$j = $contents.Count
for($row = 2; $row -lt ($contents.Count + 2); $row++){
if($row -gt 2){
}
for ($col = 1; $col -le $columnCount; $col++){
Write-Progress -Activity "Processing Table Information" -Status "Adding Row entry $i of $j" -PercentComplete (100*$i/$j)
$cell = $docTable.Cell($row,$col).Range
$cell.Font.Name="Calibri"
$cell.Font.Size="10"
$cell.Font.Bold=$FALSE
$cell.Text = $contents[$row-2].($columnProperties[$col-1])
}
$i++
}
$doctable.Columns.AutoFit()
}
any help is greatly appreciated.
Cast $content as an array of strings and see if that doesn't work better for you.
function TableOutput ($columnHeaders, $columnProperties, [string[]]$contents){
Edit: Sorry, my bad, you are passing objects with properties ad descripbed in $columnheaders, so you would need to cast it as an array of objects instead:
function TableOutput ($columnHeaders, $columnProperties, [object[]]$contents){
Tested on my end, it works fine with 1 object being passed to the function, as well as an array of two objects being passed to the function.
I'm running a stored procedure and am trying to filter out which columns are returned by GetSchemaTable()
$reader = $cmd.ExecuteReader()
$schemaTable = $reader.GetSchemaTable();
foreach ($row in $schemaTable.Rows)
{
foreach ($column in $schemaTable.Columns)
{
write-host $column;
}
}
...shows me a whole bunch of column names I don't care about and 2 that i do:
ColumnName and ColumnOrdinal
How do I go about restricting output to just those to fields?
thx
$reader = $cmd.ExecuteReader()
$reader.GetSchemaTable() | Select ColumnName, ColumnOrdinal