2

I'm new to VBA. So i was trying to append prefixes to a set of values, depending on the first number of each value. However i get a Type mismatch error on line 4. I suppose it is because I am trying to use Left() on a variable, and it is supposed to be used on a string (or something of that sorts?) How do i accomplish that?

Thank you in advance

Sub test()
a = UsedRange.Rows.Count
Testvariable = Range("A1", "A" & a)
FirstNo = Left(Testvariable, 1)
For i= 1 To a
If FirstNo(i,1) = "1" Then
Cells(i,2) = "abc" & FirstNo(i,1)
Else Cells(i,2) = "def" & FirstNo(i,1)
End if
Next
End Sub
kct360
  • 21
  • 2

4 Answers4

3

Trouble is you are trying to take the left of a Range object that happens to give you an array when assigned without the set keyword, when left wants a string.

This post here explains how to convert a range to a string, that you can then pass to left. How can I convert a range to a string (VBA)?

Sub test()
    dim a as long
    a = UsedRange.Rows.Count
    dim Testvariable as string
    Testvariable = RangeToString(Range("A1", "A" & a))
    dim FirstNo as string
    FirstNo = Left(Testvariable, 1)
    dim i as long
    For i= 1 To a
        If FirstNo(i,1) = "1" Then
            Cells(i,2) = "abc" & FirstNo(i,1)
        Else 
            Cells(i,2) = "def" & FirstNo(i,1)
        End if
    Next
End Sub

Function RangeToString(ByVal myRange as Range) as String
    RangeToString = ""
    If Not myRange Is Nothing Then
        Dim myCell as Range
        For Each myCell in myRange
            RangeToString = RangeToString & "," & myCell.Value
        Next myCell
        'Remove extra comma
        RangeToString = Right(RangeToString, Len(RangeToString) - 1)
    End If
End Function

Also, be sure to always declare your variables correctly.

a = UsedRange.Rows.Count means create a variable called a of type variant and give it the value of UsedRange.Rows.Count

Testvariable = Range("A1", "A" & a) means create a variable called Testvariable of type variant and give it the .value of the range object (an array)

If these were declared properly

    dim a as long
    a = UsedRange.Rows.Count
    dim Testvariable as string
    Testvariable = Range("A1", "A" & a)

The assignment of Testvariable would have failed with a much more obvious error, informing you that you can't convert an array to a string.

Trevor
  • 2,792
  • 1
  • 30
  • 43
  • 1
    It is not an object actually but an array, because it was not assigned using the `Set` keyword. – A.S.H Jun 28 '17 at 12:34
  • 1
    Appears to return object of type Range. https://msdn.microsoft.com/VBA/Excel-VBA/articles/range-object-excel But either way, it's not a string. – Trevor Jun 28 '17 at 12:37
  • 2
    The `Range` function returns a Range object, But when you assign it to the variable `Testvariable` without the `Set` keyword, an implicit call to `.Value` is made and the result is an array. Objects cannot be assigned without the `Set` keyword. Yes it's not a string, the answer is valid but only apart of this subtlety :) – A.S.H Jun 28 '17 at 12:40
  • 1
    I added that to an edit since it's worthwhile information. It's never good to rely on VBA implicits, which is why I changed the variables to be properly declared. – Trevor Jun 28 '17 at 12:47
  • 1
    VBA `Integer` is a 16-bit integer type, that will overflow at 32,768; better use a `Long` (32-bit integer), especially for row counts. – Mathieu Guindon Jun 28 '17 at 13:25
2
Testvariable = Range("A1", "A" & a)

Testvariable is an array (unless a=1, which is not your case here)

Left is flexible enough to accept numbers in addition to strings, but NOT arrays.

A.S.H
  • 29,101
  • 5
  • 23
  • 50
1

If I follow your flow, I think this will get you what you are looking for.

Sub test()
    Dim ws As Worksheet
    Set ws = ActiveWorkbook.ActiveSheet
    a = ws.UsedRange.Rows.Count
    TestVariable = Range("A1", "A" & a)

    For i = 1 To a
        FirstNo = Left(TestVariable(i, 1), 1)

        If FirstNo = "1" Then
            Cells(i, 2) = "abc" & FirstNo(i, 1)
        Else
            Cells(i, 2) = "def" & FirstNo
        End If
    Next i
End Sub

I think the issue was, in addition to the other two answers, was that you need to loop through the cells and output something based on each cells value. Therefore, just throw your code just above your for loop inside and change as needed.

interesting-name-here
  • 1,851
  • 1
  • 20
  • 33
1

VBA does a lot of things behind your back, to "make things easier". The problem is that these things come back and bite you later.

Undeclared variables, for example. Without Option Explicit specified at the top of every module, VBA will happily compile code that uses variables that aren't declared. Sounds useful? It would be, if the types were inferred. But instead VBA declares an on-the-fly Variant, that holds whatever you put into it (and changes its type to accomodate whatever you're assigning).

a = UsedRange.Rows.Count

Here a is an implicit Variant/Long. Declare it as a Long integer:

Dim a As Long

Even better, give it a meaningful name:

Dim usedRows As Long
usedRows = UsedRange.Rows.Count

Now the fun part:

Testvariable = Range("A1", "A" & a)

Here Testvariable is an implicit Variant/Array. How so? The code that VBA sees looks something like this:

Testvariable = Range("A1", "A" & a).Value

And because a wouldn't be 1, that .Value is referring to more than one single cell - Excel's object model gives you an array that contains the values of all cells in the specified range, and that's what Testvariable contains.

If you wanted Testvariable to refer to a range rather than just their values, you would declare a Range object variable, and assign it with the Set keyword:

Dim testVariable As Range
Set testVariable = ActiveSheet.Range("A1:A" & a)

Notice the explicit ActiveSheet here: without it the code does exactly the same thing, so why put it in? Because you want to be as explicit as possible: unqualified Range, Cells, Rows, Columns and Names calls all implicitly refer to the active worksheet - and that's yet another source of bugs (just google up "range error 1004", you'll find hundreds of Stack Overflow questions about it).

If you meant testVariable to contain the values of every cell in that specified range, then you would declare and assign testVariable like this:

Dim testVariable As Variant
testVariable = ActiveSheet.Range("A1:A" & a).Value

And now you have an array that contains all values in range "A1:A" & a of the active worksheet: that's what your code does.. implicitly.

FirstNo = Left(Testvariable, 1)

So now we want the "first number", and we're reading it off a variant array.

The Left (or Left$) function is from the VBA.Strings module, and means to works with strings; it can work with other types too (with implicit type conversions), but if you give it an object reference or an array, it won't know how to convert it for you, and VBA will raise a run-time error.

The testVariable variant array contains Variant values: most cells will contain a Double floating-point value. Others will contain a String. Some cells can contain an error value (e.g. #N/A, #VALUE!, or #REF!) - and VBA will not be able to implicitly convert such error values either.

So before you read any cell's value, you need to make sure you can read it; use the IsError function for that:

If IsError(testVariable(1, 1)) Then
    Exit Sub ' cell contains an error; cannot process
If Not IsNumeric(testVariable(1, 1)) Then
    Exit Sub ' cell doesn't contain a number; cannot process
End If

' now that we KNOW our value is a numeric value...
Dim firstNumber As Double
firstNumber = testVariable(1, 1)

Notice that (1, 1)? That's because testVariable is a 1-based 2D array (without Option Base 1 implicitly sized arrays are always 0-based, except when you're getting one from a Range), so to read the value in the first row / first column, you need to read the value at index (1, 1).

But that's not what you're trying to do.

So I was trying to append prefixes to a set of values, depending on the first number of each value.

"Each value" means you need to iterate the values, so you have a loop there.

Dim i As Long
For i = 1 To a
    '...
Next

It's not the "first number" we want, it's the firstDigit, of every cell we're looping over.

If FirstNo(i,1) = "1" Then

Here you've lost track of the types you're dealing with: FirstNo was assigned before the loop started, so its value will be constant at every iteration.

You mean to do this:

Dim values As Variant
values = ActiveSheet.Range("A1:A" & usedRows).Value

Dim i As Long
For i = 1 To usedRows
    If Not IsError(values(i)) Then

        Dim representation As String
        representation = CStr(values(i))

        Dim prefix As String
        If Left$(representation, 1) = "1" Then
            prefix = "abc"
        Else
            prefix = "def"
        End If

        ActiveSheet.Range("B" & i).Value = prefix & representation

    End If
Next

Now that everything is explicit and properly indented, it's much easier to see what's going on... and now I'd seriously question why you need VBA to do that:

[B1] = IFERROR(IF(LEFT(A1, 1) = "1", "abc" & A1, "def" & A1), "")

And drag the formula down.

Mathieu Guindon
  • 69,817
  • 8
  • 107
  • 235
  • Thanks for the very detailed explanation. What is the difference between `Testvariable` to refer to a range or values? Is it that for range, if usedRows = 5, `Testvariable` will be A1:A5 whereas for values, `Testvariable` will be the actual values? Tried both range and values for the purpose of this code, either range or values work the same. – kct360 Jun 28 '17 at 15:27
  • If it's a `Range`, then you're looking at an *object* that has state and methods. If it's an array of values, well, that's all there is to it. If you need the values and nothing else, work with the array. If you need the object and its members, work with a `Range`. As with everything, #ItDepends. – Mathieu Guindon Jun 28 '17 at 15:29
  • Not exactly sure what that really means, maybe with more experience i can truly comprehend. I've done the formula in excel prior to attempting on VBA (Thought it will be a good way to try typing my own code by converting from excel formulas to VBA, but it looks like it is much harder/complicated than expected, as evident in this example.) – kct360 Jun 28 '17 at 15:41
  • @kct360 You accidentally fell into the trap of how badly VBA handles objects. So it's understandable you got confused. This link explains the available primitive types in VBA. http://www.informit.com/articles/article.aspx?p=339929&seqNum=2 Note that Range isn't one of them, so you can deduce Range is an object instead. It's more complicated than that but hopefully it helps you get started. The other confusing thing is arrays. Arrays are like groups of the same type. Groups of strings, groups of integers, groups of objects. It's a way for you to use 1 variable to store many values. – Trevor Jun 28 '17 at 15:59
  • @TrevorD exactly what's "bad" about how VBA "handles" objects? default members are a COM thing, VBA is COM. Blame COM, not VBA =) – Mathieu Guindon Jun 28 '17 at 16:04
  • How about the way you implement such a feature into your own classes involves notepad? https://bytes.com/topic/access/insights/694992-how-declare-default-property-class-module All of VBA objects screams "after thought". – Trevor Jun 28 '17 at 16:11
  • I actually don't recall them being editable in VB6. That would have been handy to know back in the day when VB6 was relevant. :-) – Trevor Jun 28 '17 at 16:57