Wednesday, 30 December 2020

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!

The Basics

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.

No comments:

Post a Comment