[ABAP] Eine bestimmte Zeit (Sekundenbruchteile) warten

* WAIT UP TO ... SECONDS funktioniert nicht in allen Releases gleich zuverlässig
* Siehe auch BAPI_TRANSACTION_COMMIT mit Parameter WAIT = 'X'
* https://blogs.sap.com/2019/08/13/code-snippet-series-wait-a-fraction-of-a-second/
CLASS lcl_wait DEFINITION.
  PUBLIC SECTION.

    TYPES : BEGIN OF ty_s_tmstmp,
              start     TYPE timestampl, " Startzeit
              now       TYPE timestampl, " akt. Zeit
              elapsed   TYPE tzntstmpl,  " verstrichene Zeit in Sekunden
              limit     TYPE tzntstmpl,  " Limit in Sekunden
              condition TYPE abap_bool,  " Abbruch durch Bedingung oder Limit?
            END OF ty_s_tmstmp.

    CLASS-METHODS: wait
      IMPORTING
                i_time         TYPE tzntstmpl
      RETURNING VALUE(rv_info) TYPE ty_s_tmstmp.

  PRIVATE SECTION.
    CLASS-METHODS: condition_code
      RETURNING VALUE(rv_ok) TYPE abap_bool.
ENDCLASS.

CLASS lcl_wait IMPLEMENTATION.

  METHOD wait.
    rv_info = VALUE ty_s_tmstmp( elapsed = 0 limit = i_time condition = abap_false ).

    GET TIME STAMP FIELD rv_info-start.

* 1. Prüfung: Zeitlimit
    WHILE rv_info-elapsed < rv_info-limit.
* 2. Prüfung: Erfüllung der Abbruchbedingung
      IF abap_true = condition_code( ).
        rv_info-condition = abap_true.
        EXIT.
      ENDIF.

      GET TIME STAMP FIELD rv_info-now.
      rv_info-elapsed = cl_abap_tstmp=>subtract( tstmp1 = rv_info-now tstmp2 = rv_info-start ).
    ENDWHILE.

  ENDMETHOD.

  METHOD condition_code.
* Code mit Prüfbedingung hier (z.B. ENQUEUE_READ)
* Beispielcode testet, wann zufällig die Zahl 10 aus einer Seed von 0-100000 erzeugt wird
    DATA(lv_i) = cl_abap_random_int=>create( seed = cl_abap_random=>seed( ) min = 0 max = 100000 )->get_next( ).

    IF lv_i = 10.
      rv_ok = abap_true.
    ELSE.
      rv_ok = abap_false.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.

  DATA(lv_ts) = VALUE timestampl( ).

  GET TIME STAMP FIELD lv_ts.
  WRITE: / |Start: { lv_ts TIMESTAMP = ISO }|.

* max. Wartezeit in Sekunden (auch Sekundenbruchteile möglich)
  DATA(lv_info) = lcl_wait=>wait( '1.5' ).
  WRITE: / |Elapsed: { lv_info-elapsed TIMESTAMP = ISO }|.
  WRITE: / |Abbruchbedingung erfüllt: { lv_info-condition }|.

  GET TIME STAMP FIELD lv_ts.
  WRITE: / |Ende: { lv_ts TIMESTAMP = ISO }|.

[ABAP] Fabrikkalender: Anzahl Arbeitstage zu Fabrikkalenderdatum hinzurechnen

* Transaktion: SCAL (Fabrikkalender mit CUA-Oberfläche)
* Tabelle: TFACD (Fabrikkalenderdefinitionen)

* Werk
PARAMETERS: p_werks TYPE t001w-werks DEFAULT '10'.
* zu prüfendes Datum
PARAMETERS: p_date TYPE scal-date DEFAULT '20190101'.
* Kennzeichen, wie Arbeitstag berechnet werden soll:
* '+' nächster Arbeitstag
* '-' vorheriger Arbeitstag
PARAMETERS: p_corr TYPE scal-indicator DEFAULT '+'.
* Anzahl Arbeitstage, die zum Fabrikkalenderdatum dazugerechnet werden sollen
PARAMETERS: p_days TYPE i DEFAULT 3.

START-OF-SELECTION.

* Fabrikkalender zum Werk ermitteln
  SELECT SINGLE *
    INTO @DATA(lv_t001w)
    FROM t001w WHERE werks = @p_werks.

  IF sy-subrc = 0.
    DATA: lv_fabkldate TYPE scal-date.
    DATA: lv_factorydate TYPE scal-facdate.
    DATA: lv_workingday_indicator TYPE scal-indicator.

* Kalenderfunktion Fabrikkalenderdatum zu einem Datum geben
    CALL FUNCTION 'DATE_CONVERT_TO_FACTORYDATE'
      EXPORTING
        factory_calendar_id          = lv_t001w-fabkl " Schlüssel des Fabrikkalenders
        correct_option               = p_corr         " Kennzeichen, wie Arbeitstag berechnet werden soll
        date                         = p_date         " Datum, das in Fabrikkalenderdatum umzuwandeln ist
      IMPORTING
        date                         = lv_fabkldate            " Fabrikkalenderdatum
        factorydate                  = lv_factorydate          " Nummer des Arbeitstags im angegebenen Kalender
        workingday_indicator         = lv_workingday_indicator " Kennzeichen, ob Datum ein Arbeitstag ist
      EXCEPTIONS
        calendar_buffer_not_loadable = 1
        correct_option_invalid       = 2
        date_after_range             = 3
        date_before_range            = 4
        date_invalid                 = 5
        factory_calendar_not_found   = 6
        OTHERS                       = 7.

    IF sy-subrc = 0.
      WRITE: / 'Ursprgl. Datum:', p_date.
      WRITE: / 'Fabrikkalenderdatum:', lv_fabkldate.
      WRITE: / 'Nummer des Arbeitstags:', lv_factorydate.

* Anzahl Tage zu Fabrikkalenderdatum hinzuzurechnen
      lv_factorydate = lv_factorydate + p_days.

      WRITE: / 'Korrigierter Arbeitstag:', lv_factorydate.

      DATA: lv_new_date TYPE scal-date.

* Kalenderfunktion Datum zu einem Fabrikkalenderdatum geben
      CALL FUNCTION 'FACTORYDATE_CONVERT_TO_DATE'
        EXPORTING
          factorydate                  = lv_factorydate
          factory_calendar_id          = lv_t001w-fabkl
        IMPORTING
          date                         = lv_new_date
        EXCEPTIONS
          calendar_buffer_not_loadable = 1
          factorydate_after_range      = 2
          factorydate_before_range     = 3
          factorydate_invalid          = 4
          factory_calendar_id_missing  = 5
          factory_calendar_not_found   = 6
          OTHERS                       = 7.

      IF sy-subrc = 0.
        WRITE: / 'Neues Fabrikkalenderdatum:', lv_new_date.
      ENDIF.

    ENDIF.
  ELSE.
    WRITE: / |Kein Fabrikkalender zum Werk { p_werks } verfügbar.|.
  ENDIF.

[ABAP] Fabbrikkalender zum Werk lesen

* Transaktion: SCAL (Fabrikkalender mit CUA-Oberfläche)
* Tabelle: TFACD (Fabrikkalenderdefinitionen)

* Werk
PARAMETERS: p_werks TYPE t001w-werks DEFAULT '10'.
* zu prüfendes Datum
PARAMETERS: p_date TYPE scal-date DEFAULT '20190101'.
* Kennzeichen, wie Arbeitstag berechnet werden soll:
* '+' nächster Arbeitstag
* '-' vorheriger Arbeitstag
PARAMETERS: p_corr TYPE scal-indicator DEFAULT '+'.

START-OF-SELECTION.

* Fabrikkalender zum Werk ermitteln
  SELECT SINGLE *
    INTO @DATA(lv_t001w)
    FROM t001w WHERE werks = @p_werks.

  IF sy-subrc = 0.
    DATA: lv_fabkldate TYPE scal-date.
    DATA: lv_factorydate TYPE scal-facdate.
    DATA: lv_workingday_indicator TYPE scal-indicator.

* Kalenderfunktion Fabrikkalenderdatum zu einem Datum geben
    CALL FUNCTION 'DATE_CONVERT_TO_FACTORYDATE'
      EXPORTING
        factory_calendar_id          = lv_t001w-fabkl " Schlüssel des Fabrikkalenders
        correct_option               = p_corr         " Kennzeichen, wie Arbeitstag berechnet werden soll
        date                         = p_date         " Datum, das in Fabrikkalenderdatum umzuwandeln ist
      IMPORTING
        date                         = lv_fabkldate            " Fabrikkalenderdatum
        factorydate                  = lv_factorydate          " Nummer des Arbeitstags im angegebenen Kalender
        workingday_indicator         = lv_workingday_indicator " Kennzeichen, ob Datum ein Arbeitstag ist
      EXCEPTIONS
        calendar_buffer_not_loadable = 1
        correct_option_invalid       = 2
        date_after_range             = 3
        date_before_range            = 4
        date_invalid                 = 5
        factory_calendar_not_found   = 6
        OTHERS                       = 7.

    IF sy-subrc = 0.
      WRITE: / 'Werk:', p_werks.
      WRITE: / 'ID Fabrikkalender:', lv_t001w-fabkl.
      WRITE: / 'Datum:', p_date.
      WRITE: / 'Fabrikkalenderdatum:', lv_fabkldate.
      WRITE: / 'Nummer des Arbeitstags:', lv_factorydate.
      WRITE: / 'Datum ist Arbeitstag:', lv_workingday_indicator.
    ENDIF.
  ELSE.
    WRITE: / |Kein Fabrikkalender zum Werk { p_werks } verfügbar.|.
  ENDIF.

[ABAP] Zeit zu Zeit und Datum rechnen

DATA: lv_sdate TYPE d.
DATA: lv_stime TYPE t.

* akt. Datum und Zeit
lv_sdate = sy-datum.
lv_stime = sy-uzeit.

* 10:00:00Uhr
DATA: lv_addtime TYPE t VALUE '100000'.

DATA: lv_edate TYPE d.
DATA: lv_etime TYPE t.

START-OF-SELECTION.

* Zeitwerte addieren
  CALL FUNCTION 'C14B_ADD_TIME'
    EXPORTING
      i_starttime = lv_stime
      i_startdate = lv_sdate
      i_addtime   = lv_addtime
    IMPORTING
      e_endtime   = lv_etime
      e_enddate   = lv_edate.

  WRITE: / lv_sdate, lv_stime.
  WRITE: / lv_edate, lv_etime.

[ABAP] OADate (Ole Automation Date) in Zeitstempel (timestamp) umrechnen

* https://stackoverflow.com/questions/43519005/how-to-convert-java-date-to-oadate-or-vice-versa
* https://msdn.microsoft.com/de-de/library/system.datetime.tooadate(v=vs.110).aspx

* Zeitstempel im OADate-Format
DATA: lv_oadate TYPE decfloat34 VALUE '43013.765983796296296000'. " 05.10.2017 18:23:01
*DATA: lv_oadate TYPE decfloat34 VALUE '42909.777881944444444000'. " 23.06.2017 18:40:09
*DATA: lv_oadate TYPE decfloat34 VALUE '42370.000717592592592593'. " 01.01.2016 00:01:02
* Basisdatum für OADate ist der 30.12.1899
DATA: lv_basedate TYPE d VALUE '18991230'.

DATA: lv_oadays TYPE i.
DATA: lv_oatime TYPE decfloat34.
DATA: lv_hour TYPE decfloat34.
DATA: lv_min TYPE decfloat34.
DATA: lv_sec TYPE decfloat34.

DATA: lv_d TYPE d.
DATA: lv_t TYPE t.
DATA: lv_datetime TYPE timestamp.

* Vorkommateil: Tage seit Basisdatum
lv_oadays = trunc( lv_oadate ).
* Nachkommateil: Tageszeit
lv_oatime = lv_oadate - CONV decfloat34( lv_oadays ).
* Nachkommateil: Stunden
lv_hour = lv_oatime * 24.
* Nachkommateil: Minuten
lv_min = ( lv_hour - trunc( lv_hour ) ) * 60.
* Nachkommateil: Sekunden
lv_sec = round( val = ( ( lv_min - trunc( lv_min ) ) * 60 ) dec = 0 ).

* Datum: Basisdatum + Tage des Vorkommateils
lv_d = lv_basedate + lv_oadays.
* Uhrzeit
lv_t = |{ CONV string( trunc( lv_hour ) ) WIDTH = 2 ALPHA = IN }{ CONV string( trunc( lv_min ) ) WIDTH = 2 ALPHA = IN }{ CONV string( trunc( lv_sec ) ) WIDTH = 2 ALPHA = IN }|.

* Datum und Uhrzeit in einem Zeitstempelobjekt zusammenfassen
CONVERT DATE lv_d TIME lv_t INTO TIME STAMP lv_datetime TIME ZONE 'UTC'.

WRITE: / '     OADate:', lv_oadate.
WRITE: / 'Zeitstempel:', lv_datetime TIME ZONE 'UTC'.

[ABAP] Zeitstempel (timestamp) in OADate (Ole Automation Date) umrechnen

* https://stackoverflow.com/questions/43519005/how-to-convert-java-date-to-oadate-or-vice-versa
* https://msdn.microsoft.com/de-de/library/system.datetime.tooadate(v=vs.110).aspx

* Zeitstempel mit Datum und Uhrzeit
*DATA: lv_datetime TYPE timestamp VALUE '20171005182301'. " 43013.765983796296296000
*DATA: lv_datetime TYPE timestamp VALUE '20170623184009'. " 42909.777881944444444000
DATA: lv_datetime TYPE timestamp VALUE '20160101000102'. " 42370.000717592592592593
* Basisdatum für OADate ist der 31.12.1899
DATA: lv_ts_base TYPE timestamp VALUE '18991231000000'.

DATA: lv_d TYPE d.
DATA: lv_t TYPE t.

DATA: lv_oadays TYPE i.

DATA: lv_hour TYPE decfloat34.
DATA: lv_min TYPE decfloat34.
DATA: lv_sec TYPE decfloat34.

DATA: lv_oadate TYPE decfloat34.

* Timestamp in Date und Time aufsplitten
CONVERT TIME STAMP lv_datetime TIME ZONE 'UTC' INTO DATE lv_d TIME lv_t.

* Zeitdifferenz zwischen Zeitstempel und Basisdatum in Sekunden
DATA(lv_diff_sec) = cl_abap_tstmp=>subtract( tstmp1 = lv_datetime
                                             tstmp2 = lv_ts_base ).

* Vorkommateil: Zeitdifferenz zwischen Zeitstempel und Basisdatum in Tagen
lv_oadays = lv_diff_sec / 86400.
* Nachkommateil: Stunden
lv_hour = CONV decfloat34( lv_t+0(2) ) / 24.
* Nachkommateil: Minuten
lv_min = CONV decfloat34( lv_t+2(2) ) / 1440.
* Nachkommateil: Sekunden
lv_sec = CONV decfloat34( lv_t+4(2) ) / 86400.

* OADate zusammenrechnen: Vorkommateil + Nachkommateil
lv_oadate = lv_oadays + lv_hour + lv_min + lv_sec.

WRITE: / 'Zeitstempel:', lv_datetime TIME ZONE 'UTC'.
WRITE: / '     OADate:', lv_oadate.

[ABAP] Timerobjekt cl_gui_timer verwenden

* Hinweis zum Aufruf von cl_gui_timer:
* - selbst mit interval = 1 (1s) triggert der Timer nach run( ) nur alle 2s
* - parallele Listausgaben mehrerer Worker können scheinbar Deadlocks verursachen

* Workerobjekt, welches vom Timer aufgerufen wird
CLASS lcl_worker DEFINITION.
  PUBLIC SECTION.
    METHODS: constructor
      IMPORTING
        i_id TYPE i.
    METHODS: do
      IMPORTING
        i_cnt TYPE i.
    METHODS: finished.

  PRIVATE SECTION.
    DATA: gv_id TYPE i.
ENDCLASS.

CLASS lcl_worker IMPLEMENTATION.
  METHOD constructor.
    gv_id = i_id.
  ENDMETHOD.
  METHOD do.
    WRITE: / 'Do( ) - Timer ID:', gv_id, 'Counter:', i_cnt.
  ENDMETHOD.

  METHOD finished.
    WRITE: / 'Finished( ) - Timer ID:', gv_id.
  ENDMETHOD.
ENDCLASS.

* Timerobjekt, welches zyklisch Worker aufruft
CLASS lcl_timer DEFINITION.
  PUBLIC SECTION.
    METHODS: start
      IMPORTING
        i_worker    TYPE REF TO lcl_worker
        i_interval  TYPE i
        i_count_max TYPE i.

  PRIVATE SECTION.

    DATA: o_worker TYPE REF TO lcl_worker.
    DATA: gv_count_current TYPE i.
    DATA: gv_count_max TYPE i.

    METHODS: on_timer_finished FOR EVENT finished OF cl_gui_timer
      IMPORTING
          sender.
ENDCLASS.

CLASS lcl_timer IMPLEMENTATION.
  METHOD start.
* Übergabewerte ok?
    IF i_count_max >= 0 AND i_interval > 0.
      o_worker = i_worker.

* erster Aufruf -> Funktion sofort ausführen
      o_worker->do( i_cnt = 1 ).

* Zählervariablen setzen
      gv_count_current = 1.
      gv_count_max = i_count_max.

* Timer erzeugen
      DATA(o_timer) = NEW cl_gui_timer( ).
      SET HANDLER on_timer_finished FOR o_timer.
      o_timer->interval = i_interval.
      o_timer->run( ).
    ENDIF.
  ENDMETHOD.

  METHOD on_timer_finished.
* Funktion ausführen
    o_worker->do( i_cnt = gv_count_current + 1 ).

* unendlich
    IF gv_count_max = 0.
* Timer wieder ausführen
      sender->run( ).
    ELSE.
* Anzahl Durchgänge
      gv_count_current = gv_count_current + 1.

      IF gv_count_current < gv_count_max.
* Timer wieder ausführen
        sender->run( ).
      ELSE.
        o_worker->finished( ).
      ENDIF.
    ENDIF.
  ENDMETHOD.
ENDCLASS.

START-OF-SELECTION.

* 1. Timerobjekt mit Worker ID 1
  NEW lcl_timer( )->start( i_worker    = NEW lcl_worker( i_id = 1 )
                           i_interval  = 1
                           i_count_max = 10 ).

* 2. Timerobjekt mit Worker ID 2
  NEW lcl_timer( )->start( i_worker    = NEW lcl_worker( i_id = 2 )
                           i_interval  = 3
                           i_count_max = 5 ).

* 3. Timerobjekt mit Worker ID 3
  NEW lcl_timer( )->start( i_worker    = NEW lcl_worker( i_id = 3 )
                           i_interval  = 2
                           i_count_max = 3 ).

  MESSAGE 'Ok.' TYPE 'S'.