TimeSpan FromMilliseconds strange implementation?
Asked Answered
D

5

33

I recently encountered some weird behaviour in the .NET TimeSpan implementation.

TimeSpan test = TimeSpan.FromMilliseconds(0.5);
double ms = test.TotalMilliseconds; // Returns 0

FromMilliseconds takes a double as parameter. However, it seems the value is rounded internally.

If I instantiate a new TimeSpan with 5000 ticks (.5 ms), the value of TotalMilliseconds is correct.

Looking at the TimeSpan implementation in reflector reveals that the input is in fact casted to a long.

Why did Microsoft design the FromMilliseconds method to take a double a parameter instead of a long (since a double value is useless given this implementation)?

Dignadignified answered 27/3, 2011 at 16:11 Comment(2)
Sounds like a bad design. @CodeNaked's answer points out that it's documented, but that just means they documented the bad behavior. I'm with you: this is a bug. If you write it up in Connect, post the link here so people can vote for it.Callosity
connect.microsoft.com/VisualStudio/feedback/details/653782/…Ibson
E
27

The first consideration is wondering why they selected a double as the return value. Using long would have been an obvious choice. Although there already is a perfectly good property that is long, Ticks is unambiguous with a unit of 100 nanoseconds. But they picked double, probably with the intention to return a fractional value.

That however created a new problem, one that was possibly only discovered later. A double can store only 15 significant digits. A TimeSpan can store 10,000 years. It is very desirable to convert from TimeSpan to milliseconds, then back to TimeSpan and get the same value.

That isn't possible with a double. Doing the math: 10,000 years is roughly 10000 x 365.4 x 24 x 3600 x 1000 = 315,705,600,000,000 milliseconds. Count off 15 digits, best a double can do, and you get exactly one millisecond as the smallest unit that can still be stored without round-off error. Any extra digits will be random noise.

Stuck between a rock and a hard place, the designers (testers?) had to choose between rounding the value when converting from TimeSpan to milliseconds. Or to do it later when going from milliseconds to TimeSpan. They chose to do it early, a courageous decision.

Solve your problem by using the Ticks property and multiplying by 1E-4 to get milliseconds.

Eburnation answered 27/3, 2011 at 17:14 Comment(1)
I still don't understand why they didn't simply implement it in a way that keeps the precision. Something like: => this((long)milliseconds) + TimeSpan.FromTicks(1000*(milliseconds - ((long)milliseconds)))Faircloth
J
4

This is by design, obviously. The documentation says as much:

The value parameter is converted to ticks, and that number of ticks is used to initialize the new TimeSpan. Therefore, value will only be considered accurate to the nearest millisecond.

Jahncke answered 27/3, 2011 at 16:21 Comment(7)
The values is obviously converted to ticks. Howver, this is done after converting the provided value to long.Dignadignified
@Dignadignified - That's correct. Regardless of what "From" method you use, it's accuracy is limited to the nearest millisecond. They are simply chopping off the fractional value by casting it to a long.Jahncke
@Dignadignified - They probably chose the double as the parameter type, so it would "match" the other From methods. You could do FromSeconds(0.5) to get 5000ms. Logically, it make sense to use double for FromMilliseconds also, even if it could be done with an int/long.Jahncke
@Jahncke I realize that is the case. I was just wondering why Microsoft chose such an implementation. Especially, when choosing this design, then why have the FromMilliseconds method take a double instead of a long, since any additional information held in the long variable is always lost.Dignadignified
@Jahncke Thanks for pointing this out, this is very confusing. I have encountered this strange behavior with FromMilliseconds, but I didn't know the other From methods behaved the same way.Zestful
@Dignadignified - Yeah, my money would be on the fact that the other From methods take a double as well.Jahncke
@Dignadignified - Also, looking at the implementation in Reflector, all the From methods all a private Interval method. This method takes a double value and a scale. The FromMilliseconds simply passes a scale of 1. If they wanted FromMilliseconds to accept a int/long, then they would have to cast it to a double to reuse the Interval method. So it seems it would just be easier to make it take a double as well.Jahncke
L
2

Accepting a double is a logical design. You can have fractions of milliseconds.

What's happening internally is an implementation design. Even if all current implementations (of the CLI) round it first that doesn't have to be the case in the future.

Learn answered 27/3, 2011 at 16:17 Comment(1)
The use of a doule value certainly makes since. However, it seems will make that kind of "breaking" changes to the TimeSpan implementaion.Dignadignified
S
1

The problem with your code is actually the first line, where you call FromMilliseconds. As noted previously, the remarks in the documentation state the following:

The value parameter is converted to ticks, and that number of ticks is used to initialize the new TimeSpan. Therefore, value will only be considered accurate to the nearest millisecond.

In reality, this statement is neither correct nor logically sound. In reverse order:

  • Ticks are defined as "one hundred nanoseconds". By this definition, the documentation should have been written as:

    Therefore, value will only be considered accurate to the nearest millisecond tick, or one ten-millionth of a second.

  • Due to a bug or oversight, the value parameter is not converted directly to ticks prior to initializing the new TimeSpan instance. This can be seen in the reference source for TimeSpan, where the millis value is rounded prior to its conversion to ticks, rather than after. If maximum precision were to be preserved, this line of code should have read as follows (and the adjustment by 0.5 milliseconds 3 lines earlier would be removed):

    return new TimeSpan((long)(millis * TicksPerMillisecond));
    

Summary:

The documentation for the various TimeSpan.From*, with the exception of FromTicks, should be updated to state that the argument is rounded to the nearest millisecond (without including the reference to ticks).

Spiderwort answered 8/8, 2014 at 14:36 Comment(0)
F
0

Or, you could do:

double x = 0.4;

TimeSpan t = TimeSpan.FromTicks((long)(TimeSpan.TicksPerMillisecond * x)); // where x can be a double
double ms = t.TotalMilliseconds; //return 0.4

--sarcasm

TimeSpan converts the double of milliseconds to ticks so "OBVIOUSLY" you can have a TimeSpan with less than a 1ms granularity.

-/sarcasm

-- this isn't obvious at all... why this isn't done inside the .FromMilliseconds method is beyond me.

Fardel answered 29/5, 2014 at 15:12 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.