I am new to VBA, facing the following problem:
I need to return certain value, returned by IF formula, based on numeric data, kept in another worksheet.
I have written something like this, however all the time when it comes to the point of running IF part, it gives me the error Type mismatch, and the problem seems to be in the values, found by vlookup. I was trying to declare it as long, variant and so on but that didn't help. However the MsgBox returnes the result from another sheet properly. Another sheet is formatted as numbers. Any ideas how to make it work?
here is the code i have for now:
Option Explicit
Sub find()
Dim lookup As String
Dim pkgWidth, pkgLength, pkgHeight, displaySize, AllHeaders, headerweight, itemweight, classify As Range
Dim lastrow As Variant
Dim cl As Range
Dim i As Integer
Dim widthh, lengthh, Heightt, display, Weight As Variant
'this part dynamically searches for the columns I need
Set AllHeaders = Worksheets("Sheet2").Range("1:1")
Set pkgWidth = AllHeaders.find("package_width")
Set pkgLength = AllHeaders.find("package_length")
Set pkgHeight = AllHeaders.find("package_height")
Set displaySize = AllHeaders.find("display_size")
Set headerweight = Worksheets("Sheet1").Range("1:1")
Set itemweight = headerweight.find("Item Weight")
Set classify = headerweight.find("AT")
lastrow = Cells(Rows.Count, 1).End(xlUp).Row
For i = 2 To lastrow
lookup = Worksheets("Sheet1").Cells(i, 1).Value
Set cl = Worksheets("Sheet1").Cells(i, classify.Column)
'here the values are being looked up from another sheet
widthh = Application.VLookup(lookup, _
Worksheets("Sheet2").Range("A1").CurrentRegion, pkgWidth.Column, False)
lengthh = Application.VLookup(lookup, _
Worksheets("Sheet2").Range("A1").CurrentRegion, pkgLength.Column, False)
Heightt = Application.VLookup(lookup, _
Worksheets("Sheet2").Range("A1").CurrentRegion, pkgHeight.Column, False)
display = Application.VLookup(lookup, _
Worksheets("Sheet2").Range("A1").CurrentRegion, displaySize.Column, False)
Weight = Application.VLookup(lookup, _
Worksheets("Sheet1").Range("A1").CurrentRegion, itemweight.Column, False)
If display > 6 Then
If Weight < 25 Then
cl.Value = 1.01
Else
cl.Value = 1.02
End If
Else
If widthh >= 1970 Or lengthh >= 1970 Or Heightt >= 1970 Then
If Weight <= 8 Then
cl.Value = 3.01
Else
If Weight >= 35 Then
cl.Value = 3.02
Else
cl.Value = 3.03
End If
End If
Else
If Weight <= 3 Then
cl.Value = 5.01
Else
If Weight >= 8 Then
cl.Value = 5.03
Else
cl.Value = 5.02
End If
End If
End If
End If
Next i
End Sub
When using Application.VLookup (or any of its variants) you must take into account that it can return #N/A, as explained in the documentation:
If lookup_value is smaller than the smallest value in the first column of table_array, VLOOKUP returns the #N/A error value.
If for instance display gets that value, then the expression display > 6 will give you the Type mismatch error.
So to prevent that, either change the logic of your code so that VLookup is guaranteed to not return #N/A (if this is possible in your case, I cannot say), or test for this error value, like this:
If IsError(display) Then
' Treat the error condition...
ElseIf display > Then
' ...etc.
The same precaution may be needed for other variables that get the result of a VLookup call.
Related
I'm trying to create a macro that uses Index/match functions to match and pull data from one sheet into another. I did it in Excel and it works perfect. However the reports are "dynamic" (the size changes) so I need the last row of my code to be dynamic as well.
The following is what I have done. I'm NOW getting a "type mismatch" error (I emphasize "now" since every time I find a solution for one error another pop's up).
Dim prosheet As Worksheet
Dim prosheet2 As Worksheet
Set prosheet2 = ThisWorkbook.Sheets("shipstation")
Set prosheet = ThisWorkbook.Sheets("macrotestfb")
lr1 = prosheet.Cells(Rows.Count, 1).End(xlUp).Row
lr2 = prosheet2.Cells(Rows.Count, 1).End(xlUp).Row
lrship = prosheet.Cells(Rows.Count, 10).End(xlUp).Row
lrindex = prosheet2.Cells(Rows.Column, 14).End(xlUp).Row
'CALCULATE SHIPPING COST
For x = prosheet.range("j6") To lrship
x = Application.WorksheetFunction.Index(prosheet2.range("a1:n" & lrindex), Application.WorksheetFunction.Match(prosheet.range("a6:a" & lr1), prosheet2.range("a1:a" & lr2), 0), prosheet2.range("f2"))
Next x
Match, in its non array form, only likes one value in the first criterion and not a range.
Also WorksheetFunction.Match will throw an error that will stop the code if a match is not found.
I like to pull the match into its own line and test for the error.
I also adjusted your For statement.
There is no detriment to searching an entire column so I got rid of a few of you last row searches as they are not needed.
Dim prosheet As Worksheet
Dim prosheet2 As Worksheet
Dim x As Long
Dim t As Long
Set prosheet2 = ThisWorkbook.Sheets("shipstation")
Set prosheet = ThisWorkbook.Sheets("macrotestfb")
lrship = prosheet.Cells(Rows.Count, 1).End(xlUp).Row
'CALCULATE SHIPPING COST
For x = 6 To lrship
t = 0
On Error Resume Next
t = Application.WorksheetFunction.Match(prosheet.Range("A" & x), prosheet2.Range("A:A"), 0)
On Error GoTo 0
If t > 0 Then
prosheet.Cells(x, "J").Value = prosheet2.Range("F"&t)
Else
prosheet.Cells(x, "J").Value = "Item does not Exist"
End If
Next x
Note:
Instead of an Index/Match combo which you might use on the worksheet, you can use Application.Match in VBA. Something like this:
Sub GetMatch
Dim indexRng As Range, matchRng as Range
Set indexRng = ThisWorkbook.Worksheets("Sheet1").Range("A1:A10")
Set matchRng = ThisWorkbook.Worksheets("Sheet1").Range("B1:B10")
debug.print indexRng.Cells(Application.Match("something",matchRng,0)).Value
End Sub
I have a spreadsheet that is a data-entry tool for pulling equipment tags and line numbers from engineering drawings – it’s set up with a table that takes either 3-segment tags (columns A-C), 5 segment line numbers (columns A-E), or a list of complete tags (column F), with column G either concatenating the tag segments or pulling across the complete tag. I had this set up using a formula, but I’d rather avoid using complicated formulas in anything that anyone else is going to use and so I took a stab at converting the formula to VBA and putting in a Worksheet_Change procedure.
The code works fine... until you make a change to a cell on the last row of the table and then hit enter or use the down arrow key, at which point Excel crashes. Moving sideways or upwards is fine, and so is moving sideways off the changed cell before hitting enter. I tried converting the table to a regular range, and it still crashes at the last row of the data. I tried turning Application.EnableEvents to False, and that stops the crashing, but then the updating no longer triggers properly.
If the procedure is changed to Worksheet_SelectionChange, it doesn’t crash.
Just to make it more interesting, in both the Worksheet_Change and Worksheet_SelectionChange procedures, using the up/down arrow keys or the enter key fails to trigger a change, but in the Worksheet_SelectionChange procedure arrowing back down/up to the row off which I just moved triggers the update.
I’m sure there are a million ways to fix this, but I have no idea how to do it, and I haven’t had any luck finding an answer.
What I want is for the code to update column G whenever the active cell changes – regardless of whether I use the enter key, tab key, arrow keys, or the $!## mouse to change my cell selection.
I'm working on a Windows 10 machine, using Excel 2016. When I get to work tomorrow I'll see how it goes on Excel 2013.
Spreadsheet screencap, for reference: https://drive.google.com/file/d/0B_wa8YmM1J2ddjlkOWxERE5TM1k/view?usp=sharing
Any assistance would be hugely appreciated - especially if it comes with a thorough explanation about what is going on here.
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
Dim strDelim As String
Dim strConcatTag As String
Dim intActiveRow As Integer
Dim rngTagSegment As Range
Dim rngSingleTag As Range
Dim rng3SegmentTag As Range
Dim rng5SegmentTag As Range
Dim rngTagEntry As Range
Dim rngConcatTag As Range
Dim rngCheck As Range
strDelim = "-"
intActiveRow = ActiveCell.Row
Set rngSingleTag = Cells(intActiveRow, 6)
Set rng3SegmentTag = Range(Cells(intActiveRow, 1), Cells(intActiveRow, 3))
Set rng5SegmentTag = Range(Cells(intActiveRow, 1), Cells(intActiveRow, 5))
Set rngTagEntry = Range(Cells(intActiveRow, 1), Cells(intActiveRow, 6))
Set rngConcatTag = Cells(intActiveRow, 7)
If intActiveRow = 1 Then
Exit Sub
Else
Select Case True
Case WorksheetFunction.CountA(rngTagEntry) = 0
rngConcatTag = ""
Case WorksheetFunction.CountA(rng5SegmentTag) > 0 And WorksheetFunction.CountA(rngSingleTag) > 0
rngConcatTag = "Enter either a complete tag or the individual sections, not both"
Case WorksheetFunction.CountA(rng5SegmentTag) = 0 And WorksheetFunction.CountA(rngSingleTag) <> 0
rngConcatTag = UCase(Trim(rngSingleTag))
Case WorksheetFunction.CountA(rng3SegmentTag) = 3 And WorksheetFunction.CountA(rng5SegmentTag) = 3
For Each rngTagSegment In rng5SegmentTag
strConcatTag = IIf(rngTagSegment = "", Trim(strConcatTag) & "", IIf(strConcatTag = "", _
Trim(rngTagSegment.Text), Trim(strConcatTag) & strDelim & Trim(rngTagSegment.Text)))
Next
rngConcatTag = UCase(Trim(strConcatTag))
Case WorksheetFunction.CountA(rng3SegmentTag) = 3 And WorksheetFunction.CountA(rng5SegmentTag) = 5
For Each rngTagSegment In rng5SegmentTag
strConcatTag = IIf(rngTagSegment = "", Trim(strConcatTag) & "", IIf(strConcatTag = "", _
Trim(rngTagSegment.Text), Trim(strConcatTag) & strDelim & Trim(rngTagSegment.Text)))
Next
rngConcatTag = UCase(strConcatTag)
Case Else
rngConcatTag = "Incomplete Tag"
End Select
End If
End Sub
Something like this should work:
Private Sub Worksheet_Change(ByVal Target As Range)
Dim rw As Range, r As Range, dataRange As Range
Dim rngSingleTag As Range
Dim rng3SegmentTag As Range
Dim rng5SegmentTag As Range
Dim rngTagEntry As Range
Dim rngConcatTag As Range
'data entry area only (adjust to suit)...
Set dataRange = Application.Intersect(Target, Me.Range("A2:F10000"))
If dataRange Is Nothing Then Exit Sub 'nothing to do...
'process each changed row
For Each r In dataRange.Rows
Set rw = r.EntireRow
Set rngSingleTag = rw.Cells(6)
Set rng3SegmentTag = rw.Cells(1).Resize(1, 3)
Set rng5SegmentTag = rw.Cells(1).Resize(1, 5)
Set rngTagEntry = rw.Cells(1).Resize(1, 6)
Set rngConcatTag = rw.Cells(7)
Select Case True
Case filled(rngTagEntry) = 0
rngConcatTag = ""
Case filled(rng5SegmentTag) > 0 And filled(rngSingleTag) = 1
rngConcatTag = "Enter either a complete tag or the individual sections, not both"
Case filled(rng5SegmentTag) = 0 And filled(rngSingleTag) = 1
rngConcatTag = UCase(Trim(rngSingleTag))
Case filled(rng3SegmentTag) = 3 And filled(rng5SegmentTag) = 3
rngConcatTag = Tag(rng3SegmentTag)
Case filled(rng5SegmentTag) = 5
rngConcatTag = Tag(rng5SegmentTag)
Case Else
rngConcatTag = "Incomplete Tag"
End Select
Next r
End Sub
Function filled(rng)
filled = Application.CountA(rng)
End Function
Function Tag(rng) As String
Const DELIM As String = "-"
Dim c As Range, rv As String
For Each c In rng.Cells
rv = rv & IIf(Len(rv) > 0, DELIM, "") & Trim(c.Text)
Next c
Tag = rv
End Function
I'm trying to get VBA to evaluate a formula as it goes over a loop. The portion that fails is the Evaluate() function itself, or at least the syntax I'm using.
Worksheets("Sheet2").Range("C2").Offset(All, 0) = _
Evaluate("((SUMPRODUCT(SUBTOTAL(2,OFFSET(PercentMet!$I$2,ROW(PercentMet!$I$2:$I$27301)-ROW(PercentMet!$H$2),0)),PercentMet!$I$2:$I$27301,PercentMet!$G$2:$G$27301)/SUMPRODUCT(SUBTOTAL(9,OFFSET(PercentMet!$G$2,ROW(PercentMet!$G$2:$G$27301)-ROW(PercentMet!$G$2),0)),--(PercentMet!$I$2:$I$27301<>""NA""))))")
The portion that fails is the ""NA"" at the end of the formula. Using this formula each cell equates to #VALUE!
If I remove the Evaluate portion the formula works as I want, but I need Evaluate because I'm looping through various filters and each value is unique.
Entire Code is Below:
Sub EthFilter()
Application.ScreenUpdating = False
Dim EthName As Range, GradeName As Range, Rate As Variant, Grade As Variant
Dim One As Integer, Zero As Integer, All As Integer
Set EthName = Worksheets("Sheet2").Range("J1")
Set GradeName = Worksheets("Sheet2").Range("K1")
One = 0
All = 0
For Each Raeth In Range("J1:J7")
Zero = 0
Rate = EthName.Offset(One, 0)
With Worksheets("PercentMet")
.AutoFilterMode = False
With .Range("$A$1:$O$27301")
.AutoFilter Field:=6, Criteria1:=Rate
For Each Grades In Range("B2:B9")
Grade = GradeName.Offset(Zero, 0).Value
With Worksheets("PercentMet")
With .Range("$A$1:$O$27301")
.AutoFilter Field:=5, Criteria1:=Grade
Worksheets("Sheet2").Range("C2").Offset(All, 0) = _
Evaluate("((SUMPRODUCT(SUBTOTAL(2,OFFSET(PercentMet!$I$2,ROW(PercentMet!$I$2:$I$27301)-ROW(PercentMet!$H$2),0)),PercentMet!$I$2:$I$27301,PercentMet!$G$2:$G$27301)/SUMPRODUCT(SUBTOTAL(9,OFFSET(PercentMet!$G$2,ROW(PercentMet!$G$2:$G$27301)-ROW(PercentMet!$G$2),0)),--(PercentMet!$I$2:$I$27301<>""NA""))))")
End With
End With
All = All + 1
Zero = Zero + 1
Next Grades
End With
End With
One = One + 1
Next Raeth
Application.ScreenUpdating = True
End Sub
If the length of the formula is a problem then instead of this (line breaks added for clarity):
Worksheets("Sheet2").Range("C2").Offset(All, 0) = Evaluate(
"((SUMPRODUCT(SUBTOTAL(2,OFFSET(PercentMet!$I$2,ROW(PercentMet!$I$2:$I$27301)-
ROW(PercentMet!$H$2),0)),PercentMet!$I$2:$I$27301,PercentMet!$G$2:$G$27301)/
SUMPRODUCT(SUBTOTAL(9,OFFSET(PercentMet!$G$2,ROW(PercentMet!$G$2:$G$27301)-
ROW(PercentMet!$G$2),0)),--(PercentMet!$I$2:$I$27301<>""NA""))))")
you can use this form:
Worksheets("Sheet2").Range("C2").Offset(All, 0) = Worksheets("PercentMet").Evaluate(
"((SUMPRODUCT(SUBTOTAL(2,OFFSET($I$2,ROW($I$2:$I$27301)-
ROW($H$2),0)),$I$2:$I$27301,$G$2:$G$27301)/
SUMPRODUCT(SUBTOTAL(9,OFFSET($G$2,ROW($G$2:$G$27301)-
ROW($G$2),0)),--($I$2:$I$27301<>""NA""))))")
Since all the inputs come from the same sheet you can use that sheet's Evaluate method and the formula will be evaluated in the context of that sheet.
The default Application.Evaluate version uses whichever sheet is Active at the time of execution.
I am fairly new to using VBA code and want to try implementing it in a hypothetical problem I am currently working on. I want to be able to compare 2 columns to see if they have similar numbers, and the first instance of a dissimilar number will become the "Next High." For example:
Client Market
90 87
92 91.25
95 92
95.5 93.5
95
95.5
Next High:
Starting from the bottom of the list, the VBA macro should see if the Market and Client side have the same numbers. The first instance of a number that is on the Market side and not on the client side should be the "Next High". For example, in the above list, 95.5 is on the Market and Client side, and so is 95. Because the next number above 95 on the Market side is 93.5, and this is a higher number than the next number on the Client side(92), this becomes the "Next High."
I would appreciaite some direction with the VBA code! I will eleborate if this is confusing, thank you!
See the attached code and review it. I hope that this will help you get started and allow you to ask any specific questions that you may have.
Code:
Option Explicit
' This sub assumes that the columns are in ascending order already
Sub nextHigh()
' declaring variables before using them
Dim lastRowClient As Long
Dim lastRowMarket As Long
Dim i As Integer
Dim j As Integer
Dim testValue As String
Dim nextHigh As Double
Dim checkValue As String
Dim valueFound As Boolean
' initializing variables to a known value
lastRowClient = 0
lastRowMarket = 0
i = 0
j = 0
testValue = ""
nextHigh = 0
checkValue = ""
valueFound = False
' gets the last row of column 1
With Sheets("Sheet1")
lastRowClient = .Range("A" & .Rows.Count).End(xlUp).Row
lastRowMarket = .Range("B" & .Rows.Count).End(xlUp).Row
End With
' for loop to iterate over column 2. note that you can specify the step value and change it from the default of +1
For i = lastRowMarket To 2 Step -1
testValue = Cells(i, 2).value
' this loop iterates to check the found value against column 1
For j = lastRowClient To 2 Step -1
checkValue = Cells(j, 1).value
' actual check
If testValue = checkValue Then
'value was found and therefore is not "NextHigh"
valueFound = True
Exit For ' value was found so stop searching
Else
'value was notfound and therefore is "NextHigh"
valueFound = False
End If
Next j
If Not valueFound Then
' print out value to next column
Cells(2, 3).value = testValue
Exit Sub 'exits sub to avoid excessive looping
End If
Next i
End Sub
Program output:
Do you absolutely have to use VBA for this? There's a fairly simple formula you could use instead. Using your provided example data, this formula returns the correct result:
=MAX(INDEX((COUNTIF(A2:B7,A2:B7)=1)*A2:B7,))
I'm looking to use conditional formatting to mark duplicates, but I want to look for duplicates in multiple columns and mark the entry regardless of which column the duplicate is located in. This is not natively supported in any way I've found, so I made my own UDF. Code listed at the bottom of this post.
The problem is that while the UDF works as expected, I cannot get the CF to work.
I have a CF where I'm formatting some cells based on the value in the same row but different column, and I've used the formula =INDIRECT("Z" & ROW()) <> 0 with great success to achieve this. Building on this, I tried the following for my UDF:
=findCandidatesForDuplicate(ADDRESS(ROW(); COLUMN(); 4))
=findCandidatesForDuplicate("B" & ROW())
But this code does not give any formatting - none whatsoever. The applied range is $B$2:$B$4000, since that's the only range I want it to mark. I've tried changing the UDF and the CF input between passing strings to passing ranges, but nothing I do will get it to apply the formatting.
I assume since formatting is not applied, that the UDF is not run on the intended selected cells, which again I assume is caused by not being able to make Excel understand how I want it to parse the CF input.
In the Worksheet, for row 14 which I know is a duplicate, I can input =findCandidatesForDuplicate("B14") and the cell will show TRUE.
Likewise, I can input =findCandidatesForDuplicate(ADDRESS(ROW(B14); COLUMN(B14);4)) and it will also show TRUE.
The question then boils down to this: how do I make the conditional formatting engine understand what I want to do (which is run this UDF with every cell in the range the rule is applied to as an argument and mark the corresponding cell in column B)?
Here's the function code. Basically a row is regarded as a duplicate if it finds duplicate entries in any of the columns listed.
Function findCandidatesForDuplicate(rngStr As String, Optional countOnly As Boolean, Optional dbg As Boolean) As Variant
Dim rng As Range
Dim colA As Range, searchString As String, result As Long
Dim ws As Worksheet, tbl As ListObject
Set ws = Application.ThisWorkbook.Worksheets(1)
Set tbl = ws.ListObjects("Tabell1")
Set rng = ws.Range(rngStr)
Set colA = Range("A" & rng.Row)
For i = 3 To 5
searchString = colA.Offset(0, i - 1).Value
If searchString = "" Then GoTo NextIteration
'Set rng = Range(rng.Address, tbl.ListColumns(i).DataBodyRange.Address) ' Only searches downwards from input range
Set rng = Range(tbl.ListColumns(i).DataBodyRange.Address) ' Searches the entire column
result = Application.WorksheetFunction.CountIf(rng, "=" & searchString)
If result > 1 Then
If dbg = True Then Debug.Print "Found result in loop no. " & i - 2 & ", matching on value " & searchString
Exit For
End If
NextIteration:
Next i
If countOnly = True And result > 1 Then
findCandidatesForDuplicate = result
ElseIf countOnly = True Then
findCandidatesForDuplicate = 0
ElseIf result > 1 Then
findCandidatesForDuplicate = True
Else
findCandidatesForDuplicate = False
End If
End Function
I haven't found why this doesn't work, nor how to make it work, so here's a workaround:
Populate some far-off column with the UDF calling its own row, then use =INDIRECT("AAA" & ROW()) = whateverValueSetByUDF as the formula to make CF show selected formatting on the cells that trigger the UDF.
It's unelegant and requires a column-wide, manual text-to-column update on the column containing the UDF results every time something changes (filtering the table will work fine, but if you change the sorting you have to update as described)... but it works as long as you know how to use the sheet.
Also, I revised the code in order to have more options for how to format the rows. With this style of output, you can - in addition to finding the rows which are possible duplicates - highlight which columns are triggering.
Usage: Use the INDIRECT formula as above, tailor to your specific setup. I recommend setting = or <> to one of the resultWhereString values.
Function findCandidatesForDuplicate(rng As Range, Optional countOnly As Boolean, Optional dbg As Boolean) As Variant
Dim colA As Range, searchString As String, result As Long, resultWhereString As String
Dim ws As Worksheet, tbl As ListObject
Set ws = Application.ThisWorkbook.Worksheets(1)
Set tbl = ws.ListObjects("Table1")
Set colA = Range("A" & rng.Row)
For i = 3 To 5
searchString = colA.Offset(0, i - 1).Value
' --> You can add more criteria here
If searchString = "" Or searchString = "myEmail#domain.com" Then GoTo NextIteration
Set rng = Range(tbl.ListColumns(i).DataBodyRange.Address) ' Searches the entire column
result = Application.WorksheetFunction.CountIf(rng, "=" & searchString)
If result > 1 Then
If dbg = True Then Debug.Print "Found result in loop no. " & i - 2 & ", matching on value " & searchString
Select Case i
' --> Update this loop if the range of i changes
Case 3
resultWhereString = "ResultCol1"
Case 4
resultWhereString = "ResultCol2"
Case 5
resultWhereString = "ResultCol3"
End Select
Exit For
End If
NextIteration:
Next i
If countOnly = True And result > 1 Then
findCandidatesForDuplicate = result
ElseIf countOnly = True Then
findCandidatesForDuplicate = 0
ElseIf result > 1 Then
findCandidatesForDuplicate = resultWhereString
Else
findCandidatesForDuplicate = ""
End If
End Function