2026-03-18
LOCATE, INSERT and DELETE
BASIC is a simple language and most of it can be picked up in quick order, however I was never quite comfortable with LOCATE, INSERT and DELETE trio.
LOCATE does what it says on the tin, which is it locates a value inside a multivalue string and you can set at which level to do the search on.
Multivalue has 3 levels and you can locate on all 3 of them and so these functions are quite flexible. I often would roughly guess the syntax and then a few test routines would make sure it's doing what I want. However now I've built up a conceptual model of what's going on.
Scenerio
I can only think in business terms because my creativity is basically zeroed out so for an example, lets group a set of 10 invoices by their customer.
There are 3 customers and 10 invoices and so the data may look something like:
Invoice | Customer
4914 | 15
3738 | 80
3274 | 76
2424 | 76
4774 | 80
3642 | 80
4290 | 15
2203 | 80
1810 | 15
2785 | 76
In my program, the data will look like:
INVOICE.LIST = ''
*
INVOICE.LIST<1,-1> = '4914'
INVOICE.LIST<1,-1> = '3738'
INVOICE.LIST<1,-1> = '3274'
INVOICE.LIST<1,-1> = '2424'
INVOICE.LIST<1,-1> = '4774'
INVOICE.LIST<1,-1> = '3642'
INVOICE.LIST<1,-1> = '4290'
INVOICE.LIST<1,-1> = '2203'
INVOICE.LIST<1,-1> = '1810'
INVOICE.LIST<1,-1> = '2785'
*
INVOICE.LIST<2,-1> = '15'
INVOICE.LIST<2,-1> = '80'
INVOICE.LIST<2,-1> = '76'
INVOICE.LIST<2,-1> = '76'
INVOICE.LIST<2,-1> = '80'
INVOICE.LIST<2,-1> = '80'
INVOICE.LIST<2,-1> = '15'
INVOICE.LIST<2,-1> = '80'
INVOICE.LIST<2,-1> = '15'
INVOICE.LIST<2,-1> = '76'
We have a multivalue list where the first attribute contains the invoice numbers and the second attribute contains the customers.
The goal is to create a new list where we have 3 customers in the first attribute and we have the 10 invoices in the second attribute where each multivalue contains a subvalued list of invoices.
The second attribute would therefore contain both value marks and subvalue marks.
LOCATE
First thing is the syntax:
LOCATE(NEEDLE,HAYSTACK;NEEDLE.POS)
LOCATE(NEEDLE,HAYSTACK,1;NEEDLE.POS)
LOCATE(NEEDLE,HAYSTACK,1,1;NEEDLE.POS)
We have a few ways of using the LOCATE statement and we have even more ways to use it when we add in dimensioned arrays and dynamic arrays.
However the fundemental idea is that we are searching a variable for a specific value at a specific level.
Attribute Mark Level
Let's look at the simplest example, we want to create a list of customers that are attribute marked.
LOCATE(NEEDLE,HAYSTACK;NEEDLE.POS)
The program is:
NUMBER.OF.INVOICES = DCOUNT(INVOICE.LIST<1>,@VM)
*
CUSTOMER.INVOICE.LIST = ''
*
FOR I = 1 TO NUMBER.OF.INVOICES
INVOICE.ID = INVOICE.LIST<1,I>
CUSTOMER.ID = INVOICE.LIST<2,I>
*
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST;CUSTOMER.POS) ELSE
CUSTOMER.INVOICE.LIST<-1> = CUSTOMER.ID
END
*
NEXT I
*
PRINT CUSTOMER.INVOICE.LIST
*
* END OF PROGRAM.
*
END
*
We loop on the invoices and we get the invoice id and customer id.
We have a CUSTOMER.INVOICE.LIST which will be our organized list. We search forthe customer id inside that list and if we fail to find it, we then add the id. The ELSE clause is how we de-duplicate the customers.
This will output:
15þ80þ76
Value Mark Level
Above, our CUSTOMER.INVOICE.LIST is a dynamic array. By populating our customers as attributes, we can't actually have associated data in this list now.
We need to make the customers value marked and in the first attribute. The second attribute will then carry the invoices.
We can use the next variation of the LOCATE statement:
LOCATE(NEEDLE,HAYSTACK,1;NEEDLE.POS)
We need to make some small changes to the existing program:
NUMBER.OF.INVOICES = DCOUNT(INVOICE.LIST<1>,@VM)
*
CUSTOMER.INVOICE.LIST = ''
*
FOR I = 1 TO NUMBER.OF.INVOICES
INVOICE.ID = INVOICE.LIST<1,I>
CUSTOMER.ID = INVOICE.LIST<2,I>
*
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST,1;CUSTOMER.POS) ELSE
CUSTOMER.INVOICE.LIST<1,-1> = CUSTOMER.ID
END
*
NEXT I
*
PRINT CUSTOMER.INVOICE.LIST
*
The difference here is that we now specify the attribute we will be searching, CUSTOMER.POS will tell us the position if the value was found other wise it will be the last position.
In this example we are searching the first attribute.
This will output:
15ý80ý76
If we wanted to put this in the second attribute for some reason, we would do:
NUMBER.OF.INVOICES = DCOUNT(INVOICE.LIST<1>,@VM)
*
CUSTOMER.INVOICE.LIST = ''
*
FOR I = 1 TO NUMBER.OF.INVOICES
INVOICE.ID = INVOICE.LIST<1,I>
CUSTOMER.ID = INVOICE.LIST<2,I>
*
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST,2;CUSTOMER.POS) ELSE
PRINT CUSTOMER.POS
CUSTOMER.INVOICE.LIST<2,-1> = CUSTOMER.ID
END
*
NEXT I
*
PRINT CUSTOMER.INVOICE.LIST
This is straightforward. We specify the attribute we want to LOCATE in and then get the value mark position.
This will result in:
þ15ý80ý76
We have the customers with value marks in the second attribute.
However we can also write this in a different way and I think this is where my confusion stemmed from. I've seen both the above style and below style:
NUMBER.OF.INVOICES = DCOUNT(INVOICE.LIST<1>,@VM)
*
CUSTOMER.INVOICE.LIST = ''
*
FOR I = 1 TO NUMBER.OF.INVOICES
INVOICE.ID = INVOICE.LIST<1,I>
CUSTOMER.ID = INVOICE.LIST<2,I>
*
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST<2>,1;CUSTOMER.POS) ELSE
CUSTOMER.INVOICE.LIST<2,-1> = CUSTOMER.ID
END
*
NEXT I
*
PRINT CUSTOMER.INVOICE.LIST
We can use CUSTOMER.INVOICE.LIST<2> directly and search that specific level by passing it in as the variable.
Now our specification of the positions changes. Instead of 2, we use a 1.
The core idea here is that we pass in the variable we want to do the LOCATE against and the LOCATE is done against that variable.
Here, we passed in attribute 2 of CUSTOMER.INVOICE.LIST and so when we do the locate we simply need to use 1 for the attribute position. This is because when we passed in the variable, there is only a single attribute.
This means that we can pass in subsets of our dynamic and dimensioned arrays and the LOCATE will treat that subset as if it was the only thing it has access to.
The 2 examples above are functionally equivelent but I'd be curious to know if the variable is being passed by reference or by value.
If it's by reference, then the 2 LOCATES work identically regardless of what we pass in. However if it's by value, there is a memory overhead. Passing in a large list and doing a LOCATE would be expensive while passing in a subset could be much cheaper.
This idea of passing in the variable and only the variable is central to the LOCATE, INSERT and DELETE statements that I never fully internalized before.
Subvalue level
For fun, let's also put the customers in attribute 3, field 4. The goal is to have the customers be subvalued.
First, the syntax:
LOCATE(NEEDLE,HAYSTACK,1,1;NEEDLE.POS)
This is largely pointless here as similar to the attribute mark list, we would have no way of grouping things lower (though we could use subtext marks and text marks).
The program:
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST,3,4;CUSTOMER.POS) ELSE
CUSTOMER.INVOICE.LIST<3,4,-1> = CUSTOMER.ID
END
We are passing in the entire CUSTOMER.INVOICE.LIST and setting the attribute and field positions and we are going to get the CUSTOMER.POS as the subvalue position.
When we add the CUSTOMER.ID, we make sure to set the right part of our CUSTOMER.INVOICE.LIST.
Alternatively we can also write this LOCATE as:
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST<3>,1,4;CUSTOMER.POS) ELSE
CUSTOMER.INVOICE.LIST<3,4,-1> = CUSTOMER.ID
END
This example passes in attribute 3 directly to the location.
We can also do:
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST<3,4>,1,1;CUSTOMER.POS) ELSE
CUSTOMER.INVOICE.LIST<3,4,-1> = CUSTOMER.ID
END
Here we pass in attribute 3, field 4 directly. This means we need to set the attribute and field to 1 for the LOCATE as otherwise it will search incorrectly.
All 3 versions will output the same thing, however each of them is passing a different thing to LOCATE.
The output:
þþýýý15ü80ü76
We have our customers subvalued in attribute 3, field 4.
Final Program
Now with the various examples out of the way, let's write the final program that will have the customers in attribute 1 with value marks and attribute 2 with the invoices grouped by customer.
CUSTOMER.INVOICE.LIST = ''
*
FOR I = 1 TO NUMBER.OF.INVOICES
INVOICE.ID = INVOICE.LIST<1,I>
CUSTOMER.ID = INVOICE.LIST<2,I>
*
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST<1>,1;CUSTOMER.POS) THEN
CUSTOMER.INVOICE.LIST<2,CUSTOMER.POS,-1> = INVOICE.ID
*
END ELSE
CUSTOMER.INVOICE.LIST<1,-1> = CUSTOMER.ID
CUSTOMER.INVOICE.LIST<2,-1> = INVOICE.ID
END
*
NEXT I
*
PRINT CUSTOMER.INVOICE.LIST
I am passing in attribute 1 directly into the LOCATE. I will then get CUSTOMER.POS if the customer was found. If the customer was found, then I can update attribute 2 and the field CUSTOMER.POS with the invoice id. I add it at the subvalue level.
If the customer wasn't found, then I need to add the customer and I need to add the invoice. Here I can add just at the field level as there is nothing here yet.
I could do:
END ELSE
CUSTOMER.INVOICE.LIST<1,CUSTOMER.POS,1> = CUSTOMER.ID
CUSTOMER.INVOICE.LIST<2,CUSTOMER.POS,1> = INVOICE.ID
END
This would use the CUSTOMER.POS that was returned. When a LOCATE fails, the returned position will be the place where something should go, in this case the end of the variable.
The output of our program is:
15ý80ý76þ4914ü4290ü1810ý3738ü4774ü3642ü2203ý3274ü2424ü2785
And to make it a bit more clear:
15ý80ý76
4914ü4290ü1810ý3738ü4774ü3642ü2203ý3274ü2424ü2785
Attribute 1 is indeed just our customers.
Attribute 2 has 3 sets of invoices. Invoices are separated by subvalue marks while groups are separated by value marks.
With that we have covered all of the variations and working of LOCATEs.
INSERT
INSERT, like LOCATE, has multiple syntaxes depending on where you are inserting something:
INSERT(HAYSTACK,NEEDLE.POS;NEEDLE)
INSERT(HAYSTACK,1,NEEDLE.POS;NEEDLE)
INSERT(HAYSTACK,1,1,NEEDLE.POS;NEEDLE)
INSERT takes in a variable, HAYSTACK and places the NEEDLE at the specified position.
The most important thing to rememeber is that INSERT creates a copy and returns that copy. You aren't actually changing the HAYSTACK variable. The return value of the INSERT needs to be used or your insertion won't actually do anything.
INSERT and DELETE are most useful inside a LOCATE as we can do sorting this way. However we can use these statements without a LOCATE.
HAYSTACK = ''
X = INSERT(HAYSTACK,2;'Hello')
*
PRINT HAYSTACK
PRINT X
This will output:
þHello
As you can see, we insert into the HAYSTACK, however the INSERT returned a result that was placed in X.
What we need to do is:
HAYSTACK = ''
HAYSTACK = INSERT(HAYSTACK,2;'Hello')
PRINT HAYSTACK
Now when we INSERT, we also updating the HAYSTACK variable.
Now we should see:
þHello
Similar to LOCATE, we can also pass in subset of a dynamic array.
Right now, this example is doing the following:
1. Taking in the whole `HAYSTACK`
2. Making a copy
3. Inserting the element
4. Returning the copy
5. Overwriting the original with the copy
We could change this to:
HAYSTACK = ''
HAYSTACK<2> = INSERT(HAYSTACK<2>,1;'Hello')
PRINT HAYSTACK
This is functionally the same and the same steps are being followed, however the variable being copied and return could be vastly smaller.
By specifically saying attribute 2 and passing that in, we are only duplicating that attribute.
Attribute Mark Level
Let's build our customer list at the attribute mark level:
CUSTOMER.INVOICE.LIST = ''
*
CUSTOMER.INVOICE.LIST = INSERT(CUSTOMER.INVOICE.LIST,1;'15')
CUSTOMER.INVOICE.LIST = INSERT(CUSTOMER.INVOICE.LIST,2;'80')
CUSTOMER.INVOICE.LIST = INSERT(CUSTOMER.INVOICE.LIST,3;'76')
*
PRINT CUSTOMER.INVOICE.LIST
*
* END OF PROGRAM.
*
END
*
We will then get:
15þ80þ76
Value Mark level
Let's build the customer list in attribute 2 with value marks:
CUSTOMER.INVOICE.LIST = ''
*
CUSTOMER.INVOICE.LIST<2> = INSERT(CUSTOMER.INVOICE.LIST<2>,1,1;'15')
CUSTOMER.INVOICE.LIST<2> = INSERT(CUSTOMER.INVOICE.LIST<2>,1,2;'80')
CUSTOMER.INVOICE.LIST<2> = INSERT(CUSTOMER.INVOICE.LIST<2>,1,3;'76')
*
PRINT CUSTOMER.INVOICE.LIST
*
* END OF PROGRAM.
*
END
*
Here I'm passing in attribute 2 specifically. The attribute mark position is in reference to the variable passed in and because I'm passing in a whole attribute only, this will be 1. The field positions are being manually set so I can add the 3 customers.
I could change this to update the entire CUSTOMER.INVOICE.LIST and then I would set the attribute mark position to 2.
CUSTOMER.INVOICE.LIST = ''
*
CUSTOMER.INVOICE.LIST = INSERT(CUSTOMER.INVOICE.LIST,2,1;'15')
CUSTOMER.INVOICE.LIST = INSERT(CUSTOMER.INVOICE.LIST,2,2;'80')
CUSTOMER.INVOICE.LIST = INSERT(CUSTOMER.INVOICE.LIST,2,3;'76')
*
This would work as well. However the first example is better in my eyes.
Both of these programs will output:
þ15ý80ý76
Subvalue Mark Level
Now let's put the customers at attribute 3, field 4 and have them be subvalued:
CUSTOMER.INVOICE.LIST = ''
*
CUSTOMER.INVOICE.LIST<3,4> = INSERT(CUSTOMER.INVOICE.LIST<3,4>,1,1,1;'15')
CUSTOMER.INVOICE.LIST<3,4> = INSERT(CUSTOMER.INVOICE.LIST<3,4>,1,1,2;'80')
CUSTOMER.INVOICE.LIST<3,4> = INSERT(CUSTOMER.INVOICE.LIST<3,4>,1,1,3;'76')
*
This will copy and update the minimal amount of information.
Alternatively we can also just update the entire field:
CUSTOMER.INVOICE.LIST = ''
*
CUSTOMER.INVOICE.LIST<3> = INSERT(CUSTOMER.INVOICE.LIST<3>,1,4,1;'15')
CUSTOMER.INVOICE.LIST<3> = INSERT(CUSTOMER.INVOICE.LIST<3>,1,4,2;'80')
CUSTOMER.INVOICE.LIST<3> = INSERT(CUSTOMER.INVOICE.LIST<3>,1,4,3;'76')
*
This is a bit more wasteful.
Lastly, we can also update the entire variable:
CUSTOMER.INVOICE.LIST = ''
*
CUSTOMER.INVOICE.LIST = INSERT(CUSTOMER.INVOICE.LIST,3,4,1;'15')
CUSTOMER.INVOICE.LIST = INSERT(CUSTOMER.INVOICE.LIST,3,4,2;'80')
CUSTOMER.INVOICE.LIST = INSERT(CUSTOMER.INVOICE.LIST,3,4,3;'76')
*
This is creating a copy of the entire list for every insert.
All 3 examples will output the same thing:
þþýýý15ü80ü76
Sorting
Now that we have LOCATE and INSERT, we have the functions needed to do sorting.
Previously I showed the LOCATE syntax but I left a crucial part of it, we can also do a LOCATE where we pass in the order to look in.
SORT.ORDER:
'AL' - Ascending Left Justified
'AR' - Ascending Right Justified
'DL' - Descending Left Justified
'DR' - Descending Right Justified
For numeric data, we want to make the numbers right justified, for alpha data, we want to sort left justified. We also can do the sort ascending or descending.
We pass these sort options in by doing:
LOCATE(NEEDLE,HAYSTACK;NEEDLE.POS;SORT.ORDER)
LOCATE(NEEDLE,HAYSTACK,1;NEEDLE.POS;SORT.ORDER)
LOCATE(NEEDLE,HAYSTACK,1,1;NEEDLE.POS;SORT.ORDER)
When we pass in a sort order, the LOCATE statement will be much faster, previouisly we didn't use a sort and so the search was always O(n), meaning that in the worst case, LOCATE needs to look at every single element.
In a sorted list, LOCATE is actually O(log n), this is because it becomes a binary search once the data is marked as sorted.
LOCATE still works the same way where it returns the NEEDLE.POS but if a value is not found, we actually get the location of where that data should be.
By using LOCATE and INSERT together we can build a sorted list.
...
INVOICE.LIST<2,-1> = '15'
INVOICE.LIST<2,-1> = '76'
*
NUMBER.OF.INVOICES = DCOUNT(INVOICE.LIST<1>,@VM)
*
CUSTOMER.INVOICE.LIST = ''
*
FOR I = 1 TO NUMBER.OF.INVOICES
INVOICE.ID = INVOICE.LIST<1,I>
CUSTOMER.ID = INVOICE.LIST<2,I>
*
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST<1>,1;CUSTOMER.POS;'AR') THEN
CUSTOMER.INVOICE.LIST<2,CUSTOMER.POS,-1> = INVOICE.ID
*
END ELSE
CUSTOMER.INVOICE.LIST<1> = INSERT(CUSTOMER.INVOICE.LIST<1>,1,CUSTOMER.POS;CUSTOMER.ID)
CUSTOMER.INVOICE.LIST<2> = INSERT(CUSTOMER.INVOICE.LIST<2>,1,CUSTOMER.POS;INVOICE.ID)
END
*
NEXT I
*
PRINT CUSTOMER.INVOICE.LIST
*
* END OF PROGRAM.
*
END
*
The core change here was in the LOCATE statement. I added the sort order as AR. Now LOCATE will search with the sort order and if a customer isn't found it will give me the place to put that customer.
When I go to add that customer, I can then use the INSERT statement with that position to place the customer in the right spot.
This will result in the customers being sorted. However we also need to insert the invoice into the right spot as well so that it can be grouped properly.
This is why we have 2 inserts to the same list but we pass in different parts of the CUSTOMER.INVOICE.LIST. I could change this so we update the entire CUSTOMER.INVOICE.LIST but this would be wasteful as I would create many copies to build this sorted list.
Now let's add sorting to the invoices as well, I want to sort thee invoices that are already grouped.
CUSTOMER.INVOICE.LIST = ''
*
FOR I = 1 TO NUMBER.OF.INVOICES
INVOICE.ID = INVOICE.LIST<1,I>
CUSTOMER.ID = INVOICE.LIST<2,I>
*
LOCATE(CUSTOMER.ID,CUSTOMER.INVOICE.LIST<1>,1;CUSTOMER.POS;'AR') THEN
LOCATE(INVOICE.ID,CUSTOMER.INVOICE.LIST<2,CUSTOMER.POS>,1,1;INVOICE.POS;'AR') ELSE
CUSTOMER.INVOICE.LIST<2,CUSTOMER.POS> = INSERT(CUSTOMER.INVOICE.LIST<2,CUSTOMER.POS>,1,1,INVOICE.POS;INVOICE.ID)
END
*
END ELSE
CUSTOMER.INVOICE.LIST<1> = INSERT(CUSTOMER.INVOICE.LIST<1>,1,CUSTOMER.POS;CUSTOMER.ID)
CUSTOMER.INVOICE.LIST<2> = INSERT(CUSTOMER.INVOICE.LIST<2>,1,CUSTOMER.POS;INVOICE.ID)
END
*
NEXT I
*
PRINT CUSTOMER.INVOICE.LIST
Now when a customer is found in the CUSTOMER.INVOICE.LIST, instead of appending the invoice, I do a LOCATE for that invoice id in that customer's invoices.
We shouldn't have any duplicate invoices, so I have just the ELSE clause but the LOCATE will return me the position that the invoice should go.
With the INVOICE.POS and the CUSTOMER.POS, we now have enough information to insert the invoice into the right group and in the right position.
This should give us the following output:
15ý76ý80
1810ü4290ü4914ý2424ü2785ü3274ý2203ü3642ü3738ü4774
Attribute 1 is the sorted customer list, attribute 2 is the invoices for each customer sorted within the groups.
There are a number of ways this logic could be writtern as we could pass things differently. For example we could pass in the entire CUSTOMER.INVOICE.LIST and specify the attribute and fields in the functions. This way we would update the entire CUSTOMER.INVOICE.LIST each time we do an insert.
The way we've done it here is that we only update the subset that change, this is the better solution.
DELETE
The Full Program