DevDisasters

Doubly Precise VB.NET Rounding Issues Abound

You'd think that rounding should be simple, but why is it causing Ryan so much trouble? You can thank his managers for that.

In a big organization, like the one where Ryan works, management is understandably a little touchy about throughput on their systems. Therefore, Weekly Operational Pulse Reports are e-mailed out every Monday at 2 a.m.

The output isn't anything fancy, and it doesn't have to be. The reports are monospaced, tabular lists that cover the daily transactions per minute (TPM) for the last three weeks and, including the descriptive error messages and descriptive details, are a few pages in size when printed out.

The dirty secret of it all? Nobody ever really looks closely at the numbers. What it comes down to is that while management is absolutely concerned about transactional volume, what they really strive for is uniformity and predictability.

Let's say you have a day with zero records per hour. Yeah, that'll be noticed. Or, if you're like Ryan, and you have a two-week streak of 40.00 records processed per minute only to be interrupted by a Thursday with 39.99 records, you're going to have eight different bosses asking questions. After all, it's their job to ask tough questions before upper management ahead of those further up the food chain start asking.

To kick things off, Ryan started at the source: a database table with more than 10 million rows that tracked every transaction. He ran the query in the TSQL procedure that's behind the weekly e-mail and dumped that into Excel. Everything checked out -- every day, including that odd Thursday, rounded to 40, but there was one detail worth noting.

What gives? Unfortunately, the answer had to be in the VB.NET code behind the actual rounding itself. The console application behind the e-mail (PulseReport.exe) written circa 2008 and had survived two server migrations apparently had a reputation for being a little bit quirky.

So, when Ryan found the function, it was pretty much exactly as he had expected:

Public Function myRound(dblNum As Double, iDec As Integer) As Double

  Dim dblFac As Double
  Dim dblTmp As Double
    
  dblFac = 10 ^ iDec
  dblTmp = dblNum * dblFac + 0.5
  myRound = Int(dblTmp) / dblFac

End Function

Now, the oddball number that needed to be rounded was 39.995, and it needed to be rounded to two decimal places. Human logic dictates that the result should be 40.00, but after running a test program, Ryan found that the rounding routine was returning 39.99.

In fact, upon further testing, the values of 32.995, 33.995, 34.995, 35.995, 36.995, 37.995, 38.995 and 39.995 all produced the incorrect results.

What made this a head-scratching moment was when other numbers like 11.995, 49.995, 39.994 and 39.996 all produced the correct values (12.00, 50.00, 39.99 and 40.00, respectively).

Internet spelunking into the issue yielded plenty of info about Banker's rounding (rounding to the nearest even number), Asymmetric Arithmetic rounding (positive numbers away from zero, negative numbers round toward zero) and Symmetric Arithmetic rounding (negative and positive numbers round toward zero), and juicy tidbits about how the Round function is implemented in the various Microsoft products (along with people who seemed to be in the same boat as Ryan with rounding woes of their own). He also found that Double precision rounding issues in the Microsoft .NET Framework were known to cause troubles.

But he found nothing that could explain definitively why rounding 39.995 produced 39.99 instead of 40.00 in this code.

To correct the situation (after all, the bottom line is important), Ryan changed the code to:

myRound = Math.Round(value+0.0005, 2, MidpointRounding.AwayFromZero)

With that simple change, Ryan saw all of the results were as expected.

Sometimes, trying to explain misbehavior isn't quite as important as just making something work the way it should -- or at least how managers expect that it should.

About the Author

Mark Bowytz is a contributor to the popular Web site The Daily WTF. He has more than a decade of IT experience and is currently a systems analyst for PPG Industries.

comments powered by Disqus

Featured

  • Creating Reactive Applications in .NET

    In modern applications, data is being retrieved in asynchronous, real-time streams, as traditional pull requests where the clients asks for data from the server are becoming a thing of the past.

  • AI for GitHub Collaboration? Maybe Not So Much

    No doubt GitHub Copilot has been a boon for developers, but AI might not be the best tool for collaboration, according to developers weighing in on a recent social media post from the GitHub team.

  • Visual Studio 2022 Getting VS Code 'Command Palette' Equivalent

    As any Visual Studio Code user knows, the editor's command palette is a powerful tool for getting things done quickly, without having to navigate through menus and dialogs. Now, we learn how an equivalent is coming for Microsoft's flagship Visual Studio IDE, invoked by the same familiar Ctrl+Shift+P keyboard shortcut.

  • .NET 9 Preview 3: 'I've Been Waiting 9 Years for This API!'

    Microsoft's third preview of .NET 9 sees a lot of minor tweaks and fixes with no earth-shaking new functionality, but little things can be important to individual developers.

  • Data Anomaly Detection Using a Neural Autoencoder with C#

    Dr. James McCaffrey of Microsoft Research tackles the process of examining a set of source data to find data items that are different in some way from the majority of the source items.

Subscribe on YouTube