12

What is needed here is a command that generates six dates given a range of years (1987 to 2017). For example:

12/10/1987
30/04/1998
22/02/2014
17/08/2017
19/07/2011
14/05/2004

How it could be done, with sed, gawk, etc?

  • Why do you see awk as the canonical date generator? Are you open to the use of other tools? – Jeff Schaller Oct 17 at 18:55
  • when you say "given date" do you mean "generate 6 random dates between these two years" or did you want those days/months for some reason? – Jeff Schaller Oct 17 at 18:57
  • 2
    Maybe yes, it could be with others, but currently studying those GNU tools, and yes, it means "generate 6 random dates between that yearly intervals, as of 1987 to 2017, in this example. Any idea? – Raymond83 Oct 17 at 19:00
  • 9
    I would urge you to use date format YYYY-mm-dd -- that format sorts the same lexically and chronologically. – glenn jackman Oct 17 at 21:03
  • @glennjackman +1. it is the correct way as it is the beginning of a iso8601 date. ( yyyy-mm-ddThh:mm:ss[.mmm] +hh:ss ) – Olivier Dulac Oct 18 at 6:57
10

You can turn the problem into generating a random number between a number representing the first possible date and a number representing the last possible date (actually the one right after the last possible), in unix epoch format. Everything else is handled by standard date conversions. gawk has a better random number resolution than bash (float vs 15 bits integer), so I'll be using gawk. Note that the rand() result N is a float such that 0 <= N < 1, that's why the higher limit is increased below, it's a limit that can't be rolled. There's an optional 3rd parameter for the number of results.

#!/usr/bin/gawk -f
BEGIN {
    first=mktime(ARGV[1] " 01 01 00 00 00")
    last=mktime(ARGV[2]+1 " 01 01 00 00 00")
    if (ARGC == 4) { num=ARGV[3] } else { num=1 }
    ARGC=1
    range=last-first
    srand(sprintf("%d%06d", systime(), PROCINFO["pid"]))
    for (i=1; i <= num; i++) {
        print strftime("%d/%m/%Y", range*rand()+first)
    }
}   

For example:

./randomdate.gawk 1987 2017 6
26/04/1992
28/04/2010
21/04/2005
17/02/2010
06/10/2016
04/04/1998
  • 1
    Move the print strftime line into the BEGIN block so that you don't need to pipe an empty line into the awk program. awk is quite happy to run a program with just a BEGIN block with no input. – glenn jackman Oct 17 at 21:02
  • 1
    You don't need to call out to date to seed: srand(systime()) -- or for greater than once per second you could srand(sprintf("%d%06d", systime(), PROCINFO["pid"])) with GNU awk – glenn jackman Oct 17 at 21:19
  • 1
    Well on most lower end Linux systems (where I have some knowledge so used as example), the pid has only 32768 values possible by default, so there would be only 32768 available dates. and if using seconds, it would show the same result in an outer loop for results in the same second – A.B Oct 17 at 21:22
  • 1
    Why don't you loop inside awk? – glenn jackman Oct 17 at 21:25
  • 1
    @glennjackman actually I misread your previous comment, I can indeed assume there won't be more than ~ 32000 runs of the command in 1s. So I'll get rid of the external date dependency – A.B Oct 19 at 9:10
19

With date, shuf and xargs:

Convert start and end date to "seconds since 1970-01-01 00:00:00 UTC" and use shuf to print six random values in this range. Pipe this result to xargs and convert the values to the desired date format.

Edit: If you want dates of the year 2017 to be included in the output, you have add one year -1s (2017-12-31 23:59:59) to the end date. shuf -i generates random numbers including start and end.

shuf -n6 -i$(date -d '1987-01-01' '+%s')-$(date -d '2017-01-01' '+%s')\
 | xargs -I{} date -d '@{}' '+%d/%m/%Y'

Example output:

07/12/1988
22/04/2012
24/09/2012
27/08/2000
19/01/2008
21/10/1994
  • The example used by the OP for 1987 to 2017 has an example result in 2017, so the OP presumably means for the years to be inclusive. – icarus Oct 17 at 22:13
  • @icarus Good catch, thank you! – Freddy Oct 17 at 22:20
10

Perl:

perl -MTime::Piece -sE '
    my $t1 = Time::Piece->strptime("$y1-01-01 00:00:00", "%Y-%m-%d %H:%M:%S");
    my $t2 = Time::Piece->strptime("$y2-12-31 23:59:59", "%Y-%m-%d %H:%M:%S");
    do {
        my $time = $t1 + int(rand() * ($t2 - $t1));
        say $time->dmy;
    } for 1..6;
' -- -y1=1987 -y2=2017

Sample output:

10-01-1989
30-04-1995
10-12-1998
02-01-2016
04-06-2006
11-04-1987
9

Here's one way, mostly in awk:

#!/bin/sh

[ "$end" -ge "$start" ] || exit 1

awk -v start=$1 -v end=$2 '
  BEGIN {
    srand();
    for(i=1; i <= 6; i++)
      printf "%02d/%02d/%d\n", 1 + rand() * 28, 1 + rand() * 12, start + rand() * (end-start);
  }
' < /dev/null

The shell script takes two parameters and passes them as variables to awk, who reads no input and does all the work in the BEGIN block.

After seeding the random number generator, it loops 6 times over a printf statement. That print statement selects a subset of the possible dates in the range by generating a random number between 1 and 28 for the day (being safe for February), between 1 and 12 for the month, and between the given start and end years. It's random, but it's not full coverage -- it'll never print days 29-31 for months that have them.

Another way, using GNU date and bash features:

#!/bin/bash

start=$1
end=$2

[[ "$start" -le "$end" ]] || exit 1

startsec=$(date -d "1/1/$start" +%s)
for((i=1; i<=6; i++))
do
  r=$((RANDOM % (1 + end - start)*365*24*60*60))
  date -d @$((startsec + r)) +%d/%m/%Y
done

It works by computing the seconds-since-the-epoc of Jan 1st on the start date, then for each of the loops, it comes up with a random number of offset seconds to add; the random number is limited to the number of seconds spanning the given range. GNU date then manipulates that date into the desired format.

  • First way returned ./Jeff: 3: [: Illegal number:, second returned date: invalid date ‘1/1/’ and 31/12/1969 6 times equal. – Raymond83 Oct 20 at 15:21
4

If the difference in the number of years is less than about 90 you can use the $RANDOM variable in bash to give you an offset in number of days and use the limited ability of the date command to do the calculation.

#!/bin/bash
s=$(date +%s -d "1/1/$1")          # start in seconds since 1 Jan 1970
e=$(date +%s -d "1/1/$(($2+1))")   # start of end year +1 in seconds
days=$(((e-s)/(24*3600)))          # number of days from start to end
factor=$((32767/$days))            # RANDOM is 0 to 32767. See how many
toobig=$(($factor*$days))          # exact multiples of days.
                                   # if RANDOM is too large, draw again
for((i=0;i<${3:-1};i++))           # produce $3 random dates
do
    r=$RANDOM                      # find a random number < toobig
    while (( r >= toobig ))        # if toobig, then loop.
    do r=$RANDOM
    done
    offset=$(($r/$factor))         # get (0,days) from (0,factor*days)
    # output horrible day/month/year for N days past start date
    date -d "$offset days 1/1/$1" +%d/%m/%Y
done

The inner loop to select the random number attempts to correct for bias. If a random number source could give you 0, 1, 2, 3, 4, 5, 6, 7, 8 or 9 equally and you want a random number between 0 and 3 inclusive, then if you get 0 or 1 from the source you report 0, if you get 2 or 3, then you report 1, if you get 4 or 5 you report 2, if you get 6 or 7 you report 3, and if you get 8 or 9 you ignore this number from the source.

  • Returned date: invalid date ‘1/1/’ ./Icarus: line 11: syntax error in conditional expression ./Icarus: line 11: syntax error near toobig' ./Icarus: line 11: while [[ r >= toobig ]] – Raymond83 Oct 20 at 15:31
  • The script needs to be run with 3 parameters, the two years and the number of dates, so ./Icarus 1987 2017 6. I have corrected the syntax error. – icarus Oct 20 at 16:03
  • Its excellent your script, but it doesn't works with year intervals bigger than 89 years, as ./Icarus 1900 1989 52 doesn't works, but ./Icarus 1900 1987 90 works swiftly. – Raymond83 Oct 20 at 16:19
  • The answer points out that that it is limited. The $RANDOM variable in bash gives a number between 0 and 32767. 32767 days is 89.7 years. – icarus Oct 20 at 17:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.