As I have previously written, ABAP has weak static types. This means that conversions between types are easy to do, even to the point that one can unintentionally do it.

Implicit Conversions

1
2
3
4
DATA lv_integer TYPE i.
DATA lv_string TYPE string VALUE '1'.

lv_integer = lv_string.

Erlang, in contrast, has strong dynamic types and single assignments, which means that it is not possible to assign a variable of one type to a variable of another type. The only way to convert between types is to use a function that takes one type as a parameter and returns another type:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
% Because Erlang has single assignments, this only causes
% Int to be assigned the same value as Str.
Str = "1",
Int = Str,

% list_to_integer/1 has the following definition:
%
% -spec list_to_integer(string()) -> integer().
%
% ie. it takes a string and returns an integer.
Int1 = list_to_integer(Str),

% And trying to pass an integer into the function
% would crash:
{'EXIT', {badarg, _}} = catch(list_to_integer(Int1)).

Implicitly converting between types has its upsides: simply assigning a string to an integer is much simpler than invoking a function or a method. Downsides of implicit conversions in ABAP include the fact that some statements that look like simple assignment can now crash (if the values happen to be incompatible) and also that implicit conversions can corrupt the values:

1
2
3
4
5
6
7
8
9
" A string can be arbitrarily long, whereas
" a char type always has a defined max length.
DATA lv_string TYPE string VALUE '12345'.
DATA lv_char4 TYPE c LENGTH 4.

lv_char4 = lv_string.
lv_string = lv_char4.

ASSERT lv_string = '1234'.

This sort of corruption also happens when converting between integers and floating-point types. Integers cannot represent the decimal part of a floating-point number, so something must be done about it. Some programming languages drop the decimal part (in effect, round it down) while others round the value to the nearest integer. Depending on the case, either one of these solutions might be acceptable.

In ABAP, the value is rounded to the nearest integer:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
DATA lv_float TYPE f.
DATA lv_integer TYPE i.


lv_float = '1.3'.
lv_integer = lv_float.

ASSERT lv_integer = 1.


lv_float = '1.5'.
lv_integer = lv_float.

ASSERT lv_integer = 2.

Rounding and omitted characters are examples of when an implicit conversion changes the value somehow. The resulting value may still be technically valid, but not what the programmer intended.

There are also cases where an implicit conversion does not do enough:

1
2
3
4
DATA lv_serial_number_str TYPE string VALUE '1'.
DATA lv_serial_number TYPE gernr.

lv_serial_number = lv_serial_number_str.

Although this implicit conversion does not corrupt the value in any way (GERNR type is 18 characters long), the value currently in lv_serial_number is still not valid. This is because GERNR has an additional requirement: if a value consists of only numbers, the value must be left-padded with zeroes so that all the 18 characters are used. When a value arrives from the user interface, this conversion would be executed by a function module. Implicit conversion, however, does not handle this step:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DATA lv_serial_number_str TYPE string VALUE '1'.
DATA lv_serial_number TYPE gernr.

lv_serial_number = lv_serial_number_str.

CALL FUNCTION 'CONVERSION_EXIT_GERNR_INPUT'
  EXPORTING
    input  = lv_serial_number
  IMPORTING
    output = lv_serial_number.
    
ASSERT lv_serial_number = '000000000000000001'.

Various fields in ABAP (AUFNR, MATNR, DOKNR etc.) have these sorts of additional requirements. The conversion exit can be found in the domain definition of the type.

Another way that an implicit conversion can fail to do enough is when a field is defined as case-insensitive:

1
2
3
4
5
6
DATA lv_uuid_str TYPE string '5c0a123776a34c9598cba3b8a8159079'.

DATA lv_uuid TYPE sysuuid_c32.
lv_uuid = lv_uuid_str.

ASSERT lv_uuid = '5c0a123776a34c9598cba3b8a8159079'.

A UUID (or a GUID) is a string made up of 32 characters (or 36 characters, if dashes are used to structure the string). It encodes a binary value (a MAC-namespaced timestamp, a hashed value or a random value) and thus valid characters of a UUID are those used to represent hexadecimal values: 0-9 and A-F, for a total of 16 different value. Both ‘a’ and ‘A’ represent value 10, the character case is insignificant.

By default, ABAP methods that generate UUIDs return uppercase values (which is the ABAP default for case-insensitive values). Type SYSUUID_C32 is also defined to be case-insensitive. The important thing is that comparisons between UUIDs happen not according to standard character string comparison (where character case matters), but according to the logic of whether the two UUIDs (regardless of the character case), represent the same value.

However, implicit conversions just move the characters from string to the case-insensitive variable, without modifying the character case. This is one of the peculiar features of ABAP: the field is not really case-insensitive, but the metadata causes the case of the values to be normalized when they are accepted from the user interface. Technically it is still possible to store case-sensitive values in case-insensitive variables.

Class Methods Are Type-Safe

Although syntax for passing parameters to a method call resembles assignment statements where implicit conversions happen, using incompatible values produces a compilation error:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
CLASS class DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS method
      IMPORTING im_integer TYPE i.
ENDCLASS.

CLASS class IMPLEMENTATION.
  METHOD method.
  ENDMETHOD.
ENDCLASS.


START-OF-SELECTION.
  DATA lv_string TYPE string VALUE '1'.
  
  class=>method( im_integer = lv_string ).
"LV_INTEGER" is not type-compatible with formal parameter "IM_INTEGER"

Incompatible types

Depending on the case, this is either a nice sanity check or an unnecessary hinderance. For example, if a parameter expects a string type, any char type would in theory work (since a string has no upper length limit, thus allowing it to contain any char type value).

The old way to get around this check would be define a temporary variable of the correct type. In more recent versions of ABAP, the conversion can be done without the extra variable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
CLASS class DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS method
      IMPORTING im_integer TYPE i.
ENDCLASS.

CLASS class IMPLEMENTATION.
  METHOD method.
  ENDMETHOD.
ENDCLASS.


START-OF-SELECTION.
  DATA lv_string TYPE string VALUE '1'.
  
  class=>method( im_integer = CONV #( lv_string ) ).

Enter CONV #( ). A new ABAP operator, it takes a value of one type and converts it to the type inferred from the context (the # symbol means that some sort of type inference takes place). This allows for implicit conversions of parameters in method calls and is therefore a tool that programmers reach for when they get a compilation error caused by incompatible types.

Although this seems like a convenient solution, the previously listed caveats of implicit conversions apply here: while the result of CONV #( ) will be of the expected type and will thus technically work, the value may still be invalid.

Take for example timestamps. ABAP does not have a real timestamp type. Rather, packed numbers are used to represent timestamps.

Timestamp

1
2
3
4
5
6
7
8
DATA lv_date TYPE d VALUE '20250202'.
DATA lv_time TYPE t VALUE '100000'.

DATA lv_timestamp TYPE timestamp.

CONVERT DATE lv_date TIME lv_time INTO TIME STAMP lv_timestamp TIME ZONE space.

ASSERT lv_timestamp = '20250202100000'.

Unlike a UNIX timestamp (seconds since UTC 1970-01-01 00:00:00), an ABAP timestamp is simply a concatenation of an ABAP date and UTC time values, stored in a packed number variable. Unlike ABAP date values, which one can operate on using ADD and SUBTRACT statements:

1
2
3
4
5
DATA lv_date TYPE d VALUE '20250131'.

ADD 1 TO lv_date.

ASSERT lv_date = '20250201'.

ABAP timestamps really are just packed numbers and must be operated using the class CL_ABAP_TSTMP. Using ADD AND SUBTRACT might work in some cases, but would more likely just produce invalid timestamp values:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
CONSTANTS con_day_in_seconds TYPE i VALUE 86400.

DATA lv_date TYPE d VALUE '20250131'.
DATA lv_time TYPE t VALUE '100000'.

DATA lv_timestamp TYPE timestamp.

CONVERT DATE lv_date TIME lv_time INTO TIME STAMP lv_timestamp TIME ZONE space.


DATA(lv_next_day_timestamp) = lv_timestamp.
ADD con_day_in_seconds TO lv_next_day_timestamp.

ASSERT lv_next_day_timestamp = '20250131186400'.


lv_next_day_timestamp = cl_abap_tstmp=>add( tstmp = lv_timestamp
                                            secs  = con_day_in_seconds ).

ASSERT lv_next_day_timestamp = '20250201100000'.

Like an ABAP time variable, the timestamp variables above are accurate to the second. There is also a “long timestamp” type which is accurate to the millisecond (or hundreds of nanoseconds):

1
2
3
4
5
6
7
DATA lv_timestamp TYPE timestamp.
DATA lv_long_timestamp TYPE timestampl.

GET TIME STAMP FIELD lv_long_timestamp.
GET TIME STAMP FIELD lv_timestamp.

ASSERT lv_long_timestamp > lv_timestamp.

To allow for accuracy in the sub-second range, the long timestamp type simply adds a fraction part.

Long timestamp

Now, suppose you have a long timestamp value and a method which only accepts a normal timestamp. You will obviously use CONV #( ), right? After all, a long timestamp can represent any value that is representable by a timestamp, so in theory there is no issue. Right?

Keep in mind that a timestamp is simply a packed number. When we implicitly convert a packed number with a fraction part to a packed number without one, the value gets rounded to the nearest integer. While not ideal, the rounding that results from this may or may not be acceptable. For example, a second here or there might not matter when tracking physical events of humans. Tracking computer events, the rounding might give an incorrect idea of the order of events (though the more fundamental error would be to think that tracking computer activity on the level of seconds is accurate enough).

ABAP Timestamps, however, are special. A UNIX timestamp is simply some number of seconds that have passed since UTC 1970-01-01 00:00:00. By definition, adding a second (or a thousand) will always result in another valid UNIX timestamp: the new one is just one (or a thousand) seconds farther from UTC 1970-01-01 00:00:00 than the previous one. The same does not hold for ABAP timestamps.

For example, the next valid value after ABAP timestamp 20250202105959 is not 20250202105960, but instead 20250202110000. This means that instinctually reaching for CONV #( ) may result in corrupted values:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CLASS class DEFINITION.
  PUBLIC SECTION.
    CLASS-METHODS method
      IMPORTING im_timestamp TYPE timestamp.
ENDCLASS.

CLASS class IMPLEMENTATION.
  METHOD method.
  
    ASSERT im_timestamp = '20250202105960'.
  
    TRY.
        cl_abap_tstmp=>add( tstmp = im_timestamp
                            secs  = 1 ).
        
        MESSAGE 'The above method call will throw because the timestamp is invalid' TYPE 'X'.
      
      CATCH cx_root.
    ENDTRY.
    
  ENDMETHOD.
ENDCLASS.


START-OF-SELECTION.
  DATA lv_timestamp TYPE timestampl VALUE '20250202105959.6'.
  
  class=>method( CONV #( lv_timestamp ) ).

Implicit conversions are handy, but they also hide many potential issues from the programmer. In contrast, a method of type timestampl -> timestamp would convert between types while respecting their internal structure.

Instinctually reaching for CONV #( ) every time you get a compilation error caused by incompatible parameter types is a bad habit. While using CONV #( ) can sometimes be okay, one should always stop and think before using it. After all, static types – and the compilation errors they make possible – are there to warn to you about potential errors.