Dictionary doesn't display Items for certain Key (Numeric value) - vba

This is a Long one, but stay with me...
I have a Dictionary that saves "PO" as Key and "SO" as Items (there can be cases that a certain "PO" has multiple "SO".
My Excel data in a worksheet, where the Dictionary get's his values looks like this:
The code for populating the Dictionary (working) looks like this:
Option Explicit
Const OrdersDBShtName As String = "Orders_DB"
Public OrdersDBSht As Worksheet
Public LastSORow As Long
Public PODict As Object ' Public Dictionay for PO#, and keep SO per PO# (unique ID)
'======================================================================
Sub InitDict()
Dim AdminSht As Worksheet
Dim i As Long, j As Long
' set the sheet object where the "Orders_DB" data lies
On Error Resume Next
Set OrdersDBSht = ThisWorkbook.Worksheets(OrdersDBShtName)
On Error GoTo 0
If OrdersDBSht Is Nothing Then ' in case someone renamed the "Admin" Sheet
MsgBox Chr(34) & OrdersDBShtName & Chr(34) & " Sheet has been renamed, please modify it", vbCritical
End
End If
With OrdersDBSht
LastSORow = .Cells(.Rows.Count, "B").End(xlUp).Row ' get last row with data in column "B" ("SO#")
' get all SO numbers in Dictionary (SO# is unique ID)
Set SODict = CreateObject("Scripting.Dictionary")
' get all PO's in Dictionary (PO# is unique, but there can be several SO# per PO#)
Set PODict = CreateObject("Scripting.Dictionary")
Dim ID As Variant, Names As String
' add unique Category values to Dictionary object , and save Item Names in Names
For i = 2 To LastSORow
If Not PODict.Exists(.Range("A" & i).Value) Then
ID = .Range("A" & i).Value
For j = 2 To LastSORow
If .Range("A" & j).Value = ID Then
If Names = "" Then
Names = .Range("B" & j).Value ' get the SO#
Else
Names = Names & "," & .Range("B" & j).Value ' get the SO#
End If
End If
Next j
PODict.Add ID, Names
End If
ID = Empty
Names = Empty
Next i
' section below for DEBUG Only (works)
Dim Key As Variant
For Each Key In PODict.keys
Debug.Print Key & " | " & PODict(Key)
Next Key
End With
End Sub
The Problem: I have a User_Form with 2 ListBoxes.
ExistingPO_LB - a ListBox for "PO"s, reads all the Unique Keys in the Dictionary object.
ExistingSO_LB - a ListBox for "SO#", should show only Items for the Key selected in ExistingPO_LB.
In some cases (like the screen-shot below) it works:
In some cases (like the screen-shot below) it doesn't (even though the Items have been saved correctly in PODict Dictionary object):
User_Form Code
Private Sub EditSO_Btn_Click()
With Me.ExistingSO_LB
For i = 0 To .ListCount - 1
If .Selected(i) Then
EditSONumer = .List(i)
Exit For
End If
Next i
End With
If EditSONumer = 0 Then
MsgBox "No SO was selected from the list", vbInformation
Exit Sub
End If
Unload Me
AddEdit_Orders_Form.Show ' call sub Edit Order (load Add Order with the SO# data requested)
End Sub
'=========================================================
Private Sub ExistingPO_LB_Click()
' ****** This is the Sub I guess I'm missing something ******
Dim i As Long
Dim POSelected As Variant
Dim SOArr As Variant
With Me.ExistingPO_LB
For i = 0 To .ListCount - 1
If .Selected(i) Then
POSelected = .List(i)
Exit For
End If
Next i
End With
' update the SO listbox with only relevant SO (from the selected PO)
SOArr = Split(PODict(POSelected), ",") '<=== PODict(POSelected) return empty ???
With Me.ExistingSO_LB
.Clear ' clear the previous items
For i = LBound(SOArr) To UBound(SOArr)
.AddItem SOArr(i)
Next i
End With
End Sub
'=========================================================
Private Sub UserForm_Initialize()
' load all existing PO's from "Orders DB" sheet
Dim Key As Variant
' populate listbox with PO's
With Me.ExistingPO_LB
For Each Key In PODict.keys
.AddItem Key
Next Key
End With
End Sub

The numeric keys were entered as numbers and you are fetching them as strings. I suggest that you stick to one convention for your dictionary.
Sub TestDict()
Dim dict As New Dictionary
dict.Add 1, "one"
dict.Add "2", "two"
Debug.Print dict("1") ' Nothing
Debug.Print dict(1) ' one
Debug.Print dict("2") ' two
Debug.Print dict(2) ' Nothing
End Sub
Solution
Chose a convention for your dictionary and stick to it. In this application I would take the convention of always converting my keys to strings, both when inserting and when fetching. A few changes in your code can achieve it:
If Not PODict.Exists(CStr(Range("A" & i).Value) Then ' could use .Text also
PODict.Add CStr(ID), Names
SOArr = Split(PODict(CStr(POSelected)), ",") ' maybe not needed here, but to illustrate

Related

VBA Word. How to find first empty cell in Word table?

I've been trying to find the first empty cell in Word table using VBA.
The code which I've put below finds all the empty cells instead I want to find the first one after filled one. How to solve this problem?
For Each oRow In Selection.Tables(1).Rows
For Each oCell In oRow.Cells
If Selection.Text = Chr(13) & Chr(7) Then
oCell.Select
'Selection.PasteSpecial DataType:=wdPasteText
MsgBox oCell.RowIndex & " " & oCell.ColumnIndex & " is empty."
End If
Next oCell
Next oRow
Is this what you had in mind?
Sub FindNextBlank()
Dim Tbl As Table
Dim TblRow As Row
Dim HasText As Boolean
Dim LookForText As Boolean, Done As Boolean
Dim R As Long, C As Long
Dim Txt As String
LookForText = True
With ThisDocument.Tables(1)
For R = 1 To .Rows.Count
Set TblRow = .Rows(R)
For C = 1 To TblRow.Cells.Count
HasText = (Len(TblRow.Cells(C).Range.Text) > 2)
If HasText = LookForText Then
If LookForText Then
LookForText = Not LookForText
Else
Done = True
TblRow.Cells(C).Range.Select
Exit For
End If
End If
Next C
If Done Then Exit For
Next R
If Done Then
Txt = "Cell #" & C & " in row " & R & " is free."
Else
Txt = "No free cell was found that" & vbCr & _
" follows one that has text."""
End If
End With
MsgBox Txt, vbInformation, "Search result"
End Sub
For ... Each is faster but I instinctively distrust it because the sequence of items in them is usually determined by the sequence of their creation. That may or may not be top to bottom, left to right. Calling cells by their coordinates may take a little longer but you retain control of the sequence.
As you may have discovered, determining an empty cell in Word is not as straightforward as it might appear. The code below looks for the first cell where the length of the text in the cell is 1 after removing any spaces, tabs and vbCr. You might extend this to also look for vbLF, manual line breaks and other characters that might be in a cell but not visible if you have view text markers turned off.
The .Cells method of a table range is the most appropriate tool to use here because it will work even if the table has merged cells. Searching a table using the cell coordinates will fail if there are merged cells in the table. Using the .Cells method the table is searched from Top Left to Bottom right (row by column).
Option Explicit
Sub Test()
Dim myCell As Word.Range
Set myCell = FirstEmptyCell(ActiveDocument.Tables(1).Range)
myCell.Select
End Sub
' Returns the first cell that has a text length of 1
' after removing spaces and tab characters from the cell text
Public Function FirstEmptyCell(ByVal TableRange As Word.Range) As Word.Range
Dim myCell As Word.Cell
For Each myCell In TableRange.Tables(1).Range.Cells
Dim CellText As String
CellText = myCell.Range.Text
CellText = Replace(CellText, vbTab, vbNullString)
CellText = Replace(CellText, " ", vbNullString)
CellText = Replace(CellText, vbCr, vbNullString)
If Len(CellText) = 1 Then
Set FirstEmptyCell = myCell.Range
Exit Function
End If
Next
End Function
The solution is really much simpler than the other 'answers' suggest:
Dim i As Long
With Selection
If .Information(wdWithInTable) = True Then
With .Tables(1).Range
For i = 1 To .Cells.Count
With .Cells(i)
If Len(.Range.Text) = 2 Then
MsgBox " Row " & .RowIndex & ", Column " & .ColumnIndex & " is empty."
.Range.PasteSpecial DataType:=wdPasteText
Exit For
End If
End With
Next
End With
Else
MsgBox "No table selected", vbExclamation
End If
End With
I've even added some error checking.

Vba: getting all cells from a given row matching a given string

I am given a row of cells and a string to match. I want to know the positions where the string appears. I ideally need to have it in a vector in vba.
I am trying to loop over all the occurrencies of "Name To Match" over the row starting on the cell corresponding to the variable.
This is what I've tried so far:
myIndex = 0
While myIndex < maxIndexAllowed
myIndex = Match("Name To Match", Offset(Range("beginRowToInspect"), 0, myIndex, 1, maxIndexAllowed), 0) + myIndex
Wend
conceptually this is fine. But I get this error: "sub or function not defined" and the keyword Offset appears to be highlighted.
Bonus: I would be happy if I could get rid of maxIndexAllowed.
Try this:
Option Explicit
Sub FindAllMatches()
Dim Matches As New Scripting.Dictionary 'Need the Microsoft Scripting Runtime reference to work
Dim C As Range
Dim Col As Byte
Dim RowToInspect As Long
Dim NameToMatch As String
RowToInspect = 2 'here is were you set the row to inspect
NameToMatch = "x" 'here is were you set your string to match
With ThisWorkbook.Sheets("MySheet") 'change MySheet for your working sheet
Col = .Cells(RowToInspect, .Columns.Count).End(xlToLeft).Column 'last column on your row to inspect
For Each C In .Range(.Cells(RowToInspect, 1), .Cells(RowToInspect, Col))
If Not Matches.Exists(C.Value) Then
Matches.Add C.Value, C.Column 'First match we add the item and its column
Else
Matches(C.Value) = Matches(C.Value) & "," & C.Column 'Later matches will add the columns separated by ", "
End If
Next C
End With
If Matches.Exists(NameToMatch) Then
MsgBox "The columns for " & NameToMatch & " that were found are: " & Matches(NameToMatch)
Else
MsgBox NameToMatch & " was not found on row: " & RowToInspect
End If
End Sub

What part of this code brings up the msg box for selecting an item

I have paid a person to help me write a macro to populate tables in my spreadsheet. I feel like I can usually understand some of the code that is written, but this is beyond me. I'm just trying to learn how to do this for myself.
Option Explicit
Option Base 1
Dim s_no() As String
Sub createReport()
start_win.Show
End Sub
Sub ook()
Dim last As Integer
ReDim s_no(1 To 1)
If Not Sheet1.Range("A2").Value = "" Then
s_no(1) = Sheet1.Range("A2").Value
Else
MsgBox "Empty sheet"
End If
last = Cells(Sheet1.Rows.Count, 1).End(xlUp).Row
Dim i As Integer
For i = 2 To last
If already_exists(Sheet1.Range("A" & CStr(i)).Value) = False Then
ReDim Preserve s_no(1 To UBound(s_no) + 1)
s_no(UBound(s_no)) = Sheet1.Range("A" & CStr(i)).Value
End If
Next
For i = 1 To UBound(s_no)
Debug.Print s_no(i)
Next
End Sub
Function already_exists(trial)
already_exists = False
Dim i As Integer
For i = 1 To UBound(s_no)
If s_no(i) = trial Then
already_exists = True
Exit Function
End If
Next
End Function
You paid someone to write that? I'd ask for my money back.
If you supply the code within the start_win form I can rip that apart to if you want.
I've added comments to each line explaining.
Option Explicit 'All variables must be declared first.
Option Base 1 'Arrays start at 1 rather than 0.
Dim s_no() As String 'Global array variable.
'Available to all procedures in the module.
Sub createReport()
start_win.Show 'Open and display a form called `start_win`.
'Form will likely contain code as well.
End Sub
Sub ook() 'Reference to disc-worlds librarian?
'(and the name of this procedure)
Dim last As Integer 'The 'last' variable will hold values between
'–32,768 to 32,767.
'Terrible for holding row numbers.
ReDim s_no(1 To 1) 'The global variable is reassigned as an
'array containing 1 element.
If Not Sheet1.Range("A2").Value = "" Then 'Sheet1 is the sheet codename
'(name not in brackets in Project explorer).
'If cell A2 is an empty string then go to
'next line, otherwise message box.
s_no(1) = Sheet1.Range("A2").Value 'Place value from A2 into "s_no(1)"
'element of variable.
Else
MsgBox "Empty sheet" 'If cell A2 was an empty string then
'jump to this line.
End If
last = Cells(Sheet1.Rows.Count, 1).End(xlUp).Row 'Get the last row number from the
'sheet with codename Sheet1 and
'store in 'last' variable.
'Didn't I say that was a terrible idea?
'This line will fail if the last
'row is >32,767 (Overflow error).
Dim i As Integer 'Again, terrible idea - integer for row
'numbers... no. Use LONG instead.
For i = 2 To last 'Step from row 2 to last row
'(providing error hasn't happened).
If already_exists(Sheet1.Range("A" & CStr(i)).Value) = False Then 'Pass the value from
'row number 'i' in column A
'to the 'already_exists' procedure
'where it will be called 'trial'
'"Sheet1.Cells(i,2)" would be better.
ReDim Preserve s_no(1 To UBound(s_no) + 1) 'Increase the size of the 's_no'
'array while keeping an values it
'already holds.
s_no(UBound(s_no)) = Sheet1.Range("A" & CStr(i)).Value 'Place the value from column A
'in the array.
End If
Next
For i = 1 To UBound(s_no) 'Cycle through the array.
Debug.Print s_no(i) 'Place the value from the array in the immediate window.
Next
End Sub
Function already_exists(trial) 'Function to return a variant value
'(should specify the return type).
already_exists = False 'Start as False.. so it's a boolean.
'Be better to declare that in the function name.
Dim i As Integer 'There's that integer again. Just stop...
For i = 1 To UBound(s_no) 'Cycle through each element in 's_no' array.
If s_no(i) = trial Then 'Does that element equal the one passed
'from the main procedure?
already_exists = True 'If it does then return TRUE to the main procedure.
Exit Function 'Exit the function and jump back to the main procedure.
'Would be the better to exit the loop and have one
'exit point for the function.
End If
Next
End Function
Edit:
As a follow on this is how I'd write the ook procedure and do away with the already_exists function.
Sub ook()
Dim lLast As Long 'Holds last row number.
Dim i As Long 'Holds current row number.
Dim s_no As Object 'Define an object.
Dim key As Variant 'Use this to step through the populated dictionary.
Set s_no = CreateObject("Scripting.Dictionary") 'Define it as a dictionary.
With Sheet1
lLast = .Cells(.Rows.Count, 1).End(xlUp).Row
'Previously assumed that A2 would hold a value,
'so if last row in column A is 1 then A2 will be blank
'and the sheet is empty.
If lLast <> 1 Then
For i = 2 To lLast
'Does the value already exist in the dictionary object?
If Not s_no.exists(.Cells(i, 1).Value) Then
s_no.Add .Cells(i, 1).Value, .Cells(i, 1).Value 'It doesn't, so add it.
End If
Next i
For Each key In s_no
Debug.Print s_no(key)
Next key
Else
'Nothing else should happen if the sheet is empty.
MsgBox "Empty sheet", vbCritical + vbOKOnly
End If
End With
End Sub
Another edit: Dictionaries - sorry, don't usually link outside this site but it is a good tutorial.

Collect unique identifiers from one column and paste the results in a different worksheet.

What I'm looking to do is comb through a column and pull all the unique identifiers out of that column and then paste the results in a table in a different worksheet. I found the code below and it is very close to what I need. However, I have two major problems with it that I cannot figure out. First the area that this macro searches is constant ie "A1:B50". I need this to be one column and be dynamic since more data and new unique identifiers will be added to this worksheet. Second I cannot figure out how to paste my results to a specific range on a different worksheet. For example if I wanted to take the results and paste them in "sheet2" starting in at "B5" and going to however long the list of unique identifiers is.
Sub ExtractUniqueEntries()
Const ProductSheetName = "Sheet1" ' change as appropriate
Const ProductRange = "B2:B"
Const ResultsCol = "E"
Dim productWS As Worksheet
Dim uniqueList() As String
Dim productsList As Range
Dim anyProduct
Dim LC As Integer
ReDim uniqueList(1 To 1)
Set productWS = Worksheets(ProductSheetName)
Set productsList = productWS.Range(ProductRange)
Application.ScreenUpdating = False
For Each anyProduct In productsList
If Not IsEmpty(anyProduct) Then
If Trim(anyProduct) <> "" Then
For LC = LBound(uniqueList) To UBound(uniqueList)
If Trim(anyProduct) = uniqueList(LC) Then
Exit For ' found match, exit
End If
Next
If LC > UBound(uniqueList) Then
'new item, add it
uniqueList(UBound(uniqueList)) = Trim(anyProduct)
'make room for another
ReDim Preserve uniqueList(1 To UBound(uniqueList) + 1)
End If
End If
End If
Next ' end anyProduct loop
If UBound(uniqueList) > 1 Then
'remove empty element
ReDim Preserve uniqueList(1 To UBound(uniqueList) - 1)
End If
'clear out any previous entries in results column
If productWS.Range(ResultsCol & Rows.Count).End(xlUp).Row > 1 Then
productWS.Range(ResultsCol & 2 & ":" & _
productWS.Range(ResultsCol & Rows.Count).Address).ClearContents
End If
'list the unique items found
For LC = LBound(uniqueList) To UBound(uniqueList)
productWS.Range(ResultsCol & Rows.Count).End(xlUp).Offset(1, 0) = _
uniqueList(LC)
Next
'housekeeping cleanup
Set productsList = Nothing
Set productWS = Nothing
End Sub
I think your solution is a bit more tricky than it needs to be. Collecting unique ids becomes almost trivial is you use a Dictionary instead of a list. The added benefit is that a dictionary will scale much better than a list as your data set becomes larger.
The code below should provide you with a good starting point to get you going. For convenience's sake I used the reference from your post. So output will be on sheet2 to starting in cell B5 going down and the input is assumed to be on sheet1 cell B2 going down.
If you have any questions, please let me know.
Option Explicit
Sub ExtractUniqueEntries()
'enable microsoft scripting runtime --> tools - references
Dim unique_ids As New Dictionary
Dim cursor As Range: Set cursor = ThisWorkbook.Sheets("Sheet1").Range("B2") 'change as Required
'collect the unique ids
'This assumes that:
'1. ids do not contain blank rows.
'2. ids are properly formatted. Should this not be the could you'll need to do some validating.
While Not IsEmpty(cursor)
unique_ids(cursor.Value) = ""
Set cursor = cursor.Offset(RowOffset:=1)
Wend
'output the ids to some target.
'assumes the output area is blank.
Dim target As Range: Set target = ThisWorkbook.Sheets("Sheet2").Range("B5")
Dim id_ As Variant
For Each id_ In unique_ids
target = id_
Set target = target.Offset(RowOffset:=1)
Next id_
End Sub
A small modification will do it; the key is to define the ProductRange.
Sub ExtractUniqueEntries()
Const ProductSheetName = "Sheet1" ' change as appropriate
Dim ProductRange
ProductRange = "B2:B" & Range("B" & Cells.Rows.Count).End(xlUp).Row
Const ResultsCol = "E"
Dim productWS As Worksheet
Dim uniqueList() As String
Dim productsList As Range
Dim anyProduct
Dim LC As Integer
ReDim uniqueList(1 To 1)
Set productWS = Worksheets(ProductSheetName)
Set productsList = productWS.Range(ProductRange)
Application.ScreenUpdating = False
For Each anyProduct In productsList
If Not IsEmpty(anyProduct) Then
If Trim(anyProduct) <> "" Then
For LC = LBound(uniqueList) To UBound(uniqueList)
If Trim(anyProduct) = uniqueList(LC) Then
Exit For ' found match, exit
End If
Next
If LC > UBound(uniqueList) Then
'new item, add it
uniqueList(UBound(uniqueList)) = Trim(anyProduct)
'make room for another
ReDim Preserve uniqueList(1 To UBound(uniqueList) + 1)
End If
End If
End If
Next ' end anyProduct loop
If UBound(uniqueList) > 1 Then
'remove empty element
ReDim Preserve uniqueList(1 To UBound(uniqueList) - 1)
End If
'clear out any previous entries in results column
If productWS.Range(ResultsCol & Rows.Count).End(xlUp).Row > 1 Then
productWS.Range(ResultsCol & 2 & ":" & _
productWS.Range(ResultsCol & Rows.Count).Address).ClearContents
End If
'list the unique items found
For LC = LBound(uniqueList) To UBound(uniqueList)
productWS.Range(ResultsCol & Rows.Count).End(xlUp).Offset(1, 0) = _
uniqueList(LC)
Next
'housekeeping cleanup
Set productsList = Nothing
Set productWS = Nothing
End Sub

Find and Replace and Looping Though a Table

I am trying to make a user friendly excel interface to replace values in a large data set using vba.
I have a small table with the Columns "Replace What" and "Replace With" and the large data set in a worksheet together.
My goal is to hit a button and have a macro use the Find function on the data to look up the number in the Replace What Column and then paste in the data from the corresponding row of the Replace With column.
Here's my code so far:
Sub ReplaceItems()
Dim replaceList As Range
Set replaceList = ListItems("Table4").ListColummns("Replace What").DataBodyRange
Dim item As Range
For Each Cell In replaceList.Cells
Cell.Offset(0, 1).Select.Copy
item = ActiveWorksheet.Find(Cell.Value)
item.Select.Paste
Next Cell
End Sub
You could use a dictionary to quickly map the Replace What key to the Replace With values. Then check if a key appears in the cell's value (you can use a combination of Index-match and InStr/RegEx, but I would probably just loop through the cells). Finally delete the key from the cell and copy in the value, you can do this in one line using Left() and Right() functions
Ex. Using a dictionary
Sub dictionary()
Dim key As String, value As String, var As Variant
Dim d As Object
Set d = CreateObject("Scripting.Dictionary")
key = "my key"
value = "my value"
d.Add key, value
d.Add key & 1, value & 1
For Each var In d.keys
MsgBox var & " : " & d.item(var)
Next var
End Sub
Ex. Replace key with value
Sub ReplaceItems()
Dim s As String, k As String, v As String, index As Integer
s = "this is my key, I think"
k = "key"
v = "value"
index = InStr(s, k)
MsgBox Left(s, index - 1) & v & Right(s, Len(s) - index - Len(k) + 1)
End Sub
I happen to have this kind of routine so I'll share.
Like what Alter posted, I used Dictionary.
Sub test()
Dim RepList As Range, RepItem As Range
Dim rng As Range, ldbase As Variant
Dim i As Long
With Sheet1 '~~> contains your table, change to suit
Set RepList = .Range("Table4[Replace What]")
End With
With Sheet2 '~~> contains your large database, change to suit
'~~> transfer your database in an array
'~~> I this example, my target is the whole column B with data.
Set rng = .Range("B2", .Range("B" & .Rows.Count).End(xlUp))
ldbase = Application.Transpose(rng) 'dumps range values to array
End With
With CreateObject("Scripting.Dictionary")
'~~> first transfer your list in Dictionary
For Each RepItem In RepList
If Not .Exists(RepItem.Value) Then
.Add RepItem.Value, RepItem.Offset(0, 1).Value
End If
Next
'~~> here is the actual find and replace
For i = LBound(ldbase) To UBound(ldbase)
If .Exists(ldbase(i)) Then ldbase(i) = .Item(ldbase(i))
Next
rng = Application.Transpose(ldbase) '~~> dumps array values to range
End With
End Sub
HTH.