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