views:

221

answers:

3

Hi folks,

I'm trying to make an extension method that allows me to create a random time between Now and some user requested historical time point in the form of a TimeSpan.

For example : a random time between now and 1 hour ago.

So, I came up with the following Random(..) extension method.

I thought to make the random seed NOT static, but if i call this method a LOT and QUICKLY (eg. in a loop), then I thought the seed (which is based on time) isn't really random for really fast calls. is that true? (it seems to be, when i check my results)

public static DateTimeOffset Random(this DateTimeOffset value, TimeSpan timeSpan)
{
    var random = new Random();
    DateTimeOffset minDateTime = value.Subtract(timeSpan);
    int range = ((DateTime.Today - minDateTime)).Hours;
    return minDateTime.AddHours(random.Next(range));
}
A: 

You have to seed the Random number generator. A good practice would be to do the following:

var random = new Random((int)DateTime.Now.Ticks);

This should make it more random for you.

You could also create the Random generator as a static class variable so you don't have to instantiate it every time.

Adam Gritt
but if the random instance is static, wouldn't every result be the same? isn't that why you want a new seed every time?
Pure.Krome
No, you don't need a new seed every time. The RNG does maintain its state from one call to another. Take a look here: http://en.wikipedia.org/wiki/Pseudorandom_number_generator
Paulo Santos
That's not a good practice. That's a *bad* practice. You can still end up calling that many times and get the same seed each time. Use a single instance via a normal static variable won't be thread-safe - the Random class isn't thread-safe. You need to use one instance per thread, as shown by Luke.
Jon Skeet
no, that is exactly why you don't want to create a new one every time and you don't want a new seed every time.
Svish
The code I presented was to show a better way to initialize the RNG so that it would get a different starting random number. Normal initialization will result with the same initial seed and the same sequence of numbers each time. The code I presented I have used without issue many times.
Adam Gritt
@Adam: No, the parameterless constructor for Random uses Environment.TickCount. From the docs: "Initializes a new instance of the Random class, using a time-dependent default seed value."
Jon Skeet
@Adam: You say - "The code I presented I have used without issue many times." That just suggests you haven't used that code several times in very quick succession, or you haven't noticed that it's given the same results for several near-simultaneous calls. It also suggests you haven't noticed the docs for the parameterless constructor to Random. It *doesn't* solve the problem that the OP has encountered. Want me to provide an example showing your code not working? I could probably fit it (unformatted) into a comment...
Jon Skeet
Sample of failure: using System; using System.Collections.Generic; class Test { static void Main(string[] args) { var list = new List<int>(); for (int i = 0; i < 100; i++) { list.Add(AdamCode()); } foreach (int x in list) { Console.WriteLine(x); } } static int AdamCode() { var random = new Random((int)DateTime.Now.Ticks); return random.Next(100); } }
Jon Skeet
+7  A: 

Use a Random object that's created/initialised once and not every time the method is called. Another option is to pass a Random instance into the method when you call it.

You could also create overloads that allow you to do either of the above options:

public static DateTimeOffset Random(this DateTimeOffset value, TimeSpan timeSpan)
{
    if (_threadStaticRng == null)
        _threadStaticRng = new Random();

    return value.Random(timeSpan, _threadStaticRng);
}

public static DateTimeOffset Random(
    this DateTimeOffset value, TimeSpan timeSpan, Random rng)
{
    DateTimeOffset minDateTime = value.Subtract(timeSpan);
    int range = ((DateTime.Today - minDateTime)).Hours;
    return minDateTime.AddHours(rng.Next(range));
}

[ThreadStatic]
private static Random _threadStaticRng;
LukeH
originally, I had the random instance a static object, using double null locking to protect it. I DIDN'T have it thread static, though. This means, i used the same code as u did, in your first method except i had the the null check wrapped in a lock and another null check wrapper. What does ThreadStatic do, compared to just making it static?
Pure.Krome
@Pure.Krome: `ThreadStatic` ensures that each thread uses a different static object. This ensures thread-safety without the need for locks etc.
LukeH
it does just what you think. It is static per thread. Meaning each thread will end up with its own instance.
Svish
The only bad thing about this code is that when it creates a new instance it's still using the current time. If lots of threads start up at the same time, they will all get the same sequence. I think I'll add an answer which addresses this...
Jon Skeet
Cheers Jon :) And i know you know all about time .. even if this isn't about Noda Time that we're discussing :)
Pure.Krome
One thing to note the seemed to be passed over in my answer was that in this case the RNG would always instantiate with the same seed so if you loaded the application and always ran the same data through in the same order you would get the same results.
Adam Gritt
@Adam: No you wouldn't. The `Random` constructor uses `Environment.TickCount` as the seed if you don't pass one in.
LukeH
@Luke: That is odd, my bad then. For some reason the test app I just threw together seemed to always return the same number between successive runs.
Adam Gritt
+5  A: 

As others have said, the problem is that new Random() uses the current time to form the seed, and you're getting the same one lots of times.

Basically you want to create relatively few instances. As Random isn't thread-safe, you need ThreadStatic or ThreadLocal<T> - the latter is new to .NET 4.0. Here's a sample StaticRandom class (using .NET 4.0) which lets you use the Instance property to get a valid instance for this thread. Note that on type initialization, a counter is set from the current time. This is then used for successive seeds.

using System;
using System.Threading;

public static class StaticRandom
{
    private static int seed;

    private static ThreadLocal<Random> threadLocal = new ThreadLocal<Random>
        (() => new Random(Interlocked.Increment(ref seed)));

    static StaticRandom()
    {
        seed = Environment.TickCount;
    }

    public static Random Instance { get { return threadLocal.Value; } }
}

Then you can just use StaticRandom.Instance whenever you need an instance of Random.

Now to get back to the original question, it's not entirely clear what your current extension method is doing. Why are you using DateTime.Today at all? I suspect you want something like:

public static DateTimeOffset Random(this DateTimeOffset value, TimeSpan timeSpan)
{
    double seconds = timeSpan.TotalSeconds * StaticRandom.Instance.NextDouble();

    // Alternatively: return value.AddSeconds(-seconds);
    TimeSpan span = TimeSpan.FromSeconds(seconds);
    return value - span;
}

However, that will give you a completely random time - it's almost bound to be part way through a millisecond, for instance. Is that okay, or do you effectively want it to be an exact number of seconds (or minutes, or hours) based on the original timespan?

Jon Skeet
A nice snippet which I'll add to my library right away :)
Mikael Svenson
@All, any suggestions on making my answer better ?, please point out the negative things that it has and also suggestions to improve it, If u get some time. Thanks!
Mahesh Velaga
@Jon: A completly random time is perfect, but between two time points though. so, if i check your code .. in the Random(..) method, should it instead grab a random seconds (read: int) value between the value.TotalSeconds and timeSpan.TotalSeconds .. eg. StaticRandom.Instance.Next(timespan.TotalSeconds, value.TotalSeconds) .. assuming timeSpan.TotalSeconds is *lower* than value.TotalSeconds? Secondly, I don't know why i used `Today`. That's horrific. I always use `Now`. *blush*
Pure.Krome
@Pure.Krome: No, it's already doing the right thing. It's taking a random span within the given span, and subtracting it from the given date/time. So if you provide "Dec 23rd, 10:39am" and "1 hour" it will give you some time between Dec 23rd 9:39am and Dec 23rd 10:39am.
Jon Skeet
@Pure.Krome: Why would DateTime.Now be relevant either? What relevance does the current time have to your method? If it *is* involved then I don't understand what you're trying to do...
Jon Skeet
@Jon: when i talk about `Now` i was only referring to some sample time which the `value` variable represented. It could be any DateTimeOffset value .. like Dec 23 10:39am. This means i really am blond and don't understand the first two lines of code in your method *blush*. So let me try and understand it (appologies for being a noob at this). Using your dates, the first line says "Dec 23 9:39am (in secs) * a random int between 1<->int32.MaxValue. Then you convert this secs into a TimeSpan. Then u take Dec 23 10:39 - new timespan. What i don't get is the first line, unless i misunderstood it?
Pure.Krome
.. cont .. wouldn't the first line make the seconds value (which is to be converted to a timespan) some Massive, random number? way bigger than the two time points (possibly).
Pure.Krome
No, it takes *one hour* in seconds. It then creates a random timespan between 0 and one hour, and subtracts that from Dec 23rd 10:39am. Dec 23rd 10:39am isn't a TimeSpan, it's a DateTime. A TimeSpan is something like "one hour" or "5 minutes".
Jon Skeet
@Pure.Krome: Note that aside from the way of getting an instance of Random, my answer is functionally equivalent to that of tanascius. I like creating the `TimeSpan` explicitly rather than using `AddSeconds`, but they're really equivalent.
Jon Skeet
@Jon: Ahh, I've got you now. But, er.. that's not how I read your code. Secondly, the code throws an exception: `"TimeSpan overflowed because the duration is too long."` which makes sense if I understand what you're doing. Why are u getting the number of seconds, for 1 hour (or any timespan period .. eg. 1 hour, 1 day. 1 year.. ) and then *multiplying* that (number of seconds) by a random number between 0<->int.maxvalue? I thought u want something like .. `StaticRandom.Instance.Next(timeSpan.TotalSeconds)` and that result u create a new `TimeSpan` explicity, then take that from the `value`. ?
Pure.Krome
Doh - not quite, I meant to use Random.NextDouble, which gives a value between 0.0 and 0.1. Will fix it.
Jon Skeet
AHHHHHHHHHH :) ok. this is making lots more sense? Last question ... is it bad to use a cast instead? eg `var seconds = StaticRandom.Instance.Next((int)timeSpan.TotalSeconds);` I understand that it possible that any decimal's will be truncated .. but i'm wondering if i'm also missing out on something. (i still prefer the NextDouble() solution, but just want to ask about this, while I have your kind attention :) ).
Pure.Krome
Nope, that would be okay so long as the total seconds didn't exceed `int.MaxValue` :) You may want to add 1 so that the range ends up being inclusive - if you want a range over a minute, you want a value in the range [0, 60] which you get by calling Random.Next(61).
Jon Skeet
Thanks kindly Jon for helping me with this and all the questions :) I sincerly appreciate it. Merry Xmas and have a lovely time with the family, etc :) Cheers :)
Pure.Krome