Wednesday, 20 July 2022

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.

SAP ABAP Exam Prep, SAP ABAP Tutorial and Materials, SAP ABAP Career, SAP ABAP Jobs, SAP ABAP Learning, SAP ABAP Prep, SAP ABAP Preparation

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*  LOOP AT itab ... STEP -1  LOOP AT itab 
DELETE  DELETE itab ... STEP 2  DELETE itab 
INSERT  INSERT LINES OF jtab ... STEP 2  INSERT itab 
APPEND  APPEND LINES OF jtab ... STEP 2  APPEND 
FOR ... IN   o*  FOR ... IN itab STEP -1  FOR … IN itab 
NEW  NEW ... LINES OF jtab ... STEP 2  NEW 
VALUE  o VALUE ... LINES OF jtab ... STEP 2  VALUE

*only without WHERE condition

No comments:

Post a Comment