Friday 16 February 2018

VBA - ScriptControl - Predicates in JScript part 1

Summary: Predicates are functions which return booleans and are useful for filtering. Here we show how housing a predicate in a global singleton variable makes it available to a filtering function.

Continuing our JScript series, we give how to pass a Javascript delegate to another Javascript function to effect filtering.

After inventing lambda expressions and delegates for VBA using JScript we look to the next cool feature, predicates. Filtering logic technologies (such as LINQ) often allow thew user to supply a predicate, which is a function that returns true or false. A classic example is using a predicate to filter an array, so numbers less than 5 or even numbers only.

Implementing this in JScript in the ScriptControl in VBA was a little challenging because it is limited to Ecmascript 3. But here in part one I can give a block code below which demonstrates how it is possible, in part two I will integrate it into my FunctionDelegate framework and we will see a nice compact syntax .

So the key lines of code to make this fly declare a global singleton variable and then add the functions as properties identified by strings. Instead of using the ScriptControl's AddCode method I use the Eval method (it wouldn't work otherwise).

    '* add a singleton global variable then add some functions demonstrating how to use the square brackets surrounding
    '* an identifier to reference a function
    oSC.Eval "var predicates={};"
    oSC.Eval "predicates['IsEven'] = function (num) { return ((num % 2) === 0); };"
    oSC.Eval "predicates['IsSmall'] = function (num) { return num < 5; };"

Then to retrieve and execute the predicate it is simply the following where predicateName is a string containing predicate name such as 'IsEven' or 'IsSmall'... (though in reality we add some error checking in case we're asking for a predicate that wasn't added) ...

var pred = predicates[predicateName]; 
return pred(n);

So below is a big block code which demonstrates it working. The IsEven() and IsSmall() are kind of hardcoded. In contrast, the IsOdd() predicate is added in a parameterised fashion and you could see how we could break this out and expose a function to the outside world. Then underneath can be found the filterByPredicate() function which takes a predicate name and an array and filters the array using the requested predicate. Lastly, at the bottom one can find test code calling into filterByPredicate() with the three different predicates.

In part two, I will fold these concepts into the FunctionDelegate framework I have developed


Option Explicit
Option Private Module

'* Tools->References
'*   MSScriptControl    Microsoft Script Control 1.0    C:\Windows\SysWOW64\msscript.ocx

Private Sub ExperimentWithPredicates()

    Dim sProg As String

    Dim oSC As MSScriptControl.ScriptControl
    Set oSC = New MSScriptControl.ScriptControl
    oSC.Language = "JScript"
    
    '* https://docs.microsoft.com/en-us/scripting/javascript/reference/vbarray-object-javascript
    oSC.AddCode "function fromVBArray(vbArray) { return new VBArray(vbArray).toArray();}"

    'http://cwestblog.com/2011/10/24/javascript-snippet-array-prototype-tovbarray/
    sProg = "Array.prototype.toVBArray = function() {                                                                        " & _
            "   var dict = new ActiveXObject('Scripting.Dictionary');                                                        " & _
            "   for(var i = 0, len = this.length; i < len; i++)                                                              " & _
            "       dict.add(i, this[i]);                                                                                    " & _
            "   return dict.Items();                                                                                         " & _
            "};                                                                                                              "
    oSC.AddCode sProg
    
    
    
        
    '* add a singleton global variable then add some functions demonstrating how to use the square brackets surrounding
    '* an identifier to reference a function
    oSC.Eval "var predicates={};"
    oSC.Eval "predicates['IsEven'] = function (num) { return ((num % 2) === 0); };"
    oSC.Eval "predicates['IsSmall'] = function (num) { return num < 5; };"
    
    '* add a function that invokes the predicate, this is mainly error handling
    '* I needed it during development to figure out the behaviour, probably too much code now
    sProg = "function runPredicate(predicateName,n) {                                                                        " & _
            "   var pred = predicates[predicateName];                                                                        " & _
            "   if (typeof pred !== 'undefined' && pred) {                                                                   " & _
            "       return pred(n);                                                                                          " & _
            "   }                                                                                                            " & _
            "}"
    oSC.AddCode sProg
    
    
    '* quickly test these
    Debug.Assert oSC.Run("runPredicate", "IsEven", 8)
    Debug.Assert Not oSC.Run("runPredicate", "IsEven", 7)
    Debug.Assert oSC.Run("runPredicate", "IsSmall", 1)
    Debug.Assert Not oSC.Run("runPredicate", "IsSmall", 6)
    
    
    '* just to prove we could do this dynamically I give this example
    Dim sPredicateName As String
    sPredicateName = "IsOdd"
    
    Dim sPredicateSource As String
    sPredicateSource = "function (num) { return ((num % 2) === 1); }"
    
    oSC.Eval "predicates['" & sPredicateName & "'] = " & sPredicateSource & ";"
    
    '* test this as well
    Debug.Assert oSC.Run("runPredicate", sPredicateName, 7)
    Debug.Assert Not oSC.Run("runPredicate", sPredicateName, 8)


    sProg = "function filterByPredicate(predicateName, vbNumbers) {                                                          " & _
            "    var filtered = []; var numbers=fromVBArray(vbNumbers);                                                      " & _
            "    var pred = predicates[predicateName];                                                                       " & _
            "        for (var i = 0; i < numbers.length; i++) {                                                              " & _
            "            if (pred(numbers[i])) {                                                                             " & _
            "                filtered.push(numbers[i]);                                                                      " & _
            "            }                                                                                                   " & _
            "        }                                                                                                       " & _
            "    return filtered.toVBArray();                                                                                " & _
            "}                                                                                                               "
    oSC.AddCode sProg
    

    
    Dim vFiltered1 As Variant
    vFiltered1 = oSC.Run("filterByPredicate", "IsEven", Array(1, 2, 3, 4, 5, 6))
    Debug.Assert vFiltered1(0) = 2
    Debug.Assert vFiltered1(1) = 4
    Debug.Assert vFiltered1(2) = 6
    
    Dim vFiltered2 As Variant
    vFiltered2 = oSC.Run("filterByPredicate", "IsSmall", Array(1, 2, 3, 4, 5, 6))
    Debug.Assert vFiltered2(0) = 1
    Debug.Assert vFiltered2(1) = 2
    Debug.Assert vFiltered2(2) = 3
    Debug.Assert vFiltered2(3) = 4
    
    
    Dim vFiltered3 As Variant
    vFiltered3 = oSC.Run("filterByPredicate", "IsOdd", Array(1, 2, 3, 4, 5, 6))
    Debug.Assert vFiltered3(0) = 1
    Debug.Assert vFiltered3(1) = 3
    Debug.Assert vFiltered3(2) = 5

    'Stop
End Sub

No comments:

Post a Comment