I have tried my hand using for loop with Dictionary but couldn't really achieve what I want to.
I have a certain variable SomeVariable and on the value of this variable I want my foreach to work. SomeVariable can be 1,2,3 or 4
So lets say SomeVariable is 1 I want to retrieve the last item.value from among the first 3 indexes(0,1,2) inside the SomeCollection.
And if SomeVariable is 2 I want to retrieve the last item.value from among the next 3 indexes(3,4,5) inside the SomeCollection.
And so on...
For Each item As KeyValuePair(Of String, Integer) In SomeCollection
If SomeVariable = 1 Then
//....
ElseIf SwitchCount = 2 Then
//....
End If
Next
A dictionary has no defined order, so any order you perceive is transient. From MSDN:
The order of the keys in the .KeyCollection is unspecified, but it is the same order as the associated values in the .ValueCollection returned by the Values property.
Trying to use the Keys collection to determine the order shows how it is transient:
Dim myDict As New Dictionary(Of Integer, String)
For n As Int32 = 0 To 8
myDict.Add(n, "foo")
Next
For n As Int32 = 0 To myDict.Keys.Count - 1
Console.WriteLine(myDict.Keys(n).ToString)
Next
the output prints 0 - 8, in order, as you might expect. then:
myDict.Remove(5)
myDict.Add(9, "bar")
For n As Int32 = 0 To myDict.Keys.Count - 1
Console.WriteLine(myDict.Keys(n).ToString)
Next
The output is: 0, 1, 2, 3, 4, 9 (!), 6, 7, 8
As you can see, it reuses old slots. Any code depending on things to be in a certain location will eventually break. The more you add/remove, the more unordered it gets. If you need an order to the Dictionary use SortedDictionary instead.
You can't access the dictionary by index but you can access the keys collection by index. You don't need a loop for this at all.
So something like this.
If SomeVariable = 1 Then
Return SomeCollection(SomeCollection.Keys(2))
ElseIf SomeVariable = 2 Then
...
End If
If it is truly structured you could do this:
Return SomeCollection(SomeCollection.Keys((SomeVariable * 3) - 1))
You probably need some error checking and ensuring that the length of the dictionary is correct but this should put you on the right track.
You can always use a generic SortedDictionary, I only use C# so here's my example:
SortedDictionary<int,string> dict = new SortedDictionary<int, string>();
foreach( KeyValuePair<int,string> kvp in dict) { ... }
Related
This is my first class with a default member and I'm still getting a feel for it. I have a let / get property data:
class ArrayClass
private D as variant
Property Get data() As Variant
data = D
End Property
Property Let data(arg1 As Variant)
D = arg1
End Property
I have added the following in my .cls file (using notepad) to make data the default parameter which looks like this:
Property Get data() As Variant
Attribute data.VB_UserMemId = 0
data = D
End Property
I'm just testing this to see how well this works using an array for "D":
Dim testArray As ArrayClass
Dim passArray(5, 1) As Variant
passArray(0, 0) = 1
passArray(1, 0) = 2
passArray(2, 0) = 3
passArray(3, 0) = 4
passArray(4, 0) = 5
passArray(5, 0) = 6
passArray(0, 1) = 7
passArray(1, 1) = 8
passArray(2, 1) = 9
passArray(3, 1) = 10
passArray(4, 1) = 11
passArray(5, 1) = 12
Set testArray = new ArrayClass
testArray = passArray
testArray(1, 1) = 5
Debug.Print testArray(2, 1)
It "mostly" works. The "testArray = passArray" calls the "Let Data" property, and assigns the passArray to parameter "D" inside the object.
Also, "Debug.Print testArray(2,1)" also works. That calls the "Get Data" property, and it returns the index values of 2,1 of the "D" parameter in the object.
My problem is the "testArray(1,1) = 5" instruction. The intent was to assign the 1,1 index to parameter D to the number 5. But what happens is it calls the "Get Data" property, instead of the "Let Data" property.
To be clear, I wasn't really expecting it to work because I'm not yet sure how to do it. But I'm at a loss on why its calling the "get property" instead of the "let property" being that the instruction is on the left side of the equal sign.
Anyone have any ideas on how to make it work? Thanks.
What you're experiencing is standard VBA behaviour.
The line testArray(1,1) = 5 first makes a copy of the of the D array (indeed calling Get) and then value 5 is assigned to the 1,1 index of the new/copy array.
You can only call Let to pass a single value as that's what you definition expects:
Property Let data(arg1 As Variant)
D = arg1
End Property
You can't call it with testArray(1,1) because that passes 2 values. Obviously, you made it clear that you intend to update just one member of the internal D array but that's simply not possible via that Let property.
What you could do is to define a new property that expects 3 parameters:
Property Let item(ByVal index1 As Long, ByVal index2 As Long, ByVal newValue As Variant)
D(index1, index2) = newValue
End Property
and call it with testArray.item(1, 1) = 5 or maybe define this new property as the default.
Consider declaring D as an array - as it currently stands, you can pass anything e.g. testArray = "test" which I don't think is what you want. So, maybe declare it as Private D() As Variant and then update the data properties to receive and return an array of Variant type:
Property Get data() As Variant()
data = D
End Property
Property Let data(arg1() As Variant)
D = arg1
End Property
I was able to figure out how to do this the way I want to do it. The secret is using the parameter arrays in the input fields of my Get / Let properties.
Property Let data(ParamArray sizes() As Variant, data As Variant)
end property
Property Get data(ParamArray sizes() As Variant) As Variant
Attribute data.VB_UserMemID = 0 'This line makes the data property the default property.
'It is Only visible/editable in .cls file opened in text editer
end property
So it turns out that the let property has two different types of input arguments a "arglist" and a value. The last argument in the list is your value (and is the number to the right of the equal sign). But since VBA (and all other languages to my knowledge) requires if you use an optional input, then all inputs to the right of it must also be optional, including the value input.
But if you use ParamArray, then VBA knows which parameters are the inputs, and which one is the value, thus enabling you to have optional inputs on your let property. All my inputs for these properties are optional. Then I just use if statements to map what I'm supposed to output based on the number of input values.
So now I can access my array parameter inside my class using the exact same syntax as an array outside my class:
passArray(0, 0) = 1
passArray(1, 0) = 2
passArray(2, 0) = 3
passArray(0, 1) = 4
passArray(1, 1) = 5
passArray(2, 1) = 6
testArray = passArray
Debug.Print testArray(2, 1) 'returns 6
testArray(2, 1) = 7.5
Debug.Print testArray(2, 1) 'returns 7.5
Here's my string:
gamename\jbnightfire\hostname\testserver\hostport\26015
I'm trying to split this string into a dictionary(of string, string). Please note that this string is not static and could contain many more keys and values.
The dictionary would contain the following:
key = gamename, value = jbnightfire
key = hostname, value = testserver
key = hostport, value = 26015
I've tried at least 5 different methods and can't seem to get any that work. It should be extremely simple, but for the life of me I cannot get it to work. Any help would be greatly appreciated. Thanks.
Split the string and loop through it with a step of 2 to advance 2 items at a time. That means the key would be at index i, and i + 1 would be the value.
Dim input = "gamename\jbnightfire\hostname\testserver\hostport\26015"
Dim split = input.Split(New String() { "\" },
StringSplitOptions.RemoveEmptyEntries)
Dim dict As New Dictionary(Of String, String)
For i As Integer = 0 To split.Length - 1 Step 2
dict.Add(split(i), split(i + 1))
Next
Note that the above approach uses the Add method, which will throw an exception if a duplicate key exists. Use the above approach if you don't expect duplicates.
Alternately, you may assign the value directly to key, in which case it will overwrite any existing value if the key exists:
dict(split(i)) = split(i + 1)
Another option is to use ContainsKey and skip it if it exists, or perform some additional logic to determine whether to skip it or keep it. For example, this will skip it if it already exists:
For i As Integer = 0 To split.Length - 1 Step 2
If dict.ContainsKey(split(i)) Then Continue For
dict.Add(split(i), split(i + 1))
Next
i have this code, and i want to access some variables.
Dim k1 as String = "Something"
Dim k2 as String = "Something"
... to k230
------------------Then i have this:
Dim rnd = New Random()
Dim nextValue = rnd.Next(230)
For i = 0 To 230
If nextValue = i Then
MsgBox('k+i') <--BUT READ THIS AS A VARIABLE.
End If
i = i + 1
Next
i readed some similar questions, but them doesn't apply to this case.
Consider using arrays here:
http://msdn.microsoft.com/en-us/library/vstudio/wak0wfyt.aspx
An array is a set of values that are logically related to each other,
such as the number of students in each grade in a grammar school.
By using an array, you can refer to these related values by the same
name, and use a number that’s called an index or subscript to tell
them apart. The individual values are called the elements of the
array. They’re contiguous from index 0 through the highest index
value.
Try using a Dictionary:
Dim k As New Dictionary(Of Integer, String)()
k.Add(1, "Something")
k.Add(2, "Something")
'... to 230
Messagebox.Show(k(i))
I have this project that let user inputs 5 different numbers from 1 to 50. But I want to validate it before saving to DB that i will be 5 unique numbers. What's the best and fastest way to do this?
You can use HashSet(Of T) to check this:
Dim numbers As IEnumerable(Of Integer) = GetInputFromUser()
Dim hash As HashSet(Of Integer) = new HashSet(Of Integer)(numbers)
Dim unique As Boolean = hash.Count = numbers.Count()
This will be much more efficient than options requiring a sort + iteration.
Check this code
Private Function HasDuplicates(ByVal arr As Array) As Boolean
For i As Integer = 0 To arr.Length - 1
If Not arr(i) Is Nothing Then
Dim l As Integer = Array.LastIndexOf(arr, arr(i))
If l <> i Then Return True
End If
Next
Return False
End Function
Reed Copsey's suggestion to use hash sets was a good one (I hadn't worked with the HashSet class before).
But I then discovered that the IEnumerable class offers an extension method called Distinct that copies the unique values from a source IEnumerable to a target IEnumerable.
Borrowing the first line of Reed's sample code, here's the new sample VB.NET code:
Dim numbers As IEnumerable(Of Integer) = GetInputFromUser()
Dim isUnique As Boolean = (numbers.Distinct.Count = numbers.Count)
The variable isUnique is True if the numbers IEnumerable contains no duplicate values, or False if it contains one or more duplicate values.
Put in an array, sort it and check if elements 1,2 2,3 3,4 and 4,5 are different (in a loop).
Pseudocode:
integer numbers[50]
zeroarray(numbers, 50)
integer count = 0;
while (count < 5)
{
integer value = getinput()
if (value >= 1 and value <= 50)
if (numbers[value] = 0)
{
count = count + 1
numbers[value] = 1
}
else
reject duplicate
else
reject invalid
}
You can try this very simple method:
Filtering Arrays using LINQ
To simplifiy lets say the user inputs 5 different numbers from 0 to 49.
You can create a Boolean Array IsUsed(49) with 50 elements.
Then when the user input the value iInputNum=30 you can set IsUsed(30)=TRUE.
Next time, when the user input the second value iInputNum=7, you can set IsUsed(7)=TRUE
In this way you can check in a very fast way if the number was already inserted.
if IsUsed(iInputNum) then
'Skip the Input (put the right code here)
else
'Accept the number
IsUsed(iInputNum)=TRUE
'save iInputNum in the database
end if
Do not forget to clear the array after inserting all 5 numbers.
Remenber to put the right index in order to handle the number 1-50 (e not 0-49)
Here's an alternate solution, not sure how it compares, efficiency wise, to the other solutions, but it seems to work (uses LINQ).
Dim numbers As List<int> = getListOfNumbers()
Dim allUnique As Boolean = numbers.Distinct().Count() = numbers.Count()
Very late to the party, but what about something like this (C#, sorry)?
byte[] hits = new byte[51];
byte[] entries = new byte[] { 1, 12, 12, 32, 26, 49 };
foreach (var entry in entries)
{
hits[entry]++;
}
The elements in the hits array are automatically initialized to 0. When the foreach loop is complete, hits will contain the occurrence count for every number in entries. If any is greater than 1, you have dupes.
How can I determine if I'm in the final loop of a For Each statement in VB.NET?
The generally, collections on which you can perform For Each on implement the IEnumerator interface. This interface has only two methods, MoveNext and Reset and one property, Current.
Basically, when you use a For Each on a collection, it calls the MoveNext function and reads the value returned. If the value returned is True, it means there is a valid element in the collection and element is returned via the Current property. If there are no more elements in the collection, the MoveNext function returns False and the iteration is exited.
From the above explanation, it is clear that the For Each does not track the current position in the collection and so the answer to your question is a short No.
If, however, you still desire to know if you're on the last element in your collection, you can try the following code. It checks (using LINQ) if the current item is the last item.
For Each item in Collection
If item Is Collection.Last Then
'do something with your last item'
End If
Next
It is important to know that calling Last() on a collection will enumerate the entire collection. It is therefore not recommended to call Last() on the following types of collections:
Streaming collections
Computationally expensive collections
Collections with high tendency for mutation
For such collections, it is better to get an enumerator for the collection (via the GetEnumerator() function) so you can keep track of the items yourself. Below is a sample implementation via an extension method that yields the index of the item, as well as whether the current item is the first or last item in the collection.
<Extension()>
Public Iterator Function EnumerateEx(Of T)(collection As IEnumerable(Of T))
As IEnumerable(Of (value As T, index As Integer, isFirst As Boolean, isLast As Boolean))
Using e = collection.GetEnumerator()
Dim index = -1
Dim toYield As T
If e.MoveNext() Then
index += 1
toYield = e.Current
End If
While e.MoveNext()
Yield (toYield, index, index = 0, False)
index += 1
toYield = e.Current
End While
Yield (toYield, index, index = 0, True)
End Using
End Function
Here is a sample usage:
Sub Main()
Console.WriteLine("Index Value IsFirst IsLast")
Console.WriteLine("----- ----- ------- ------")
Dim fibonacci = {0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89}
For Each i In fibonacci.EnumerateEx()
Console.WriteLine(String.Join(" ", $"{i.index,5}",
$"{i.value,5}",
$"{i.isFirst,-7}",
$"{i.isLast,-6}"))
Next
Console.ReadLine()
End Sub
Output
Index Value IsFirst IsLast
----- ----- ------- ------
0 0 True False
1 1 False False
2 1 False False
3 2 False False
4 3 False False
5 5 False False
6 8 False False
7 13 False False
8 21 False False
9 34 False False
10 55 False False
11 89 False True
It probably would be easier to just use a For loop instead of ForEach. But, similarly, you could keep a counter inside your ForEach loop and see if its equal to yourCollection.Count - 1, then you are in the last iteration.
With a foreach, you cannot know this until it is too late (ie. you're out of the loop).
Note, I'm assuming you're using something where you only have an IEnumerable interface. If you have a list, array, etc. then follow the other answers here which uses .Count or similar to find out how many items there are, and thus you can keep track of where you are in the collection.
However, with just IEnumerable/IEnumerator, there is no way to know for sure wether or not there are more, if you use foreach.
If you need to know this, use IEnumerable yourself, which is what foreach does.
The below solution is for C# but should translate easily to VB.NET:
List<Int32> nums = new List<Int32>();
nums.Add(1);
nums.Add(2);
nums.Add(3);
IEnumerator<Int32> enumerator = nums.GetEnumerator();
if (enumerator.MoveNext())
{
// at least one value available
while (true)
{
// make a copy of it
Int32 current = enumerator.Current;
// determine if it was the last value
// if not, enumerator.Current is now the next value
if (enumerator.MoveNext())
{
Console.Out.WriteLine("not last: " + current);
}
else
{
Console.Out.WriteLine("last: " + current);
break;
}
}
}
enumerator.Dispose();
This will print:
not last: 1
not last: 2
last: 3
The trick is to take a copy of the current value, then ask the enumerator to attempt to move on to the next one. If that fails, the copy you made was indeed the last value, otherwise there is more.
here is a simple thing i dont know if it is politically correct but it works
for each item in listOfThings
if(item = listOfThings.last)then
'you found the last loop
end if
next
Short answer: You can't
Long answer: There's nothing in the semantics of a For Each statement that allows you to identify whether you're running the first, last or any particular iteration.
For Each is built in the IEnumerable and IEnumerable<> interfaces, and the behavior is dependent on the implementation you're calling. It's valid, though confusing, for the collection you're iterating to return elements in a different order every time. Fortunately, List<> doesn't do this.
If you really need to know that a particular iteration is the last, you could identify the last element (in advance) and then take different action when you encounter that element.
Easier would be to detect the first iteration (say, through a boolean) and then do something different.
An example (C#, but the VB will be similar):
StringBuilder result = new StringBuilder();
bool firstTime = true;
foreach(string s in names)
{
if (!firstTime)
{
result.Append(", ");
}
result.Append(s);
firstTime = false;
}
Check if the element is the last element of the container.
Why do you want to do that?
You could just place instructions after the loop. (That you execute on the last element)
It would be easier to use a For loop instead of a ForEach, however you could do something like
If item.Equals(itemCollection(itemCollection.Count)) Then
...
End If
inside of your ForEach loop... Assuming the object has properly overridden the Equals method.
This is probably much more resource intensive than just using a For loop or keeping track of the current index in a separate variable.
I'm not sure if this is the correct syntax, it's been a long time since I've used VB.
Using a standard "For Each" loop you can't. The intent of a "for Each" loop is to allow you to concentrate on the data in lieu of the underlying collection.
I keep coming back to this post, so I decided to sit down and think about this issue and I came up with this:
For Each obj In myCollection
Console.WriteLine("current object: {0}", obj.ToString)
If Object.ReferenceEquals(obj, myCollection.Last()) Then
Console.WriteLine("current object is the last object")
End If
Next
You could even reduce the If...Then...End If statement to a single line if you wanted to. I'm not sure if calling .Last() on every iteration has a large impact on performance, but you can always assign it to a variable outside of the loop if that's what keeps you awake at night.
this code sample might help
For Each item in Collection
If ReferenceEquals(Collection.Item(Collection.Count - 1), item) Then
'do something with your last item'
End If
Next
If you are tied to IEnumerable. Do you have to be inside the foreach loop? If not you could declare a variable just before the foreach loop. Set it during the loop. Then, use it after the loop (if its not null)
Well there are few workaround
Suppose you are working with CheckBoxList
Then this code sample might help :
Dim count As Integer = 0
Dim totalListCount As Integer = CheckBoxList.Items.Count()
For Each li In CheckBoxList.Items
count += 1
// This is the Last Item
If count = totalListCount THEN
// DO Somthing.....
END IF
NEXT
For Each xObj In xColl
If xObj = xColl(xColl.Count) Then ...
Next