How can I build sums of values from DataTables in vb.net? - vb.net

I'm using VB.Net to retrieve data from a database, do some calculations, and then show this data in a datagridview. I already imported the data from the database into a DataTable in my software. So far so good.
Now I want to add up values from columns where another column in the same row has a specific text.
Here's an example. I have data (in the DataTable) in the following form:
Date // type // value
10/09/2020 // value80 // 92
10/09/2020 // value71 // 5
10/09/2020 // value63 // 2
10/07/2020 // value80 // 85
10/07/2020 // value71 // 10
10/07/2020 // value63 // 1
I want to build the sum of the 3 type values, where "value80", "value71" and "value63" are from the same date. The new variable value_sum should be calculated such that for 10/09/2020:
value_sum = value80 + value71 + value63 = 92 + 5 + 2 = 99
and for 10/7/2020:
value_sum = value80 + value71 + value63 = 85+ 10 + 1 = 96
How can I calculate the new values in order to save them in another DataTable?

In the Linq query we Group By the date field and select the date and the sum. See https://learn.microsoft.com/en-us/dotnet/visual-basic/language-reference/queries/group-by-clause There is a .CopyToDataTable method (https://learn.microsoft.com/en-us/dotnet/api/system.data.datatableextensions.copytodatatable?view=netcore-3.1) but I don't know if or how it works with anonymous types. It is easy enough to just loop through the query result and add the rows to the new DataTable.
Private Sub OPCode()
'Create original DataTable with your sample data
Dim dt As New DataTable
dt.Columns.Add("ValueDate", GetType(Date))
dt.Columns.Add("Type", GetType(String))
dt.Columns.Add("Value", GetType(Integer))
With dt.Rows
.Add(#10/09/2020#, "value80", 92)
.Add(#10/08/2020#, "value71", 5)
.Add(#10/09/2020#, "value85", 2)
.Add(#10/07/2020#, "value80", 85)
.Add(#10/07/2020#, "value71", 10)
.Add(#10/07/2020#, "value63", 1)
End With
'Select the date groups and sum by group
Dim TotalsByDate = From row In dt.AsEnumerable
Group By DateValue = row.Field(Of Date)("ValueDate")
Into Dates = Group, Sum(row.Field(Of Integer)("Value"))
Select DateValue, Sum
'Prepare a new DataTable
Dim dt2 As New DataTable
dt2.Columns.Add("DateGroup", GetType(Date))
dt2.Columns.Add("DateSum", GetType(Integer))
'Fill the new DataTable
For Each item In TotalsByDate
dt2.Rows.Add(item.DateValue, item.Sum)
Next
'Display results
DataGridView1.DataSource = dt2
End Sub

Related

is that possible to calculate the value between 2 or more textbox that are grouped?

I have a table that consists of 8 columns (A, B, C, etc). Column D and E are grouped by each month. Is it possible to calculate D and E value and return the value in column G?
Here is needed more information about your scenario, as yes it’s possible, even before you make this DataTable. However in an unknown scenario, you can do that even after, as code below shows.
(And if I well understand what you want to do):
Dim dataTable As New DataTable
dataTable.Columns.Add("A")
dataTable.Columns.Add("B")
dataTable.Columns.Add("C")
dataTable.Columns.Add("D")
dataTable.Columns.Add("Result_OF_B_C")
For i As Integer = 0 To 10
dataTable.Rows.Add(dataTable.NewRow)
dataTable.Rows(dataTable.Rows.Count - 1).ItemArray =
{"ColumnA" + i.ToString, i, i + i / 2, "ColumnD" + i.ToString}
Next
Dim newDataTable As DataTable = (From R In dataTable.Rows).Select(Function(R)
Dim Cr As DataRow = CType(R, DataRow)
Cr.Item("Result_OF_B_C") = CDbl(Cr.Item("B")) + CDbl(Cr.Item("C"))
Return Cr
End Function).CopyToDataTable
DataGridView1.DataSource = newDataTable

Import data from excel and consolidate

I'm using the below code to read from an excel file and add certain columns into a listview. Once imported i'm then exporting them to a CSV (code not shown).
My problem is that the excel file is a till extract and it displays the data by transaction which results in thousands of lines. I would like to perform the excel equivalent of SUMIF based on the EPoS line and consolidate the info if thats possible?
Sample of data below...
Public Structure ExcelRows
Dim Unit As String
Dim Outlet As String
Dim EPoS As String
Dim Quantity As String
Dim Value As String
Dim DateSale As String
End Structure
Public ExcelRowList As List(Of ExcelRows) = New List(Of ExcelRows)
Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
End Sub
Public Function GetInfo() As Boolean
Dim Completed As Boolean = False
Dim MyExcel As New Excel.Application
Dim enUK As New CultureInfo("en-GB")
Dim DOS As String = "01/04/15"
MyExcel.Workbooks.Open("C:\Dropbox\Tills\taRunAction1.xlsx")
MyExcel.Sheets("Report").Activate()
MyExcel.Range("A10").Activate()
Dim ThisRow As New ExcelRows
Do
If MyExcel.ActiveCell.Value > Nothing Or MyExcel.ActiveCell.Text > Nothing Then
ThisRow.Unit = MyExcel.ActiveCell.Value
MyExcel.ActiveCell.Offset(0, 1).Activate()
ThisRow.Outlet = MyExcel.ActiveCell.Value
MyExcel.ActiveCell.Offset(0, 1).Activate()
ThisRow.DateSale = MyExcel.ActiveCell.Value
MyExcel.ActiveCell.Offset(0, 2).Activate()
ThisRow.EPoS = MyExcel.ActiveCell.Value
MyExcel.ActiveCell.Offset(0, 1).Activate()
ThisRow.Quantity = MyExcel.ActiveCell.Value
MyExcel.ActiveCell.Offset(0, 1).Activate()
ThisRow.Value = MyExcel.ActiveCell.Value
ExcelRowList.Add(ThisRow)
MyExcel.ActiveCell.Offset(1, -6).Activate()
Else
Completed = True
Exit Do
End If
Loop
MyExcel.Workbooks.Close()
MyExcel = Nothing
Return Completed
End Function
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
If GetInfo() = True Then
For Each xItem In ExcelRowList
Dim lViewItem As ListViewItem
lViewItem = ListView1.Items.Add(xItem.Unit)
lViewItem.SubItems.AddRange(New String() {xItem.Outlet, xItem.EPoS, xItem.Quantity, xItem.Value, xItem.DateSale})
Next
End If
End Sub
#Plutonix's answer is a good idea and I totally agree with his comments about changing your ExcelRows structure/class fields from strings to the appropriate numerical type.
Another possibility is to use LINQ to group the data from your ExcelRowList. Something like the following
Dim results = From r In ExcelRowList
Group By r.EPoS
Into Group, Sum(r.Value * r.Quantity)
Note that this won't work as is because r.Value and r.Quantity are strings so you can't multiply them. So you should change the field types in your structure and then when you are looping through the cells, cast the cell value to the correct type. You will have to decide what to do if the cast fails.
Group By on MSDN.
One way to do this would be to use a DataTable:
Private dt As DataTable
...
dt = New DataTable
dt.Columns.Add("EPOS", GetType(Integer))
dt.Columns.Add("Unit", GetType(String))
dt.Columns.Add("Quantity", GetType(Integer))
dt.Columns.Add("Value", GetType(Decimal))
dt.Columns.Add("Total", GetType(Decimal))
dt.Columns.Add("Date", GetType(DateTime))
Dim keys As DataColumn() = {dt.Columns("EPOS")}
dt.PrimaryKey = keys
It is not clear whether "VALUE" is the Unit Price or Sale Amount because all the units are 1. I am not sure I would repeat data such as the SaleDate over and over, The key is that the DataTable could replace the XLRows Structure.
One key is that you want to work with properly Typed data so you can multiply and add. The XLRow structure is not properly typed - everything is string rather than Date, Decimal, Integer etc.
A. XLRow Class to Convert
For this, convert the XLRow structure to a class. Its role would be to take in string data from XL and convert to Typed data. Next, the DataTable is used to collect the rollup data.
For this, the PrimaryKey definition above is critical. It will prevent you from adding a second (or 1000th) "Pepsi" item and allow you to find that summary item. Perhaps best of all, you can get rid of the ListView and use a DataGridView:
dgv.DataSource = dt
With one line of code, the DGV will create the columns and display all the rows in the DataTable. Unlike a ListView it will update itself as the underlying data in the DataTable changes. To iterate your data to summarize inside a loop:
' get the row for this EPOS code
Dim dr As DataRow = dt.Rows.Find(xl.EPOS)
If dr IsNot Nothing Then
' we already have this item, increment Quan, TotalSales:
dr("Quantity") += xl.Quantity
dr("Total") += (xl.Quantity * xl.Value)
Else
' new transaction item, add it:
dt.Rows.Add(xl.EPOS, xl.Unit, xl.Quantity,
xl.Value, (xl.Quantity * xl.Value), xl.DateSale)
End If
This is pretty concise, because as you read data from XL, it is added to the Summary. There is no need to import all the detail data (XLS rows) into a collection or datatable before you perform the rollup.
B. Use Linq instead of the XLRow structure
For this omit the Primary Key in the DataTable. In this case, the DataTable would collect the raw XLS data instead of the XLRow structure. Do convert to numerics for the rollup. Next, use linq to summarize the data; perhaps into another DataTable. Example:
Sample Data:
dt.Rows.Add(10001, "Ginger Ale", 1, 2.25, #4/5/2015#)
dt.Rows.Add(34582, "Pepsi", 3, 6.0, #4/5/2015#)
dt.Rows.Add(10002, "Chips", 1, 3.25, #4/5/2015#)
dt.Rows.Add(34582, "Pepsi", 1, 2.0, #4/5/2015#)
dt.Rows.Add(78301, "Roast Duck", 1, 15.25, #4/5/2015#)
dt.Rows.Add(34582, "Pepsi", 1, 2.0, #4/5/2015#)
dt.Rows.Add(34582, "Pepsi", 1, 2.0, #4/5/2015#)
dt.Rows.Add(10002, "Chips", 1, 3.25, #4/5/2015#)
dt.Rows.Add(34582, "Pepsi", 1, 2.0, #4/5/2015#)
To get the summary info:
' group the data by EPOS code
Dim drs = From row In dt.AsEnumerable()
Group row By ID = row.Field(Of Integer)("EPOS") Into Group
Select Group
Dim TotSales As Decimal
Dim TotUnits As Integer
' each DRS is a collection of all the items with the same EPOS code
Dim dr As DataRow()
Console.WriteLine("EPOS Item Lines Units Total Sales")
' get the total sales in each group
For n As Integer = 0 To drs.Count - 1
dr = drs(n) ' the current EPOC group
TotUnits = dr.Sum(Function(t) t.Field(Of Integer)("Quantity"))
' Sales could just be TotUnits * dr(0)("Value")
' sample data makes it unclear if Value is the UNITPRICE or SALEAMOUNT
' This assumes it is SALEAMOUNT such that 2 Pepsi = 4.00
TotSales = dr.Sum(Function(t) t.Field(Of Decimal)("Value"))
' ToDo: do something interesting with the totals
Console.WriteLine("{0} {1} {2} {3} {4}",
dr(0)("EPOS"),
dr(0)("Unit").ToString,
dr.Length.ToString("D2"),
TotUnits.ToString,
TotSales.ToString("C2"))
Next
The output:
EPOS Item Lines Units Total Sales
10001 Ginger Ale 01 1 $2.25
34582 Pepsi 05 7 $14.00
10002 Chips 02 2 $6.50
78301 Roast Duck 01 1 $15.25
The key is that it does act much like SUMIF once you have the Typed data in a useful structure. The result shows that the data has 5 Pepsi entries, 7 total units and 7*2 = 14.
I think the loop version is slightly easier to manage and debug, and it much more economical since the Summary is built on the fly as XLS rows are read in.

Editing Datable before binding to listview in vb.net

hi this is my code to get datable from query but i need to check for some values like if there is 0 in any column then replace it with N.A.
Dim op As New cad
Dim dt As DataTable = op.Fetchbooks().Tables(0)
Dim sb As New StringBuilder()
Dim rp As System.Data.DataRow
If dt.Rows.Count > 0 Then
ListView1.DataSource = dt
ListView1.DataBind()
DropDownList1.DataSource = dt
DropDownList1.DataTextField = "Description"
DropDownList1.DataValueField = "Description"
DropDownList1.DataBind()
End if
so is there any way to check some values and edit in datable before binding ???
First, if possible i would modify your sql-query in Fetchbooks instead to replace 0 with N.A.. I assume you are querying the database.
However, if you want to do it in memory:
For Each row As DataRow In dt.Rows
For Each col As DataColumn In dt.Columns
If row.IsNull(col) OrElse row(col).ToString = "0" Then
row(col) = "N.A."
End If
Next
Next
Sure: you can iterate through the table, making whatever value replacements are appropriate (assuming that your replacement values are type-compatible with what came from the query).
Something like:
For Each row as DataRow in dt.Rows
if row("columnname") = "left" then
row("columnname") = "right"
Next
Then bind it to the grid, and you should see your updated values.
Once you have the DataTable object you can manipulate it as you need:
For Each rp In dt.Select("COLUMN_A = '0'")
rp("COLUMN_A") = "N.A."
Next
Note that this assumes that COLUMN_A is defined as a string type, not a numeric.
The challenge will be if you intend on saving this data back to it's source and you don't want the original 0 value to be saved instead of N.A.. You could add dt.AcceptChanges immediately after the above loop so that it will appear as the Fetchbooks query had these values all along.

Split datatable into n datatables

I need to dynamically split a datatable into multiple datatables and the number of subsequent datatables will vary. The end user will enter a value and this will determine the number of datatables that will be derived from the original. For example: the user enters 10, the original dt has 20 rows. There will be 10 dt's with 2 rows each created. However, if the original dt has 11 rows, there would be 9 dt's with 1 row and 1 dt with 2 rows created. How can I accomplish this in vb.net without hardcoding a bunch if rules? I have read through and tried the post below but it still does not get me there.
Split a collection into `n` parts with LINQ?
You can use LINQ's GroupBy:
Dim tbl1 = New DataTable
tbl1.Columns.Add("ID", GetType(Int32))
tbl1.Columns.Add("Text", GetType(String))
For rowIndex As Int32 = 1 To 11
tbl1.Rows.Add(rowIndex, "Row " & rowIndex)
Next
Dim tableCount = 10 ' what the user entered '
Dim divisor = tbl1.Rows.Count / tableCount ' needed to identify each group '
Dim tables = tbl1.AsEnumerable().
Select(Function(r, i) New With {.Row = r, .Index = i}).
GroupBy(Function(x) Math.Floor(x.Index / divisor)).
Select(Function(g) g.Select(Function(x) x.Row).CopyToDataTable())

Convert the following VB code to a LINQ statement?

I only want to return a certain number of rows into the DataTable via LINQ's Take or use it on the Rows property, but I am not sure how or where to do it: Here is the current code:
Dim dt As DataTable = GetDataTable("sp", params)
For Each dr As DataRow In dt.Rows
Dim o As New OR()
o.P = p
o.Id = dr ("A")
o.R = dr ("B")
Next
Would it be something like:
Dim dt As DataTable = GetDataTable("sp", params).AsEnumerable().Take(10)
When I run the above, I get the error:
The 'TakeIterator' start tag on line 4 position 60 does not match the end tag of 'Error'. Line 4, position 137.
Unable to cast object of type '<TakeIterator>d__3a1[System.Data.DataRow]' to type 'System.Data.DataTable'.`
If you need a DataTable:
Dim dt As DataTable = (From row in GetDataTable("sp", params) Take 10).CopyToDataTable()
If you need a List(Of [Or]) (you need the brackets since Or is a keyword)
Dim list As List(Of [Or]) = (From row In GetDataTable("sp", params)
Select New [Or]() {
o.Id = row.Field(Of Int32)("Id"),
o.R = row.Field(Of String)("R")
}).Take(10).ToList()
Edit:
is there a way I can also include a Where with the Take, for example, I have 3 statuses in my table, Open, Semi-Open, Closed, I only want to Take 10 for Open, but I want to leave Semi-Open and Closed alone. As an example Semi-Open and Closed will have 5 records combined and I will take 10 from Open, so I would get a total of 15 rows
Yes, there's a way:
Dim dt = GetDataTable("sp", params)
Dim open10 = From row In dt
Where row.Field(Of String)("Status") = "Open"
Take 10
Dim rest = From row In dt
Where row.Field(Of String)("Status") <> "Open"
Dim tblResult = open10.Concat(rest).CopyToDataTable()