Given that I have a column of values in the format
01.2020
12.2021
3.2019
02.2020
etc.
over a million rows or so, I want a VBA function to find the maximum value of the digits to the left of the period.
Something like
Option Explicit
Sub test()
Dim r As Range, c As Range
Dim max As Long, current As Long
Dim s As String
With År_2020
Set r = .Range(.Range("D2"), .Range("D" & .Rows.Count).End(xlUp))
End With
For Each c In r
current = CLng(Left(c, InStr(1, c, ".", vbBinaryCompare) - 1))
If current > max Then max = current
Next c
Debug.Print max
End Sub
works, but I feel like there ought to be a simpler solution.
Can someone please give some input on whether they can find a simpler solution?
Formula solution
formula in B1: =VALUE(LEFT(A1,FIND(".",A1)-1))
in C1: =MAX(B:B)
VBA solution
Dim ws As Worksheet
Set ws = ThisWorkbook.Worksheets("Sheet1")
Dim LastRow As Long 'find last used row
LastRow = ws.Cells(ws.Rows.Count, "A").End(xlUp).Row
Dim ValuesToConvert() As Variant 'read data into array
ValuesToConvert = ws.Range("A1", "A" & LastRow).Value 'will throw an error if there is only data in A1 but no data below, make sure to cover that case
Dim iVal As Variant
For iVal = LBound(ValuesToConvert) To UBound(ValuesToConvert)
'truncate after period (might need some error tests if period does not exist or cell is empty)
ValuesToConvert(iVal, 1) = CLng(Left(ValuesToConvert(iVal, 1), InStr(1, ValuesToConvert(iVal, 1), ".") - 1))
Next iVal
'get maximum of array values
Debug.Print Application.WorksheetFunction.Max(ValuesToConvert)
Seems to work. And it is short.
Sub test()
Dim max As Long
max = [MAX(TRUNC(D:D))]
Debug.Print max
End Sub
I always like to propose a naive solution. On the face of it you could do this:
=TRUNC(MAX(--D:D))
(leave the truncation till the end because the part of the number after the decimal point doesn't affect the result)
As pointed out below, this only works correctly for those locales that use a decimal point (period) not a decimal comma.
Related
Before I start, I just want to thank every contributor ahead of time. I've only posted one question before, and I was amazed at how quickly I got responses and how much I learned after studying the solution. I'm hoping I will have enough reputation points soon to start upvoting good solutions I find here.
Anyways, what I'm trying to do is return one number, and that number is the maximum number of names that appear in a single cell of a worksheet column. Each cell in that column can have any number of names in it. Each name is delimited by a pipe "|", so I count the pipes and then add one to get the number of names in each cell. For example: Cell value is "Bob | Jon | Larry" = 2pipes +1 = 3 names.
My code below works, but I need to do this on tens of thousands of records. I don't think my solution is a good or efficient way to do it (tell me if I'm wrong). So my questions are:
Is there a better way to accomplish this, such as without looping through every cell in the range?
If there isn't a totally different approach to this, how can I avoid actually printing the name counts in cells in a new column? Could I store these values in an array and calculate the max of the array? (maybe there is already a thread on this topic you could point me to?)
Sub charCnt()
Application.ScreenUpdating = True
Application.Calculation = xlCalculationManual
Dim wb As Workbook: Set wb = ThisWorkbook
Dim ws As Worksheet: Set ws = Worksheets("Leasing")
Dim vRange As Variant
Dim iCharCnt As Integer
Dim iRows As Integer
Dim i As Integer
Dim iMax As Integer
Const sFindChar As String = "|"
iRows = ws.Cells(Rows.Count, "A").End(xlUp).Row 'count number of rows
For i = 1 To iRows
vRange = Cells(i, "O") 'column O has the names
iCharCnt = Len(vRange) - Len(Replace(vRange, sFindChar, "")) 'find number of | in single cell.
ws.Cells(i, "W") = iCharCnt 'column W is an empty column I use to store the name counts
Next i
iMax = Application.WorksheetFunction.Max(Range("W:W")) + 1 'return max from column W
Application.ScreenUpdating = True
Application.Calculation = xlCalculationAutomatic
MsgBox ("Max number of names in one cell is " & iMax) ' show result
End Sub
Max Number of Substrings
Option Explicit
Sub charCount()
Const cCol As String = "O"
Const fRow As Long = 1
Const Delimiter As String = "|"
Dim wb As Workbook: Set wb = ThisWorkbook
Dim ws As Worksheet: Set ws = wb.Worksheets("Leasing")
Dim lRow As Long: lRow = ws.Cells(ws.Rows.Count, cCol).End(xlUp).Row
Dim rg As Range: Set rg = ws.Cells(fRow, cCol).Resize(lRow - fRow + 1)
Dim Data As Variant: Data = rg.Value
Dim i As Long
For i = 1 To UBound(Data, 1)
Data(i, 1) = Len(Data(i, 1)) - Len(Replace(Data(i, 1), Delimiter, ""))
Next i
Dim iMax As Long: iMax = Application.Max(Data) + 1
MsgBox ("Max number of names in one cell is " & iMax) ' show result
End Sub
A close-to-formula approach
Combining worksheet functions CountA() and FilterXML() allows to get all substring counts separated by the pipe character |:
Sub CountSubstrings(StartCell As Range, TargetRng As Range)
'Purp.: count items separated by pipes
'Meth.: via worksheetfunction FILTERXML()
'Note: assumes target in same sheet as StartCell (could be changed easily)
'a) enter formula into entire target range
Const PATTERN$ = _
"=IF(LEN($),COUNTA(FILTERXML(""<t><s>""&SUBSTITUTE($,""|"",""</s><s>"")&""</s></t>"",""//s"")),0)"
TargetRng.Formula2 = Replace(PATTERN, _
"$", StartCell.Parent.Name & "!" & StartCell.Address(False, False))
'b) optional overwriting of formulae
'TargetRng = TargetRng.Value
'c) display maximum result
MsgBox Application.Max(TargetRng)
End Sub
Hint: You can even shorten code as follows if you want to include the fully qualified workbook + worksheet reference in the formula assignment. Just use the additional argument External:=True in .Address (resulting e.g. in something like '[Test.xlsm]Sheet1'!A2):
TargetRng.Formula2 = Replace(PATTERN, _
"$", StartCell.Address(False, False, External:=True))
Possible Example call
With Sheet1
CountSubstrings .Range("A2"), .Range("D2:D5")
End With
Further link
C.f. JvdV's encyclopaedia-like site demonstrating the various possibilities to use FilterXML()
Brilliant answer by VBasic2008. I thought I would look at it purely as a coding exercise for myself. Alternative below provided for interest only.
Option Explicit
Sub CountMaxNames()
Dim arr1(), i, j, count As Long, tally As Long, ws As Worksheet
Set ws = ThisWorkbook.Worksheets("leasing")
arr1 = ws.Range("O1:O" & ws.Range("O" & Rows.count).End(xlUp).Row)
count = 0: tally = 0
For Each i In arr1
For j = 1 To Len(i)
If Mid(i, j, 1) = "|" Then count = count + 1
Next j
count = count + 1
If count >= tally Then tally = count
count = 0
Next i
MsgBox "Maximum number of names in one cell is " & tally
End Sub
Is it possible to find a row with 2 criteria?
I'm importing survey anwsers to a worksheet, now I want to find the answers of a specified person
I need to find the row in the worksheet(ImportLimesurvey) that has 2 specified cell values:
In that row:
the value of the C-cell has to be one of the highest value in that column (I used the function Application.WorksheetFunction.Max(rng))
This value means how much of the survey is filled in. The highest value stands in multiple answer-rows. The highest value is different for every survey. (example, if a survey has 7 pages and the participant fills in all pages :the highest value is 7 for that person, but if the person didn't complete that survey, the value could be e.g. 3), So the filter of the highest value is if the participant completed the whole survey.
the value of the L-cell has to be the same as the cell (Worksheets("Dataimport").Range("M2")
M2= accountnumber of the person I need the answers from
The correct row has to be pasted to (Worksheets("Dataimport").Range("A7")
This is my current code:
Dim g As Range
Dim rng As Range
Set rng = Worksheets("ImportLimesurvey").Range("C:C")
d = Application.WorksheetFunction.Max(rng)
With Worksheets("ImportLimesurvey").Range("L:L")
Set g = .Find(Worksheets("Dataimport").Range("M2"), LookIn:=xlValues)
g.Activate
End With
e = Range("C" & (ActiveCell.Row))
If e = d Then
ActiveCell.EntireRow.Copy _
Destination:=Worksheets("Dataimport").Range("A7")
End If
The problem here is that he finds the row with the right account number, but the answer with the C-value isn't always the highest. It picks (logically) just the first row with that accountnumber. So how can I find the row that matches those 2 criteria?
Thanks in advance
P.S. I'm new to VBA so I tried to be as specific as possible but if you need any additional info, just ask for it ;)
dmt32 forom mrexcel.com found a solution.
Link to topic: https://www.mrexcel.com/board/threads/find-row-with-2-criteria.1157983/
His code works fine:
Sub FindMaxValue()
Dim FoundCell As Range, rng As Range
Dim MaxValue As Long
Dim Search As String, FirstAddress As String
Dim wsDataImport As Worksheet, wsImportLimesurvey As Worksheet
With ThisWorkbook
Set wsDataImport = .Worksheets("Dataimport")
Set wsImportLimesurvey = .Worksheets("ImportLimesurvey")
End With
Search = wsDataImport.Range("M2").Value
If Len(Search) = 0 Then Exit Sub
With wsImportLimesurvey
Set FoundCell = .Range("L:L").Find(Search, LookIn:=xlValues, lookat:=xlWhole)
If Not FoundCell Is Nothing Then
FirstAddress = FoundCell.Address
Do
With FoundCell.Offset(, -9)
If .Value > MaxValue Then Set rng = FoundCell: MaxValue = .Value
End With
Set FoundCell = .Range("L:L").FindNext(FoundCell)
If FoundCell Is Nothing Then Exit Do
Loop Until FoundCell.Address = FirstAddress
rng.EntireRow.Copy wsDataImport.Range("A7")
MsgBox Search & Chr(10) & "Record Copied", 64, "Match Found"
Else
MsgBox Search & Chr(10) & "Record Not Found", 48, "Not Found"
End If
End With
End Sub
Still thanks for the tips.
Firstly, Visual Basic conceptual topics is a great read to help in writing 'better' code. The biggest thing I encourage is to use meaningful variable names.
It's much easier to understand your code when you have variable names like HighestCount or TargetSheet etc. rather than names like a or b etc.
The answer to your question is yes.
I would write something like this:
Option Explicit
Public Function HighestSurveyRow(ByVal TargetAccountNumber As Long) As Long
Dim ImportLimeSurveySheet As Worksheet
Set ImportLimeSurveySheet = ThisWorkbook.Sheets("ImportLimeSurvey")
Dim LastRow As Long
Dim TargetRow As Long
Dim SurveyCountArray As Variant
Dim ArrayCounter As Long
With ImportLimeSurveySheet
ArrayCounter = 1
LastRow = .Cells(.Rows.Count, 12).End(xlUp).Row
ReDim SurveyCountArray(1 To LastRow, 1 To 2)
For TargetRow = 1 To LastRow
If .Cells(TargetRow, 12).Value = TargetAccountNumber Then
SurveyCountArray(ArrayCounter, 2) = TargetRow
SurveyCountArray(ArrayCounter, 1) = .Cells(TargetRow, 3).Value
ArrayCounter = ArrayCounter + 1
End If
Next TargetRow
End With
Dim ResultArray(1 To 2) As Variant
Dim ArrayElement As Long
For ArrayElement = 1 To UBound(SurveyCountArray, 1)
If SurveyCountArray(ArrayElement, 1) > ResultArray(1) Then
ResultArray(1) = SurveyCountArray(ArrayElement, 1)
ResultArray(2) = SurveyCountArray(ArrayElement, 2)
End If
Next ArrayElement
HighestSurveyRow = ResultArray(1)
End Function
Sub FindRowForSurveyResults()
With ThisWorkbook.Sheets("DataImport")
.Range("A7").Value = HighestSurveyRow(.Range("M2").Value)
End With
End Sub
It's split into a Function and a Subroutine. The Function executes most of the code and returns the row number. The Sub calls this function and writes this returned value to cell A7 on "DataImport".
The sub can be broken down as follows;
Using a with statement helps reduce code clutter of defining the worksheet twice.
The only thing the sub is doing is assigning a value to cell A7. To get the value it calls the function and assigns the parameter TargetAccountNumber as the value from cell M2.
The function can be broken down into the following steps;
All variables are declared and the target worksheet for the function is set.
The LastRow of column L is found to establish our maximum length of the Array and search range.
The Loop searches from Row 1 to the LastRow and compares the values from column L. If it matches the TargetAccountNumber parameter then the column C value and the row number are stored into the Array.
Once the Loop is done, another Loop is run to find the highest number. The first iteration will always store the first row's data. Each iteration after that compares the values stored in the SurveyCountArray with the current value of ResultArray(1) and if the value is greater, ResultArray(1) is updated with the value, ResultArray(2) is updated with the Row number.
Once the 2nd loop is done, the Row in ResultArray(2) is assigned to the function for the Sub to write to the worksheet.
It can definately be improved and refined to work faster and more efficiently, especially if you have a very large data set, but this should help get you thinking about ways you can use loops and arrays to find data.
Note: There could be duplicate rows for the outcome (say a user submits the same survey 3 times with the same answers), which I haven't tested for - I think this code would return the highest row number that matches the required criteria but could be tweaked to throw an error or message or even write all row numbers to the sheet.
I have been trying to find something that can help me online but no luck. I am trying to compare a value in column A with a value in Cell E1 and if match I want to put an X in column B next to the match in Column A.
here is my code I go so far:
Option Explicit
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim a As Integer
Dim i As Integer
Dim x As Range
Dim y As Range
a = Worksheets("Sheet1").Cells(Rows.Count, 1).End(xlUp).Row
i = Worksheets("Sheet1").Range("E1")
x = Worksheets("Sheet1").Range("B1:a")
y = Worksheets("Sheet1").Range("A1:a")
'For Each cell In y
'if y = i then
'print "X" in column B next to the value
'MsgBox (i)
End Sub
thanks for your help in advance
Dan
There are a few things here that are worth mentioning. When you want to specify a range using .Range you have to specify the columns on both sides of the : ; furthermore, it takes a string. This means that what you're passing is "B1:a" which doesn't make sense to the computer because it doesn't know you want it to use the value of a instead of the letter. You need to pass "B1:B" & a to the .Range. What this does is concatenate the value you found in the variable a to the string so it appears as one string to the computer.
I personally think it's easier to take all of the values as a column vector instead of dimming the x's as a range because it makes the iteration a little easier. Instead of keeping track of what row I'm on, Counter will always tell me where I am since I'm just moving down a single column. As an added bonus, this reduces the times you access the worksheet which helps speed up your macro.
Although it's commented out, it's worth noting that the loop at the bottom of your sub wouldn't work because you haven't properly closed off the if or the for.
I'm not sure what you intended this for, but it's never a bad idea to use meaningful names so you can look back on your code and figure it out without too much effort. For example, I've renamed your a variable to lastrow which at a glance describes what value it stores.
Below your code that I've altered
Option Explicit
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim lastrow As Long
Dim Criteria As Long
Dim x() As Variant
Dim Counter As Long
lastrow = Worksheets("Sheet1").Cells(Rows.Count, 1).End(xlUp).Row
Criteria = Worksheets("Sheet1").Range("E1").Value
x = Worksheets("Sheet1").Range("B1:B" & lastrow).value
For Counter = 1 To UBound(x)
If x(Counter,1) = Criteria Then
Worksheets("Sheet1").Cells(Counter, "B").Value = "X"
End If
Next Counter
MsgBox (Criteria)
End Sub
I little bit different approach. This find the last row in column A.
I also included if you want to match by wildcard, i.e. you want to find 45 in 645.
Sub Worksheet_SelectionChange()
Dim lrow As Integer
Dim a As Integer
Dim i As String
Dim Val As String
lrow = Worksheets("Sheet1").Cells(Rows.Count, "A").End(xlUp).Row 'Find last row
i = Worksheets("Sheet1").Range("E1") 'Set cell where compare value is
For a = 1 To lrow 'Loop from row 1 to last row in column A
Val = Cells(a, "A").Value 'Set value to compare in Column A
'If Val Like "*" & i & "*" Then 'Use this if you want to find 45 in 645, so wildcard
If Val = i Then 'Exact match
Cells(a, "B").Value = "X" 'Put X in column B
End If
Next a
MsgBox "Match Criteria: " & (i)
End Sub
I have a very simple question, which unfortunately I can not resolve. Thus would appreciate your help. Here is the thing:
I should obtain last column from the range as a range. For example, if I have A26:D32, I should get D26:D32 as a result and input into the loop.
This is the code I have so far:
Function getSmth(CustomCol As Range)
Dim i As Double
For Each cell In CustomCol 'start of the loop. CustomCol here should return D26:D32 already
If cell.Value > i Then
i = cell.Value
End If
Next
....
End Function
What I have tried to do was writing CustomCol.Columns(6), as I know the last column, but it did not work out.
Would really be glad for your help!
You can try this:
Function GetLasRangeCol(rng As Range) As Range
Set GetLasRangeCol = rng.Columns(rng.Columns.Count).Cells
End Function
That you may use in your calling code as:
For Each cell In GetLasRangeCol(myRange)
Where ‘myRange’ is a valid range reference, i.e: either a variable of Range type or some Range object (like ‘Range(“A5:B21”)’) to get last column out of
There's probably a shorter way to it, but this does the trick:
Function lastColumn(rg As Range) As Range
Set lastColumn = Range(Cells(Range(Split(rg.Address(0, 0), ":")(0)).Row, _
Range(Split(rg.Address(0, 0), ":")(1)).Column).Address(0, 0) _
& ":" & Split(rg.Address(0, 0), ":")(1))
End Function
It take a Range object as a parameter and returns a Range object.
Example Usage:
Sub demo()
Dim rg As Range, rg2 As Range
Set rg = Range("A26:D32")
Set rg2 = lastColumn(rg)
Debug.Print "The last column is: " & rg2.Address
End Sub
...returns: The last column is: $D$26:$D$32
There's probably a "tidier" method (perhaps using INTERSECT) but this will work fine.
Explanation:
This is the same function as above, but broken down so it's easier to understand:
Function lastColumn(rg As Range) As Range
Dim firstRow, lastRow, firstCol, lastCol, leftPart, rightPart
leftPart = Split(rg.Address(0, 0), ":")(0)
rightPart = Split(rg.Address(0, 0), ":")(1)
firstRow = Range(leftPart).Row
firstCol = Range(leftPart).Column
lastCol = Range(rightPart).Column
Set lastColumn = Range(Cells(firstRow, lastCol), Range(rightPart))
End Function
More Information:
MSDN : Application.Range Property (Excel)
MSDN : Range.Cells Property (Excel)
Office Support : Split Function (VBA)
If you want the last column as a range, then
myRange.Columns(myRange.Columns.Count)
If you want the number of the last column then
myRange.Columns(myRange.Columns.Count).Column
To find last column in row 1:
Dim LastCol As Long
LastCol = Sheets("Sheet1").Cells(1, Columns.Count).End(xlToLeft).Column
I am trying to get the earliest start date (min) and the furthest end date (max) based on criteria in a source column. I have created several functions based on a solution I found on the internet. I have also tried an array formula solution without using VBA. Neither of the approaches have worked. I have found similar questions/answers on SO but none that correctly apply to my situation.
In my example below I have a Task worksheet and an Export worksheet. The Export worksheet is the source data. In the Task worksheet I am trying to enter a formula that finds the minimum start date. Each Task ID can have several dates so I am trying to find the lowest and highest start dates for each of the tasks. I originally tried using an array formula but ran into the same problem which is that sometimes the formula produces the correct answer and sometimes it gives an incorrect answer and I cannot locate the source of the issue. Any help is much appreciated!
VBA Functions:
Function getmaxvalue(Maximum_range As Range)
Dim i As Double
For Each cell In Maximum_range
If cell.Value > i Then
i = cell.Value
End If
Next
getmaxvalue = i
End Function
Function getminvalue(Minimum_range As Range)
Dim i As Double
i = getmaxvalue(Minimum_range)
For Each cell In Minimum_range
If cell.Value < i Then
i = cell.Value
End If
Next
getminvalue = i
End Function
Function GetMinIf(SearchRange As Range, SearchValue As String, MinRange As Range)
Dim Position As Double
Position = 1
Dim getminvalue As Double
getminvalue = MinRange.Rows(1).Value
For Each cell In SearchRange
If LCase(SearchValue) = LCase(cell.Value) And MinRange.Rows(Position).Value < getminvalue Then
getminvalue = MinRange.Rows(Position).Value
End If
Position = Position + 1
Next
GetMinIf = getminvalue
End Function
Function GetMaxIf(SearchRange As Range, SearchValue As String, MaxRange As Range)
Dim Position As Double
Position = 1
Dim getmaxvalue As Double
For Each cell In SearchRange
If LCase(SearchValue) = LCase(cell.Value) And MaxRange.Rows(Position).Value > getmaxvalue Then
getmaxvalue = MaxRange.Rows(Position).Value
End If
Position = Position + 1
Next
GetMaxIf = getmaxvalue
End Function
The issue is that you are trying to equate positions incorrectly. Use this for the MinIf, it no longer needs the secondary function:
Function GetMinIf(SearchRange As Range, SearchValue As String, MinRange As Range)
Dim srArr As Variant
srArr = Intersect(SearchRange.Parent.UsedRange, SearchRange).Value
Dim mrArray As Variant
mrarr = Intersect(MinRange.Parent.UsedRange, MinRange).Value
Dim minTemp As Double
minTemp = 9999999999#
Dim i As Long
For i = 1 To UBound(srArr, 1)
If LCase(SearchValue) = LCase(srArr(i, 1)) And mrarr(i, 1) < minTemp Then
minTemp = mrarr(i, 1)
End If
Next i
GetMinIf = minTemp
End Function
Max:
Function GetMaxIf(SearchRange As Range, SearchValue As String, MaxRange As Range)
Dim srArr As Variant
srArr = Intersect(SearchRange.Parent.UsedRange, SearchRange).Value
Dim mrArray As Variant
mrarr = Intersect(MaxRange.Parent.UsedRange, MaxRange).Value
Dim maxTemp As Double
maxTemp = 0
Dim i As Long
For i = 1 To UBound(srArr, 1)
If LCase(SearchValue) = LCase(srArr(i, 1)) And mrarr(i, 1) > maxTemp Then
maxTemp = mrarr(i, 1)
End If
Next i
GetMaxIf = maxTemp
End Function
As far as formula go IF you have OFFICE 365 then use MINIFS
=MINIFS(Export!F:F,Export!A:A,A2)
=MAXIFS(Export!G:G,Export!A:A,A2)
If not use AGGREGATE:
=AGGREGATE(15,7,Export!$F$2:F$26/(Export!$A$2:A$26=A2),1)
=AGGREGATE(14,7,Export!$G$2:G$26/(Export!$A$2:A$26=A2),1)
I was trying to use Scott's method as part of a macro to transform an invoice. However, the rows of the invoice fluctuate every month and could grow to as many as a million in the future. Anyway, the bottomline is that I had to write the formula in a way where I could make the last row dynamic, which made the macro go from taking 10-15 minutes (by hardcoding a static last row like 1048576 to run to ~ 1 minute to run. I reference this thread to get the idea for the MINIFS workaround and another thread to figure out how to do a dynamic last row. Make vba excel function dynamic with the reference cells
I'm sure there are other methods, perhaps using offset, etc. but I tried other methods and this one was pretty quick. Anyone can use this VBA formula if they do the following:
15 to 14 to do a maxifs, keep as is for minifs
change the relevant rows and columns in Cells(rows, columns) format below.
The True/False parameters passed to .Address() will lock/unlock the rows/columns respectively (i.e. add a $ in front if True).
Change the last row
First, get the last row
Dim LastRow As Long
LastRow = Cells(Rows.Count, "A").End(xlUp).Row
Second, here is the dynamic minifs
Range("F2").Formula = "=AGGREGATE(15,7," & Range(Cells(2, 6), Cells(LastRow, 6)).Address(True, True) & "/(" & Range(Cells(2, 1), Cells(LastRow, 1)).Address(True, True) & "=" & Range(Cells(2, 1), Cells(2, 1)).Address(False, True) & "),1)"
Third, autofill down.
Range("F2").AutoFill Destination:=Range("F2:F" & LastRow)