Create a dictionary with value from an Array - vba

I am working on a report and trying to output my data like this
Input:
Output
I am thinking of using a dictionary with each key has multiple value of Items&their but i don't know how to implement.

Using a dictionary would not work for your chart example. A dictionary is used to hold ONLY TWO string values using a hashtable. An example of using a dictionary properly is using the two words "hello" and "a polite greeting to another human". The first string is for the word and the second is the definition of the word, just like a regular dictionary.
I would provide you with two better options. You could create a 2D array which holds all your values, but you would need to output the labels separately from the values. You could also create a file which holds the labels and the values for your chart.
If you need more help, please provide more information on what you are trying to do with the chart displayed. An array is better for calculations but a file is better for storing the values and displaying the chart.

Second answer would be to use a dictionary of dictionaries. Again input data is
With the following code
Sub DemoDictOfDict()
Dim rg As Range, rgHdr As Range
Dim dict As Dictionary
Dim sngDict As Dictionary
Dim wkb As Workbook, wks As Worksheet
Set wkb = ActiveWorkbook
Set wks = wkb.ActiveSheet
Set rg = wks.Range("A1").CurrentRegion
' Retrieving the header line from the data
Set rgHdr = rg.Rows(1).Offset(, 1).Resize(, rg.Columns.Count - 1)
Dim vHdr As Variant
vHdr = rgHdr.Value
Dim sngRow As Range
Dim sngCell As Range
Dim vdat As Variant
Dim k As Long
' Creating the dictionary which will be used
' to hold the dictionaries with the data
Set dict = New Dictionary
' Looping through the rows of the data
' as we are not using the listobject one has to use offset and resizte
For Each sngRow In rg.Offset(1).Resize(rg.Rows.Count - 1).Rows
' getting the number for different columns
' again listobject not used, so working with resizte and offset
vdat = sngRow.Offset(, 1).Resize(, sngRow.Columns.Count - 1)
' Creating the dictionary containing the numbers
Set sngDict = New Dictionary
For k = LBound(vHdr, 2) To UBound(vHdr, 2)
' Adding the values to the "inner" dictionary
sngDict.Add vHdr(1, k), vdat(1, k)
Next k
' Adding the dictionary to the dictionary
Set dict(sngRow.Cells(1, 1).Value) = sngDict
Next
' output to immediate windows
Dim outName As String, vDict As Dictionary
Dim key As Variant, i As Long
For i = 0 To dict.Count - 1
outName = dict.Keys(i)
Set vDict = dict.Items(i)
Debug.Print outName
For Each key In vDict.Keys
Debug.Print key, vDict(key)
Next
Next i
End Sub
you get the following output in the immediate window

First answer as said in one of my comments you could use Power Query to unpivot the data. As example data I took data like this
After unpivoting the data with PowerQuery (goto Data then From Table/Range, select the columns with Apple, Orange etc. in the Power Query Editor, select the tab Transform and there Unpivot columns) one gets
Then you pivot the data again and you get

Unless you're going to do something else with the collected data then there's no need for any intermediate data structure:
Dim data, r As Long, c As Long
data = ActiveSheet.Range("A3:D5").Value
For r = 2 To UBound(data, 1)
Debug.Print data(r, 1)
For c = 2 To UBound(data, 2)
Debug.Print data(1, c) & " " & data(r, c)
Next c
Debug.Print "Checked."
Next r

The location of the data should be the same as the picture to work.
Sub test()
Dim vDB As Variant
Dim vR() As Variant
Dim i As Long, r As Long, n As Long
Dim c As Integer, j As Integer
vDB = Range("a3").CurrentRegion
r = UBound(vDB, 1)
c = UBound(vDB, 2)
For i = 3 To r
n = n + 1
ReDim Preserve vR(1 To n)
vR(n) = vDB(i, 1)
For j = 2 To c
n = n + 1
ReDim Preserve vR(1 To n)
If vDB(i, j) = "" Then
vR(n) = "Checked"
Else
vR(n) = vDB(2, j) & Space(1) & vDB(i, j)
End If
Next j
Next i
Sheets.Add
Range("a1").Resize(n) = WorksheetFunction.Transpose(vR)
End Sub
Result Sheet

Related

Fill Collection with Elements of list efficiently

i have programmed a procedure to find all values of a list and store them in a collection. There are identical values but each value only should be stored once.
Here is my vba code:
For intRow = intStart To ActiveSheet.Cells(Rows.count, 1).End(xlUp).Row
k = 1
Do
If k > colData.count Then
colData.Add Trim(Cells(intRow, intClmn).Value)
Exit Do
ElseIf Trim(Cells(intRow, intClmn)) = colData.Item(k) Then
Exit Do
End If
k = k + 1
Loop
Next i
I wonder if there is a more efficient way to get those values though. Do you know a more efficient way to collect values of a list?
If I've understood then I would record copying and pasting the column to a blank worksheet, and using the Remove Duplicates feature on this area to result in a column of distinct values that you can iterate.
As the new (temporary) worksheet is blank other than the retained values, you could use UsedRange to iterate all its cells:
For rng In Sheets("TempSht").UsedRange
Next rng
or again use End(xlUp) (or xlDown).
Could even get the entire range into an array if appropriate:
Dim arr As Variant
arr = WorksheetFunction.Transpose(Range("A1:A3"))
I ommited declaration of intStart and intClmn as well as calculating their values.
You can use Dictionary object and operate with an array instead of cells.
You need to add a reference in order to use early binding, a great answer is already here. You need Microsoft Scripting Runtime reference.
Dim vArr(), i As Long, j As Long, DataRange As Range
'Dim intStart As Long, intClmn As Long
'intStart = 1: intClmn = 7
' Declaring and creating a dictionary (choose one and wisely)
'--------------------------------------------------------------
' Late binding
Dim iDict As Object
Set iDict = CreateObject("Scripting.Dictionary")
' Early binding (preferable, you need to enable reference)
'Dim iDict As Scripting.Dictionary
'Set iDict = New Scripting.Dictionary
'--------------------------------------------------------------
' Define range of your data (may vary, modify so it suits your needs)
With ActiveSheet
Set DataRange = .Range(.Cells(intStart, 1), _
.Cells(.Cells(.Rows.Count, 1).End(xlUp).Row, intClmn))
End With
' Populate an array with trimmed values
' I'm not sure how productive it is comparing to calling Trim in a loop so..
' You're free to test it
vArr = Evaluate("IF(ROW(), TRIM(" & DataRange.Address & "))")
' Loop through array
For i = LBound(vArr, 1) To UBound(vArr, 1)
For j = LBound(vArr, 2) To UBound(vArr, 2)
' Add an item with the key of vArr(i, j),
' otherwise change an existing item with this key to vArr(i, j)
iDict(vArr(i, j)) = vArr(i, j)
Next j
Next i

Gather data tidy in Excel using VBA

What the case is:
So I got a "results sample" in excel format that needs filtering and reshaping to look nice. It is a result that will be not identical all the time but it follows similar rules. I have to filter it further and make it a little more tidy. I have figured out the filtering part, but I am not sure how to sort the remaining data, in a tidy way.
What the situation is:
There are six columns involved.
Notice: Real deal is not THAT simple, but what I need can be demonstrated using such a simple example and then I can manage more complex stuff myself I suppose.
For our example we use columns from B to G
The data are set as pairs of a "title" and a value.
For instance, if you look the first example picture I provide, The first detais the pair B3 and C3.
As you can see, looking at the same picture, D3 and E3 is an empty pair.
Same goes for D4 - E4 and F4 - G4 and so on until a last one at B11 - C11.
Starting data example:
[
What I want to achieve:
I would like, using Visual Basic for Applications, to sort the data, starting from let's say for our example B3 (see second picture) and fill three SETS of two columns, (BC, DE, FG) if there are no data inside those cells.
Notice: If a cell like D3 is null then SURELY E3 will be null too so there can be just only one check. I mean we can check either value columns or title columns.
Notice2: The B,D,F or C,E,G columns DON'T have to be sorted. I just want all the not-null values of B,D,F and their respective values from C,E,G gathered together neat so printing will not need 30 pages but just a few (too many spaces between is causing it and I try to automate the cleanup)
Here's something to start with. The first double loop populates a VBA Collection with Range variables that refer to the Cells that contain the titles.
The associated values are obtained by using an offset. The middle double loop performs a bubble sort on the latter (highly inefficient - you might want to replace it with something else). The next if statement creates a 2nd sheet if it doesn't exist on which to write out the results (last loop).
Option Explicit
Sub GatherData()
Dim lastRow As Integer, lastCol As Integer
Dim r As Integer, c As Integer
Dim vals As Collection
Set vals = New Collection
With Sheets(1)
lastCol = .UsedRange.Columns(.UsedRange.Columns.Count).Column
lastRow = .UsedRange.Rows(.UsedRange.Rows.Count).row
For c = 1 To lastCol Step 2
For r = 1 To lastRow
If (Trim(Cells(r, c).Value) <> "") Then
vals.Add .Cells(r, c)
End If
Next
Next
End With
' Bubble Sort
Dim i As Integer, j As Integer
Dim vTemp As Range
For i = 1 To vals.Count - 1
For j = i + 1 To vals.Count
If vals(i).Value > vals(j).Value Then
Set vTemp = vals(j)
vals.Remove j
vals.Add vTemp, vTemp, i
End If
Next j
Next i
Dim sht2 As Worksheet
If ThisWorkbook.Worksheets.Count = 1 Then
Set sht2 = ThisWorkbook.Sheets.Add(After:=ThisWorkbook.Sheets(1))
Else
Set sht2 = Worksheets(2)
End If
With sht2
r = 3
c = 2
For i = 1 To vals.Count
.Cells(r, c).Value = vals(i).Value
.Cells(r, c + 1).Value = vals(i).Offset(, 1).Value
c = c + 2
If c = 8 Then
r = r + 1
c = 2
End If
Next
End With
End Sub
Here is a method using the Dictionary object. I use early binding which requires setting a reference to Microsoft Scripting Runtime. If you are going to be distributing this, you might want to convert this to late-binding.
We assume that your data is properly formed as you show it above. In other words, all the titles are in even numbered columns; and the results are in the adjacent cell.
We create the dictionary using the Title as the Key, and the adjacent cell value for the Dictionary item.
We collect the information
Transfer the Keys to a VBA array and sort alphabetically
create a "Results Array" and populate it in order
write the results to a worksheet.
I will leave formatting and header generation to you.
By the way, there is a constant in the code for the number of Title/Value pair columns. I have set it to 3, but you can vary that.
Enjoy
Option Explicit
Option Compare Text 'If you want the sorting to be case INsensitive
'set reference to Microsoft Scripting Runtime
Sub TidyData()
'Assume Titles are in even numbered columns
'Assume want ColPairs pairs of columns for output
'Use dictionary with Title as key, and Value as the item
Dim dctTidy As Dictionary
Dim arrKeys As Variant
Dim wsSrc As Worksheet, wsRes As Worksheet, rRes As Range
Dim vSrc As Variant, vRes As Variant
Dim LastRow As Long, LastCol As Long
Dim I As Long, J As Long, K As Long, L As Long
Dim V As Variant
'in Results
Const ColPairs As Long = 3
'Set Source and results worksheet and range
Set wsSrc = Worksheets("sheet1")
Set wsRes = Worksheets("sheet2")
Set rRes = wsRes.Cells(1, 2)
'Read source data into variant array
With wsSrc.Cells
LastRow = .Find(what:="*", after:=.Item(1, 1), _
LookIn:=xlValues, searchorder:=xlByRows, searchdirection:=xlPrevious).Row
LastCol = .Find(what:="*", after:=.Item(1, 1), _
LookIn:=xlValues, searchorder:=xlByColumns, searchdirection:=xlPrevious).Column
vSrc = .Range(.Cells(1, 1), .Cells(LastRow, LastCol))
End With
'Collect the data into a dictionary
Set dctTidy = New Dictionary
For I = 1 To UBound(vSrc, 1)
For J = 2 To UBound(vSrc, 2) Step 2
If vSrc(I, J) <> "" Then _
dctTidy.Add Key:=vSrc(I, J), Item:=vSrc(I, J + 1)
Next J
Next I
'For this purpose, we can do a simple sort on the dictionary keys,
' and then create our results array in the sorted order.
arrKeys = dctTidy.Keys
Quick_Sort arrKeys, LBound(arrKeys), UBound(arrKeys)
'Create results array
ReDim vRes(1 To WorksheetFunction.RoundUp(dctTidy.Count / ColPairs, 0), 1 To ColPairs * 2)
I = 0
J = 0
For Each V In arrKeys
K = Int(I / ColPairs) + 1
L = (J Mod ColPairs) * 2 + 1
vRes(K, L) = V
vRes(K, L + 1) = dctTidy(V)
I = I + 1
J = J + 1
Next V
'write the results
Set rRes = rRes.Resize(UBound(vRes, 1), UBound(vRes, 2))
With rRes
.Worksheet.Cells.Clear
.Value = vRes
.HorizontalAlignment = xlCenter
End With
End Sub
Sub Quick_Sort(ByRef SortArray As Variant, ByVal first As Long, ByVal last As Long)
Dim Low As Long, High As Long
Dim Temp As Variant, List_Separator As Variant
Low = first
High = last
List_Separator = SortArray((first + last) / 2)
Do
Do While (SortArray(Low) < List_Separator)
Low = Low + 1
Loop
Do While (SortArray(High) > List_Separator)
High = High - 1
Loop
If (Low <= High) Then
Temp = SortArray(Low)
SortArray(Low) = SortArray(High)
SortArray(High) = Temp
Low = Low + 1
High = High - 1
End If
Loop While (Low <= High)
If (first < High) Then Quick_Sort SortArray, first, High
If (Low < last) Then Quick_Sort SortArray, Low, last
End Sub
Assuming we got all variables set and initialized properly, in this example:
Sheets("sheetname").Select ' because stupid things can happen...
For i = 3 To 13
Let newrangeT = "B" & i '
Let newrangeV = "C" & i '
If Sheets("sheetname").Range(newrangeV) <> "" Then
values(Position) = Sheets("sheetname").Range(newrangeV)
titles(Position) = Sheets("sheetname").Range(newrangeT)
Position = Position + 1
Else
' Don't do anything if the fields are null
End If
Next i
Sheets("sheetname").Range("B1:G13").Clear
' We then get each data from the arrays with a For loop.
' We set a columnset variable to 1.
' We set a currentrow variable to 3.
' If columnset is 1 data will enter in B and C and columnset = columnset +1
' Then if columnset is 2 we set data to DE and columnset = columnset +1
' But if columnset is 2we set data to FG and columnset = 1 and currentrow = currentrow +1
' Iterating the arrays will result in a neat setting of the data, but it will add zeros for all the nulls. Thus we need an If statement that will exclude that values checking the TITLE array (that should contain a title instead). if the value is not 0 then... we run what I describe, otherwise we do nothing.
Putting the data in the array is half of the trick.
Then we clear the area.
We set two string variables to declare ranges (actually cell reference) for every cell iterated in the loop. Here I demonstrated only for column set B,C
but we have to do the same for the rest of the columns.
The If statement here checks for null. You might have different needs, so changing the if statement changes the filtering. Here I check if the cells are not null. If the cells of column C contain data, put those data in values array and the respective B data on titles array but where? Position starts as 1 and we then iterate it +1 each time it adds something.
You can set data from an array using this command:
' current_row is set to the first row of the spreadsheet we wanna fill.
Sheets("sheetname").Select ' because stupid things can happen...
newrangeV = "C" & current_row
Sheets("sheetname").Range(newrangeV) = values(j)
The rest is just putting things together.
In any case, I wanna thank both of the people involved in this question, because I might didn't got the solution, but I got an idea of how to do other stuff, like accidentally learning something new. Cheers.

Invalid procedure call or argument in excel vba

I am using a filter on column1 of sheet1 with a unique no, get filtered values from column Z into an array (arr) object and paste those values as a string in one cell in "Dashboard" sheet. Repeat this process for all unique values on column 1 of sheet1. I am getting multiple errors 1. "Invalid Procedure call or argument" at Join() method. 2. Getting values into rng object and array. Can I get your help on where am I going wrong with this. Many Thanks.
Dim d As Object, c As Range, k, tmp As String
Dim TestRg As Range
Dim arr() As Variant
Dim i As Integer
Dim myCell As Range
Dim rng As Range
i = 2
Set d = CreateObject("scripting.dictionary")
Columns(1).Select
For Each c In Selection
tmp = Trim(c.Value)
If Len(tmp) > 0 Then d(tmp) = d(tmp) + 1
Next c
For Each k In d.Keys
If IsNumeric(k) Then
Set TestRg = Range("A1:AQ" & LastRow(ActiveSheet))
TestRg.AutoFilter Field:=1, Criteria1:=k, Operator:=xlFilterValues
LasRow = ActiveSheet.UsedRange.Rows(ActiveSheet.UsedRange.Rows.Count).Row
Set rng = ActiveSheet.Range("Z1" & ":" & "Z" & LasRow).SpecialCells(xlCellTypeVisible)
rng.Activate
arr = rng.Value
Worksheets("Dashboard").Range("A" & i) = k
Worksheets("Dashboard").Range("E" & i).Resize(UBound(arr, 1)).Value = Join(arr, " ")
i = i + 1
Erase arr
End If
Next k
as for the very error your post is asking help for, it comes out of:
Worksheets("Dashboard").Range("E" & i).Resize(UBound(arr, 1)).Value = Join(arr, " ")
since arr is a Two-dimensional array while Join() function requires a One-dimensional one
you then should:
declare arr as a simple Variant:
Dim arr As Variant
fill it in a way so as to generate a One-dimensional array
arr = Application.Transpose(rng.Value)
other than that it seems to me your code have some more issues
you may want to fix this error first and then, should those issues actually arise and shouldn't you be able to fix, make a new post

COUNTIF() in 'For' loop

I have a column with nearly 100k and am trying to determine how many times a value occurs repeatedly in that column. I can do it row by row currently, but this is menial as a programmer, through something like =COUNTIF(D:D,D2). Yet that only returns D2 matches in column D.
I need to iterate through all values of D returning countif, therefore revealing all of the values repetitions in the column. I can remove duplicates later! So I have a dev. button a basic sub, or function (man this is new to me) and something along the lines of the most basic for loop ever. Just getting caught up on how to implement the COUNTIF() to to the loop properly.
Right now I'm looking at:
Sub doloop()
Dim i As Integer
i = 1
Do While i < D.Length
Cells(i, 8).Value =CountIf(D:D,D[i])
i = i + 1
Loop
End Sub
That code is incorrect obviously but it is where I'm at and may help for anyone more familiar with other languages.
Use Application.WorksheetFunction.CountIf() in your loop.
Private Sub doloop()
Dim lastRow As Long
Dim d As Double
Dim r As Range
Dim WS As Excel.Worksheet
Dim strValue As String
Dim lRow As Long
'Build your worksheet object
Set WS = ActiveWorkbook.Sheets("sheet1")
'Get the last used row in column A
lastRow = WS.Cells(WS.Rows.count, "D").End(xlUp).Row
'Build your range object to be searched
Set r = WS.Range("D1:D" & lastRow)
lRow = 1
WS.Activate
'Loop through the rows and do the search
Do While lRow <= lastRow
'First, get the value we will search for from the current row
strValue = WS.Range("D" & lRow).Value
'Return the count from the CountIf() worksheet function
d = Application.worksheetFunction.CountIf(r, strValue)
'Write that value to the current row
WS.Range("H" & lRow).Value = d
lRow = lRow + 1
Loop
End Sub
I believe you are trying to write the value to the cell, that is what the above does. FYI, if you want to put a formula into the cell, here is how that is done. Use this in place of WS.Range("H" & lRow).Value = d
WS.Range("H" & lRow).Formula = "=CountIf(D:D, D" & lRow & ")"
Sounds like you may want to look into using tables in Excel and capitalizing on their features like filtering and equation autofill. You may also be interested in using a PivotTable to do something very similar to what you're describing.
If you really want to go about this the programmatic way, I think the solution Matt gives answers your question about how to do this using CountIf. There's a big detriment to using CountIf though, in that it's not very computationally efficient. I don't think the code Matt posted will really be practical for processing the 100K rows mentioned in the OP (Application.ScreenUpdating = false would help some). Here's an alternative method that's a lot more efficient, but less intuitive, so you'll have to decide what suites your needs and what you feel conformable with.
Sub CountOccurances()
'Define Input and Output Ranges
'The best way to do this may very from case to case,
'So it should be addressed seperately
'Right now we'll assume current sheet rows 1-100K as OP specifies
Dim RInput, ROutput As Range
Set RInput = Range("D1:D100000")
Set ROutput = Range("E1:E100000")
'Define array for housing and processing range values
Dim A() As Variant
ReDim A(1 To RInput.Rows.Count, 0)
'Use Value2 as quicker more accurate value
A = RInput.Value2
'Create dictionary object
Set d = CreateObject("Scripting.Dictionary")
'Loop through array, adding new values and counting values as you go
For i = 1 To UBound(A)
If d.Exists(A(i, 1)) Then
d(A(i, 1)) = d(A(i, 1)) + 1
Else
d.Add A(i, 1), 1
End If
Next
'Overwrite original array values with count of that value
For i = 1 To UBound(A)
A(i, 1) = d(A(i, 1))
Next
'Write resulting array to output range
ROutput = A
End Sub
You can also modify this to include the removal of replicates you mentioned.
Sub CountOccurances_PrintOnce()
'Define Input and Output Ranges
'The best way to do this may very from case to case,
'So it should be addressed seperately
'Right now we'll assume current sheet rows 1-100K as OP specifies
Dim RInput, ROutput As Range
Set RInput = Range("D1:D100000")
Set ROutput = Range("F1:F9")
'Define array for housing and processing range values
Dim A() As Variant
ReDim A(1 To RInput.Rows.Count, 0)
'Use Value2 as quicker more accurate value
A = RInput.Value2
'Create dictionary object
Set d = CreateObject("Scripting.Dictionary")
'Loop through array, adding new values and counting values as you go
For i = 1 To UBound(A)
If d.Exists(A(i, 1)) Then
d(A(i, 1)) = d(A(i, 1)) + 1
Else
d.Add A(i, 1), 1
End If
Next
'Print results to VBA's immediate window
Dim sum As Double
For Each K In d.Keys
Debug.Print K & ": " & d(K)
sum = sum + d(K)
Next
Debug.Print "Total: " & sum
End Sub

Reverse order of For Each loop

One of the most powerful things about VB is ability to loop through objects in a collection WITHOUT referring to the index - for each loop.
I find it very useful only want to remove objects from a collection.
When doing removing objects from a predefined such as rows on a spread sheet the code is simpler if I use indexing and start at the largest and work back to the first. (Step -1 with an iterator) (otherwise requires an offset as the For each moves the enumerator pointer back to the previous object once the active one is deleted)
eg.
For intA = 10 to 1 step -1
' ...
Next
What about when using a For Each | Next
eg.
For each rngCell in Selection.Cells
' ...
Next
How could I loop backwards using the for each loop syntax?
It's not possible to loop backwards using the for each loop syntax.
As an alternative you can use a For i = a To 1 Step -1 loop:
Sub reverseForEach()
Dim i As Long, rng As Range
Set rng = ActiveSheet.Range("A1:B2")
For i = rng.Cells.Count To 1 Step -1
Debug.Print rng.item(i).Address
' Or shorthand rng(i) as the Item property
' is the default property for the Range object.
' Prints: $B$2, $A$2, $B$1, $A$1
Next i
End Sub
This works with all collections that have the Item property. For instance Worksheets, Areas or Shapes.
Note: The order of the loop when using on the Range object is from right to left, then up.
For built in collections (eg a Range) the short answer is: you can't. For user defined collections the answer linked by #VBlades might be useful, although the cost might outweigh the benifit.
One work around is to seperate the identification of items to be removed from the actual removal. Eg, for a range, build up a new range variable using Union, then process that variable, eg delete all the rows in one go. For the Range example, you can also take advantage of the Variant Array method to further speed things up.
Whether or not any of this is useful will depend on your actual use case.
There are other good answers but here's another alternative method of "stepping backwards" through a Range.
Function to Invert Range into Array
This function returns a "backwards Range Array" that can be used with For..Each:
Function ReverseRange(rg As Range) As Range()
Dim arr() As Range, r As Long, c As Long, n As Long
With rg
ReDim arr(1 To .Cells.Count) 'resize Range Array
For r = .Cells(.Rows.Count, 1).Row To .Cells(1, 1).Row Step -1
For c = .Cells(1, .Columns.Count).Column To .Cells(1, 1).Column Step -1
n = n + 1
Set arr(n) = .Worksheet.Cells(r, c) 'set cell in Array
Next c
Next r
End With
ReverseRange = arr 'return Range Array as function result
End Function
Example Usage:
Sub test()
Dim oCell
For Each oCell In ReverseRange(ActiveSheet.Range("E5:A1"))
Debug.Print oCell.Address 'do something here with each cell
Next oCell
End Sub
use a second variable that is set as your wanted counter and use this one in your code
'ex: Loop from n = 19 to 16
For i = 0 To 3
n = 19 - i
'your code here using n as the counter
Next
Only for Range collections. They are more complicated if they have more than 1 Area.
Basically there are two loops, the first one keeps the index of all the cells in an array and the second one creates a union of ranges from back to front
Option Explicit
Private Sub Main()
Dim InvertedRange As Range
Set InvertedRange = InvertRange(Application.Union(ActiveSheet.Range("A1:A2"), _
ActiveSheet.Range("F6:F7"), ActiveSheet.Range("E4:F5"), ActiveSheet.Range("E1")))
Dim ActualRange As Range
For Each ActualRange In InvertedRange
Debug.Print (ActualRange.Address(False, False) & " : " & ActualRange.Value)
Next ActualRange
End Sub
Public Function InvertRange(ByVal rngRange_I As Range) As Range
Dim RangesArray() As Long
ReDim RangesArray(1 To rngRange_I.Count, 1 To rngRange_I.Count)
Dim ActualArea As Range
Dim ActualRange As Range
Dim ArrayIndex As Long
For Each ActualArea In rngRange_I.Areas
For Each ActualRange In ActualArea
ArrayIndex = ArrayIndex + 1
RangesArray(ArrayIndex, 1) = ActualRange.Row
RangesArray(ArrayIndex, 2) = ActualRange.Column
Next ActualRange
Next ActualArea
Dim ActualRow As Long
Dim ActualColumn As Long
ActualRow = RangesArray(UBound(RangesArray, 1), 1)
ActualColumn = RangesArray(UBound(RangesArray, 2), 2)
With rngRange_I.Worksheet
Dim InvertedRange As Range
Set InvertedRange = .Cells(ActualRow, ActualColumn)
For ArrayIndex = UBound(RangesArray, 1) To LBound(RangesArray, 1) Step -1
ActualRow = RangesArray(ArrayIndex, 1)
ActualColumn = RangesArray(ArrayIndex, 2)
Set InvertedRange = Application.Union(InvertedRange, _
.Cells(ActualRow, ActualColumn))
Next ArrayIndex
End With
Set InvertRange = InvertedRange
End Function
You can use a stack (LIFO data structure) for inventing your list and the code would be something like this:
Dim aStack as Object
Set aStack = CreateObject("System.Collections.Stack")
For Each arngCell in Selection.Cells
aStack.Push(arngCell)
Next
While aStack.Count > 0
rngCell = aStack.Pop
' ...
End While
Set stack = Nothing