Skip to Main Content
  • Questions
  • Best practice for rounding issue with date math.

Breadcrumb

Question and Answer

Chris Saxon

Thanks for the question, Brad.

Asked: May 15, 2025 - 5:46 pm UTC

Last updated: May 19, 2025 - 3:30 pm UTC

Version: 23ai

Viewed 1000+ times

You Asked

I ran into this problem with 1% of our records converting views from SQL Server to Oracle where my testing found different results when doing a compare during UAT.

We have a system that needs to express time rounded to tenths of hours because of industry norms in aviation.

In the example below you can see I am finding the difference between two dates, but the result seems to be rounding incorrectly. Normally to test these issues is to try converting the output to a char to see if there is an unseen precision issue.

In this case, I know that Oracle is computing the RAW_DIFF value as .649999999 and is the reason it appears to round incorrectly. What I found surprising is that RAW_CHAR did not return .649999999.

My question is what is the best practice for avoiding this case because I can see how an analyst writing a query would expect ROUND(<val>,1) to just work since this is a 1% kind of issue?



START_DATE 5/7/2025 10:46:00 PM
START_CHAR 2025-05-07 22:46:00
END_DATE 5/7/2025 10:07:00 PM
START_CHAR 2025-05-07 22:07:00
RAW_DIFF 0.65
RAW_CHAR .650000000000 -- actually .649999999 depending on the query tool
SINGLE_ROUND 0.6
DOUBLE_ROUND 0.7


create table ROUND_TEST
(
  id         NUMBER generated always as identity,
  start_date DATE,
  end_date   DATE
);

  insert into round_test
  (start_date, end_date)
values
  (to_date('5/7/2025 10:46:00 PM','MM/DD/YYYY HH:MI:SS PM'), to_date('5/7/2025 10:07:00 PM','MM/DD/YYYY HH:MI:SS PM'));


  
select t.start_date,
       to_char(t.start_date, 'YYYY-MM-DD HH24:MI:SS') start_char,
       t.end_date,
       to_char(t.end_date, 'YYYY-MM-DD HH24:MI:SS') end_char,
       (t.start_date - t.end_date) * 24 as raw_diff,
       to_char((t.start_date - t.end_date) * 24,'9,999.999999999999') as raw_char,
       round((t.start_date - t.end_date) * 24, 1) as single_round,
       round(round((t.start_date - t.end_date) * 24, 3), 1) as double_round
  from round_test t

and Chris said...

Add more decimal digits to the format to ensure you're showing the full raw value. A number has a precision of up to 40 digits, so forty 9s should be enough in this case:

select to_char (
         (t.start_date - t.end_date) * 24,
         '9,999.9999999999999999999999999999999999999999'
       ) as raw_char
from   round_test t;

RAW_CHAR
-----------------------------------------------
      .6499999999999999999999999999999999999992


Check your tools to see about setting this as the default to help make this clear.

There are various methods you can use to overcome the problem. Double rounding as you've shown is one.

Another is to cast either or both values to a timestamp. This makes the difference between them an interval. You'll then need to extract out the minutes to convert these to tenths of an hour and add these onto the hours elapsed:

select start_date - cast ( end_date as timestamp ) int_diff, 
       extract ( minute from start_date - cast ( end_date as timestamp ) ) mins_diff, 
       round ( 
         extract ( minute from start_date - cast ( end_date as timestamp ) ) / 60, 1
       ) mins_diff
from   round_test;

INT_DIFF             MINS_DIFF  MINS_DIFF
------------------- ---------- ----------
+00 00:39:00.000000         39         .7


Whatever you method you use, you can reduce the chance of analyst mistakes by baking the formula into the table. Do this by adding a virtual column to the table:

alter table round_test add 
  hours_duration number(6,1) as ( 
    round ( 
      round ( ( start_date - end_date ) * 24, 3 ), 
      1 
    )
  );

select * from round_test;

        ID START_DATE           END_DATE             HOURS_DURATION
---------- -------------------- -------------------- --------------
         1 07-MAY-2025 22:46:00 07-MAY-2025 22:07:00             .7


This saves everyone having to duplicate the rounding logic.

Rating

  (6 ratings)

Comments

Using an interval expression

Stew Ashton, May 16, 2025 - 5:33 pm UTC

Another way to get an interval is to use an interval expression:
select round(extract(minute from (start_date - end_date) day to second) / 60, 1) 
from round_test
Please note that if the number of minutes is 57 or more, the result will be 1, meaning 1 hour. What is the greatest time difference you need to handle, and what result do you want to see in that case?

Rounding twice

mathguy, May 17, 2025 - 6:15 pm UTC

You probably chose the simplest example to illustrate the problem, but that may mislead us into thinking all your cases are equally simple. You need to clarify if all your differences will be less than one hour (I assume not), and also whether your "date" values may have non-zero "seconds" components (I assume yes, although in your simplified example they do not). Chris's and Stew's solutions would have to be adjusted for both possibilities - difference greater than 1 hour, and non-zero seconds components. The formulas will become quite messy; the double-rounding seems much cleaner by comparison. (In addition, it is not clear a priori that dividing by 60 rather than 86400 may not lead to the same kind of rounding errors in the results - it's certainly less likely, but not impossible, at least not without a good argument or a lot of testing.)

Note that rounding to 3 places first works in your case, but only because 0.1 hours (6 minutes) is less than 1000 seconds. If instead of integer multiples of 6 minutes you were interested in integer multiples of 24 minutes, or 45 minutes, the first rounding should consider more decimal places, if the "seconds" component isn't always zero. I won't discuss this further since it's not your use case, but something to be aware of nevertheless; the magic number "3" you chose for the first rounding works, but only for a very specific reason.

As to "best practice", I would say "documentation" is probably the right option. A brief comment in the query itself, explaining that the double-rounding corrects for rounding errors (you may even include a specific example: Oracle computes (2340/86400) * 24 = 0.6499999999999999999999999999999999999992 rather than 0.65, which is the correct value - this is your "39 minutes difference" case and illustrates why you need two roundings), as well as a more detailed explanation in the code documentation, if your organization follows that practice.

@mathguy: adjustments and tests

Stew Ashton, May 18, 2025 - 8:59 am UTC

Mathguy, you say: "Chris's and Stew's solutions would have to be adjusted for both possibilities - difference greater than 1 hour, and non-zero seconds components."

- For the difference greater than 1 hour, I did mention that, which it would have been more polite to acknowledge.
- The non-zero seconds are not an issue in this case, since 1/10 of an hour is exactly 6 whole minutes, so the boundaries are at whole minutes: 3, 9, 15, 21 and so on. I did test non-zero seconds anyway.

"The formulas will become quite messy"

How messy depends on whether the difference can be 24 hours or more, which was the point of my question.

"..it is not clear a priori that dividing by 60 rather than 86400 may not lead to the same kind of rounding errors in the results"

I tested all intervals from 0 seconds to 59 minutes and 59 seconds, and I found this result at 2 minutes and 59 seconds:
with dates as (
  select sysdate start_date, sysdate - interval '2:59' minute to second end_date
  from dual
)
select round(extract(minute from (start_date - end_date) day to second) / 60, 1) hr,
round(round((start_date - end_date) * 24, 3), 1) as double_round
from dates

        HR DOUBLE_ROUND
---------- ------------
         0 .1          
I'll leave you all to decide which result is correct.

Inner round

Stew Ashton, May 18, 2025 - 9:09 am UTC

After further testing, it appears that the "double round" solution gets the same result as the "interval" solution if the inner round integer parameter is between 4 and 38.

2'59" issue

mathguy, May 18, 2025 - 9:14 pm UTC

Oops - I used the wrong logic in my comment.

The "rounding" calculations in the OP's attempt are always based on numbers representing hours - not "tens of an hour". As such, what is relevant is that one second is 1/3600 of an hour, so the inner rounding should be to at least four decimal places (1/10000) - precisely in order to avoid the issue Stew encountered: rounding something strictly less than "3 minutes" to only 3 decimal places will exactly equal "3 minutes", and then the further rounding to one decimal place will give the wrong result. The same happens at any "boundary" minus 1 second, and for the same reason.

The inner rounding should be to 4 decimal places (or more - the result will be the same, as Stew also found).

Completing the interval expression solution

Stew Ashton, May 19, 2025 - 8:33 am UTC

I suppose mathguy considers the interval solutions to be "messy" because they involve multiple uses of the EXTRACT function. I would counter that the interval expression, besides working correctly, is the most self-documenting solution because it says explicitly that a DAY TO SECOND interval is being used. It also states explicitly the logic used to convert each component to a value in hours, whereas the date-based solution requires understanding of Oracle date arithmetic and of an obscure workaround.

When possible and convenient, an inline view could of course limit the tedious repetition of the same code:
with data as (select (start_date - end_date) day to second ids from round_test)
select
  extract(day from ids) * 24 +
  extract(hour from ids) +
  round(extract(minute from ids) / 60, 1) hours
from data
P.S. Funny that no one has mentioned the fact that start_date is later than end_date :-)
Chris Saxon
May 19, 2025 - 3:30 pm UTC

When possible and convenient, an inline view could of course limit the tedious repetition of the same code:

I still think a virtual column is the best way to avoid repetition here - particularly as it sounds like this formula will be used in many places.

P.S. Funny that no one has mentioned the fact that start_date is later than end_date

That did puzzle me when first answering ;)