How does one use a &GROUP inside a QUEUE?

For something I am working on, I need a queue which contains a FIELD which can contain multiple groups as in, multiple different kind of groups, below is the definition (this is all used in a Clarion to C# project)

Inside my .inc file

ExampleParameters GROUP,TYPE
ReferenceKey        CSTRING(500 + 1)
ConnectionKey       CSTRING(1024 + 1)
                  END

Account           GROUP,TYPE
Id                  CSTRING(38 + 1)
Name                CSTRING(128 + 1)
Platform            LONG
Parameters          &GROUP
                  END

Accounts          QUEUE(EsA:Account),TYPE
                  END

Inside the application itself:

MyAccounts QUEUE(Accounts ) END

Later on in the application I have below logic to load information from the database (where I store the accounts and parameters in two separate tables (parameters contains key/value columns)

LoadToMyAccounts ROUTINE
DATA
idx LONG
ref ANY
    CODE
        OPEN(BoekhoudAccounts, ReadOnly);
        IF ERRORCODE() THEN STOP('Kan de Boekhoud Accounts bestand niet openen:|' & ERROR() & '|') END
        OPEN(BoekhoudAccountParameters, ReadOnly);
        IF ERRORCODE() THEN STOP('Kan de Boekhoud Accounts Parameters niet openen:|' & ERROR() & '|') END
        
        IF RECORDS(BoekhoudAccounts) = 0
            CLEAR(BoekhoudAccounts);
            BoeAcc:Id = '';
            BoeAcc:InGebruik = TRUE;
            BoeAcc:Name = '<PLATFORM>';
            BoeAcc:Platform = 1;
            ADD(BoekhoudAccounts);
            CLEAR(BoekhoudAccounts);
            
            CLEAR(BoekhoudAccountParameters);
            BoeAccPar:AccountId = '';
            BoeAccPar:Key = 'ReferenceKey';
            BoeAccPar:Value = 'SomeValue';
            ADD(BoekhoudAccountParameters);
            CLEAR(BoekhoudAccountParameters);
        END
       
        SET(BoekhoudAccounts);
        IF ERRORCODE() THEN STOP('Kan de Boekhoud Accounts niet in laden:|' & ERROR() & '|') END
        LOOP
            NEXT(BoekhoudAccounts);
            IF ERRORCODE() THEN BREAK END
            
            CLEAR(MyAccounts);
            MyAccounts.Id = BoeAcc:Id;
            MyAccounts.Name = BoeAcc:Name;
            MyAccounts.Platform = BoeAcc:Platform;
            
            CASE MyAccounts.Platform
                OF 1
                    DO LoadSpecificPlatform;
                ELSE
                    CYCLE;
            END
            
            ADD(MyAccounts);
            MyAccounts.Parameters &= NULL; ! Without I got a crash
            DISPOSE(MyAccounts.Parameters);
            CLEAR(MyAccounts);
        END
        
        CLOSE(BoekhoudAccounts);
        CLOSE(BoekhoudAccountParameters);
        
        MESSAGE('Loaded Accounts');
        
        LOOP i# = 1 TO RECORDS(MyAccounts) BY 1
            CLEAR(MyAccounts);
            GET(MyAccounts, i#);
            IF ERROR() THEN BREAK END
            idx = 0;
            LOOP
                idx += 1;
                ref &= WHAT(MyAccounts.Parameters, idx);
                IF ref &= NULL THEN BREAK END
                 
                ! ref is always empty
                MESSAGE('On Account[' & MyAccounts.Id & ']|' & WHO(MyAccounts.Parameters, idx) & ':|' & ref & '||');
            END
        END

LoadSpecificPlatform ROUTINE
DATA
idx LONG
ref ANY
parameters LIKE(ExampleParameters)
    CODE
        CLEAR(BoekhoudAccountParameters);
        BoeAccPar:AccountId = MyAccounts.Id;
        idx = 0;
        LOOP
            idx += 1;
            BoeAccPar:Key = CLIP(LEFT(WHO(parameters, idx)));
            IF CLIP(BoeAccPar:Key) = ''
                BREAK;
            END
            
            GET(BoekhoudAccountParameters, BoeAccPar:AccountIdKeyKey);
            IF ERRORCODE() THEN CYCLE END
            
            ref &= WHAT(parameters, idx);
            IF ref &= NULL THEN BREAK END
            
            ref = CLIP(LEFT(BoeAccPar:Value));
        END
        
        MyAccounts.Parameters &= parameters;

Thing is, where I say “ref is always empty” - the messaged value is always empty - as if the reference is not populated.

So can anyone help? Or is it just bad practice to add this to a QUEUE with &GROUP to allow dynamic parameters?

Did you assign the Parameters &GROUP reference a pointer to the actual group?

Parameters &= NEW (My Actual Group Area with the parameters definition)

It is not bad practice to add a &GROUP structure within a QUEUE. You can even pass to procedures a generic GROUP structure pointer as a parameter to do some useful generic programming.

Like anything else you have to be careful with what you NEW and DISPOSE.

Regards
Roberto Artigas

As far as I know NEW doesn’t work with GROUP - so no I didn’t NEW it - but as you can see in the last snippet, I do assign it:

MyAccounts.Parameters &= parameters;

Greetings -
You are correct. I did not scroll down enough to see the assignment.
My apologies. At this point I am not sure that I can help.
Hopefully someone else can jump in and assist you.
Regards,
Roberto Artigas

In LoadSpecificPlatform you have

MyAccounts.Parameters &= parameters

But that’s a problem as the right hand side is a variable that is scoped to the routine, so now you have a reference to memory somewhere in the stack that has been given back

Even if that were not a problem, the parameters variable in the routine is never to set anything,
TBH I’m not sure what you were trying to write.

As it was pointed out earlier CW does not let you NEW a group for some strange reason
You can work around this by allocating the bytes a NEW STRING( Size(theGroup))
or by redeclaring the group as a ctTheGroup CLASS(TheGroup),TYPE
and then NEW the class

I wonder if you’d be happier with a nested queue of Key,Value Pairs

parameters is being set inside the loop using WHAT through a reference - I know this works because I can show them with a message.

The thing I am writing is some code which loads data from the database to a QUEUE to keep it accessible in memory - the reason I don’t use a nested QUEUE is because I can’t Marshall that to my C# dll - hence why the QUEUE is based on a GROUP I defined, the GROUP works when set manually, but I want to keep all accounts available in-memory for ease of use/access.

Not being able to NEW a GROUP is something I think is weird, because it would solve this easily - I’ll try your two workarounds and see if that helps.

I found how to do it here: Why can't I New() a GROUP and what workaround/alternatives are there?

But it seems, when you use a STRING then WHAT and WHO no longer work for some reason, any fix for that?

As I think I can’t use the CLASS workaround as it means I’ll need a QUEUE per Parameter type

This is the new code I tried for LoadSpecificPlatform but now the MESSAGE only contains the field names and no values, I checked and seems ‘NO KEY’ is triggered, so WHO no longer returns the name of the field - any solution for that?

LoadSpecificPlatform ROUTINE
DATA
idx LONG
ref ANY
parametersString &STRING
parameters &ExampleParameters
    CODE
        parametersString &= NEW STRING(SIZE(ExampleParameters));
        parameters &= ADDRESS(parametersString);
        
        parameters.ConnectionKey = '';
        parameters.ReferenceKey = '';

        CLEAR(BoekhoudAccountParameters);
        BoeAccPar:AccountId = MyAccounts.Id;
        idx = 0;
        LOOP
            idx += 1;
            BoeAccPar:Key = CLIP(LEFT(WHO(parameters, idx)));
            IF CLIP(BoeAccPar:Key) = ''
                BREAK;
            END
            
            GET(BoekhoudAccountParameters, BoeAccPar:AccountIdKeyKey);
            IF ERRORCODE() THEN CYCLE END
            
            ref &= WHAT(parameters, idx);
            IF ref &= NULL THEN BREAK END
            
            ref = CLIP(LEFT(BoeAccPar:Value));
        END

        MESSAGE(|
            'ConnectionKey: ' & CLIP(parameters.ConnectionKey) & '|' |
          & 'ReferenceKey: ' & CLIP(parameters.ReferenceKey) |
        );
        
        MyAccounts.Parameters &= parameters;

In C#, a single reference can point to different types, and there are operators like is to properly identify them.

In Clarion, you could use multiple references to typed groups and NOT &= NULL to identify the type, as shown in TestQue1 ROUTINE below.

However, it may be simpler to just declare the groups directly in the queue, as shown in TestQue2 ROUTINE.

  PROGRAM
  MAP
  END

GroupAType          GROUP,TYPE
NameA                 STRING(10)
ValueA                LONG
                    END

GroupBType          GROUP,TYPE
NameB                 STRING(20)
ValueB                DECIMAL(15,2)
                    END

TestQue1            QUEUE
Id                    LONG
Data                  &STRING
GroupA                &GroupAType
GroupB                &GroupBType
                    END

TestQue2            QUEUE
Id                    LONG
GroupA                LIKE(GroupAType)
GroupB                LIKE(GroupBType)
                    END

  CODE
  
  DO TestQue1
  DO TestQue2
  
TestQue1            ROUTINE
  DATA 
idx LONG
str ANY
  CODE
 
  !Add GroupA
  CLEAR(TestQue1)
  TestQue1.Id = 1
  TestQue1.Data &= NEW STRING(SIZE(GroupAType))
  TestQue1.GroupA &= ADDRESS(TestQue1.Data)
  TestQue1.GroupA.NameA = 'Name A'
  TestQue1.GroupA.ValueA = 1234
  ADD(TestQue1)

  !Add GroupB
  CLEAR(TestQue1)
  TestQue1.Id = 2
  TestQue1.Data &= NEW STRING(SIZE(GroupBType))
  TestQue1.GroupB &= ADDRESS(TestQue1.Data)
  TestQue1.GroupB.NameB = 'Name B'
  TestQue1.GroupB.ValueB = 45.67
  ADD(TestQue1)

  !Show Queue
  str = 'Test 1|'
  LOOP idx = 1 TO RECORDS(TestQue1)
    GET(TestQue1,idx)
    str = str &' id:'&TestQue1.Id
    IF NOT TestQue1.GroupA &= NULL
      str = str &' NameA:'&TestQue1.GroupA.NameA&' ValueA:'&TestQue1.GroupA.ValueA
    END
    IF NOT TestQue1.GroupB &= NULL
      str = str &' NameB:'&TestQue1.GroupB.NameB&' ValueB:'&TestQue1.GroupB.ValueB
    END
    str = str &'|'
  END
  MESSAGE(str)

  !Free Queue
  LOOP idx = 1 TO RECORDS(TestQue1)
    TestQue1.GroupA &= NULL
    TestQue1.GroupB &= NULL
    DISPOSE(TestQue1.Data)
  END
  FREE(TestQue1)
  
TestQue2            ROUTINE
  DATA 
idx LONG
str ANY
  CODE
 
  !Add GroupA
  CLEAR(TestQue2)
  TestQue2.Id = 1
  TestQue2.GroupA.NameA = 'Name A'
  TestQue2.GroupA.ValueA = 1234
  ADD(TestQue2)

  !Add GroupB
  CLEAR(TestQue2)
  TestQue2.Id = 2
  TestQue2.GroupB.NameB = 'Name B'
  TestQue2.GroupB.ValueB = 45.67
  ADD(TestQue2)

  !Show Queue
  str = 'Test 2|'
  LOOP idx = 1 TO RECORDS(TestQue2)
    GET(TestQue2,idx)
    str = str &' id:'&TestQue2.Id
    IF TestQue2.GroupA.NameA
      str = str &' NameA:'&TestQue2.GroupA.NameA&' ValueA:'&TestQue2.GroupA.ValueA
    END
    IF TestQue2.GroupB.NameB
      str = str &' NameB:'&TestQue2.GroupB.NameB&' ValueB:'&TestQue2.GroupB.ValueB
    END
    str = str &'|'
  END
  MESSAGE(str)

  !Free Queue
  FREE(TestQue2)

Sadly that won’t do as I need the FIELD to have the same name as its marshalled to a struct in C# - like I said, the group itself works - but I am looking how to create a QUEUE out of it so I can keep all active accounts in memory instead of looking them up each time - but perhaps I should just give this up and just look them up when needed as it seems this cannot be done in Clarion as one cannot NEW a GROUP (for which I cannot find the reason)

One option is to use a string field and store the groups as text, using a format like JSON or CSV. It’s trivial to parse the text into a type in C#.

The group works, it does what it needs to - it’s the QUEUE I’m not getting to work as one cannot NEW a group to set the field - using a STRING to get an address works and I can set it - but WHAT and WHO won’t work anymore on the reference group field so I would be needing to hard code the fields instead of relying on a bit of dynamic resolution… So I’ll leave the QUEUE be and just create a GROUP instance where needed till I find a solution