Technical Articles
Reverse Loop finally possible with STEP addition for ABAP Internal Tables
Are you tired of taking the secondary road to your destination? You might feel like this if you’re writing DO
or WHILE
statements with a READ ... INDEX
to loop backwards across the lines of an internal table in ABAP. And if your internal table is a hashed table… you’re almost good to go to turn around. The good thing in brief: You’ve been heard! With SAP BTP ABAP Environment 2202 the new addition STEP
is introduced. STEP
combines the loop order with the step size – you’re getting two in one. If you want to take the main road to your destination, follow this blog to get most out of the new addition.
Basics
While the main use case for STEP
might be the LOOP
statement, there’re more statements, expressions, and operators to use the addition with. But first, we’ll look at the usage of STEP
itself: The loop order and step size are defined by STEP n
where n
has a positive or negative sign for the loop order and where n
is a numeric expression position of operand type i
for the step size. For example, if you wanted to loop across every second table line in a backward order, you’d simply write STEP -2
. Other statements, expressions, and operators to use STEP
with are: FOR ... IN
, DELETE
, INSERT
, APPEND
, NEW
, and VALUE
. We’ll go through them step by step. (Want a quick overview? Scroll down.)
Looping across an internal table
The LOOP
statement and the FOR ... IN
expression can be used with the keywords USING KEY
, FROM
, TO
, and WHERE
. Together with USING KEY
, the processing order of the internal table can be defined. Together with FROM
, TO
, or WHERE
, the subset of lines can be specified. Combining the additions STEP
and WHERE
is possible with the LOOP
statement and the FOR ... IN
expression. In this case, the value of n
can either be 1
or -1
. A reverse processing order can be achieved by the LOOP
statement and the FOR ... IN
expression as well. When combining the additions FROM
and TO
with STEP
, both row numbers need to be adjusted according to the value of n
: If the value of n
is less than 0, the row number of FROM
needs to be greater than the row number of TO
or equal to TO
, for example, FROM 3 TO 1 STEP -1
.
Deleting lines of an internal table
The DELETE
statement can be used with the same keywords as mentioned above: USING KEY
, FROM
, TO
, and WHERE
. The difference is that it’s possible to define the step size but it’s not possible to reverse the processing order for the DELETE
statement using STEP
. Combining the additions STEP
and WHERE
is not possible because at runtime it wouldn’t be clear which condition should be evaluated first.
Adding lines of an internal table to a target table
The APPEND
and INSERT
statements can be combined with the additions FROM
, TO
, and USING KEY
. Adding multiple lines of an internal table to a target table, the same syntax and effect applies as for LOOP
except that the value of n
cannot be negative and no reverse processing order is possible with STEP
.
Adding lines of an internal table to construct a table
The VALUE
and NEW
operators can be used with the same keywords as mentioned above: FROM
, TO
, and USING KEY
. Adding multiple lines of an internal table when constructing an internal table, the same syntax and effect applies as for LOOP
except that the value of n
cannot be negative and no reverse processing order is possible with STEP
.
Use case
Let’s look at some small examples regarding purchase orders and customer benefits and see how STEP
can be used with all the statements, expressions, and operators mentioned above.
Purchase order
Imagine that one part of your work concerns customers and their purchase orders. For inquiries, you’ve prepared a joined table with customer data and purchased item data. This internal table has two customer-related columns that are connected to a customer table and three purchase-related columns that are connected to an order table. The internal table could look as follows.
Customer ID | Customer Name | Item ID | Purchase Date | Processing Date |
00000001 | Customer A | 781029348 | 08.11.2021 | 09.11.2021 |
00000001 | Customer A | 781028275 | 17.11.2021 | 18.11.2021 |
00000001 | Customer A | 781029350 | 03.12.2021 | 06.12.2021 |
00000002 | Customer B | 781029348 | 07.12.2021 | 08.12.2021 |
00000003 | Customer C | 781029353 | 15.12.2021 | 16.12.2021 |
00000004 | Customer D | 781029321 | 15.12.2021 | 16.12.2021 |
00000005 | Customer E | 781029342 | 16.12.2021 | 17.12.2021 |
In the first code section below, we define our playground for the use case. Each example of the use case is represented by a method. The customer_purchase
type definition is used for the internal table of customer data and the constant co_example_customer
is used to represent a specific customer.
CLASS purchase_orders DEFINITION.
PUBLIC SECTION.
TYPES:
BEGIN OF customer_purchase,
cust_id TYPE c LENGTH 8,
cust_name TYPE string,
item_id TYPE c LENGTH 9,
purch_date TYPE datn,
proc_date TYPE datn,
END OF customer_purchase,
customer_purchases TYPE STANDARD TABLE OF customer_purchase
WITH EMPTY KEY.
METHODS constructor.
METHODS purchase_order.
METHODS quarterly_order.
METHODS discount.
METHODS tombola.
PRIVATE SECTION.
CONSTANTS co_example_customer TYPE c LENGTH 8 VALUE '00000001'.
DATA m_customer_purchases TYPE purchase_orders=>customer_purchases.
ENDCLASS.
CLASS purchase_orders IMPLEMENTATION.
"...
ENDCLASS.
START-OF-SELECTION.
DATA(inquiry) = NEW purchase_orders( ).
inquiry->purchase_order( ).
inquiry->quarterly_order( ).
inquiry->discount( ).
inquiry->tombola( ).
cl_demo_output=>display( ).
In the following sample code, we create an internal table and fill it with values to mimic the example scenario. The table and its values are needed for all examples.
CLASS purchase_orders IMPLEMENTATION.
METHOD constructor.
m_customer_purchases = VALUE customer_purchases(
( cust_id = '00000001' cust_name = `Customer A` item_id = '781029348'
purch_date = `20211108` proc_date = `20211109` )
( cust_id = '00000001' cust_name = `Customer A` item_id = '781028275'
purch_date = `20211117` proc_date = `20211118` )
( cust_id = '00000001' cust_name = `Customer A` item_id = '781029350'
purch_date = `20211203` proc_date = `20211206` )
( cust_id = '00000002' cust_name = `Customer B` item_id = '781029348'
purch_date = `20211207` proc_date = `20211208` )
( cust_id = '00000003' cust_name = `Customer C` item_id = '781029353'
purch_date = `20211215` proc_date = `20211216` )
( cust_id = '00000004' cust_name = `Customer D` item_id = '781029321'
purch_date = `20211215` proc_date = `20211216` )
( cust_id = '00000005' cust_name = `Customer E` item_id = '781029342'
purch_date = `20211216` proc_date = `20211217` ) ).
ENDMETHOD.
"...
ENDCLASS.
Imagine now that you receive an inquiry and need to extract all entries of the customer with the ID 00000001, listed from the newest date to the oldest date. For this scenario, you can simply loop backwards with the STEP
addition and a WHERE
condition. The syntax may look like this: LOOP AT itab ASSIGNING FIELD-SYMBOL(<fs>) STEP -1 WHERE cust_id = '00000001'
.
CLASS purchase_orders IMPLEMENTATION.
"...
METHOD purchase_order.
TYPES:
BEGIN OF order,
tabix TYPE sy-tabix,
item_id TYPE c LENGTH 9,
purch_date TYPE datn,
proc_date TYPE datn,
END OF order.
DATA orders TYPE TABLE OF order.
LOOP AT m_customer_purchases REFERENCE INTO DATA(customer_purchase)
STEP -1 WHERE cust_id = co_example_customer.
orders = VALUE #( BASE orders
( tabix = sy-tabix item_id = customer_purchase->item_id
purch_date = customer_purchase->purch_date
proc_date = customer_purchase->proc_date ) ).
ENDLOOP.
cl_demo_output=>write( |Orders of { m_customer_purchases[ cust_id =
co_example_customer ]-cust_name } ({ co_example_customer })| ).
cl_demo_output=>write( orders ).
ENDMETHOD.
"...
ENDCLASS.
Of course, this would work in other ways too, for example, with a SORT
statement. But look at the results, if you don’t use the addition STEP
.
Orders of Customer A (00000001)
TABIX | ITEM_ID | PURCH_DATE | PROC_DATE |
5 | 781029350 | 2021-12-03 | 2021-12-06 |
6 | 781028275 | 2021-11-17 | 2021-11-18 |
7 | 781029348 | 2021-11-08 | 2021-11-09 |
You’ll get the above result when applying the SORT
statement and the one below when using the STEP
addition. The important difference is evident in the column TABIX
which represents the sy-tabix
. Above, the sy-tabix
starts with 5 and ends with 7. Below, the sy-tabix
starts with 3 and ends with 1. Meaning that the addition STEP
doesn’t change the actual sort order of the internal table, but the processing order of the current loop (with its sort order).
Looking at the result of the example, you’ll get information about the customer and their orders sorted from last to first. The sy-tabix
is added here to emphasize the processing order.
Orders of Customer A (00000001)
TABIX | ITEM_ID | PURCH_DATE | PROC_DATE |
3 | 781029350 | 2021-12-03 | 2021-12-06 |
2 | 781028275 | 2021-11-17 | 2021-11-18 |
1 | 781029348 | 2021-11-08 | 2021-11-09 |
The result lists all entries for the customer with the ID 00000001 from the newest to the oldest date. A precondition is that the table is filled in an appending logic; the most recent purchase order is the latest entry. As mentioned above, bear in mind that STEP
can only have the value of 1
or -1
when used together with WHERE
. If you want to test the result using STEP 1
, you don’t have to write it because it’s evaluated implicitly.
Quarterly orders
For a similar scenario, you want to list all purchase orders of all customers in a specific time period from the newest to the oldest date to store statistical data. This can be achieved simply by using a FOR
expression together with the STEP
addition. To make the quarterly selection as smooth as possible, additional code is added here.
CLASS purchase_orders IMPLEMENTATION.
"...
METHOD quarterly_order.
TYPES:
BEGIN OF order,
tabix TYPE sy-tabix,
item_id TYPE c LENGTH 9,
purch_date TYPE datn,
proc_date TYPE datn,
END OF order,
orders TYPE TABLE OF order WITH EMPTY KEY,
BEGIN OF quarter,
quarter_start TYPE datn,
quarter_end TYPE datn,
END OF quarter,
quarters TYPE TABLE OF quarter WITH EMPTY KEY.
DATA quarter_name TYPE string.
DATA(quarters) = VALUE quarters( ( quarter_start = `20211001`
quarter_end = `20211231` ) ).
DATA(quarter) = REF #( quarters[ lines( quarters ) ] ).
DATA(q_start) = quarter->quarter_start.
DATA(q_end) = quarter->quarter_end.
DATA(orders) = VALUE orders( FOR <fs> IN m_customer_purchases
INDEX INTO idx STEP -1 WHERE ( purch_date >= q_start AND purch_date <= q_end )
( tabix = idx item_id = <fs>-item_id
purch_date = <fs>-purch_date
proc_date = <fs>-proc_date ) ).
DATA(quarter_start_month) = substring( val = q_start off = 4 len = 2 ).
CASE quarter_start_month.
WHEN `01`.
quarter_name = `Quarter 1`.
WHEN `04`.
quarter_name = `Quarter 2`.
WHEN `07`.
quarter_name = `Quarter 3`.
WHEN `10`.
quarter_name = `Quarter 4`.
ENDCASE.
DATA(quarter_year) = substring( val = q_start len = 4 ).
cl_demo_output=>write(
|Orders of all customers in { quarter_name } { quarter_year }:| ).
IF orders IS NOT INITIAL.
cl_demo_output=>write( orders ).
ELSE.
cl_demo_output=>write( |No orders found.| ).
ENDIF.
ENDMETHOD.
"...
ENDCLASS.
This time, the FOR
expression is used instead of the LOOP
statement to identify all purchase orders of the last quarter and to see which orders were recently purchased and processed. Both options work the same way.
Executing the method returns a table with all orders from all customers of the fourth quarter of 2021. No customer-specific data is displayed here because the result might be processed for further calculations.
Orders of all customers in Quarter 4 2021:
TABIX | ITEM_ID | PURCH_DATE | PROC_DATE |
7 | 781029342 | 2021-12-16 | 2021-12-17 |
6 | 781029321 | 2021-12-15 | 2021-12-16 |
5 | 781029353 | 2021-12-15 | 2021-12-16 |
4 | 781029348 | 2021-12-07 | 2021-12-08 |
3 | 781029350 | 2021-12-03 | 2021-12-06 |
2 | 781029350 | 2021-11-17 | 2021-11-18 |
1 | 781029350 | 2021-11-08 | 2021-11-09 |
Did you notice that the result table looks the same as the internal table itab
with only one difference?
Discounts
Imagine now that at some point you want to give regular customers a discount for every third order. It’s almost the year’s end and you want to know how many discounts you’ve given to every customer because there’ll be a special give-away for all customers who received at least one discount in the last quarter of the year. If we wanted to get a complete list of granted discounts, we’d simply apply a LOOP AT ... GROUP BY
statement without needing the STEP
addition. That’s why we’ll only take a look at a specific customer, like the one from the previous example with the ID 00000001.
CLASS purchase_orders IMPLEMENTATION.
"...
METHOD discount.
TYPES:
BEGIN OF order,
tabix TYPE sy-tabix,
item_id TYPE c LENGTH 9,
purch_date TYPE datn,
proc_date TYPE datn,
END OF order,
orders TYPE TABLE OF order WITH EMPTY KEY,
BEGIN OF discount_candidate,
tabix TYPE sy-tabix,
item_id TYPE c LENGTH 9,
purch_date TYPE datn,
proc_date TYPE datn,
END OF discount_candidate,
discount_candidates TYPE TABLE OF discount_candidate WITH EMPTY KEY.
DATA(orders) = VALUE orders( FOR <fs> IN m_customer_purchases
INDEX INTO idx STEP -1 WHERE ( cust_id = co_example_customer )
( tabix = idx item_id = <fs>-item_id
purch_date = <fs>-purch_date
proc_date = <fs>-proc_date ) ).
DELETE orders WHERE purch_date <= `20211001` OR purch_date >= `20211231`.
DATA(discounts) = VALUE discount_candidates( ( LINES OF orders FROM 3 STEP 3 ) ).
FINAL(is_discount_granted) = xsdbool( discounts IS NOT INITIAL ).
cl_demo_output=>write( |Discount of { m_customer_purchases[ cust_id =
co_example_customer ]-cust_name } ({ co_example_customer
}) granted: { is_discount_granted }| ).
ENDMETHOD.
"...
ENDCLASS.
The result is presented in a single sentence. A table-like result is applied in the next example of the use case. If you’d generate an output with the sy-tabix
of the discounts
table, the value 1
of the purchase date 2021-11-09
would be returned which again emphasizes that the table keeps the actual order despite being processed backwards.
Discount of Customer A (00000001) granted: X
These examples show use cases of the addition STEP
for looping in reverse order and for looping with a step size greater than 1
, as well as combined. The next and last example for the use case is another usage of STEP
for adding lines of an internal table.
Surprise tombola
After the year’s end, you plan to give every customer who didn’t get a special discount at the year’s end the chance to win a prize at a surprise tombola. Each order represents a lottery ticket. To avoid that customers can win more than once, a LOOP AT ... GROUP BY
statement is used. At this point, STEP
is already used to reduce the number of hits. To ensure that several tombolas can be performed with the same result set, you first list the result in a separate internal table win_candidates
. Afterwards, you determine the winning customers by inserting the rows into the customer_benefit
table and using STEP
again to finalize the result.
CLASS purchase_orders IMPLEMENTATION.
"...
METHOD tombola.
TYPES:
BEGIN OF customer_benefit,
cust_id TYPE c LENGTH 8,
discount_granted TYPE abap_bool,
tombola_won TYPE abap_bool,
END OF customer_benefit,
customer_benefits TYPE STANDARD TABLE OF customer_benefit WITH EMPTY KEY.
DATA(customer_benefit) = VALUE customer_benefits( ).
DATA(win_candidates) = VALUE customer_benefits( ).
DATA(customers_with_discounts) = VALUE customer_benefits(
( cust_id = co_example_customer
discount_granted = abap_true ) ).
LOOP AT m_customer_purchases REFERENCE INTO DATA(tombola) STEP 2
GROUP BY ( cust_id = tombola->cust_id ) REFERENCE INTO DATA(customer).
IF NOT line_exists( customers_with_discounts[ cust_id = customer->cust_id ] ).
INSERT VALUE #( cust_id = customer->cust_id tombola_won = abap_true )
INTO TABLE win_candidates.
ENDIF.
ENDLOOP.
INSERT LINES OF customers_with_discounts INTO TABLE customer_benefit.
INSERT LINES OF win_candidates STEP 2 INTO TABLE customer_benefit.
cl_demo_output=>write( customer_benefit ).
ENDMETHOD.
ENDCLASS.
The INSERT ... LINES OF
statement could be replaced by the VALUE ... LINES OF
statement as well, but it’s shown here to demonstrate the alternative usage.
Data from customers who received a discount is first stored in the internal table customer_benefit
. Afterwards, data from those who didn’t receive a discount is added to the same customer_benefit
table. The number of customers from the second selection is reduced by using STEP
. These second selection represents the winners of the tombola.
CUST_ID | DISCOUNT_GRANTED | TOMBOLA_WON |
00000001 | X | |
00000003 | X |
Quick Check
Get a quick overview of how to apply the keywords with STEP
where o stands for it is possible and x stands for it is not possible.
Keyword | n > 1 | -n | Syntax | Reference |
LOOP |
o* | o | LOOP AT itab ... STEP -1 |
LOOP AT itab |
DELETE |
o | x | DELETE itab ... STEP 2 |
DELETE itab |
INSERT |
o | x | INSERT LINES OF jtab ... STEP 2 |
INSERT itab |
APPEND |
o | x | APPEND LINES OF jtab ... STEP 2 |
APPEND |
FOR ... IN |
o* | o | FOR ... IN itab STEP -1 |
FOR … IN itab |
NEW |
o | x | NEW ... LINES OF jtab ... STEP 2 |
NEW |
VALUE |
o | x | VALUE ... LINES OF jtab ... STEP 2 |
VALUE |
*only without WHERE
condition
Further information
You should now know how to use the new addition STEP
in your projects. Use STEP
for looping backward across the lines of an internal table and for defining a step size. The examples given in this blog are intended for demonstration purposes only. From now on, you may take the short and direct route to your destination instead of taking detours. Go and explore your new shortcut. Are you excited to use the new addition STEP
? Write your thoughts in the comment section. Don’t miss out on new language elements and follow my profile (https://people.sap.com/lenapadeken) for similar blog posts.
Great new feature, very useful examples! Thanks.
Thanks. I understand that STEP -1 is useful, but I almost never needed anything else than +1 or -1. As you say "you’ve been heard", I guess I can ask something like UP TO n ROWS for the future, and maybe SAP could implement SQL-like notation for internal tables, like ORDER BY table_index DESCENDING and more 😉 (I'm not confident in the performance of SELECT ... FROM @itab ...)
You can be confident as long as the SQL statement can be executed in the table buffer and no push down to the database is necessary.
Interesting.
I must confess that at first I was surprised to see that the reversed order applies also for hashed table/secondary index. I was even more surprised to find out that SORT statement sorts also hashed tables.
As written in the documentation ... 😉
Great stuff. Thanks for sharing.
What's this about?
Looks like static single assignment as wished here ?
A small hint of what may come next... 😉
Yes exactly, that will be ABAP's answer to the request for the static single assignment. When using FINAL(var), var is read-only in the rest of its context. We call that an "immuatable variable".
Amazing. How does that work for references then? Is just the reference immutable or also the data/object pointed to? My guess would be the former.
Sure, like for any assignment.
Ooh, now that's even better news than STEP IMHO 🙂
But... why FINAL? FINAL already has a very different compile-time meaning with classes and methods so is a little confusing.
CONST(var) is more intuitive and is something we're also used to from other languages. Expanding a CONSTANTS statement to allow run-time assignment is also intuitive (and something I've often wanted to do), and it means one less keyword to learn.
It is not really const. Within a loop FINAL(...) can assign another value.
Ugh, ABAP's pseudo-block-scope topic again. 🙂
This makes sense in a block scope, and is exactly how JS works.
ABAP pretends to do block scoping in some scenarios, so why not here? A DATA() constructor can occur within a loop. And as you are aware we have the this peculiar behaviour where any of the commented lines will generate an error both inside and outside the loop:
I'm all for immutability, but if not CONSTANT then maybe call it PROTECTED, FIXED or something more intuitive? Or just CONST, which is already different from CONSTANTS?
FINAL just seems weird on several levels.
Agree completely. Explaining this to both newbies and experienced ABAPers will be awkward...
I agree that keyword FINAL is already used in classes and it should've been another keyword. And now it seems like you're saying if FINAL is placed in a LOOP then its value will keep changing. So, it's not even "final", technically?
Hello Jelena,
for me it makes completely sense. You can also need an immutable variable inside a loop. The context where the variable is defined is crucial.
Regards PP
Link to the blog post about FINAL posted today: https://blogs.sap.com/2022/10/25/finally-a-declaration-of-immutable-variables-with-final-in-abap
Thank you for the blog