Type mismatch when comparing two strings - excel

I have the following code that is comparing a combobox on a userform(GUI) to a populated cell on sheet2 of my workbook and I am getting a "type mismatch" error. This was all working until another sub shifted some data into the cells being compared on sheet 2.
My issue lies with if Worksheets(sheet2).cells(1,i).value = LCase(GUI.superCB.Value) then
Worksheets(sheet2).cells(1,i).value Now shows up in the watch as a Variant/Integer which made me think that when the data was shifted it changed the "style" of that cell.
Private Sub NextButton_Click() ''' adds check boxes to frame
Dim i As Integer
'Dim superColm As Integer
For i = 5 To 12
If Worksheets(Sheet2).Cells(1, i).Value = LCase(GUI.superCB.Value) Then 'problem line is right here
superColm = i
Exit For
Else
End If
Next i
NextButton.Visible = False
superCB.Visible = False
Run.Visible = True
Frame1.Visible = True
Dim chk As Control
Dim idx As Integer
Dim lastrow As Integer
lastrow = Worksheets(Sheet2).Cells(Rows.Count, superColm).End(xlUp).Row
For idx = 1 To lastrow - 1
Set chk = GUI.Frame1.Controls.add("Forms.CheckBox.1", idx, True)
'set chk = gui.Frame1.Controls.Add(
chk.Visible = True
chk.Left = 5
chk.Top = (idx - 1) * (chk.Height + 2)
chk.Caption = Cells(idx + 1, superColm) & " " & idx
Next
With Me.Frame1
.ScrollBars = fmScrollBarsVertical
If lastrow <= 10 Then
.ScrollHeight = .InsideHeight * 1.5
ElseIf lastrow <= 15 Then
.ScrollHeight = .InsideHeight * 2.25
ElseIf lastrow <= 20 Then
.ScrollHeight = .InsideHeight * 3
ElseIf lastrow <= 25 Then
.ScrollHeight = .InsideHeight * 3.9
ElseIf lastrow <= 30 Then
.ScrollHeight = .InsideHeight * 4.75
ElseIf lastrow <= 35 Then
.ScrollHeight = .InsideHeight * 5.35
Else
.ScrollHeight = .InsideHeight * 6.25
End If
.ScrollWidth = .InsideWidth * 9
End With
End Sub
If I have sheet 2 as the active sheet Cells(1,i).value will work however, I need to have sheet 2 hidden from the user in the end. With this working it makes me think that the cell style is not the issue.
I have tried going to Excel.Workbooks("Shawn_sch_v1.2.xlsm").worksheets(sheet2).cells(1,i).value and everything down to the base cells() hoping it was missing a sheet reference but nothing has helped.

A String can safely be compared against any other data type in VBA... except Error.
Comparing a Variant/Error against anything will throw a type mismatch error.
This code is implicitly accessing whatever ActiveSheet is:
chk.Caption = Cells(idx + 1, superColm) & " " & idx
Cells should be qualified with the specific Worksheet object you mean to work with. If the active sheet contains a value that can't be coerced into a String (e.g. #VALUE! or #REF!), that will throw a type mismatch error.
Worksheets(Sheet2).Cells(1, i).Value = ...
Here Sheet2 is an identifier. The Worksheets indexer wants either an integer value, or a string. If Sheet2 is the code name of a worksheet in ThisWorkbook, you don't need to dereference it from Worksheets - just use it:
Sheet2.Cells(1, i).Value = ...
The Worksheet class doesn't have a default property, so Debug.Print Worksheets(Sheet2) throws error 438 object doesn't support this property or method - and a subsequent member call like .Cells(1, i), also throws a type mismatch error. If you don't have a Sheet2 string variable holding a worksheet name, I suspect that's the bug you're having right now... which means everything above is just what's waiting to bite you :)
If Sheet2 is a string variable that contains a valid sheet name, you can use the IsError function to verify whether a Variant is a Variant/Error:
If Not IsError(Sheet2.Cells(1, i).Value) Then
' value is safe to compare against a string
Else
' comparing the cell value to anything will throw error 13
End If
Lastly, I would advise against using Rows as a global variable, since it's already a global-scope identifier ([_Global].Rows, implicitly referring to ActiveSheet). Now, renaming that variable with Find/Replace is going to be pretty hard to do without breaking your code: Rubberduck's "rename" refactoring could probably help with doing that safely (disclaimer: I manage that OSS VBIDE add-in project).

This will be fixed with doing a check on the range object data type first.
Worksheets(Sheet2).Cells(1, i).Value is the range object. This can change data types every time the range is modified depending on how it is modified.
LCase(GUI.superCB.Value) This appears to be a form control. If the range is an integer, they cannot compare.
Try something like this:
Dim i As Integer
Dim iRange as String
'Dim superColm As Integer
`This is untested
For i = 5 To 12
iRange = Worksheets(sheet2).Cells(1, i).Text
If iRange = LCase(GUI.superCB.Value) Then 'problem line is right here
superColm = i
Exit For
Else
End If
Next i
The idea is to first be sure that the data types are the same.
You may need to use .Text or .Value2 instead of .Value for the range. If it is possible that the range object will be Empty or Nothing , then you also need to check for those too.
Edit: Changed .Value to .Text
Edit2: This answer is incorrect.

Related

Using an IF statement with a string using .value

Does the .value command return strings and if not how do you have an IF statement check if the cell(s) holds the correct string.
I am working on a budget sheet where a certain column sets which department the budget and its breakdown should be forwarded to.
Sub calcMonthly()
Dim ws As Worksheet
Dim wssum As Worksheet
'set worksheets to copy values
Set ws = Sheets("Sheet 1")
Set wssum = Sheets("Sheet 2")
Dim i As Integer
Dim j As Integer
Dim k As Integer
Dim bumonth As Currency
Dim busum As Currency
'sort through Departments for % breakdown
For k = 0 To 18
'sort through months
For i = 0 To 11
busum = 0
'sort through each item
For j = 0 To 350
bumonth = 0
bumonth = CCur(ws.Cells(3 + j, 37 + k).Value * ws.Cells(3 + j, 24 + i).Value)
busum = busum + bumonth
Next j
'row C holds the string which details if the item if physical hardware or digital then uploads it to the cell
If ws.Cells(3 + j, 3) = "SW" Then
wssum.Cells(3 + k, 2 + i).Value = busum
Else
wssum.Cells(3 + k, 14 + i).Value = busum
End If
Next i
Next k
End Sub
.Value does return a string, but it doesn't look like you included it in the IF statement.
If ws.Cells(3 + j, 3).value = "SW" Then
Range.Value returns a Variant whose subtype depends on the content of the cell.
Given a #N/A, #VALUE, #REF!, or any other cell error value, it returns a Variant/Error that can't be coerced into anything other than a Variant - trying to compare it to a string or numeric value or expression will throw error 13 "type mismatch".
You can avoid this runtime error by evaluating whether the variant subtype is Error using the IsError function, ideally by capturing the cell's value into a local Variant variable first, so you don't have to access the cell twice.
Given an empty cell that has no formula, no value, no content whatsoever, it returns a Variant/Empty; the IsEmpty function can be used to validate this variant subtype.
Given a Date value, it returns a Variant/Date. Given any numeric value, it returns a Variant/Double. Given a TRUE or FALSE value, it returns a Variant/Boolean.
And given a String value, it does return a Variant/String.
Note that the default member of the Range class is a hidden [_Default] property with two optional parameters:
When no parameters are provided, an explicit call to .Value is equivalent. Explicit member calls should generally be preferred over implicit default member calls, and whether implicit or explicit, the calls should be consistent:
If ws.Cells(3 + j, 3) = "SW" Then ' implicit: .Value
wssum.Cells(3 + k, 2 + i).Value = busum
Else
wssum.Cells(3 + k, 14 + i).Value = busum
End If

how to not enter if statement inside a loop if it have been executed

I have a for loop, and inside it i have if statement.
In my Excel I have a list that contains each value one time. Once I found it i don't want the code to even check the conditional, i want it to skip this part of the if statement completely each time the loop is executed, is it possible?
Here is my code and list:
the first iteration of the loop will find that "c" is the value so it will do what inside it (xc = i)
I don't want the code to even check "ElseIf Cells(1, i) = "c" again, like the following image, is this possible?
code as text:
Sub test()
Dim i, xa, xb, xc As Integer
For i = 1 To 5
If Cells(i, 1) = "a" Then
xa = i
ElseIf Cells(i, 1) = "b" Then
xb = i
ElseIf Cells(i, 1) = "c" Then
xc = i
End If
Next i
End Sub
My initial interpretation of your need was "if the code hits 'c' again, just don't act".
To do so, you could modify the logic as follows:
ElseIf (xc = 0) And (Cells(i, 1) = "c") Then
This way, as soon as xc is set, the first boolean expression would be False, and the overall condition would not ever be met again. As mentioned by #TimWilliams, VBA would still evaluate the second boolean expression, unlike other languages that feature short-circuiting options. #Gene's answer describes a way around this. Typically, for better performance, you would evaluate the simple conditions first, before resorting to costly ones.
Additional notes
In VBA, you must give a type to each variable. In your Dim line, only xc is an Integer, while the other variables are Variants.
An unqualified Cells() call operates on the currently active worksheet, which might not be the expected one. Suggestion: qualify Cells() with the CodeName of your worksheet. The CodeName is what you see or specify under a worksheet's (Name) property as seen from the Visual Basic editor. For example, if (Name) is Sheet1, use Sheet1.Cells(). This will only work if the code resides in the same workbook as Sheet1. If the code is behind the worksheet itself, you can even use Me.Cells().
When dealing with cell values as your code does, VBA is (silently) being nice and understands that, among the numerous properties of the Range class, Value is what you are interested in. It is better practice, however, to explicitly state the target property, such as in Sheet1.Cells(i, j).Value.
EDIT
Knowing the values will be distinct and that there are about 60 of them, I suggest you simply use a Dictionary, as shown below, to get each value's row in one go, without a cascade of Ifs:
Option Explicit
Sub test()
Dim i As Integer
Dim dict As Object 'Scripting.Dictionary
Set dict = CreateObject("Scripting.Dictionary")
For i = 1 To 5
dict(Cells(i, 1).Value) = i
Next
Debug.Print dict("a") '4
Debug.Print dict("b") '2
Debug.Print dict("c") '1
'Etc.
End Sub
if i understood your question you can try this code:
Sub test()
Dim i, xa, xb, xc As Integer
Dim a, b, c As Boolean
a = False
b = False
c = False
For i = 1 To 5
If Cells(i, 1) = "a" And a <> True Then
xa = i
a = True
ElseIf Cells(i, 1) = "b" And b <> True Then
xb = i
b = True
ElseIf Cells(i, 1) = "c" And c <> True Then
xc = 1
c = True
End If
Next i
End Sub
Boolean variable is setted true for example only when the cells(i,1)="a" and after the next "a" value are skipped...
hope this helps
I just wanted to "mod" Ferdinando's code so it's a bit more "readable", I think. The main (the substantive) difference between this version and Ferdinando's or Excelosaurus' is that the cell is not even tested once the value is detected. Remember that the question was: I don't want the code to even check "ElseIf Cells(1, i) = "c" again... So, this version does exactly that.
Sub test()
Dim i As Integer, xa As Integer, xb As Integer, xc As Integer
Dim aFound As Boolean, bFound As Boolean, cFound As Boolean
Dim r As Range
For i = 1 To 5
Set r = Cells(i, 1)
If Not aFound Then
If r = "a" Then xa = i: aFound = True
ElseIf Not bFound Then
If r = "b" Then xb = i: bFound = True
ElseIf Not cFound Then
If r = "c" Then xc = i: cFound = True
End If
Next i
End Sub
I don't like the idea of 60 ElseIfs. Please examine the code below. In order to test it, create a worksheet called "TestSheet" and enter your A1:A5 to cells H2:H6.
Sub TestSpike()
' 06 Jan 2019
Dim Rng As Range
Dim Items As Variant
Dim Spike As String
Dim Tmp As String
Dim i As Integer
Dim R As Long
Items = Split("c|b|0|a|1", "|")
With Worksheets("TestSheet").Columns("H")
For R = 2 To 6
Tmp = CStr(.Cells(R).Value)
If InStr(1, Spike, Tmp, vbTextCompare) = 0 Then
Spike = Spike & "|" & Tmp
On Error Resume Next
i = Application.WorksheetFunction.Match(Tmp, Items, 0)
If Err Then
MsgBox Tmp & " wasn't found in Array"
Else
MsgBox "i = " & i & " = Item " & Tmp
End If
End If
Next R
End With
End Sub
The code has a "Spike". Each item is first checked against the Spike. If it is found there no further tests are carried out. Else, it is added to the Spike.
New items, after being added to the Spike, are checked against the Array "Items" which would hold your 60 elements, separated by Chr(124) thus, Split("c|b|0|a|1", "|"). I use the worksheet function MATCH to look for the item in the array. The result is an index number (or an error, if not found). You can use this index number in a Select Case statement to process each item distinct from others, basically the same way as you now process it when the If statement returns True.
One idea you may find useful with this kind of setup is to use the index from the Match function to return a value from another array. The other array might, for example, contain function names and you use Application.Run to call a different function for each item. This would run significantly faster than examining 60-odd Select Case statements.

Dynamic range based formula results' change every time I switch sheet Excel VBA

I wrote a function that supposed to get a specific part of a specific Column, and then, by comparing each entry of the column to the value of the cell that is left to it, count the times a specific condition is met.
It all works alright, except one problem.. if I use the function on "Sheet1", get a result and then switch to "Sheet2" and use the function on this sheet it changes the result on "Sheet1" for some reason.
Function countStable(rangeObj As Range) 'rangeObj that being passed is a namedRange(Synamic Range)
Application.Volatile
ActiveSheet.Select
Dim entry, preEntryVal, entryVal As Variant
Dim counters(1 To 5, 1 To 1) As Integer
Dim cStable, cIncreased, cDecreased, cAdded, cLost
cStable = 0
cIncreased = 0
cDecreased = 0
cAdded = 0
cLost = 0
Set rangeObj = Intersect(rangeObj, rangeObj.Parent.UsedRange)
For Each entry In rangeObj
If Not IsEmpty(entry.Value) And Not IsEmpty(ActiveSheet.Range("A" & entry.Row)) Then
entryVal = entry.Value
preEntryVal = ActiveSheet.Cells(entry.Row, entry.Column - 1).Value
If entryVal = preEntryVal Then
cStable = cStable + 1
ElseIf InStr(entryVal, "-") And Not (InStr(preEntryVal, "-")) Then
cLost = cLost + 1
ElseIf Not InStr(entryVal, "-") And InStr(preEntryVal, "-") Then
cAdded = cAdded + 1
ElseIf preEntryVal < entryVal Then
cDecreased = cDecreased + 1
ElseIf preEntryVal > entryVal Then
cIncreased = cIncreased + 1
End If
End If
counters(1, 1) = cStable
counters(2, 1) = cIncreased
counters(3, 1) = cDecreased
counters(4, 1) = cAdded
counters(5, 1) = cLost
Next
countStable = counters
End Function
As commented inside the code, rangeObj that is being passed as parameter was defined in the name manager and it is based on an Offset formula.
I know it changes the values on both sheets because of the dynamic range, but not sure why.. I don't want it to be changed.
Help please?
In several places, the code references the ActiveSheet. Wherever the function appears, it will reflect the value of whatever sheet is active. You'll want to use the parent of the supplied range object instead.
Dim currentSheet as Worksheet
Set currentSheet = rangeObj.Parent
Then, search and replace ActiveSheet with currentSheet in the method.

cells().value = ... application or object defined error

I want to create a summary table in a new sheet, and at the moment I'm just doing it very crudely. I will try a more elegant solution in the future.
Anyway, this is the code I have so far:
Sub createsummarytable()
Worksheets.Add().Name = "datasummary"
With Worksheets("datasummary")
Dim i As Long
Dim Startpoint As Long
Startpoint = -5
For i = 1 To 40
.Cells(Startpoint + (5 * i), 1).Value = "Block" & "i"
Next i
End With
End Sub
I am getting the error in the title on line: .Cells(Startpoint + (5 * i), 1).Value = "Block" & "i"
If anyone wants to make the code more elegant in addition to solving the error, that would be appreciated.
Off-by-one. There is no column/row 0 in Excel; -5 + (5 * 1) evaluates to 0:
.Cells(0, 1).Value = 42 'same error
You need to adjust by +1:
For i = 1 To 40
.Cells(Startpoint + (5 * i) + 1, 1).Value = "Block" & i
Next i
If your code works as intended, describe your working code in a new question on Code Review.
#Mat's Mug picked up on the off-by-one error, and #kpg987 got your use of "i" as a string-literal instead of a variable, but your code can be improved.
Here are some of the changes I made:
Scope the procedure to Private or Public
Use meaningful names
Use Constants to determine the starting row and number of each rows for each block. StartingPoint = -5 isn't useful, whereas START_ROW = 1 is quite clear about what it is and where it will actually start.
Adjust the formula to use the constants.
Refer to the target workbook explicitly, otherwise the active workbook will be used.
Set a reference to the added worksheet when using the add command.
Use the strongly typed reference with the With command, because With Worksheets("datasummary") will be late bound)
Use camelCasing for variable names
Result:
Private Sub createSummaryTable()
Const WORKSHEET_NAME As String = "datasummary"
Const START_ROW As Long = 1
Const BLOCK_COLUMN as Long = 1
Const BLOCK_SIZE As Long = 5
Const BLOCK_COUNT As Long = 40
Const BLOCK_PREFIX As String = "Block"
Dim dataSummary As Worksheet
Set dataSummary = ThisWorkbook.Worksheets.Add
With dataSummary
.Name = WORKSHEET_NAME
Dim blockCounter As Long
For blockCounter = 1 To BLOCK_COUNT
.Cells(START_ROW + (BLOCK_SIZE * (blockCounter - 1)), BLOCK_COLUMN).Value = BLOCK_PREFIX & blockCounter
Next blockCounter
End With
End Sub
a "no-loop" approach:
Private Sub createSummaryTable2()
Dim dataSummary As Worksheet
With ThisWorkbook.Worksheets.Add
.Name = "datasummary"
With .Range("A1").Resize((40 - 1) * 5 + 1)
.FormulaR1C1 = "=IF(MOD(ROW(),5)=1,""Block"" & int(ROW()/5)+1,"""")"
.Value = .Value
End With
End With
End Sub

Vba - Rows Property

I am a novice of Vba.
I have been litteraly fighting all day with this bit of code:
Sub ComandsCompactVisualization()
Dim x, i As Integer
Dim CellToAnalyse As Range
x = 2
i = 0
For i = 0 To 5 Step 1
Set CellToAnalyse = Worksheets("Comandi").Cells(x + i, 2)
If Not CellToAnalyse.Font.ColorIndex = 2 Then
Worksheets("Comandi").Rows("x+i:2").Hidden = True
End If
Next i
End Sub
I am trying to hide all the rows that in cell (x+i,2) have not got red text.
I am almost there but... Rows does not seem to accept as content Rows("x+i:2").
I obtain Runtime error 13 "Type mismatch".
If I substitute its content with Rows("2:2") row 2 is deleted but I am not any more able to hide all the other rows that do not have red text in column 2.
Ideas?
Anything between quotes "like this" is just a string. To perform arithmetic on x you need to do this first, then concatenate it to the other part of the string. Like this:
.Rows((x + i) & ":2")
BTW Isn't red 3..?
Sub ComandsCompactVisualization()
Dim x as long, i As Long 'You must declare ALL variables (x was as variant in your code)
Dim CellToAnalyse As Range
dim WS as Worksheet
'x = 2 'if x is always the same value, no need to calculate it each loop
'i = 0 'numbers are initialized to be 0, strings to be "", boolean to be false.
set WS=Sheets("Commandi")
For i = 0 To 5 ' Step 1
Set CellToAnalyse = WS.Cells(2 + i, 2)
If CellToAnalyse.Font.ColorIndex <> 2 Then
CellToAnalyse.entirerow.hidden=true
' WS.Rows(2+i).entirerow.hidden = true is the same result
End If
Next i
End Sub

Resources