VBA Excel Using an array element to reference a cell in a Word document's table - arrays

I'm using an Excel userform to push data entered on the form into a Word Document. The Word document has several tables and the data from the form textbox controls needs to go into specific cells on the Word document. There are 15 checkboxes with 15 matching textboxes on the Excel form, so I figured the easiest way to handle it is with an array so I could loop through it.
Addressing the checkboxes (the Program array) isn't an issue because they're named "chkbox1-chkbox15" and there are matching Content Controls in the Word table tagged chkbox1-chkbox15, so that loop works properly.
The problem is with the textboxes' text. The cells the data needs to go in is kind of all over the place and I don't have the luxury of altering the table layout. So I figured I'd create a 15-element array that contains the row & column location of the target cell.
Here's the relevant part of my code:
Dim ImpactCells(15) As String
'Specify Form cell locations for specific programs
ImpactCells(0) = "4,2" 'Program 1
ImpactCells(1) = "5,2" 'Program 2
ImpactCells(2) = "6,2" 'Program 3
ImpactCells(3) = "4,4" 'Program 4
ImpactCells(4) = "5,4" 'Program 5
ImpactCells(5) = "6,4" 'Program 6
ImpactCells(6) = "7,2" 'Program 7
ImpactCells(7) = "10,2" 'Program 8
ImpactCells(8) = "11,2" 'Program 9
ImpactCells(9) = "12,2" 'Program 10
ImpactCells(10) = "13,2" 'Program 11
ImpactCells(11) = "10,4" 'Program 12
ImpactCells(12) = "11,4" 'Program 13
ImpactCells(13) = "12,4" 'Program 14
ImpactCells(14) = "13,4" 'Program 15
For i = 0 To 14 'Count of Programs Array
If Programs(i) <> "" Then 'If array element is not blank
Set cc = .SelectContentControlsByTag("Program" & i + 1).Item(1) 'Select the Content Control with the Program(array index) tag
cc.Checked = True 'Check it
End If
If ImpactCount(i) <> "" Then
'Code to populate cells here
.Tables(9).Cell(ImpactCells(i)).Range.Text = Me.Controls("txtImpact" & i + 1).Text
End If
Next
The line thats causing an error is:
.Tables(9).Cell(ImpactCells(i)).Range.Text = Me.Controls("txtImpact" & i + 1).Text
It says: Compile Error: Argument not optional and highlights the word cell. The code works when I put specific cell locations instead of ImpactCells(i).
Any help would be appreciated. Suggestions for a more efficient way of doing this would also be great, keeping in mind that I can't modify the way the Word table is laid out.

Cell requires a row and a column argument (both numeric). You're passing a single String argument.
You could try something like this:
Dim arr
'...
'...
If ImpactCount(i) <> "" Then
'Code to populate cells here
arr = split(ImpactCells(i),",") 'convert to array
.Tables(9).Cell(arr(0), arr(1)).Range.Text = _
Me.Controls("txtImpact" & i + 1).Text
End If

Related

Extracting unique values from row in Excel

I need to summarize unique values from a row into a column that's in the same row. My goal is in the second row of the attached image where T:Z contains the data and AA:AC contains the summary (I typed the values in for the demo). The first row is what is currently occurring where I tried using a nested if function for values greater than zero, but I also tried using an index match function to no avail. The issue is I either receive duplicates in AA:AC or not all values are included.
Currently using Excel 2016
So if I understand you correctly, you are going to have a sheet of rows of data. You want to look in the columns T:Z and then generate a list of unique values (non-zero) in the columns AA:AC. I assume that you know you will never have more than 3 unique values, but I can't be sure that this wasn't just an omission.
Either way, the below code should work:
Sub Find_Uniques()
Dim X As Integer, Y As Integer, Z As Integer
Dim Temp_Strings() As String
For X = 1 to 10000 'This assumes you don't have more than 10,000 rows of data
ReDim Temp_Strings(1 to 5) As String
For Y = 20 to 26
If Range(Cells(X,Y).Address).Value <> "" And Range(Cells(X,Y).Address).Value <> 0 Then
For Z = 1 to 5
If Temp_Strings(Z) = "" Then
Temp_Strings(Z) = Range(Cells(X,Y).Address).Value
Exit For
End If
If Temp_Strings(Z) = Range(Cells(X,Y).Address).Value Then Exit For
Next Z
End If
Next Y
For Z = 1 to 5
If Temp_Strings(Z) <> "" Then Range(Cells(X,Z+26).Address)).Value = Temp_String(Z)
Next Z
Next X
End Sub
Thank you all for your help. Instead of extracting the data from the row, I wrote a macro that changed the zeros to blanks, deleted the blank cells, and shifted them to the left. After that it was easy to cut the range and paste it into the old data set to be analyzed.
Sub clean_data()
Sheets("Reason data").Range("H:Z").Replace 0, ""
Call delete_blanks
End Sub
Sub delete_blanks()
Sheets("Reason data").Range("H:Z").SpecialCells(xlCellTypeBlanks).Delete (xlToLeft)
Call move_data
End Sub
Sub move_data()
'Copies reason data and pastes it into data worksheet
Sheets("Reason data").Range("A3:K3", Sheets("Reason data").Range("A3:F3").End(xlDown)).Cut _
Sheets("Data").Range("A1").End(xlDown).Offset(1)
End Sub

Excel VBA code needed to copy row if criteria is met

I have an excel document with two sheets. Sheet 1 has columns A-Q and Sheet 2 has columns A-H. What I need is a code that will copy the information in a row from sheet 1 to sheet 2 if the criteria is met. The criteria is the word "Awarded" in column L (Sheet 1).
Also is it possible to have only specific columns in the row copied?
A B C D E F G H I J K L M N
X X Awarded X X
I would like to have only columns C,D,M, and N copied from the row if the word "awarded" is in column L. This information would be copied to Sheet 2 in the following fashion
Sheet 1 Sheet 2
D --> B
C --> C
M --> D
N --> F
I hope I'm being clear. Thanks in advance and let me know if I need to clarify!+
This is the code I currently have, which works. Only problem is it copies the entire row of information into sheet 2 when I only want rows D,C,M, and N to be copied.
Sub testing()
Set a = Sheets("Sheet1")
Set b = Sheets("Sheet2")
Dim d
Dim j
d = 1
j = 2
Do Until IsEmpty(a.Range("L" & j))
If a.Range("L" & j) = "Awarded" Then
d = d + 1
b.Rows(d).Value = a.Rows(j).Value
End If
j = j + 1
Loop
End Sub
First what you should do is change your data structure. Assuming you are using Excel 2007 or later, there is a great feature called Tables. If you highlight all of your data and go to Insert->Table, select the "My Table Has Headers" checkbox, and press ok, you will see a nicely formatted table. Do that for both of the data sets on each sheet.
This is more than just pretty formatting though, it is what is called a ListObject. In your VBA code, use the following to reference it:
Dim Table1 as ListObject, Table 2 as ListObject
Dim HeaderIndex as Integer
Dim MyColumnRange as Range
Set Table1 = Sheet1.ListObjects("TableName1")
`Change the table name under Formulas->Name Manager
Set Table2 = Sheet1.ListObjects("TableName2")
HeaderIndex = Application.WorksheetFunction.Match("ColumnLHeaderName", _
Table1.HeaderRowRange, 0)
Set MyColumnRange = Table1.ListColumns(HeaderIndex).DataBodyRange
MyColumnRange.Select
At this point, the select statement is just to show you what range you are dealing with now. The HeaderIndex refers to the header sub component of the table ListObject. Using Match() will allow you to specify the name of the column header without hard coding it's position. (i.e. if your data starts in column A, the header value in column L will return HeaderIndex = 12)
Now that you know what column you want, you select the ListColumn object. Then, the DataBodyRange is used to select the range component of that object. This is the entire range in that column. You can then iterate down the list to find the data you want.
EDIT: Updated Example:
'Specify your ranges you will be copying from beforehand, adding as many as you need here.
HeaderIndex_D = Application.WorksheetFunction.Match("ColumnXHeaderName", _
Table1.HeaderRowRange, 0)
HeaderIndex_C = Application.WorksheetFunction.Match("ColumnXHeaderName", _
Table1.HeaderRowRange, 0)
HeaderIndex_M = Application.WorksheetFunction.Match("ColumnXHeaderName", _
Table1.HeaderRowRange, 0)
HeaderIndex_N = Application.WorksheetFunction.Match("ColumnXHeaderName", _
Table1.HeaderRowRange, 0)
Set ColumnRange_D= Table1.ListColumns(HeaderIndex_D).DataBodyRange
Set ColumnRange_C= Table1.ListColumns(HeaderIndex_C).DataBodyRange
Set ColumnRange_M= Table1.ListColumns(HeaderIndex_M).DataBodyRange
Set ColumnRange_N= Table1.ListColumns(HeaderIndex_N).DataBodyRange
'Now, loop through each row that exists in your table. If the testing
'condition contained in MyColumnRange you previously defined is met,
'then assign the destination cell (which can be defined in the same way
'as above) equal to the lookup range's current row value (specified by i)
For i = 1 to MyColumnRange.Rows.Count
If MyColumnRange(i) = "Awarded" Then
DestinationCell1.Value = ColumnRange_D(i)
DestinationCell2.Value = ColumnRange_C(i)
DestinationCell3.Value = ColumnRange_M(i)
DestinationCell4.Value = ColumnRange_N(i)
End If
Next i

Add value to the last empty cell in a defined dynamic column

My model takes two numbers from one sheet, adds the average to another sheet in the last cell of a defined column. The problem that I have is that when I insert a new column, the references get missed up and I'm trying to have a macro that would 1. take the average 2. look for a specific column on the second sheet 3. paste the averaged value to the last cell.
Please help me with this I have been trying to get my head around it for a long time.
my problem is that I have to insert new columns and I need to keep the references dynamic when adding a value to the last empty cell in a column. For example: if i have salary as col A, and expenses as Col B - in this model that I have now I put in .Cells(emptyRow, 1) and .Cells(emptyRow, 2) now if I insert a column between A and B the references 1 and 2 will not work. Is there anyway that I can work around this where if i add a new column it wont mess up the references in the macro?
Thank you.
This is the code that I have right now but it does not really work - when I insert a new column the column defined name does not shift right.
Sub demo()
Dim expCol As Long, FirstEmptyRow As Long
Range("B:B").Cells.Name = "expenses"
expCol = Range("expenses").Column
FirstEmptyRow = Cells(Rows.Count, expCol).End(xlUp).Row + 1
Cells(FirstEmptyRow, expCol).Value = 123
End Sub
P.S. 123 here is just an example for testing purposes. The value that would replace it in my model is the average I talk about in the question.
If your columns have headers (I guess they do), and your data has no gaps just use
Range("1:1").Find(columnName).End(xlDown).Offset(1,0) = 123
If a column can have just a header but no values, you need to add additional check if second row isn't empty.
If you create a named range this way (rather than the Range.Cells.Name way you were using), then when inserting columns the reference will be dynamic. Now if you insert columns between A and B later in the code, you can still use expCol and FirstEmptyRow to reference the first empty cell in the expenses column, where ever it may have moved to on the sheet, as long as you update them after each column insertion.
Sub Demo()
Dim expensesrng As Range
Dim Expenses As Range
Dim expCol As Long
Dim Exprng As Range
Dim FirstEmptyRow As Long
'set the original range to use for the expense column
Set expensesrng = Range(Range("B1"), Range("B1").End(xlDown))
'add the named range
ActiveWorkbook.Names.Add Name:="Expenses", RefersTo:=expensesrng
' create a variable to refer to the Expenses Range
Set Exprng = ActiveWorkbook.Names("Expenses").RefersToRange
expCol = ActiveWorkbook.Names("Expenses").RefersToRange.Column
FirstEmptyRow = Exprng.End(xlDown).Offset(1, 0).Row
Cells(FirstEmptyRow, expCol).Value = 123
'after inserting columns then you will have to get/update the column number
'of the expense named range and the first empty row before adding your new expense
'data to it
Range("B:B").Insert Shift:=xlShiftToRight
expCol = ActiveWorkbook.Names("Expenses").RefersToRange.Column
FirstEmptyRow = expensesrng.End(xlDown).Offset(1, 0).Row
Cells(FirstEmptyRow, expCol).Value = 123
End Sub

Lcase operation on array taking too long

I'm working with a script designed to compare values returned from a form against values from a database dumped to an array, via GetRows. The purpose of the check is to compare form values against database values and only update the matching ids' rows in the database.
I've seen this done with hidden variables in forms, but as we have quite a few users online at any given time, the values on the db end could change while a user was completing the form.
Currently, the code uses an inner and outer loop to run this comparison, with a temporary variable being assigned the current col/row from the aforementioned array. An lcase and trim operation are performed on the value to obtain the temporary variable.
This is causing a considerable performance drain, and I was wondering if the lcase/trim functionality could perhaps be performed during the creation of that array, rather than in a looping situation?
Here's my code:
**note: this utilizes the FastString Class for concatenation, thus the "FastString" and ".Append"
dim iRowLoop, iColLoop, zRowLoop, strChange, tempDbValsCase
Set strChange = New FastString
for iRowLoop = 0 to ubound(arrDbVals, 2)
for zRowLoop = 0 to ubound(arrFormComplete)
''#****below line is what is causing the bottleneck, according
''#****to a timer test
tempDbValsCase = lcase(trim(arrDbVals(1, iRowLoop)))
''#****
if (mid(trim(arrFormComplete(zRowLoop)),1,8) = trim(arrDbVals(0, iRowLoop))) AND (mid(trim(arrFormComplete(zRowLoop)),9) <> tempDbValsCase) then
dim strFormAllVals
strFormAllVals = arrFormComplete(zRowLoop)
strChange.Append strFormAllVals & ","
end if
next
next
On the database side (MS SQL Server 2008), the table from which the array is derived through GetRows contains the bit datatype column "Complete". The lcase and trim operations are performed upon this column of the array. Does the bit datatype add any hidden characters in the output? Visually, I don't detect any, but when I compare a value of "True" from the form input against a value from the array that looks like "True," it doesn't match, until I run the lcase and trim on the "Complete" column.
Try
dim iRowLoop, iColLoop, zRowLoop, strChange, tempDbValsCase
dim iCount1, iCount2, match
Set strChange = New FastString
iCount1 = ubound(arrDbVals, 2)
iCount2 = ubound(arrFormComplete)
for iRowLoop = 0 to iCount1
for zRowLoop = 0 to iCount2
' Assign array lookup to a variable '
tempDbValsCase = arrDbVals(1, iRowLoop)
' ...and then perform operations on it one at a time '
tempDbValsCase = trim(tempDbValsCase)
tempDbValsCase = lcase(tempDbValsCase)
' Assign this array lookup to a variable and perform trim on it '
match = trim(arrFormComplete(zRowLoop))
if (mid(match,1,8) = trim(arrDbVals(0, iRowLoop))) AND (mid(match,9) <> tempDbValsCase) then
strChange.Append match & ","
end if
next
next

BULK INSERT with inconsistent number of columns

I am trying to load a large amount data in SQL server from a flat file using BULK INSERT. However, my file has varying number of columns, for instance the first row contains 14 and the second contains 4. That is OK, I just want to make a table with the max number of columns and load the file into it with NULLs for the missing columns. I can play with it from that point. But it seems that SQL Server, when reaching the end of the line and having more columns to fill for that same row in the destination table, just moves on to the next line and attempts to put the data on that line to the wrong column of the table.
Is there a way to get the behavior that I am looking for? Is there an option that I can use to specify this? Has anyone run into this before?
Here is the code
BULK INSERT #t
FROM '<path to file>'
WITH
(
DATAFILETYPE = 'char',
KEEPNULLS,
FIELDTERMINATOR = '#'
)
BULK INSERT isn't particularly flexible. One work-around is to load each row of data into an interim table that contains a single big varchar column. Once loaded, you then parse each row using your own routines.
My workaround (tested in T-SQL):
Create table with colum count = minimum column count of your import file
Run bulk insert (it will succeed now)
In last table column, you will find all rest items (including your item separator)
If it is necessery for you, create another full-columned table, copy all columns from first table, and do some parsing only over last column.
Example file
alpha , beta , gamma
one , two , three , four
will look like this in your table:
c1 | c2 | c3
"alpha" | "beta" | "gamma"
"one" | "two" | "three , four"
Another workaround is to preprocess the file. It may be easier to write a small standalone program to add terminators to each line so it can be BULK loaded properly than to parse the lines using T-SQL.
Here's one example in VB6/VBA. It's certainly not as fast as the SQL Server bulk insert, but it just preprocessed 91000 rows in 10 seconds.
Sub ColumnDelimiterPad(FileName As String, OutputFileName As String, ColumnCount As Long, ColumnDelimiter As String, RowDelimiter As String)
Dim FileNum As Long
Dim FileData As String
FileNum = FreeFile()
Open FileName For Binary Access Read Shared As #FileNum
FileData = Space$(LOF(FileNum))
Debug.Print "Reading File " & FileName & "..."
Get #FileNum, , FileData
Close #FileNum
Dim Patt As VBScript_RegExp_55.RegExp
Dim Matches As VBScript_RegExp_55.MatchCollection
Set Patt = New VBScript_RegExp_55.RegExp
Patt.IgnoreCase = True
Patt.Global = True
Patt.MultiLine = True
Patt.Pattern = "[^" & RowDelimiter & "]+"
Debug.Print "Parsing..."
Set Matches = Patt.Execute(FileData)
Dim FileLines() As String
Dim Pos As Long
Dim MissingDelimiters
ReDim FileLines(Matches.Count - 1)
For Pos = 0 To Matches.Count - 1
If (Pos + 1) Mod 10000 = 0 Then Debug.Print Pos + 1
FileLines(Pos) = Matches(Pos).Value
MissingDelimiters = ColumnCount - 1 - Len(FileLines(Pos)) + Len(Replace(FileLines(Pos), ColumnDelimiter, ""))
If MissingDelimiters > 0 Then FileLines(Pos) = FileLines(Pos) & String(MissingDelimiters, ColumnDelimiter)
Next
If (Pos + 1) Mod 10000 <> 0 Then Debug.Print Pos + 1
If Dir(OutputFileName) <> "" Then Kill OutputFileName
Open OutputFileName For Binary Access Write Lock Read Write As #FileNum
Debug.Print "Writing " & OutputFileName & "..."
Put #FileNum, , Join(FileLines, RowDelimiter)
Close #FileNum
Debug.Print "Done."
End Sub
The varying number of columns means it can't be parsed by the bulk insert code.
How does it know the correct number of columns? What if you supply too many?
You'll have to upload it to a table with 4 columns, and split out the rest later (or one big column)
Or pre-process it to generate an equal number of columns.
Try specifying a ROW terminator along with your field terminator.
BULK INSERT #t
FROM '<path to file>'
WITH
(
DATAFILETYPE = 'char',
KEEPNULLS,
FIELDTERMINATOR = '#',
ROWTERMINATOR = '\n' --Or whatever signifies the end of a row in your flatfile.
)
More info on this can be found here:
http://msdn.microsoft.com/en-us/library/ms191485.aspx

Resources