Safe dating in ABAP
Every so often a developer needs to involve code related to dates in their work. I have come across some “interesting” and “creative” approaches to this which might work under some conditions, but do provide a risk. Let’s talk dates in ABAP. I’ll explain how they work, common mistakes and my suggestion for consistent handling… Please let me know if you have other suggestions!
(This is a blog post I originally posted on LinkedIn a while back, but obviously this is where it belongs.)
There is a predefined data type DATS which you declare just as you would expect:
DATA lv_date type dats.
To assign it the value of todays date, which always is available in the sy-datum field:
lv_date = sy-datum.
The type dats is a character type field formatted as YYYYMMDD. This means that you can also assign a specific date using this format, like so:
lv_date = '20190329'.
There is also some additional functionality using this datatype compared to using just any 8 characters, in that you can add or subtract a number of days to the date and it will still be consistent:
lv_date = lv_date + 1. lv_date = lv_date - 9. lv_date = lv_date + 14.
This will give lv_date the values 20190330, 20190321 and finally 20190404, correctly passing into next month. This also works across year ends, let’s code like it’s 1999:
lv_date = '19991231'. lv_date = lv_date + 1.
This results in lv_date = 20000101, millennuim bug safe and all!
Leap years are no issue:
lv_date = '20160228'. lv_date = lv_date + 1. lv_date = lv_date + 1.
The first addition will give lv_date = 20160229 and the next one lv_date = 20160301
Year, month and day
The simplest way to separate year, month or day from a date is to use the offset and length specifications to specify only a section of the date. This is possible as the date type is a character type field.
By adding an offset using plus-sign and a number, like +4, the starting position of the section is defined.
By adding a length using a number in parantheses, like (2), the length of the section is defined.
Since the internal formatting of the date is YYYYMMDD, the sections are as follows:
- The “day section” starts at offset 6 and is 2 characters long. (YYYYMMDD).
- The “month section” starts at offset 4 and is 2 characters long. (YYYYMMDD).
- The “year section” starts at the beginning, i.e. offset 0, and is 4 characters long. (YYYYMMDD).
Let’s do it in code:
lv_date = '20190329'. lv_day = lv_date+6(2). lv_month = lv_date+4(2). lv_year = lv_date(4).
lv_day will be ’29’, lv_month ’03’ and lv_year ‘2019’. Note that for the year, since the offset is 0 it can be omitted in the code.
How about changing the date?
Ok, so far so good. This works just fine. But now we get to the risky business I’ve seen people attempt. We can also use offset and length at the LHS (left hand side) of the assignment operaion. This means manipulating only a section of the date. This comes in handy and quite useful at many times, but will be quite a risk when it comes to dates.
So lets change the month:
lv_date = '20190329'. lv_date+4(2) = '05'.
Setting the “month section” to 05 results, correctly, in lv_date = 20190529.
We can also use offset and length on both sides of the assignment operation:
lv_date = '20190329'. lv_date+4(2) = lv_date+4(2) + 3.
Adding 3 to the “month section” 03, results, correctly, in lv_date = 20190629.
Note however that we have lost almost every date-logic built in to the dats data type here. Nothing is hindering us from adding 7 months to October, for instance:
lv_date = '20191012'. lv_date+4(2) = lv_date+4(2) + 7.
Adding 7 to the “month section” 10, results in lv_date = 20191712
The calculation in itself is logically correct, but 20191712 “ain’t no month I ever heard of”.
Easily fixable, you say by checkning if lv_date+4(2) > 12, if so add 1 to the year section instead and decrease the month section by 12:
lv_date = '20191712'. WHILE lv_date+4(2) > 12. lv_date(4) = lv_date(4) + 1. lv_date+4(2) = lv_date+4(2) - 12. ENDWHILE.
Sure, we end up with 20200512, but it’s starting to get a little complex, right? And we’ve only just begun… 🙂
We also need to find a way to handle for instance adding 3 months to the last of January.
lv_date = '20190131'. lv_date+4(2) = lv_date+4(2) + 3.
Results in lv_date = 20190431. And since april only has 30 days, we’re screwed again. And then there is february with it’s 28 days and the leap years. Fully possible to fix it all, of course, but there are a couple of considerations to make.
I’ve seen some close-enough solutions, like considering a month to be 30 days, and simply adding 30 days to the entire date for each month, like so:
lv_date = '20190329'. lv_date = lv_date + ( 3 * 30 ).
Results in lv_date = 20190627. I’d say 2 days are missing. I have also seen adding years by adding 365 days per year:
lv_date = '20190329'. lv_date = lv_date + ( 5 * 365 ).
Results in lv_date = 20240327. 2 days missing again, this time due to leap years in 2020 and 2024.
So what do we do?
Enter class cl_reca_date. This is a neat class for doing almost anything date-related. In addition to not having to think about all the odds and ends, using this class will also yield a code that is a lot more readable and easier to understand and maintain. And that is more important than one might think.
Let’s try it out using the last example above, adding 5 years to 20190329:
lv_date = '20190329'. lv_date = cl_reca_date=>add_to_date( id_years = 5 id_date = lv_date ).
Now we DO get 20240329, as expected.
Another fail from above, adding 7 months to october:
lv_date = '20191012'. lv_date = cl_reca_date=>add_to_date( id_months = 7 id_date = lv_date ).
And we immediately end up with 20200512.
If you need to move back 8 years and 12 days and then forward 5 months from that (highly unclear why, though):
lv_date = '20191012'. lv_date = cl_reca_date=>add_to_date( id_years = -8 id_months = 5 id_days = -12 id_date = lv_date ).
We end up with lv_date = 20120229. Hey, look, it’s the leap day of 2012. 🙂
Other examples are calculating the difference between two dates, split into years, months and days:
cl_reca_date=>get_date_diff( EXPORTING id_date_from = lv_first_date id_date_to = lv_second_date IMPORTING ed_years = lv_number_of_years ed_months = lv_number_of_months ed_calendar_days = lv_number_of_days ).
Or another classic, checking a date for validity:
IF cl_reca_date=>is_date_ok( lv_date ). " Date is ok ELSE. " No can do ENDIF.
To summarize, ABAP has a somewhat powerful built-in date datatype, but it’s easy to get lost in the details when more than adding days is needed. The class cl_reca_date is my go-to for anything date-related and is what I keep recommending to colleagues as well.
There are more methods than I gave examples of here, including calculating with business calendar, intersections, weekdays, periods, and so on, etc. I am not going to provide examples for every one of them but I recommend you check them out next time you need to work with dates, if you haven’t already.
Happy dating! Stay safe! And please let me know If you have other suggestions!
Thanks for the information. I am not comfortable with using a "library" which is not an official API. If SAP don't provide an API, I would love that a developer proposes one in a public Git repository. Any volunteer? 😉
Hah. Well. Your're right of course, and I haven't really looked at it that way. More like "Hey, here's a perfect class delivered by SAP that does what I need, let's use it now and always. SAP is not likely going to break it, and it's not wort the effort to replicate all that I need".
While I still believe that to be true (this will most likely "always" work), the right way to go ahead is your way and a public date utility class on GitHub... 😊
thanks for the great article and your knowledge share. Sandra has also some good ideas, when you are looking into the future and developing your side-by-side extension in the cloud. Without the public API from SAP you can't use the class. Actual the class is not available on Steampunk, so we can't use it.
On Premise the use will be fine, but the code will also not be Cloud Ready, if it's your companies strategy.
Thank you, Björn!
Yes, I'm guilty of not thinking big enough. Of course we need a open, correct and consistent class to deploy both on premise and for use in the Steampunk environment.
Whoever wrote the CL_RECA package designed everything to be re-usable e.g. IF_RECA_MESSAGE_LIST
In theory if they had created any 100% generic interface/class it should have been put in a generic package but knowing SAP and having worked in Germany I imagine politics got in the way.
In any event when finding such standard SAP classes making a Z copy on GitHub is indeed the way forward.
As an aside it is good to have as much Z code as possible combatable with Steampunk but if history is anything to go by, the point at which Steampunk reaches mass adoption will be in 30 to 40 years time. I am basing that calculation on the fact that ABAP Objects was introduced in the year 2000 and to this day most new ABAP code is written in a procedural manner which is why you still have blogs written in 2019 with the title "Having a hard time getting my head around anything OO".
I had expected that the statement PERFORM would be outlawed in Steampunk but it is not. If I was being really cynical I would say some big companies are betting they can make SAP add extra things to the Steampunk environment a tiny bit at a time until it can compile ABAP code written in 1997.
I guess it's one of the the downsides of the close-to-infinite backwards compatibility, to have procedural ABAP written in 2020. And it's a pity that probably a minority is even bothering trying, but great credit to all who do!
And regarding CL_RECA_DATE again, that one is in big part an OO wrapper with some of the methods calling one or several function modules behind the surface. Which is fine in and of itself, as-is, but obviously won't do outside of an existing SAP installation. I didn't mention it in the post but one of the reasons I use it is because it's simplpy an OO collection of useful date methods...
Cheers to your insight! // Jörgen
just to note it is difficult to outlaw anything in ABAP. e.g PERFORM:
my solution: encapsulate the save logic in PERFORM ON COMMIT so
PERFORM ON COMMIT LEVEL is not obsolete and I cannot see how to replace it with a function module call.
Obligatory "boo" for using Hungarian Notation (lv_...) 🙂 but I like your writing and appreciate the effort.
In the past, I've created some global Z classes to handle some date operations that probably replicate some of what cl_reca... does. Just like others, I don't trust SAP classes unless they're officially approved and/or too complicated to recreate. SAP does change their code without warning, sometimes drastically, so "fool me once..." 🙂
Thanks for sharing!
Haha, yes Jelena, I'm guilty of being a slow adopter of getting rid of the Hungarian notation, for several reasons. However, I'm happy to share that the great shift actually happened for me during my Advent of Code experience; the first solutions are full of lv_ lt_ ls_ etc, but the later ones are not. The transformation is actually visible 😁
And from the comments I've received here, it appears as if I'm going to create an open source data class...
Thank you, Björn, for pointing out this class! I was not familiar with it. It might come in handy one of these days! (Though I won't use the class directly: I will re-use its functionality in my own ZCL_DATE class.)
Of course, the name of this class should have been CL_DATE. Why the heck a Real-Estate development team would have to write this generic class in the first place?
It is very unfortunate SAP never did follow Microsoft's lead in developing a harmonized, foundational class library. Microsoft did so back in 2000 (!) with the release of the first version of the .NET Framework. I have never understood why SAP didn't develop such a class library. Or maybe I do -- the lack of namespaces in the ABAP language. But that's another topic 🙂
I believe the lack of a harmonized foundational class library in SAP is THE major reason ABAP OO never took off as it should have.
YES! That is a pity. For many "library-type-things" in ABAP, we are still required to use function modules for the functionality or accidentally find a class that works (like in this example) and hope it will not break. 🙂
And from the comments I’ve received here, it appears as if I’m going to create an open source data class…
Imagine SAP would have put the effort they wasted on a Java stack into developing a harmonized ABAP class library! How sweet that would have been!
I believe this was also number 1 on the list of post-TechEd open ABAP questions from Lars Hvam.
Class/function is also an API. SAP never provided a clearly defined public library of function modules (even BAPIs were documented inconsistently), then carried on the same loosey-goosey approach to the OOP world. I agree completely: why is there no CL_DATE with the basic date functions? And where / how can anyone find what classes are OK to use?
The good news is that there is an official Time library in SAP Cloud Platform ABAP Environment (XCO) so it might be a good starting point for the (far) future.