Created
September 2, 2025 09:00
-
-
Save sandraros/0fc8ffaa763739e9028c31b66d64fcad to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
DEMO | |
DATA(csv_reader) = zcl_csv_reader=>create( | |
it_csv = concat_lines_of( table = string_table sep = |\n| ) | |
i_column_separator = |\t| ). | |
" Skip first line | |
TRY. | |
csv_reader->jump_to_next_line( ). | |
CATCH cx_root INTO DATA(error). | |
APPEND VALUE #( line = |{ error->get_text( ) } { csv_reader->line_number } { csv_reader->character_number }| ) TO log. | |
ENDTRY. | |
IF error IS NOT BOUND. | |
DO. | |
TRY. | |
csv_reader->start_of_line_else_next_line( ). | |
table_scen_upl = VALUE #( | |
BASE table_scen_upl | |
( scenid = EXACT #( to_upper( csv_reader->get_next_field( ) ) ) | |
text_scen = EXACT #( csv_reader->get_next_field( ) ) | |
seqnr = EXACT #( csv_reader->get_next_field( ) ) | |
actid = EXACT #( to_upper( csv_reader->get_next_field( ) ) ) | |
tcode = EXACT #( to_upper( csv_reader->get_next_field( ) ) ) | |
text_act = EXACT #( csv_reader->get_next_field( ) ) ) ). | |
IF csv_reader->get_all_next_fields_in_line( ) IS NOT INITIAL. | |
RAISE EXCEPTION TYPE zcx_acnip_cia EXPORTING text = 'too many fields'. | |
ENDIF. | |
CATCH zcx_csv_reader_end_of_file. | |
EXIT. | |
CATCH cx_root INTO error. | |
" field too long, too many fields in CSV line, etc. | |
APPEND VALUE #( line = |{ error->get_text( ) } { csv_reader->line_number } { csv_reader->character_number }| ) TO log. | |
ENDTRY. | |
ENDDO. | |
ENDIF. | |
CLASS | |
CLASS zcx_csv_reader DEFINITION INHERITING FROM cx_static_check. | |
ENDCLASS. | |
CLASS zcx_csv_reader_end_of_line DEFINITION INHERITING FROM zcx_csv_reader. | |
ENDCLASS. | |
CLASS zcx_csv_reader_end_of_file DEFINITION INHERITING FROM zcx_csv_reader. | |
ENDCLASS. | |
CLASS zcx_csv_reader_no_line_start DEFINITION INHERITING FROM zcx_csv_reader. | |
ENDCLASS. | |
CLASS zcx_csv_reader_program_error DEFINITION INHERITING FROM cx_no_check. | |
ENDCLASS. | |
CLASS zcl_csv_reader DEFINITION. | |
PUBLIC SECTION. | |
CLASS-METHODS create | |
IMPORTING | |
it_csv TYPE string | |
i_column_separator TYPE clike | |
RETURNING | |
VALUE(result) TYPE REF TO zcl_csv_reader. | |
"! Read all the next fields in the current line and position at the start of the next line | |
"! (e.g. if the current position is at the start of the file or line, all the fields of the current line are returned). | |
"! If already at the end of the line, it will not fail but returns no field | |
"! and will position at the start of the next line. | |
"! If already at the end of the file, it will fail. | |
METHODS get_all_next_fields_in_line | |
RETURNING | |
VALUE(result) TYPE string_table | |
RAISING | |
zcx_csv_reader_end_of_file. | |
METHODS get_next_field | |
RETURNING | |
VALUE(result) TYPE string | |
RAISING | |
zcx_csv_reader_end_of_line | |
zcx_csv_reader_end_of_file. | |
"! Provided that the current position is at the start of the current line (possibly of the first line i.e. start of file), | |
"! it will return all the fields of the current line and will position to the start of the next line. | |
"! Otherwise it will raise the exception zcx_csv_reader_no_line_start, or | |
"! the exception zcx_csv_reader_end_of_file if the position was already at the end of the file. | |
METHODS get_next_line | |
RETURNING | |
VALUE(result) TYPE string_table | |
RAISING | |
zcx_csv_reader_no_line_start | |
zcx_csv_reader_end_of_file. | |
METHODS jump_to_next_line | |
RAISING | |
zcx_csv_reader_end_of_file. | |
METHODS start_of_line_else_next_line | |
RAISING | |
zcx_csv_reader_end_of_file. | |
DATA file_offset TYPE i READ-ONLY. | |
DATA line_number TYPE i READ-ONLY. | |
DATA character_number TYPE i READ-ONLY. | |
PRIVATE SECTION. | |
TYPES ty_state TYPE i. | |
DATA csv TYPE string. | |
DATA i_column_separator TYPE c LENGTH 1. | |
DATA state TYPE ty_state. | |
CONSTANTS : | |
BEGIN OF lcs_state, | |
start_of_file TYPE ty_state VALUE 1, | |
start_of_line TYPE ty_state VALUE 2, | |
start_of_field TYPE ty_state VALUE 3, | |
end_of_line TYPE ty_state VALUE 5, | |
end_of_file TYPE ty_state VALUE 6, | |
quoted_field TYPE ty_state VALUE 7, | |
non_quoted_field TYPE ty_state VALUE 8, | |
"! End of field to be confirmed: if next character is double quote again. | |
double_quote_question TYPE ty_state VALUE 9, | |
middle_crlf_non_quoted_field TYPE ty_state VALUE 11, | |
END OF lcs_state. | |
ENDCLASS. | |
CLASS zcl_csv_reader IMPLEMENTATION. | |
METHOD create. | |
result = NEW zcl_csv_reader( ). | |
result->csv = it_csv. | |
result->i_column_separator = i_column_separator. | |
result->state = lcs_state-start_of_file. | |
result->file_offset = -1. | |
result->character_number = 1. | |
ENDMETHOD. | |
METHOD get_all_next_fields_in_line. | |
IF state = lcs_state-end_of_file. | |
RAISE EXCEPTION TYPE zcx_csv_reader_end_of_file. | |
ENDIF. | |
DO. | |
TRY. | |
APPEND get_next_field( ) TO result. | |
CATCH zcx_csv_reader_end_of_line. | |
state = lcs_state-start_of_line. | |
ADD 1 TO line_number. | |
character_number = 1. | |
RETURN. | |
CATCH zcx_csv_reader_end_of_file. | |
RETURN. | |
ENDTRY. | |
ENDDO. | |
ENDMETHOD. | |
METHOD get_next_field. | |
DATA csv_char TYPE string. | |
CASE state. | |
WHEN lcs_state-end_of_line. | |
RAISE EXCEPTION TYPE zcx_csv_reader_end_of_line. | |
WHEN lcs_state-end_of_file. | |
RAISE EXCEPTION TYPE zcx_csv_reader_end_of_file. | |
ENDCASE. | |
DATA(read_next_character) = abap_true. | |
DATA(look_ahead) = abap_false. | |
DO. | |
"--------------------------- | |
" processing common to several states : read next character | |
"--------------------------- | |
IF read_next_character = abap_true OR look_ahead = abap_true. | |
ADD 1 TO file_offset. | |
IF file_offset >= strlen( csv ). | |
state = lcs_state-end_of_file. | |
RETURN. " exit the method | |
ELSE. | |
csv_char = substring( val = csv off = file_offset len = 1 ). | |
ENDIF. | |
IF look_ahead = abap_true. | |
SUBTRACT 1 FROM file_offset. | |
ENDIF. | |
IF read_next_character = abap_true. | |
ADD 1 TO character_number. | |
ENDIF. | |
read_next_character = abap_false. | |
look_ahead = abap_false. | |
ENDIF. | |
"--------------------------- | |
" state processing | |
"--------------------------- | |
CASE state. | |
WHEN lcs_state-start_of_file. | |
line_number = 1. | |
state = lcs_state-start_of_line. | |
WHEN lcs_state-start_of_line | |
OR lcs_state-start_of_field. | |
" the character is expected to be the start of a field | |
IF csv_char = '"'. | |
state = lcs_state-quoted_field. | |
read_next_character = abap_true. | |
ELSEIF csv_char = |\r| | |
OR csv_char = |\n|. | |
" empty line | |
state = lcs_state-end_of_line. | |
RAISE EXCEPTION TYPE zcx_csv_reader_end_of_line. | |
ELSE. | |
" only change the state, remain on the current character | |
state = lcs_state-non_quoted_field. | |
ENDIF. | |
WHEN lcs_state-non_quoted_field. | |
IF csv_char = |\r|. | |
" Could be either \r alone or \r followed with \n (\r\n), both mean only ONE end of line | |
state = lcs_state-middle_crlf_non_quoted_field. | |
look_ahead = abap_true. | |
ELSE. | |
IF csv_char = i_column_separator. | |
state = lcs_state-start_of_field. | |
RETURN. " exit the method | |
ELSEIF csv_char = |\n|. | |
state = lcs_state-end_of_line. | |
RETURN. " exit the method | |
ELSE. | |
result = result && csv_char. | |
ENDIF. | |
read_next_character = abap_true. | |
ENDIF. | |
WHEN lcs_state-middle_crlf_non_quoted_field. | |
IF csv_char = |\n|. | |
ADD 1 TO file_offset. | |
ENDIF. | |
state = lcs_state-end_of_line. | |
RETURN. " exit the method | |
WHEN lcs_state-quoted_field. | |
" processing any of the characters after the initial double quote | |
IF csv_char <> '"'. | |
result = result && csv_char. | |
ELSE. | |
" at this stage, we don't know if it's followed by another double quote (means single quote i.e. not end of field) or not (means end of field) | |
state = lcs_state-double_quote_question. | |
ENDIF. | |
read_next_character = abap_true. | |
WHEN lcs_state-double_quote_question. | |
" LOOK AHEAD | |
" there was a double quote in the quoted field (after the initial double quote) | |
" Either it's a doubled double quote (both are to be interpreted as a single double quote) | |
" or a different character means it's really the end of the quoted field (special: "aa" can be followed with text bb which then means aabb). | |
IF csv_char = '"'. | |
result = result && csv_char. | |
state = lcs_state-quoted_field. | |
read_next_character = abap_true. | |
ELSE. | |
state = lcs_state-non_quoted_field. | |
ENDIF. | |
WHEN lcs_state-end_of_line. | |
" impossible | |
RAISE EXCEPTION TYPE zcx_csv_reader_program_error. | |
WHEN lcs_state-end_of_file. | |
" impossible | |
RAISE EXCEPTION TYPE zcx_csv_reader_program_error. | |
ENDCASE. | |
ENDDO. | |
" impossible because RETURN is always used to leave the DO-ENDDO loop above (i.e. not EXIT). | |
RAISE EXCEPTION TYPE zcx_csv_reader_program_error. | |
ENDMETHOD. | |
METHOD get_next_line. | |
IF state = lcs_state-end_of_file. | |
RAISE EXCEPTION TYPE zcx_csv_reader_end_of_file. | |
ELSEIF state <> lcs_state-start_of_file | |
AND state <> lcs_state-start_of_line. | |
RAISE EXCEPTION TYPE zcx_csv_reader_no_line_start. | |
ENDIF. | |
result = get_all_next_fields_in_line( ). | |
ENDMETHOD. | |
METHOD jump_to_next_line. | |
get_all_next_fields_in_line( ). | |
ENDMETHOD. | |
METHOD start_of_line_else_next_line. | |
IF state <> lcs_state-start_of_line. | |
jump_to_next_line( ). | |
ENDIF. | |
ENDMETHOD. | |
ENDCLASS. | |
CLASS ltc_csv_reader DEFINITION | |
FOR TESTING | |
DURATION SHORT | |
RISK LEVEL HARMLESS. | |
PRIVATE SECTION. | |
METHODS empty_field FOR TESTING RAISING cx_static_check. | |
METHODS empty_file FOR TESTING RAISING cx_static_check. | |
METHODS empty_line_get_next_field FOR TESTING RAISING cx_static_check. | |
METHODS empty_line_get_next_line FOR TESTING RAISING cx_static_check. | |
METHODS field_contains_space FOR TESTING RAISING cx_static_check. | |
METHODS get_all_next_fields_in_line FOR TESTING RAISING cx_static_check. | |
METHODS get_next_field_jump_to_next_li FOR TESTING RAISING cx_static_check. | |
METHODS get_next_line FOR TESTING RAISING cx_static_check. | |
METHODS get_next_line_end_of_file FOR TESTING RAISING cx_static_check. | |
METHODS get_next_line_no_line_start FOR TESTING RAISING cx_static_check. | |
METHODS line_empty_field_get_next_fiel FOR TESTING RAISING cx_static_check. | |
METHODS line_empty_field_get_next_line FOR TESTING RAISING cx_static_check. | |
"! "ab"cd,efgh is the same as abcd,efgh | |
METHODS mixed_quoted_field FOR TESTING RAISING cx_static_check. | |
METHODS non_quoted_field FOR TESTING RAISING cx_static_check. | |
METHODS non_quoted_field_cr_not_lf FOR TESTING RAISING cx_static_check. | |
METHODS non_quoted_field_cr_cr_lf FOR TESTING RAISING cx_static_check. | |
METHODS non_quoted_field_cr_lf FOR TESTING RAISING cx_static_check. | |
METHODS non_quoted_field_lf FOR TESTING RAISING cx_static_check. | |
"! "aa","bb" | |
METHODS quoted_field FOR TESTING RAISING cx_static_check. | |
"! "a\t\r\na"\t"a\t\r\na"\r\n means one line with two fields each containing two lines each with the letter a | |
METHODS quoted_field_cr_lf_tab FOR TESTING RAISING cx_static_check. | |
METHODS quoted_field_with_double_quote FOR TESTING RAISING cx_static_check. | |
ENDCLASS. | |
CLASS ltc_csv_reader IMPLEMENTATION. | |
METHOD empty_field. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |\t| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = || ). | |
ENDMETHOD. | |
METHOD empty_file. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = || i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = || ). | |
TRY. | |
csv_reader->get_next_field( ). | |
CATCH zcx_csv_reader_end_of_file INTO DATA(end_of_file_error) ##NO_HANDLER. | |
ENDTRY. | |
cl_abap_unit_assert=>assert_bound( act = end_of_file_error msg = 'second get_next_field should have failed because reader at end of file' ). | |
TRY. | |
csv_reader->get_next_line( ). | |
CATCH zcx_csv_reader_end_of_file INTO end_of_file_error ##NO_HANDLER. | |
ENDTRY. | |
cl_abap_unit_assert=>assert_bound( act = end_of_file_error msg = 'get_next_line should have failed because reader at end of file' ). | |
ENDMETHOD. | |
METHOD empty_line_get_next_field. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |\r\n| i_column_separator = |\t| ). | |
TRY. | |
csv_reader->get_next_field( ). | |
CATCH zcx_csv_reader_end_of_line INTO DATA(end_of_line_error) ##NO_HANDLER. | |
ENDTRY. | |
cl_abap_unit_assert=>assert_bound( act = end_of_line_error msg = 'get_next_field should have failed because the line is empty' ). | |
ENDMETHOD. | |
METHOD empty_line_get_next_line. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |\r\n| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_line( ) exp = VALUE string_table( ) ). | |
ENDMETHOD. | |
METHOD field_contains_space. | |
" space must be kept whatever it's at the beginning, middle or end. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = | a b | i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = | a b | ). | |
ENDMETHOD. | |
METHOD get_all_next_fields_in_line. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |line 1 field1\tfield2\r\nline 2 field1\tfield2\r\n| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_all_next_fields_in_line( ) exp = VALUE string_table( ( |line 1 field1| ) ( |field2| ) ) ). | |
ENDMETHOD. | |
METHOD get_next_field_jump_to_next_li. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |line 1 field1\tfield2\r\nline 2 field1\tfield2\r\n| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |line 1 field1| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |field2| ). | |
TRY. | |
csv_reader->get_next_field( ). | |
CATCH zcx_csv_reader_end_of_line INTO DATA(end_of_line_error) ##NO_HANDLER. | |
ENDTRY. | |
cl_abap_unit_assert=>assert_bound( act = end_of_line_error msg = 'get_next_field should have failed at end of line' ). | |
csv_reader->jump_to_next_line( ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |line 2 field1| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |field2| ). | |
ENDMETHOD. | |
METHOD get_next_line. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |line 1 field1\tfield2\r\nline 2 field1\tfield2\r\n| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_line( ) exp = VALUE string_table( ( |line 1 field1| ) ( |field2| ) ) ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_line( ) exp = VALUE string_table( ( |line 2 field1| ) ( |field2| ) ) ). | |
ENDMETHOD. | |
METHOD get_next_line_end_of_file. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |line 1 field1\tfield2| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_line( ) exp = VALUE string_table( ( |line 1 field1| ) ( |field2| ) ) ). | |
ENDMETHOD. | |
METHOD get_next_line_no_line_start. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |line 1 field1\tfield2\r\nline 2 field1\tfield2\r\n| i_column_separator = |\t| ). | |
csv_reader->get_next_field( ). | |
TRY. | |
csv_reader->get_next_line( ). | |
CATCH zcx_csv_reader_no_line_start INTO DATA(no_line_start) ##NO_HANDLER. | |
ENDTRY. | |
cl_abap_unit_assert=>assert_bound( act = no_line_start msg = 'get_next_line should have failed because reader not positioned at start of line' ). | |
ENDMETHOD. | |
METHOD line_empty_field_get_next_fiel. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |""\r\n| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = || ). | |
ENDMETHOD. | |
METHOD line_empty_field_get_next_line. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |""\r\n| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_line( ) exp = VALUE string_table( ( ) ) ). | |
ENDMETHOD. | |
METHOD mixed_quoted_field. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |"ab"cd\tefgh| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |abcd| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |efgh| ). | |
ENDMETHOD. | |
METHOD non_quoted_field. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |ab\tcd| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |ab| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |cd| ). | |
ENDMETHOD. | |
METHOD non_quoted_field_cr_not_lf. | |
" one "end of line" mark | |
ENDMETHOD. | |
METHOD non_quoted_field_cr_cr_lf. | |
" two "end of line" marks | |
ENDMETHOD. | |
METHOD non_quoted_field_cr_lf. | |
" one "end of line" mark | |
ENDMETHOD. | |
METHOD non_quoted_field_lf. | |
" one "end of line" mark | |
ENDMETHOD. | |
METHOD quoted_field. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |"ab"| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |ab| ). | |
ENDMETHOD. | |
METHOD quoted_field_cr_lf_tab. | |
" Any CR, LF or TAB character is kept as it is. CR or LF is never considered as an "end of line" mark. | |
" A field separator character inside a quoted field is never considered as a field separator. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |"a\t\r\nb"\t"a\t\r\nb"\r\n| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |a\t\r\nb| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |a\t\r\nb| ). | |
ENDMETHOD. | |
METHOD quoted_field_with_double_quote. | |
DATA(csv_reader) = zcl_csv_reader=>create( it_csv = |"""a""""b"""| i_column_separator = |\t| ). | |
cl_abap_unit_assert=>assert_equals( act = csv_reader->get_next_field( ) exp = |"a""b"| ). | |
ENDMETHOD. | |
ENDCLASS. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment