RSV-Challenge icon indicating copy to clipboard operation
RSV-Challenge copied to clipboard

Add support for ABAP

Open Stenway opened this issue 5 months ago • 2 comments

ABAP implementation requested

Stenway avatar Jan 10 '24 10:01 Stenway

Hi, this should do the trick. I'm pretty sure most people working in SAP have access to a system with MM and therefore the MARA table for the demo code at the bottom, and even if they dont, they can just swap out some the SQL and use whatever other table they fancy.

Keep in mind that this implementation is fairly slow because SAP doesnt easily support the packing of strings into bytestrings (and vice versa). Ive played around with evil SYSTEM-CALL wizardry that can make reading and writing the data faster (albeit at the cost of not packing the binary, leaving the "00" at the end of most character's byte representations and thus almost doubling the total size of the binary), but that would probably never make it to production code, unless someone official from SAP wants to start supporting RSV, because using those commands is (officially speaking) forbidden in client code.

As for the "non-tabular data" in the specification, you can't really represent that in ABAP on a technical level because its a very statically typed language, so just creating a data object that dynamically fits "non-tabular data" is an annoying task because you'd have to create a whole program at runtime that exactly defines the specific data you want to put in it, which completely defeats the purpose of it all. So yeah, I kinda had to omit that, because there really isn't a clever workaround that could be used en masse.

I also made sure to overengineer the decoder a little so it may use the header names for the correct mapping to the corresponding fields in the target table.

As for compatibility, this works all the way down until 7.31. That's the oldest ABAP version I can work on.

REPORT.
CLASS lcl_rsv DEFINITION.
  PUBLIC SECTION.
    "I'm just gonna assume whoever calls this isnt giving me something stupid like:
    " - a nested table
    " - reference types as columns
    " - an elementary line type (like, just make a structure with one element if you need that, sheesh)
    " - the wrong parameters regarding whether or not there is or should be a header
    CLASS-METHODS:
      encode
        IMPORTING
          it_data       TYPE INDEX TABLE
          iv_codepage   TYPE abap_encod DEFAULT '4110' "UTF-8
          iv_add_header TYPE boolean OPTIONAL
        EXPORTING
          ev_result     TYPE xstring,
      decode
        IMPORTING
          iv_rsv        TYPE xsequence
          iv_codepage   TYPE cpcodepage DEFAULT '4110' "UTF-8
          iv_has_header TYPE boolean OPTIONAL
          iv_use_header TYPE boolean OPTIONAL
        EXPORTING
          et_data       TYPE INDEX TABLE.
  PRIVATE SECTION.
    CLASS-METHODS:
      get_flat_table_components
        IMPORTING
          it_data       TYPE INDEX TABLE
        EXPORTING
          et_components TYPE abap_component_tab.
    CONSTANTS: BEGIN OF gc_byte,
                 eov  TYPE x LENGTH 1 VALUE 255, "End of value
                 null TYPE x LENGTH 1 VALUE 254, "Only useful for decoding, there is no nullable non-reference type in ABAP
                 eol  TYPE x LENGTH 1 VALUE 253, "End of line
               END OF gc_byte.
ENDCLASS.
CLASS lcl_rsv IMPLEMENTATION.
  METHOD encode.
    DATA: lt_components     TYPE abap_component_tab,
          lv_string         TYPE string,
          lv_xstring        TYPE xstring,
          lv_num_components TYPE i.

    FIELD-SYMBOLS: <ls_component> LIKE LINE OF lt_components,
                   <gv_string>    TYPE any,
                   <gv_xstring>   TYPE any,
                   <is_data>      TYPE any,
                   <iv_any>       TYPE any.

    DEFINE push_to_xstring.
      lv_string = &1.
      lv_xstring = cl_bcs_convert=>string_to_xstring( iv_codepage = iv_codepage iv_string = lv_string ).
    END-OF-DEFINITION.

    CLEAR ev_result.

    "Get the columns
    get_flat_table_components(
      EXPORTING
        it_data       = it_data
      IMPORTING
        et_components = lt_components ).

    "Optional header
    IF iv_add_header = abap_true.
      LOOP AT lt_components ASSIGNING <ls_component>.
        push_to_xstring <ls_component>-name.
        CONCATENATE ev_result lv_xstring gc_byte-eov INTO ev_result IN BYTE MODE.
      ENDLOOP.
      CONCATENATE ev_result gc_byte-eol INTO ev_result IN BYTE MODE.
    ENDIF.

    "Now the data
    lv_num_components = lines( lt_components ).
    CLEAR lt_components.
    LOOP AT it_data ASSIGNING <is_data>.
      DO lv_num_components TIMES.
        ASSIGN COMPONENT sy-index OF STRUCTURE <is_data> TO <iv_any>.
        push_to_xstring <iv_any>.
        CONCATENATE ev_result lv_xstring gc_byte-eov INTO ev_result IN BYTE MODE.
      ENDDO.
      CONCATENATE ev_result gc_byte-eol INTO ev_result IN BYTE MODE.
    ENDLOOP.
  ENDMETHOD.
  METHOD decode.
    DATA: lt_components      TYPE abap_component_tab,
          lv_num_components  TYPE i,
          lt_rsv             TYPE TABLE OF xstring,
          lt_line_values     TYPE TABLE OF xstring,
          lt_mixmap          TYPE TABLE OF i,
          lv_string          TYPE string,
          lv_xstring         TYPE xstring.

    FIELD-SYMBOLS: <gv_string>  TYPE any,
                   <gv_xstring> TYPE any,
                   <lv_rsv>     TYPE xstring,
                   <lv_value>   TYPE xstring,
                   <es_data>    TYPE any,
                   <ev_any>     TYPE any,
                   <lv_mixmap>  LIKE LINE OF lt_mixmap.

    DEFINE push_to_string.
      lv_xstring = &1.
      lv_string = cl_bcs_convert=>xstring_to_string( iv_cp = iv_codepage iv_xstr = lv_xstring ).
    END-OF-DEFINITION.

    CLEAR et_data.

    "Get the columns
    get_flat_table_components(
      EXPORTING
        it_data       = et_data
      IMPORTING
        et_components = lt_components ).

    "Get the basic table form
    SPLIT iv_rsv AT gc_byte-eol INTO TABLE lt_rsv IN BYTE MODE.

    "Delete the header line before processing the data
    IF iv_use_header = abap_true OR iv_has_header = abap_true.
      "Decode the header line and then create the mapping table
      IF iv_use_header = abap_true.
        READ TABLE lt_rsv INDEX 1 ASSIGNING <lv_rsv>.
        SPLIT <lv_rsv> AT gc_byte-eov INTO TABLE lt_line_values IN BYTE MODE.
        LOOP AT lt_line_values ASSIGNING <lv_value>.
          push_to_string <lv_value>.
          READ TABLE lt_components TRANSPORTING NO FIELDS WITH KEY name = lv_string.
          IF sy-subrc = 0.
            APPEND sy-tabix TO lt_mixmap.
          ELSE.
            APPEND INITIAL LINE TO lt_mixmap.
          ENDIF.
          DELETE lt_line_values.
        ENDLOOP.
      ENDIF.
      DELETE lt_rsv INDEX 1.
    ENDIF.

    "Now the data
    lv_num_components = lines( lt_components ).
    CLEAR lt_components.

    "Unchanged order, make a simple mapping table
    IF lt_mixmap IS INITIAL.
      DO lv_num_components TIMES.
        APPEND sy-index TO lt_mixmap.
      ENDDO.
    ENDIF.

    "Failsafe if someone actually wants to add nulls (which are the same as empty values to us)
    REPLACE ALL OCCURRENCES OF gc_byte-null IN TABLE lt_rsv WITH gc_byte-eov IN BYTE MODE.

    "Start mapping the data around
    LOOP AT lt_rsv ASSIGNING <lv_rsv>.
      SPLIT <lv_rsv> AT gc_byte-eov INTO TABLE lt_line_values IN BYTE MODE.
      APPEND INITIAL LINE TO et_data ASSIGNING <es_data>.
      LOOP AT lt_mixmap ASSIGNING <lv_mixmap> WHERE table_line IS NOT INITIAL.
        ASSIGN COMPONENT <lv_mixmap> OF STRUCTURE <es_data> TO <ev_any>.
        READ TABLE lt_line_values INDEX sy-tabix ASSIGNING <lv_value>.
        push_to_string <lv_value>.
        <ev_any> = lv_string.
      ENDLOOP.
      DELETE lt_rsv.
    ENDLOOP.
  ENDMETHOD.
  METHOD get_flat_table_components.
    DATA: lo_tabledescr         TYPE REF TO cl_abap_tabledescr,
          lo_structdescr        TYPE REF TO cl_abap_structdescr,
          lo_includedescr       LIKE lo_structdescr,
          lt_include_components LIKE et_components.

    FIELD-SYMBOLS <es_component> LIKE LINE OF et_components.

    lo_tabledescr ?= cl_abap_tabledescr=>describe_by_data( it_data ).
    lo_structdescr ?= lo_tabledescr->get_table_line_type( ).
    et_components = lo_structdescr->get_components( ).
    LOOP AT et_components ASSIGNING <es_component> WHERE as_include = abap_true.
      lo_includedescr ?= <es_component>-type.
      lt_include_components = lo_includedescr->get_components( ).
      DELETE et_components.
      INSERT LINES OF lt_include_components INTO et_components INDEX sy-tabix.
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.
  "Use whatever table you like to test it, i suppose something like a BI system doesnt have the MARA
  DATA: gt_mara   TYPE TABLE OF mara,
        gv_result TYPE xstring.

  SELECT * FROM mara UP TO 300 ROWS INTO TABLE gt_mara.

  lcl_rsv=>encode(
    EXPORTING
      it_data       = gt_mara
      iv_add_header = abap_true
    IMPORTING
      ev_result     = gv_result ).
  CLEAR gt_mara.

  "If it properly decodes the data should land back here in gt_mara
  lcl_rsv=>decode(
    EXPORTING
      iv_rsv        = gv_result
      iv_has_header = abap_true
    IMPORTING
      et_data       = gt_mara ).

  "Bonus test: mixed order.
  TYPES: BEGIN OF gy_weird_mara,
           mstae TYPE mara-mstae,
           matnr TYPE matnr,
           loekz TYPE mara-lvorm,
         END OF gy_weird_mara.
  DATA gt_weird_mara TYPE TABLE OF gy_weird_mara.
  lcl_rsv=>decode(
    EXPORTING
      iv_rsv        = gv_result
      iv_use_header = abap_true
    IMPORTING
      et_data       = gt_weird_mara ).

  BREAK-POINT.

Wralth avatar Jan 10 '24 16:01 Wralth

I always wanted to take a stab at a coding challenge. I wrote this up on a sandbox system running Netweaver 7.50. There is no error checking if the input is bad or good. But I did test it with the examples from the spec. My goal was just to keep it simple looking.

CLASS zcl_rsv_util DEFINITION
  PUBLIC FINAL
  CREATE PUBLIC.

  PUBLIC SECTION.
    CONSTANTS end_of_value TYPE xstring VALUE 'FF'.
    CONSTANTS null_value   TYPE xstring VALUE 'FE'.
    CONSTANTS end_of_row   TYPE xstring VALUE 'FD'.

    DATA rsv         TYPE STANDARD TABLE OF xstringtab.
    DATA rsv_decoded TYPE STANDARD TABLE OF stringtab.

    TYPES encode_tab TYPE STANDARD TABLE OF stringtab.

    METHODS decode_rsv
      IMPORTING rsv_xstring TYPE xstring.

    METHODS encode_rsv
      IMPORTING encode_tab         TYPE encode_tab
      RETURNING VALUE(rsv_xstring) TYPE xstring.
ENDCLASS.


CLASS zcl_rsv_util IMPLEMENTATION.
  METHOD decode_rsv.
    DATA rows         TYPE xstringtab.
    DATA row_strings  TYPE stringtab.
    DATA row_xstrings TYPE xstringtab.

    SPLIT rsv_xstring AT end_of_row INTO TABLE rows IN BYTE MODE.
    LOOP AT rows ASSIGNING FIELD-SYMBOL(<row>).
      CLEAR row_strings.
      SPLIT <row> AT end_of_value INTO TABLE row_xstrings IN BYTE MODE.
      LOOP AT row_xstrings ASSIGNING FIELD-SYMBOL(<row_xstring>).
        IF <row_xstring> = null_value.
          CLEAR <row_xstring>.
        ENDIF.
        APPEND cl_abap_codepage=>convert_from( source = <row_xstring> ) TO row_strings.
      ENDLOOP.
      APPEND row_xstrings TO rsv.
      APPEND row_strings TO rsv_decoded.
    ENDLOOP.
  ENDMETHOD.

  METHOD encode_rsv.
    LOOP AT encode_tab ASSIGNING FIELD-SYMBOL(<encode_row>).
      LOOP AT <encode_row> ASSIGNING FIELD-SYMBOL(<encode_value>).
        rsv_xstring = |{ rsv_xstring }{ cl_abap_codepage=>convert_to( source = <encode_value> ) }{ end_of_value }|.
      ENDLOOP.
      rsv_xstring = |{ rsv_xstring }{ end_of_row }|.
    ENDLOOP.
  ENDMETHOD.
ENDCLASS.

joocoo avatar Jan 25 '24 17:01 joocoo