I'm assuming that in vba the type Boolean is just a 16 bit integer. So why and how does Debug.Print differentiate between the two?
Debug.Print -1 = True
Debug.Print -1
Debug.Print True
Will output:
True
-1
True
Line 1 evaluates to (if -1 equals true) then print true else print false
Line 2 evaluates to print -1 (as you asked)
Line 3 evaluates to print true (as you asked)
-1 is true and 0 is false in VBA
You could also assume that a string is an array of bytes, or that a 16-bit integer is a series of 0's and 1's.
The answer is because VBA as a language provides an abstraction for Boolean values.
That's all there is to it: it knows and defines that True and False are Boolean literals; their respective underlying values are nothing but plumbing to make it work: False maps to 0, and any non-zero value converts to True, with -1 being returned for Boolean-to-numeric conversions.
IOW, this isn't about Debug.Print, it's about VBA's type system.
Boolean variables are stored as 16-bit (2-byte) numbers, but they can only be True or False.
When other numeric types are converted to Boolean values, 0 becomes
False and all other values become True.
When Boolean values are converted to other data types, False becomes 0 and True becomes -1.
On your first Debug.Print you evaluate an expression (-1 = True) which is True.
The second prints an Integer.
The third prints a Boolean.
Boolean Data Type
Related
This question already has answers here:
Weird behaviour of NOT
(3 answers)
Closed 3 years ago.
This has me utterly baffled.
Sub testChangeBoolean()
Dim X As Boolean ' default value is False
X = True ' X is now True
X = Not X ' X is back to False
End Sub
But I'm trying to toggle the .FitText property in a table cell (in Word). .FitText starts as True.
If I assign it to X:
Sub testChangeBoolean()
Dim X As Boolean ' again, default is False
X = Selection.Cells(1).FitText ' does set X to True
X = Not X ' X is still True!
End Sub
I just don't understand what I'm doing wrong.
I believe the explanation has to do with how older programming languages (WordBasic and early VBA) stored the integer values of True and False. In those days, True = -1 and False = 0.
Newer programming languages still use 0 for False, but 1 for True.
The majority of Word's Boolean type properties continue to use -1 for True (Font.Bold, for example), which has been cause for confusion and frustration for programmers working with the Interop in newer languages. So, at some point, some developers at Microsoft decided to use the new way and assigned the integer value of 1 to True for some new functionality. Such as FitText.
Considering the following code sample, where X is of type Boolean and y of type Integer:
If FitText is True, the integer value is 1
If reversing the values, using Not shows that the Boolean remains "True" because its integer value is not 0, it's -2
Setting the integer value directly to True gives -1
This is confusing, indeed, but does explain why Not is not giving the expected result.
Sub testChangeBoolean()
Dim X As Boolean ' again, default is False
Dim Y As Integer
X = Selection.Cells(1).FitText ' does set X to True
Y = Selection.Cells(1).FitText
Debug.Print X, Y ' result: True 1
X = Not X ' X is still True!
Y = Not Y
Debug.Print X, Y ' result: True -2
X = False
Y = True
Debug.Print X, Y ' result: False -1
End Sub
To add on to Cindy's excellent answer, I want to point out that while VBA normally has safeguards to coerce the values when assigning to a Boolean data type, this can be circumvented. Basically, if you write a random value to a memory address that's not yours, then you should expected undefined behavior.
To help demonstrate this, we'll (ab)use LSet which essentially allow us to copy the value without actually assigning.
Private Type t1
b As Boolean
End Type
Private Type t2
i As Integer
End Type
Private Sub Demo()
Dim i1 As t2
Dim b1 As t1
Dim b As Boolean
i1.i = 1
LSet b1 = i1
b = b1.b
Debug.Print b, b1.b, i1.i
Debug.Print CInt(b), CInt(b1.b), i1.i
End Sub
Note the line b = b1.b is basically equivalent to what we did in the OP code
X = Selection.Cells(1).FitText
That is, assigning a Boolean to another Boolean. However, because I wrote to the b1.b using LSet, bypassing VBA runtime checks, it doesn't get coerced. When reading the Boolean, VBA does implicitly coerce it into either True or False, which seems misleading but is correct because any falsy results is one that equals 0 (aka False), and any truthy results is one that doesn't. Note that the negative for truthy means that both 1 and -1 are truthy.
Had I assigned the 1 to a Boolean variable directly, VBA would have had coerced it into -1/True and thus there'd be no problem. But evidently with FitText or LSet, we are basically writing to the memory address in an uncontrolled fashion, so that VBA start to behave strangely with this particular variable since it expects the Boolean variable to already had its contents coerced but wasn't.
It's because of the internal Long value coming from this property, as explained by Cindy Meister. We should always use CInt to avoid this.
Sub testChangeBoolean2()
Dim X As Boolean ' again, default is False
X = CInt(Selection.Cells(1).FitText) ' [Fixed] does set X to True
X = Not X ' X is False!
End Sub
I'm trying to write a VBA code with if the i get an error.
I want the code to check if one of the values ("S925,S936,S926,G") is not on cell 10.
Sub checklist()
Dim x
Dim LineType
NumRows = Cells(Rows.Count, "j").End(xlUp).Row
For x = 2 To NumRows
If LineType = "G" Then
If Not InStr("S925,S936,S926,G", cellsCells(x, 10).Value) Then
cells Cells(x, 52).Interior.Color = rgbCrimson
cells Cells(x, 52).Value = "G"
End If
End If
End If
Next x
End Sub
This won't cause an error but it will cause issues with your program so I'll explain it.
InStr doesn't return a Boolean but the index of the first occurrence of the search string. If the string isn't found it returns 0.
For example InStr("12345", "23") will return 2.
Because everything except 0 is cast as True, something like If Instr(....) Then will perform as expected.
However if you use If Not InStr(....) Then something else can/will happen
If Not InStr("12345", "23") Then
Debug.Print "test evaluated as True!"
End If
this prints test evaluated as True! even though "23" is contained in "12345". This is not because InStr returned False though. We can replace the InStr expression with 2 to better understand:
Debug.Print 2 '2 (duh)
Debug.Print CBool(2) 'True (2 converted to Boolean)
Debug.Print Not 2 '-3
Debug.Print CBool(Not 2) 'True (-2 converted to Boolean)
Wy is Not 2 evaluated as -3? That's because the 2 isn't converted to Boolean before Not is applied but the Not is applied bit-wise to the 2, which means every bit is flipped. So 2 (0010) becomes 1101 which is -3 because the computer uses the two's complement to express negative numbers. (Actually more bits are used for Integer but it works the same.) As -3 is not 0, it will be converted to True. Since Not 0 will also be evaluated as True (0000 will be converted to 1111 which is -1 as two's complement) the expression Not InStr(...) will always be evaluated as True.
This bit-wise behavior isn't noticed when working with Booleans because they are represented as 0000 and 1111 internally. This also becomes apparent like this:
Debug.Print 1 = True 'False
Debug.Print CBool(1) = True 'True
Debug.Print -1 = True 'True
Debug.Print CBool(-1) = True'True
Debug.Print CInt(True) '-1 (True converted to Integer)
As you can see here, the True is converted to an Integer rather than the Integer being converted to a Boolean for the = comparison.
Long explanation, short fix: Use If InStr(...) > 0 Then instead of If InStr(...) Then and If InStr(...) = 0 Then instead of If Not InStr(...) Then.
PS: This can also cause confusing behavior if you combine two InStr tests with And because And will be applied bitwise as well.
how can i check if a string only contains 4 digit numbers ( or a year )
i tried this
Dim rgx As New Regex("^/d{4}")
Dim number As String = "0000"
Console.WriteLine(rgx.IsMatch(number)) // true
number = "000a"
Console.WriteLine(rgx.IsMatch(number)) // false
number = "000"
Console.WriteLine(rgx.IsMatch(number)) //false
number = "00000"
Console.WriteLine(rgx.IsMatch(number)) // true <<< :(
this returns false when its less than 4 or at characters but not at more than 4
thanks!
I actually wouldn't use a regex for this. The expression is deceptively simple (^\d{4}$), until you realize that you also need to evaluate that numeric value to determine a valid year range... unless you want years like 0013 or 9015. You're most likely going to want the value as an integer in the end, anyway. Given that, the best validation is probably just to actually try to convert it to an integer right off the bat:
Dim numbers() As String = {"0000", "000a", "000", "00000"}
For Each number As String In numbers
Dim n As Integer
If Integer.TryParse(number, n) AndAlso number.Length = 4 Then
'It's a number. Now look at other criteria
End If
Next
Use LINQ to check if All characters IsDigit:
Dim result As Boolean = ((Not number Is Nothing) AndAlso ((number.Length = 4) AndAlso number.All(Function(c) Char.IsDigit(c))))
You should use the .NET string manipulation functions.
Firstly the requirements, the string must:
Contain exactly four characters, no more, no less;
Must consist of a numeric value
However your aim is to validate a Date:
Function isKnownGoodDate(ByVal input As String) As Boolean 'Define the function and its return value.
Try 'Try..Catch statement (error handling). Means an input with a space (for example ` ` won't cause a crash)
If (IsNumeric(input)) Then 'Checks if the input is a number
If (input.Length = 4) Then
Dim MyDate As String = "#01/01/" + input + "#"
If (IsDate(MyDate)) Then
Return True
End If
End If
End If
Catch
Return False
End Try
End Function
You may experience a warning:
Function isKnownGoodDate does not return a value on all code
paths. Are you missing a Return statement?
this can be safely ignored.
Using a VB6 app on Windows 7 both lines return TRUE, because somehow the decimal separator is not considered:
IsNumeric("123.45")
IsNumeric("123,45")
On Windows 8 or Windows 2012, the same code returns TRUE or FALSE depending on the regional settings. Considering the comma as decimal separator defined in regional settings, then:
IsNumeric("123.45") returns FALSE
IsNumeric("123,45") returns TRUE
Is there any way to restore the "old" behaviour without recompiling the app?
This is not a new issue with which version of Windows you are using. It's always been based on the locale settings of the machine.
What I did for my application is I made my own functions:
Public Function IsNumber(ByRef Expression As Variant) As Boolean
Select Case VarType(Expression)
Case vbInteger, vbLong, vbSingle, vbDouble, vbCurrency, vbDate, vbBoolean, vbDecimal, vbByte
IsNumber = True
Case vbString
Dim Negative As Boolean
Dim Number As Boolean
Dim Period As Boolean
Dim Positive As Boolean
Dim X As Long
For X = 1& To Len(Expression)
Select Case Mid$(Expression, X, 1&)
Case "0" To "9"
Number = True
Case "-"
If Period Or Number Or Negative Or Positive Then Exit Function
Negative = True
Case "."
If Period Or Exponent Then Exit Function
Period = True
Case "E", "e"
If Not Number Then Exit Function
If Exponent Then Exit Function
Exponent = True
Number = False
Negative = False
Period = False
Case "+"
If Not Exponent Then Exit Function
If Number Or Negative Or Positive Then Exit Function
Positive = True
Case vbSpace, vbTab, vbVerticalTab, vbCr, vbLf, vbFormFeed
If Period Or Number Or Exponent Or Negative Then Exit Function
Case Else
Exit Function
End Select
Next X
IsNumber = Number
End Select
End Function
If you're dealing with strings explicitly, then you could simplify that function. And also you likely don't want to deal with exponents, so this might be more suited:
Public Function IsNumber(ByRef Expression As String) As Boolean
Dim Negative As Boolean
Dim Number As Boolean
Dim Period As Boolean
Dim X As Long
For X = 1& To Len(Expression)
Select Case Mid$(Expression, X, 1&)
Case "0" To "9"
Number = True
Case "-"
If Period Or Number Or Negative Then Exit Function
Negative = True
Case "."
If Period Then Exit Function
Period = True
Case vbSpace, vbTab, vbVerticalTab, vbCr, vbLf, vbFormFeed
If Period Or Number Or Negative Then Exit Function
Case Else
Exit Function
End Select
Next X
IsNumber = Number
End Function
When you are in need of converting the number, use Str() instead of CLng()/CInt()/CDbl()/CSng()/Val(). Str() treats periods as decimals regardless of locale, just like my IsNumber() function above.
How come IsDate("13.50") returns True but IsDate("12.25.2010") returns False?
I got tripped up by this little "feature" recently and wanted to raise awareness of some of the issues surrounding the IsDate function in VB and VBA.
The Simple Case
As you'd expect, IsDate returns True when passed a Date data type and False for all other data types except Strings. For Strings, IsDate returns True or False based on the contents of the string:
IsDate(CDate("1/1/1980")) --> True
IsDate(#12/31/2000#) --> True
IsDate(12/24) --> False '12/24 evaluates to a Double: 0.5'
IsDate("Foo") --> False
IsDate("12/24") --> True
IsDateTime?
IsDate should be more precisely named IsDateTime because it returns True for strings formatted as times:
IsDate("10:55 AM") --> True
IsDate("23:30") --> True 'CDate("23:30") --> 11:30:00 PM'
IsDate("1:30:59") --> True 'CDate("1:30:59") --> 1:30:59 AM'
IsDate("13:55 AM") --> True 'CDate("13:55 AM")--> 1:55:00 PM'
IsDate("13:55 PM") --> True 'CDate("13:55 PM")--> 1:55:00 PM'
Note from the last two examples above that IsDate is not a perfect validator of times.
The Gotcha!
Not only does IsDate accept times, it accepts times in many formats. One of which uses a period (.) as a separator. This leads to some confusion, because the period can be used as a time separator but not a date separator:
IsDate("13.50") --> True 'CDate("13.50") --> 1:50:00 PM'
IsDate("12.25") --> True 'CDate("12.25") --> 12:25:00 PM'
IsDate("12.25.10") --> True 'CDate("12.25.10") --> 12:25:10 PM'
IsDate("12.25.2010")--> False '2010 > 59 (number of seconds in a minute - 1)'
IsDate("24.12") --> False '24 > 23 (number of hours in a day - 1)'
IsDate("0.12") --> True 'CDate("0.12") --> 12:12:00 AM
This can be a problem if you are parsing a string and operating on it based on its apparent type. For example:
Function Bar(Var As Variant)
If IsDate(Var) Then
Bar = "This is a date"
ElseIf IsNumeric(Var) Then
Bar = "This is numeric"
Else
Bar = "This is something else"
End If
End Function
?Bar("12.75") --> This is numeric
?Bar("12.50") --> This is a date
The Workarounds
If you are testing a variant for its underlying data type, you should use TypeName(Var) = "Date" rather than IsDate(Var):
TypeName(#12/25/2010#) --> Date
TypeName("12/25/2010") --> String
Function Bar(Var As Variant)
Select Case TypeName(Var)
Case "Date"
Bar = "This is a date type"
Case "Long", "Double", "Single", "Integer", "Currency", "Decimal", "Byte"
Bar = "This is a numeric type"
Case "String"
Bar = "This is a string type"
Case "Boolean"
Bar = "This is a boolean type"
Case Else
Bar = "This is some other type"
End Select
End Function
?Bar("12.25") --> This is a string type
?Bar(#12/25#) --> This is a date type
?Bar(12.25) --> This is a numeric type
If, however, you are dealing with strings that may be dates or numbers (eg, parsing a text file), you should check if it's a number before checking to see if it's a date:
Function Bar(Var As Variant)
If IsNumeric(Var) Then
Bar = "This is numeric"
ElseIf IsDate(Var) Then
Bar = "This is a date"
Else
Bar = "This is something else"
End If
End Function
?Bar("12.75") --> This is numeric
?Bar("12.50") --> This is numeric
?Bar("12:50") --> This is a date
Even if all you care about is whether it is a date, you should probably make sure it's not a number:
Function Bar(Var As Variant)
If IsDate(Var) And Not IsNumeric(Var) Then
Bar = "This is a date"
Else
Bar = "This is something else"
End If
End Function
?Bar("12:50") --> This is a date
?Bar("12.50") --> This is something else
Peculiarities of CDate
As #Deanna pointed out in the comments below, the behavior of CDate() is unreliable as well. Its results vary based on whether it is passed a string or a number:
?CDate(0.5) --> 12:00:00 PM
?CDate("0.5") --> 12:05:00 AM
Trailing and leading zeroes are significant if a number is passed as a string:
?CDate(".5") --> 12:00:00 PM
?CDate("0.5") --> 12:05:00 AM
?CDate("0.50") --> 12:50:00 AM
?CDate("0.500") --> 12:00:00 PM
The behavior also changes as the decimal part of a string approaches the 60-minute mark:
?CDate("0.59") --> 12:59:00 AM
?CDate("0.60") --> 2:24:00 PM
The bottom line is that if you need to convert strings to date/time you need to be aware of what format you expect them to be in and then re-format them appropriately before relying on CDate() to convert them.
Late to the game here (mwolfe02 answered this a year ago!) but the issue is still real, there are alternative approaches worth investigating, and StackOverflow is the place to find them: so here's my own answer...
I got tripped up by VBA.IsDate() on this very issue a few years ago, and coded up an extended function to cover cases that VBA.IsDate() handles badly. The worst one is that floats and integers return FALSE from IsDate, even though date serials are frequently passed as Doubles (for DateTime) and Long Integers (for dates).
A point to note: your implementation might not require the ability to check array variants. If not, feel free to strip out the code in the indented block that follows Else ' Comment this out if you don't need to check array variants. However, you should be aware that some third-party systems (including realtime market data clients) return their data in arrays, even single data points.
More information is in the code comments.
Here's the Code:
Public Function IsDateEx(TestDate As Variant, Optional LimitPastDays As Long = 7305, Optional LimitFutureDays As Long = 7305, Optional FirstColumnOnly As Boolean = False) As Boolean
'Attribute IsDateEx.VB_Description = "Returns TRUE if TestDate is a date, and is within ± 20 years of the system date.
'Attribute IsDateEx.VB_ProcData.VB_Invoke_Func = "w\n9"
Application.Volatile False
On Error Resume Next
' Returns TRUE if TestDate is a date, and is within ± 20 years of the system date.
' This extends VBA.IsDate(), which returns FALSE for floating-point numbers and integers
' even though the VBA Serial Date is a Double. IsDateEx() returns TRUE for variants that
' can be parsed into string dates, and numeric values with equivalent date serials. All
' values must still be ±20 years from SysDate. Note: locale and language settings affect
' the validity of day- and month names; and partial date strings (eg: '01 January') will
' be parsed with the missing components filled-in with system defaults.
' Optional parameters LimitPastDays/LimitFutureDays vary the default ± 20 years boundary
' Note that an array variant is an acceptable input parameter: IsDateEx will return TRUE
' if all the values in the array are valid dates: set FirstColumnOnly:=TRUE if you only
' need to check the leftmost column of a 2-dimensional array.
' * THIS CODE IS IN THE PUBLIC DOMAIN
' *
' * Author: Nigel Heffernan, May 2005
' * http://excellerando.blogspot.com/
' *
' *
' * *********************************
Dim i As Long
Dim j As Long
Dim k As Long
Dim jStart As Long
Dim jEnd As Long
Dim dateFirst As Date
Dim dateLast As Date
Dim varDate As Variant
dateFirst = VBA.Date - LimitPastDays
dateLast = VBA.Date + LimitFutureDays
IsDateEx = False
If TypeOf TestDate Is Excel.Range Then
TestDate = TestDate.Value2
End If
If VarType(TestDate) < vbArray Then
If IsDate(TestDate) Or IsNumeric(TestDate) Then
If (dateLast > TestDate) And (TestDate > dateFirst) Then
IsDateEx = True
End If
End If
Else ' Comment this out if you don't need to check array variants
k = ArrayDimensions(TestDate)
Select Case k
Case 1
IsDateEx = True
For i = LBound(TestDate) To UBound(TestDate)
If IsDate(TestDate(i)) Or IsNumeric(TestDate(i)) Then
If Not ((dateLast > CVDate(TestDate(i))) And (CVDate(TestDate(i)) > dateFirst)) Then
IsDateEx = False
Exit For
End If
Else
IsDateEx = False
Exit For
End If
Next i
Case 2
IsDateEx = True
jStart = LBound(TestDate, 2)
If FirstColumnOnly Then
jEnd = LBound(TestDate, 2)
Else
jEnd = UBound(TestDate, 2)
End If
For i = LBound(TestDate, 1) To UBound(TestDate, 1)
For j = jStart To jEnd
If IsDate(TestDate(i, j)) Or IsNumeric(TestDate(i, j)) Then
If Not ((dateLast > CVDate(TestDate(i, j))) And (CVDate(TestDate(i, j)) > dateFirst)) Then
IsDateEx = False
Exit For
End If
Else
IsDateEx = False
Exit For
End If
Next j
Next i
Case Is > 2
' Warning: For... Each enumerations are SLOW
For Each varDate In TestDate
If IsDate(varDate) Or IsNumeric(varDate) Then
If Not ((dateLast > CVDate(varDate)) And (CVDate(varDate) > dateFirst)) Then
IsDateEx = False
Exit For
End If
Else
IsDateEx = False
Exit For
End If
Next varDate
End Select
End If
End Function
A Tip for people still using Excel 2003:
If you (or your users) are going to call IsDateEx() from a worksheet, put these two lines in, immediately below the function header, using a text editor in an exported .bas file and reimporting the file, because VB Attributes are useful, but they are not accessible to the code editor in Excel's VBA IDE:
Attribute IsDateEx.VB_Description = "Returns TRUE if TestDate is a date, and is within ± 20 years of the system date.\r\nChange the defaulte default ± 20 years boundaries by setting values for LimitPastDays and LimitFutureDays\r\nIf you are checking an array of dates, ALL the values will be tested: set FirstColumnOnly TRUE to check the leftmost column only."
That's all one line: watch out for line-breaks inserted by the browser! ...And this line, which puts isDateEX into the function Wizard in the 'Information' category, alongside ISNUMBER(), ISERR(), ISTEXT() and so on:
Attribute IsDateEx.VB_ProcData.VB_Invoke_Func = "w\n9"
Use "w\n2" if you prefer to see it under the Date & Time functions: beats hell outta losing it in the morass of 'Used Defined' functions from your own code, and all those third-party add-ins developed by people who don't do quite enough to help occasional users.
I have no idea whether this still works in Office 2010.
Also, you might need the source for ArrayDimensions:
This API declaration is required in the module header:
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory" _
(Destination As Any, _
Source As Any, _
ByVal Length As Long)
…And here's the function itself:
Private Function ArrayDimensions(arr As Variant) As Integer
'-----------------------------------------------------------------
' will return:
' -1 if not an array
' 0 if an un-dimmed array
' 1 or more indicating the number of dimensions of a dimmed array
'-----------------------------------------------------------------
' Retrieved from Chris Rae's VBA Code Archive - http://chrisrae.com/vba
' Code written by Chris Rae, 25/5/00
' Originally published by R. B. Smissaert.
' Additional credits to Bob Phillips, Rick Rothstein, and Thomas Eyde on VB2TheMax
Dim ptr As Long
Dim vType As Integer
Const VT_BYREF = &H4000&
'get the real VarType of the argument
'this is similar to VarType(), but returns also the VT_BYREF bit
CopyMemory vType, arr, 2
'exit if not an array
If (vType And vbArray) = 0 Then
ArrayDimensions = -1
Exit Function
End If
'get the address of the SAFEARRAY descriptor
'this is stored in the second half of the
'Variant parameter that has received the array
CopyMemory ptr, ByVal VarPtr(arr) + 8, 4
'see whether the routine was passed a Variant
'that contains an array, rather than directly an array
'in the former case ptr already points to the SA structure.
'Thanks to Monte Hansen for this fix
If (vType And VT_BYREF) Then
' ptr is a pointer to a pointer
CopyMemory ptr, ByVal ptr, 4
End If
'get the address of the SAFEARRAY structure
'this is stored in the descriptor
'get the first word of the SAFEARRAY structure
'which holds the number of dimensions
'...but first check that saAddr is non-zero, otherwise
'this routine bombs when the array is uninitialized
If ptr Then
CopyMemory ArrayDimensions, ByVal ptr, 2
End If
End Function
Please keep the acknowledgements in your source code: as you progress in your career as a developer, you will come to appreciate your own contributions being acknowledged.
Also: I would advise you to keep that declaration private. If you must make it a public Sub in another module, insert the Option Private Module statement in the module header. You really don't want your users calling any function with CopyMemoryoperations and pointer arithmetic.