VBA: Second loop overwriting first loop - loops

Quick and, I assume, relatively easy question regarding VBA in Excel. I'm writing my first nested loop and I'm running into the problem where my second increment overwrites the first one. Here's the code:
Sub RandNumRang()
Dim i As Integer, Row As Integer, Col As Integer
Dim j As Integer
Dim LastRow As Long
Dim NoRows As Integer
Dim MinNum As Double, MaxNum As Double
Dim NumFormula As String
Col = 1 'Write random numbers on Column A
'Get the range and number of random values needed
For j = 3 To 4
NoRows = ActiveSheet.Range("G" & j).Value 'No of random values needed
MinNum = ActiveSheet.Range("E" & j).Value 'Min Range
MaxNum = ActiveSheet.Range("F" & j).Value 'Max Range
NumFormula = "=RANDBETWEEN(" & MinNum & "," & MaxNum & ")" 'Generate between Range
i = 0
For Row = 2 To NoRows + 1
i = i + 1
ActiveSheet.Cells(Row, Col).Value = i 'ID
ActiveSheet.Cells(Row, Col + 1).Formula = NumFormula 'Random Value
ActiveSheet.Cells(Row, Col + 2).FormulaR1C1 = "=TIME(0,0,RC[-1])" 'Convert to time
Next Row
Next j
End Sub
I'm trying to generate a set of random time intervals. The code does exactly what I want it to do except my values are being overwritten. The j loop will eventually have more than 2 increments but this code will have the same overwriting problem regardless. I'm pretty sure it has something to do with me specifying
For Row=2 To NoRows + 1
as the starting point of the nested loop but again this being my first run at a VBA script I figured I'd reach out because every fix I've tried has failed. Thanks in advance, and let me know if you need any further details.
Also, I have attached a screenshot of the worksheet for reference:

So far as I can see it is performing as expected. You set:
Col = 1
This then remains the same for both loops. Then you have:
For Row = 2 To NoRows + 1
So in your second loop you are always going to start at row 2, column 1 so it is always going to overwrite.
You either need to set Col to be 4 (or 5) in the second iteration or else keep a track of where you are in the rows and append to the end,

This works for your example:
Sub RandNumRang()
Dim Row As Integer, Col As Integer
Dim j As Integer
Dim LastRow As Long
Dim NoRows As Integer
Dim MinNum As Double, MaxNum As Double
Dim NumFormula As String
Col = 1
Row = 2
For j = 3 To 4
NoRows = Range("G" & j).Value
MinNum = Range("E" & j).Value
MaxNum = Range("F" & j).Value
NumFormula = "=RANDBETWEEN(" & MinNum & "," & MaxNum & ")"
For Row = Row To (Row + NoRows - 1)
Cells(Row, Col).Value = Row - 1
Cells(Row, Col + 1).Formula = NumFormula 'Random Value
Cells(Row, Col + 2).FormulaR1C1 = "=TIME(0,0,RC[-1])" 'Convert to time
Next Row
Next j
End Sub

Related

Loop to return an array when row number and array number does not match. Problems with nested loop

I have a problem where I have created an Array with variables and I want to enter the values in my Array in a separate column which does not match the row index of my Array.
I want to loop through a column and I want to return a value from an Array which does not correspend with the row index of the column. That could for example be to return the first value of my Array on the sixth row.
I Think that my problem probably lies in that I don't know how to set up the nested loop.
Many thanks for any help
I have created my Array like this
Sub arraytest()
Dim MonthArray() As String
Dim Lastrow As Long
Dim StartRow As Byte
StartRow = 2
Dim r As Byte
Lastrow = Range("B" & StartRow).CurrentRegion.Rows.count
If Lastrow > 0 Then
ReDim MonthArray(StartRow To Lastrow)
For r = StartRow To Lastrow
MonthArray(r) = Range("C" & r).Value
Next r
End If
End Sub
So if I have the values in my Array
MonthArray()
Month 1
Month 2
Month 3
Month 4
Month 5
Month 6
Then a simple loop without taking into account row index would be
For i = StartRow To Lastrow
If (Cells(i, "A").Value = "USA:" or Cells(i, "A").Value = "EU:") Then _
Cells(i, "B").Value = " " Else Cells(i, "B").Value = MonthArray(i) <<<
Next i
This would return a table in this order
1 USA:
2 Data MonthArray(2)
3 Data MonthArray(3)
4 EU:
5 Data MonthArray(5)
6 Data MonthArray(6)
But I need the array to be returned like this:
1 USA:
2 Data MonthArray(1)
3 Data MonthArray(2)
4 EU:
5 Data MonthArray(3)
6 Data MonthArray(4)
So, in this case, I want to add the value from my Array if the value in the A column is not USA or EU
What I have tried is this
r = 1
For i = StartRow To Lastrow
If (Cells(i, "A").Value = "USA" or Cells(i, "A").Value = "EU") Then _
Cells (i, "B").Value = " " Next i Else Cells(i, "B").Value = MonthArray (r) <<<
r = r + 1
Next i
However, I want
r = r + 1
To occur only if (Cells(i, "A").Value = "USA" or Cells(i, "A").Value = "EU")
Any help is highly appreciated
If you have a contiguous range for your MonthArray, don't worry about looping and just use:
Dim MonthArray() As Variant, StartRow as Long, LastRow as Long
StartRow = 2
Lastrow = Cells(StartRow, "B").CurrentRegion.Rows.count
MonthArray = Range(Cells(StartRow, "C"), Cells(LastRow, "C")).Value
Then we move into using the array, like your code indicates:
Dim r as Long, i as Long
r = 1
For i = StartRow To Lastrow
If UCase(Cells(i, "A").Value) = "USA" or UCase(Cells(i, "A").Value) = "EU" Then
Cells(i, "B").Value = " "
Else
Cells(i, "B").Value = MonthArray(r,1)
r = r + 1
End If
Next i
Need your r = r+1 in the loop as you move down.
Edit1:
Make sure to add in Sheet references. Assumption made from my testing, where I don' want to be overwriting my cells in B if I determine LastRow based on col B, e.g.:
With Sheets("MonthSource")
Dim MonthArray() As Variant, StartRow as Long, LastRow as Long
StartRow = 2
Lastrow = .Cells(StartRow, "B").CurrentRegion.Rows.count
MonthArray = .Range(.Cells(StartRow, "C"), .Cells(LastRow, "C")).Value
End With
With Sheets("Destination")
Dim r as Long, i as Long
r = 1
For i = StartRow To Lastrow
If UCase(.Cells(i, "A").Value) = "USA" or UCase(.Cells(i, "A").Value) = "EU" Then
.Cells(i, "B").Value = " "
Else
.Cells(i, "B").Value = MonthArray(r,1)
r = r + 1
End If
Next i
End With
Something like this should work for you:
Sub tgr()
Dim ws As Worksheet
Dim MonthArray() As Variant
Dim StartRow As Long
Dim LastRow As Long
Dim i As Long, r As Long
'Always fully qualify workbook and worksheet you're working with, change this as necessary
Set ws = ActiveWorkbook.ActiveSheet
StartRow = 2
LastRow = ws.Cells(StartRow, "B").CurrentRegion.Rows.Count
'Load the values of column C into an array directly, no loop required
With ws.Range(ws.Cells(StartRow, "C"), ws.Cells(LastRow, "C"))
If .Row < StartRow Then Exit Sub 'No data
If .Cells.Count = 1 Then
'Only a single value found in column C, force array type by manually redimming and adding the value
ReDim MonthArray(1 To 1, 1 To 1)
MonthArray(1, 1) = .Value
Else
'Multiple values found in column C, can insert values into array directly
MonthArray = .Value
End If
End With
'Initialize your array index counter variable at 0 to start
r = 0
'Begin loop of rows
For i = StartRow To LastRow
'Check contents of column A
Select Case UCase(Trim(ws.Cells(i, "A").Value))
Case "USA:", "EU:"
'do nothing
Case Else
'increase array index counter variable
r = r + 1
'Output the appropriate array value to column B
ws.Cells(i, "B").Value = MonthArray(r, 1)
End Select
Next i 'advance row counter
End Sub

Loop to replicate values into an array

I’m in a situation where I need to reproduce something in VBA and a bit stuck given my lack of understanding of object oriented language and VBA in general.
Problem:
I need to produce an array or vector based on a 2 column table.
The first range (column) contains unit counts.
The second range (column) contains numeric values.
I need to replicate the value based on the number of units.
For example,
if the first row contains 3 units with a value of $100
I need the array to contain $100, $100, $100.
This will need to be looped thru each row containing units.
So if row 2 contains 2 units with a value of $50
I need to complete array to be $100, $100, $100, $50, $50, and so on.
I understand this situation will require ReDim the array based on the total values. My struggle is I’ve been unable to figure out the nested for loops.
I get how to replicate the value based on the number of “units” like the below...
ReDim arr(0 To x - 1)
For i = 0 To x - 1
arr(i) = rng.Offset(0, 1).Value
Next
What is the best way to loop thru each row and replicate the values for each row in the range based on the unit count?
If anyone is familiar with R, I'm essentially looking for something that achieves the rep() function (e.g., rep(df$b, df$a)) and return the values in a single array.
Any help is greatly appreciated. Thanks
Or a one liner which uses the REPT function as you would have used in r :)
This assumes your data is in A1:B10 - the length can be made variable
s = Split(Join(Application.Transpose(Evaluate("=INDEx(REPT(B1:B10&"","",A1:A10),,1)"))), ",")
An an example, to dump the new to array to C1
s = Split(Join(Application.Transpose(Evaluate("=INDEx(REPT(B1:B10&"","",A1:A10),,1)"))), ",")
[c1].Resize(UBound(s), 1) = Application.Transpose(s)
When you say Row contains 3 units, do you mean the cell has value 3 or 3 Units? If it is 3 then you may not have to Redim the array in the loop. Simply find the sum of values in Col A which has units and Redim it in one go as shown below.
Sub Sample()
Dim ws As Worksheet
Dim Ar() As String
Dim n As Long, i As Long, lRow As Long
'~~> Change this to the relevant sheet
Set ws = Sheet6
With ws
n = Application.WorksheetFunction.Sum(.Columns(1))
ReDim Ar(t To n)
lRow = .Range("A" & .Rows.Count).End(xlUp).Row
n = 1
For i = 1 To lRow
If Len(Trim(.Range("A" & i).Value)) <> 0 Then
For j = 1 To .Range("A" & i).Value
Ar(n) = .Range("B" & i).Value
n = n + 1
Next j
End If
Next i
For i = LBound(Ar) To UBound(Ar)
Debug.Print Ar(i)
Next i
End With
End Sub
Screenshot
And if the cell has 3 Units then you will have to store the values of Col A in an array, do a replace on Unit/Units, find the sum and finally use the above code. Here is an example
Sub Sample()
Dim ws As Worksheet
Dim Ar() As String, tmpAr As Variant
Dim n As Long, i As Long, j As Long, k As Long, lRow As Long
'~~> Change this to the relevant sheet
Set ws = Sheet6
With ws
lRow = .Range("A" & .Rows.Count).End(xlUp).Row
tmpAr = .Range("A1:A" & lRow).Value
For i = LBound(tmpAr) To UBound(tmpAr)
tmpAr(i, 1) = Replace(tmpAr(i, 1), "Units", "")
tmpAr(i, 1) = Trim(Replace(tmpAr(i, 1), "Unit", ""))
n = n + Val(tmpAr(i, 1))
Next i
ReDim Ar(t To n)
n = 1
For i = 1 To lRow
If Len(Trim(.Range("A" & i).Value)) <> 0 Then
k = Val(Trim(Replace(Replace(.Range("A" & i).Value, "Units", ""), "Unit", "")))
For j = 1 To k
Ar(n) = .Range("B" & i).Value
n = n + 1
Next j
End If
Next i
For i = 1 To UBound(Ar)
Debug.Print Ar(i)
Next i
End With
End Sub
Screenshot
if your data is already in an array then ReDim'ing will delete it's contents. You can ReDim Preserve but it's an expensive operation, better to create a new array to put the results into.
I have assumed the data is contained within a Named Range called "Data" with Units being the first column and Values being the second column.
if your data changes regularly you can create a dynamic range using the OFFSET function i.e. =OFFSET(Sheet1!$A$1,0,0,COUNTA(Sheet1!$A:$A),2) assuming your data starts in cell A1 and there is no header row.
Sub ProcessData()
Dim DataArr() As Variant
Dim QtyColArr() As Variant
Dim ResultArr() As Variant
Dim TotalQty As Long
Dim i As Long, j As Long, k As Long
'store data into array
DataArr = Range("Data") 'assume data stored in named range called "Data"
'store Qty col into 1D array
QtyColArr = Range("Data").Resize(, 1)
'sum all qty vals
TotalQty = Application.Sum(QtyColArr)
're-size ResultsArray
ReDim ResultArr(1 To TotalQty)
'Initialize ResultsArr counter
k = LBound(ResultArr)
'loop DataArr
For i = LBound(DataArr) To UBound(DataArr)
'loop qty for current row
For j = 1 To DataArr(i, 1)
'copy value
ResultArr(k) = DataArr(i, 2)
'iterate ResultsArr counter
k = k + 1
Next j
Next i
'output to intermediate window
Debug.Print "{" & Join(ResultArr) & "}"
End Sub

using row range to dim array, and use array to iterate through For loop

Please look at my sample data and code to understand what I'm trying to do.
I need to use the value of Cells(, 3) to define a range to populate a Trialnumber(18) array. I need the array to iterate through a For loop, to count filled cells in column H for each trial and print the count to column T in the last row of each trial. I will also need the array for further data analysis in future(Unless someone can come up with a better solution).
At the moment I am experimenting with 3 modules of code, trying to get the desired solution.
Module 2 is the only one with no errors, and prints the value in the right cell, but it is printing the total filled cell count (562), rather than per trial (expected value = 1 or 2).
Module 1 is as follows:
Sub dotcountanalysis()
Dim startpoint As Long
startpoint = 1
Dim lastrow As Long
lastrow = Cells(Rows.Count, 3).End(xlUp).Row
Dim i As Long
With Worksheets("full test")
For i = 1 To 18
For n = startpoint To lastrow + 1
If Cells(n, 3).Value <> "Trial, " & CStr(i) Then
Dim nMinusOne As Long
nMinusOne = n - 1
Dim trialCount As Long
'Set Trialnumber(i-1) = Range(cells(startpoint, 3), cells(n-1, 3))
trialCount = Application.WorksheetFunction.CountA(Range("H" & CStr(startpoint) & ":" & "H" & CStr(nMinusOne)))
Range("T" & CStr(startpoint) & ":" & "T" & CStr(nMinusOne)).Value = trialCount
startpoint = n
Exit For
End If
Next n
Next i
End With
End Sub
It returns a "method _range of object _global falied" error on line: trialCount = Application.WorksheetFunction.CountA(Range("H" & CStr(startpoint) & ":" & "H" & CStr(nMinusOne)))
Module 3 is as follows:
Sub dotcountanalysis3()
Dim pressedCount As Long
Dim myCell As Range
Dim pressedRange As Range
'create trials array
Dim t(18) As Range
'set range for trialnumber (t)
Dim startpoint As Long
startpoint = 1
Dim lastrow As Long
lastrow = Cells(Rows.Count, 3).End(xlUp).Row
For i = 1 To 18
For n = startpoint To lastrow
startpoint = 7
If Cells(n, 3).Value <> "Trial, " & CStr(i) Then
Set t(i - 1) = Range(Cells(startpoint, 3), Cells(n, 3))
n = n + 1
startpoint = n
Exit For
End If
Next n
Next i
'count presses in each trial
With Worksheets("full test")
For i = 0 To 17
pressedCount = Application.WorksheetFunction.CountA _
(.Range(.Cells(t(), "H"), .Cells(.Rows.Count, "H")))
If pressedCount = 0 Then Exit Sub
'make sure there are cells or else the next line will fail
Set pressedRange = .Columns("H").SpecialCells(xlCellTypeConstants)
For Each myCell In pressedRange.Cells
'only loop through the cells containing something
.Cells(myCell.Row, "T").Value = pressedCount
Next myCell
Next i
End With
End Sub
It returns a run-time "type mismatch" error on line: pressedCount = Application.WorksheetFunction.CountA _
(.Range(.Cells(t(), "H"), .Cells(.Rows.Count, "H")))
Edit: I have updated code in mod 3 and updated error.
When counting things I like to use a dictionary object, and arrays are faster than going row by row on the sheet.
This will count unique combinations of Block+Trial: to count only by trial you would just use k = d(r, COL_TRIAL)
Dim dBT As Object 'global dictionary
Sub dotcountanalysis()
'constants for column positions
Const COL_BLOCK As Long = 1
Const COL_TRIAL As Long = 2
Const COL_ACT As Long = 7
Dim rng As Range, lastrow As Long, sht As Worksheet
Dim d, r As Long, k, resBT()
Set sht = Worksheets("full test")
lastrow = Cells(Rows.Count, 3).End(xlUp).Row
Set dBT = CreateObject("scripting.dictionary")
Set rng = sht.Range("B7:H" & lastrow)
d = rng.Value 'get the data into an array
ReDim resBT(1 To UBound(d), 1 To 1) 'resize the array which will
' be placed in ColT
'get unique combinations of Block and Trial and counts for each
For r = 1 To UBound(d, 1)
k = d(r, COL_BLOCK) & "|" & d(r, COL_TRIAL) 'create key
dBT(k) = dBT(k) + IIf(d(r, COL_ACT) <> "", 1, 0)
Next r
'populate array with appropriate counts for each row
For r = 1 To UBound(d, 1)
k = d(r, 1) & "|" & d(r, 2) 'create key
resBT(r, 1) = dBT(k) 'get the count
Next r
'place array to sheet
sht.Range("T7").Resize(UBound(resBT, 1), 1) = resBT
'show the counts in the Immediate pane (for debugging)
For Each k In dBT
Debug.Print k, dBT(k)
Next k
End Sub

How do I use an array to speed up this process?

I want to assign and store two calculated values to a single scenario ("i") in an array. Then I want to dump one of those values (for each "i") in one column and the other value in another column, once the loops are completed. If you look under 'UI, that's what I want to essentially accomplish, but I want them all to spit out at once after the loops are completed, instead of each one spitting out one at a time. I heard an array would be the best/fastest way to do this, but I don't know how to even go about using one.
Thanks
Sub Test_Scenarios()
Dim i As Long, Scenario_Count As Long
Dim j As Integer
'Delete current values on "Testing Output" tab
Sheets("Testing Output").Range("B1:B3").ClearContents
Sheets("Testing Output").Range("A6:AA1000000").ClearContents
'Test scenarios
Scenario_Count = Sheets("Testing Input").Range("B1").Value
For i = 1 To Scenario_Count
For j = 1 To 2
If j = 1 Then Sheets("AA").Range("ZC").Value = "No"
If j = 2 Then Sheets("AA").Range("ZC").Value = "Yes"
Calculate
'UI
If j = 1 Then Sheets("Testing Output").Range("R" & 5 + i).Value = Sheets("User Input").Range("B26").Value
If j = 2 Then Sheets("Testing Output").Range("S" & 5 + i).Value = Sheets("User Input").Range("B26").Value
Next j
Next i
End Sub
There's a good discussion of using Arrays to with Excel ranges at http://www.cpearson.com/excel/ArraysAndRanges.aspx, but I'll include some basics here.
To read data from an Excel Range in to an array:
Dim Arr() As Variant
Arr = Range("A1:B10")
To write data from an array to an Excel Range:
Range("E1:F10").Value = Arr
When writing the array back to the range, the size of the array must match the size of the Range. You can check the size of the array using UBound:
myRange.Resize(UBound(Arr, 1), UBound(Arr, 2))
You access data in the array by specifying the position in each dimension:
Arr(2, 3) = 7
Edit due to extra info about the question:
The example below creates an empty array and sizes it according to the number of scenarios, then stores values as it goes through the loop. The values from the loop are written to the output range after the loops are complete:
Option Base 1
Sub Test_Scenarios()
Dim i As Long, Scenario_Count As Long
Dim j As Integer
'Delete current values on "Testing Output" tab
Sheets("Testing Output").Range("B1:B3").ClearContents
Sheets("Testing Output").Range("A6:AA1000000").ClearContents
'Test scenarios
Scenario_Count = Sheets("Testing Input").Range("B1").Value
Dim arr() As Variant
ReDim arr(Scenario_Count, 2)
Dim outputRange As Range
Set outputRange = Sheets("Testing Output").Range("R5")
Set outputRange = outputRange.Resize(Scenario_Count, 2)
For i = 1 To Scenario_Count
For j = 1 To 2
'Calculate
Sheets("User Input").Range("B26").Value = Sheets("User Input").Range("B26").Value + i + j
'UI
arr(i, j) = Sheets("User Input").Range("B26").Value
Debug.Print "i: " & i & " j: " & j & " value: " & arr(i, j)
Next j
Next i
outputRange.Value = arr
End Sub
The loops are still reading and writing to the spreadsheet, as we don't have any other information about the calculations.

Find multiple values, concatenate cooresponding values in other column, write to cell

Problem:
Nothing is being written into cells in column P. The line Cells(x, "P").Value = failingClasses should do this.
Description: (VBA script below)
I've got a column with ID numbers. There can be multiple rows with each ID number. What I need to do is concatenate all the corresponding values in another column and write this into a cell in the original row. This needs to be done for each row in the sheet.
Field 1 is where the IDs are, field 6 is where the information I want to concatenate is, I'm trying to write the concatenation into column P.
Right now, I think that the computation is being done correctly, but for what ever reason it isn't writing to the cell in P?
Macro takes for ever to run. Between 1k and 2k rows when run.
Thanks!
Worksheets("RAW GRADE DATA").Select
' Turn off auto calc update and screen update -- saves speed
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False
Dim x As Long, y As Long, totalGradeEntries As Long, failingClasses As String, failingClassesCell As Excel.Range
totalGradeEntries = Cells(Rows.Count, 1).End(xlUp).Row
For x = totalGradeEntries To 1 Step -1
failingClasses = ""
For y = totalGradeEntries To 1 Step -1
If Cells(y, 1).Value = Cells(x, 1).Value And Cells(x, 6) <> "02HR" Then
failingClasses = failingClasses & " " & Cells(y, 1).Value
End If
Cells(x, "P").Value = failingClasses
Next y
Next x
' Turn calc and screen update back on
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
I got the bones of a solution to this work, thanks to Ron Rosenfeld -- Here is the code, working on a test sheet with 3 columns of data, the Unique IDs being in column 1.
Sub CalcArrary()
'Declare variables
Dim numRows As Integer, calcArray() As Variant
'Set the number of rows in the sheet
numRows = ActiveSheet.Range("A1").Offset(Sheet1.Rows.Count - 1, 0).End(xlUp).Row
ReDim calcArray(numRows - 1, 4)
For i = 0 To numRows - 2
calcArray(i, 1) = Range("A" & i + 2)
calcArray(i, 2) = Range("B" & i + 2)
calcArray(i, 3) = Range("C" & i + 2)
Next i
For b = 0 To numRows - 2
For c = 0 To numRows - 2
If calcArray(c, 1) = calcArray(b, 1) And calcArray(c, 3) < 60 Then
calcArray(b, 4) = calcArray(b, 4) & calcArray(c, 2) & ", " & calcArray(c, 3) & "% "
End If
Next c
Next b
For d = 0 To numRows - 2
ActiveSheet.Range("D" & d + 2) = calcArray(d, 4)
Next d
End Sub

Resources