vba column address from column number - excel

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

Related

Finding cells that do not match a predefined specific pattern in Excel using VBA

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.

Excel VBA Sort when Inputting Data, Updated in All Other Sheet?

I am new to VBA Excel.
I wanna create a database for each month, with 1 'Main Sheet' for the list of names, and the list on 'Main Sheet' will sort automatically as a row when I entered data along with the update of the other sheet.
I found this code, and it's only updated/sorting in the sheet entered (let's say the 'Main Sheet'). Is that possible if the entered data and sorting updated automatically in all other sheets?
Private Sub Worksheet_Change(ByVal Target As Excel.Range)
If Target.Column = 1 Then
Dim lastRow As Long
lastRow = Cells(Rows.Count, 1).End(xlUp).Row
Range("A2:BN2" & lastRow).Sort key1:=Range("A2:A" & lastRow), order1:=xlAscending, Header:=xlNo
End If
End Sub
I tried referencing/paste the link to the name for each month (start from A2), but I have no idea to keep the column beside A2 (B2, C2, etc) sticking with the A2 as a full row.
Thanks!
Look at this part of your code for ideas how to solve your problem.
Range("A2:BN2" & lastRow).Sort Key1:=Range("A2:A" & lastRow)
The instruction is to sort Range("A2:BN2" & lastRow) on Key1. The range to sort starts at A2 and ends with the last used row in column BN. All columns of all rows are included, as they should. But lastRow was determined in column 1, which is column A. We hope that all columns have the same length. Observe that we don't know on which tab the range is located. By default, if no sheet is specified, Excel will presume the ActiveSheet. This is borne out by the fact that the code is located in a worksheet event procedure. Of course, this code is linked to the sheet on whose code module it is placed. It won't run when another sheet is active.
However, the syntax for specifying a range for a particular sheet would look like this.
With Worksheets("MySheet")
lastRow = .Cells(.Rows.Count, 1).End(xlUp).Row
Set MyRange = .Range("A2:BN2" & lastRow)
End With
Please observe all the leading periods, each of which links the statement it precedes to the worksheet specified in the With statement. Imagine a loop in which the sheet name is changed on each turn, thereby defining the same range on a different sheet on each instance.
The Key argument of the Sort method specified the column on which to sort. It's called "Key" instead of "Column" because there are sheet columns and range columns. In your case the sheet rows are different from your range rows because your range starts in row 2 but the range columns are identical with the sheet columns. Anyway, Key1:=Range("A2:A" & lastRow) isn't a very good pointer to the column you want to sort on. 1 single cell would suffice, like Key1:=Range("A2") - or Key1:=Cells(2, 1) as I would prefer.
As you see, this part is a lot simpler. The only important point to observe is that the cell specifying the Key must be within the range to be sorted. This also means that you can't specify a Key on the ActiveSheet for a sort range on another tab. When you construct your loop, therefore, you will need to take a cell from the properly defined sort range as Key.

VBA to reference the same cell even after the addition or deletion of rows & Columns

I'm using VBA code to write to cells in excel. For eg.
Range("C3") = code
Or
Cells(3,3) = code
If a row is inserted in the sheet, the code does then not update accordingly and would still write to Range("C3") etc. So the code then writes to the incorrect cell.
Is there a better way I can structure my code so it will update accordingly? Perhaps using a table instead of cells?
One solution is to used Named Ranges. You can define a Named Range in Formula Tab by clicking on Name Manager.
Then you will write.
Range("Name of the Range") = code
My believe is that named ranges update automatically when a row or column is inserted, so your code will print the value in the correct cell.
Thanks, good idea. I ended up defining the column filled with values as a range, then use a for loop to search for the defined string. That way it doesnt matter what row it is in aslong as the name and string doesnt change (Using a Named Range will probably be better for that exact reason).
Worksheets("Sheet1").Select
Set WS = ActiveSheet
With WS
LastRow = .Cells(.Rows.Count, 2).End(xlUp).Row 'Determine the last row number with data in it for column B
For Each acell In .Range("B1:B" & LastRow) 'Defines the search range from B1 to last row
If acell.Value = "String Searched For" Then
'Do stuff based on found cell location
End If
If acell.Value = "String Searched For#2" Then
'Do stuff based on found cell location#2
End If
Next
End With

Selecting an area with exception of merged cells in dynamic range

IMPORTANT EDIT: The main issue here is caused by hidden merged cells that are causing the entirity of their active range to be selected. Unless you know a way how to dynamically skip merged cells (in a dynamic range), the it most likely won't help. Have changed the entirity of question accordingly
any idea what am I doing wrong?
Got the following code, fyi the function find_last_row returns the value of last active row as integer. In this case, the returned variable would be 40
Private Sub initalize_button_Click()
Dim lastRow As Integer
Dim ws As Worksheet: Set ws = Sheets("Training_Planner")
lastRow = find_last_row
With ws
.Activate
.Range("E5:H" & lastRow).Select
End With
End Sub
Pretty basic code, should open the worksheet Training_Planner and select from E5 to HlastRow (in this case lastRow is 40) so the selected range should be E5:H40
Here is the expected result:
What I get instead:
Curiously enough, it selects only active range, but it's as if it didn't pay attention to columns, instead of the expected E5:H40 i get B5:I40
Any idea what's causing this?
Ok, first of all, if your range is gonna start always as E5, your range is 50% dinamic, because it starts always in same column and same row. Your Range is (Cells(a,b),Cells(c,d)), this means a = 5 and b = 5 (Column E).
Also, you say and Inputbox asks users for end cell of range (in your example is H40, but this is dynamic).
So, my code checks EVERY SINGLE cell in the range formed, and then, using Application.UnionI set a final big range. We cannot just use an array to select all of them, because your range is dynamic, and selecting ranges with arrays is limited to 30 args, so we need to update our FinalRange for each cell.
Dim MyCell As Range
Dim RangeWanted As Range
Dim MyFinalRange As Range
Set RangeWanted = Range("E5:" & InputBox("Cell Address")) 'User inputs Final Cell of Range. Start is always E5
'let's get all invididual addresses of each cell inthat dynamic RangeWanted
For Each MyCell In RangeWanted
If MyCell.MergeCells = False Then 'If not merged, we add it to FinalRange
If MyFinalRange Is Nothing Then
Set MyFinalRange = MyCell
Else
Set MyFinalRange = Application.Union(MyFinalRange, MyCell)
End If
End If
Next MyCell
Set RangeWanted = Nothing
MyFinalRange.Select
With this code, from Range("E5:H40") in image,yellow cells are merged. I want to select only the not merged ones. And using this code, I get this:
My example is with Range("E5:H40") but it works also with other ranges.
Try it and adapt the code to your needs.
Whenever something small like this is happening, simply try to simplify as much as you can. In your case, it would be this:
Sub TestMe()
Worksheets("Training_Planner").Range("E5:H40").Select
End Sub
If it selects E5:H40 then everything is ok. If not, try to select it manually. Probably you have a hidden row, which is merged from B to I, thus it is happenning this way.
Instead of this:
.Range("E5:H" & lastRow).Select
Try going with this:
.Range("E5", (Cells(Rows.Count, "H").End(xlUp))).Select
It count all the rows "H" has and then goes up until it finds the first item. And it will then select from "E1" to last item in "H"

Find the last row with data and autoFit everything

I'm creating an extract in Excel with data from a datatable in my vb.net application. It extracts everything correctly. Now I'm trying to do a bit of design work to make it all pretty. It seems like I'm having an issue with probably something every easy but for some reason I keep getting the below error.
Public member 'XlDirection' on type 'ApplicationClass' not found.
My goal is to find the last row of data in Column A, and then take all cells starting A4 and do .columns.autofit on all columns starting A4 The reason why I'm doing that is because cells A1 - A3 have some long text values in them and I want them to be as they are.
Code:
Dim wSheet As Microsoft.Office.Interop.Excel.Worksheet
Dim _excel As New Microsoft.Office.Interop.Excel.Application
wSheet = wBook.ActiveSheet()
Dim lRow As Long
With wSheet
lRow = .Range("A" & .Rows.Count).End(_excel.XlDirection.xlUp).Row
.Range("A4" & lRow).Columns.AutoFit()
End With
The original error was due to the fact that XlDirection is an enumeration in the Microsoft.Office.Interop.Excel namespace - it isn't a member of Excel.Application. The bit _excel.XlDirection.xlUp should be:
Microsoft.Office.Interop.Excel.XlDirection.xlUp
The second issue is that the Range you build here...
lRow = .Range("A" & .Rows.Count).End(_excel.XlDirection.xlUp).Row
.Range("A4" & lRow).Columns.AutoFit()
...just appends a row number to the end of "A4". So if the last row was 42, the range you would auto-fit would be "A442". It needs to be ("A4:A" & lRow).
But that still only auto-fits column A. If that's your intent, stop here. If you need to auto-fit all the columns (as indicated by the phrase "all columns starting A4" - my emphasis), read on.
First, you don't need to find the last row number - you're working with Columns when you're performing your auto-fit, so Rows.Count works as well as anything else. You really need to find the right-most column, but I'd skip all of that and just offset the used range down by 3 rows:
With wSheet
.UsedRange.Offset(3).Columns.AutoFit
End With

Resources