All Forums >> [General Forum] >> Off-Topic Lounge >> RE: Weekly/Monthly Challenge? Do you like VisualBasicScript.com? Link to us and help spread the word about our forum. Thanks!
"There's the one man who learns by reading, the two men that learn by watching, and the rest of us have to pee on the electric fence for ourselves." - Roy Rogers
"Would you like to touch my monkey?" - Dieter (Mike Meyers)
Hey, Digital Skream. I've only noticed the irony of your sig. Did you write it like that on purpose?
Yes, I did.
_____________________________
"There's the one man who learns by reading, the two men that learn by watching, and the rest of us have to pee on the electric fence for ourselves." - Roy Rogers
"Would you like to touch my monkey?" - Dieter (Mike Meyers)
to give TNO's contribution its due and (perhaps) to lure some other people to this topic, I want to show you the script I currently use to tinker with the problem.
To explain the structure of my test scripts and the reasons for it, I'd like to point you to the very simple program I posted for the second challenge. Comparing this to the more elaborate version may help you to make sense of my remarks.
As I very/too often said, I start all my scripts with
Option Explicit
to avoid problems due to misspelled variable names; furthermore Dimming my variables forces me to think about them very carefully. Do I really need it? Could I avoid a variable by using an expression or a function? In which scope the variable must be known? Some people like to Dim all their variables at once and initialize them later/as needed:
Dim oFSO, i, iIterations, x, t1, t2, t3, e1, e2, e3, iNumItems Set oFSO = CreateObject("Scripting.FileSystemObject") ... iIterations = 3
I prefer to keep Dimming (=Declaring) and intializing close together, because this may serve as a kind of documentation for the variable(s):
Dim oFSO : Set oFSO = CreateObject("Scripting.FileSystemObject") ... Dim iIterations : iIterations = 3
As close as possible to its first use I associate the variable name with its data type and its (first/start) value, hoping to make it easy for my feeble memory. Do you want to comment on
Dim i, j
, TNO?
While I'm aware of the problems caused by global variables, I will use them, if I think it advantageous. To make them stand out, I use a "g" prefix in their names:
Dim goTNOArray2 ' needed for TNOJoin()
For two reasons, I restrict my main/top level code to the Dimming of global variables and the returning of an error/success code to the operating system:
WScript.Quit doMain()
The first reason: That way I can easily run different portions of code - tackling various sub problems or trying out different soultions - from the same .vbs file by writing more than one 'main' function and using reordering or commenting to switch between them:
The second reason: That way I can't introduce global variables 'by accident'. If I had Dimmed sSep at the top level, then using it in the toDispString( vX ) function would be very tempting
As it is, I can't use this easy way out, but have to acknowledge the pending problem: Is it ok to hard code the inner separator or must I feed it to the function via parameter? Do I really want to implement a toDispString( vX, sSep ) or even a toDispString( vX, aSeps ) function to handle (deeply) nested arrays?
So a typical main function looks like this:
Function doMain() Dim nRval : nRVal = 0 ... WScript.Echo "##### VBScript Forum Challenge 001: JoinX" ... doMain = nRVal End Function
The return/error code feature isn't used in most of my test scripts, but it proved valuable for 'real world' scripts that are scheduled or used from other programs.
Testing/trying out means testing/trying more than one thing. So most test scripts use a simple
' the strings we want to test Dim aTStrs : aTStrs = Array( _ "" _ , "a b c" _ )
or a more complicated/structured array of data
' the arrays we want to test (each one named for easy reference) Dim aTArrs : aTArrs = Array( _ "empty" , Array() _ , "one string" , Array( "a" ) _ ... , "objects" , Array( New RegExp, CreateObject( "ADODB.Connection" ), oObj, Err ) _ )
to be fed to the code/functions. This setup and the effort put into the formatting makes it easy to add interesting/critical test cases. Of course the structure of the data is reflected in the code to loop over them. For simple arrays simple loops:
' loop over all strings to test For Each sTest In aTStrs ... Next
for implicitly structured arrays, the Step feature of the For loop comes handy:
' loop over all arrays to test For nTArr = 0 To UBound( aTArrs ) Step 2 ... WScript.Echo "=====", nTArr / 2, "Array:", aTArrs( nTArr ) ' <-- the name ... sRes = fncJ( aTArrs( nTArr + 1 ), sSep ) ' <-- the array to feed to the function ... Next
' loop over all functions to use For nFunc = 0 To UBound( aFuncs ) WScript.Echo "-----", nFunc, "Function:", aFuncs( nFunc )( 0 ) Set fncJ = aFuncs( nFunc )( 1 ) ' the function used sSep = aFuncs( nFunc )( 2 ) ' the separator used for display sRes = fncJ( aTArrs( nTArr + 1 ), sSep ) ' call the function WScript.Echo ">|" + sRes + "|<" Next
or by For Each:
Dim aFunc For Each aFunc In aFuncs WScript.Echo "----- Function:", aFunc( 0 ) Set fncJ = aFunc( 1 ) ' the function used sSep = aFunc( 2 ) ' the separator used for display sRes = fncJ( aTArrs( nTArr + 1 ), sSep ) ' call the function WScript.Echo ">|" + sRes + "|<" Next
If you don't need a numeric index variable, For Each is more convenient; but changing elements needs For, because the For Each variable is a copy. Run
Function ForForEach() Dim aTest : aTest = Array( "zero", "one", "two" ) Dim nIdx : nIdx = 0 Dim sTest For Each sTest In aTest sTest = CStr( nIdx ) nIdx = nIdx + 1 Next WScript.Echo "For Each", Join( aTest ) For nIdx = 0 To UBound( aTest ) aTest( nIdx ) = CStr( nIdx ) Next WScript.Echo "For ", Join( aTest ) ForForEach = 0 End Function
to see the difference.
For Each zero one two For 0 1 2
The script demonstrates 3 Join functions: The VBScript Join() that fails even for simple cases:
===== 2 Array: the empties ----- Function: VBS Join >|Typen unverträglich|< i.e. Type mismatched caused by Null ----- Function: TNO Join >|[vbEmpty]|[vbNull]|[vbObject]|< ----- Function: Our Join >|<Empty>|<Null>|<Nothing>|<
I don't want to spend further words on this miserable thing. The second functions is (my revision of) TNO's Join() function, that really is an significant improvement. The nits I feel the need to pick don't lower its value - that goes without saying.
No surprise, that I don't like the use of the global variable (though - as I said before - I use them myself if they are proved necessary). How could goTNOArray2 be avoided? By passing the second array as an parameter:
''# TNOJoinNOGV - calls TNO's Join (no global variable) ' ############################################################################ Function TNOJoinNOGV( ByRef Array1, Sep ) TNOJoinNOGV = TNOJoinNOGV_( Array1, Array(), Sep ) End Function
(3) that passes the second array to the recursive work function:
Function TNOJoinNOGV_( Array1, Array2, Sep ) Dim JS : Set JS = CreateObject( "JS.Array" ) Dim i, j
For Each i In Array1 If IsArray(i) Then TNOJoinNOGV_ i, Array2, Sep Else Select Case VarType(i) Case 0 JS.Push Array2, "[vbEmpty]" Case 1 JS.Push Array2, "[vbNull]" ... Case 13 JS.Push Array2, "[vbDataObject]" Case 17 JS.Push Array2, CStr(i) End Select End If Next
TNOJoinNOGV_ = Join( Array2, Sep ) End Function
Compare this to OURJoin
''# OURJoin - crossbred of contributions ' ############################################################################ Function OURJoin( aX, sSep ) ReDim aClone( UBound( aX ) ) Dim nIdx : nIdx = 0 Dim vElm For Each vElm In aX aClone( nIdx ) = toDispString( vElm ) nIdx = nIdx + 1 Next OURJoin = Join( aClone, sSep ) End Function
that uses a second array too - but one that is dimensioned suitably right from the start (no need for growing/push) and keeps it local (no parameter passing to the recursive work function toDispString()).
My verdict on the reliance on the JS.Array component is the same as what I said about ginolard's use of the .NET System.Collections.ArrayList. Non native VBScript means are ok if unavoidable, but I'd prefer a 'pure' solution. In TNO's case the foreign component is used only to make the dynamic growing of the second array more efficient. But that isn't necessary: We know the UBound of the second array beforehand - we nead exactly as many elements as the first one contains:
''# TNOJoinNOJS - calls TNO's Join (no JS.Array) ' ############################################################################ Function TNOJoinNOJS( ByRef Array1, Sep ) ReDim Array2( UBound( Array1 ) ) TNOJoinNOJS = TNOJoinNOJS_( Array1, Array2, Sep ) End Function
Function TNOJoinNOJS_( Array1, Array2, Sep ) Dim JS : Set JS = CreateObject( "JS.Array" ) Dim nIdx : nIdx = 0 Dim vElm
For Each vElm In Array1 If IsArray( vElm ) Then Array2( nIdx ) = TNOJoinNOJS( vElm, Sep ) Else Select Case VarType( vElm ) Case 0 Array2( nIdx ) = "[vbEmpty]" Case 1 Array2( nIdx ) = "[vbNull]" Case 2 Array2( nIdx ) = CStr( vElm ) Case 3 Array2( nIdx ) = CStr( vElm ) Case 4 Array2( nIdx ) = CStr( vElm ) Case 5 Array2( nIdx ) = CStr( vElm ) Case 6 Array2( nIdx ) = CStr( vElm ) Case 7 Array2( nIdx ) = CStr( vElm ) Case 8 Array2( nIdx ) = vElm Case 9 Array2( nIdx ) = "[vbObject]" Case 10 Array2( nIdx ) = "["&CStr( vElm )&"]" Case 11 Array2( nIdx ) = CStr( vElm ) Case 12 Array2( nIdx ) = "[vbVariant]" Case 13 Array2( nIdx ) = "[vbDataObject]" Case 17 Array2( nIdx ) = CStr( vElm ) End Select End If nIdx = nIdx + 1 Next
TNOJoinNOJS_ = Join( Array2, Sep ) End Function
I'm not happy with the use of magic numbers instead of the predefined VBScript constants:
Case vbEmpty ' 0 Uninitialized (default) sRVal = "<Empty>"
may be more characters to enter than
Case 0 Array2( nIdx ) = "[vbEmpty]"
but you write a script once (or - if you are like me - about 12 times), but you resp. your clients read it 4711 times. Think of all the time wasted to remember/check that VarType 0 means Empty (and not Null).
As most of the solutions presented here, TNO's function flattens arrays of arrays:
All my usual carping must not hide the fact that the third function - OURJoin() - wouldn't exist without all your contributions to this topic. It's much easier to program on the base of concrete working code than doing it from scratch. All fame and glory is due to the participants; extra thanks to TNO for his code (and the JS.Array which downloaded, installed and worked flawlessly for me). I just crossbred all the suggestions with some of my own thoughts. For example my very first version of the JoinX() function used string concatenation, DiGiTAL.SkReAM's dictionary approach and dm_4ever's use of Split and ReDim made me realize the (irony of the) possibility to let VBScript's Join() do the concatenation. Likewise there would be no dictionary joining without ebgreen. And I'm still waiting for ginolard to come up with some String.Join( ArrayList, "," ) magic to make OURJoined effort obsolete.
I think that splitting the joining into a 'put together' loop (see above) and a 'handle each element according to its data type' part (see below) makes the code easy to understand. mcds99, don't cry! You would make me happy/smile, if you could post some questions as to the what, how, and why of the code that forces me to make the code better (to comprehend). There are some points worth discussing in the monster Select Case. Should the (german) "," really replaced with "." in real numbers? Would it be necessary to check the locale? Should dates be delimited? Should strings be not delimited? How to discriminate between a plain date and the datetime midnight?
Ok, it's after midnight in Germany - here is all the code:
And the output
What still needs to be done:
(1) All Join functions strive to display the elements of the array; I would like to have a second version (JoinY (?)) that returns a string that could be fed to Eval() - as a kind of (simple) persistency feature. [I was not amused when ebgreen hijacked the persistency problem in http://www.visualbasicscript.com/fb.aspx?m=49431 and even mentioned XML - I had to revise my second challenge (see below)]
(2) Some objects - with default properties - are difficult/impossible to display:
The VBScript Split() function is nice. Lots of sample of its creative use in this forum. But other languages - notably Perl - provide an even nicer split(): one that allows a regular expression to be used instead of a mere string.
The easy version of the challenge: Write a
Split( expression, regexp|regexp.pattern )
function that uses a regexp(.pattern) - e.g. "[,;:]" - to split a string - e.g "a,b;c:d" - into an array ["a","b","c","d"]. To motivate you: Think of something like:
function that mimics the VBScript Split() regarding the fancy options.
For even more ambitious persons: try to implement some of the Perl features like splitting a string into an array of its characters or the option to return the separators as well.
Not to predetermine your approach, but simply to give you an idea of how to start: this is my test script that eagerly waits for your ideas/code/input:
As you can see, I included (commented out) a call to OURJoin() as an advertisement for the first challenge.
thanks a lot! Very promising approach and nice implementation! I incorporated your function into my test script (I removed the joining part to keep the function compatible to VBScript's Split()). Doing this, I had to realize that my simple plain array setup for the test cases doesn't scale: the split pattern has to be specified too. My quick and dirty revison