Skip to content

underflow

Peter Halasz edited this page Apr 2, 2019 · 9 revisions

Update (now patched by Microsoft)

I have reported this issue to Microsoft, complete with test code, a patch and advice on better ways it could be patched:

Microsoft have rewritten my lengthy report and made their own PR, which will be part of .NET Core 3.0


Underflow issue

.NET's Built-in TimeSpan.Parse() has been causing me grief. I've been testing TimeSpanParser's output against it, only to find my results are fine, while Microsoft's TimeSpan.Parse() acts erratically. It's all from numbers that are too small. These are called overflows, which is the term I'll use here, even if they really ought to be called underflows.

At first I thought Microsoft was simply rounding up these small numbers, and I had hopes of reverse engineering this mysterious rounding algorithm, as seen in this now-retired to-do list item:

  • TODO: Raise identical exceptions to TimeSpan.Parse(), and do it in identical scenarios. (e.g. Overflow timespans which are just the right amount smaller than the tick size)

But this turns out to be impossible or, at least, undesirable.

So what's the problem? When you try to parse a very small time period, that is, when the seconds reaches 8 decimal places, e.g. TimeSpan.Parse("0:00:00.00000001"), it means you're trying to parse a number where the precision has dropped to less than the 100-nanosecond ticks which TimeSpan stores its numbers, and so an OverflowException should be thrown. But Microsoft only starts throwing exceptions around 9 decimal places. At 8 it acts very oddly, inconsistently, and incorrectly. The documented behavior is to throw an OverflowException for all these cases, but instead incorrect results are given.

Some highlights:

  • TimeSpan.Parse("0:00:00.0000005").Ticks; // this is 5 ticks given in 7 decimal places. This returns 5. Well done, Microsoft. ✔️✔️✔️✔️✔️
  • TimeSpan.Parse("0:00:00.00000050").Ticks; // this should also be 5 ticks. Microsoft counts 50. ❌
  • TimeSpan.Parse("0:00:00.00000055").Ticks; // this ought to be 5.5 ticks. Round up or round down? Neither: Microsoft gives us 55 ❌
  • TimeSpan.Parse("0:00:00.00000000"); // same length as the previous one. Could be exactly equal toTimeSpan.zero, but this time you'll get thrown an OverflowException 🙄
  • TimeSpan.Parse("0:00:00.00000099").Ticks; // Why does this number in particular throw an OverflowException? 🤷
  • Both of these return a TimeSpan of 123456 ticks:
TimeSpan.Parse("0:00:00.0123456") ==   // ✔️ 123456 ticks
TimeSpan.Parse("0:00:00.00123456")     // ❌ also 123456 ticks

See also

Results dump

TimeSpan.Parse("0:00:00.00000000"); // OverflowException due to zeroes. 0 d.p. + 8 zeroes. (Could have returned 0)
TimeSpan.Parse("0:00:00.00000001").Ticks; // == 1 but should give 0
TimeSpan.Parse("0:00:00.00000002").Ticks; // == 2 but should give 0
TimeSpan.Parse("0:00:00.00000003").Ticks; // == 3 but should give 0
TimeSpan.Parse("0:00:00.00000004").Ticks; // == 4 but should give 0
TimeSpan.Parse("0:00:00.00000005").Ticks; // == 5 but should give 0
TimeSpan.Parse("0:00:00.00000006").Ticks; // == 6 but should give 0
TimeSpan.Parse("0:00:00.00000007").Ticks; // == 7 but should give 0
TimeSpan.Parse("0:00:00.00000008").Ticks; // == 8 but should give 0
TimeSpan.Parse("0:00:00.00000009"); // OverflowException
TimeSpan.Parse("0:00:00.00000010").Ticks; // == 10 but should give 1
TimeSpan.Parse("0:00:00.00000011").Ticks; // == 11 but should give 1
TimeSpan.Parse("0:00:00.00000012").Ticks; // == 12 but should give 1
TimeSpan.Parse("0:00:00.00000013").Ticks; // == 13 but should give 1
TimeSpan.Parse("0:00:00.00000014").Ticks; // == 14 but should give 1
TimeSpan.Parse("0:00:00.00000015").Ticks; // == 15 but should give 1
TimeSpan.Parse("0:00:00.00000016").Ticks; // == 16 but should give 1
TimeSpan.Parse("0:00:00.00000017").Ticks; // == 17 but should give 1
TimeSpan.Parse("0:00:00.00000018").Ticks; // == 18 but should give 1
TimeSpan.Parse("0:00:00.00000019").Ticks; // == 19 but should give 1
TimeSpan.Parse("0:00:00.00000020").Ticks; // == 20 but should give 2
TimeSpan.Parse("0:00:00.00000021").Ticks; // == 21 but should give 2
TimeSpan.Parse("0:00:00.00000022").Ticks; // == 22 but should give 2
TimeSpan.Parse("0:00:00.00000023").Ticks; // == 23 but should give 2
TimeSpan.Parse("0:00:00.00000024").Ticks; // == 24 but should give 2
TimeSpan.Parse("0:00:00.00000025").Ticks; // == 25 but should give 2
TimeSpan.Parse("0:00:00.00000026").Ticks; // == 26 but should give 2
TimeSpan.Parse("0:00:00.00000027").Ticks; // == 27 but should give 2
TimeSpan.Parse("0:00:00.00000028").Ticks; // == 28 but should give 2
TimeSpan.Parse("0:00:00.00000029").Ticks; // == 29 but should give 2
TimeSpan.Parse("0:00:00.00000030").Ticks; // == 30 but should give 3
TimeSpan.Parse("0:00:00.00000031").Ticks; // == 31 but should give 3
TimeSpan.Parse("0:00:00.00000032").Ticks; // == 32 but should give 3
TimeSpan.Parse("0:00:00.00000033").Ticks; // == 33 but should give 3
TimeSpan.Parse("0:00:00.00000034").Ticks; // == 34 but should give 3
TimeSpan.Parse("0:00:00.00000035").Ticks; // == 35 but should give 3
TimeSpan.Parse("0:00:00.00000036").Ticks; // == 36 but should give 3
TimeSpan.Parse("0:00:00.00000037").Ticks; // == 37 but should give 3
TimeSpan.Parse("0:00:00.00000038").Ticks; // == 38 but should give 3
TimeSpan.Parse("0:00:00.00000039").Ticks; // == 39 but should give 3
TimeSpan.Parse("0:00:00.00000040").Ticks; // == 40 but should give 4
TimeSpan.Parse("0:00:00.00000041").Ticks; // == 41 but should give 4
TimeSpan.Parse("0:00:00.00000042").Ticks; // == 42 but should give 4
TimeSpan.Parse("0:00:00.00000043").Ticks; // == 43 but should give 4
TimeSpan.Parse("0:00:00.00000044").Ticks; // == 44 but should give 4
TimeSpan.Parse("0:00:00.00000045").Ticks; // == 45 but should give 4
TimeSpan.Parse("0:00:00.00000046").Ticks; // == 46 but should give 4
TimeSpan.Parse("0:00:00.00000047").Ticks; // == 47 but should give 4
TimeSpan.Parse("0:00:00.00000048").Ticks; // == 48 but should give 4
TimeSpan.Parse("0:00:00.00000049").Ticks; // == 49 but should give 4
TimeSpan.Parse("0:00:00.00000050").Ticks; // == 50 but should give 5
TimeSpan.Parse("0:00:00.00000051").Ticks; // == 51 but should give 5
TimeSpan.Parse("0:00:00.00000052").Ticks; // == 52 but should give 5
TimeSpan.Parse("0:00:00.00000053").Ticks; // == 53 but should give 5
TimeSpan.Parse("0:00:00.00000054").Ticks; // == 54 but should give 5
TimeSpan.Parse("0:00:00.00000055").Ticks; // == 55 but should give 5
TimeSpan.Parse("0:00:00.00000056").Ticks; // == 56 but should give 5
TimeSpan.Parse("0:00:00.00000057").Ticks; // == 57 but should give 5
TimeSpan.Parse("0:00:00.00000058").Ticks; // == 58 but should give 5
TimeSpan.Parse("0:00:00.00000059").Ticks; // == 59 but should give 5
TimeSpan.Parse("0:00:00.00000060").Ticks; // == 60 but should give 6
TimeSpan.Parse("0:00:00.00000061").Ticks; // == 61 but should give 6
TimeSpan.Parse("0:00:00.00000062").Ticks; // == 62 but should give 6
TimeSpan.Parse("0:00:00.00000063").Ticks; // == 63 but should give 6
TimeSpan.Parse("0:00:00.00000064").Ticks; // == 64 but should give 6
TimeSpan.Parse("0:00:00.00000065").Ticks; // == 65 but should give 6
TimeSpan.Parse("0:00:00.00000066").Ticks; // == 66 but should give 6
TimeSpan.Parse("0:00:00.00000067").Ticks; // == 67 but should give 6
TimeSpan.Parse("0:00:00.00000068").Ticks; // == 68 but should give 6
TimeSpan.Parse("0:00:00.00000069").Ticks; // == 69 but should give 6
TimeSpan.Parse("0:00:00.00000070").Ticks; // == 70 but should give 7
TimeSpan.Parse("0:00:00.00000071").Ticks; // == 71 but should give 7
TimeSpan.Parse("0:00:00.00000072").Ticks; // == 72 but should give 7
TimeSpan.Parse("0:00:00.00000073").Ticks; // == 73 but should give 7
TimeSpan.Parse("0:00:00.00000074").Ticks; // == 74 but should give 7
TimeSpan.Parse("0:00:00.00000075").Ticks; // == 75 but should give 7
TimeSpan.Parse("0:00:00.00000076").Ticks; // == 76 but should give 7
TimeSpan.Parse("0:00:00.00000077").Ticks; // == 77 but should give 7
TimeSpan.Parse("0:00:00.00000078").Ticks; // == 78 but should give 7
TimeSpan.Parse("0:00:00.00000079").Ticks; // == 79 but should give 7
TimeSpan.Parse("0:00:00.00000080").Ticks; // == 80 but should give 8
TimeSpan.Parse("0:00:00.00000081").Ticks; // == 81 but should give 8
TimeSpan.Parse("0:00:00.00000082").Ticks; // == 82 but should give 8
TimeSpan.Parse("0:00:00.00000083").Ticks; // == 83 but should give 8
TimeSpan.Parse("0:00:00.00000084").Ticks; // == 84 but should give 8
TimeSpan.Parse("0:00:00.00000085").Ticks; // == 85 but should give 8
TimeSpan.Parse("0:00:00.00000086").Ticks; // == 86 but should give 8
TimeSpan.Parse("0:00:00.00000087").Ticks; // == 87 but should give 8
TimeSpan.Parse("0:00:00.00000088").Ticks; // == 88 but should give 8
TimeSpan.Parse("0:00:00.00000089").Ticks; // == 89 but should give 8
TimeSpan.Parse("0:00:00.00000090").Ticks; // == 90 but should give 9
TimeSpan.Parse("0:00:00.00000091").Ticks; // == 91 but should give 9
TimeSpan.Parse("0:00:00.00000092").Ticks; // == 92 but should give 9
TimeSpan.Parse("0:00:00.00000093").Ticks; // == 93 but should give 9
TimeSpan.Parse("0:00:00.00000094").Ticks; // == 94 but should give 9
TimeSpan.Parse("0:00:00.00000095").Ticks; // == 95 but should give 9
TimeSpan.Parse("0:00:00.00000096").Ticks; // == 96 but should give 9
TimeSpan.Parse("0:00:00.00000097").Ticks; // == 97 but should give 9
TimeSpan.Parse("0:00:00.00000098").Ticks; // == 98 but should give 9
TimeSpan.Parse("0:00:00.00000099").Ticks; // OverflowException, but could have returned 9
Clone this wiki locally