Im using VBA to program a function in excel that will search a list looking for certain names, count when certain sought for names come up and then output these counter values to individual cells.
How do I allocate the values to the function itself when I have a multi cell function? Ive chosen 4 cells next to each other in the same column and pressed CTRL-SHFT-ENTER to get a multi cell function I just dont know how to allocate results to the function so that it will show in the selected cells. What I've done so far is shown below:
Function ROM(ByVal lookup_value As Range, _
ByVal lookup_column As Range, _
ByVal return_value_column As Long) As String
Application.ScreenUpdating = False
Dim i As Long
Dim resultCount As Long
Dim resultsArray() As String
Dim arraySize As Long
Dim myrange As Range
Dim results As String
Dim TSS As Long
Dim OSS As Long
Dim AWS As Long
Dim JLI As Long
Dim answers(1 To 3, 1 To 1) As Variant
' The following code works out how many matches there are for the lookup and creates an
' array of the same size to hold these results
Set myrange = lookup_column
arraySize = Application.WorksheetFunction.CountIf(myrange, lookup_value.Value)
ReDim resultsArray(arraySize - 1)
' A counter for the results
resultCount = 0
TSS = 0
OSS = 0
AWS = 0
JLI = 0
' The equipment ID column is looped through and for every match the corresponding Equipment Type is
' saved into the resultsArray for analysis
For i = 1 To lookup_column.Rows.count
If Len(lookup_column(i, 1).Text) <> 0 Then
If lookup_column(i, 1).Text = lookup_value.Value Then
' If statement to ensure that the function doesnt cycle to a number larger than the
' size of resultsArray
If (resultCount < (arraySize)) Then
resultsArray(resultCount) = (lookup_column(i).Offset(0, return_value_column).Text)
results = (lookup_column(i).Offset(0, return_value_column).Text)
resultCount = resultCount + 1
' The following code compares the string to preset values and increments
' the counters if any are found in the string
If (InStr(results, "TPWS TSS") > 0) Then
TSS = TSS + 1
ElseIf (InStr(results, "TPWS OSS")) Then
OSS = OSS + 1
ElseIf (InStr(results, "JUNCTION INDICATOR (1 Route)") > 0) Then
JLI = JLI + 1
ElseIf (InStr(results, "AWS")) Then
AWS = AWS + 1
End If
End If
End If
End If
Next
answers(1, 1) = TSS
answers(1, 2) = OSS
answers(1, 3) = AWS
answers(1, 4) = 0
ROM = answers
Application.ScreenUpdating = True
End Function
When I try running the function it keeps saying type mismatch for answers. The cells selected for the multi cell formula are F18, G18, H18 and I18.
To return array functions from VBA
your function must be of type Variant
your output array must match the selected range - in your case it must be 1-dimensional
whereas you are dimensioning a 2-dimensional array
Try this
Function MyArray() As Variant
Dim Tmp(3) As Variant
Tmp(0) = 1
Tmp(1) = "XYZ"
Tmp(2) = 3
Tmp(3) = 4
MyArray = Tmp
End Function
Now select F18..I18, enter =MyArray() and press Ctrl+Shift+Enter
Hope this helps.
This may vary depending on the version of Excel you are using. I am using the Office2003 suite and the solutions presented above do not work with this version of Excel.
I find that you need a two diminsion array output to Excel with the values in the second diminsion.
I'll borrow MikeD's example above and modify it to work in Excel2003.
Function MyArray() As Variant
Dim Tmp() As Variant
redim Tmp(3,0) as Variant
Tmp(0,0) = 1
Tmp(1,0) = "XYZ"
Tmp(2,0) = 3
Tmp(3,0) = 4
MyArray = Tmp
End Function
Note that you can re-diminsion your array to use a dynamic output, but you must select a large enough range to encompass all of your output when you insert the function into Excel.
First, you're getting the type mismatch because you're trying to assign the result to a String. If you assign to a Variant you'll avoid that problem.
Second, your answers array should be dimensioned as:
Dim answers(3) As Variant
The following code should work for you if I've understood the problem correctly.
Function ROM(ByVal lookup_value As Range, _
ByVal lookup_column As Range, _
ByVal return_value_column As Long) As Variant
Application.ScreenUpdating = False
Dim i As Long
Dim resultCount As Long
Dim resultsArray() As String
Dim arraySize As Long
Dim myrange As Range
Dim results As String
Dim TSS As Long
Dim OSS As Long
Dim AWS As Long
Dim JLI As Long
Dim answers(3) As Variant
' The following code works out how many matches there are for the lookup and creates an
' array of the same size to hold these results
Set myrange = lookup_column
arraySize = Application.WorksheetFunction.CountIf(myrange, lookup_value.Value)
ReDim resultsArray(arraySize - 1)
' A counter for the results
resultCount = 0
TSS = 0
OSS = 0
AWS = 0
JLI = 0
' The equipment ID column is looped through and for every match the corresponding Equipment Type is
' saved into the resultsArray for analysis
For i = 1 To lookup_column.Rows.Count
If Len(lookup_column(i, 1).Text) <> 0 Then
If lookup_column(i, 1).Text = lookup_value.Value Then
' If statement to ensure that the function doesnt cycle to a number larger than the
' size of resultsArray
If (resultCount < (arraySize)) Then
resultsArray(resultCount) = (lookup_column(i).Offset(0, return_value_column).Text)
results = (lookup_column(i).Offset(0, return_value_column).Text)
resultCount = resultCount + 1
' The following code compares the string to preset values and increments
' the counters if any are found in the string
If (InStr(results, "TPWS TSS") > 0) Then
TSS = TSS + 1
ElseIf (InStr(results, "TPWS OSS")) Then
OSS = OSS + 1
ElseIf (InStr(results, "JUNCTION INDICATOR (1 Route)") > 0) Then
JLI = JLI + 1
ElseIf (InStr(results, "AWS")) Then
AWS = AWS + 1
End If
End If
End If
End If
Next
answers(0) = TSS
answers(1) = OSS
answers(2) = AWS
answers(3) = 0
ROM = answers
Application.ScreenUpdating = True
End Function
Related
Please help.
I'm trying to create a user defined function that will use an array as multiple criteria (Pedigree) to check for corresponding parents (Parent) and then sum their respective ranges (Sumrange).
I've managed to create code that will check if a parent is in the pedigree range which will then return a result of 1 or 0. This will not return true values if blanks verify blanks. I intend to create an array with these 1's and 0's to then SumProduct it with my Sumrange. My problem is that I am unable create an array of these 1's and 0's and SumProduct them with the Sumrange without returning a #value.
This below code doesn't include the SumProduct portion but just returns 1 or 0 based on the criteria.
Function ProdIfs(Parent As Range, Pedigree As Range, Sumrange As Range) As Long
Application.Volatile
Dim i As Variant
Dim j As Variant
Dim result As Integer
result = 0
For Each i In Parent
For Each j In Pedigree
If i.Value = "" Or j.Value = "" Then
result = result
ElseIf i.Value = j.Value Then
result = 1: GoTo NextIteration
End If
Next j
NextIteration:
Next i
ProdIf = result
End Function
Thanks for you help.
Thanks to Super Symmetry for getting this 99% of the way there.
Since the original code returned a 1 or 0, I changed the code to provide a sumproduct.
Also I've made the PedigreeRange loop through columns instead of rows to fit the way my Pedigree data is.
Function ProdIfs(Parent As Range, Pedigree As Range, Sumrange As Range) As Variant
Application.Volatile
Dim i As Long
Dim j As Long
Dim result() As Variant
ReDim result(1, 1 To Parent.Rows.Count)
Dim x As Long
For i = 1 To Parent.Rows.Count
x = 0
result(1, i) = x
For j = 1 To Pedigree.Columns.Count
If Parent.Cells(i, 1).Value <> "" And Pedigree.Cells(1, j) <> "" And Parent.Cells(i, 1) = Pedigree.Cells(1, j) Then
x = 1
Exit For
End If
Next j
result(1, i) = x * Sumrange(i, 1).Value
Next i
ProdIfs = WorksheetFunction.Sum(result)
End Function
Thanks again. If there are any improvements that can be made to this please let me know.
Answer changed following comments
If you want to return an array, you actually have to create and populate an array in your function and make sure the return type is Variant.
Try this
Function ProdIfs(Parent As Range, Pedigree As Range, Sumrange As Range) As Variant
Application.Volatile
Dim i As Long
Dim j As Long
Dim result() As Integer ' The return value must be an array
ReDim result(1 To Parent.Rows.Count, 1 To 1) ' Assuming Parent is 1 column
For i = 1 To Parent.Rows.Count
result(i, 1) = 0 ' set to 0 by default but always good to do it explicitly
For j = 1 To Pedigree.Rows.Count
If Parent.Cells(i, 1).Value <> "" And Parent.Cells(i, 1) = Pedigree.Cells(j, 1) Then
result(i, 1) = 1
Exit For
End If
Next j
Next i
ProdIfs = result
End Function
Edit: following your answer
You just need to keep a running sum.
To make your code run faster you should read values of these ranges and process them in memory. (It is much faster than asking excel for values in cells).
The return value should be a Double
This feels like a SumIfs ranther than a ProdIfs
Function ProdIfs(Parent As Range, Pedigree As Range, Sumrange As Range) As Double
Application.Volatile
Dim i As Long
Dim v As Variant
Dim vParent As Variant: vParent = Parent.Value
Dim vPedigree As Variant: vPedigree = Pedigree.Value
Dim vSumRange As Variant: vSumRange = Sumrange.Value
ProdIfs = 0
For i = 1 To UBound(vParent, 1)
For Each v In vPedigree
If len(v) > 0 And v = vParent(i, 1) Then
ProdIfs = ProdIfs + vSumRange(i, 1)
Exit For
End If
Next v
Next i
End Function
I am new to VBA and I need to expand an existing Worksheet and keep its formatting. There are 7 sections with variable length (in rows) and a width of 14 columns that need to be completed. So what I am trying to do is the following:
find the lines where the sections start
select the data out of each section and save it into an array (i thought about this as an array of length 7 and each entry contains a 2-dim. array with the data in it)
select my new data and expand the existing array (created in the last step) with that new data
override the current sheet with my new created array
add formatting
I managed to do step 1 and am currently struggling at step 2: I need to create an array with variable length where I can insert the data.
My code so far:
' this should create the array with the 7 entries
' "myArray" contains the row-numbers where the sections start
Function GenerateSheetArray(sheet As Worksheet, myArray As Variant) As Variant
Dim finalArray As Variant
Dim myInt As Integer
'here each entry should be filled
For i = 0 To 6
myInt = myArray(i)
finalArray(i) = GenerateArrayPart(sheet, myInt)
Next
GenerateSheetArray = finalArray
End Function
'This should fill each entry with the data of corresponding section
Function GenerateArrayPart(sheet As Worksheet, headline As Integer) As Variant
Dim leftIndex As Integer, rightIndex As Integer, rowcount As Integer
Dim sheetArray() As Variant
rowcount = 0
leftIndex = 1
rightIndex = 14
i = headline + 1
Do While sheet.Cells(i, 1) <> ""
rowcount = rowcount + 1
i = i + 1
Loop
If (rowcount > 0) Then
For colIndex = leftIndex To rightIndex
For rowIndex = 1 To rowcount
Row = headline + rowIndex
sheetArray(rowIndex - 1, colIndex - 1) = sheet.Cells(Row, colIndex)
Next
Next
End If
GenerateArrayPart = sheetArray
End Function
Now my problem is, that VBA throws an error at this line:
'atm rowIndex and colIndex are 1, Row is 40
'I know that there is data in that cell
sheetArray(rowIndex - 1, colIndex - 1) = sheet.Cells(Row, colIndex)
VBA says:
Index out of range
in method GenerateArrayPart.
How can this happen? I thought that variant can be pretty much everything and also does not need bounds to be used?
You are not having any value in the array. Thus, the array is only declared and not dimensionized.
Try this:
Dim finalArray As Variant
Redim finalArray(6)
Now, the array would have 7 values inside. From 0 to 6. The same error happens in the Function GenerateArrayPart, with the array sheetArray. There you need to declare the array as a multidimensional array. E.g. Redim sheetArray (N, M).
To see some small working sample, take a look at the code below:
Sub TestMe()
Dim finalArr As Variant
ReDim finalArr(6)
Dim i As Long
For i = LBound(finalArr) To UBound(finalArr)
finalArr = GenerateArrPart(i)
Next i
For i = LBound(finalArr) To UBound(finalArr)
Debug.Print i; finalArr(i, i)
Next i
End Sub
Public Function GenerateArrPart(a As Long) As Variant
Dim i As Long
Dim arrReturn As Variant
ReDim arrReturn(a + 1, a + 1)
For i = LBound(arrReturn) To UBound(arrReturn)
arrReturn(i, i) = a * i
Next i
GenerateArrPart = arrReturn
End Function
This is the output:
0 0
1 6
2 12
3 18
4 24
5 30
6 36
7 42
Write a subroutine in VBA to generate a winning lotto ticket consisting of 6 integer numbers randomly drawn from 1 to 40.
In order to have a small simulation animation, range("A1:E8") should contain the numbers 1 to 40 and the subroutine should then cycle through these numbers using a colored cell and then momentarily pause 2 seconds on a selected winning number. The list of winning numbers drawn should then be printed in the range("G2:G7"). In case a number drawn has already been drawn previously in the list, then a new number should be redrawn.
I have only been able to do as follows.
Option Explicit
Sub test1()
Sheet1.Cells.Clear
Dim i As Integer
For i = 1 To 40
Cells(i, 1) = i
Next
End Sub
'-----------------------------
Option Explicit
Option Base 1
Function arraydemo(r As Range)
Dim cell As Range, i As Integer, x(40, 1) As Double
i = 1
For Each cell In r
x(i, 1) = cell.Value
i = i + 1
Next cell
arraydemo = x
End Function
Sub test3()
Dim x() As String
chose = Int(Rnd * UBound(x))
End Sub
I got stuck elsewhere, the sub test3(), does not seem appropriate here. I need some suggestions. Also, I appologise for my poor formatting, I am new to this.
Populating your range like this:
range("A1:E8") should contain the numbers 1 to 40
Sheet1.Cells.Clear
Dim i As Integer
Dim rng as Range
Set rng = Range("A1:E8")
For i = 1 To 40
rng
Next
generate a winning lotto ticket consisting of 6 integer numbers randomly drawn from 1 to 40
Using a dictionary object to keep track of which items have been picked (and prevent duplicate) in a While loop (until there are 6 numbers chosen):
Dim picked as Object
Set picked = CreateObject("Scripting.Dictionary")
'Select six random numbers:
i = 1
While picked.Count < 6
num = Application.WorksheetFunction.RandBetween(1, 40)
If Not picked.Exists(num) Then
picked.Add num, i
i = i + 1
End If
Wend
Using the Application.Wait method to do the "pause", you can set up a procedure like so:
'Now, show those numbers on the sheet, highlighting each cell for 2 seconds
For Each val In picked.Keys()
rng.Cells(picked(val)).Interior.ColorIndex = 39 'Modify as needed
Application.Wait Now + TimeValue("00:00:02")
rng.Cells(picked(val)).Interior.ColorIndex = xlNone
Next
The list of winning numbers drawn should then be printed in the range("G2:G7").
Print the keys from the picked dictionary:
Range("G2:G7").Value = Application.Transpose(picked.Keys())
Putting it all together:
Sub Lotto()
Dim i As Integer, num As Integer
Dim rng As Range
Dim picked As Object 'Scripting.Dictionary
Dim val As Variant
'Populate the sheet with values 1:40 in range A1:E8
Set rng = Range("A1:E8")
For i = 1 To 40
rng.Cells(i) = i
Next
'Store which numbers have been already chosen
Set picked = CreateObject("Scripting.Dictionary")
'Select six random numbers:
i = 1
While picked.Count < 6
num = Application.WorksheetFunction.RandBetween(1, 40)
If Not picked.Exists(num) Then
picked.Add num, i
i = i + 1
End If
Wend
'Now, show those numbers on the sheet, highlighting each cell for 2 seconds
For Each val In picked.Keys()
rng.Cells(val).Interior.ColorIndex = 39 'Modify as needed
Application.Wait Now + TimeValue("00:00:02")
rng.Cells(val).Interior.ColorIndex = xlNone
Next
'Display the winning series of numbers in G2:G7
Range("G2:G7").Value = Application.Transpose(picked.Keys())
End Sub
NOTE This absolutely will not work on Excel for Mac, you would need to use a Collection instead of a Dictionary, as the Scripting.Runtime library is not available on Mac OS.
In addition to the excellent answer given by member David Zemens, following is the universal function written in "pure" Excel VBA, which does not contain any Excel Worksheet Functions, neither Dictionary Object (re: CreateObject("Scripting.Dictionary").
Option Explicit
'get N random integer numbers in the range from LB to UB, NO repetition
'general formula: Int ((UpperBound - LowerBound + 1) * Rnd + LowerBound)
Function RandomNumbers(LB As Integer, UB As Integer, N As Integer) As Variant
Dim I As Integer
Dim arrRandom() As Integer
Dim colRandom As New Collection
Dim colItem As Variant
Dim tempInt As Integer
Dim tempExists As Boolean
'check that ArraySize is less that the range of the integers
If (UB - LB + 1 >= N) Then
While colRandom.Count < N
Randomize
' get random number in interval
tempInt = Int((UB - LB + 1) * Rnd + LB)
'check if number exists in collection
tempExists = False
For Each colItem In colRandom
If (tempInt = colItem) Then
tempExists = True
Exit For
End If
Next colItem
' add to collection if not exists
If Not tempExists Then
colRandom.Add tempInt
End If
Wend
'convert collection to array
ReDim arrRandom(N - 1)
For I = 0 To N - 1
arrRandom(I) = colRandom(I + 1)
Next I
'return array of random numbers
RandomNumbers = arrRandom
Else
RandomNumbers = Nothing
End If
End Function
'get 5 Random numbers in the ranger 1...10 and populate Worksheet
Sub GetRandomArray()
Dim arr() As Integer
'get array of 5 Random numbers in the ranger 1...10
arr = RandomNumbers(1, 10, 5)
'populate Worksheet Range with 5 random numbers from array
If (IsArray(arr)) Then
Range("A1:A5").Value = Application.Transpose(arr)
End If
End Sub
The function
Function RandomNumbers(LB As Integer, UB As Integer, N As Integer)
returns array of N random numbers in the range LB...UB inclusively without repetition.
Sample Sub GetRandomArray() demonstrates how to get 5 random numbers in the range 1...10 and populate the Worksheet Range: it could be customized for any particular requirements (e.g. 8 from 1...40 in PO requirements).
APPENDIX A (Courtesy of David Ziemens)
Alternatively, you can do similar without relying on Collection object at all. Build a delimited string, and then use the Split function to cast the string to an array, and return that to the calling procedure.
This actually returns the numbers as String, but that shouldn't matter for this particular use-case, and if it does, can easily be modified.
Option Explicit
Sub foo()
Dim arr As Variant
arr = RandomNumbersNoCollection(1, 40, 6)
End Sub
'get N random integer numbers in the range from LB to UB, NO repetition
'general formula: Int ((UpperBound - LowerBound + 1) * Rnd + LowerBound)
Function RandomNumbersNoCollection(LB As Integer, UB As Integer, N As Integer)
Dim I As Integer
Dim numbers As String ' delimited string
Dim tempInt As Integer
Const dlmt As String = "|"
'check that ArraySize is less that the range of the integers
If (UB - LB + 1 >= N) Then
' get random number in interval
Do
Randomize
tempInt = Int((UB - LB + 1) * Rnd + LB)
If Len(numbers) = 0 Then
numbers = tempInt & dlmt
ElseIf InStr(1, numbers, tempInt & dlmt) = 0 Then
numbers = numbers & tempInt & dlmt
End If
Loop Until UBound(Split(numbers, dlmt)) = 6
numbers = Left(numbers, Len(numbers) - 1)
End If
RandomNumbersNoCollection = Split(numbers, dlmt)
End Function
I'm hopelessly trying to find a better way of filling a range contents. This way produces the correct results but is very slow. Can anyone point me in the correct direction in terms of how to fill a 2d array or otherwise to speed up the algorithm? I would love a code snippet someone has had success with or even just links that show a cleaner method.
here is my OLD code:
----------------
f = 1
maxcol = 'func call to get last non blank col ref .ie could return T, R, H.etc
For f = 1 To UBound(filenames)
Set aDoc = LoadXmlDoc(filenames(f))
For Each c In Worksheets("Results").Range("A1:" & maxcol & "1")
c.Offset(f, 0).Value = aNode.Text
Next c
Worksheets("Results").Range(maxcol & "1").Offset(f, 0).Value = filenames(f)
Next f
UPDATED CODE:
----------
Dim aDoc As DOMDocument
Dim aNode As IXMLDOMNode
Dim numOfXpaths As Integer
Dim filenames As Variant
Dim f As Integer
Dim maxcol As String
Dim rngStart As Range
Dim nColIndex As Long
Dim lngCalc As Long
'Dim numOfFiles As Integer
Dim aXpaths As Variant
numOfFiles = UBound(filenames)
colToRow aXpaths, numOfXpaths
maxcol = Number2Char(numOfXpaths)
ReDim aValues(1 To numOfFiles, 1 To numOfXpaths + 1) As Variant
For f = 1 To numOfFiles
Set aDoc = LoadXmlDoc(filenames(f))
For nColIndex = 1 To numOfXpaths
If aDoc.parseError Then
aValues(f, nColIndex) = "XML parse error:"
Else
Set aNode = aDoc.selectSingleNode(aXpaths(nColIndex))
aValues(f, nColIndex) = aNode.Text
End If
Next nColIndex
aValues(f, numOfXpaths + 1) = filenames(f)
Next f
Worksheets("Results").Range("A1").Offset(1, 0).Resize(numOfFiles, numOfXpaths + 1).Value = aValues
Function colToRow(ByRef aXpaths As Variant, ByRef numOfXpaths As Integer)
Dim xpathcount As Integer
Dim c As Integer
'Dim aXpaths As Variant
xpathcount = Worksheets("Xpaths").Cells(Rows.Count, "A").End(xlUp).Row - 1
ReDim aXpaths(1 To xpathcount + 1) As Variant
For c = 0 To xpathcount
Worksheets("Results").Range("A1").Offset(0, c) = Worksheets("Xpaths").Range("A1").Offset(c, 0)
Worksheets("Results").Range("A1").Offset(0, c).Columns.AutoFit
aXpaths(c + 1) = Worksheets("Xpaths").Range("A1").Offset(c, 0)
Next c
Worksheets("Results").Range("A1").Offset(0, xpathcount + 1) = "Filename"
'colToRow = xpathcount + 1
numOfXpaths = xpathcount + 1
End Function
Function Number2Char(ByVal c) As String
Number2Char = Split(Cells(1, c).Address, "$")(1)
End Function
To do this efficiently you should generate a 2-dimensional data with the data you want to write, then write it all in one go.
Something like the following. I prefer 0-based arrays for compatibility with other languages whereas you seem to be using a 1-based array (1 to UBound(filenames). So there may be off-by-one errors in the following untested code:
f = 1
maxcol = 'func call to get last non blank col ref .ie could return T, R, H.etc
' 2D array to hold results
' 0-based indexing: UBound(filenames) rows and maxcol columns
Dim aValues(0 to UBound(filenames)-1, 0 To maxcol-1) As Variant
Dim rngStart As Range
Dim nColIndex As Long
For f = 1 To UBound(filenames)
Set aDoc = LoadXmlDoc(filenames(f))
aValues(f-1, 0) = filenames(f)
For nColIndex = 1 To maxCol-1
aValues(f-1, nColIndex) = aNode.Text
Next nColIndex
Next f
' Copy the 2D array in one go
Worksheets("Results").Offset(1,0).Resize(UBound(filenames),maxCol).Value = aValues
As you're getting you results from XML, have you looked into using XML Maps to display the information - might not be suitable for your situation, but worth a try.
This link below shows some stuff about using XML maps in Excel.
The syntax of the line to load an XML string into a define map is similar to this:
ActiveWorkbook.XmlMaps("MyMap").ImportXml(MyXMLDoc,True)
You might want to look at my code in "Using Variant Arrays in Excel VBA for Large Scale Data Manipulation", http://www.experts-exchange.com/A_2684.html (further detail provided in the hyperlink)
Note that as I don't have your data above to work with the article provides a sample solution (in this case efficiently deleting leading zeroes) to meet you filling a range from a 2d array requirement.
Key points to note
The code handles non contigious ranges by use of Areas
When using variant arrays alwasy test that the range setting the array size is bigger than 1 cell - if not you cant use a variant
The code readas from a range, runs a manipulation, then dumps back to the same range
Using Value2 is slightly moe efficient than Value
Here is the code:
'Press Alt + F11 to open the Visual Basic Editor (VBE)
'From the Menu, choose Insert-Module.
'Paste the code into the right-hand code window.
'Press Alt + F11 to close the VBE
'In Xl2003 Goto Tools … Macro … Macros and double-click KillLeadingZeros
Sub KillLeadingZeros()
Dim rng1 As Range
Dim rngArea As Range
Dim lngRow As Long
Dim lngCol As Long
Dim lngCalc As Long
Dim objReg As Object
Dim X()
On Error Resume Next
Set rng1 = Application.InputBox("Select range for the replacement of leading zeros", "User select", Selection.Address, , , , , 8)
If rng1 Is Nothing Then Exit Sub
On Error GoTo 0
'See Patrick Matthews excellent article on using Regular Expressions with VBA
Set objReg = CreateObject("vbscript.regexp")
objReg.Pattern = "^0+"
'Speed up the code by turning off screenupdating and setting calculation to manual
'Disable any code events that may occur when writing to cells
With Application
lngCalc = .Calculation
.ScreenUpdating = False
.Calculation = xlCalculationManual
.EnableEvents = False
End With
'Test each area in the user selected range
'Non contiguous range areas are common when using SpecialCells to define specific cell types to work on
For Each rngArea In rng1.Areas
'The most common outcome is used for the True outcome to optimise code speed
If rngArea.Cells.Count > 1 Then
'If there is more than once cell then set the variant array to the dimensions of the range area
'Using Value2 provides a useful speed improvement over Value. On my testing it was 2% on blank cells, up to 10% on non-blanks
X = rngArea.Value2
For lngRow = 1 To rngArea.Rows.Count
For lngCol = 1 To rngArea.Columns.Count
'replace the leading zeroes
X(lngRow, lngCol) = objReg.Replace(X(lngRow, lngCol), vbNullString)
Next lngCol
Next lngRow
'Dump the updated array sans leading zeroes back over the initial range
rngArea.Value2 = X
Else
'caters for a single cell range area. No variant array required
rngArea.Value = objReg.Replace(rngArea.Value, vbNullString)
End If
Next rngArea
'cleanup the Application settings
With Application
.ScreenUpdating = True
.Calculation = lngCalc
.EnableEvents = True
End With
Set objReg = Nothing
End Sub
Here is the code that applies an advanced filter to the column A on the Sheet1 worksheet (List range) by using the range of values on the Sheet2 (criteria range):
Range("A1:A100").AdvancedFilter Action:=xlFilterInPlace, CriteriaRange:= _
Sheets("Sheet2").Range("A1:A10"), Unique:=False
After running this code, I need to do something with the rows that are currently visible on the screen.
Currently I use a code like this:
For i = 1 to maxRow
If Not ActiveSheet.Row(i).Hidden then
...do something that I need to do with that rows
EndIf
Next
Is there any simple property that can give me a range of rows visible after applying an advanced filter?
ActiveSheet.Range("A1:A100").Rows.SpecialCells(xlCellTypeVisible)
This yields a Range object.
Lance's solution will work in the majority of situations.
But if you deal with large/complex spreadsheets you might run into the "SpecialCells Problem". In a nutshell, if the range created causes greater than 8192 non-contiguous areas (and it can happen) then Excel will throw an error when you attempt to access SpecialCells and your code won't run. If your worksheets are complex enough you expect to encounter this problem, then it is recommended you stick with the looping approach.
It's worth noting that this problem is not with the SpecialCells property itself, rather it is with the Range object. This means that anytime that you attempt to obtain a range object that could be very complex you should either employee an error handler, or do as you already have done, which is to cause your program to work on each element of the range (split the range up).
Another possible approach would be to return an array of Range Objects and then loop through the array. I have posted some example code to play around with. However it should be noted that you really should only bother with this if you expect to have the problem described or you just want to feel assured your code is robust. Otherwise it's just needless complexity.
Option Explicit
Public Declare Function GetTickCount Lib "kernel32" () As Long
Public Sub GenerateProblem()
'Run this to set up an example spreadsheet:
Dim row As Long
Excel.Application.EnableEvents = False
Sheet1.AutoFilterMode = False
Sheet1.UsedRange.Delete
For row = 1 To (8192& * 4&) + 1&
If row Mod 3& Then If Int(10& * Rnd) 7& Then Sheet1.Cells(row, 1&).value = "test"
Next
Sheet1.UsedRange.AutoFilter 1&, ""
Excel.Application.EnableEvents = True
MsgBox Sheet1.UsedRange.SpecialCells(xlCellTypeVisible).address
End Sub
Public Sub FixProblem()
'Run this to see various solutions:
Dim ranges() As Excel.Range
Dim index As Long
Dim address As String
Dim startTime As Long
Dim endTime As Long
'Get range array.
ranges = GetVisibleRows
'Do something with individual range objects.
For index = LBound(ranges) To UBound(ranges)
ranges(index).Interior.ColorIndex = Int(56 * Rnd + 1)
Next
'Get total address if you want it:
startTime = GetTickCount
address = RangeArrayAddress(ranges)
endTime = GetTickCount
Debug.Print endTime - startTime, ; 'Outputs time elapsed in milliseconds.
'Small demo of why I used a string builder. Straight concatenation is about
'10 times slower:
startTime = GetTickCount
address = RangeArrayAddress2(ranges)
endTime = GetTickCount
Debug.Print endTime - startTime
End Sub
Public Function GetVisibleRows(Optional ByVal ws As Excel.Worksheet) As Excel.Range()
Const increment As Long = 1000&
Dim max As Long
Dim row As Long
Dim returnVal() As Excel.Range
Dim startRow As Long
Dim index As Long
If ws Is Nothing Then Set ws = Excel.ActiveSheet
max = increment
ReDim returnVal(max) As Excel.Range
For row = ws.UsedRange.row To ws.UsedRange.Rows.Count
If Sheet1.Rows(row).Hidden Then
If startRow 0& Then
Set returnVal(index) = ws.Rows(startRow & ":" & (row - 1&))
index = index + 1&
If index > max Then
'Redimming in large increments is an optimization trick.
max = max + increment
ReDim Preserve returnVal(max) As Excel.Range
End If
startRow = 0&
End If
ElseIf startRow = 0& Then startRow = row
End If
Next
ReDim Preserve returnVal(index - 1&) As Excel.Range
GetVisibleRows = returnVal
End Function
Public Function RangeArrayAddress(ByRef value() As Excel.Range, Optional lowerindexRV As Variant, Optional upperindexRV As Variant) As String
'Parameters left as variants to allow for "IsMissing" values.
'Code uses bytearray string building methods to run faster.
Const incrementChars As Long = 1000&
Const unicodeWidth As Long = 2&
Const comma As Long = 44&
Dim increment As Long
Dim max As Long
Dim index As Long
Dim returnVal() As Byte
Dim address() As Byte
Dim indexRV As Long
Dim char As Long
increment = incrementChars * unicodeWidth 'Double for unicode.
max = increment - 1& 'Offset for array.
ReDim returnVal(max) As Byte
If IsMissing(lowerindexRV) Then lowerindexRV = LBound(value)
If IsMissing(upperindexRV) Then upperindexRV = UBound(value)
For index = lowerindexRV To upperindexRV
address = value(index).address
For char = 0& To UBound(address) Step unicodeWidth
returnVal(indexRV) = address(char)
indexRV = indexRV + unicodeWidth
If indexRV > max Then
max = max + increment
ReDim Preserve returnVal(max) As Byte
End If
Next
returnVal(indexRV) = comma
indexRV = indexRV + unicodeWidth
If indexRV > max Then
max = max + increment
ReDim Preserve returnVal(max) As Byte
End If
Next
ReDim Preserve returnVal(indexRV - 1&) As Byte
RangeArrayAddress = returnVal
End Function
Public Function RangeArrayAddress2(ByRef value() As Excel.Range, Optional lowerIndex As Variant, Optional upperIndex As Variant) As String
'Parameters left as variants to allow for "IsMissing" values.
'Code uses bytearray string building methods to run faster.
Const incrementChars As Long = 1000&
Const unicodeWidth As Long = 2&
Dim increment As Long
Dim max As Long
Dim returnVal As String
Dim index As Long
increment = incrementChars * unicodeWidth 'Double for unicode.
max = increment - 1& 'Offset for array.
If IsMissing(lowerIndex) Then lowerIndex = LBound(value)
If IsMissing(upperIndex) Then upperIndex = UBound(value)
For index = lowerIndex To upperIndex
returnVal = returnVal & (value(index).address & ",")
Next
RangeArrayAddress2 = returnVal
End Function
You can use the following code to get the visible range of cells:
Excel.Range visibleRange = Excel.Application.ActiveWindow.VisibleRange
Hope this helps.