Excel VBA: How to Extend a Range Given a Current Selection - excel

I want to do something like:
E18-(1,1) &":" &E18+(1,1)
My intent is to keep the selection of range E18 (value = B) and extend the selection to D16:F20.
If I have a cell's range of E18 and I want to extend the range to D16:F20, how can I do this?

You mean like this?
SYNTAX
ExpandRange [Range], [Number of Col on left], [Number of Rows on Top], [Number of Col on right], [Number of Rows down]
Sub Sample()
Debug.Print ExpandRange(Range("B5"), 1, 1, 1, 1) '<~~ $A$4:$C$6
Debug.Print ExpandRange(Range("A1"), 1, 1, 1, 1) '<~~ Error
Debug.Print ExpandRange(Range("XFD4"), 1, 1, 1, 1) '<~~ Error
Debug.Print ExpandRange(Range("XFD1048576"), 1, 1, 1, 1) '<~~ Error
Debug.Print ExpandRange(Range("E5"), 1, 1, 1, 1) '<~~ $D$4:$F$6
End Sub
Function ExpandRange(rng As Range, lft As Long, tp As Long, _
rt As Long, dwn As Long) As String
If rng.Column - lft < 1 Or _
rng.Row - tp < 1 Or _
rng.Column + rt > ActiveSheet.Columns.Count Or _
rng.Row + dwn > ActiveSheet.Rows.Count Then
ExpandRange = "Error"
Exit Function
End If
ExpandRange = Range(rng.Offset(-1 * tp, -1 * lft).Address & ":" & _
rng.Offset(dwn, rt).Address).Address
End Function

Here is the simple code that I use to resize an existing selection.
Selection.Resize(Selection.Rows.Count + 5, Selection.Columns.Count + 50).Select
This will add 5 to the row count and 50 to the column count. Adapt to suit your needs.

You can use Application.WorksheetFunction.Offset() which is richer than VBA's Offset and does everything required by the question.
I think it does what Siddharth Rout ExpandRange does, without the need of a UDF.

Range(Cells(WorksheetFunction.Max(1, Selection.Row - 1), _
WorksheetFunction.Max(1, Selection.Column - 1)), _
Cells(WorksheetFunction.Min(Selection.Worksheet.Rows.Count, _
Selection.Row + 1), _
WorksheetFunction.Min(Selection.Worksheet.Columns.Count, _
Selection.Column + 1))).Select
upd: thanks Siddharth Rout for formating my msg

How to select and extend a range from anywhere on a sheet, to anywhere on a sheet.
This is my first post. I know I'm a little bit late to the party, and it's obvious to me that most of the people here are far far more experienced and skilled than I am. So I doubt my solutions include much of their "big picture" nuanced considerations, but I've verified they work for me and I hope they work for all of you too.
Okay, so back to the question.
Here is how I do it.
Example One
To do this for the exact scenario posed by your question, if you’re starting at E18 and you want to extend the range to D16:F20, use the code below. As long as you have room for the full range, your active cell can actually be anywhere, and that range will follow it.
Range(ActiveCell.Offset(-2, -1), ActiveCell.Offset(2, 1)).Select
Example Two
If you’ve already selected a range, and then you want to expand it further (let’s say and additional 2 rows down and 1 column to the right), then do this:
Range(Selection, Selection.Offset(2, 1)).Select
Example Three
If you want to select a range of all the contiguous cells containing data, starting from the active cell and continuing down until it reaches a blank cell, and then also add the cells from 1 column to the left, then do this:
Range(ActiveCell, Selection.End(xlDown).Offset(0, -1)).Select

Instead of returning an absolute address, I modifying the syntax above to return a range. Credit goes to Siddharth Rout = )
Function ExpandRG(rng As Variant, lft As Long, tp As Long, rt As Long, dwn As Long) _
As Range
Set ws = rng.Parent
If rng.Column - lft < 1 Or _
rng.Row - tp < 1 Or _
rng.Column + rt > ActiveSheet.Columns.Count Or _
rng.Row + dwn > ActiveSheet.Rows.Count Then
MsgBox "Out of range"
Exit Function
End If
Set rng = ws.Range(rng.Offset(-1 * tp, -1 * lft).Address & ":" & _
rng.Offset(dwn, rt).Address)
End Function
Sub aa()
Dim ori_add, O_add, New_add As Range
Set ori_add = Range("B2")
Set O_add = ori_add
Call ExpandRG(ori_add, 1, 1, 1, 1)
Set New_add = ori_add
MsgBox "Original address " & O_add.Address & ", new address is" & New_add.Address
End Sub

Related

Counter is working, but how to make it list counted values?

I have a macro with counter for unique values that met specific conditions. As you can see on the image, I have list of unique values in column F. Macro checks, if value is listed in column AE (can contain duplicated lines) and checks if there is no "OB" in column AH. Then returns how many values it found in cell K2. But I need this counter to also list these values in column AD, but I am struggling to make it happen. I checked many forums and managed to crash Excel twice already. Any ideas how to achieve it?
Dim myTbl As range, mStr As String, Miss As Long, xCol As Variant
Set myTbl = Sheets("OB").range("AE2") '
xCol = "AH"
mStr = ""
Set myTbl = range(myTbl, myTbl.End(xlDown).Offset(0, 1))
xCol = Cells(1, xCol).Column - myTbl.Cells(1, 1).Column + 1
For i = 1 To myTbl.Rows.count
If myTbl.Cells(i, 1) <> "" Then
If myTbl.Cells(i, xCol) <> "OB" And InStr(1, mStr, "##" & myTbl.Cells(i, 1), vbTextCompare) = 0 Then
mStr = mStr & "##" & myTbl.Cells(i, 1)
Miss = Miss + 1
End If
End If
Next i
If Miss > 0 Then
range("K2") = Miss & " still active"
range("K2").Font.ColorIndex = 46
Else
range("K2") = "None"
range("K2").Font.ColorIndex = 10
End If
Please, test the next code. It, also, is able to return how many occurrences per each Value x have been found (if more than one per each exist):
Sub ExtractUniqueCondValues()
Dim sh As Worksheet, lastR As Long, arr, i As Long, dict As Object
Set sh = Sheets("OB")
lastR = sh.Range("AE" & sh.rows.count).End(xlUp).row
arr = sh.Range("AE2:AH" & lastR).Value
Set dict = CreateObject("Scripting.Dictionary")
For i = 1 To UBound(arr)
If arr(i, 4) <> "OB" Then dict(arr(i, 1)) = dict(arr(i, 1)) + 1
Next i
sh.Range("K2").Value = dict.count
sh.Range("AD2").Resize(dict.count, 1).Value = Application.Transpose(dict.Keys)
End Sub
About occurrences per each 'Value x' element, it can return in an adiacent column 'Value 2| 1 andValue 4` | 2, for your picture case... Of course, if it may have relevance for your purpose. The dictionary already keeps this data.
Maybe using formulas is an option for you? See column G where the formula in G2 is the following and copied down.
=IF(COUNTIFS(AE:AE,F2,AH:AH,"<>OB")>0,F2,"")
Using Count or Countifs may be an option instead of VBA.

Expanding on a written VBA script for Excel

In my quest to improve the quality of life at work, I've searched for an answer and wound up borrowing this code (posted my current attempt at bottom of the post) to extract differences between two worksheets. While it returns the basic information, it is less QoL change than my current method, which, while it works most of the time, still fails. The current method is as follows:
=IF(COUNTIFS(New!$H:$H, Old!$H2, New!$C:$C, Old!$C2,New!$B:$B, Old!$B2)<1, Old!$H2, "")
This code spans across several columns to populate the appropriate information (appointment time, date, patient name, patient ID, notes, etc). This goes on a sheet called "Removed", and I have one for "Added" where New and Old are reversed.
I attempted to modify the borrowed code to paste entire rows instead of just one column, but I seem to be failing at every turn, mainly because I am new to VBA and do not have a full grasp of it yet. Changing the first For loop to:
For i = LBound(valsM, 1) To UBound(valsM, 1)
If IsError(Application.Match(valsM(i, 1), valsQ, 0)) Then
mm = mm + 1
Worksheets("Old").Cells(i).EntireRow.Copy Destination:= Worksheets("New").Cells(mm, 1)
End If
Next i
is obviously the incorrect way, and I suspect it's due to the whole thing being based on arrays. What must I change in the script to accommodate 16 columns of information that must be moved over to appropriate pages? Bonus would be putting them all on one page and appending a 17th column Q that indicates removed or added. Appreciate the help.
Sub YouSuckAtVBA()
Dim i As Long, mm As Long
Dim valsM As Variant, valsQ As Variant, valsMM As Variant
With Worksheets("New")
valsM = .Range(.Cells(1, "A"), .Cells(.Rows.Count, "A").End(xlUp)).Value2
End With
With Worksheets("Old")
valsQ = .Range(.Cells(1, "H"), .Cells(.Rows.Count, "H").End(xlUp)).Value2
End With
ReDim valsMM(1 To (UBound(valsM, 1) + UBound(valsQ, 1)), 1 To 2)
mm = 1
valsMM(mm, 1) = "value"
valsMM(mm, 2) = "missing from"
For i = LBound(valsM, 1) To UBound(valsM, 1)
If IsError(Application.Match(valsM(i, 1), valsQ, 0)) Then
mm = mm + 1
Worksheets("Old").Cells(i).EntireRow.Copy Destination:=Worksheets("New").Cells(mm, 1)
End If
Next i
For i = LBound(valsQ, 1) To UBound(valsQ, 1)
If IsError(Application.Match(valsQ(i, 1), valsM, 0)) Then
mm = mm + 1
Worksheets("New").Cells(i).EntireRow.Copy Destination:=Worksheets("Old").Cells(mm, 1)
End If
Next i
valsMM = helperResizeArray(valsMM, mm)
With Worksheets("Test")
With .Cells(.Rows.Count, "A").End(xlUp).Offset(1, 0)
.Resize(UBound(valsMM, 1), UBound(valsMM, 2)) = valsMM
End With
End With
End Sub
Function helperResizeArray(vals As Variant, x As Long)
Dim arr As Variant, i As Long
ReDim arr(1 To x, 1 To 2)
For i = LBound(arr, 1) To UBound(arr, 1)
arr(i, 1) = vals(i, 1)
arr(i, 2) = vals(i, 2)
Next i
helperResizeArray = arr
End Function
If you have Office 365 you can use the new Filter-Function
The screenshot shows the formulas using a very basic example.
"Table old" and "Table new" are created via "Insert > Table" therefore it is possible to reference the column names within the formula instead of B or D

Connect a Heading with Subheadings

I am currently working on the following excel sheet:
this is the excel sheet
I am quite new to Excel and I just found out about the VBA function. Do you guys think that it is possible to write a code that does essentially the following:
If [Total Debt Stock] -> select the next 6 cells below and add to these cells "TDS" at the beginning.
And follwing this: If [short-term debt] -> select the next 2 (for instance) cells below and add "STD".
The aim is to copy the following pdf and also keep the "embeddedness" structure of the pdf (i.e. Albania-Total Debt-Short Term Debt- Official creditior) so that STATA can use that easily this is the pdf
Has anyone ever done something like this?
Thanks in advance!
All the best,
Greg
If I understand you correctly, then I think you can use the following:
Public Sub Main()
Dim lRow As Long
Dim lLoop As Long
For lRow = 1 To Cells(Rows.Count, 1).End(xlUp).Row
If InStr(1, LCase(Cells(lRow, 1)), "total debt stock") > 0 Then
For lLoop = 1 To 6
Cells(lRow + lLoop, 1) = "TDS-" & Cells(lRow + lLoop, 1)
Next lLoop
lRow = lRow + 6
End If
If InStr(1, LCase(Cells(lRow, 1)), "short-term debt") > 0 Then
For lLoop = 1 To 2
Cells(lRow + lLoop, 1) = "STD-" & Cells(lRow + lLoop, 1)
Next lLoop
lRow = lRow + 2
End If
Next lRow
End Sub
For testing if a cell "contains" either Total Debt Stock or Short-term debt, I change everything to lowercase, so you might want to change that. I also added a hyphen after "TDS" and "STD".

VBA optimizing code to run faster, user created function is way too slow

I have written the below function which basically VLOOKUPs all the results associated with the value being VLOOKUPd and stacks them in a list.
For example
A 1
A 2
A 3
A 4
A 5
A 6
B 7
B 8
B 9
B 0
if we VLOOKUP on value A the result should be 1, 2, 3, 4, 5, 6
A 1 1, 2, 3, 4, 5, 6
A 2 1, 2, 3, 4, 5, 6
A 3 1, 2, 3, 4, 5, 6
A 4 1, 2, 3, 4, 5, 6
A 5 1, 2, 3, 4, 5, 6
A 6 1, 2, 3, 4, 5, 6
B 7 N/A
B 8 N/A
B 9 N/A
B 0 N/A
But the function takes too much time to run on more than 50 rows of data, is there a way to make it run faster and hopefully not crash the Excel file?
Function MYVLOOKUP(lookupval, lookuprange As Range, indexcol As Long)
Dim r As Range
Dim result As String
result = ""
For Each r In lookuprange
If r = lookupval Then
If result = "" Then
result = result & " " & r.Offset(0, indexcol - 1)
Else
result = result & ", " & r.Offset(0, indexcol - 1)
End If
End If
Next r
MYVLOOKUP = result
End Function
You could consider using the Find() method of the Range object like so:
Function MYVLOOKUP(lookupval, lookuprange As Range, indexcol As Long) As String
Dim foundRange As Range
Dim foundArr() As String: ReDim foundArr(0 To 0)
Dim firstFoundAddress As String
'perform the first find
Set foundRange = lookuprange.Find(lookupval)
'Capture address to avoid looping
firstFoundAddress = foundRange.Address
'Find values
Do While Not foundRange Is Nothing
'Bump the array if this isn't the first element
If foundArr(0) <> "" Then ReDim Preserve foundArr(0 To UBound(foundArr) + 1)
'Add to the array
foundArr(UBound(foundArr)) = foundRange.Offset(, indexcol - 1).Value
'Lookup next value
Set foundRange = lookuprange.Find(What:=lookupval, After:=foundRange)
'Exit if we looped
If foundRange.Address = firstFoundAddress Then Exit Do
Loop
'join the results for output
MYVLOOKUP = Join(foundArr, ",")
End Function
Find() is very quick to run and you won't have to iterate your entire search range.
#JNevill just beat me to it, but wanted to post my code anyway. :)
This will work for a sorted list and return #N/A if lookupval isn't found.
Public Function MyVlookup(lookupval As Variant, lookuprange As Range, indexcol As Long) As Variant
Dim rFound As Range
Dim itmCount As Long
Dim rReturns As Variant
Dim itm As Variant
Dim sReturn As String
With lookuprange
'After looks at the last cell in first column,
'so first searched cell is first cell in column.
Set rFound = .Columns(1).Find( _
What:=lookupval, _
After:=.Columns(1).Cells(.Columns(1).Cells.Count), _
LookIn:=xlValues, _
LookAt:=xlWhole, _
SearchDirection:=xlNext, _
MatchCase:=True)
If Not rFound Is Nothing Then
itmCount = Application.WorksheetFunction.CountIf(lookuprange, lookupval)
rReturns = rFound.Offset(, indexcol - 1).Resize(itmCount)
For Each itm In rReturns
sReturn = sReturn & itm & ","
Next itm
MyVlookup = Left(sReturn, Len(sReturn) - 1)
Else
MyVlookup = CVErr(xlErrNA)
End If
End With
End Function
Edit - almost works. =MyVlookup("A",$A6:$B$10,2) on the sample data returns #VALUE rather than 6.
You haven't provided any information on how the UDF is deployed but I'll bet that is at least half of the problem.
I'm betting you are recreating that concatenated string for every duplicate in column A. Further, I reckon there is a pretty good chance you are using full column references.
I'm going to assume that your data starts in row 2.
The extent of the numbers in column B is,
b2:index(b:b, match(1e99, b:b))
The extent of the duplicated identifiers in column A is,
a2:index(a:a, match(1e99, b:b))
If you have already concatenated a result for the identifier in column A then it is a lot faster to retrieve that result from above then it is to build it again. Further, if you are looking above the current row to see if a result has already been processed and it hasn't been processed then there is no reason to include those rows in the current concatenation build.
In C2 use this formula and fill down to the extent of the values in columns A & B.
=iferror(index(c$1:C1, match(a2, a$1:a1, 0)), MYVLOOKUP(a2, a$1:index(b:b, match(1e99, b:b)), 2))
If your data actually starts in row 1then usethis formula in C1.
=MYVLOOKUP(a2, a$1:index(b:b, match(1e99, b:b)), 2)
Example:
Consider the above formula in C10. It looks for a match to A10 within A1:A9; if found, it returns the previously concatenated string from the associated row in column C. If not found, it builds a new concatenated string but only from the identifiers starting in row 10 from column A and the values starting with row 10 in column B down to the row containing the last number in column B.

Unique Count Formula for large dataset

I am having trouble determining a way to enter a 1 or 0 into an adjacent cell to indicate whether or not a value is unique when working with a large dataset. I have read of multiple methods for accomplishing this, however none of them seem efficient for my purposes: I am using an instance of Excel 2010 (so I do not have the Distinct Count feature in PivotTables, and when I try to use PowerPivot it crashes my file due to processing limitations.
In this StackOverflow question: Simple Pivot Table to Count Unique Values there are suggestions to use SUMPRODUCT or COUNTIF, but when working with 50,000+ rows as I am, this causes terrible performance and a file size of ~35 MB instead of ~3 MB. I wanted to know if there is a better solution for a large, dynamic dataset whether it is a formula or VBA.
An example of what I would like to accomplish is (with the Unique column being the adjacent cell):
Name Week Unique
John 1 1
Sally 1 1
John 1 0
Sally 2 1
I attempted to script the same functionality of COUNTIF but with no success:
For Each Cell In ThisWorkbook.Worksheets("Overtime & Type Data").Range("Z2:Z" & DataLastRow)
If Worksheets("Overtime & Type Data").Cells(Cell.Row, 26) <> Worksheets("Overtime & Type Data").Cells(Cell.Row - 1, 26) Then
FirstCell = Cell.Row
End If
If (Worksheets("Overtime & Type Data").Range(Cells(FirstCell, 26), Cells(Cell.Row, 26)) = Worksheets("Overtime & Type Data").Range(Cells(Cell.Row, 26))) = True Then
Cell.Value = 1
Else
Cell.Value = 0
End If
Next Cell
This code ran on over 130,000 rows successfully in less than 3 seconds. Adjust the column letters to fit your dataset.
Sub tgr()
Const colName As String = "A"
Const colWeek As String = "B"
Const colOutput As String = "C"
Dim ws As Worksheet
Dim rngData As Range
Dim DataCell As Range
Dim rngFound As Range
Dim collUniques As Collection
Dim arrResults() As Long
Dim ResultIndex As Long
Dim UnqCount As Long
Set ws = ThisWorkbook.Sheets("Overtime & Type Data")
Set rngData = ws.Range(colName & 2, ws.Cells(Rows.Count, colName).End(xlUp))
Set collUniques = New Collection
ReDim arrResults(1 To rngData.Cells.Count, 1 To 1)
On Error Resume Next
For Each DataCell In rngData.Cells
ResultIndex = ResultIndex + 1
collUniques.Add ws.Cells(DataCell.Row, colName).Value & ws.Cells(DataCell.Row, colWeek).Value, ws.Cells(DataCell.Row, colName).Value & ws.Cells(DataCell.Row, colWeek).Value
If collUniques.Count > UnqCount Then
UnqCount = collUniques.Count
arrResults(ResultIndex, 1) = 1
Else
arrResults(ResultIndex, 1) = 0
End If
Next DataCell
On Error GoTo 0
ws.Cells(rngData.Row, colOutput).Resize(rngData.Cells.Count).Value = arrResults
End Sub
One approach is to sort by Name and Week. Then you can determine Unique for any row by comparing with the previous row.
If you need to preserve the order, you could first write a column of Index numbers (1, 2, 3, ...) to keep track of order. After calculating Unique, sort by Index to restore the original order.
The whole process could be done manually with relatively few steps, or automated with VBA.
I'm not sure how well this will work with 50000 values, but it goes through ~1500 in about a second.
Sub unique()
Dim myColl As New Collection
Dim isDup As Boolean
Dim myValue As String
Dim r As Long
On Error GoTo DuplicateValue
For r = 1 To Sheet1.UsedRange.Rows.Count
isDup = False
'Combine the value of the 2 cells together
' and add that string to our collection
'If it is already in the collection it errors
myValue = Sheet1.Cells(r, 1).Value & Sheet1.Cells(r, 2).Value
myColl.Add r, myValue
If isDup Then
Sheet1.Cells(r, 3).Value = "0"
Else
Sheet1.Cells(r, 3).Value = "1"
End If
Next
On Error GoTo 0
Exit Sub
DuplicateValue:
'The value is already in the collection so put a 0
isDup = True
Resume Next
End Sub
Just about any bulk operation will beat a loop involving worksheet cells. You might be able to trim the time down a bit by performing all of the calculations in memory and only returning the values back to the worksheet en masse when it is complete.
Sub is_a_dupe()
Dim v As Long, vTMP As Variant, vUNQs As Variant, dUNQs As Object
Debug.Print Timer
On Error GoTo bm_Uh_Oh
Set dUNQs = CreateObject("Scripting.Dictionary")
With Worksheets("Sheet1")
vTMP = .Range(.Cells(2, 1), .Cells(Rows.Count, 2).End(xlUp)).Value2
ReDim vUNQs(1 To UBound(vTMP, 1), 1 To 1)
For v = LBound(vTMP, 1) To UBound(vTMP, 1)
If dUNQs.Exists(Join(Array(vTMP(v, 1), vTMP(v, 2)))) Then
vUNQs(v, 1) = 0
Else
dUNQs.Add Key:=Join(Array(vTMP(v, 1), vTMP(v, 2))), _
Item:=vTMP(v, 2)
vUNQs(v, 1) = 1
End If
Next v
.Cells(2, 3).Resize(UBound(vUNQs, 1), 1) = vUNQs
End With
Debug.Print Timer
bm_Uh_Oh:
dUNQs.RemoveAll
Set dUNQs = Nothing
End Sub
Previous experience tells me that the variety of data (as well as hardware, etc) will impact timing the process but in my random sample data I received these elapsed times.
 50K records ..... 0.53 seconds
130K records .... 1.32 seconds
500K records .... 4.92 seconds

Resources