How to "flatten" or "collapse" a 2D Excel table into 1D? - vba

I have a two dimensional table with countries and years in Excel. eg.
1961 1962 1963 1964
USA a x g y
France u e h a
Germany o x n p
I'd like to "flatten" it, such that I have Country in the first col, Year in the second col, and then value in the third col. eg.
Country Year Value
USA 1961 a
USA 1962 x
USA 1963 g
USA 1964 y
France 1961 u
...
The example I present here is only a 3x4 matrix, but the real dataset i have is significantly larger (roughly 50x40 or so).
Any suggestions how I can do this using Excel?

You can use the excel pivot table feature to reverse a pivot table (which is essentially what you have here):
Good instructions here:
http://spreadsheetpage.com/index.php/tip/creating_a_database_table_from_a_summary_table/
Which links to the following VBA code (put it in a module) if you don't want to follow the instructions by hand:
Sub ReversePivotTable()
' Before running this, make sure you have a summary table with column headers.
' The output table will have three columns.
Dim SummaryTable As Range, OutputRange As Range
Dim OutRow As Long
Dim r As Long, c As Long
On Error Resume Next
Set SummaryTable = ActiveCell.CurrentRegion
If SummaryTable.Count = 1 Or SummaryTable.Rows.Count < 3 Then
MsgBox "Select a cell within the summary table.", vbCritical
Exit Sub
End If
SummaryTable.Select
Set OutputRange = Application.InputBox(prompt:="Select a cell for the 3-column output", Type:=8)
' Convert the range
OutRow = 2
Application.ScreenUpdating = False
OutputRange.Range("A1:C3") = Array("Column1", "Column2", "Column3")
For r = 2 To SummaryTable.Rows.Count
For c = 2 To SummaryTable.Columns.Count
OutputRange.Cells(OutRow, 1) = SummaryTable.Cells(r, 1)
OutputRange.Cells(OutRow, 2) = SummaryTable.Cells(1, c)
OutputRange.Cells(OutRow, 3) = SummaryTable.Cells(r, c)
OutputRange.Cells(OutRow, 3).NumberFormat = SummaryTable.Cells(r, c).NumberFormat
OutRow = OutRow + 1
Next c
Next r
End Sub
-Adam

In Excel 2013 need to follow next steps:
select data and convert to table (Insert -> Table)
call Query Editor for table (Power Query -> From Table)
select columns that contain years
in context menu select 'Unpivot Columns'-command.
Support Office: Unpivot columns (Power Query)
In Excel 2016, Power Query is called Get & Transform and it is found in the Data tab.

#Adam Davis's answer is perfect, but just in case you're as clueless as I am about Excel VBA, here's what I did to get the code working in Excel 2007:
Open the workbook with the Matrix that needs to be flattened to a table and navigate to that worksheet
Press Alt-F11 to open the VBA code editor.
On the left pane, in the Project box, you'll see a tree structure representing the excel objects and any code (called modules) that already exist. Right click anywhere in the box and select "Insert->Module" to create a blank module file.
Copy and paste #Adman Davis's code from above as is into the blank page the opens and save it.
Close the VBA editor window and return to the spreadsheet.
Click on any cell in the matrix to indicate the matrix you'll be working with.
Now you need to run the macro. Where this option is will vary based on your version of Excel. As I'm using 2007, I can tell you that it keeps its macros in the "View" ribbon as the farthest right control. Click it and you'll see a laundry list of macros, just double click on the one called "ReversePivotTable" to run it.
It will then show a popup asking you to tell it where to create the flattened table. Just point it to any empty space an your spreadsheet and click "ok"
You're done! The first column will be the rows, the second column will be the columns, the third column will be the data.

Flattening a data matrix (aka Table) can be accomplished with one array formula¹ and two standard formulas.
      
The array formula¹ and two standard formulas in G3:I3 are is,
=IFERROR(INDEX(A$2:A$4, MATCH(0, IF(COUNTIF(G$2:G2, A$2:A$4&"")<COUNT($1:$1), 0, 1), 0)), "")
=IF(LEN(G3), INDEX($B$1:INDEX($1:$1, MATCH(1E+99,$1:$1 )), , COUNTIF(G$3:G3, G3)), "")
=INDEX(A:J,MATCH(G3,A:A,0),MATCH(H3,$1:$1,0))
Fill down as necessary.
While array formulas can negatively impact performance due to their cyclic calculation, your described working environment of 40 rows × 50 columns should not overly impact performance with a calculation lag.
¹ Array formulas need to be finalized with Ctrl+Shift+Enter↵. Once entered into the first cell correctly, they can be filled or copied down or right just like any other formula. Try and reduce full-column references to ranges more closely representing the extents of your actual data. Array formulas chew up calculation cycles logarithmically so it is good practise to narrow the referenced ranges to a minimum. See Guidelines and examples of array formulas for more information.

For anyone who wants to use the PivotTable to do this and is following the below guide:
http://spreadsheetpage.com/index.php/tip/creating_a_database_table_from_a_summary_table/
If you want to do it in Excel 2007 or 2010 then you first need to enable the PivotTable Wizard.
To find the option you need to go to "Excel Options" via the Main Excel Window icon, and see the options selected in the "customize" section, then select "Commands Not in the Ribbon" from the "Choose Commands from:" dropdown and "PivotTable and PivotChart Wizard" needs to be added to the right.. see the image below.
Once that is done there should be a small pivottable wizard icon in the quickbar menu at the top of the Excel window, you can then follow the same process as shown in the link above.

I developed another macro because I needed to refresh the output table quite often (input table was filled by other) and I wanted to have more info in my output table (more copied column and some formulas)
Sub TableConvert()
Dim tbl As ListObject
Dim t
Rows As Long
Dim tCols As Long
Dim userCalculateSetting As XlCalculation
Dim wrksht_in As Worksheet
Dim wrksht_out As Worksheet
'##block calculate and screen refresh
Application.ScreenUpdating = False
userCalculateSetting = Application.Calculation
Application.Calculation = xlCalculationManual
'## get the input and output worksheet
Set wrksht_in = ActiveWorkbook.Worksheets("ressource_entry")'## input
Set wrksht_out = ActiveWorkbook.Worksheets("data")'## output.
'## get the table object from the worksheet
Set tbl = wrksht_in.ListObjects("Table14") '## input
Set tb2 = wrksht_out.ListObjects("Table2") '## output.
'## delete output table data
If Not tb2.DataBodyRange Is Nothing Then
tb2.DataBodyRange.Delete
End If
'## count the row and col of input table
With tbl.DataBodyRange
tRows = .Rows.Count
tCols = .Columns.Count
End With
'## check every case of the input table (only the data part)
For j = 2 To tRows '## parse all row from row 2 (header are not checked)
For i = 5 To tCols '## parse all column from col 5 (first col will be copied in each record)
If IsEmpty(tbl.Range.Cells(j, i).Value) = False Then
'## if there is time enetered create a new row in table2 by using the first colmn of the selected cell row and cell header plus some formula
Set oNewRow = tb2.ListRows.Add(AlwaysInsert:=True)
oNewRow.Range.Cells(1, 1).Value = tbl.Range.Cells(j, 1).Value
oNewRow.Range.Cells(1, 2).Value = tbl.Range.Cells(j, 2).Value
oNewRow.Range.Cells(1, 3).Value = tbl.Range.Cells(j, 3).Value
oNewRow.Range.Cells(1, 4).Value = tbl.Range.Cells(1, i).Value
oNewRow.Range.Cells(1, 5).Value = tbl.Range.Cells(j, i).Value
oNewRow.Range.Cells(1, 6).Formula = "=WEEKNUM([#Date])"
oNewRow.Range.Cells(1, 7).Formula = "=YEAR([#Date])"
oNewRow.Range.Cells(1, 8).Formula = "=MONTH([#Date])"
End If
Next i
Next j
ThisWorkbook.RefreshAll
'##unblock calculate and screen refresh
Application.ScreenUpdating = True
Application.Calculate
Application.Calculation = userCalculateSetting
End Sub

VBA solution may not be acceptable under some situations (e.g. cannot embed macro due to security reasons, etc.). For these situations, and otherwise too in general, I prefer using formulae over macro.
I am trying to describe my solution below.
input data as shown in question (B2:F5)
column_header (C2:F2)
row_header (B3:B5)
data_matrix (C3:F5)
no_of_data_rows (I2) = COUNTA(row_header) + COUNTBLANK(row_header)
no_of_data_columns (I3) = COUNTA(column_header) + COUNTBLANK(column_header)
no_output_rows (I4) = no_of_data_rows*no_of_data_columns
seed area is K2:M2, which is blank but referenced, hence not to be deleted
K3 (drag through say K100, see comments description) = ROW()-ROW($K$2) <= no_output_rows
L3 (drag through say L100, see comments description) = IF(K3,IF(COUNTIF($L$2:L2,L2)
M3 (drag through say M100, see comments description) = IF(K3,IF(M2 < no_of_data_columns,M2+1,1),"-")
N3 (drag through say N100, see comments description) = INDEX(row_header,L3)
O3 (drag through say O100, see comments description) = INDEX(column_header,M3)
P3 (drag through say P100, see comments description) = INDEX(data_matrix,L3,M3)
Comment in K3: Optional: Check if expected no. of output rows has been achieved. Not required, if one only prepares this table limited to no. of output rows.
Comment in L3: Goal: Each RowIndex (1 .. no_of_data_rows) must repeat no_of_data_columns times. This will provide index lookup for row_header values. In this example, each RowIndex (1 .. 3) must repeat 4 times. Algorithm: Check how many times RowIndex has occurred yet. If it less than no_of_data_columns times, continue using that RowIndex, else increment the RowIndex. Optional: Check if expected no. of output rows has been achieved.
Comment in M3: Goal: Each ColumnIndex (1 .. no_of_data_columns) must repeat in a cycle. This will provide index lookup for column_header values. In this example, each ColumnIndex (1 .. 4) must repeat in a cycle. Algorithm: If ColumnIndex exceeds no_of_data_columns, restart the cycle at 1, else increment the ColumnIndex. Optional: Check if expected no. of output rows has been achieved.
Comment in R4: Optional: Use column K for error handling, as shown in column L and column M. Check if looked up value IsBlank to avoid incorrect "0" in the output because of blank input in data_matrix.

updated ReversePivotTable function so i can specify number of header columns and rows
Sub ReversePivotTable()
' Before running this, make sure you have a summary table with column headers.
' The output table will have three columns.
Dim SummaryTable As Range, OutputRange As Range
Dim OutRow As Long
Dim r As Long, c As Long
Dim lngHeaderColumns As Long, lngHeaderRows As Long, lngHeaderLoop As Long
On Error Resume Next
Set SummaryTable = ActiveCell.CurrentRegion
If SummaryTable.Count = 1 Or SummaryTable.Rows.Count < 3 Then
MsgBox "Select a cell within the summary table.", vbCritical
Exit Sub
End If
SummaryTable.Select
Set OutputRange = Application.InputBox(prompt:="Select a cell for the 3-column output", Type:=8)
lngHeaderColumns = Application.InputBox(prompt:="Header Columns")
lngHeaderRows = Application.InputBox(prompt:="Header Rows")
' Convert the range
OutRow = 2
Application.ScreenUpdating = False
'OutputRange.Range("A1:D3") = Array("Column1", "Column2", "Column3", "Column4")
For r = lngHeaderRows + 1 To SummaryTable.Rows.Count
For c = lngHeaderColumns + 1 To SummaryTable.Columns.Count
' loop through all header columns and add to output
For lngHeaderLoop = 1 To lngHeaderColumns
OutputRange.Cells(OutRow, lngHeaderLoop) = SummaryTable.Cells(r, lngHeaderLoop)
Next lngHeaderLoop
' loop through all header rows and add to output
For lngHeaderLoop = 1 To lngHeaderRows
OutputRange.Cells(OutRow, lngHeaderColumns + lngHeaderLoop) = SummaryTable.Cells(lngHeaderLoop, c)
Next lngHeaderLoop
OutputRange.Cells(OutRow, lngHeaderColumns + lngHeaderRows + 1) = SummaryTable.Cells(r, c)
OutputRange.Cells(OutRow, lngHeaderColumns + lngHeaderRows + 1).NumberFormat = SummaryTable.Cells(r, c).NumberFormat
OutRow = OutRow + 1
Next c
Next r
End Sub

Code with the claim for some universality
The book should have two sheets:
Sour = Source data
Dest = the "extended" table will drop here
Option Explicit
Private ws_Sour As Worksheet, ws_Dest As Worksheet
Private arr_2d_Sour() As Variant, arr_2d_Dest() As Variant
' https://stackoverflow.com/questions/52594461/find-next-available-value-in-excel-cell-based-on-criteria
Public Sub PullOut(Optional ByVal msg As Variant)
ws_Dest_Acr _
arr_2d_ws( _
arr_2d_Dest_Fill( _
arr_2d_Sour_Load( _
arr_2d_Dest_Create( _
CountA_rng( _
rng_2d_For_CountA( _
Init))))))
End Sub
Private Function ws_Dest_Acr(Optional ByVal msg As Variant) As Variant
ws_Dest.Activate
End Function
Public Function arr_2d_ws(Optional ByVal msg As Variant) As Variant
If IsArray(arr_2d_Dest) Then _
ws_Dest.Cells(1, 1).Resize(UBound(arr_2d_Dest), UBound(arr_2d_Dest, 2)) = arr_2d_Dest
End Function
Private Function arr_2d_Dest_Fill(Optional ByVal msg As Variant) As Variant
Dim y_Sour As Long, y_Dest As Long, x As Long
y_Dest = 1
For y_Sour = LBound(arr_2d_Sour) To UBound(arr_2d_Sour)
' without the first column
For x = LBound(arr_2d_Sour, 2) + 1 To UBound(arr_2d_Sour, 2)
If arr_2d_Sour(y_Sour, x) <> Empty Then
arr_2d_Dest(y_Dest, 1) = arr_2d_Sour(y_Sour, 1) 'iD
arr_2d_Dest(y_Dest, 2) = arr_2d_Sour(y_Sour, x) 'DTLx
y_Dest = y_Dest + 1
End If
Next
Next
End Function
Private Function arr_2d_Sour_Load(Optional ByVal msg As Variant) As Variant
arr_2d_Sour = ReDuce_rng(ws_Sour.UsedRange, 1, 0).Offset(1, 0).Value
End Function
Private Function arr_2d_Dest_Create(ByVal iRows As Long)
Dim arr_2d() As Variant
ReDim arr_2d(1 To iRows, 1 To 2)
arr_2d_Dest = arr_2d
arr_2d_Dest_Create = arr_2d
End Function
Public Function CountA_rng(ByVal rng As Range) As Double
CountA_rng = Application.WorksheetFunction.CountA(rng)
End Function
Private Function rng_2d_For_CountA(Optional ByVal msg As Variant) As Range
' without the first line and without the left column
Set rng_2d_For_CountA = _
ReDuce_rng(ws_Sour.UsedRange, 1, 1).Offset(1, 1)
End Function
Public Function ReDuce_rng(rng As Range, ByVal iRow As Long, ByVal iCol As Long) _
As Range
With rng
Set ReDuce_rng = .Resize(.Rows.Count - iRow, .Columns.Count - iCol)
End With
End Function
Private Function Init()
With ThisWorkbook
Set ws_Sour = .Worksheets("Sour")
Set ws_Dest = .Worksheets("Dest")
End With
End Function
'https://youtu.be/oTp4aSWPKO0

Related

Running a Vba function on button press not working in excell

this is a simple stock price change code
my code is function(with parameters)
Function VanillaCall(S0 As Single, Exercise As Single, Mean As Single, sigma As Single, _
Interest As Single, Time As Single, Divisions As Integer, Runs As Integer) As Single
deltat = Time / Divisions
interestdelta = Exp(Interest * deltat)
up = Exp(Mean * deltat + sigma * Sqr(deltat))
down = Exp(Mean * deltat - sigma * Sqr(deltat))
pathlength = Int(Time / deltat)
piup = (interestdelta - down) / (up - down)
pidown = 1 - piup
Temp = 0
For Index = 1 To Runs
upcounter = 0
For j = 1 To pathlength
If Rnd > pidown Then upcounter = upcounter + 1
Next j
callvalue = Application.Max(S0 * (up ^ upcounter) * (down ^ (pathlength - upcounter)) - Exercise, 0) / (interestdelta ^ pathlength)
Temp = Temp + callvalue
Next Index
VanillaCall = Temp / Runs
End Function
parameters are passed from cells in excel.
i want to execute this function from button click and display return value in a cell say b12.
i have tried putting the code inside a button sub but its not working ,a call vanillacall inside sub too isnt working.
like..
private sub button1_click()
call vanillacall
end sub
Private Sub button1_click()
Range("B12").Value = vanillacall(....)
End Sub
As per your request, Pass arguments in Range like below. Below code is just for example (due to the changes in excel data)
Sub testing33()
Range("B12") = sample(Range("A5"), Range("B5"))
End Sub
Function sample(a As Range, b As Range)
sample = a.Cells.Value & ", " & b.Cells.Value
End Function
I'd do something like the below which would allow me to pick the range containing the data I want to pass to the function (as long as the range is contiguous and contains 8 cells) and pick the cell I want to output the result to.
Private Sub button1_click()
Dim inRng As Range, outRng As Range
inSelect:
Set inRng = Application.InputBox("Select Range to Calculate", Type:=8)
If inRng.Cells.Count <> 8 Then
MsgBox "Select a range with 8 cells!", vbCritical
GoTo inSelect
End If
outSelect:
Set outRng = Application.InputBox("Select Cell to Output To", Type:=8)
If outRng.Cells.Count > 1 Then
MsgBox "Select only one cell!", vbCritical
GoTo outSelect
End If
outRng.Value = VanillaCall(inRng.Cells(1), inRng.Cells(2), inRng.Cells(3), inRng.Cells(4), inRng.Cells(5), inRng.Cells(6), inRng.Cells(7), inRng.Cells(8))
End Sub
You need to get the values from the sheet and save in variables. Then pass the variables to the function. Then output the result to the sheet somewhere. You will need to adjust the range addresses and worksheet name as appropriate.
Private sub button1_click()
dim ws as worksheet
Set ws = worksheets("Sheet1") ' < change the sheet name as appropriate
dim S0 As Single
dim Exercise As Single
dim Mean As Single
dim sigma As Single
dim Interest As Single
dim Time As Single
dim Divisions As Integer
dim Runs As Integer As Single
S0 = ws.Range("B1") '< specify the cell that has this data
Exercise = ws.Range("B2") '< specify the cell that has this data
Mean = ws.Range("B3") '< specify the cell that has this data
sigma = ws.Range("B4") '< specify the cell that has this data
Interest = ws.Range("B5") '< specify the cell that has this data
Time = ws.Range("B6") '< specify the cell that has this data
Divisions = ws.Range("B7") '< specify the cell that has this data
Runs = ws.Range("B8") '< specify the cell that has this data
dim Result as Single
Result = vanillacall(S0, Exercise , Mean, sigma, Interest, Time, Divisions, Runs)
ws.Range("B10") = Result '<specify the cell where you want the result
end sub

Excel range subtraction, overlooking errors in some cells possible?

I am having trouble figuring out how to subtract two ranges from each other, some cells in range H:H have "#N/A" while in range D:D there are no errors. I know in Excel it's a simple "=H2-D2" and drag that down but I'm in the process of recording a Macro and wanted to automate the subtraction as well. So far this is what I have:
Dim quantity1, quantity2, rIntersect, Qdiff, x As Range
Set quantity1 = Range("D:D")
Set quantity2 = Range("H:H")
Set rIntersect = Intersect(quantity1, quantity2)
For Each x In quantity1
If Intersect(rIntersect, x) Is Nothing Then
If Qdiff Is Nothing Then
Set Qdiff = x
Else
Set Qdiff = Application.Union(Qdiff, x)
End If
End If
Next x
Range("J2").Select
Dim lastRowJ As Long
lastRowJ = Range("A" & Rows.Count).End(xlUp).Row
Range("J2").AutoFill Destination:=Range("J2:J" & lastRowJ)
Place this procedure in a standard code module:
Public Sub Subtract()
[j2:j99] = [h2:h99-d2:d99]
End Sub
If you like how that works, I'm happy to embellish it so that it is not hard-coded for 98 rows only. Let me know.
UPDATE
Here is a version that will deal with any number of rows. It keys off of column D. So if there are 567 numbers in column D, then you will get 567 corresponding (subtracted) results in column J.
This assumes that the data start in row 2, and that there are no blank cells until the numbers in column D end.
If you are going to call this from the Macro Dialog then you should keep it Public. If on the other hand you are going to call it from another procedure in the same module, then you can make it Private.
Here is the enhanced solution:
Public Sub Subtract()
Dim k&
Const F = "iferror(h2:h[]-d2:d[],0)"
k = [count(d:d)]
[j2].Resize(k) = Evaluate(Replace(F, "[]", k + 1))
End Sub
Note that the routine now handles the errors and places a ZERO value in column J when the corresponding value in column H is an error. If you would prefer to have something other than a ZERO (like a blank for instance) when there are errors in column H, just let me know and I'll update to whatever you want.
UPDATE 2
Here is how to handle displaying blanks instead of zeroes:
Public Sub Subtract()
Dim k&
Const F = "iferror(if(h2:h[]-d2:d[]=0,"""",h2:h[]-d2:d[]),0)"
k = [count(d:d)]
[k2].Resize(k) = Evaluate(Replace(F, "[]", k + 1))
End Sub

Referencing Multiple Sheets with large sets of Data

I am fairly new to VBA and having some general obstacles with basic syntax. I am using the below code to trim leading spaces and color code an ActiveSheet I am currently working on.
I have another Worksheet called "Country" that I would like to apply the same logic to the current sheet I am using. I am also having difficulties using the most efficient code to find any cells with values of "AcctTotal" , " CurrTotal" and " BravoTotal" (there are about 14,000 rows of data). I am currently highlighting the whole spreadsheet and utilizing "UsedRange" to find these cells.
To sum it up:
I would like to trim leading spaces and color code any values of "AcctTotal" , " CurrTotal" and " BravoTotal" in two worksheets: "Currency" and "Country"
Sub ColorCodeCurrency()
Dim r As Range
For Each r In Selection
If r.Value = " AcctTotal" Then
r.Value = LTrim(r.Value)
Intersect(r.EntireRow, ActiveSheet.UsedRange).Interior.ColorIndex = 15
End If
Next r
Dim s As Range
For Each s In Selection
If s.Value = " CurrTotal" Then
s.Value = LTrim(s.Value)
Intersect(s.EntireRow, ActiveSheet.UsedRange).Interior.ColorIndex = 40
End If
Next s
Dim t As Range
For Each t In Selection
If t.Value = " BravoTotal" Then
t.Value = LTrim(t.Value)
Intersect(t.EntireRow, ActiveSheet.UsedRange).Interior.ColorIndex = 35
End If
Next t
End Sub
Most of the problem is that you're doing the same thing three times. The 'For Each' statement is going through every cell three times. If you joined it into
for each r in selection
if r.value ="AcctTotal" then
'do something
elseif r.value = "CurrTotal" then
'do something else
elseif r.value = "BravoTotal" then
'do the third thing
end if
In addition to what Maudise said, when you refer to your data, you can use syntax like:
Sheets("Country").Range("A1:E14000")
If it's possible to make changes to your source data, you may find it helpful to format it as a table for easy reference. Use the Name Manager to give the table a useful name. Then, you can say something like:
For Each r In Sheets("Country").Range("CountryTable")
You could try this way:
Public Sub ColorCode ()
Dim i As Integer, j As Integer, m As Integer, n As Integer
i = Range("A:A").End(xlDown).Row
j = Cells.End(xlToRight).Column
For m = 1 To i
For n = 1 To j
If Cells(m, n).Value < 50 Then
Cells(m, n).Interior.ColorIndex = 13
End If
Next n
Next m
End Sub
One solution is to call this code placed in a module into "This workbook" in "Private Sub Workbook_Open()".

Copy unique records from one workbook to another master workbook

I need some help with copying unique records from one workbook to a master workbook please.
Each month I receive a new workbook with data and I want to be able to copy all new records in that new workbook to one master workbook which will have all the amalgamted records. There is one unique reference field which can be used for the lookup to identify a new record.
In addition to this what I want to do is update values which are in 3 columns for ALL existing records on the master workbook which might be on the new workbook.
Example
Master workbook
Ref Name Value 1 Value 2 Value 3 Description
123 TR 100 50 200 xxxxxxxxxxxxxxx
111 WE 90 45 400 xxxxxxxxxxxxxxx
New workbook
Ref Name Value 1 Value 2 Value 3 Description
123 TR 300 200 200 xxxxxxxxxxxxxxx
456 MA 100 500 700 xxxxxxxxxxxxxxx
Update master workbook
Ref Name Value 1 Value 2 Value 3 Description
123 TR 300 200 200 xxxxxxxxxxxxxxx
111 WE 90 45 400 xxxxxxxxxxxxxxx
456 MA 100 500 700 xxxxxxxxxxxxxxx
I'd appreciate any help with this please. Thanks
I wrote a small module that does what you want (and even more). I tried to make it as generic as possible, but I had to assert a few things and limit it somehow - otherwise it would get quickly out of hand (as I think it already did.. kind of).
The limitations/assertions are the following:
1. the records are considered to be laid out only in rows (as per your example).
2. there is no column checking during the update or insertion of values. The program assumes that both master and new workbooks contain the same columns and laid in the exact same order.
3. There is no validation check for duplicate reference values. The "ref" column that you indicate as your primary key in each data range, is assumed to contain unique values (for that data range).
Apart from those assumptions, my solution is enhanced with flexible arguments (optional or autoconfigurable - see how dataRange is determined) to allow for several types of operation.
optional colorAlertOption flag: allows updated or inserted entries to be colored in order to be more distinguisable (true by default)
optional rangeWithHeaders flag: helps to determine if the supplied dataRange argument needs to be resized (remove headers) or not (true by default)
optional refColIndex integer: the relative to the dataRange - not the whole worksheet - column number pinpointing the column containing the unique references. (1 by default)
required dataRangeNew, dataRangeMaster (Range) arguments: flexible representations of the data-ranges for the new and master datasets respectively. You can either provide them explicitly (e.g. "$A$1:$D$10") or by giving only a single cell contained anywhere within the data-range. The only predicates are that the data-range should be isolated from other possible data coexisting on the same sheet (by means of blank rows or columns) and that it contains at least 1 row.
You can call the updateMasterDataRange procedure like this:
call updateMasterDataRange (Workbooks(2).Sheets("new").Range("a1"), Workbooks(1).Worksheets("master").Range("a1"))
Notice the fully qualified data ranges, including the workbooks and the worksheets in the mix. If you don't prepend these identifiers, VBA will try to associate the unqualified Range with ActiveWorkbook or/and ActiveWorksheet, with unpredictable results.
Here goes the body of the module:
Option Explicit
Option Base 1
Public Sub updateMasterDataRange( _
ByRef dataRangeNew As Range, ByRef dataRangeMaster As Range, _
Optional refColIndexNew As Integer = 1, Optional refColIndexMaster As Integer = 1, _
Optional colorAlertOption = True, Optional rangeWithHeaders = True)
' Sanitize the supplied data ranges based on various criteria (see procedure's documentation)
If sanitizeDataRange(dataRangeMaster, rangeWithHeaders) = False Then GoTo rangeError
If sanitizeDataRange(dataRangeNew, rangeWithHeaders) = False Then GoTo rangeError
' Declaring counters for the final report's updated and appended records respectively
Dim updatedRecords As Integer: updatedRecords = 0
Dim appendedRecords As Integer: appendedRecords = 0
' Declaring the temporary variables which hold intermediate results during the for-loop
Dim updatableMasterRefCell As Range, currentRowIndex As Integer, updatableRowMaster As Range
For currentRowIndex = 1 To dataRangeNew.Rows.Count
' search the master's unique references (refColMaster range) for the current reference
' from dataRangeNew (refcolNew range)
Set updatableMasterRefCell = dataRangeMaster.Columns(refColIndexMaster).Find( _
what:=dataRangeNew.Cells(currentRowIndex, refColIndexNew).Value, _
lookat:=xlWhole, searchorder:=xlByRows, searchDirection:=xlNext)
' perform a check to see if the search has returned a valid range reference in updatableMasterRefCell
' if it is found empty (the reference value in refCellNew is unique to masterDataRange)
If updatableMasterRefCell Is Nothing Then
Call appendRecord(dataRangeNew.Rows(currentRowIndex), dataRangeMaster, colorAlertOption)
appendedRecords = appendedRecords + 1
'ReDim Preserve appendableRowIndices(appendedRecords)
'appendableRowIndices(appendedRecords) = currentRowIndex
Else
Set updatableRowMaster = Intersect(dataRangeMaster, updatableMasterRefCell.EntireRow)
Call updateRecord(dataRangeNew.Rows(currentRowIndex), updatableRowMaster, colorAlertOption)
updatedRecords = updatedRecords + 1
End If
Next currentRowIndex
' output an informative dialog to the user
Dim msg As String
msg = _
"sheet name: " & dataRangeMaster.Parent.Name & vbCrLf & _
"records updated: " & updatedRecords & vbCrLf & _
"records appended: " & appendedRecords
MsgBox msg, vbOKOnly, "--+ Update report +--"
Exit Sub
rangeError:
MsgBox "Either range argument is too small to operate on!", vbExclamation, "Argument Error"
End Sub
Sub appendRecord(ByVal recordRowSource As Range, ByRef dataRangeTarget As Range, Optional ByVal colorAlertOption As Boolean = True)
Dim appendedRowTarget As Range
Set dataRangeTarget = dataRangeTarget.Resize(Rowsize:=dataRangeTarget.Rows.Count + 1)
Set appendedRowTarget = dataRangeTarget.Rows(dataRangeTarget.Rows.Count)
appendedRowTarget.Insert shift:=xlDown, copyorigin:=xlFormatFromLeftOrAbove
Set appendedRowTarget = appendedRowTarget.Offset(-1, 0)
' resize datarangetarget to -1 row (because cells' shifting incurred a +1 row to dataRangeTarget)
Set dataRangeTarget = dataRangeTarget.Resize(Rowsize:=dataRangeTarget.Rows.Count - 1)
recordRowSource.Copy appendedRowTarget
If colorAlertOption = True Then
' fills the cells of the newly appended row with lightgreen color
appendedRowTarget.Interior.color = RGB(156, 244, 164)
End If
End Sub
Sub updateRecord(ByVal recordRowSource As Range, ByVal updatableRowTarget As Range, Optional ByVal colorAlertOption As Boolean = True)
recordRowSource.Copy updatableRowTarget
If colorAlertOption = True Then
' fills the cells of the updated row with lightblue color
updatableRowTarget.Interior.color = RGB(164, 189, 249)
End If
End Sub
Private Function sanitizeDataRange(ByRef target As Range, ByVal rangeWithHeaders As Boolean) As Boolean
' if data range comprises only 1 cell then try to expand the range to currentRegion
' (all neighbouring cells until the selection reaches boundaries of blank rows or columns)
If target.Cells.Count = 1 Then
Set target = target.CurrentRegion
End If
' remove headers from data ranges if flag RangeWithHeaders is true
If (rangeWithHeaders) Then
If (target.Rows.Count >= 2) Then
Set target = target.Offset(1, 0).Resize(Rowsize:=(target.Rows.Count - 1))
Else
sanitizeDataRange = False
End If
End If
sanitizeDataRange = IIf((target.Rows.Count >= 1), True, False)
End Function
The results of a simple execution on your example gave the expected results, as you can see in the attached picture. There is even a dialogue with a brief report on the accomplished operations.
You haven't got much of a start. Will this outline get you started?
open all 3 workbooks
for masterrow = beginrow to endrow
if match in newsheet then
updaterow = newrow
else
updaterow = masterrow
end if
next masterrow
' now pick up unmatched newrows
for newrow = beginrow to endrow
if not match in updatesheet then
updaterow = newrow
end if
next newrow
EDIT: CodeVortex did the whole thing. My outline was flawed.
open both workbooks
appendrow = endrow of mastersheet
for newrow = beginrow to endrow
if match in mastersheet then
update masterrow
else
append into appendrow
appendrow = appendrow + 1
end if
next newrow

Excel VBA - Perform Operations on visible cells only

I have a database that has in excess on 200,000 rows. When I was writing a VBA script I had a database of about 20,000 rows in mind so I didn't care whether the database was filtered or not because the VBA script ran quickly. So given the realization that the database is huge and testing the VBA script I was surprised to notice how slowly it ran. So without further to say this is how my code looks like :
Set wsDB = ThisWorkbook.Sheets("DB")
Dim nameIndex As Long: nameIndex = Application.Match(name, wsDB.Rows(1), 0)
Dim formula As String
formula = "=IFERROR(AVERAGEIFS(" + GRA(nameIndex) + "," + GRA(dateIndex) + ",R2C," + GRA(cellNameIndex) + ",RC1" + "),"""")"
where GRA is a function that returns the address of the range of a column.
Private Function GRA(ByRef rngIndex As Long)
GRA = "DB!" + CStr(Range(Cells(2, rngIndex), Cells(rowNos, rngIndex)).Address(1, 1, xlR1C1, 0, 0))
End Function
So given that I now filter the table beforehand how can I adjust my code so that it ignores all the hidden rows and takes into account only what is visible. Of course I am aware that a simple dirty solution would be to simply copy the filter database and paste it in a new sheet but that will affect the performance which is what I'm trying to improve.
You can use the following function to return a range of only visible cells.
Function VisibleCells(Rng As Range) As Variant
Dim R As Range
Dim Arr() As Integer
Dim RNdx As Long
Dim CNdx As Long
If Rng.Areas.Count > 1 Then
VisibleCells = CVErr(xlErrRef)
Exit Function
End If
ReDim Arr(1 To Rng.Rows.Count, 1 To Rng.Columns.Count)
For RNdx = 1 To Rng.Rows.Count
For CNdx = 1 To Rng.Columns.Count
Set R = Rng(RNdx, CNdx)
If (R.EntireRow.Hidden = True) Or _
(R.EntireColumn.Hidden = True) Then
Arr(RNdx, CNdx) = 0
Else
Arr(RNdx, CNdx) = 1
End If
Next CNdx
Next RNdx
VisibleCells = Arr
End Function
The above code came from http://www.cpearson.com/excel/VisibleCells.aspx.
Normally I would only post code that I write however this does exactly what I was thinking.