B4J Question How to identify null values [solved]

Didier9

Well-Known Member
Licensed User
Longtime User
Trying to find an elegant solution to this common problem.
I use a map to save and retrieve program settings.
It works fairly well in most cases but a difficulty arises when the map returns a null setting.
B4X:
Dim b as Boolean
b = mapSettings.Get(i)
If mapSettings.Get(i) returns null, Java cannot convert a null into a boolean and crashes.
There is no function (that I am aware of) to test if a variable is boolean (like there is IsNumber() for instance)
so the ugly solution is to put this in a Try/Catch block.
B4X:
Dim b as Boolean
Try
  b = mapSettings.Get(i)
Catch
  b = False
End Try
I personally do not find that very elegant, one reason being the large red dump into the log when running the program in debug mode. Try/Catch is very convenient for many potentially unforeseen circumstances but I should be able to determine if a variable is of type Boolean or not without Try/Catch.
Open to any suggestion/best practice.
Thank you
 

Didier9

Well-Known Member
Licensed User
Longtime User
OOOps...
Typo as usual. I should have used mapSettings.GetValueAt(i)

But the question remains, is there a way to check a variable for null?
 
Last edited:
Upvote 0

OliverA

Expert
Licensed User
Longtime User
You could check for Null before assigning the value
B4X:
Dim b as Boolean = False ' Default value. Not changed if we encounter a Null object
If mapSettings.GetValueAt(i) <> Null Then b = mapSettings.GetValueAt(i)
 
Upvote 0

William Lancee

Well-Known Member
Licensed User
Longtime User
I always use the .containsKey method, it makes the code more transparent. If you want to use indexed values you should use B4XOrderedMaps.
Note: GetValueAt is deprecated. And as you know, to iterate through a map easily, use: For Each kw as String In mapSettings.Keys

B4X:
    Dim result As Boolean = False
    Dim s As String = "some key"
    If mapSettings.ContainsKey(s) Then result = mapSettings.Get(s)
    Log(result)

    mapSettings.Put(s, True)
    If mapSettings.ContainsKey(s) Then result = mapSettings.Get(s)
    Log(result)
 
Last edited:
Upvote 0

TILogistic

Expert
Licensed User
Longtime User
help topic;

very useful verification method.
and the conditions of the consultations can be extended.

B4X:
Dim x As Int = 4
Log(IIF( x = 2, "It is an even number", "x is not 2"))

Dim a As Boolean
Log(IIF(a = Null, "IsNull", "NotNull"))

Public Sub IIF(InputQuestion As Boolean, OutputTrue As Object, OutputFalse As Object) As Object
    If InputQuestion Then Return OutputTrue Else Return OutputFalse
End Sub
 
Upvote 0

Didier9

Well-Known Member
Licensed User
Longtime User
I always use the .containsKey method, it makes the code more transparent. If you want to use indexed values you should use B4XOrderedMaps.
Note: GetValueAt is deprecated. And as you know, to iterate through a map easily, use: For Each kw as String In mapSettings.Keys

B4X:
    Dim result As Boolean = False
    Dim s As String = "some key"
    If mapSettings.ContainsKey(s) Then result = mapSettings.Get(s)
    Log(result)

    mapSettings.Put(s, True)
    If mapSettings.ContainsKey(s) Then result = mapSettings.Get(s)
    Log(result)
In my case, as a result of a previous programming error, the key was defined but had a Null value, so ContainsKey() would not have flagged it.

Testing for Null as OliverA suggested would work, regardless of circumstances anyways, so I will combine both methods :)

I like Erel's reference to GetDefault, which I have not yet used but it still crashes if the value is not in the proper format. My aim was to come up with a simple method able to deal with an incorrect setting in the map file. Any piece of code that relies on external inputs (keyboard or files) must be able to deal with garbage.

For instance, I saved a string variable "EthernetPort=9000", then manually edited the map file and replaced the line with "EthernetPort="
When the program tried to read the parameter with this:
B4X:
Dim EthernetPort as String
EthernetPort = mapSettings.GetDefault( "EthernetPort", 5000 )
It crashed because Null is not a valid string:
B4X:
Error occurred on line: 6372
java.lang.NumberFormatException: empty String
    at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1842)
    at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)
    at java.lang.Double.parseDouble(Double.java:538)
...
GetDefault works if there is no parameter currently defined with that name, not if the parameter is corrupted. It would be nice if GetDefault could simply return the default if the comparison failed for any reason.
 
Upvote 0

William Lancee

Well-Known Member
Licensed User
Longtime User
I am curious. This works. The error message you got doesn't mention Null, it just says that you tried to convert an empty string to a number.

B4X:
    mapSettings.Put("EthernetPort", Null)
    Log(mapSettings.Get("EthernetPort"))        'log: null
    
    Dim EthernetPort As String
    EthernetPort = mapSettings.GetDefault("EthernetPort", 5000 )
    Log(EthernetPort)                            'log: 5000

B4X:
    mapSettings.Put("EthernetPort", "")
    Log("=" & mapSettings.Get("EthernetPort"))        'log: =
    
    Dim EthernetPortNum As Int
    EthernetPortNum = mapSettings.GetDefault("EthernetPort", 5000 )
    Log("=" & EthernetPortNum)                            'log: Error message
 
Upvote 0

Didier9

Well-Known Member
Licensed User
Longtime User
I was not able to reproduce the original problem where the map file returned a Null for a key that was present in the file but had no value.
That map file was created by accident as the program had crashed for a different reason. As I was troubleshooting the original problem, I came across the issue of the program crashing when reading that particular key and down I went into the rabbit hole... and the original file was overwritten.
Anyhow, that prompted me to collect the good recommendations from above and I have devised this function that seems to do the job (always return a value of the same type as the default, returning the default if the value cannot be retrieved from the file for whatever reason) and is easier to read
B4X:
Sub GetSettings
    Dim mapSettings as Map
    mapSettings = File.ReadMap( File.DirData( AppName ), AppName & ".cfg" )

    BaudRate = GetMapValue( mapSettings, "BaudRate", 115200 )
    COMPort = GetMapValue( mapSettings, "COMPort", "COM1" )
    DebugMode = GetMapValue( mapSettings, "DebugMode", False )
    [...]
End Sub

Sub GetMapValue( map As Map, key As String, default As Object ) As Object
    Dim s As String, i As Int, b As Boolean, f As Float, l As Long, obj As Object
    Try
        If default Is String Then
            s = map.GetDefault( key, default )
            obj = s
        Else If default Is Int Then
            i = map.GetDefault( key, default )
            obj = i
        Else If default Is Boolean Then
            b = map.GetDefault( key, default )
            obj = b
        Else If default Is Float Then
            f = map.GetDefault( key, default )
            obj = f
        Else If default Is Long Then
            l = map.GetDefault( key, default )
            obj = l
        End If
    Catch
        obj = default
    End Try
    Return obj
End Sub ' GetMapValue()
(even though the Try-Catch will still cause the log to be flooded by red ink on occasion):
 
Upvote 0

William Lancee

Well-Known Member
Licensed User
Longtime User
In principle your routine works, but as you say the Try construct is not a 'nice' way of doing it.

Also, it doesn't account for strings that are empty, i.e. ComPort = ""

I think the following is slightly better, but still doesn't prevent ComPort = "%^as ?" being returned as a corrupted string value

Anyway I'm starting to sound pedantic, I'll stop now. I think you have it well under control.

B4X:
    Dim mapSettings As Map = CreateMap("BaudRate": "fast", "COMPort": "non-existing", "DebugMode": "corrupted")
    Dim BaudRate As Long = GetSetting(mapSettings, "BaudRate", 115200)
    Dim COMPort As String = GetSetting(mapSettings, "COMPort", "COM1")
    Dim DebugMode As Boolean = GetSetting(mapSettings, "DebugMode", False)
    Log(BaudRate & TAB & COMPort & TAB & DebugMode)

B4X:
Sub GetSetting(map As Map, key As String, default As Object) As Object
    Dim result As Object = map.GetDefault(key, default)
    Dim s As String = result
    If s.Length = 0 Then Return default
    If default Is String Then Return s
    If default Is Boolean Then
        If s <> "true" And s <> "false" Then Return default Else Return result
    Else
        If IsNumber(s) = False Then Return default Else Return result
    End If
End Sub
 
Upvote 0

Didier9

Well-Known Member
Licensed User
Longtime User
The empty string issue is something that can be handled higher up in the app, as long as it does not cause the app to crash. My main objective, as a general purpose interface to the map file was to have something that does not crash, is easy to use and will give the user a chance to identify and fix the problem.

As a low level interface to the map file, I think that will do the job. There is only one Try statement (even though it is called multiple times) and it is easy to add new variables and pretty clear how the code works.
 
Upvote 0
Top