如何获得一个具有线性分布的随机日期?

3

我希望从值(如果我使用了正确的术语,则为反线性分布)中获得随机毫秒值。

实际上,我想要在两个时间点 early late 之间的随机时间点 t (在我的情况下为 Date ),其中 t 趋向于 early 的概率要比趋向于 late 的概率大得多。 late本身可能的概率是0.0

我的当前java代码只使用均匀分布,因此我计划将其修改为(反)线性分布

public Date getRandomDate(Date early, Date late) {
  long diff = late.getTime() - early.getTime();
  final int randVal = rand.nextInt((int) diff);
  Calendar cal = Calendar.getInstance();
  cal.setTime(early);
  cal.add(Calendar.MILLISECOND, randVal);
  return cal.getTime();
}

这个能解决你的问题吗:https://dev59.com/sm025IYBdhLWcg3wf2KE? - GhostCat
1
我不太确定你在问什么。是关于如何获取随机数的线性分布,还是在将其应用于日期时遇到问题? - Thomas
2
顺便说一下,你可以直接使用long构造Date对象(https://docs.oracle.com/javase/7/docs/api/java/sql/Date.html#Date(long))。不需要使用Calendar类。例如:`return new Date(early.getTime()+randVal);` - rustyx
3个回答

3

借鉴这个回答中类似问题的思路,你可以通过两次随机调用并取最小值来完成操作:

final int randVal = Math.min(rand.nextInt((int) diff), rand.nextInt((int) diff));

最后,这里是另一种更复杂的方法,使用累积分布函数(x^2)来解决x:

int randVal = (int) Math.floor(diff * (1.0 - Math.sqrt(rand.nextDouble())));
if(randVal >= diff) randVal = 0; // handle the edge case

为满足您的要求,我们从1.0中减去了平方根,以反转分布,即将更大的密度放置在范围底部。

"min" 更好。我想要线性的,而不是平方的。 - towi
@towi 信不信由你,线性分布 [0,1) 的 CDF 是 x^2。请查看我提供的维基百科链接。或者如果你想要双重确认的话,可以在数学 StackExchange 上查看。 ;) - Patrick Parker

2

Parker提供的答案似乎是正确且良好的。

使用java.time

该问题使用了过时的麻烦日期时间类,这些类现在已被java.time类取代。下面是相同类型的代码以及Parker的解决方案,使用java.time进行重写。

Instant

首先,如果必须使用java.util.Date对象,请将其转换为/from InstantInstant类表示UTC上的时间轴上的一个时刻,分辨率为纳秒(最多九(9)位小数)。要进行转换,请查看添加到旧类中的新方法。

Instant instant = myJavaUtilDate.toInstant();  // From legacy to modern class.
java.util.Date myJavaUtilDate = java.util.Date.from( instant ) ;  // From modern class to legacy.

让我们重写方法签名,但传递和返回Instant对象。
public Instant getRandomDate( Instant early , Instant late) {

验证early参数确实早于later参数。或者断言下面看到的Duration不是负数(!duration.isNegative())。
    if( early.isAfter( late) ) { … }  // Assert `early` is not after `late`.

半开

计算最早和最晚时刻之间的差值。这是使用半开方法定义时间跨度时经常使用的方式,其中开始时间是包括在内的,结束时间是不包括在内的。

持续时间

持续时间类表示这样一段时间,以秒为单位的总数加上纳秒为单位的分数。

    Duration duration = Duration.between( early , late ) ;

进行随机数计算时,我们需要一个整数。为了处理纳秒级的分辨率,我们需要一个64位的long而不是32位的int

ThreadLocalRandom

提示:如果在多个线程中生成这些值,请使用ThreadLocalRandom类。引用文档:

在并发程序中,如果适用,使用ThreadLocalRandom而不是共享的Random对象通常会遇到更少的开销和争用。

我们可以通过调用ThreadLocalRandom::nextLong(origin, bound)以半开放风格指定范围,其中起点是包含的,边界是排除的。
    long bound = duration.toNanos() ;
    long nanos1 = ThreadLocalRandom.current().nextLong( 0 , bound ); 
    long nanos2 = ThreadLocalRandom.current().nextLong( 0 , bound ); 
    long nanos = Math.min( nanos1 , nanos2 );  // Select the lesser number.
    Instant instant = early.plusNanos( nanos );
    return instant ;
}

实例

请查看以下代码在IdeOne.com上的实时运行

我们提取生成的每个日期(仅限LocalDate)的日期时间值数量,以便以一种随意的方式调查结果,以验证我们所需的结果是否偏向较早的日期。

测试工具展示了如何将时区(ZoneId)分配给Instant以获得一个ZonedDateTime对象,并从中提取LocalDate。如果您希望通过某个特定地区的挂钟时间而不是UTC来查看Instant对象,请使用该指南。

/* package whatever; // don't place package name! */

import java.util.*;
import java.lang.*;
import java.io.*;

import java.util.concurrent.ThreadLocalRandom ;
import java.util.TreeMap ;

import java.time.*;
import java.time.format.*;
import java.time.temporal.*;

/* Name of the class has to be "Main" only if the class is public. */
class Ideone
{
    public static void main (String[] args) throws java.lang.Exception
    {
        Ideone app = new Ideone();
        app.doIt();
    }

    public void doIt() {
        ZoneId z = ZoneId.of( "America/Montreal" ) ;
        int count = 10 ;
        LocalDate today = LocalDate.now( z );
        LocalDate laterDate = today.plusDays( count );
        Instant start = today.atStartOfDay( z ).toInstant();
        Instant stop = laterDate.atStartOfDay( z ).toInstant();

        // Collect the frequency of each date. We want to see bias towards earlier dates.
        List<LocalDate> dates = new ArrayList<>( count );
        Map<LocalDate , Integer > map = new TreeMap<LocalDate , Integer >();
        for( int i = 0 ; i <= count ; i ++ ) {
            LocalDate localDate = today.plusDays( i ) ; 
            dates.add( localDate );  // Increment to next date and remember.
            map.put( localDate , new Integer( 0 ) ); // Prepopulate the map with all dates.
        }
        for( int i = 1 ; i <= 10_000 ; i ++ ) {
            Instant instant = this.getRandomInstantBetween( start , stop );
            LocalDate localDate = instant.atZone( z ).toLocalDate();
            Integer integer = map.get( localDate );
            map.put( localDate , integer +  1);  // Increment to count each time get a hit on this date.
        }
        System.out.println( map );
    }

    public Instant getRandomInstantBetween( Instant early , Instant late) {

        Duration duration = Duration.between( early , late ) ;
        // Assert the duration is positive or zero: ( ! duration.isNegative() )

        long bound = duration.toNanos() ;
        ThreadLocalRandom random = ThreadLocalRandom.current() ;
        long nanos1 = random.nextLong( 0 , bound ); // Zero means the `early` date is inclusive, while `bound` here is exclusive.
        long nanos2 = random.nextLong( 0 , bound ); 
        long nanos = Math.min( nanos1 , nanos2 );  // Select the lesser number.
        Instant instant = early.plusNanos( nanos );

        return instant;
    }
}

这里是一些样本结果。我认为它们看起来不错,但我不是统计学家。使用需自担风险。
{2017-02-24=1853,2017-02-25=1697,2017-02-26=1548,2017-02-27=1255,2017-02-28=1130,2017-03-01=926,2017-03-02=706,2017-03-03=485,2017-03-04=299,2017-03-05=101,2017-03-06=0} {2017-02-25=930,2017-02-26=799,2017-02-27=760,2017-02-28=657,2017-03-01=589,2017-03-02=470,2017-03-03=342,2017-03-04=241,2017-03-05=163,2017-03-06=49,2017-03-07=0} {2017-02-25=878,2017-02-26=875,2017-02-27=786,2017-02-28=676,2017-03-01=558,2017-03-02=440,2017-03-03=370,2017-03-04=236,2017-03-05=140,2017-03-06=41,2017-03-07=0}

关于java.time

Java 8及其后续版本内置了java.time框架。这些类取代了旧的、麻烦的遗留日期时间类,如java.util.DateCalendarSimpleDateFormat

Joda-Time项目现在处于维护模式,建议迁移到java.time类。

要了解更多,请查看Oracle教程。并在Stack Overflow上搜索许多示例和解释。规范是JSR 310
如何获取java.time类? ThreeTen-Extra项目扩展了java.time的附加类。该项目是java.time可能未来添加的一个试验场。您可能会在这里找到一些有用的类,例如Interval, YearWeek, YearQuarter以及更多

谢谢你介绍新的Java时间类,我知道它们但还没有被广泛使用。我同意你的看法,也许你的帖子能够帮助纠正这种情况。唯一的担心就是固有的Zawinski“现在我们又多了一个问题”的情况。 - towi
@towi 不确定您引用Zawinski的意思是什么。如果您担心java.time的质量或可靠性,那就不用担心了。java.time类是由同样的人员构建的,由Stephen Colebourne“jodastephen”领导,他们花费了多年时间开发了非常成功的Joda-Time库。他们从中学到的东西,重新设计了java.time。现在,java.time是Java的内置部分,并且具有与Java发布相关的广泛测试。我所知道的唯一错误是解析字符串时的边角情况。 - Basil Bourque
不,我不关心质量。我只是担心有太多的日期/时间API,这很令人困惑。而且你总是会有不同的第三方库需要不同的日期/时间API。因此,你总是在来回转换。这是因为旧的API可能永远不会消失。我每天都使用DateCalendarXMLGrCal、我的自己的WDate和有时候的Jodatime。已经有20种可能的API转换了。现在我将有6*5=30种。不要误解我,我理解需要不同的日期/时间API背后的原因。只是它并不像我希望的那么简单。 - towi
@towi 是的,日期时间是一团糟。整个信息技术行业在如此多年中忽略了这个关键主题,令人震惊。Joda-Time和java.time是我所知道的第一个认真尝试以全面方式解决这个问题的工具。现在我们已经内置了java.time,Joda-Time的使用将被逐步淘汰。是的,Java中的传统日期时间类将与我们共存很多年。我建议您使用添加到这些旧类中的新方法来转换为java.time。在java.time中执行业务逻辑、数据存储和数据交换。只在必要时进行转换。 - Basil Bourque

0
也许你可以像这个答案中所示,将类比应用于日期。
Java:具有非均匀分布的随机整数

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接