The problem i run into is that sometimes entire headers and data values are missing in the dataset and therefore using the last row in the script the data is shifted up by one. For example, if i removed H11:H12 completely on sheet1 then the values for the H column associated with the data set in A11:K11 will actually be from the data set A13:K13 (or cell value H14).
The spaces shown in the second image would not be present if the respective header is not present.
Question: Given the following code; Do you think it is possible to match the data to headers and use the original offset row number alongside the column that it is matched to on sheet 2 and paste the values there? Instead the current code (and only method that worked was to find the last row).
Examples/Thoughts:
I'm thinking that the script will have to take a cell (such as D9 and recognizes it is a D and offsets to select D10 and matches that D9 record to sheet 2 column D and pastes the D10 data in D10 rather than D5.
second example, Script takes I17 and recognizes it matches I to sheet 2 column I and then offsets to select/copy and pastes the I19 data in I18 rather than I9.
Sub main()
Dim hedaerCell As Range
Dim labelsArray As Variant
With ThisWorkbook.Worksheets("Sheet2") '<--| reference "headers" worksheet
For Each hedaerCell In .Range("A1:K1") '<--| loop through all "headers"
labelsArray = GetValues(hedaerCell.Value) '<--| fill array with all labels found under current "header"
.Cells(.Rows.Count, hedaerCell.Column).End(xlUp).Offset(1).Resize(UBound(labelsArray)).Value = Application.Transpose(labelsArray)
Next
End With
End Sub
Function GetValues(header As String) As Variant
Dim f As Range
Dim firstAddress As String
Dim iFound As Long
With ThisWorkbook.Worksheets("Sheet1").UsedRange '<--| reference "data" worksheet
ReDim labelsArray(1 To WorksheetFunction.CountIf(.Cells, header)) As Variant '<--| size an array to store as many "labels" as passed 'header' occurrences
Set f = .Find(what:=header, LookIn:=xlValues, lookat:=xlWhole) '<--| start seraching for passed 'header'
If Not f Is Nothing Then
firstAddress = f.Address
Do
iFound = iFound + 1
labelsArray(iFound) = f.Offset(1)
Set f = .FindNext(f)
Loop While f.Address <> firstAddress
End If
End With
GetValues = labelsArray
End Function
Addition:
Seems like there is an exception that prevents these cell values from being copied over, if i do it manually the below screenshot would be correct. Any tips to diagnose?
Very strange because the line with the red dot copies fine in both but those four lines seem to fail.
I'm leaving my previous answer up for posterity's sake, but now that you've clarified your question I have a better answer for you.
I'm going to assume the following: 1. every two rows is a pair of headers/data; 2. the sets of row pairs may be unequal in length because if a particular header is missing for a particular row pair, there is no blank because the headers/data are shifted left; 3. there will be no blanks in the header rows until the end of the row 4. there may be blanks in the data row 5. the output should be every header (even if it only appears in 1 row) and rows of the associated data, one per header/data pair in the original sheet.
For example:
A|B|C|D|F|G|H|I <--- some headers (missing E)
1|2|3|4|6|7|8|9 <--- data row 1
A|C|D|E|G|H|I <--- some headers (missing B and F)
1|3|4|5|7|8|9 <--- data row 2
is a valid input sheet and the resulting output sheet would be:
A|B|C|D|E|F|G|H|I <--- all headers
1|2|3|4| |6|7|8|9 <--- data row 1
1| |3|4|5| |7|8|9 <--- data row 2
Use a Scripting.Dictionary of Scripting.Dictionarys to keep track of the possibly different length row pairs of headers/data. The Scripting.Dictionary of headers allows you to add new headers as they appear. The nested Scripting.Dictionarys allow you to keep track of only those rows which have a value for a particular header, but also preserve the row number for later.
As noted in the comments, the code iterates through this structure to display ALL headers and the data associated with each row. "((inputRow - 1) / 2)" calculates the output row number. You'll notice I like to iterate for loops over the count and then use offsets for indexing. I find it easier to reason about my code this way, and I find operations are easier, but you could potentially change it if you want.
Public Sub CopyDataDynamically()
Dim inputSheet As Worksheet
Dim outputSheet As Worksheet
Dim headers As Scripting.Dictionary
Set headers = New Scripting.Dictionary
Dim header As String
Dim data As String
Dim inputRow As Long
Dim inputColumn As Long
Set inputSheet = Worksheets("Sheet1")
Set outputSheet = Worksheets("Sheet2")
inputRow = 1
While Not inputSheet.Cells(inputRow, 1) = ""
inputCol = 1
While Not inputSheet.Cells(inputRow, inputCol) = ""
header = inputSheet.Cells(inputRow, inputCol).Value
data = inputSheet.Cells(inputRow + 1, inputCol).Value
If Not headers.Exists(header) Then
headers.Add header, New Scripting.Dictionary
End If
headers(header).Add ((inputRow - 1) / 2) + 1, data
inputCol = inputCol + 1
Wend
inputRow = inputRow + 2
Wend
'Output the structure to the new sheet
For c = 0 To headers.Count - 1
outputSheet.Cells(1, c + 1).Value = headers.Keys(c)
For r = 0 To ((inputRow - 1) / 2) - 1
If headers(headers.Keys(c)).Exists(r + 1) Then
outputSheet.Cells(r + 2, c + 1).Value = headers(headers.Keys(c))(r + 1)
End If
Next
Next
End Sub
I suggest, rather than copying column by column, you instead copy row by row.
Public Sub CopyData()
Dim inputRow As Long
Dim outputRow As Long
Dim inputSheet As Worksheet
Dim outputSheet As Worksheet
Set inputSheet = Worksheets("Sheet1")
Set outputSheet = Worksheets("Sheet2")
'First, copy the headers
inputSheet.Rows(1).Copy outputSheet.Rows(1)
'Next, copy the first row of data
inputSheet.Rows(2).Copy outputSheet.Rows(2)
'Loop through the rest of the sheet, copying the data row for each additional header row
inputRow = 3
outputRow = 3
While inputSheet.Cells(inputRow, 1) <> ""
inputRow = inputRow + 1 'increment to the data row
inputSheet.Rows(inputRow).Copy outputSheet.Rows(outputRow)
inputRow = inputRow + 1 'increment to the next potential header row
outputRow = outputRow + 1 'increment to the next blank output row
Wend
End Sub
Related
I have not used VBA for sometime so am very rusty... What I have is a number of records stored vertically (in a single column) and I want to use VBA to stack them side by side (into a table).
My general thoughts about how this would flow:
Start at first range
Copy data
Paste data in cell B3 of output page (just named Sheet2)
Loop back to previous range and offset by 51 rows
Copy data
Paste data in cell C3 of output page (offset by 1 column each time)
My attempt so far:
Sub Macro1()
FiftyOne = 51 ' Offset by 51 rows for every chunk
StartRange = "L262:L303" ' Start at this range of data to copy, each chunk is identical in size
OutputRange = B3 ' Paste in output at B3, but need to offset by one column each time
Range(StartRange).Offset(FiftyOne, 0).Select
Selection.Copy
Sheets("Sheet2").Select
Range("B3").Offset(0, 1).Select
ActiveSheet.Paste
End Sub
I know this is a rather lame attempt to tackle this flow, but I am really struggling with how to loop through this. I would appreciate some advice on how to do this, or a better approach to the general flow.
Edit after accepting Wolfie's answer:
I want to assign column headings, by getting the values from C258 and looping down (in a similar way to before) 51 rows at a time, to paste into row 2 of sheet2 (B2, C2, ...).
Here is my current attempt:
Sub NameToTable()
' Assign first block to range, using easily changable parameters
' Remember to "Dim" all of your variables, using colon for line continuation
Dim blocksize As Long: blocksize = 51
Dim firstrow As Long: firstrow = 258
Dim rng As Range
Set rng = ThisWorkbook.Sheets("Sheet1").Range("C" & firstrow & blocksize - 1)
' tablestart is the upper left corner of the "pasted" table
Dim tablestart As Range: Set tablestart = ThisWorkbook.Sheets("Sheet2").Range("B2")
Dim i As Long ' Looping variable i
Dim nblocks As Long: nblocks = 10 ' We're going to loop nblocks number of times
For i = 0 To nblocks - 1
' Do the actual value copying, using Resize to set the number of rows
' and using Offset to move down the original values and along the "pasted" columns
tablestart.Offset(0, i).Resize(blocksize, 1).Value = _
rng.Offset(blocksize * i, 0).Value
Next i
End Sub
Your logic seems alright, this code will create a 51 x n table, lining up each vertical block of 51 cells in its own column.
Note, it's much quicker to assign the .Value than copying and pasting, if you need formats too then you could copy/paste or similarly set format properties equal.
Sub ColumnToTable()
' Assign first block to range, using easily changable parameters
' Remember to "Dim" all of your variables, using colon for line continuation
Dim blocksize As Long: blocksize = 51
Dim firstrow As Long: firstrow = 262
Dim rng As Range
Set rng = ThisWorkbook.Sheets("Sheet1").Range("L" & firstrow & ":L" & firstrow + blocksize - 1)
' tablestart is the upper left corner of the "pasted" table
Dim tablestart As Range: Set tablestart = ThisWorkbook.Sheets("Sheet2").Range("B3")
Dim i As Long ' Looping variable i
Dim nblocks As Long: nblocks = 10 ' We're going to loop nblocks number of times
For i = 0 To nblocks - 1
' Do the actual value copying, using Resize to set the number of rows
' and using Offset to move down the original values and along the "pasted" columns
tablestart.Offset(0, i).Resize(blocksize, 1).Value = _
rng.Offset(blocksize * i, 0).Value
Next i
End Sub
Set the nblocks value to suit your needs, this is the number of resulting columns in your output table. You could get it dynamically by knowing the number of rows in the original column. Or you could use some while logic, careful to make sure that it does eventually exit of course!
Dim i As Long: i = 0
Do While rng.Offset(blocksize*i, 0).Cells(1).Value <> ""
tablestart.Offset(0, i).Resize(blocksize, 1).Value = rng.Offset(blocksize * i, 0).Value
i = i + 1
Loop
Edit: to get your column headings, keep in mind that the column headings are only 1 cell, so:
' Change this:
Set rng = ThisWorkbook.Sheets("Sheet1").Range("C" & firstrow & blocksize - 1)
' To this:
Set rng = ThisWorkbook.Sheets("Sheet1").Range("C" & firstrow)
Tip: + is used for adding numerical values, whilst & is used for concatenating stings.
Now when you're looping, you don't need the Resize, because you are only assigning 1 cell's value to 1 other cell. Resulting sub:
Sub NameToTable()
Dim blocksize As Long: blocksize = 51
Dim firstrow As Long: firstrow = 258
Dim rng As Range
Set rng = ThisWorkbook.Sheets("Sheet1").Range("C" & firstrow)
Dim tablestart As Range: Set tablestart = ThisWorkbook.Sheets("Sheet2").Range("B2")
Dim i As Long: i = 0
Do While rng.Offset(blocksize*i, 0).Value <> ""
tablestart.Offset(0, i).Value = rng.Offset(blocksize * i, 0).Value
i = i + 1
Loop
End Sub
When dealing with your worksheets in excel, each time you reference them adds overhead and slows down the code, what you want to do is take all of the info off your spreadsheet into an array then use Application.Transpose to transpose it for you.
You can then use 'Resize' to make sure your destination range is the same size and set the values.
Sub CopyAndTransRange(src As Range, dest As Range)
Dim arr As Variant 'Needs to be a variant to take cell values
arr = Application.Transpose(src.Value) 'Set to array of values
On Error GoTo eh1dim 'Capture error from vertical 1D range
dest.Resize( _
UBound(arr, 1) - LBound(arr, 1) + 1, _
UBound(arr, 2) - LBound(arr, 2) + 1 _
) = arr 'Set destination to array
Exit Sub
eh1dim:
dest.Resize( _
1, _
UBound(arr) - LBound(arr) + 1 _
) = arr 'Set row to 1D array
End Sub
Note, Application.Transpose will fall over with some arrays in weird circumstances like if there is more than 255 characters in a string in the given array, for those situations you can write your own Transpose function to flip the array for you.
Edit:
When you feed a vertical 1-dimensional range and transpose it, VBA converts it to a 1-dimensional array, I've rewritten so it captures the error when this happens then adjusts accordingly.
Just made this example which has values 1 through 7 populated on the first 7 rows of column A. This code effectively loops through each of the values, and transposes horizontally so all values are on a single row (1).
Dim rng As Range
Dim crng As Range
Static value As Integer
Set rng = ActiveSheet.Range("A1", Range("A1").End(xlDown))
For Each crng In rng.Cells
ActiveSheet.Range("A1").Offset(0, value).value = crng.value
If value <> 0 Then
crng.value = ""
End If
value = value + 1
Next crng
First we grab the required range and then iterate through each cell. Then using the offset method and an incrementing integer, we can assign their values horizontally to a single row.
It's worth noting that this would work when trying to transpose both vertically and horizontally. The key is the offset(column, row).
Just adjust where you place your incrementing Integer.
Hope this helps.
I reformatted a range of Sheets("Records") in a workbook as a Table (named "RecordsTable") to make it easier to do INDEX(MATCH,MATCH) functions for generating reports.... but now I screwed up my looping routine for filling that range from the input on Sheets("FORM").
It used to be:
Set r = Sheets("Records").Range(A & Rows.Count).End(x1Up).Offset(1, 0)
i = 0
For Each c In Range("dataRange")
'dataRange is a list of cells to reference from the FORM input sheet
r.Offset(0, i).Value = Worksheets("FORM").Range(c)
i = i + 1
Next
However this code is now selecting the first row at the END of "RecordsTable" (row 501, as I defined 500 rows in my table) and inserting the data there.
I tried to change it to this:
Set r = Sheets("Records").ListObjects("RecordsTable").DataBodyRange("A" & Rows.Count).End(x1Up).Offset(1, 0)
i = 0
For Each c In Range("dataRange")
r.Offset(0, i).Value = Worksheets("FORM").Range(c)
i = i + 1
Next
But this code is still selecting row 501 and making that row part of "RecordsTable".
How can I properly Set "r" to = the first empty row in "RecordsTable"?
For reference, Column "A" in "RecordsTable" has the header [INV #]. Also, when I step into the "Set r = ..." line, Rows.Count is returning a value of 1million+ (ie, total rows on the sheet) - if I understand this correctly, I want it to return a value of 500 (ie, total rows in table) - is that correct?
EDIT
"dataRange" is a single column list of cell references (I do have them labeled in column B, as #chrisneilsen suggest:
A
J6
Y6
J8
J10
Y8
etc.
They are the cells on Sheets("FORM") that I need to pull data from and populate into my table, in the order indicated in "dataRange".
Assuming you really have a Table, adding data to a Table (ListObject) using it's properties and methods:
Sub Demo()
Dim lo As ListObject
Dim c As Range
Set lo = Worksheets("Records").ListObjects("RecordsTable")
For Each c In Sheets("V").Range("dataRange")
If Not lo.InsertRowRange Is Nothing Then
lo.InsertRowRange.Cells(1, 1) = Sheets("FORM").Range(c)
Else
lo.ListRows.Add.Range.Cells(1, 1) = Sheets("FORM").Range(c)
End If
Next
End Sub
Note: looping a range on sheet V and using that as a pointer to data on sheet FORM, copied from your answer - I'm assuming you know what you are doing here
Based on OP comment, adding data a single new row
Sub Demo()
Dim lo As ListObject
Dim c As Range, TableRange As Range
Dim i As Long
Set lo = Worksheetsheets("Records").ListObjects("RecordsTable")
If Not lo.InsertRowRange Is Nothing Then
Set TableRange = lo.InsertRowRange
Else
Set TableRange = lo.ListRows.Add.Range
End If
i = 1
For Each c In Sheets("V").Range("dataRange")
TableRange.Cells(1, i) = Sheets("FORM").Range(c)
i = i + 1
Next
End Sub
Note, this assumes that the order of the table columns is the same as the order of dataRange. It may be better to include table field names in dataRange to avoid any mismatch issues
As mentioned in updated OP, if column labels are in the next column, replace the For loop with this (and add Dim r as Range, col as long to declarations)
For Each c In Sheets("V").Range("dataRange")
If Not c = vbNullString Then
Set r = Worksheets("FORM").Range(c.Value)
col = lo.ListColumns(c.Offset(, 1).Value).Index
TableRange.Cells(1, col) = r.Value
End If
Next
Question detail
My code below stores the results from a calculation in array funds() and repeats that process for the selected range. By the end there will be a one dimensional array with 170 values. I need to access the array from a certain point every loop to fill the new row with different values.
Problem in detail
The core problem I am having is printing that array to a range on the workbook which is made up of 10 rows by 17 columns.
I managed to get it to go down a row once the "for each cell in selected range" loop exits, but now it fills the new row with the same initial array values!
Here is the current output:
What have i tried?
I have tried the Redim but get overwhelmed by the length of examples.
I have tried manual copy and paste but feel like that is cheating...
I have researched how to delete elements through the process of copying..
Overall I am sure there is a simple way that everyone knows how to use! but what is it?
In a nutshell...
Every loop remove the initial 17 values, then print the next 17 array values to new row in range. Repeat 10 times.
The code
Option Explicit
Public funds(0 To 170) As Variant
Sub cumulativeperformance()
Dim selectedrange As Range
Dim cell As Range
Dim value1 As Double
Dim Value2 As Long
Dim i, x, d, c As Integer
Dim total(0 To 170) As Double
'Copy the table to report
Worksheets("Data").Range("C3:T13").Copy
Sheets("Report").Range("B39").PasteSpecial
Worksheets("Data").Range("B3:T13").Copy
Sheets("Report").Range("A39").PasteSpecial xlPasteValues
'Repeat 9 times
c = 39
For d = 0 To 9
c = c + 1
Set selectedrange = Worksheets("Report").Range(Cells(c, 3), Cells(c, 19))
For Each cell In selectedrange
value1 = cell.Value
'get the value of cell to the left of current cell
Value2 = cell.Offset(0, -1).Value
'get the difference to previous month
value1 = value1 / Value2 - 1
'assign result + 1 to array
total(x) = value1 + 1
'If initial fund slot is 0, then store first result of calculation in that slot
If i = 0 Then
funds(i) = total(0) - 1
ElseIf i > 0 Then
'Do calculation on remaining values and store in fund array
funds(i) = (funds(i - 1) + 1) * total(i) - 1
End If
'MsgBox "cumulative performance: " & funds(I) & " Cell address: " & cell.Address
i = i + 1
x = x + 1
Next
'Write from one dimensional Array To The worksheet
Dim Destination As Range
Dim j As Integer
Set Destination = Range(Cells(c, 3), Cells(c, 3)) 'starts at
Set Destination = Destination.Resize(1, 17) 'UBound(funds))
Destination.Value = funds
'MsgBox "Hi there"
Next d
'one-off commands in here
Range("C40:S49").NumberFormat = "0.00%"
Call portfoliomay
End Sub
The destination range and the source array should have the same dimensions to be able to assign the values correctly, as commented by Ron Rosenfeld. This is possible by either using a 1-dimension array to reuse 10 times for just one row at a time array(columns), or a 2-dimensions array for the full destination range (10x17) array(rows, columns).
Method #1: 1-dimension array
Use a 1-dimension array of 17 values, for a row by row operation, one row at a time. Initially declare the array as a dynamic array Dim funds() ..., so you'll be able to easily reset it. Then set its zero based length ReDim funds(16) ... at the beginning of each For d = 0 To 9 iteration. The rest of your code will stay the same. With this method your original destination assignment should work as expected Destination.Value = funds (or with an equivalent shorter statement Cells(c, 3).Resize(1, 17) = funds).
Method #2: 2-dimensions array
You can declare funds as a zero based 2-dimensions array Dim funds(9, 16) .... But then there is no straight forward way to put the data in row by row. The destination assignment will be to the whole range at once Cells(40, 3).Resize(10, 17) = funds after your calculation loops end. You will also need to adjust the funds directives to indicate the row funds(d, i) = .... This may be the most efficient way (performance wise) to put in the data in your sheet, as putting data in cells is relatively time consuming.
*To do it row by row with a 2-dimensions array you'll have to use a workaround like the ones described here return an entire row of a multidimensional array in VBA to a one dimensional array.
Other adjustments
You will need to adjust your total array to have the same dimensions and directives as the funds, or adjust i and x calculations. To adjust i and x and leave total as is, add i = 0 at the beginning of your For d iteration, and use only total(x).
edited after OP's confirmation his goal is optimizing code (see at the end)
I'm adding a different "flavor" of array/ranges use and showing some possible code enhancements
Variant variable as array
there's no need to Dim or Redim any array, just declare ita as a pure Variant variable and fill it with the range values that will host the final results
some thing like
funds = repRng.Value
where repRng is the Range of the "Report" sheet that you want to fill with funds array itself
reduce variables
there's no need for a total array at all. just use a simple Double variable
Dim appropriately
Dim i, x, d, c As Integer
would result in declaring i, x and d variables as of Variant type and only c as of Integer type
to have all those variables declared as integer you must type:
Dim i As Integer, x As Integer, d As Integer, c As Integer
but we'll use much less of them
reduce code
since you're assigning
value1 = value1 / Value2 - 1
and then
total(x) = value1 + 1
you could merge those two statements into the single
total(x) = value1 / Value2
which, for what above said, we'll change to:
total = value1 / Value2
copy/paste
these statements:
Worksheets("Data").Range("C3:T13").Copy
Sheets("Report").Range("B39").PasteSpecial
Worksheets("Data").Range("B3:T13").Copy
Sheets("Report").Range("A39").PasteSpecial xlPasteValues
actually do the same as:
Worksheets("Data").Range("B3:T13").Copy
Sheets("Report").Range("A39").PasteSpecial xlPasteValues
which can also be written as:
With Worksheets("Data").Range("B3:T13")
Sheets("Report").Range("A39").Resize(.Rows.Count, .Columns.Count).Value = .Value
End With
this approach both reduces time (not an issue for such little a range) and doesn't use the clipboard (which at least you'd take care releasing with Application.CutCopyMode = False)
for what above said, this statement will be used to initialize repRng Range variable, too
With Worksheets("Data").Range("B3:T13")
Set repRng = Sheets("Report").Range("A39").Resize(.Rows.Count, .Columns.Count) '<--| define the range where to paste data
repRng.Value = .Value '<--| paste data
End With
Reduce variables (part2)
your d variable is used only for iterating through rows you just previously copied and pasted, but you're using hard coded values for its span and then making it relative to another hard coded reference row index (c = 39)
you'd better exploit consistente reference to the range you're actually dealing with, like (pseudo code)
Dim oneRow As Range
For Each oneRow In repRng.Rows '<--| loop through rows of your relevant data range
For Each cell In oneRow.Cells '<--| loop through cells of the current data range row
'code
Next cell
Next row
where repRng is a Range object referencing relevant cells of sheet "Report" you want to loop through
The final outcome will be the following code:
Option Explicit
Public funds As Variant '<--| declare the simple Variant variable that will be "turned" into an array as long as we'll initialize it to a "Range" values
Sub cumulativeperformance()
Dim cell As Range, repRng As Range, oneRow As Range
Dim value1 As Double, total As Double
Dim value2 As Long
Dim iRow As Long, jCol As Long '<--| better use "Long" instead of "Integer" when dealing with numbers that cope with Excel rows indexs
'Copy table values to report
With Worksheets("Data").Range("B3:T13")
Set repRng = Sheets("Report").Range("A39").Resize(.Rows.Count, .Columns.Count) '<--| define the range where to paste data
repRng.Value = .Value '<--| paste data
End With
With repRng
Set repRng = .Offset(1, 2).Resize(.Rows.Count - 1, .Columns.Count - 2) '<--| redefine the relevant data range to loop through
End With
With repRng '<--| assume your relevant data range as reference
funds = .Value '<--| have funds array properly dimensioned by filling it with relevant data pasted values: they'll be rewritten in following loops
For Each oneRow In .Rows '<--| loop through rows of your relevant data range
iRow = iRow + 1 '<--| update array row counter
jCol = 1 '<--|for each new row restart array column counter
For Each cell In oneRow.Cells '<--| loop through cells of the current data range row
value1 = cell.Value '<--|get the value of current cell
value2 = cell.Offset(0, -1).Value '<--|get the value of cell to the left of current cell
total = value1 / value2 '<--|evaluate the ratio
If jCol = 1 Then
funds(iRow, jCol) = total - 1 '<--| If initial fund slot is 1, then store first result of calculation in that slot
Else
funds(iRow, jCol) = (funds(iRow, jCol - 1) + 1) * total - 1 '<--| Do calculation on remaining values and store in fundS array
End If
jCol = jCol + 1 'update array column counter
Next cell
Next oneRow
.Value = funds '<--| fill your relevant data range with funds values
.NumberFormat = "0.00%"
End With
' Call portfoliomay
End Sub
further optimization would avoid the If jCol = 1 Then check for every row, since it's not up to some unknown condition: we know for sure that every new row will start with a column index 1
so, for every row, we can
act on its initial column:
funds(iRow, 1) = GetTotal(oneRow.Cells(1, 1)) - 1 'evaluate funds current row first slot (column)
relying on a specific GetTotal() function
Function GetTotal(cell As Range) As Double
Dim value1 As Double
Dim value2 As Long
value1 = cell.Value '<--|get the value of current cell
value2 = cell.Offset(0, -1).Value '<--|get the value of cell to the left of current cell
GetTotal = value1 / value2 '<--|evaluate the ratio
End Function
where we collected the code to calculate total value "attached" to a single cell
do calculation for subsequent columns
jCol = 2 '<--|for each new row restart array column counter
For Each cell In Range(oneRow.Cells(1, 2), oneRow.Cells(1, oneRow.Cells.Count)) '<--| evaluate funds current row remaining slots
funds(iRow, jCol) = (funds(iRow, jCol - 1) + 1) * GetTotal(cell) - 1
jCol = jCol + 1 'update array column counter
Next cell
exploiting the same GetTotal() function
Finally the updated code would be:
Option Explicit
Public funds As Variant '<--| declare the simple Variant variable that will be "turned" into an array as long as we'll initialize it to a "Range" values
Sub cumulativeperformance()
Dim cell As Range, repRng As Range, oneRow As Range
Dim iRow As Long, jCol As Long '<--| better use "Long" instead of "Integer" when dealing with numbers that cope with Excel rows indexs
'Copy table values to report
With Worksheets("Data").Range("B3:T13")
Set repRng = Sheets("Report").Range("A39").Resize(.Rows.Count, .Columns.Count) '<--| define the range where to paste data
repRng.Value = .Value '<--| paste data
End With
With repRng
Set repRng = .Offset(1, 2).Resize(.Rows.Count - 1, .Columns.Count - 2) '<--| redefine the relevant data range to loop through
End With
With repRng '<--| assume your relevant data range as reference
funds = .Value '<--| have funds array properly dimensioned by filling it with relevant data pasted values: they'll be rewritten in following loops
For Each oneRow In .Rows '<--| loop through rows of your relevant data range
iRow = iRow + 1 '<--| update array row counter
funds(iRow, 1) = GetTotal(oneRow.Cells(1, 1)) - 1 'evaluate funds current row first slot (column)
jCol = 2 '<--|for each new row restart array column counter
For Each cell In Range(oneRow.Cells(1, 2), oneRow.Cells(1, oneRow.Cells.Count)) '<--| evaluate funds current row remaining slots
funds(iRow, jCol) = (funds(iRow, jCol - 1) + 1) * GetTotal(cell) - 1
jCol = jCol + 1 'update array column counter
Next cell
Next oneRow
.Value = funds '<--| fill your relevant data range with funds values
.NumberFormat = "0.00%"
End With
' Call portfoliomay
End Sub
Function GetTotal(cell As Range) As Double
Dim value1 As Double
Dim value2 As Long
value1 = cell.Value '<--|get the value of current cell
value2 = cell.Offset(0, -1).Value '<--|get the value of cell to the left of current cell
GetTotal = value1 / value2 '<--|evaluate the ratio
End Function
some final(?) notes:
A. Public variables
these are used to share variables among different subs/function across different modules
but it's usually a bad practice using them, being preferable put those variables in subs/function parameters to carry them along where needed
with the code as in the question, there is no other sub/function using funds, so it better move its declaration into cumulativeperformance():
Option Explicit
Sub cumulativeperformance()
Dim funds As Variant '<--| declare the simple Variant variable that will be "turned" into an array as long as we'll initialize it to a "Range" values
Dim cell As Range, repRng As Range, oneRow As Range
B. simplify GetTotal()
it can be simplified to
Function GetTotal(cell As Range) As Double
With cell
GetTotal = .Value / .Offset(0, -1).Value '<--|evaluate the ratio
End With
End Function
taking advantage of the `With cell` statement and referring to it
I wrote the below macro in Excel (2010) VBA to add markers to contracts with various issues to a master tracker. While doing some size testing I am getting error 400 when I attempt to run with an input of 50,000 contracts (array Contracts), but it runs fine with 40,000 (took about 14 minutes). Any ideas at why I am getting the error? Commented in the code where it is stopping at 50,000. Thank you!
Sub UploadNew()
''''''''''''''''''''''''Add All Contracts to End of Master'''''''''''''''''''''''''''''''
'Set up the array Contracts which will house the new contracts to be uploaded
Dim Contracts() As String
Dim size As Long
Dim R As Integer
Dim N As Long
'This sets up the value for N as the end of the current master list
N = Worksheets("Master").Cells(Rows.Count, "A").End(xlUp).Row + 1
'Determine size of array and store it into variable size
size = Worksheets("Update").Cells(Rows.Count, "A").End(xlUp).Row - 1
'Identifies which Remediation column to add the marker to
R = Application.WorksheetFunction.VLookup(Worksheets("Update").Range("F2"), Range("E14:G263"), 3, False)
'Having counted size we can redimension the array
ReDim Contracts(size)
'Insert the values in column A into the array
Dim i As Long
For i = 1 To size
Contracts(i) = Range("A1").Offset(i)
Next i
'Takes each value in the array and adds it to the end of the master list using N
For i = 1 To size
Worksheets("Master").Range("A" & N).Value = Contracts(i)
N = N + 1
Next i
'Remove the duplicates from the master tab based on the first column
Worksheets("Master").Range("A:ZZ").RemoveDuplicates Columns:=Array(1)
'Remove blank rows from Master
Dim rng As Range
Set rng = Worksheets("Master").Range("A2:A" & N).SpecialCells(xlCellTypeBlanks)
rng.EntireRow.Delete
''''''''''''''''''''''''Add All Contracts to End of Master'''''''''''''''''''''''''''''''
'''''''''''''''''''''Place New Contract Marker for Each Contract'''''''''''''''''''''''''
'This searches all the contracts in the master and places a 1 R columns to the right of
'the found contract
For i = 1 To size
Dim rgFound As Range
Set rgFound = Worksheets("Master").Range("A2:A" & N).Find(Contracts(i))
'! Code is stopping about here with 50,000 contracts, doesn't add a single marker !'
With rgFound.Offset(, R)
.Value = "1"
.NumberFormat = "General"
End With
Next i
'''''''''''''''''''''Place New Contract Marker for Each Contract'''''''''''''''''''''''''
End Sub
This rewrite bulk loads and bulk unloads the array. I've swapped out a worksheet MATCH function for the Range.Find method since there should be guaranteed matches.
Sub UploadNew()
''''''''''''''''''''''''Add All Contracts to End of Master'''''''''''''''''''''''''''''''
'Set up the array Contracts which will house the new contracts to be uploaded
Dim Contracts As Variant
Dim i As Long, N As Long, R As Integer
With Worksheets("Update")
'Identifies which Remediation column to add the marker to
'I have no idea why you are looking up F2 in column E (and returning value from column G) on the Updates worksheet
R = Application.WorksheetFunction.VLookup(.Range("F2"), .Range("E14:G263"), 3, False)
'AT THIS POINT R SHOULD BE AN INTEGER BETWEEN 2 and 16384
'NOT LARGER OR SMALLER OR TEXT
'CHECK WITH A WATCH WINDOW!!!!!!!!!!!
'Insert the values in column A into the array (SKIP HEADER ROW)
Contracts = .Range(.Cells(2, "A"), .Cells(Rows.Count, "A").End(xlUp)).Value2
End With
With Worksheets("Master")
'This sets up the value for N as the end of the current master list
N = .Cells(Rows.Count, "A").End(xlUp).Row + 1
'Takes each value in the array and adds it to the end of the master list using N
.Range("A" & N).Resize(UBound(Contracts, 1), UBound(Contracts, 2)) = Contracts
'Remove the duplicates from the master tab based on the first column
.Range("A:ZZ").RemoveDuplicates Columns:=Array(1)
'Remove blank rows from Master
If CBool(Application.CountBlank(.Range("A2:A" & N))) Then _
.Range("A2:A" & N).SpecialCells(xlCellTypeBlanks).EntireRow.Delete
''''''''''''''''''''''''Add All Contracts to End of Master'''''''''''''''''''''''''''''''
'''''''''''''''''''''Place New Contract Marker for Each Contract'''''''''''''''''''''''''
'This searches all the contracts in the master and places a 1 R columns to the right of
'the found contract
For i = LBound(Contracts, 1) To UBound(Contracts, 1)
With .Cells(Application.Match(Contracts(i, 1), .Columns(1), 0), R)
.Value = "1"
.NumberFormat = "General"
End With
Next i
End With
'''''''''''''''''''''Place New Contract Marker for Each Contract'''''''''''''''''''''''''
End Sub
btw, regarding Dim rgFound As Range ; do not declare a variable in a loop. Declare it outside the loop and assign it new values inside the loop.
I have a code that reads in the new arrangement of columns from a text file and then rearrange the original columns by copying it in at the correct place, however there is a bug in my code. Instead of copying just 1 column it seems to copy all columns to the right of the column that i want to copy..
so i guess the error is here
'copy the old range
ws.Range(ws.Cells(Settings.rowHeader + 1, CounterCol), ws.Cells(lrow, CounterCol)).Copy
I want to copy the range AW3:AW80 to A3:A80, but do i need to copy AW:AW to A:A instead? If i do so the stuff in row 1 will be deleted, below is the full code:
Sub insertColumns()
Call Settings.init
Dim i As Integer
Dim ws As Worksheet
Dim lrow As Integer
Dim columNames As Object
Dim temp As Variant
'fill dictionary with columnnames from text file
Set columNames = FileHandling.getTypes(Settings.columnFile)
Set ws = ActiveWorkbook.Sheets("List")
'Get max column and row number
lColumn = HelpFunctions.getLastColumn(ws, Settings.rowHeader)
lrow = HelpFunctions.getLastRow(ws, HelpFunctions.getColumn("*part*", ws, Settings.rowHeader))
'Insert all new columns in reverse order from dictionary
temp = columNames.keys
For i = columNames.Count - 1 To 0 Step -1
ws.Columns("A:A").Insert Shift:=xlToRight
ws.Range("A" & Settings.rowHeader).Value = temp(i)
Next i
'last column
lastColumn = lColumn + columNames.Count
'we loop through old cells
CounterCol = columNames.Count + 1
Do While CounterCol <= lastColumn
j = 0
'through each elemnt in dictionary
For Each element In temp
j = j + 1
'compare the old rowheader with any of the rowheader in DICTIONARY
If UCase(ws.Cells(Settings.rowHeader, CounterCol).Value) = element Then
'copy the old range
ws.Range(ws.Cells(Settings.rowHeader + 1, CounterCol), ws.Cells(lrow, CounterCol)).Copy
'paste it
ws.Cells(Settings.rowHeader + 1, j).Select
ws.Paste
'format the new row
ws.Cells(Settings.rowHeader + 1, j).EntireColumn.AutoFit
'Delete the old row
ws.Columns(CounterCol).EntireColumn.Delete
'decrease the last column by one since we just deleted the last column
lastColumn = lastColumn - 1
found = True
'Exit For
End If
Next element
'Prompt the user that the old column does not match any of the new column
If Not found Then
MsgBox (UCase(ws.Cells(Settings.rowHeader, CounterCol)) & " was not a valid column name please move manually")
End If
'reset the found
found = False
'go to nect column
CounterCol = CounterCol + 1
Loop
End Sub
Below is a screenshot of the dictionary.
After the first iteration/first copy it should have only copied over the part number column, but as can been seen it has copied over more than just the first column(everything except of drawing number)
Q: I want to copy the range AW3:AW80 to A3:A80, but do i need to copy AW:AW to A:A instead?
A: No. Any range can be copied.
Rather than trying to debug your code, I'll give you a hint about how to debug such a thing. Lines like
ws.Range(ws.Cells(Settings.rowHeader + 1, CounterCol), ws.Cells(lrow, CounterCol)).Copy
are hard to debug because they are trying to do too much. You have 4 instances of the dot operator and suspected that the problem was with the last one (.Copy). The problem is almost certainly that your code isn't grabbing the range that you think it is grabbing. In other words, one or more of your method invocations earlier in the line needs debugging. In such a situation it is useful to introduce some range variables, set them equal to various values and print their addresses to the immediate window to see what is happening. As an added benefit, having set range variables allows you to use the full power of intellisence in the VBA editor. Something like:
Dim SourceRange As Range, Cell1 As Range, Cell2 As Range
'
'
'
Set Cell1 = ws.Cells(Settings.rowHeader + 1, CounterCol)
Set Cell2 = ws.Cells(lrow, CounterCol)
Set SourceRange = ws.Range(Cell1, Cell2)
Debug.Print Cell1.Address & ", " & Cell2.Address & ", " & SourceRange.Address
'
'Once the above is debugged:
'
SourceRange.Copy 'should work as expected
It is possible that you are copying the range that you want to copy but that your larger program still isn't working. In that case you have some sort of logic error and should be trying to copy some other range. Even then, the above exercise still helps because it makes it clear exactly what your original line was doing.
'go to nect column
CounterCol = CounterCol + 1
needed to be deleted. It has to do that the column shifts left when i deleted rows.
Thanks for the help. I hope the code can be used for others who might need to add columns, but still copy over content from old columnsin the right order.