<body><script type="text/javascript"> function setAttributeOnload(object, attribute, val) { if(window.addEventListener) { window.addEventListener('load', function(){ object[attribute] = val; }, false); } else { window.attachEvent('onload', function(){ object[attribute] = val; }); } } </script> <div id="navbar-iframe-container"></div> <script type="text/javascript" src="https://apis.google.com/js/platform.js"></script> <script type="text/javascript"> gapi.load("gapi.iframes:gapi.iframes.style.bubble", function() { if (gapi.iframes && gapi.iframes.getContext) { gapi.iframes.getContext().openChild({ url: 'https://www.blogger.com/navbar.g?targetBlogID\x3d34883048\x26blogName\x3dYour+Friendly+ABAPer\x26publishMode\x3dPUBLISH_MODE_BLOGSPOT\x26navbarType\x3dBLUE\x26layoutType\x3dCLASSIC\x26searchRoot\x3dhttps://friendlyabaper.blogspot.com/search\x26blogLocale\x3den_US\x26v\x3d2\x26homepageUrl\x3dhttp://friendlyabaper.blogspot.com/\x26vt\x3d-8534208955155839123', where: document.getElementById("navbar-iframe-container"), id: "navbar-iframe", messageHandlersFilter: gapi.iframes.CROSS_ORIGIN_IFRAMES_FILTER, messageHandlers: { 'blogger-ping': function() {} } }); } }); </script>

Peeking inside a variant

Recently I had to work on a program, which should run, among other ABAP programs, in a background job. The challenge was that this program was supposed to figure out which dates were used by the program that is a preceding step in the same job. Since the programs run using the same variant name (content might be changed by the users but the name stays the same), the task here really is to figure out what’s inside the variant.

It wasn’t quite difficult to find a function module that reads the variant - RS_VARIANT_CONTENTS (I found it on the always useful list on the ERPGenie website) but here are some more details on how this FM works.

In the program in question the users can either save the dates “as is” in the variant or use the selection variable, in which case the dates would change for every run. Two internal tables can be received from the FM: L_SELOP and VALUTAB. L_SELOP contains the selections that were saved as variant and VALUTAB contains the values. If the selection variable has been used, VALUTAB contains the actual date values, calculated with that variable, which is very neat.

Here is the test code that I used. The program name is RVV50R10C (delivery due list) and my variant was named TEST. You can, of course, use this FM with any program and variant.
DATA: t_valutab LIKE rsparams OCCURS 0 WITH HEADER LINE,
t_selop LIKE vanz OCCURS 0 WITH HEADER LINE.

CALL FUNCTION 'RS_VARIANT_CONTENTS'
EXPORTING
report = 'RVV50R10C'
variant = 'TEST'
TABLES
l_selop = t_selop
valutab = t_valutab
EXCEPTIONS
variant_non_existent = 1
variant_obsolete = 2
OTHERS = 3.

IF sy-subrc <> 0.
MESSAGE ID sy-msgid TYPE sy-msgty NUMBER sy-msgno
WITH sy-msgv1 sy-msgv2 sy-msgv3 sy-msgv4.
ENDIF.
In the TEST variant I’ve chosen the range of dates from [today – 10 days] till today using a selection variable. Here is the content of the t_selop table for the date field that I’m interested in (the table contains all the possible selection fields):

The technical field name (T_SELOP-NAME field) can be found either in the program body (look for SELECTION-OPTIONS or PARAMETER) or (the easy way), start the program and then hit [F1] on the field. In the pop-up window, click on [Technical info] – the field name will be in the 'Screen field':

T_SELOP doesn’t say what exactly was in the selection variable (bummer!), but at least we know that a variable was used: see field VNAME. If there is no selection variable, the field VNAME is empty.

T_valutab is very similar to a regular range table. Here is the content of the t_valutab for the date field:

I ran the program on 10/19, so you can see that it nicely calculated the date 10 days back (LOW field). Of course we could run into a problem if the program 1 starts before midnight but program 2 after midnight. Even though in our case it will not happen due to the business restrictions, there is a possible solution: use an FM to look up the job start time and, if it’s not equal to sy-datum, adjust the dates in t_valutab. Maybe I’ll explore this concept in one of the future posts. Have fun!

posted by Your Friendly ABAPer @ 21:38,
Direct link to this post


The pure and simple truth about BINARY SEARCH

The pure and simple truth is rarely pure and never simple.
Oscar Wilde

Last week I got an email from a worried user that some information was missing on a sales report. After few hours of exhaustive debugging with a few time-outs in between, I realized that an obscure READ TABLE... command comes back with SY-SUBRC = 4. It was looking for a combination of material number and customer number in an internal table. Both numbers were right, leading zeroes and all. I had the whole table in front of me in the debugging window and the record, which READ was supposed to find, was indeed present in it. “What do you mean sy-subrc is 4?! Here is that record, right there, you dumbass!”, - almost yelled I at the poor innocent Dell monitor.

Here I should probably mention that READ TABLE command had BINARY SEARCH addition. As I’ve learned from my very long programming (not ABAP) experience, sometimes if you just make things simpler it might actually solve the problem. So I’ve just commented out the BINARY SEARCH part and ran the program again. Now it worked like a charm. OK, now I had to get to the bottom of this.

I set up a very simple test program:
DATA: BEGIN OF i_test OCCURS 0,
key1,
key2,
non_key,
END OF i_test.

PERFORM populate_table.

SORT i_test BY key1.
LOOP AT i_test.
WRITE: / i_test-key1, i_test-key2, i_test-non_key.
ENDLOOP.

READ TABLE i_test WITH KEY key1 = 'B' BINARY SEARCH.
IF sy-subrc = 0.
WRITE: / 'Found:'.
WRITE: / i_test-key1, i_test-key2, i_test-non_key.
ELSE.
WRITE: / 'Not found'.
ENDIF.
(I’ve omitted populate_table routine because it just populates i_test with some test data). Here is the output from the program:












OK, so it found the very first record with B. So far so good. I changed the READ line to add the second key:
READ TABLE i_test WITH KEY key1 = 'B' key2 = 'Z' BINARY SEARCH.
It did find B Z record. Alright, let’s make the things a bit more interesting:
READ TABLE i_test WITH KEY key2 = 'Z' key1 = 'B' BINARY SEARCH.
Here comes the ‘Not found’ message! However, after I changed SORT from key1 to key2 it was able to find the B Z record again. Now let’s kick it up a notch. I changed SORT and READ commands as follows:
SORT i_test BY key1 ASCENDING key2 DESCENDING.
...
READ TABLE i_test WITH KEY key1 = 'B' key2 = 'A' BINARY SEARCH
Here is the content of the i_test table after the sorting so that you guys could follow:
A Z C
A Z D
A A A
A A B
B Z E
B B C
B A A
B A B
C C A
Z B B
Z A A
The results were as follows: the test above (B A) came back with ‘Not found’ (this time switching key1 and key2 in READ did not help). The things got even curioser when the A A record was found but Z A was not.

So what’s the deal with this damn binary search? I really like the simple explanation that one guy gave in an SDN post: ”Let’s say you have numbers 1..to ..100 in a table and you are searching for 34. It would read the 50th record and if it is say 50 next it would read the 25th record and if it is say 25 it would carry to read the 38th record and so on.”

Back to my example. There were 11 entries in my test table. The binary search started by splitting the table in half and it got the middle record (B B C). “OK,” thought the computer. “Since I’m looking for Z and A, let me look at the second part of the list (because Z > B). Oh, now I see C C A, we are getting closer! Let’s look at what’s left after that.” Naturally, at this point the only records to search were only C C A, Z B B and Z A A. So it split the list in half again and got Z B B. “OMG, I went too far! Let me get back real quick. Hmm... I see C C A. C is less than Z, which means that there is no record with Z and A. Oh well... SY-SUBRC = 4. Buhbye!”.

As I finally found out, the problem with the sales report was that the internal table was first sorted by one field, which would have worked fine with the READ, but then re-sorted by another field somewhere in the middle. It looks like a good idea to sort the table right before the binary search, which I will do in the future.

Obviously, with BINARY SEARCH what you see is not always what you get. To get the right result, the table must be sorted by the right field and in ascending order. If this is not done properly, sometimes binary search might still work correctly, depending on what data is inside the table. But sometimes you might wish it didn’t work at all because it could make finding an error a major pain in the back.

While I was on it, I also ran the runtime analysis a few times. With the small amount of data in my test program ordinary READ actually worked even faster than READ ... BINARY SEARCH. However, with thousands of records and about 10 fields (as in my sales report), BINARY SEARCH performs much better. I’m pretty sure that hashed table would be even more efficient (unfortunately, it can not be used in that specific report).

posted by Your Friendly ABAPer @ 21:27,
Direct link to this post


Fun with numbers

It seems that ABAP is one of the few languages that does not have an operator to validate whether the field is numeric or not. In the system where most of the key fields (VBELN, MATNR, etc.) are CHAR but usually contain only numbers an IS NUMERIC operator would come in handy, don’t you think?

Oh well, as Mr. Zorg used to say: "You want something done, do it yourself!". And so I started this quest with a simple task to find a way to check whether the field is a number. I’m not going to lie to you – the idea to define a constant with numbers only and to use IF ... CO... was stolen from one of the SAP programs. Here is my first test program:

CONSTANTS: numbers(10) VALUE '1234567890'.
DATA: test(10).

test = '123ABC'.
PERFORM test_check USING test.

test = '123'.
PERFORM test_check USING test.

FORM test_check USING p_test.
IF p_test CO numbers.
WRITE: / p_test , ' contains only numbers'.
ELSE.
WRITE: / p_test , ' contains alpha characters'.
ENDIF.
ENDFORM. " test_check
But the result surprised me:

123ABC contains alpha characters
123 contains alpha characters
What?! Since when 123 is not a number?! Well, obviously, sometimes the owls are not what they seem. Here is what the documentation says:

CO (Contains Only):

c1 contains only characters from the string c2.
If c1 or c2 is of type C, the comparison takes into account the full length of the field, including blanks at the end.
Doh! Damn SAP with their blanks... OK, I can work around this. Not sure if there is a better way to do this but I found my own method to pad a number with leading zeroes by using SHIFT and TRANSLATE operators. Here is my test number two (I changed only the test_check routine:

FORM test_check USING VALUE(p_test).

SHIFT p_test RIGHT DELETING TRAILING ' '.
TRANSLATE p_test USING ' 0'.

IF p_test CO numbers.
WRITE: / test , ' contains only numbers'.
ELSE.
WRITE: / test , ' contains alpha characters'.
ENDIF.
ENDFORM. " test_check
The result:

123ABC contains alpha characters
123 contains only numbers
Tadah! Note that if you don’t use FORM ... USING VALUE... then the variable test will be converted to ‘0000000123’. (Boy, I feel so smart right now. :) )

OK. But this piece of magic has actually very limited application. What if the field contains characters like ‘+’, ‘,’ or ‘.’, which can also be a part of the number? Coincidentally, on one of the SAP forums someone has posted a question how to convert a string (for example '107,400.99') into an integer. Since I was already on this numeric quest, I continued in this new direction. Here is what I came up with:

DATA: input_string TYPE string,
output_integer TYPE i.

input_string = '+107,400.99'.

CATCH SYSTEM-EXCEPTIONS
arithmetic_errors = 1
conversion_errors = 2.
TRANSLATE input_string USING ', '.
CONDENSE input_string NO-GAPS.
output_integer = input_string.
ENDCATCH.

IF sy-subrc = 0.
WRITE: output_integer.
ELSE.
MESSAGE 'Not a number' TYPE 'E'.
ENDIF.
The result: 107,401 (since its type I the decimals have been rounded). This piece of code also works when input_string is type CHAR and output can actually be any numeric type. It works with NUMC, P (with or without DECIMALS) and currency types equally well. Also the input string can have plus and minus sign and it can be upfront or at the end of the number – it will still work. The standard ABAP type conversion will take care of converting decimals and the sign, so we only have to remove the thousand separator (‘,’ in this case). CATCH clause will catch an exception if the string contains any other characters (e.g. letters).

Of course, there are also some related functional modules available:
MOVE_CHAR_TO_NUM – This is a good all-purpose FM (works with CHAR but not with STRING though). Its major advantage is that the thousands separator is not limited to a comma, like in my example above.
HRCM_STRING_TO_AMOUNT_CONVERT – this FM can be used for the conversion of amounts since it takes the currency into account. For the simple string to number conversion it is a bit too bulky, in my opinion.
CATS_NUMERIC_INPUT_CHECK – this one has a very limited application IMHO. It does not convert character fields to numeric fields. Basically all it does it checks if the field is numeric, removes the thousands separator and, if there is a negative sign, moves it to the end. It could not handle the number '+107,400.99' and threw a "not numeric" exception.

posted by Your Friendly ABAPer @ 21:01,
Direct link to this post


User parameters (PARAMETER ... MEMORY ID)

The company that I currently work for has just recently implemented SAP at one of the locations and now we are working on the implementation at another location. We have several custom transactions where users must enter an ID (Sales Organization, Plant, etc.) of their location. Naturally, we started thinking what we could do so that the users wouldn’t need to type those IDs again and again. (In case you are wondering – we were simply too busy to think about it during the first implementation.)

So last week I went to talk to our security administrator and she told me that she’s been entering all the applicable organizational IDs on the user profiles. There are other transactions for the user profile maintenance (e.g. SU01), which most likely you will not be authorized to use, but she showed me this one, SU3 with the Parameters tab:

At first I thought that we will have to find some function module and get those parameters somehow by user ID. But then I realized that those parameters can actually be used in the MEMORY ID addition to the PARAMETER command. If, for example, a user has the parameter VKO = ZZZ in SU3, then p_vkorg will be populated with ZZZ by default when the user runs the program below. Here I also check if the user has authorization to display the data for this sales organization:

PARAMETER: p_vkorg TYPE vkorg MEMORY ID VKO.

AUTHORITY-CHECK OBJECT 'V_VBRK_VKO'
ID 'VKORG' FIELD p_vkorg
ID 'ACTVT' FIELD ‘03’.

IF sy-subrc <> 0.
* display error message here.
ENDIF.

Here are some other useful memory IDs:

BUK - Company code (BUKRS)
EKO - Purchasing Org (EKORG)
LAG - Storage Location (LGORT)
LGN - Warehouse Number (LGNUM)
WRK - Plant (WERKS)

The memory IDs are stored in the TPARA table, there are like thousands of them. Not sure if those parameters have any other use and if the custom parameters could be maintained and how... I’ll let you know if and when I find out.

By the way, while I was playing around with this, I found that WERKS is actually a structure. While it’s OK to define the parameters with TYPE VKORG, it is better not to define them with TYPE WERKS. Use TYPE WERKS_D instead.

posted by Your Friendly ABAPer @ 22:40,
Direct link to this post


Trouble with hashed tables

Today I learned the hard way that hashed internal tables work not exactly as I was expecting. What we (or at least I) learn from, say, BC400 class or the ABAP reference is that there are standard, sorted and hashed internal tables. Standard tables are kind of all-purpose, the sorted ones are better for LOOP AT ... WHERE and hashed tables are good if you need to do READ TABLE with unique key. And for some reason I was assuming that if I read some data that has duplicates into a hashed table it will be nicely populated and duplicates will simply be skipped. Well, assumption is mother of all screw-ups, as they say. Very true.

In my defense, I actually went through my BC400 materials and ABAP reference and could not find any clues on this, so here is some info on how this actually works. Let’s say you’re trying to get a list of all the deliveries and material numbers and you want the unique numbers only. Here is a bad idea example:

TYPES: BEGIN OF deliveries,
vbeln TYPE vbeln,
matnr TYPE matnr,
END OF deliveries.
DATA: i_deliveries TYPE HASHED TABLE OF deliveries
WITH UNIQUE KEY vbeln matnr.

SELECT vbeln matnr
INTO TABLE i_deliveries
FROM lips.

This program will end with a dump if there is any VBELN with more than one record with the same MATNR. However, this disaster can be easily avoided by changing SELECT to SELECT DISTINCT. Another option (depending on your task) would be to SELECT into a standard table, then do SORT, DELETE ADJACENT DUPLICATES and copy the content to a hashed table. This seems a bit redundant (most likely SELECT DISTINCT is going to work faster) but might be necessary sometimes, you never know.

Also be careful when doing, for example,

i_deliveries_hashed[] = i_deliveries[].

(Here i_deliveries is a standard table and i_deliveries_hashed is a hashed table.) If there are records in i_deliveries with duplicates (based on the hashed table key), this will also fall into a short dump. Good old SORT and DELETE ADJACENT DUPLICATES will help here as well.

posted by Your Friendly ABAPer @ 20:42,
Direct link to this post