CATScript - The Beauty of Code

CATScript is a scripting language specific to CATIA, a widely used computer-aided design (CAD) software.

In this article, I’d like to put down some more reusable code snippets to make a programme’s live easier.

Text Concatenation with Array

The following coding example is likely the most efficient way of managing large amounts of text information.
Note when using the Join function, vbNewLine needs to be declared only once.

Option Explicit
Language="VBSCRIPT"

Sub CATMain()
	Dim i As Integer
	Dim msg As Array
	
	msg = Array ( _
		"entia non sunt",_
		"multiplicanda praeter",_
		"necessitatem",_
		"",_
		"Entities should not be",_
		"multiplied beyond necessity."_
	)

	' loop through each individual array item
	' For i = 0 To UBound(msg)	MsgBox msg(i) Next

	' show the array's content
	MsgBox Join (msg, vbNewLine), vbOkOnly + vbInformation, "Message:"
End Sub

If you are short in time, don’t read any further in this chapter, use the above code, modify it as required and you are done.

Similar code but implemented as a Class...

As a comparison, here is an alternative class which basically offers the same functionality but obviously needs a lot more code. I initially coded this class a while a go, at that time I was not aware of the benefits using a simple Array data declaration.

Note that the index of the string array starts at zero (0) in the same way as for array data objects.

Class CustomStrArray
    Private data() As String
	Private is_set As Boolean

	' data index starts at 0
    Private Sub Class_Initialize() : ReDim data(0) : is_set = False : End Sub
    Private Sub Class_Terminate()  : Erase data : End Sub

    Public Property Get Count() As Long
		Count = UBound(data)
	End Property

    Public Sub Add (ByVal str As String)
		If is_set = False Then
			data(0) = str
			is_set = True
		Else
			ReDim Preserve data(UBound(data) + 1)
			data(UBound(data)) = str
		End If
    End Sub

    Public Function Item (ByVal i As Integer) As String
        Item = data(i)
    End Function
	
	Public Function GetAllText () As String
		Dim i As Integer
		Dim txt As String

		If Not is_set Then
			txt = ""
		Else
			txt =  data(0)
			For i = 1 To UBound(data)
				txt = txt + vbNewLine + data(i)
			Next
		End If
		GetAllText = txt
	End Function
End Class

Sub CATMain ()

	Dim i As Integer
	Dim cdata As CustomStrArray
	Set cdata = New CustomStrArray

	cdata.Add "Hello"
	cdata.Add "world"
	cdata.Add "..."

	' class function example call
	MsgBox cdata.GetAllText, vbOkOnly, "Text info:"

	' loop through all individual string items...
	' note that the index starts at zero (same as for arrays)

	For i = 0 To cdata.Count
		MsgBox cdata.Item(i), vbOkOnly, "String data Item:"
	Next

	Set cdata = Nothing
End Sub
Similar code, extended with boundary check ...

Another variation of the code with improved solid rock boundary check. If a boundary given to the Item sub-function might be out of range, an error will be raised.

Class CustomStrArray
    Private data() As String
    Private Sub Class_Initialize() : ReDim data(0) : data(0) = "" : End Sub
    Private Sub Class_Terminate()  : Erase data : End Sub

    Public Property Get Count() As Long
        If UBound(data) = 0 And data(0) = "" Then
            Count = 0
        Else
            Count = UBound(data) + 1
        End If
    End Property

    Public Sub Add (ByVal str As String)
        If UBound(data) = 0 And data(0) = "" Then
            data(0) = str
        Else
            ReDim Preserve data(UBound(data) + 1)
            data(UBound(data)) = str
        End If
    End Sub

    Public Function Item (ByVal i As Integer) As String
        If i >= 0 And i <= UBound(data) Then
            Item = data(i)
        Else
            Err.Raise 9, "CustomStrArray", "Subscript out of range"
        End If
    End Function

    Public Function GetAllText() As String
        Dim i As Integer
        Dim result As String
        If Count = 0 Then
            GetAllText = ""
        Else
            result = data(0)
            For i = 1 To UBound(data)
                result = result & vbNewLine & data(i)
            Next
            GetAllText = result
        End If
    End Function
End Class

Sub CATMain()
    Dim cdata As CustomStrArray
    Set cdata = New CustomStrArray

    cdata.Add "The"
    cdata.Add "quick"
    cdata.Add "brown"
    cdata.Add "fox..."

    MsgBox cdata.GetAllText()

    Set cdata = Nothing
End Sub

Dictionaries

While I typically use the commands built into the CATScript language, there is also additional functionality available via CreateObject. This way, dictionary objects are provided and can be employed to address problems requiring more complex data structures.

Example code, read more...

Sub CATMain ()

	Dim dict As Dictionary
	Set dict = CreateObject("Scripting.Dictionary") ' late binding

	' add items to the collection ...
	
	' The Key can be any data type.
	' The Item can be any data type, an object, array, collection or even a dictionary.
	' So you could have a Dictionary of Dictionaries, Array and Collections.
	' But most of the time it will be a value(date, number or text).
	
	dict.Add 0, "First Item"
	dict.Add 1, "Second Item"

	' access and display items
	Dim item As Variant

	For Each item In dict.Keys : MsgBox dict(item) : Next

	' remove an item
	dict.Remove 0

	' check if an item exists
	If dict.Exists(2) Then MsgBox "Key2 exists in the collection."

End Sub

MsgBox Goodies

The array data type can also be used to implement a clean and lean MsgBox call.

I personally like the “Select Case” statement together with predefined enumerations.
This way, there is no need to hard code return values.

Sub UserSelectionCmd()
	Dim msg As Array
	msg = Array (_
		"This function allows to ....",_
		"<your text description here>",_
		"<some more explanations...>",_
		"","",_
		"YES = " + vbTab + "Do you like to continue?",_
		"No = "  + vbTab + "<Do semething else instead...>")

	Do While True
		Select Case MsgBox ( _
				Join (msg, vbNewLine),_
				vbQuestion + vbYesNoCancel, "User Selection:")

		Case vbYes: Exit Do
		Case vbNo:  Call DoSomethingElseCmd ()
		Case vbCancel: Exit Sub
		End Select
	Loop
End Sub

Sub CATMain()
	UserSelectionCmd
End Sub

More about Dynamic Arrays

I prefer dynamic array allocation because it is the most flexible approach. At the same time, I try to avoid writing excessive code, so I looked for a clean and concise solution to meet this requirement:

Standard way to set up some more array variables manually.

Sub CATMain ()
	Dim i As Integer
	Dim item, var() As Variant

	ReDim var(0)          : var(0) = 1
	ReDim Preserve var(1) : var(1) = "Hello"
	ReDim Preserve var(2) : var(2) = True

	For i = LBound(var) To UBound(var)
	  MsgBox TypeName (var(i)) + " = " + CStr(var(i))
	Next

	' also possible: 
	For Each item In var
	  MsgBox TypeName (item) + " = " + CStr(item)
	Next
End Sub

Accessing every element in an array can also be achieved using a For Each loop, which is often the most readable and concise option.

The following code mimics a dynamic array. The size of the array is handled on the fly at the time when adding a new array item:

Sub Add (ByRef arr() As String, ByVal new_item As String)
    ReDim Preserve arr (UBound(arr) + 1)
    arr (UBound(arr)) = new_item
End Sub

Sub CATMain()
	Dim arr() As String
	ReDim arr(-1)

	' Add items dynamically
	Add arr, "First"
	Add arr, "Second"
	Add arr, "Third"
	MsgBox Join(arr, vbNewLine)
End Sub

This is likely the most comprehensive version for a dynamically allocated array with the fewest lines of code.

Notes:

  • The array index starts at 0 which is the default behavior for array variables.

  • If the array would be declared as Dim arr() As Variant the array is capable to handle any kind of variable.

    Hint: If an object is assigned as array item, you probably would need to declare the Set keyword as usual.

  • To loop through each item of the array, one can do this as usaully like toe following:

    	Dim arr() As String
    
    	' MsgBox CStr(LBound(arr)) + "/" + CStr(UBound(arr))
    
    	Dim i As Integer
    	For i = LBound(arr) To UBound(arr)
    		MsgBox "Index " & i & ": " & arr(i)
    	Next
    

    and as an alternative:

    	MsgBox Join(arr, vbNewLine)
    

Using brakets to access the value of an array element

The standard way to access the value of an array element is by using (). As usual, you need an array type declaration to do so.

When using the Split() command, a variation of the () cast statement can be used without requiring an extra variable.

Notice that the syntax also allows to append (0) to the Split() command in order to access the specified array element. Because this style is not widely used, I’d like to draw special attention to it.

Example use case:

Const DELIMITER = "~"

Sub CATMain ()
	Dim str As String
	str = Join(Array("Hello", "world", "!"), DELIMITER)

	MsgBox Split(str,DELIMITER )(0)
End Sub

Language specific notes

Option Explicite

' declaration at the very beginning of a macro:
Option Explicite

What does it mean?

This declaration tells the interpreter to check, if a variable hase been declared before usage (like e.g. Dim i As Integer : Dim str As Str, etc…).

All variables whatsoever will be checked before evaluation when loading the macro into memory.

I would highly recommend to use this option as it helps to produce more stable code.

CATScript allows to use variables on the fly and without declaration at all. Although possible, I would not recomment this style. It leads to more error prone code where errors might occure at runtime just like: “variable not set” (or similar).

Where to declare variables

A variable can be declared at any place in the source code. Never the less, I do not use this flexibility and tend to strictly declare variables at the beginning of a sub function.

Coding this way has the benefit so that the code is more readable and focusing on the logic rather than speding time too much on DIM statements.

Code structure:

  1. There is one block of DIM statements at the beginning.
  2. What follows next usually is to initialize the variables and
  3. subsequently the programming logic follows…
Example code ...

' // attempt to open a data file
' //
Private Function openDataFileForWriting ( _
			ByVal fileName As String, _
			ByRef ostream As CATIATextStream) As Boolean

	Dim err_num As Integer
	Dim Overwrite As Boolean : Overwrite = True
	Dim fs As FileSystem
	Dim f As File

	openDataFileForWriting = False
	Set fs = CATIA.FileSystem

	On Error Resume Next
	Set f = fs.CreateFile(fileName, Overwrite)
	err_num = Err.Number
	On Error Goto 0

	If err_num <> 0 Then Exit Function

	' attempt to open the output file
	On Error Resume Next
	Set ostream = f.OpenAsTextStream ("ForWriting")
	err_num = Err.Number
	On Error Goto 0

	If err_num <> 0 Then Exit Function

	openDataFileForWriting = True
End Function

Error handling

In the example above, error handling is coded the most pragmatic way. Each individual command where an error might occure is surrounded by “On Error Resume Next” and corresponding “On Error Goto 0”.

Also notice that both calls clear the Error.Number so it is necessary to declare an interger variable in case the error number would be needed to code some more logic around the error. Due to this behavior “Err.Clear” command in most cases is not required.

Is it necessary to unset Variables

As CATScript has a build in garbage collector (gc), it is simply not needed to unset a variable or to use the Erase command to clean memory.

My strategy as of now is that I do not bother to try to clean up memory (any more). We are dealing with an interpreter with a build-in gc, so it is save to do so. I never faced memory problems with CATScript in any way.