How to customize sort order of strings - arrays

I am completely new to using string arrays. Consider this example:
I have the following values in a text box:
bufallo#2000
lice#20
cell#1
rat#150
cow#10000
When I sort them, they are sorted alphabetically, as in the list above. However, I need them to sort in descending order using the integer value that follows the # symbol. So, for instance, I want the above list to be sorted like this:
cow#10000
bufallo#2000
rat#150
lice#20
cell#1
I have no idea on how to arrange them in descending order like that.

While doing all your logic in a single LINQ expression can prove how clever you are :) sometimes the code is just easier to read and follow if you do it in a more verbose fashion. So, if you don't want to use LINQ, you could create your own IComparer class which contains your custom sorting algorithm:
Public Class MyComparer
Implements IComparer(Of String)
Public Function Compare(ByVal x As String, ByVal y As String) As Integer Implements IComparer(Of String).Compare
Dim xParts() As String = x.Split("#"c)
Dim yParts() As String = y.Split("#"c)
'Get the integer value (after the # symbol) of each parameter
Dim xValue As Integer = 0
Dim yValue As Integer = 0
If xParts.Length = 2 Then
Integer.TryParse(xParts(1), xValue)
End If
If yParts.Length = 2 Then
Integer.TryParse(yParts(1), yValue)
End If
'Compare y-to-x instead of x-to-y because we want descending order
Return yValue.CompareTo(xValue)
End Function
End Class
In this example, IComparer is a standard .NET framework interface, which you are implementing in your MyComparer class. The Compare method (as defined by IComparer) simply takes two parameters and compares them. If x is less than y (i.e. x comes before y in the sort-order), the method will return a negative number (e.g. -1). If x is greater than y, it will return a positive number (e.g. 1). And if x and y are equal, then the method will return 0.
In this case, however, since all we want to do is use the standard integer sorting, we can just call Integer.CompareTo which compares two integers and returns the negative, positive, or zero, as appropriate.
Then, when you call the Array.Sort method, you can give it one of your custom IComparer objects so that it uses your custom sorting algorithm instead of the default behavior:
Dim arrayToSort() As String = New String() {"bufallo#2000", "lice#20", "cell#1", "rat#150", "cow#10000"}
Array.Sort(arrayToSort, New MyComparer())
The Sort method will use the IComparer object that you gave it to perform the sorting. Each time it needs to compare two items in the array to see which should come first, it will call MyComparer.Compare and use the value returned by that method to determine the proper sorting.
You can re-use the same MyComparer class everywhere in your code that you need to sort items using that same algorithm, which is another benefit over the LINQ approach. Implementing your own IComparer classes allows you to make all sorts of very powerful customize-able sort orders.

You want to order by the numeric part of the string? No need for Regex.
You can use String.Split and Enumerable.OrderByDescending:
Dim number As Int32 = Int32.MinValue
Dim orderedLines = From line In TextBox1.Lines
Let parts = line.Split("#"c)
Let numericPart = parts.Last()
Let success = Int32.TryParse(numericPart, number)
Select LineInfo = New With {line, number}
Order By LineInfo.number Descending
Select LineInfo.line
' if you want to reassign it to the TextBox:
TextBox1.Lines = orderedLines.ToArray()

Related

Searching a multi-dimensional array with VB.Net to count occurrences of a string

I have Dictionary defined as:
Private cdEmailsUploaded As New ConcurrentDictionary(Of String, Boolean)
The string contains the fullpath to an email file (MSG) and I want to create a function that returns a count of those that contain a given string in their path e.g "ProjectX".
I have assumed, possibly incorrectly, that it would be best to convert the Dictionary into an Array and then search it for the string and count the results. If there is a better way then please let me know.
The problem is that I can't get my head around the syntax to get the embedded function to search the string part of the array and not the Boolean.
What I have at the moment is:
Function QtyUploadedToLocation(sFind As String) As Integer
Dim emailsArr = cdEmailsUploaded.ToArray
Dim result As String() = Array.FindAll(emailsArr, Function(s) s.Key.StartsWith(sFind))
Return result.Length
End Function
The error I'm getting is:
Value of type 'KeyValuePair(Of String, Boolean)()' cannot be converted
to 'String()' because 'KeyValuePair(Of String, Boolean)' is not
derived from 'String'.
The signature of Array.FindAll is:
Public Shared Function FindAll(Of T) (array As T(), match As Predicate(Of T)) As T()
That means that for a dictionary type, it's working in KeyValuePair(Of TKey, TValue) throughout, and in particular, the result is an array of key/value pairs, not an array of string.
If you just want the count, you could a) fix the intermediate type, b) use type inference to avoid having to say what the type is (if you have Option Infer On), or c) just return the count directly, i.e.
Return Array.FindAll(emailsArr, Function(kvp) kvp.Key.StartsWith(sFind)).Length
If you want the array of strings for further processing, you can use the Linq Select function to get that sequence, i.e.
Dim matchingKeys = _
Array.FindAll(emailsArr, Function(kvp) kvp.Key.StartsWith(sFind)) _
.Select(Function(kvp) kvp.Key)
This will be an IEnumerable(Of String), you could use ToArray if you need it as an array or just leave it as-is if you want to do some other processing on it.

Check if any element in an array matches my condition

I've got an array with 2 values in it. I want to use an If statement to see whether any of the values in the array equals the SelectedValue of my DropDown.
So far, everything I've tried only gives negative results (only the Else condition is reached):
Dim twoValues() As String = {"one", "two"}
If Me.ddlDropdown.SelectedValue.ToLower.Equals(twoValues.Any) Then
'do something
Else
'do something else
End If
You can invert the comparison and use LINQ's Any() method to compare elements of the array to the provided value, until a matching value is found.
Since the comparison needs to be case-insensitive, you can use the overload of String.Equals() that accepts a StringComparison argument.
Better avoid ToLower() without a CultureInfo selector when a case-insensitive comparison is needed.
Also, the InvariantCulture may apply or not, depending on the languages involved in the comparison.
If the current language is the local language and cannot be directly associated with the InvariantCulture, use StringComparison.CurrentCultureIgnoreCase instead. Or build a custom comparer.
Dim twoValues As String() = {"one", "two"}
Dim value = Me.ddlDropdown.SelectedValue.ToString()
If twoValues.Any(Function(s) s.Equals(value, StringComparison.InvariantCultureIgnoreCase)) Then
'do something
Else
'do something else
End If

vb.net - List (Of String) Sorting, Numbers Ascending, Alphabet Descending

I am trying to sort a list of string into a very particular order. I have a list of items which come in the format of "X123XXX" where X can be a letter, and the 123 can be any number.
Dim AList As New List(Of String)
AList.Add("B200")
AList.Add("A100X")
AList.Add("A100XY")
AList.Add("A100XYZ")
AList.Sort()
'Actual Return in order (AList(1).tostring etc.)
'A100X
'A100XY
'A100XYZ
'B200
'Desired Return
'A100XYZ
'A100XY
'A100X
'B200
The problem I have is that Not only do I need to sort the Numbers Ascending and Letters Descending, but I also need to sort the first letter Ascending.
I was hoping that, as the parts are always in the same format (letter + 3 numbers + Optional up to 3 letters) - that I could just select the first 4 and sort that ASC and then DESC on the last 3. But i have no idea how to do that in vb.net. I've had a look around, and can only find the IComparable stuff, which doesn't seem like it would be very useful.
You can get what you want by relying on some LINQ methods (OrderBy, ThenBy, etc.); they allow you to perform all the modifications you need.
Code delivering what you want:
AList = AList.OrderBy(Function(x) x.Substring(0, 1)).ThenBy(Function(x) Convert.ToInt32(x.Substring(1, 3))).ThenByDescending(Function(x) x.Substring(4)).ToList()
UPDATE
The code above orders the last part (i.e., letters from the 5th position until the end) in alphabetically descending order; but apparently the OP wants this last sorting to be performed on account of the length of the strings (i.e., longest one first and shortest one last). The following code delivers such a behaviour:
AList = AList.OrderBy(Function(x) x.Substring(0, 1)).ThenBy(Function(x) Convert.ToInt32(x.Substring(1, 3))).ThenByDescending(Function(x) x.Substring(4).Length).ToList()
An alternate approach to the anonymous function, create a Class that implements IComparer(Of String):
Private Class CodeComparer
Implements System.Collections.Generic.IComparer(Of String)
Function Compare(ByVal a As String, ByVal b As String) As Integer _
Implements IComparer(Of String).Compare
Dim result As Integer = a.Substring(0, 4).CompareTo(b.Substring(0, 4))
If result = 0 Then
result = a.Substring(4).CompareTo(b.Substring(4)) * -1
End If
Return result
End Function
End Class
This sorts by the first 4 characters in ascending order, then by the remaining characters in decending order (hence the * -1).
You can then sort the list using:
items.Sort(New CodeComparer())
Edit: You can also use the same algorithm with an anonymous function passed to the Sort method.
items.Sort(Function(a, b)
Dim result As Integer = a.Substring(0, 4).CompareTo(b.Substring(0, 4))
If result = 0 Then result = a.Substring(4).CompareTo(b.Substring(4)) * -1
Return tmp
End Function)

How to convert Array of values in VBA to array of other value types, based on user input?

So I have an class returns array of singles. Problem is that new requrement came up where I need to be able provide these values as a single string, which can be done with Join except Join requres that array is made up of strings not singles.
I guess I could write a new method in my class that provides same array but with values as strings. However I was wondering if I could just convert existing method to accept something like Optional ValType as vbVarType as a parameter and accordingly change the type of values in ouputed array.
Is this doable in a relatively DRY way?? I'd love to avoid code that looks like:
Select Case ValType
Case #something1
#omitted code
Case #something2
#omitted codev
Case #something3
#omitted code
........
UPDATE: I guess what I am looking for is a formula like Cstr() except that I'd like it to work on an Array and have it expect parameeter describing value to convert to.
Can you simply have an array of type Variant rather than Single? The following code seems to work ok:
Dim arrInt() As Variant
ReDim arrInt(0 To 3)
arrInt(0) = 0
arrInt(1) = 1
arrInt(2) = 2
arrInt(3) = 3
Debug.Print Join(arrInt, "#")

VB storing text from txt file to 2D array using Comma Delimiter

I've searched for an answer, but struggling to adapt my findings into my code/text file.
I have this text file;
20,Ben
10,Dave
7,Bob
Scores and names.
I want pull the data from the text file into a 2D array, for example:
array(0, 0)
array(1, 0)
array(0, 1)
array(1, 1)
array(0, 2)
array(1, 2)
which would translate to;
array(20)
array(Ben)
array(10)
array(Dave)
array(7)
array(Bob)
Thanks in advance
As mentioned in my comment,i would use a class for this.
For example Player with two properties: Name As String and Score As Int32. Then you can create a List(Of Player) instead. That's much more readable, reusable and maintainable and also less error-prone than fiddling around with indices.
Public Class Player
Public Property Name As String
Public Property Score As Int32
End Class
Here is a readable LINQ query that initializes a List(Of Player) from lines in a text-file:
Dim allPlayers = From line In System.IO.File.ReadLines("path")
Let Columns = line.Split(","c)
Where Columns.Length = 2
Let Score = Int32.Parse(Columns(0).Trim())
Let Name = Columns(1).Trim()
Select New Player With {.Score = Score, .Name = Name}
Dim playerList As List(Of Player) = allPlayers.ToList()
If you need an array use ToArray instead of ToList.
You can access alist like an array via indexer(list(0)) or via LINQ methods:
Dim firstPlayer As Player = playerList.FirstOrDefault() ' is Nothing if there are no players '
Console.WriteLine("Name of the player: " & firstPlayer.Name)
Console.WriteLine("His score is: " & firstPlayer.Score)
or in a loop:
For Each player In playerList
Console.WriteLine("Player {0} has scored {} points.", player.Name, player.Score)
Next
By the way, LINQ is useful to keep your code readable, under the hood it also uses loops. So you could simply use OrderByDescendending to order your players by score and output the top 3 with the highest score:
Dim best3Players = From player In playerList
Order By Player.Score Descending
Take 3
Select String.Format("{0}({1})", player.Name, player.Score)
Dim output = String.Join(", ", best3Players)
Windows.Forms.MessageBox.Show(output) ' since you have mentioned messagebox '
If you know the file is not going to be terribly big, the simplest approach would be to simply use the File.ReadAllLines method, which returns a single-dimension array of strings containing one item per line in the file. You could then loop through each line and use the String.Split method to split apart the two values from the line, for instance:
Dim lines() As String = File.ReadAllLines("leaderboard.csv")
Dim values(lines.Length - 1, 1) As String
For i As Integer = 0 to lines.Length - 1
Dim parts() As String = lines(i).Split(","c)
values(i, 0) = parts(0)
values(i, 1) = parts(1)
Next
However, it would be better if you created a class to store all of the data from each row, like this:
Public Class LeaderboardEntry
Public Property PlayerName As String
Public Property Score As Integer
End Class
Then, you could load the values into a list of those objects, like this:
Dim entries As New List(Of LeaderboardEntries)()
For Each i As String In File.ReadAllLines("leaderboard.csv")
Dim parts() As String = lines(i).Split(","c)
Dim entry As New LeaderboardEntry()
entry.Score = parts(0)
entry.PlayerName = parts(1)
entries.Add(entry)
Next
Then, instead of saying values(0, 0) to get the score of the first entry, you would say entries(0).Score, which is much more readable, especially if you add more fields to each line in the file. For more information on the value of using a class for something like this, check out my answer to this question.
Bear in mind however, that the above code assumes that each line is properly formatted. If the file contained any lines that did not include a comma, it would fail. If you need to handle that situation, you'd need to add some extra checking. Also, in the above scenario, the player name cannot contain any commas. The best way to handle that would be to use proper CSV formatting, like this:
30,"Doe, John"
And then your quotes would need to be escaped, etc., in which case, it would be worth using the TextFieldParser class to read the CSV file rather than reinventing the wheel. Another option would be to simply disallow the users from entering a comma in their name. If you take that approach, however, it would be better to pick a less common character for your delimiter, such as a pipe symbol (|).
Instead of using a CSV file, I would recommend using an XML file. Then you could easily store the data, even if it grows in complexity, and read and write to it using either the XmlSerializer, XmlDocument, or XDocument.
If all you need to do is store simple values like this, though, an even better option may to be to use the My.Settings feature.

Resources