I have been working on the problem below for a while now and I got a very basic version to work without error handling. My goal is to have this macro run from the personal macro workbook.
I am importing pipe delimited text files from various sources and none of the formatting in the headers match (some providers decided to play around with the entire layout). With this issue at hand, I created an Excel workbook named Reference(aka Map) with the incoming files layout and standardized/corrected columns' name formatting.
The good news for me is, the file ID will always be on column A. I have about 65 files that need to processed each month so I need to minimize all possible steps and therefore need to have the Reference workbook closed.
With this, I looked around online and put together most of the solution to pull in the new headers based on the ID located in A3. Yet a dilemma still exists, sometimes the ID on A3 will not exist on the Reference workbook - I need to have the vlookup move down to the next row until the result does not equal #N/A or 0 or blank.
At this point I got the 'Do Loop Until' to find the correct row for the first match - works perfectly with out any code following it.
As soon as the vlookup finds a row with an existing ID, then run the snippet below to populate the remaining headers. The only side effect of the next step is, for some reason rID offsets +1 row, undoing the 'Do Loop until' if the final row does not contain a matching ID.
'> Populate remining headers
For Each cell In rng1
cell.Value = ("=VLOOKUP(" & cID & rID & "," & map & "," & i & ",FALSE)")
i = i + 1
Next
This is what I have so far:
Sub DAc_lookup_headers()
Dim wb1 As Workbook 'Current text/file
Dim map As String 'reference Map
Dim cID As String 'wb1 look up column A
Dim rID As String 'wb1 starting row number
Dim rng1 As Range 'wb1 Collection header range
Dim i As Long 'Index number per cell in range
Set wb1 = ActiveWorkbook
Set rng1 = wb1.ActiveSheet.[A1:G1]
map = ("'C:\Users\x165422\Desktop\New folder\[Reference.xlsx]Ref'!$A$1:$I$13")
rID = 3 'Row where ID is - will increment + 1 if not found
cID = "A" 'Column where ID is
i = 3 'Starting vlookup Index number - to increment per cell in range
'>Look for ID until value is found
Do
wb1.ActiveSheet.[a1].Value = ("=VLOOKUP(" & cID & rID & "," & map & "," & i & ",FALSE)")
rID = rID + 1
Loop Until wb1.ActiveSheet.[a1].Text <> "#N/A" Or "0"
'> Populate remining headers
For Each cell In rng1
cell.Value = ("=VLOOKUP(" & cID & rID & "," & map & "," & i & ",FALSE)")
i = i + 1
Next
'> Convert to values
With rng1
.Value = .Value
End With
End Sub
I'll try to help a bit, because you show effort to learn. I will give you some useful tips regarding overall VBA coding and also regarding your issues. Please try to remember them.
It's difficult to read your code, because your variables are poorly named. In VBA there is a difference between a cell and it's value. In your code you have rng1 and rng2. I see that currently rng1 represents cell's value, and rng2 - a cell itself. I'm not sure if you intentionally did that, but now you will understand it. IF you mean a cell itself, then naming the variable rng2 is well understood. But IF you mean cell's value, naming the variable rng1 is misleading. That's only for being able to read the code more easily. Since you defined this variable as string, I suspect that you are expecting it to receive a value of string type. So, you should name your variable something in relation with it's type, i.e. something beginning with str... or s... which would mean that this variable is of string type, e.g. strValue or sID. preferable approach is to use str, as s could be confused with single type (there is such data type in VBA too). You should apply this logic to every variable that you use. String - str..., long - lng..., integer - int..., double - dbl..., boolean - bln..., etc. One type is kind of specific and you use it here too. It's the object type. Objects are not only objects, they are broken down to many different types, and within your code you use range and workbook objects too. You can name them o... or obj..., or usually it's better to use a more specific name, like rng... (for range) or wb... (for workbook). Usual data type variables take values without Set clause, while objects always need Set in order to be associated with some actual object, like range, workbook, sheet, etc. In your code we see wb2 variable which is a string. As now you know, that's not good.
So, in this case you should rename rng1 if you expect it to be string and not a range and learn to always use this naming convention. Same goes to wb2.
Always use Option Explicit as the topmost line in your code module, before all of the subs. This simply prevents from typos. And that's actually an indispensable line. I always use it and everyone should. After doing this, I see that you code will not run, because cell variable is not defined. Use Dim cell As Range, because cell is actually an object of range type. Set is omitted in this case, because For Each ... In ... Next loop does that for you.
Now your actual problems.
You would find Worksheet.UsedRange useful for determining columns count. That represents the area in a sheet from A1 to the last used cell (that's actually an intersection of the last used column and the last used row). Given that your data spans from the column A and when it ends there are no more data to the right not belonging to any column, you might use ActiveSheet.UsedRange.Columns.Count to get the number of columns in your UsedRange and hopefully in your sheet.
I mentioned UsedRange first, because I wanted you to get acquainted with it. We'll use it to solve the problem of vlookup. So the first step that I would suggest before the vlookup is to find the cell with your ID. We should first dim variables that we'll use, e.g. rng3:
Dim rng3 As Range
Then my suggestion is to find the cell with ID looping through cells in the column A, beginning A3, but not looping until the end of the column, because there are 1m rows, but until we reach the last actually used row:
For Each cell In wb1.ActiveSheet.Range("A3:A" & wb1.ActiveSheet.UsedRange.Rows.Count)
If cell <> "" Then
Set rng3 = cell
Exit For 'we found the value, no need to continue looping, so we exit the For loop
End If
Next 'there is no need to write "cell" after "Next", because VBA knows which loop it is in.
Now, that we have the cell where your ID is, we can put lookups in:
i = 2 'Starting vlookup Index number per cell in range
For Each cell in rng2
cell.Value = "=VLOOKUP(" & rng3.Address & "," & wb2 & "," & i & ",FALSE)" 'again, wb2 is not a nice name for String variable
i = i + 1
Next
I hope here are no typos, as I have not tested it. However, I've double checked the code, it looks ok.
I got it to work with:
Sub DAc_lookup_headers()
Dim wb1 As Workbook 'Current text/file
Dim map As String 'reference Map
Dim cID As String 'wb1 look up column A
Dim rID As String 'wb1 starting row number
Dim rng1 As Range 'wb1 Collection header range
Dim i As Long 'Index number per cell in range
Set wb1 = ActiveWorkbook
Set rng1 = wb1.ActiveSheet.[A1:G1]
map = ("'C:\Users\x165422\Desktop\New folder\[Reference.xlsx]Ref'!$A$1:$I$13")
rID = 2 'Row where ID is - will increment + 1 if not found
cID = "A" 'Column where ID is
i = 3 'Starting vlookup Index number - to increment per cell in range
'>Look for ID until value is found
Do
rID = rID + 1
wb1.ActiveSheet.[a1].Value = ("=VLOOKUP(" & cID & rID & "," & map & "," & i & ",FALSE)")
Loop Until wb1.ActiveSheet.[a1].Text <> "#N/A" Or "0"
'> Populate remining headers
For Each cell In rng1
cell.Value = ("=VLOOKUP(" & cID & rID & "," & map & "," & i & ",FALSE)")
i = i + 1
Next
'> Convert to values
With rng1
.Value = .Value
End With
End Sub
Related
Am trying to make a VBA validation sheet on Excel to find all the cells that do not match a predefined pattern and copy it to another sheet
My pattern is "4 numbers/5 numbers"
Ex: 1234/12345 is accepted
2062/67943 is accepted
372/13333 is not accepted
1234/1234 is not accepted etc...
I tried to put the following in the conditions sheet : <>****/***** and <>????/????? and both did not work (am not sure about the correctness of the approach as am still a beginner in VBA)
For the code itself, this is what I wrote :
Sub GuaranteeElig()
Sheets.Add After:=ActiveSheet
ActiveSheet.Name = SheetName
Sheets("MainSheet").UsedRange.AdvancedFilter Action:= _
xlFilterCopy,
CriteriaRange:=Sheets("ConditionsSheet").Range("B1:B2"), _
CopyToRange:=Range("A1"), Unique:=False
End Sub
Any tips on how I can do it ?
Thanks in advance :)
As long as the values of the numbers are independent and do not matter, and it is only the Length of the numerical strings that count, you could use a for loop on the cells from the "search" sheet (I assume this is the MainSheet as shown in your code?) where your values are contained.
From there, I'll give you a couple ways to place the data in the validation sheet (assuming this is your ConditionsSheet as shown in your code?) where you are trying to pinpoint the values.
(You may need to change part of your approach depending on how you want the incorrect set of values laid out on your secondary sheet - but this should get you started.) I added a TON of comments as you say you're new to VBA - these will help you understand what is being done.
Sub GuaranteeElig()
'Adding this to help with performance:
Application.ScreenUpdating = False
'Assuming you are adding a sheet here to work with your found criteria.
Sheets.Add After:=ActiveSheet
ActiveSheet.Name = "ConditionsSheet"
'Using the naming bits below I am assuming the data you are searching for is on MainSheet
'Get used range (most accurate and efficient way I have found yet, others on S.O.
'may have better ways for this - research it if this does not work for you)
'I have had problems using the Sheets().UsedRange method.
Dim c as Long 'This may not be necessary for you if you are looping through only column "A"
Dim r as Long
'Cells(y,x) method uses numerical values for each row (y) or column (x).
c = Cells(1, Columns.Count).End(xlToLeft).Column 'May not be necessary depending on your needs.
'Using this because you have "UsedRange" in your
'code.
'.End(xlToLeft) signifies we are going to the end of the available cell range of
'Row 1 and then performing a "Ctrl+Left Arrow" to skip all blank cells until we hit
'the first non-blank cell.
r = Cells(Rows.Count, 1).End(xlUp).Row
'.End(xlUp) method is similar - we go to the end of the available cell range for the
'column ("A" in this case), then performing a "Ctrl+Up Arrow" to skip all blank cells.
'If you have a header row which spans across the sheet, this is your best option,
'unless you have 'helper' cells which extend beyond the final column of this header
'row. I am assuming Row 1 is a header in this case - change to your needs.
'For your Rows - choose the column which contains congruent data to the bottom of
'your used range - I will assume column 1 in this case - change to suit your needs.
Dim i as long
Dim j as integer
Dim cel as Range
Dim working_Str() as String 'String Array to use later
Dim string1 as String
Dim string2 as String
Dim badString as Boolean
For i = 2 to r Step 1 'Step down from row 2 to the end of data 1 Row at a time
'Row 1 is header.
set cel=Cells(i, 1) 'Sets the cell to check - assuming data is in Column "A"
'i will change from for loop so 'cel' changes from "A2555"
'to "A2554" to "A2553" etc.
working_Str=Split(cel.Value, "/", -1) 'Splits the value based on "/" inside of cel
string1=working_Str(0) 'what we hope will always be 4 digits
string2=working_Str(1) 'what we hope will always be 5 digits
If Len(string1)<>4 Then 'string1 _(xxxx)_(/)(don't care) does not equal 4 digits in length
badString = True
Elseif Len(string2)<>5 Then ''string1 (don't care)(/)_(xxxxx)_ does not equal 5 digits in length
badString = True
End If
If badString Then 'If either strings above were not correct length, then
'We will copy cell value over to the new sheet "ConditionsSheet"
'Comment the next 2 commands to change from going to one row at a time to
'Matching same row/Cell on the 2nd sheet. Change to suit your needs.
j = j + 1 'Counter to move through the cells as you go, only moving one cell
'at a time as you find incorrect values.
Sheets("ConditionsSheet").Range("A" & j).Value=cel.Value 'sets the value on other sheet
'UNComment the next command to change from going to one row at a time to
'matching same row/cell on the 2nd sheet. Change to suit your needs.
'Sheets("ConditionsSheet").Range("A" & i).Value=cel.Value
End if
badString = False 'resets your boolean so it will not fail next check if strings are correct
Next i
'Returning ScreenUpdating back to True to prevent Excel from suppressing screen updates
Application.ScreenUpdating = True
End Sub
UPDATE
Check the beginning and ending lines I just added into the subroutine. Application.ScreenUpdating will suppress or show the changes as they happen - suppressing them makes it go MUCH quicker. You also do not want to leave this setting disabled, as it will prevent Excel from showing updates as you try to work in the cell (like editing cell values, scrolling etc. . . Learned the hard way. . .)
Also, if you have a lot of records in the given row, you could try putting the data into an array first. There is a great example here at this StackOverflow Article.
Accessing the values of a range across multiple rows takes a LOT of bandwidth, so porting the range into an Array first will make this go much quicker, but it still may take a bit. Additionally, how you access the array information will be a little different, but it'll make sense as you research it a little more.
Alternative To VBA
If you want to try using a formula instead, you can use this - just modify for the range you are looking to search. This will potentially take longer depending on processing speed. I am entering the formula on 'Sheet2' and accessing 'Sheet1'
=IF(COUNTIF(Sheet1!A1,"????/?????"),1,0)
You are spot on with the search pattern you want to use, you just need to use a function which uses wildcard characters within an "if" function. What you do with the "If value is true" vs "If value is false" bits are up to you. COUNTIF will parse wildcards, so if it is able to "count" the cell matching this string combination, it will result in a "True" value for your if statement.
Regex method, this will dump the mismatched value in a worksheet named Result, change the input range and worksheet name accordingly.
In my testing, 72k cells in UsedRange takes about 4seconds~:
Option Explicit
Sub GuaranteeElig()
Const outputSheetName As String = "Result"
Dim testValues As Variant
testValues = ThisWorkbook.Worksheets("MainSheet").UsedRange.Value 'Input Range, change accordingly
Const numPattern As String = "[\d]{4}\/[\d]{5}"
Dim regex As Object
Set regex = CreateObject("VBScript.Regexp")
regex.Pattern = numPattern
Dim i As Long
Dim n As Long
Dim failValues As Collection
Set failValues = New Collection
'Loop through all the values and test if it fits the regex pattern - 4 digits + / + 5 digits
'Add the value to failValues collection if it fails the test.
For i = LBound(testValues, 1) To UBound(testValues, 1)
For n = LBound(testValues, 2) To UBound(testValues, 2)
If Not regex.Test(testValues(i, n)) Then failValues.Add testValues(i, n)
Next n
Next i
Erase testValues
Set regex = Nothing
If failValues.Count <> 0 Then
'If there are mismatched value(s) found
'Tranfer the values to an array for easy output later
Dim outputArr() As String
ReDim outputArr(1 To failValues.Count, 1 To 1) As String
For i = 1 To failValues.Count
outputArr(i, 1) = failValues(i)
Next i
'Test if output worksheet exist
Dim outputWS As Worksheet
On Error Resume Next
Set outputWS = ThisWorkbook.Worksheets(outputSheetName)
On Error GoTo 0
'If output worksheet doesn't exist, create a new sheet else clear the first column for array dump
If outputWS Is Nothing Then
Set outputWS = ThisWorkbook.Worksheets.Add
outputWS.Name = outputSheetName
Else
outputWS.Columns(1).Clear
End If
'Dump the array starting from cell A1
outputWS.Cells(1, 1).Resize(UBound(outputArr, 1)).Value = outputArr
Else
MsgBox "No mismatched value found in range"
End If
Set failValues = Nothing
End Sub
If you do not need duplicate values in the list of mismatched (i.e. unique values) then sound out in the comment.
My file has two identical Worksheets for users to input two different sets of assumption variables, called "InputA" and "InputB". I want to quickly switch which Input sheet is feeding into the other sheets of the model.
Using Find and Replace took over 5 minutes, and there were over 350,000 references to "InputA".
I tried the following macro, which takes an instant to run, but unfortunately also changes all references in the workbook, effectively keeping everything referenced to the original input sheet.
Sheets("InputA").Name = "temp"
Sheets("InputB").Name = "InputA"
Sheets("temp").Name = "InputB"
Is there a way to execute the macro but prevent any references to worksheets from changing to the new sheet name, basically freezing everything except the sheet name change? Or perhaps any other solution that will work quickly? I don't want to go through 350,000 instances and rewrite using INDIRECT(), as that is the only other solution I've seen, because my references are complex and nested and that will take an age.
Thanks.
Assuming that your 2 Input-Sheets have the same structure, I would suggest the following:
Create a named range on Workbook-level, name it for example InputData. This range should contain all data from InputA.
Create a helper-sheet and name it Input - you can later set it to hidden.
Mark the range in the new sheet that is exactly the size of the Input-Data-Range and enter the formula =InputData as Array-formula. You can do so by entering Ctrl+Shift+Enter. The formula should have curly brackets and the sheet should now display the data of InputA.
Change all you formulas to refer to the helper Sheet Input instead of InputA.
Create a macro:
Sub switchInput()
Const sheetAName = "InputA"
Const sheetBName = "InputB"
With ThisWorkbook.Names("inputData")
If InStr(.RefersTo, sheetAName) > 0 Then
.RefersTo = Replace(.RefersTo, sheetAName, sheetBName)
Else
.RefersTo = Replace(.RefersTo, sheetBName, sheetAName)
End If
End With
End Sub
This routine will just change the definition of the named range to point either to the first or second input sheet. As a consequence, the helper sheet will show the data of the selected Input-Sheet. All your formulas itself stays unchanged.
As stated in the comments, you could take the approach recommended by Damian and use a conditional in all relevant cells. The generic conditional would be
=IF(A1="InputA",FORMULA INPUTA,FORMULA INPUTB)
This formula makes A1 the cell that decides which input to pull. This will make changing the around 350.000 output formulas in your workbook the bottleneck, the following macro takes care of changing all the formulas to conatin the conditional:
Sub changeFormulas()
Dim rng As Range, cll As Range
Set rng = shOutput.Range("A2:C10") 'Your relevant worksheet and range here
Dim aStr As String, bStr As String, formulaStr As String
aStr = "InputA"
bStr = "InputB"
For Each cll In rng
If cll.HasFormula And InStr(1, cll.Formula, aStr, 1) Then
formulaStr = Right(cll.Formula, Len(cll.Formula) - 1)
cll.Formula = "=IF(A1=" & Chr(34) & aStr & Chr(34) & "," & formulaStr & "," & Replace(formulaStr, aStr, bStr) & ")" 'Change A1 to the reference most suited for your case
End If
Next cll
End Sub
This might take a bit of time, since it has to access all the relevant cells one by one, but it will only have to run once.
To explain: This macro will go through all the cells in your range rng specified at the top. If a cell has a formula in it and the formula contains "InputA" it will change that formula to fit the generic conditional (with the cells own formula of course). Chr(34) is the quotation mark ", I find using Chr(34) more readable than multiple quotation marks """, but that is just preference.
I have a column number , say columnNumber = 4 . I need the used range of this column. I know how to find the last used row, and I could convert the column number to a column number like so
ColumnLetter = Split(Cells(1, ColumnNumber).Address, "$")(1)
LastRow = sht.Cells(sht.Rows.Count, ColumnLetter).End(xlUp).Row
and then build an address like so
rngaddy = ColumnLetter & "1:" & ColumnLetter & LastRow
and finally do
Range(rngaddy)
But is there an easier way to find the complete used range of a column given it's number ?
Dim rngaddy As Range
With Sheet1
Set rngaddy = .Range(.Cells(1, 4), .Cells(.Rows.Count, 4).End(xlUp))
End With
and if, for some reason, you want to see the address in A1 notation, merely:
debug.print rngaddy.address
Note that in doing it this way, rngaddy is, itself, the range object and not a string. So no need to do Range(rngaddy)
You could return the last populated cell is in columns # col with this:
MsgBox Cells(sht.Rows.Count,col).End(xlUp).Address
If you want to return the first populated cell as well, you could use:
MsgBox IIf(IsEmpty(Cells(1,col)),Cells(1,col).End(xlDown),Cells(1,col)).Address
Therefore this would return only the "used" range of Column #4 (D):
Sub Example_GetUsedRangeOfColumn()
Const col = 4
Dim sht As Worksheet
Set sht = Sheets("Sheet1")
MsgBox Range(IIf(IsEmpty(Cells(1, col)), Cells(1, col).End(xlDown), _
Cells(1, col)), Cells(sht.Rows.Count, col).End(xlUp)).Address
End Sub
So with this example:
...the above procedure would return: .
My preferred method is to use ListObjects aka Excel Tables to hold any input data whenever I possibly can. ListObjects are named ranges that Excel automatically maintains on your behalf, and because they grow automatically when new data is added, they give you a very robust way of referencing ranges in Excel from VBA, that is more immune to users doing things that might otherwise break code reliant on the .End(xlUp) approach.
? Range("MyTable").ListObject.ListColumns("Column 1").DataBodyRange.Address
$A$3:$A$7
Often I'll give the column concerned a named range of its own, in case the user (or a developer) later wants to change the Table column name, and use that name in my code instead.
? Range("FirstColumn").Address
$A$3:$A$7
If somebody (perhaps me) adds rows/columns above/left of the range of interest or shuffles the order of Table columns around, or changes the name of a column, the code still references the intended range and doesn't need to be changed.
? Range("FirstColumn").Address
$C$4:$C$8
? Range(Range("FirstColumn").Address & ":" & Range("FirstColumn").EntireColumn.cells(1).address).Address
$C$1:$C$8
Granted, that method of getting the range from the top cell (which may be above the ListObject) to the bottom of the column concerned is kinda long, but once you start using ListObjects more in your code you normally don't care what is above or below them...you just want the goods held inside.
I haven't used .End(xlUp) in years, other than to find where my data ends should I be in the process of turning it into a ListObject. But I'm a ListObject evangelist...your mileage may vary :-)
to get the real UsedRange of a columns you could use:
With Columns(columnNumber).SpecialCells(xlCellTypeConstants)
Set rngaddy = .Parent.Range(.Areas(1), .Areas(.Areas.Count))
End With
where rngaddy is a Range object
of course what above would fail if the column has no "constant" cells, then you may want to add some error trapping or entry check (e.g. If WorksheetFunction.CountA(Columns(columnNumber)) = 0 Then Exit Sub
Or
Option Explicit
Public Sub test()
Const columnNumber As Long = 4
Dim rngaddy As Range
Set rngaddy = Intersect(Columns(2), ActiveSheet.UsedRange): Debug.Print rngaddy.Address
End Sub
I'm self taught and having an issue with something I thought would be easy. I have a spreadsheet that i need to loop through column O and find any cell with data and clear contents of the corresponding cell in column H. The spread is sheet is ever changing so the last row of data is always changing. Below is the code I have been playing with but can't seem to get to work. Any help would be awesome..
Dim deleterate As Long
Dim ws As Worksheet
' set object
Set ws = Sheets("Import file")
' loop through the data to find the $$ amounts in column o
For deleterate = ws.Range("O" & Row.count).End(xlUp).Row To 1 Step -1
' indentify values in O which have value
If ws.Range("O" & deleterate).Value <> 0 Then
Cells(0, 7).ClearContents
End If
You can't have a row of 0, so your line
Cells(0, 7).ClearContents
will give an error. You should also always qualify which sheet you are referring to, unless you know you want to use the ActiveSheet.
So you should use
ws.Cells(deleterate, 8).ClearContents
to specify the correct row and column (because "H" is the 8th column, not the 7th) or even
ws.Cells(deleterate, "H").ClearContents
or, to be consistent with your previous line (the If statement), you could use
ws.Range("H" & deleterate).ClearContents
I am new to vba programming hence I need your expert help in trying to be able to able to read all values from the following excel sheet into a ADODB recordset object using VBA 2003
The recordset will be populated as follows
'Create new recordset with the following fields
Dim rsData as new ADODB.Recordset
rsData.Fields.Append "Month", adVarChar, 20
rsData.Fields.Append "Product", adVarChar, 20
rsData.Fields.Append "Type", adVarChar, 50
rsData.Fields.Append "Value", adVarChar, 50
rsData.Open
'for each row in spreadsheet read the following info
rsData.Addnew
rsData.Fields("Month") = 'value from row 2 Jan followed by data below
rsData.Fields("Product") = "Color" ' Value from B5
rsData.Fields("Type") = "MK1" ' value from C5
rsData.Fields("Value") = "111=" ' value from D6
'Now move to next set of values for Feb
rsData.Addnew
rsData.Fields("Month") = 'value from row 2 FEB
rsData.Fields("Product") = "Shade" ' Value from F5
rsData.Fields("Type") = "AB2" ' value from G5
rsData.Fields("Value") = "345=ABX" ' value from H5
'Now move to next set of values for Mar
rsData.Addnew
rsData.Fields("Month") = 'value from row 2 MAR
rsData.Fields("Product") = "Color" ' Value from F5
rsData.Fields("Type") = "3FG" ' value from G5
rsData.Fields("Value") = "PLZ" ' value from H5
'Now move to next row
rsData.Addnew
rsData.Fields("Month") = 'value from row 2 Jan
rsData.Fields("Product") = "Color" ' Value from F5
rsData.Fields("Type") = "MK2" ' value from C6
rsData.Fields("Value") = "234=BZX" ' value from D6
...and so on
Please note, **the data may move around but the overall layout will remain unchanged.
As you can see from the following diagram. the order has changed: Jan , march, Feb**
Your problem isn't really VBA for Excel, the problem is that that's a garbage data source. If the blocks can move around and be in any order, you really have no easy way of telling where they'll be and how much data is in them. That being the case you'd be much better off asking the questions:
Does the source data need to be in this format; and
Is there a way that we can do it better?
("Better" = more structured, more predictable.)
Also you're thinking too much in terms of each row being a record. Each block is quite separate from the others and since each one has a unique "header record" (being the month) I'd be inclined to process each block in turn rather than trying to jump from one to the other as your sample code tries to do.
The following should give you enough of a grounding to be able to navigate your way through an Excel worksheet. It's just something that I whacked together quickly and have not bullet-proofed though I did test it with a mock-up of the sheet in your second illustration and it did work. It should be enough to help set you on the right course, but I again emphasise... what you have is not a true data source. It's a report that needs to be almost arbitrarily parsed. You need to see whether that can be addressed before you do anything.
Sub DemonstrateReadingFromExcel()
'Every cell in Excel is a range.
'A range can also be a collection of cells.
'Ranges have properties that you can query. More importantly
'you can redefine a range by offsetting it from
'your current range, which makes it easy to step through a block.
Dim rng_Month As Excel.Range
Dim rng_Data As Excel.Range
'Let's define some string variables that you can use to assign
'to your recordset's fields.
Dim s_Month As String
Dim s_Product As String
Dim s_Type As String
Dim s_Value As String
Dim l_RowCurrent As Long
Dim l_RowLastType As Long
Dim l_ColumnOfMonth As Long
'We have to start with the cell containing the month.
'Rather than reading row by row, you'd be better off
'reading a whole block at a time.
'Your big problem will be telling WHERE the cell containing
'that month is and for that reason I think you need to seriously
'look at WHY the data is in the format that it is and whether
'you can actually use a much more structured data source.
'For now though let's pretend that you have some magic way of knowing
'where the range is and assign it to a range variable.
'ActiveSheet is a reference to the active worksheet but just as you
'can use a range variable to store a reference to a range,
'you can use a worksheet variable to store a reference to a worksheet
'(even one which is not the active sheet) if you want to.
'I'm only using ActiveSheet for convenience.
'You need to use the Set statement because you're assigning an object.
Set rng_Month = ActiveSheet.Range("C2")
'Ranges have properties like their column number.
'We already know the column number for this range but let's
'assume that we don't in a more general solution.
l_ColumnOfMonth = rng_Month.Column
'Now let's check that the range is valid.
'Don't worry about what the number means,
'just look at the error description.
'If this is True then there must be something wrong with the range.
If l_ColumnOfMonth < 2 Then
Err.Raise vbObjectError + 20000, , "There are no columns to the left of the month. " _
& "The range is invalid."
End If
'Now let's find out where the last Type entry occurs in the current
'block. We go up from the bottom of the column to do that.
'In this case we're passing the row and column to the Cells
'property. This defines a range for us representing the bottom
'of the Type column. Using End(xlUp) is the same as pressing the
'[End] key then the [Up arrow] key. It takes us to the last
'populated row, which we read the .Row property of.
l_RowLastType = ActiveSheet.Cells( _
ActiveSheet.Rows.Count, l_ColumnOfMonth).End(xlUp).Row
'If this is the same as the Month's own row, there's no data
'in that block.
If l_RowLastType = rng_Month.Row Then
Err.Raise vbObjectError + 20000, , "There are no Type entries in this block. "
End If
'So we've checked that the range looks OK.
'Before we proceed, let's store the month for this block.
'We just get the Value property of the range.
s_Month = rng_Month.Value
'We know that the first product should be 3 rows down and
'one row to the left of the month, so let's just keep looping
'through and reading the values for each row in the block
'until we reach the end of it. We know the end because
'we have its row stored in the l_RowLastType variable.
Set rng_Data = rng_Month.Offset(3, -1)
'Let's get the name of the first product.
s_Product = rng_Data.Value
'If that's nothing, there's a problem.
If s_Product = "" Then
Err.Raise vbObjectError + 20000, , "No valid product in the expected location. "
End If
'Now let's loop through each row.
For l_RowCurrent = rng_Month.Row + 3 To l_RowLastType
'Let's look at another way that we can get a reference to
'a range; by using the Cells property of the sheet.
'For that we specify the row number and column number.
Set rng_Data = ActiveSheet.Cells(l_RowCurrent, rng_Month.Column - 1)
'We know that there won't be a product on each row,
'so if there isn't one we just use the previous one.
'Otherwise we assign the new product.
If rng_Data.Value <> "" Then
s_Product = rng_Data.Value
End If
'Now let's get the type, which is offset 0 rows, 1 column
'to the right of the product.
s_Type = rng_Data.Offset(0, 1)
'If that's a blank cell (like row 8 in your
'second example we won't do anything else.
'We only proceed if it's populated.
If s_Type <> "" Then
s_Value = rng_Data.Offset(0, 2)
'Now at this point you have gathered all of your values
'into variables, and can feed them to your recordset.
'In this case though we'll just output
'a messagebox.
MsgBox "Row " & rng_Data.Row & " is for month " & s_Month _
& ", product " & s_Product & ", Type " & s_Type _
& ", Value " & s_Value
End If
Next
ExitPoint:
On Error Resume Next
Set rng_Month = Nothing
On Error GoTo 0
Exit Sub
ErrorHandler:
MsgBox "Error " & Err.Number & vbCrLf & Err.Description
Resume ExitPoint
End Sub