Find and string in a string and change it - excel

I have a 2-dimensional array of values looking like that:
In a different table, I have long strings with VALUE_1, VALUE_2 that can be found anywhere. It looks like in the table below:
Now, I want to write a program that translates the existing VALUE_1, VALUE_2 etc. in the long strings by adding the respective element in the 2nd dimension of the array (/BB, /CCC etc.) and if necessary duplicating and separating the values with a comma and a blank space. So VALUE_1 for example is turned into VALUE_1/BB, VALUE_1/A for each finding in the string. The result is supposed to look exactly like in the table below.
That's challenging. I my first approach I tried to locate the VALUE_1, VALUE_2 in the strings by using InStr() but I don't think that this will help me since only the first hit is taken into consideration. I need every occurrence.
For i = 1 To Worksheets("table2").Range("H1").End(xlDown).Row
For j = LBound(arr2) To UBound(arr2)
If InStr(Worksheets("table2").Range("H" & i), arr2(j, 0)) > 0 Then
Worksheets("table2").Range("H" & i).Font.Bold = True
End If
Next j
Next i

Use your 2D table to build a scripting dictionary so that value1 is associated with the concatenation of all column values in column 2 that have value 1 in the first column.
In the (untested) code below the array (ipArray)is that derived from the 2D range.
Public Function GetReplacements(ByVal ipArray As Variant) As Scripting.dictionary
Dim myD As Scripting.dictionary
Set myD = New Scripting.dictionary
Dim myIndex As Long
For myIndex = LBound(ipArray) To UBound(ipArray)
Dim myKey As String
myKey = ipArray(myIndex, 1)
Dim myItem As String
myItem = ipArray(myIndex, 2)
If myD.exists(myKey) Then
myD.Item(myKey) = myD.Item(myKey) & ", " & myKey & myItem
Else
myD.Add myKey, myKey & myItem
End If
Next
Set GetReplacements = myD
End Function
Now when you find an item such as "Value 1" you can replace with the value retrieved from the dictionary.

Building on #freeflow's excellent answer, I would also use a Scripting.Dictionary to hold the mappings from VALUE1 etc. to the target text.
I would then use Replace for each key in the Dictionary. You can loop like:
Dim key as Variant
For Each key in dict
Replace(<your string>, CStr(key), dict(key))
Next key
This will work so long as all your 'find' strings are totally unique i.e. none of them appears within another - so if you had "Value" and "Value 1" it would not work. Also, the simplest form of this method only works if there is a one-to-one mapping of text strings.
Thus, if your sample data is representative, you would want to look into using the Count argument of Replace so that you can replace the second occurrence of VALUE_4 with the different text, and so on.
I would do this by storing the dict values as an array e.g.
Dim my_arr(1 to 3) as String
my_arr(1) = "VALUE_4/CCC"
my_arr(2) = "VALUE_4/DDDD"
my_arr(3) = "VALUE_4/A"
dict.Add "VALUE_4", my_arr
Then when you are looping through, you can keep track of a counter (call it 'i' for example) and then you can just use Replace with a count of 1, increment 'i' by 1, and then use 'i' in each iteration to call on the relevant element of the array stored against VALUE_4 in the dict, like:
For Each key in dict
For i = LBound(dict(key)) to UBound(dict(key))
Replace (<your string>, CStr(key), dict(key)(i), 1, 1)
Next i
Next key
Hopefully you can build from there to what you need? Having reread your original post, I actually think my simplest solution would work (but I'll leave the more complex solution there in case it's of use to you or others), so long as dict is used to store the one-to-one mapping of, for example, "VALUE_1" to "VALUE_1/BB, VALUE_1/A" - you can loop through your original table and build those strings by concatenation - maybe even directly in the dict:
For Each cell in TableCol1 ' assuming it is cells, otherwise use an appropriate form of loop
tmp_str = cell.Value2
If dict.Exists(tmp_str) Then
dict(tmp_str) = dict(tmp_str) + ", " + tmp_str+cell.Offset(0,1).Value2
Else
dict.Add tmp_str, tmp_str + cell.Offset(0,1).Value2
End If
Next cell

Related

How do i go through two lists with a for each or other method in vb.net

I have a question, in vb.net, how do I go through two lists with a for each or another method?
for example i have defined 3 list
Public NSPS As New List(Of String)
Public CONTENEDOR As New List(Of String)
Public IDCONTENEDOR As New List(Of String)
I have 2 excel files with the same variable CONTENEDOR, and in the other its called IDCONTENEDOR
So i need to create a third excel file that finds the rows with the same IDcontenedor and it adds the variable NSPS as appropriate, to that row in the new excel .
What is the correct way to go through 2 lists with 2 cycles in vb.net
to just fill information in a new excel i use this code
Dim aux As Integer = 1
While (aux <= CONTENEDOR.Count)
hoja1.Cells("B" & aux + 1).Value = NPLANILLA.Item(aux - 1)
aux += 1
End While
aux = 1
Thanks in advance!
I am not sure if I understand your problem. The following answer is based on the best guess I could make.
To find a value corresponding to a key or id, use a dictionary. Dictionaries are much faster than loops for lookups.
Dim dict As new Dictionary(Of String, String)
Assuming that the 2 variables CONTENEDOR and IDCONTENEDOR have the same length and that the first list contains the values corresponding to the ids at the same index, you can fill the dictionary with
For i As Integer = 0 To CONTENEDOR.Count - 1
dict.Add(IDCONTENEDOR(i), CONTENEDOR(i))
Next
Now given an id of type String, you can get the value with
Dim value As String
If dict.TryGetValue(id, value) Then
NSPS.Add(value)
Else
' We did not find a value with this id.
End If
I am not sure where you get the id from, maybe from NPLANILLA.Item(aux - 1)? If it is typed as Object then convert it to String:
Dim id As String
id = CType(NPLANILLA.Item(aux - 1), String)

Remove duplicates in VBA Combobox

I have the below code to populate a listbox, therefore I want to remove duplicates from my combobox. I Don't know how to do it:
Private Sub CommandButton1_Click()
Dim ws_suivi As Worksheet
Set ws_suivi = ActiveWorkbook.Worksheets("suivi")
Fin_Liste_suivi = ws_suivi.Range("A65530").End(xlUp).Row
For i = 2 To Fin_Liste_suivi
UserForm_SDE.ComboBox_Type_Rapp.AddItem ws_suivi.Range("AD" & i)
Next
UserForm_SDE.Show
End Sub
It is often worth searching to see if a Library for VBA exists that will save you reinventing the wheel.
It is a particular annoyance of VBA that whilst we have such useful structures as Collections and Scripting.Dictionaries there is no easy way to get information into such objects or to do much processing of the data once those objects are populated.
I had a project which had a lot of processing of arrays/scripting.dictionariews and to make my life a little easier I created a VBA library in C# called Kvp (for Key Value Pairs) which is a bit like a Scripting.Dictionary on steriods.
You can download the library, source code, documentation for the Kvp object from here
Once you have added a reference to the Kvp library you can declare a Kvp object in the standard way.
Dim myKvp as Kvp
Set myKvp=New Kvp
You can then add a 1D range from an excel spreadsheet in a single statement
myKvp.AddByIndexFromArray <excel range>.Value
which gives a Kvp of long integers vs cell values
The OP wishes a list of unique values. To do this with a Kvp we can use the Mirror method to create a Kvp of the unique values.
Dim myMirroredKvp as Kvp
set myMirroredKvp=myKvp.Mirror
The Mirror method returns a Two item Kvp where item 0 is a Kvp of unique items vs the first Key at which the item was found and item 1 is a Kvp of original Keys vs value where the values are a duplicate.
You can then get an array of the keys using the GetKeys method
Dim myUniqueValues as Variant
myUniqueValues = myMirroredKvp.GetItem(0).GetKeys
Or should you want the items sorted in reverse order
myUniqueValues - myMirroredKvp.GetItem(0).GetKeysDescending
The above can be shortened to
myUniqueValues = myKvp.Mirror.GetItem(0).GetKeysDescending
I've found the Kvp library quite useful. I hope you do to!!
While you could load the list to a Dictionary, you might find it simpler to try using WorksheetFunction.CountIf to check if the item is further up your list (and has, thus, already been included):
If (i=2) OR (WorksheetFunction.CountIf(ws_suivi.Range(ws_suivi.Cells(2,30),ws_suivi.Cells(i-1,30)), ws_suivi.cells(i,30).Value)<1) Then
UserForm_SDE.ComboBox_Type_Rapp.AddItem ws_suivi.Range("AD" & i)
End If
As a side-note: Since Excel 2007 increased the Row Limit from 65536 (216) to 1048576 (220), you may want to change Fin_Liste_suivi = ws_suivi.Range("A65530").End(xlUp).Row to Fin_Liste_suivi = ws_suivi.Cells(ws_suivi.Rows.Count, 1).End(xlUp).Row
I found :
Dim Valeur As String
Dim i As Integer
Dim j As Integer
'For each element in the list
For i = 0 To lst_ref.ListCount - 1
Valeur = Combobox.List(i)
For j = i + 1 To Combobox.ListCount - 1
'If the element exist, delete it
If Valeur = Combobox.List(j) Then
Call Combobox.RemoveItem(j)
End If
Next j
Next i
It take the beggining of the combobox and check if the value is red again in to the end of the combobox.

VB.net Trim function

I have an issue with trim the string method NOT working completely I have reviewed MS Docs and looked of forums but with no luck... It's probably something simple or some other parameter is missing. This is just a sample,
Please note I need to pick up text before and after #, hence than I was planning to use # as a separator. Trim start # #, Trim End # #. I can't use The last Index or Replace per my understanding they have no direction. But perhaps I am misunderstood MS docs regards to trim Start and End as well...
thanks!
Dim str As String = "this is a #string"
Dim ext As String = str.TrimEnd("#")
MsgBox(ext)
ANSWER:
I found a solution for my problem, if you experience similar please see below:
1st: Trim end will NOT scan for the "character" from the Right as I originally thought it will just remove it from the right.... A weak function I would say:). IndexOf direction ID would be a very simple and helpful. Regards My answer was answered by Andrew, thanks!
Now there is another way around it if you try to split a SINGLE String INTO - QTY based on CHARACTER separation and populate fields accordingly.
Answer is ArrayList. Array List will ID each String so you can avoid repeated populations and etc. After you can use CASE or IF to populate accordingly.
Dim arrList As New ArrayList("this is a # string".Split("#"c)) ' Will build the list of your strings
Dim index As Integer = 1 ' this will help us index the strings 1st, 2nd and etc.
For Each part In arrList 'here we are going thru the list
Select Case index ' Here we are identifying which field we are populating
Case 1 '1st string(split)
MsgBox("1 " & arrList(0) & index) '1st string value left to SPLIT arrList(0).
Case 2 '2nd string(split)
MsgBox("2 " & arrList(1) & index) '2nd string value left to SPLIT arrList(1).
End Select
index += 1 'Here we adding one shift thru strings as we go
Next
Rather than:
Dim str As String = "this is a #string"
Dim ext As String = str.TrimEnd("#")
Try:
Dim str As String = "this is a #string"
Dim ext As String = str.Replace("#", "")
Dim str As String = "this is a #string"
Dim parts = str.Split("#"c)
For Each part in parts
Console.WriteLine($"|{part}|")
Next
Output:
|this is a |
|string|
Maybe there is a better way as we know there are multiple things to do the same thing.
The solution I used is below:
Dim arrList As New ArrayList("this is a # string".Split("#"c)) ' Will build the list of your strings
Dim index As Integer = 1 ' this will help us index the strings 1st, 2nd and etc.
For Each part In arrList 'here we are going thru the list
Select Case index ' Here we are identifying which field we are populating
Case 1 '1st string(split)
MsgBox("1 " & arrList(0) & index) '1st string value left to SPLIT arrList(0).
Case 2 '2nd string(split)
MsgBox("2 " & arrList(1) & index) '2nd string value left to SPLIT arrList(1).
End Select
index += 1 'Here we adding one shift thru strings as we go
Next

Strange behavior of range when used as key in dictionary

I have the following code:
Dim dicMyHash As Dictionary
Dim rngMyRange As Range
' A1 is empty - although the outcome is the same in any case
Set rngMyRange = Range("A1")
Set dicMyHash = New Dictionary
dicMyHash.Add Key:=rngMyRange(1), Item:=0
Debug.Print dicMyHash.Exists(rngMyRange(1).Value) ' returns False
Debug.Print rngMyRange(1) = rngMyRange(1).Value ' returns True
This behavior is somewhat unexpected. Is there some type casting going on in the background? rngMyRange(1).Value property returns a variant, whereas rngMyRange(1) is rngMyRange.item(1), which is a range. However, casting rngMyRange(1) to Variant gives the same results..
Also, adding keys is by value (so a copy of rngMyRange(1) is passed as a key). But still I cannot get why .Exists does not find the key..
Thank you in advance!
So here, we have three different values being passed around:
The original range.
Range.Value, which is a variant.
The copy of (1) which is internal to the dictionary.
If you compare these with equal signs, they are all the same. But according to Dictionary.Exists they are all different.
Why? When you use an equal sign with an object, the equal sign forces the object to call its default property. The default property of Range is Range.Value, which is why r = r.Value and also r = r.Offset(0, 0).
But for a dictionary this isn't so smart. Think about it: Every call to Dictionary.Exists would cause every object used as a key to call its default property. This can get really expensive and it can potentially trigger a lot of side effects. So instead, Dictionary.Exists tests the following:
Are you comparing an object to a non-object? Automatic fail.
Are you comparing two non-ojects? Return a = b.
Are you comparing two objects? Return a Is b.
So r is not the same as r.Value, since one is an object and the other is a non-object. And if you make a copy of r, like with r.Offset(0, 0), those are not the same either since they still point to two different objects, even if the objects have identical contents.
This, on the other hand, will work, since you will make r into the same object as d.Keys(0):
Dim d As Scripting.Dictionary
Dim r As Range
Set r = [a1]
Set d = New Dictionary
d.Add r, 0
Set r = d.Keys(0)
Debug.Print d.Exists(r)
I think the reason of your situation is that rngMyRange is recognised as an two- dimensional array and both array dimensions are passed to your dictionary.
If you change the line which adding element into Dictionary into this one:
dicMyHash.Add Key:=rngMyRange(1).value, Item:=0
it starting to work as you expect- both check points return true.
You could additionally analyse this situation in Locals Window while debugging of your code.
I'm not sure how you are putting this to use, but this will return True:
Sub test()
Dim dicMyHash As Dictionary
Dim rngMyRange As Range
Set rngMyRange = Range("A1")
Set dicMyHash = New Dictionary
dicMyHash.Add Key:=rngMyRange(1).Value, Item:=0 ' assign it with Value
Debug.Print dicMyHash.Exists(rngMyRange(1).Value)
End Sub
So then you'll have an item with a key of whatever's in A1.
I believe the reason it doesn't work without Value is that you are assigning a Range to the Key. It would make more sense to me if you were assigning the range to the Dictionary's Item.

Excel VBA - Add 1-dimensional array to multi-dimensional array without looping

I have a question regarding "creating a matrix" out of single arrays wihtout having to loop through:
From a function I get back one array with data (Return_Calc, rows = n-1). I am looking for something like
Output(n-1, j-1) = Return_Calc(Nav_Range)
At the moment, I am doing this:
Temp = Return_Calc(Nav_range)
For i = 1 To n - 1
Output(i - 1, j - 1) = Temp(i - 1)
Next i
The current option works. I was just wondering if there is another possibility without looping. Thanks for your help!
I'm not sure if you would be happy with that proposal. It presents possibility of creating Array-of-Arrays which, in some situation, would work similar to Multidimensional Array. You could consider solving your problems this way.
Here is a sample code how to create and which way you could retrieve data from final array.
Sub Array_Workaround()
Dim oneDimArrA, oneDimArrB
oneDimArrA = Array(1, 2, 3, 4)
oneDimArrB = Array("A", "B", "C", "D")
Dim multiDimArr
'creating multidemmnsional array
multiDimArr = Array(oneDimArrA, oneDimArrB)
'get element- different to standard syntax
Debug.Print multiDimArr(0)(0) '--> 1
Debug.Print multiDimArr(0)(1) '--> 2
Debug.Print multiDimArr(1)(1) '--> B
End Sub
There is one important benefit of presented solution- each internal array can have different dimension.

Resources