Need To Return GroupBy Data With Counts But Selecting Multiple Fields Using LINQ - vb.net

Okay, I'm sure this is simple for someone but it is starting to cause me to tear my hear out so I apologise in advance if this seems like a simple question:
I have the following class:
Public Class Ticket
Public Property ticket_id As String
Public Property ticket_assignee As String
Public Property ticket_email As String
End Class
I am trying to use LINQ to group my list by the ticket_email field but allow me to get the ticket_assignee field and count of each group.
So for example:
For each email address, return me the assignee name and how many tickets in that email group like below:
'fred#someplace.com' = Fred, 4
'joe#someplace.com' = Joe, 2
'bob#someplace.com' = Bob, 8
etc...
I am still trying to get my head around LINQ and would love to understand it more. Any help would be appreciated thankyou.

A Query Syntax Solution:
Sub Main
Dim q = from entry in Ticket.GetTickets
group entry by entry.ticket_assignee Into NewGroup = Group, Count()
select new with{
.Assignee = NewGroup.Select(function(x) x.ticket_assignee).First,
.Email = NewGroup.Select(function(x) x.ticket_email).First,
.Count = Count}
'q.dump()
End Sub
' Define other methods and classes here
Public Class Ticket
Public Property ticket_id As Integer
Public Property ticket_assignee As String
Public Property ticket_email As String
Public Shared Function GetTickets as list(of Ticket)
Return New List(of Ticket) From {New Ticket With {.ticket_id = 1, .ticket_assignee = "Fred", .ticket_email = "fred#ticket.de"},
New Ticket With {.ticket_id = 2, .ticket_assignee = "Fred", .ticket_email = "fred#ticket.de"},
New Ticket With {.ticket_id = 3, .ticket_assignee = "Fred", .ticket_email = "fred#ticket.de"},
New Ticket With {.ticket_id = 4, .ticket_assignee = "Anna", .ticket_email = "Anna#ticket.de"},
New Ticket With {.ticket_id = 5, .ticket_assignee = "Anna", .ticket_email = "Anna#ticket.de"},
New Ticket With {.ticket_id = 6, .ticket_assignee = "Jack", .ticket_email = "Jack#ticket.de"}}
End Function
End Class
Gives you:
For learning Linq I highly recommend LinqPad. You can choose VB.NET Program, copy my code, uncomment the .dump() line and run it.

Dim tickets As New List(Of Ticket)
For i = 1 To 3
tickets.Add(New Ticket With {.ticket_id = i.ToString, .ticket_email = "a", .ticket_assignee = "me"})
Next
For i = 4 To 6
tickets.Add(New Ticket With {.ticket_id = i.ToString, .ticket_email = "b", .ticket_assignee = "me2"})
Next
Dim group = tickets.GroupBy(Function(x) x.ticket_assignee).Select(Function(y) New Tuple(Of String, Integer)(y.First.ticket_assignee, y.Count))

Related

Visual Basic Text File

I'm currently learning about Visual Basic text files but I came across a problem. I'm supposed to create a text file (Players) with data inside and I have to design a form with listbox to include the players’ names that are more than 30 years old.
This is my current code:
Dim q1 = From itm As String In IO.File.ReadAllLines("Players.txt")
Let Data=itm.Split(","c)
Let fname = Data(0)
Let age = Data(4)
Let newline = fname * " "& age
Where age > 30
For Each itm1 As String in q1
ListBox1.Items.Add(itm1)
Next
My expected output should show the names of players that are over 30 years old. Thank you in advance to anyone that can help me solve this issue.
You can use linq. For example: assume you have a txt like that
Giuseppe, 30
Pippo, 13
Luca, 32
to extract only over 30 years old you can do...
Dim obj = My.Computer.FileSystem.ReadAllText("Players.txt").Split(vbCrLf).ToList()
Dim ret = (From a In obj Where a.Split(",")(1) > 30 Select a).ToList
The result is
Luca, 32
Best to use a class to define Player. I also made a class Players to hide the file processing from the consumer.
Public Class Player
Public Property Name As String
Public Property Age As Integer
End Class
Public Class Players
Private _list As New List(Of Player)()
Public ReadOnly Property List As IEnumerable(Of Player)
Get
Return _list
End Get
End Property
Public Sub New(path As String)
Dim lines = File.ReadAllLines(path)
For Each line In lines
Dim split = line.Split(","c)
If split.Count = 2 Then
_list.Add(New Player() With {.Name = split(0), .Age = Integer.Parse(split(1))})
End If
Next
End Sub
End Class
And use databinding to populate the ListBox
Dim ps = New Players("Players.txt")
Me.ListBox1.DataSource = ps.Items.Where(Function(p) p.Age >= 30).ToList()
Me.ListBox1.DisplayMember = "Name"
If you're not into the whole Players class and Items property, you can still use the Player class, and just do all the processing in your consuming code (it's basically the same thing, but the processing code is not encapsulated in the model).
Dim ps = New List(Of Player)()
Dim lines = File.ReadAllLines("Players.txt")
For Each line In lines
Dim split = line.Split(","c)
If split.Count = 2 Then
ps.Add(New Player() With {.Name = split(0), .Age = Integer.Parse(split(1))})
End If
Next
Me.ListBox1.DataSource = ps.Where(Function(p) p.Age >= 30).ToList()
Me.ListBox1.DisplayMember = "Name"

Difference between two Lamba Expressions in LINQ

I am trying to understand the difference between these two Lambda Expressions within LINQ Join method.
The two lines I do not understand are those starting with Function(aaa) ... and Function(bbb) ....
Why do I have explicitly name the field Name in the second example for both aaa and bbbwhile in the first one it works without it?
On the other hand in the first example bbb.Owner points to Owner field of Pet but it does not tell that Name field of the Person shall be used to make the join.
First example (comes from .Net Framework documentation with variable names changed):
Structure Person
Public Name As String
Public SecondName As String
End Structure
Structure Pet
Public Name As String
Public Owner As Person
End Structure
Dim magnus As New Person With {.Name = "Hedlund, Magnus"}
Dim terry As New Person With {.Name = "Adams, Terry"}
Dim charlotte As New Person With {.Name = "Weiss, Charlotte"}
Dim barley As New Pet With {.Name = "Barley", .Owner = terry}
Dim boots As New Pet With {.Name = "Boots", .Owner = terry}
Dim whiskers As New Pet With {.Name = "Whiskers", .Owner = charlotte}
Dim daisy As New Pet With {.Name = "Daisy", .Owner = magnus}
Dim people As New List(Of Person)(New Person() {magnus, terry, charlotte})
Dim pets As New List(Of Pet)(New Pet() {barley, boots, whiskers, daisy})
Dim query = people.Join(pets,
Function(aaa) aaa,
Function(bbb) bbb.Owner,
Function(ccc, ddd) _
New With {.OwnerName1 = ccc.Name, .Pet1 = ddd.Name})
Second example (my code)
Structure MyObject
Public Name As String
Public Value As Integer
End Structure
Dim Test1 As New List(Of MyObject) From {
New MyObject With {.Name = "a", .Value = 1},
New MyObject With {.Name = "b", .Value = 2},
New MyObject With {.Name = "c", .Value = 3}
}
Dim Test2 As New List(Of MyObject) From {
New MyObject With {.Name = "a", .Value = 11},
New MyObject With {.Name = "b", .Value = 22},
New MyObject With {.Name = "c", .Value = 33}
}
Dim Joined = Test1.Join(Test2,
Function(aaa) aaa.Name,
Function(bbb) bbb.Name,
Function(ccc, ddd) New With {
.Name1 = ccc.Name,
.Value1 = ccc.Value,
.Value2 = ddd.Value})
IF you look at the documentation, you'll see that the first function is the selector for the outer sequence and the second is the selector for the inner sequence.
The value form the inner and outer selectors must have the same type (TKey) and will be compared using the default comparer for that type (there's an overload that allows you to pass a comparer).
The third function is the result selector that receives a TOuter and a TInner instances that match according to the selected keys and returns a TResult value.
So, in the first example TKey is Person. outerSelector and innerSelector must return a Person, thus aaa and bbb.
On the second example you chose to compare the Name field and not the entire MyObject structure.

(VB.NET) Find all duplicates in a list of objects based on multiple properties

I have a list of CommissionStatement objects which i created. I need to create a new list which only holds the duplicates found in this list based on 3 properties: Firm; Provider; and Total (ie each of these 3 have to be the same in 2 or more objects for it to be recognized as a duplicate)
Object is a simple object of strings at the moment.
Private Class CommissionStatement
Property Provider As String
Property Firm As String
Property Source As String
Property Media As String
Property Total As String
Property Received As String
End Class
I have a list of all CommissionStatments as follows:
Dim fileLocation As String = importText.Text
Dim csvText As String = My.Computer.FileSystem.ReadAllText(fileLocation).Replace(", ", " ")
Dim providerString As String = ""
Dim allStatements = New List(Of CommissionStatement)
Dim countIndex As Integer = 0, maxIndex As Integer = csvText.Split(vbLf).Length
For Each line As String In csvText.Split(vbLf)
'' Remove the top row
countIndex += 1
If countIndex = 1 Then
Continue For
End If
statementProgress.Value = ((countIndex / maxIndex) * 100)
'' Build the New commissionStatement object and add it to the allStatements list
If Not line = "" Then
Dim commissionStatement = New CommissionStatement
With commissionStatement
.Provider = line.Split(",")(0)
.Firm = line.Split(",")(1)
.Source = line.Split(",")(2)
.Media = line.Split(",")(3)
.Total = line.Split(",")(4)
End With
providerString &= commissionStatement.Provider & ","
allStatements.Add(commissionStatement)
End If
Next
First post on StackOverflow so sorry if its not very clear! The duplicate list needs to also be a list of CommissionStatements which contain the duplicates from the allStatements list based on Firm Provider and Total
Your best bet is to use a lambda expression. The function below should do what you ask.
Private Function GetDuplicateCommisionStatements(tempStatement As CommissionStatement) As List(Of CommissionStatement)
Return allStatements.FindAll(Function(x) x.Firm = tempStatement.Firm And x.Provider = tempStatement.Provider And x.Total = tempStatement.Total)
End Function
And use it like this..
duplicatelist = GetDuplicateCommisionStatements(testCommisionStatement)
using your own object names of course.
Incidentally, you could shorten the sub using the With statement like below
Private Function GetDuplicateCommisionStatements(tempStatement As CommissionStatement) As List(Of CommissionStatement)
With tempStatement
Return allStatements.FindAll(Function(x) x.Firm = .Firm And x.Provider = .Provider And x.Total = .Total)
End With
End Function

LINQ query is not working

I am trying to create a dictionary from a list. I am trying to filter the list such that it contains the id that I am adding as a key to the dictionary. So dictionary would be Key, List - with id as Key.
I have no idea why the following won't work - although the rawlist contains ids, filteredlist is empty. I am checking the value within the loop. Fresh set of eyes please?
resultList is dictionary(string, List)
For Each c As String In listIds
Dim filteredlist = rawList.Where(Function(x) x.id.Trim().Contains(c.Trim())).ToList()
resultList.Add(c,filteredlist)
Next
I need the filtered list. I have tried Contains, Equals and "="
i.e.
Dim filteredlist = rawList.Where(Function(x) x.id.Trim().Equals(c.Trim())).ToList()
and
Dim filteredlist = rawList.Where(Function(x) x.id.Trim() = (c.Trim())).ToList()
Please look into it thanks!
Looks like you have it backwards. Typically you have a list for looking for something first and not iterate through that list which IMHO takes longer.
Filter Linq Child Collection by another List/Array
Public Class POC
Public Property Id As Integer
Public Property Desc As String
Public Property Orders As List(Of Order)
End Class
Public Class Order
Public Property Id As Integer
Public Property Desc As String
End Class
Private Shared Function GetOrders(numberOfOrders As Integer) As List(Of Order)
Dim orders = New List(Of Order)()
For i As Integer = 1 To numberOfOrders
orders.Add(New Order() With { .Id = i, .Desc = "{i} Order" })
Next
Return orders
End Function
Private Shared Function GetPOCOsAndOrders() As List(Of POC)
Return New List(Of POC)() From { _
New POC() With { .Id = 1, .Desc = "John", .Orders = GetOrders(1) },
New POC() With { .Id = 2, .Desc = "Jane", .Orders = GetOrders(2) },
New POC() With { .Id = 3, .Desc = "Joey", .Orders = GetOrders(3) }
End Function
Private Shared Sub Main(args As String())
Dim orders = New List(Of Integer)() From { 2, 3 }
Dim items = GetPOCOsAndOrders()
Dim peopleAndOrdersWhereOrderNumberIsGreaterThanTwo = items.Where(Function(x) x.Orders.Any(Function(y) orders.Contains(y.Id)))
'I should only get the last two people out of three and their orders
peopleAndOrdersWhereOrderNumberIsGreaterThanTwo.ToList().ForEach(Function(x) Console.WriteLine("{x.Id} {x.Desc} {x.Orders.Count}"))
Console.ReadLine()
End Sub
There are many silght variations possible with the limited information you have given us, but a working example is:
Option Infer On
Option Strict On
Module Module1
Public Class datum
Property Id As String
Property Name As String
Public Sub New()
' empty constructor
End Sub
Public Sub New(Id As String, Name As String)
Me.Id = Id
Me.Name = Name
End Sub
End Class
Sub Main()
' sample data...
Dim rawData = New List(Of datum)
rawData.Add(New datum("cat", "Lulu"))
rawData.Add(New datum("cat", "Uschi"))
rawData.Add(New datum("snake", "Sid"))
rawData.Add(New datum("fox", "Reynard"))
rawData.Add(New datum("mouse", "Jerry"))
' what to look for:
Dim listIds As New List(Of String) From {"CAT", "mouse", "ELK"}
' somewhere to store the results:
Dim dict As New Dictionary(Of String, List(Of String))
' filter by what to look for:
For Each c In listIds
Dim foundItems = rawData.Where(Function(x) String.Compare(x.Id, c, StringComparison.OrdinalIgnoreCase) = 0)
' only add items to the dictionary if the Id was found:
If foundItems.Count() > 0 Then
' use the case of the id from the raw data:
dict.Add(foundItems(0).Id, foundItems.Select(Function(f) f.Name).ToList())
End If
' alternative which includes dictionary entries where the Id was not found:
'dict.Add(If(foundItems.Count = 0, c, foundItems(0).Id), foundItems.Select(Function(f) f.Name).ToList())
Next
' show the dictionary contents:
For Each kvp In dict
Console.WriteLine(kvp.Key & ": " & String.Join(", ", kvp.Value))
Next
Console.ReadLine()
End Sub
End Module
Output:
cat: Lulu, Uschi
mouse: Jerry
I have included commented-out code for some variations you may want.

LINQ query to group data between two list collections, populating a nested object (VB.Net)

I have these objects:
Public Class MakeInfo
Public Property Name As String
Public Property Description As String
Public Property Stock As StockInfo
End Class
Public Class ModelInfo
Public Property Make As String
Public Property Name As String
Public Property Description As String
Public Property Stock As StockInfo
End Class
Public Class StockInfo
Public Property Count As Integer
Public Property MinPrice As Double
End Class
Using LINQ I need to take a List(Of MakeInfo) and a List(Of ModelInfo) and aggregate the StockInfo from ModelInfo into the List(Of MakeInfo).
So for each MakeInfo I will have a total count of all stock where MakeInfo.Name = ModelInfo.Make, and also a minimum price.
I think it's going to be something like this, but I'm having trouble accessing the nested object and not sure if it's going to be possible with a single query. I've tried a few variations, but here's where I'm up to:
newList = From ma In makeData _
Group Join mo In modelData _
On ma.Name Equals mo.Make _
Into Group _
Select New MakeInfo With {.Name = m.Name, _
.Description = m.Description, _
.Stock = ?}
If you are actually trying to modify the original MakeInfo objects, you should stop the query with the group. newList will have each make paired with the group of models of that make. You can iterate through newList, and for each make, aggregate the models into the StockInfo for the make.
Dim newList = From ma In makeData _
Group Join mo In modelData _
On ma.Name Equals mo.Make _
Into Group
For Each g In newList
g.ma.Stock = g.Group.Aggregate(
New StockInfo() With {.MinPrice = Double.NaN},
Function(si, model)
Return New StockInfo With {
.Count = si.Count + 1,
.MinPrice = If(Double.IsNaN(si.MinPrice) OrElse
model.Stock.MinPrice < si.MinPrice,
model.Stock.MinPrice,
si.MinPrice)}
End Function)
Next
If you actually want new instances, just change the loop to create a new item instead of modifying the existing make.
Dim results As New List(Of MakeInfo)()
For Each g In newList
Dim newMake As New MakeInfo()
newMake.Name = g.ma.Name
newMake.Description = g.ma.Description
newMake.Stock = g.Group.Aggregate( same as before )
results.Add(newMake)
Next
I eventually found the right way to do this and it doesn't require loop iteration:
Dim new_makes = From ma In makes
Join mo In models On mo.Make Equals ma.Name
Group By ma.Name, ma.Description
Into TotalCount = Sum(mo.Stock.Count), LowestPrice = Min(mo.Stock.MinPrice)
Select New MakeInfo With {.Name = Name, _
.Description = Description, _
.Stock = New StockInfo With {.Count = TotalCount, .MinPrice = LowestPrice}}
I was certain that it would be possible.