Solving variable variable's names issue in excel - excel

I have a programming issue concerning variable variable's names
I need to make an questionaire in excel where answers to certain questions will either hide or unhide certain rows. I have no idea how to optimize it, although I searched for the solution for quite a while.
Code sample which performs an action on one question
Private Function RowNo(ByVal text1 As String) As Long
Dim f As Range
Set f = Columns(2).Find(text1, Lookat:=xlWhole)
If Not f Is Nothing Then
RowNo = f.Row
Else
RowNo = 0
End If
End Function
Dim QAr As Variant
Dim YtQ1Ar As Variant
Dim YtQ1, rYtQ1 As Long
QAr = Array("Q1")
YtQ1Ar = Array("1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.7.1", "1.7.2", "1.7.23", "1.7.24", "1.8", "1.9", "1.10", "1.11", "1.12", "1.13")
For Q = LBound(QAr) To UBound(QAr)
For YtQ1 = LBound(YtQ1Ar) To UBound(YtQ1Ar)
rYtQ1 = RowNo(YtQ1Ar(YtQ1))
If rYtQ1 > 0 Then
Rows(rYtQ1).Hidden = (UCase(Cells(RowNo("1."), ColAn).Value) <> "TAK")
Else
Debug.Print "'" & YtQ1Ar(YtQ1) & "' was not found!"
End If
Next YtQ1
Next Q
Now, I want to perform similar actions on many different questions.
At first I wanted to create a similar arrays and variables with names
Q1, YtQ1Ar;
Q2, YtQ2Ar
... and so on, but I found out that it is impossible to use a variable variable's names in a loop in VBA.
Can you please help me with an idea how to solve that issue? Or do I have to rewrite the code for each question?

There are several ways of creating 'lists' of variables. Three of the most common are:
Collections, exactly as MacroMan's code - take note of how he declares his variables (use a datatype for each declaration).
Multi-dimensional arrays, you can reference each of the indexes independently. This probably wouldn't suit you as the number of sub-questions might vary for each question but, nevertheless, a snippet of your code might be:
Dim questions(10, 20) As Variant 'where first dimension is question number and second is sub-question item.
questions(0,0)="1.1"
questions(0,1)="1.2"
' etc.
Array of Arrays, you can keep a one-dimensional array for each of your sub-question arrays. This might be more suitable to you, like so:
Dim questions(10) As Variant
questions(0) = Array("1.2", "1.3", "1.4", "1.5") 'etc.
questions(1) = Array("2.2", "2.4", "2.6") 'etc.
Having said that, your code is a touch inefficient because it runs the .Find routine in every iteration of your loop and it will throw an unhandled error if any of the sub-question items don't exist in line: Rows(rYtQ).Hidden = (UCase(Cells(RowNo("1."), ColAn).Value) <> "TAK").
Architecturally, you'd be far better to read all of the relevant rows into some kind of storage (say a Range or Collection) in one routine, and in a second routine, check each question to see if those rows need to be hidden. This will give you greater speed and much more flexibility (e.g. to toggle the hidden/unhidden whenever an answer is changed). Sorry it's such a lengthy answer, but it gives you an idea of how important a planned programme structure is.
In the code below, I've given you an example of this. I've used a Class object to make it more obvious (this might be a bit black belt VBA so you may want to ignore it, but it does make the point clearly). So...
First insert a Class Module (Insert ~> Class Module) and name it cQuestionFields. Then paste this code into it:
Option Explicit
Private mQuestionNumber As Integer
Private mAnswerCell As Range
Private mQuestionRange As Range
Private mUnHiddenKey As String
Private mHideUnhideRows As Range
Public Property Get QuestionNumber() As Integer
QuestionNumber = mQuestionNumber
End Property
Public Function AnswerIsChanged(cell As Range) As Boolean
AnswerIsChanged = Not Intersect(cell, mAnswerCell) Is Nothing
End Function
Public Sub HideOrUnhideRows()
Dim answer As String
answer = UCase(CStr(mAnswerCell.Value2))
mHideUnhideRows.EntireRow.Hidden = (answer <> mUnHiddenKey)
End Sub
Public Function InitialiseQuestion(questionNum As Integer, _
questionColumn As Range, _
answerColumn As Range, _
unhideKey As String) As Boolean
Dim ws As Worksheet
Dim thisQ As String
Dim nextQ As String
Dim startCell As Range
Dim endCell As Range
Dim offsetQtoA As Integer
'Assign the question number
mQuestionNumber = questionNum
'Assign column offset between question and answer
offsetQtoA = answerColumn.Cells(1).Column - _
questionColumn.Cells(1).Column
'Convert question number to string format "n."
thisQ = CStr(questionNum) & "."
nextQ = CStr(questionNum + 1) & "."
'Find cell of this question
Set ws = questionColumn.Worksheet
Set startCell = questionColumn.Cells.Find( _
What:=thisQ, _
After:=questionColumn.Cells(1), _
LookIn:=xlFormulas, _
LookAt:=xlWhole, _
SearchOrder:=xlRows, _
SearchDirection:=xlNext, _
MatchCase:=True)
'Check the question exists
If startCell Is Nothing Then
InitialiseQuestion = False
Exit Function
End If
'Set the answer cell
Set mAnswerCell = startCell.Offset(, offsetQtoA)
'Find the last cell within this question range
Set endCell = questionColumn.Cells.Find( _
What:=nextQ, _
After:=startCell, _
LookIn:=xlFormulas, _
LookAt:=xlWhole, _
SearchOrder:=xlRows, _
SearchDirection:=xlNext, _
MatchCase:=True)
'If nothing is found, set end of column
If endCell Is Nothing Then
Set endCell = ws.Cells(ws.Rows.Count, questionColumn.Column).End(xlUp)
Else
Set endCell = endCell.Offset(-1)
End If
'Define the search range for this question
Set mQuestionRange = ws.Range(startCell, endCell)
'Assign the hiding key
mUnHiddenKey = unhideKey
InitialiseQuestion = True
End Function
Public Sub AssignTargetRows(ParamArray questions() As Variant)
Dim questionItem As Variant
Dim lastCell As Range
Dim foundCell As Range
'Find the relevant cells for each question item
Set lastCell = mQuestionRange.Cells(1)
For Each questionItem In questions
Set foundCell = mQuestionRange.Cells.Find( _
What:=CStr(questionItem), _
After:=lastCell, _
LookIn:=xlFormulas, _
LookAt:=xlWhole, _
SearchOrder:=xlRows, _
SearchDirection:=xlNext, _
MatchCase:=True)
'If the question item exists, add it to our range
If Not foundCell Is Nothing Then
If mHideUnhideRows Is Nothing Then
Set mHideUnhideRows = foundCell
Else
Set mHideUnhideRows = Union(mHideUnhideRows, foundCell)
End If
Set lastCell = foundCell
End If
Next
End Sub
Now in your module, paste the calling codes:
Option Explicit
Private mQuestionBank As Collection
Public Sub Main()
Dim q As cQuestionFields
'Assign all your values for each question
PopulateQuestionBank
'Loop through each question to test for hiding
For Each q In mQuestionBank
q.HideOrUnhideRows
Next
End Sub
Public Sub ActIfAnswerChanged(Target As Range)
Dim cell As Range
Dim q As cQuestionFields
' Loop through cells in target to see if they are answer cells
For Each cell In Target.Cells
For Each q In mQuestionBank
If q.AnswerIsChanged(cell) Then q.HideOrUnhideRows
Next
Next
End Sub
Public Sub PopulateQuestionBank()
Dim ws As Worksheet
Dim q As cQuestionFields
Dim validQ As Boolean
Set mQuestionBank = New Collection
'Assign the worksheet holding the question.
'You can change this whenever any of your question are on a different sheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
'Question 1: note change question and answer columns to yours.
Set q = New cQuestionFields
validQ = q.InitialiseQuestion(questionNum:=1, _
questionColumn:=ws.Columns(2), _
answerColumn:=ws.Columns(4), _
unhideKey:="TAK")
If validQ Then
q.AssignTargetRows "1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.7.1", "1.7.2", "1.7.23", "1.7.24", "1.8", "1.9", "1.10", "1.11", "1.12", "1.13"
mQuestionBank.Add q, Key:=CStr(q.QuestionNumber)
End If
'Question 2
Set q = New cQuestionFields
validQ = q.InitialiseQuestion(questionNum:=2, _
questionColumn:=ws.Columns(2), _
answerColumn:=ws.Columns(4), _
unhideKey:="TAK")
If validQ Then
q.AssignTargetRows "2.2", "2.3", "2.4", "2.5", "2.6"
mQuestionBank.Add q, Key:=CStr(q.QuestionNumber)
End If
'Question 3
Set q = New cQuestionFields
validQ = q.InitialiseQuestion(questionNum:=3, _
questionColumn:=ws.Columns(2), _
answerColumn:=ws.Columns(4), _
unhideKey:="TAK")
If validQ Then
q.AssignTargetRows "3.7", "3.7.3", "3.7.2", "3.7.23", "3.7.24"
mQuestionBank.Add q, Key:=CStr(q.QuestionNumber)
End If
End Sub
You'll see that I've added a routine called ActIfAnswerChanged. This is what I mean by added flexibility. If you post the following code in your Worksheet_Change event (double click your question sheet in your VBA editor and select this event), then it will run hide/unhide the rows whenever an answer is changed.
Private Sub Worksheet_Change(ByVal Target As Range)
ActIfAnswerChanged Target
End Sub

Try something like:
Dim QColl As Collection
Dim Q As Long
Dim YtQAr As Variant
Dim YtQ As Long, rYtQ As Long
Set QColl = New Collection
QColl.Add Array("1.2", "1.3", "1.4", "1.5"), Key:="Q1"
QColl.Add Array("2.2", "2.3"), Key:="Q2"
For Q = 1 To QColl.Count
YtQAr = QColl.Item("Q" & Q)
For YtQ = LBound(YtQAr) To UBound(YtQAr)
rYtQ = RowNo(YtQAr(YtQ))
If rYtQ > 0 Then
Rows(rYtQ).Hidden = (UCase(Cells(RowNo("1."), ColAn).Value) <> "TAK")
Else
Debug.Print "'" & YtQAr(YtQ) & "' was not found!"
End If
Next YtQ
Next Q

Related

Excel VBA Function to determine row number

I built a function to determine the first row in which data exists. When I call the data i keep getting an error stating object required. How do I get around this error and is this the best way to accomplish my goal? TYIA!
Sub rename()
Dim strOldType As String
Dim correctrow As Long
Dim a As Range
Set a = startrow(correctrow)
Range("s" & a).Select
strOldType = Selection.Value
End Sub
Function startrow(firstroww)
Dim strRow As String
Dim firstrow As Range
Range("ab1").Select
strRow = Selection.Value
If strRow <> "" Then
firstroww = 1
Else
Range("ab1").Activate
Selection.End(xlDown).Select
firstroww = ActiveCell.Row()
End If
End Function
You can try a function like this. You will need to pass a range into the function as seen below in Sub Test().
The custom function can find the first used cell below from any starting point.
Option Explicit
Function FR(Start As Range) As Long
Select Case Start
Case <> ""
FR = Start.Row
Case Else
FR = Start.End(xlDown).Row
End Select
End Function
Sub Test()
MsgBox FR(Range("A1"))
End Sub
Don't try to use Select inside a function.
Use Set to assign a range object. Do not use Set to assign a number to a variable.
Make up your mind whether you want a row number or a range object. You bounce back and forth between the two with no regard for result.
Corrected code:
Sub rename()
Dim strOldType As String
Dim correctrow As Long
Dim a As LONG '<~~ correction
correctrow = 1 '<~~ correction
a = startrow(correctrow) '<~~ correction
strOldType = Range("s" & a).Value
End Sub
Function startrow(firstroww)
if Range("ab" & firstrow) <> "" then '<~~ correction
startrow = firstrow
else
startrow = Range("ab" & firstrow).end(xldown).row
end if
End Function
First Cell in Column Function
It is assumed that you're looking for a VBA function to use in Excel to calculate the first non-empty row of a column (specified by a range).
Features
The Volatile method marks a user-defined function as volatile. A
volatile function must be recalculated whenever calculation occurs in
any cells on the worksheet. A nonvolatile function is recalculated
only when the input variables change (VBA Help).
At least for the sake of correctness, you have to use IsEmpty
instead of "" for the reason e.g. if the cell in the resulting row
contains a formula that evaluates to "", it will be ignored.
The Find Method Version uses the Find method to calculate the First Row, which is safer than
the End Version e.g. if you input a value into the first cell of the
column i.e. the result is 1 and you hide the first row, the result of
the End Version will not be 1.
The formula can be inserted in the same column as SelectRange
Column. In some cases the End Version would not show the correct
result or create a circular reference. Therefore ThisCell is used
in the End version and 0 is returned if no value was found in SelectRange column.
Find Method Version
Function FirstRowFind(SelectRange As Range) As Long
Application.Volatile
Dim FirstCell As Range
With Columns(SelectRange.Column)
Set FirstCell = .Find("*", .Cells(.Cells.Count), -4123, 1, 2, 1)
End With
If Not FirstCell Is Nothing Then
FirstRowFind = FirstCell.Row
End If
End Function
Find Method
Instead of
Set FirstCell = .Find("*", .Cells(.Cells.Count), -4123, 1, 2, 1)
you can use
Set FirstCell = .Find("*", .Cells(.Cells.Count), _
xlFormulas, xlWhole, xlByColumns, xlNext)
or
Set FirstCell = .Find(What:="*", After:=.Cells(.Cells.Count), _
LookIn:=xlFormulas, LookAt:=xlWhole, _
SearchOrder:=xlByColumns, SearchDirection:=xlNext)
The parameters for the arguments LookAt(unimportant in this case) and SearchDirection(Default is Next) can be omitted, but since I couldn't find any difference in efficiency, I didn't.
Usage in Excel
For Column AB:
=FirstRowFind(AB1)
=FirstRowFind(AB20)
=FirstRowFind(AB17:AH234)
End Version (Not recommended)
Function FirstRowEnd(SelectRange As Range) As Long
Application.Volatile
Dim FirstCell As Range
If Application.ThisCell.Column = SelectRange.Column Then Exit Function
If Not IsEmpty(SelectRange.Cells(1)) Then
FirstRowEnd = 1
Else
Set FirstCell = Cells(1, SelectRange.Column).End(xlDown)
FirstRowEnd = FirstCell.Row
If FirstRowEnd = Rows.Count And IsEmpty(FirstCell) Then
FirstRowEnd = 0
End If
End If
End Function
Usage in Excel
For Column AB:
=FirstRowEnd(AB1)
=FirstRowEnd(AB20)
=FirstRowEnd(AB17:AH234)
You can use this:
ActiveSheet.UsedRange.Row
ActiveSheet.UsedRange.Column
The UsedRange object is the largest rectangle covering all nonempty cells.

Cant paste - Excel VBA

I am working on a code that will automate a process. I want it to copy from various files to other files with formulas, calculate, and then back again.
I have encountered, a message 'Run-time error '1004', the pastespecial method of range class failed' , when tried to paste. The message appears ONLY when I am using variables to declare the first cell, in order to copy a range of values.
When I use a direct cell description everything works fine.
I'm also using a custom function for obtaining the column letter, of a given field name.
Function ActiveColumnName(fieldname As String, fieldnames_line As Integer) As String
Range("A" & fieldnames_line & ":AB" & fieldnames_line).NumberFormat = "#"
Cells.find(What:=fieldname, After:=ActiveCell, LookIn:=xlFormulas, LookAt _
:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:= _
False, SearchFormat:=False).Activate
ActiveColumnNumber = ActiveCell.Column
Dim m As Integer
Dim ActiveColumnName As String
ActiveColumnName = ""
Do While (ActiveColumnNumber > 0)
m = (ActiveColumnNumber - 1) Mod 26
ActiveColumnName = Chr(65 + m) + ActiveColumnName
ActiveColumnNumber = Int((ActiveColumnNumber - m) / 26)
Loop
End Function
sub main ()
Dim firstrow_data_main As Integer
Dim firstrow_fieldnames_main As Integer
firstrow_data_main = 16
firstrow_fieldnames_main = 15
Range(ActiveColumnName("<FIELDNAME>", firstrow_fieldnames_main) & firstrow_data_main, Range(ActiveColumnName("ÄÅÔÅ", firstrow_fieldnames_main) & Rows.Count).End(xlUp).Offset(-1)).Select
Application.CutCopyMode = False
Selection.Copy
Workbooks.Open help_file '"help_file" is any given .xls path with formulas
Dim firstrow_data_help As Integer
Dim firstrow_fieldnames_help As Integer
firstrow_data_help = 7
firstrow_fieldnames_help = 4
'NOW WHEN I USE THIS, DOESN'T WORK:
-> Range(ActiveColumnName("<FIELDNAME>", firstrow_fieldnames_help) & firstrow_data_help).Select
'WHEN I USE THIS, WORKS FINE:
-> Range("L7").Select
Selection.PasteSpecial Paste:=xlPasteValues, Operation:=xlNone, SkipBlanks:=False, Transpose:=False
Application.CutCopyMode = False
End Sub
When it doesn't work, it opens the .xls, and the desirable cell is indeed selected, but no pate. I understand that has something to do with the clipboard, but I cannot figure it out. Any suggestions?
Remove all the Select and Activate by referring to the cells directly, See HERE for more information.
Look into Cells() instead of Range and avoid the whole need to convert column numbers to letters, as Cells() uses numbers.
Avoid the Clipboard when values are the only thing you want and simply assign the value to the new cells (This will require that both ranges are the same size, so use Resize())
Always denote the parent sheet of the range, it will cut down on the errors.
Code refactored
Sub main()
Dim firstrow_data_main As Integer
Dim firstrow_fieldnames_main As Integer
Dim rng As Range
Dim tWb As Workbook
Dim ws As Worksheet
Dim tWs As Worksheet
Dim firstrow_data_help As Integer
Dim firstrow_fieldnames_help As Integer
Set ws = ThisWorkbook.ActiveSheet
Set tWb = Workbooks.Open(help_file)
Set tWs = tWb.ActiveSheet
firstrow_data_main = 16
firstrow_fieldnames_main = 15
firstrow_data_help = 7
firstrow_fieldnames_help = 4
With ws
Set rng = .Range(.Cells(firstrow_data_main, firstrow_fieldnames_main), .Cells(.Rows.Count, firstrow_fieldnames_main).End(xlUp).Offset(-1))
tWs.Cells(firstrow_data_help, firstrow_fieldnames_help).Resize(rng.Rows.Count, rng.Columns.Count).Value = rng.Value
End With
End Sub
I think the problem may be here:
ActiveColumnName("<FIELDNAME>", firstrow_fieldnames_main)
This implies that ActiveColumnName is a matrix with n x (1 to 2) dimension. If you want to concatenate a name to a variable, you have to use (example):
"YourStringHere" & YourVariableHere & "AnotherString"
Which in your case would be:
ActiveColumnName("<FIELDNAME>" & firstrow_fieldnames_main)
So if I correctly understood (<FIELDNAME> is a bit obscure), the whole command should be:
Range(ActiveColumnName("<FIELDNAME>" & firstrow_fieldnames_help) & "," & firstrow_data_help).Select
First of all, you should compile your VBA before running. The VBA compiler caught this off the bat:
Dim ActiveColumnName As String
is unnecessary because you assigned ActiveColumnName As String when you defined the function in line 1.
You use a lot of references to active cells and selecting cells. This is known to cause runtime errors. See this post: "How to avoid using Select in Excel Vba Macros".
I suspect the fieldname is not where you think it should be in your help_file, i.e. it isn't in Row 4. This would mean the code wouldn't know where to paste the data. In general, the best way to debug is to piece the code into the smallest action possible to see what's causing the error (see SpreadSheet Guru Strategies). Can you run the following code to see what the output is?
Function ActiveColumnName(fieldname As String, fieldnames_line As Integer) As String
Range("A" & fieldnames_line & ":AB" & fieldnames_line).NumberFormat = "#"
Cells.Find(What:=fieldname, After:=ActiveCell, LookIn:=xlFormulas, LookAt _
:=xlWhole, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:= _
False, SearchFormat:=False).Activate
ActiveColumnNumber = ActiveCell.Column
Dim m As Integer
ActiveColumnName = ""
Do While (ActiveColumnNumber > 0)
m = (ActiveColumnNumber - 1) Mod 26
ActiveColumnName = Chr(65 + m) + ActiveColumnName
ActiveColumnNumber = Int((ActiveColumnNumber - m) / 26)
Loop
End Function
Sub main()
Workbooks.Open "help_file" '"help_file" is any given .xls path with formulas
Dim firstrow_data_help As Integer
Dim firstrow_fieldnames_help As Integer
firstrow_data_help = 7
firstrow_fieldnames_help = 4
MessageBox = ActiveColumnName("FIELDNAME", firstrow_fieldnames_help) & firstrow_data_help
End Sub
thank you all for your response! I've tried all of your suggestions one by one but encountered various issues along the way. Nevertheless all of your suggestions helped me to grow a different perspective on the subject. The solution I've end up with, derives from your suggestion to discard ".select" as a way of reference and to use "rng" variables and of course to get rid of the double reference "ActiveColumnName".I know i have a long way to go but for the moment the this thing works!!thanks!!
Sub main()
Dim firstrow_data_main As Integer
Dim firstrow_fieldnames_main As Integer
Dim firstrow_data_help As Integer
Dim firstrow_fieldnames_help As Integer
firstrow_data_main = 16
firstrow_fieldnames_main = 15
firstrow_data_help = 7
firstrow_fieldnames_help = 4
Dim rng1 As Range
Dim rng2 As Range
Set rng1 = Range(ActiveColumnName("<FIELDNAME>", firstrow_fieldnames_main) & firstrow_data_main, Range(ActiveColumnName("<FIELDNAME>", firstrow_fieldnames_main) & Rows.Count).End(xlUp).Offset(-1))
cells_selected = rng1.Rows.Count
Workbooks.Open <help_file>
Set rng2 = Range(ActiveColumnName("<FIELDNAME>", firstrow_fieldnames_help) & firstrow_data_help, Range(ActiveColumnName("<FIELDNAME>", firstrow_fieldnames_help) & cells_selected + firstrow_data_help - 1))
rng1.Copy rng2
End Sub

How to search and replace using VBA in Excel?

I am new to VBA Excel programming. Consider an Excel sheet with nxn values. My task is to search for text called "TOOTHBRUSH BATT" from A column. A column consists of multiple "TOOTHBRUSH " value.
Once the value is found suppose in cell A11 then I need to change text in D11 ie corresponding D column to "BATTERY". D11 will already have some text, I need to replace that text with "BATTERY"
My code is
Sub replacement()
Dim S As String
Dim H As String
S = "TOOTHBRUSH BATT"
For i = 1 To Range("A1").End(xlDown).Row
If Range("A" & i) = S Then
Range("D" & i) = "BATTERY"
End If
Next i
End Sub
nRow = Worksheets(1).Range("A:A").Find(What:="*TOOTHBRUSH BATT*", after:=Range("A1"), LookIn:=xlFormulas, LookAt _
:=xlWhole, SearchOrder:=xlByRows, SearchDirection:=xlNext, MatchCase:= _
False, SearchFormat:=False).Row
Worksheets(1).Cells(nRow,"D") = "BATTERY"
By using auto filter (below code not tested)
Worksheets(1).autofiltermode = false
Worksheets(1).Range("A:B").autofilter
Worksheets(1).AutoFilter.Range.AutoFilter Field:=1, Criteria1:="*TOOTHBRUSH BATT*"
dim nRng as range
If Worksheets(1).AutoFilter.Range.Offset(1,0).Columns(1).SpecialCells(xlCellTypeVisible).Count > 1 Then
set nRng = Worksheets(1).AutoFilter.Range.Offset(1,0).Columns(2).Resize(Worksheets(1).AutoFilter.Range.Rows.Count - 1, 1).SpecialCells(xlCellTypeVisible)
nRng.value = "BATTERY"
End If
This is Similar to Eric's Answer.
' Declare range to set to the first cell we find
Dim find as Range
set find = Range("A:A").Cells.Find(What:="TOOTHBRUSH BATT")
' This is the cell Address (in case it keeps looping back to beginning)
Dim addy as string
if not find is nothing then addy = find.address
' If we've found a cell then Keep Do something with it
Do while not find is nothing
find.Value = "BATTERY"
' Find the next Cell
set find = Range("A:A").Cells.Find(What:="TOOTHBRUSH BATT", After:= find)
' If the next found cell is the first one then exit sub/function
if find.address = addy then exit sub
Loop

Split one column into multiple columns

I was wondering if anybody can kindly advise how to split a string with comma-separated values into multiple columns. I have been trying to figure this out but have been having a hard time finding a good solution. (also checked online, seems several that comes close but not necessarily fit what I exactly need)
Let's say I have a worksheet, call it "example", for instance,
and in the worksheet has the following strings under multiple
rows but all in column "A".
20120112,aaa,bbb,ccc,3432
20120113,aaa,bbb,ccc
20120113,ddd,bb,ccc,ddd,eee,fff,ggg,hhhh
20120132,aaa,bbb,ccc
20120112,aaa,bbb,ccc
20120112,xxx,bbb,ggg,ggg,333
20120112,aaa,bbb,ccc
20120112,abbd,bbb,ccc
How can I create a macro that will split the above into multiple columns.
Just several points
(1) I should be able to specify the worksheet name
ex: something like
worksheets("example").range(A,A) '
(2) The number of columns and rows are not fixed, and so I do not
know how many comma-separated values and how many rows there
would be before I run the vba script.
You could use InputBox() function and get the name of the sheet with data which shlould be splitted.
Then copy the data into variant array, split them and create new array of splitted values.
Finally assign the array of splitted values back to excel range. HTH
(Notice that the source data are modified directly so finally it is separated into columns and original un-splitted state is lost. But it is possible to modify the code so the original data won't be overwritten.)
Option Explicit
Private Const sourceColumnName As String = "A"
Private Const delimiter As String = ","
Public Sub Splitter()
' splits one column into multiple columns
Dim sourceSheetName As String
Dim sourceSheet As Worksheet
Dim lastRow As Long
Dim uboundMax As Integer
Dim result
On Error GoTo SplitterErr
sourceSheetName = VBA.InputBox("Enter name of the worksheet:")
If sourceSheetName = "" Then _
Exit Sub
Set sourceSheet = Worksheets(sourceSheetName)
With sourceSheet
lastRow = .Range(sourceColumnName & .rows.Count).End(xlUp).row
result = SplittedValues(data:=.Range(.Cells(1, sourceColumnName), _
.Cells(lastRow, sourceColumnName)), _
partsMaxLenght:=uboundMax)
If Not IsEmpty(result) Then
.Range(.Cells(1, sourceColumnName), _
.Cells(lastRow, uboundMax)).value = result
End If
End With
SplitterErr:
If Err.Number <> 0 Then _
MsgBox Err.Description, vbCritical
End Sub
Private Function SplittedValues( _
data As Range, _
ByRef partsMaxLenght As Integer) As Variant
Dim r As Integer
Dim parts As Variant
Dim values As Variant
Dim value As Variant
Dim splitted As Variant
If Not IsArray(data) Then
' data consists of one cell only
ReDim values(1 To 1, 1 To 1)
values(1, 1) = data.value
Else
values = data.value
End If
ReDim splitted(LBound(values) To UBound(values))
For r = LBound(values) To UBound(values)
value = values(r, 1)
If IsEmpty(value) Then
GoTo continue
End If
' Split always returns zero based array so parts is zero based array
parts = VBA.Split(value, delimiter)
splitted(r) = parts
If UBound(parts) + 1 > partsMaxLenght Then
partsMaxLenght = UBound(parts) + 1
End If
continue:
Next r
If partsMaxLenght = 0 Then
Exit Function
End If
Dim matrix As Variant
Dim c As Integer
ReDim matrix(LBound(splitted) To UBound(splitted), _
LBound(splitted) To partsMaxLenght)
For r = LBound(splitted) To UBound(splitted)
parts = splitted(r)
For c = 0 To UBound(parts)
matrix(r, c + 1) = parts(c)
Next c
Next r
SplittedValues = matrix
End Function
If you don't need to work on this task later again, here is a manual way as workaround:
Use a text editor (Notepad++) to replace "," to "tab".
Copy the content and paste into an empty Excel sheet.
Or you can try Excel import the data from file ("," as separator).
In case you need an automatic script, try this:
1) Press Ctrl+F11 to open VBA editor, insert a Module.
2) click the Module, add code inside as below.
Option Explicit
Public Function LastRowWithData(ByRef sht As Excel.Worksheet, Optional colName As String = "A") As Long
LastRowWithData = sht.Range(colName & sht.Rows.Count).End(xlUp).Row
End Function
Sub SplitToColumns(ByRef sColNames As String, ByRef strSeparator As String, ByRef rngDest As Excel.Range)
Dim arrColNames As Variant, i As Long
arrColNames = Split(sColNames, strSeparator)
For i = LBound(arrColNames) To UBound(arrColNames)
rngDest.Offset(0, i).Value = arrColNames(i)
Next i
End Sub
Sub PerformTheSplit()
Dim totalRows As Long, i As Long, sColNames As String
totalRows = LastRowWithData(Sheet1, "A")
For i = 1 To totalRows
sColNames = Sheet1.Range("A" & i).Value
Call SplitToColumns(sColNames, ",", Sheet2.Range("A" & i))
Next i
End Sub
3) Suppose you have the column name in Sheet1:
Press "Alt+F8" to run macro "PerformTheSplit", you will see result in Sheet2:
I would just use the Text-to-Columns wizard, with VBA routines to allow you to select the sheet and range to process, as you request above.
The Input boxes are used to obtain the sheet and range to process, and will default to the Active Sheet and Selection. This could certainly be modified in a variety of ways.
The built-in text to columns feature is then called, and, although you did not so specify, ti seems your first column represents a date in YMD format, so I added that as an option -- it should be obvious how to remove or change it if required.
Let me know how it works for you:
Option Explicit
Sub TTC_SelectWS_SelectR()
Dim WS As Worksheet, R As Range
Dim sMB As String
Dim v
On Error Resume Next
Set WS = Worksheets(Application.InputBox(Prompt:="Enter Worksheet Name: ", _
Title:="Select Worksheet", _
Default:=ActiveSheet.Name, _
Type:=2))
If Err.Number <> 0 Then
sMB = MsgBox("Invalid Worksheet Name", vbRetryCancel)
If sMB = vbRetry Then TTC_SelectWS_SelectR
Exit Sub
End If
On Error GoTo 0
Set R = (Application.InputBox(Prompt:="Select Range to Process: ", _
Title:="Select Range", _
Default:=Selection.Address, _
Type:=8))
Set R = WS.Range(R.Address)
R.TextToColumns DataType:=xlDelimited, textqualifier:=xlTextQualifierDoubleQuote, _
consecutivedelimiter:=False, Tab:=False, semicolon:=False, comma:=True, Space:=False, _
other:=False, fieldinfo:=Array(Array(1, xlYMDFormat))
End Sub

Find cells that have common values in vba

I am attempting to write a macro that will loop over a column and take each cell and find all the other cells that are approximate matches and move them to another spread sheet. I thought of using the find method but I am unsure how to implement it for this. I have pasted what I have done so far, which isn't much. I am rather new to vba so any help would be greatly appreciated.
Sub Extract()
Dim i As Long, count As Long, rng1 As Range
Set rng1 = Sheet1.Range(Range("N1"), Range("N1").End(xlDown))
count = 2
For i = 1 To Sheet1.Range(Range("N1"), Range("N1").End(xlDown)).Rows.count
Sheet1.Cells(count, 14).Select
count = count + 1
Next i
End Sub
This is a bare-bones solution to get you going. Bare-bones because things like search string, search column, worksheets etc. are hard coded. The 'matches' are placed in a worksheet called 'Matches' in the same 'position' as the 'Data' sheet (Col A) from which they have been extracted.
Sub findlikes()
Dim wsDat As Worksheet, wsMat As Worksheet
Dim strSearch As String, firstAdd As String
Dim fndCell As Range
Dim srchCol As Long, numFnd As Long
Set wsDat = Sheets("Data")
Set wsMat = Sheets("Matches")
srchCol = 1 'Col A
strSearch = "Alka-Seltzer"
Set fndCell = wsDat.Columns(srchCol).Find(What:=strSearch, LookIn:=xlValues, _
LookAt:=xlPart, SearchOrder:=xlByRows, SearchDirection:=xlNext, _
MatchCase:=False, SearchFormat:=False)
If Not fndCell Is Nothing Then
firstAdd = fndCell.Address
numFnd = 1
Do
wsMat.Range(fndCell.Address).Value = fndCell.Value
Set fndCell = wsDat.Columns(srchCol).FindNext(fndCell)
numFnd = numFnd + 1
Loop While Not fndCell Is Nothing And fndCell.Address <> firstAdd
Else
MsgBox "Search String Not Found"
End If
End Sub
This approach used the Find (and FindNext) methods that you mentioned in your original post.
Further references to these can be found here and here.

Resources