Features Differences

This document attempts to describe the features separating Harbour from
CA-Cl*pper.

/* TODO: @FunPointer(), and all other Harbour extensions. */

Harbour Macro Compiler
----------------------
The Harbour Macro Compiler offers 2 additional layers of functionality
controlled by means of hb_SetMacro()* function, not available in CA-Cl*pper.

hb_SetMacro( HB_SM_HARBOUR, .T. ) will enable macro compilation and
evaluation of complex expressions not supported by CA-Cl*pper like:

 - exp++, exp--, var += exp, (exp), etc..
 - Nested codeblocks.
 - Expressions longer then 254 characters.

hb_SetMacro( HB_SM_XBASE, .T. ) will enable macro compilation and
evaluation of comma separated lists in all contexts where lists are
acceptable by CA-Cl*pper*, including:

  - { &cMacro } // Literal array elements list.
  - SomeArray[ &cMacro ] // Array index list.
  - SomeFun( &cMacro ) // Arguments list.
  - ( &cMacro ) // parenthesized list expression.

*CA-Cl*pper only supports list macros within codeblocks context.

Both these extra layers are activated by default.

* See also -k Compiler switch.

LIST Command
------------

LIST &cMacro

LIST in CA-Cl*pper [superficially] supports macros of lists expressions.
No error will be produced, and all expressions in the list will be
evaluated, but *only* the *last* expression will be displayed. This is
not documented in either the LIST Command or the Macro Operator
descriptions, but is the de-facto behavior in all CA-Cl*pper 5.x versions.

Harbour instead will not only evaluate all of the expressions in
such list macro, but will also display all such values. This default
behavior may be disabled with hb_SetMacro( HB_SM_XBASE, .F. )*

* See also -k Compiler switch.

INIT/EXIT and startup procedures
--------------------------------

In CA-Cl*pper the startup procedure is always the first procedure/function
of the main module, even if such symbol is an INIT or EXIT symbol. In
such case the program will never execute the "main" symbol. In Harbour
the first *non* INIT/EXIT symbol, will be executed as the main symbol
after all INIT procedures have been executed.

FOR EACH statement
------------------
Harbour has support enumeration loop with the following syntax:

   FOR EACH var1 [,var255] IN expr1 [,expr255] [DESCEND]
      [EXIT]
      [LOOP]
      ...
   NEXT

Note:
   - expr can be a string or an array
   - enumerator variable 'var<n>' stores a reference to the element of
     an array or a string specified by 'expr<n>' thus assigments to the
     enumerator changes the value of given array element
   - after the loop the controlling variable(s) store the value which
     they had before entering the loop
   - the enumeraqtor variable supports the following properties
     :__enumindex - the loop counter for variable
     :__enumbase  - the value that is being traversed
     :__enumvalue - the value of variable

for example:
   a := 'A'
   b := 'B'
   FOR EACH a, b IN { 1, 2, 3, 4 }, "abcd"
      ? a, b   //prints: 1 a
               //        2 b
               //        3 c
               //        4 d
   NEXT
   ? a, b   //prints: A B

   // you can use EXIT statement inside the loop
   FOR EACH a IN { 1, 2, 3, 4 }
      IF a:__enumindex == 3
         ? a
         EXIT
      ENDIF
   NEXT

   arr := { 1, 2, 3 }
   str := "abc"
   FOR EACH a, b IN arr, str
      a *= 2
      str := Upper( str )
   NEXT
   // now 'arr' stores { 2, 4, 6 }
   // howerer 'str' still stores "abc"

Notice the difference:
   FOR EACH a IN someValue
      ? a:__enumindex   //prints current value of the index
      ? (a):__enumindex //sends '__enumindex' message to the current value
   NEXT

WITH OBJECT
-----------
Harbour supports the following statement:

   WITH OBJECT expression
      ...
   ENDWITH

   Inside this WITH OBJECT/END enclosure you can use the simplified
   form of sending messages to the object. You can use the syntax
   :message( [params] )
   :property
   to send messages to the object specified by 'expression'

for example:
   WITH OBJECT myobj:a[ 1 ]:myitem
      :message( 1 )
      :value := 9
   ENDWITH

   The above code is equivalent to:
   myobj:a[ 1 ]:myitem:message( 1 )
   myobj:a[ 1 ]:myitem:value := 9

   Inside WITH OBJECT/END you can access (or even assign a new object)
   using a special reserved property :__withobject

       The runtime error will be generated at the time of message
       sending (or property access/assign) if <objexpression>
       is not a value of type object.

for example:
   CREATE CLASS foo
      VAR name INIT 'FOO'
   ENDCLASS

   CREATE CLASS bar
      VAR name INIT 'BAR'
   ENDCLASS

   WITH OBJECT foo():new()
      ? :name                 //prints 'FOO'
      ? :__withobject:name    //also prints 'FOO'
      ? :__withobject := bar():new()
      ? :name                 //prints 'BAR'
   ENDWITH

Source : https://github.com/harbour/core/blob/master/doc/clipper.txt

Hash vs Table

Consider a table for customers records with two character fields : Customer ID and customer name:

Cust_ID Cust_Name
CC001 Pierce Firth
CC002 Stellan Taylor
CC003 Chris Cherry
CC004 Amanda Baranski

 It’s known all possible and necessary operations on a table: APPEND, DELETE, SEEK and so on; by the way, for SEEK we need an index file also.

Listing this table is quite simple:

USE CUSTOMER
WHILE .NOT. EOF()
   ? CUST_ID, CUST_NAME
   DBSKIP()
ENDDO

 If our table is sufficiently small, we can find a customer record without index and SEEK :

LOCATE FOR CUST_ID = “CC003”
? CUST_ID, CUST_NAME

If we want all our data will stand in memory and we could manage it more simple and quick way, we would use an array ( with some considerations about size of table; if it is too big, this method will be problematic ) :

aCustomer := {}    // Declare / define an empty array
USE CUSTOMER
WHILE .NOT. EOF()
   AADD(aCustomer, { CUST_ID, CUST_NAME } )
   DBSKIP()
ENDDO
Traversing this array is quite simple :

FOR nRecord := 1 TO LEN( aCustomer )

    ? aCustomer[ nRecord, 1 ], aCustomer[ nRecord, 2 ]
NEXT
or :

a1Record := {}

FOR EACH a1Record IN aCustomer
   ? a1Record[ 1 ], a1Record[ 2 ]
NEXT

And locating a specific record too:

nRecord := ASCAN( aCustomer, { | a1Record | a1Record[ 1 ] == “CC003” } )

? aCustomer[ nRecord, 1 ], aCustomer[ nRecord, 2 ]

A lot of array functions are ready to use for maintain this array : ADEL(), AADD(), AINS() etc …

Now, let’s see how we could use a hash for achieve this job :

hCustomer := { => } // Declare / define an empty hash

USE CUSTOMER
WHILE .NOT. EOF()
   hCustomer[ CUST_ID ] := CUST_NAME
   DBSKIP()
ENDDO
Let’s traversing :

h1Record := NIL

FOR EACH h1Record IN hCustomer
   ? h1Record: __ENUMKEY(),h1Record:__ENUMVALUE()
NEXT

Now, we have a bit complicate our job; a few field addition to the table :

No: Field Name Type Width  Dec Decription

1

 CUST_ID

C

 5

0

Id ( Code )

2

 CUST_NAME

C

10

0

Name

3

 CUST_SNAM

C

10

0

Surname

4

 CUST_FDAT

D

 8

0

First date

5

 CUST_ACTV

L

 1

0

Is active ?

6

 CUST_BLNCE

N

11

2

Balance

 While <key> part of an element of a hash may be C / D / N / L type; <xValue> part of hash too may be ANY type of data, exactly same as arrays.

So, we can make fields values other than first ( ID) elements of an array:

hCustomer := { => } // Declare / define an empty hash
USE CUSTOMER
WHILE .NOT. EOF()
   a1Data:= { CUST_NAME, CUST_SNAM, CUST_FDAT, CUST_ACTV, CUST_BLNCE }
   hCustomer[ CUST_ID ] := a1Data
   DBSKIP()
ENDDO
Let’s traversing :

h1Record := NIL

FOR EACH h1Record IN hCustomer
   a1Key  := h1Record:__ENUMKEY()
   a1Data := h1Record:__ENUMVALUE()
   ? a1Key
   AEVAL( a1Data, { | x1 | QQOUT( x1 ) } )
NEXT
*-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._
/*
Hash vs Tables
 
*/
#define NTrim( n ) LTRIM( STR( n ) )
#define cLMarj SPACE( 3 )
PROCEDURE Main()

  SET DATE GERM
  SET CENT ON
  SET COLO TO "W/B"
  SetMode( 40, 120 )
 
  CLS
 
  hCustomers := { => } // Declare / define an empty PRIVATE hash
 
  IF MakUseTable() 
 
     Table2Hash()
 
     * Here the hash hCustomers may be altered in any way
 
     ZAP
 
     Hash2Table()
 
  ELSE
      ? "Couldn't make / USE table"
  ENDIF
 
  ?
  @ MAXROW(), 0
  WAIT "EOF HashVsTable.prg"
 
RETURN // HashVsTable.Main()
*-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.
PROCEDURE Table2Hash()
   hCustomers := { => } 
   WHILE .NOT. EOF()
     hCustomers[ CUST_ID ] := CUST_SNAM
     DBSKIP()
   ENDDO
 
   ListHash( hCustomers, "A hash transferred from a table (single value)" )
 
   hCustomers := { => } // Declare / define an empty hash
   DBGOTOP()
   WHILE .NOT. EOF()
      hCustomers[ CUST_ID ] := { CUST_NAME, CUST_SNAM, CUST_FDAT, CUST_ACTV, CUST_BLNCE }
      DBSKIP()
   ENDDO
 
   ListHash( hCustomers, "A hash transferred from a table (multiple values)" )
 
RETURN // Table2Hash()

*-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.

PROCEDURE Hash2Table()
   LOCAL h1Record,;
         c1Key,;
         a1Record,;
         n1Field
 
   FOR EACH h1Record IN hCustomers
      c1Key := h1Record:__ENUMKEY()
      a1Record := h1Record:__ENUMVALUE()
      DBAPPEND()
      FIELDPUT( 1, c1Key )
      AEVAL( a1Record, { | x1, n1 | FIELDPUT( n1 + 1 , x1 ) } )
   NEXT h1Record
   DBGOTOP()
 
   ?
   ? "Data trasferred from hash to table :"
   ?
   WHILE ! EOF()
      ? STR( RECN(), 5), ''
      FOR n1Field := 1 TO FCOUNT()
         ?? FIELDGET( n1Field ), ''
      NEXT n1Field
      DBSKIP()
   ENDDO 
 
RETURN // Hash2Table()

*-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.

PROCEDURE ListHash( hHash, cComment )
 
  LOCAL x1Pair
 
  cComment := IF( HB_ISNIL( cComment ), '', cComment )
 
  ? 
  ? cComment // , "-- Type :", VALTYPE( hHash ), "size:", LEN( hHash )
  ?
  IF HB_ISHASH( hHash ) 
     FOR EACH x1Pair IN hHash
        nIndex := x1Pair:__ENUMINDEX()
        x1Key := x1Pair:__ENUMKEY()
        x1Value := x1Pair:__ENUMVALUE()
        ? cLMarj, NTrim( nIndex ) 
*       ?? '', VALTYPE( x1Pair )
        ?? '', x1Key, "=>"
*       ?? '', VALTYPE( x1Key ) 
*       ?? VALTYPE( x1Value ) 
        IF HB_ISARRAY( x1Value ) 
           AEVAL( x1Value, { | x1 | QQOUT( '', x1 ) } )
        ELSE 
           ?? '', x1Value
        ENDIF 
     NEXT
  ELSE
    ? "Data type error; Expected hash, came", VALTYPE( hHash ) 
  ENDIF HB_ISHASH( hHash )
RETURN // ListHash()
*-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.

FUNCTION MakUseTable() // Make / USE table
 
 LOCAL cTablName := "CUSTOMER.DBF"
 LOCAL lRetval, aStru, aData, a1Record 
 
 IF FILE( cTablName ) 
    USE (cTablName)
 ELSE
    aStru := { { "CUST_ID", "C", 5, 0 },;
               { "CUST_NAME", "C", 10, 0 },;
               { "CUST_SNAM", "C", 10, 0 },;
               { "CUST_FDAT", "D", 8, 0 },;
               { "CUST_ACTV", "L", 1, 0 },;
               { "CUST_BLNCE", "N", 11, 2 } }
    * 
    * 5-th parameter of DBCREATE() is alias - 
    * if not given then WA is open without alias 
    *                              ^^^^^^^^^^^^^ 
    DBCREATE( cTablName, aStru, , .F., "CUSTOMER" ) 
 
    aData := { { "CC001", "Pierce", "Firth", 0d20120131, .T., 150.00 },; 
               { "CC002", "Stellan", "Taylor", 0d20050505, .T., 0.15 },;
               { "CC003", "Chris", "Cherry", 0d19950302, .F., 0 },;
               { "CC004", "Amanda", "Baranski", 0d20011112, .T., 12345.00 } }
 
    FOR EACH a1Record IN aData
        CUSTOMER->(DBAPPEND())
        AEVAL( a1Record, { | x1, nI1 | FIELDPUT( nI1, X1 ) } )
    NEXT a1Record 
    DBGOTOP()
 
 ENDIF 
 
 lRetval := ( ALIAS() == "CUSTOMER" )
 
RETURN lRetval // MakUseTable()

*-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._.-._
 
HashVsTable