Generate a random number in a fully POSIX compliant shell, 'dash'...

Hi all...
Apologies for any typos, etc...
This took a while but it didn't beat me...

Although there are many methods of generating random numbers in a POSIX shell this uses integer maths and a simple C source to create an executable to get epoch to microseconds accuracy if it is needed. I take no credit for the C source, the credit is inside the script.
It is an exercise in futility but I love finding the limits of a language.
This is only tested in OSX 10.14.6 and not Linux at the moment for the C source only as I have no idea if gcc will compile.

#!/usr/local/bin/dash
# #!/bin/sh
#
# Fully POSIX random number generator.
# Wichmann-Hill method.
# https://en.wikipedia.org/wiki/Wichmann%E2%80%93Hill

# Initialise variables as global.
EPOCH=1234567890.987654
SEED1=1
SEED2=1
SEED3=1
VALUE=0
TEST_VAL=1
TEST_NUM=256

# Get epoch value to microseonds.
epoch()
{
cat << 'EPOCH_MICROSECS' > /tmp/epoch_microsecs.c
/* 'epoch_microsecs.c' */
/* Thanks to Perderabo for this little snippet modified to suit my needs. */
/* It saved me the bother of working it out by myself. */
/* https://www.unix.com/programming/1991-time-microseconds-2.html */

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

int main(void)
{
    struct timeval tv;
    struct timezone tz;
    struct tm *tm;
    gettimeofday(&tv, &tz);
    tm=localtime(&tv.tv_sec);
    printf("%ld.%06d", tv.tv_sec, tv.tv_usec);
    exit(0);
}
EPOCH_MICROSECS
# Compile the above using gcc.
gcc -Wall -pedantic -ansi -o /tmp/epoch_microsecs /tmp/epoch_microsecs.c -lm
}

# Use clock timer for seeds.
time_seeds()
{
    # Create the three required seeds using the clock.
    # Each must be between 1 and 30000 inclusive.
    EPOCH=$( /tmp/epoch_microsecs )
    SEED1=${EPOCH#????????????}
    SEED1=$( expr ${SEED1} + 1 )
    if [ ${SEED1} -gt 30000 ]
    then
        SEED1=$(( SEED1 / 4 ))
    fi
    #
    EPOCH=$( /tmp/epoch_microsecs )
    SEED2=${EPOCH#????????????}
    SEED2=$( expr ${SEED2} + 1 )
    if [ ${SEED2} -gt 30000 ]
    then
        SEED2=$(( SEED2 / 4 ))
    fi
    #
    EPOCH=$( /tmp/epoch_microsecs )
    SEED3=${EPOCH#????????????}
    SEED3=$( expr ${SEED3} + 1 )
    if [ ${SEED3} -gt 30000 ]
    then
        SEED3=$(( SEED3 / 4 ))
    fi
}

# Use user defined values for seeds.
fixed_seeds()
{
    # All seeds must be an integer, 1 to 30000!
    # Called as 'fixed_seeds <SEED1> <SEED2> <SEED3><CR>'
    SEED1=${1}
    SEED2=${2}
    SEED3=${3}
    check_seeds
}

# Use external user defined values for seeds. 
manual_seeds()
{
    # All seeds must be an integer, 1 to 30000!
    printf "Enter first integer seed, 1 to 30000:- "
    read -r SEED1
    printf "Enter second integer seed, 1 to 30000:- "
    read -r SEED2
    printf "Enter third integer seed, 1 to 30000:- "
    read -r SEED3
    check_seeds
}

# Check user seeds for basic errors.
# Any other errors are taken care of by the shell itself.
check_seeds()
{
    if [ "${SEED1}" = "" ] || [ "${SEED2}" = "" ] || [ ${SEED3} = "" ]
    then
        echo "Usage1: fixed_seeds <SEED1> <SEED2> <SEED3>"
        echo "OR..."
        echo "Usage2: manual_seeds and then follow the on screen prompts!"
        echo "Aborting..."
        exit 1
    fi
    if [ ${SEED1} -lt 1 ] || [ ${SEED2} -lt 1 ] || [ ${SEED3} -lt 1 ]
    then
        echo "ERROR! One or more of the seeds are wrong! Aborting..."
        exit 2
    fi
    if [ ${SEED1} -gt 30000 ] || [ ${SEED2} -gt 30000 ] || [ ${SEED3} -gt 30000 ]
    then
        echo "ERROR! One or more of the seeds are wrong! Aborting..."
        exit 3
    fi
}

# This is the working part of this Wichmann-Hill random module...
whrandom()
{
    SEED1=$(( ( 171 * SEED1 ) % 30269 ))
    SEED2=$(( ( 172 * SEED2 ) % 30307 ))
    SEED3=$(( ( 170 * SEED3 ) % 30323 ))
    
    VALUE=$(( ( ( SEED1 * 1000000000 ) / 30269 ) + ( ( SEED2 * 1000000000 ) / 30307 ) + ( ( SEED3 * 1000000000 ) / 30323 ) ))
    VALUE=$(( VALUE % 1000000000 ))

    printf "%.9f" "${VALUE}e-9"
}

# Test loop only, press Ctrl-C to stop...
test()
{
    while true
    do
        # Save fixed point value to disk...
        # For some reason this does not work, 'TEST_VAL=$( whramdom )'. ;'(
        whrandom > /tmp/VAL
        # Immediately read it back.
        read -r TEST_VAL < /tmp/VAL
        # Remove any leading zeros, (0).
        TEST_VAL=$( expr ${TEST_VAL#??} + 0 )
        # Give a test number, 256 for this DEMO so as to have 0 to 255 values.
        TEST_NUM=256
        # Multiply the two variables together......
        TEST_VAL=$(( TEST_VAL * TEST_NUM ))
        # ......AND correct for the fixed point position removing anything beyond a decimal point.
        printf "%.0f\n" "${TEST_VAL}e-9"
    done
}

epoch

time_seeds

test

Results OSX 10.14.6, using dash called from default shell...

Last login: Sat Jan 18 21:21:52 on ttys000
AMIGA:amiga~> cd Desktop/Code/Shell
AMIGA:amiga~/Desktop/Code/Shell> ./WH_random_POSIX.sh
98
49
123
233
4
90
73
145
56
198
166
10
205
110
153
.
.
.
.
155
235
141
245
242
45
50
^C
AMIGA:amiga~/Desktop/Code/Shell> _

Looking forwards to input from the big guns to improve upon it...

4 Likes

Thanks so much for posting these kinds of projects. I wish everyone would do this, regardless of their skill level. Sharing is the operative word these days.

On a similar note:

In 2020, I prefer we call on less experienced users to ask questions about code, versus the "old style from before 2019" of asking "experts" and "heavy hitters" to improve code.

One of my main goals at unix.com in 2020 is to encourage new users and people with less experience to feel good about asking questions and coding, even if they make a lot of mistakes, and their code is not "pretty", versus promoting "better, cleaner, shorter, standards-based code"......

If we have an environment for the "big guns" or the less than 1% or even 0.1% (or less), this will discourage the 99.9% of people (or more) who are not "big guns"...

I'm trying hard to refocus the site away from "elitism" as was the focus for the past number of years (by evolution, not be design), and toward "sharing", especially for beginners and those who may not normally feel comfortable sharing their code and projects with "experts" or "big guns". This also applies to the latest tech (not legacy tech), where everyone is learning together.

"Learn By Sharing Regardless of Your Skill Level"

Thanks so much!

2 Likes

Better replace all expr with arithmetics by the builtin=faster $(( ))

    SEED1=$(( ${SEED1} + 1 ))
TEST_VAL=$(( 0${TEST_VAL#??} ))

As the clock is very quickly cycling inside the microsecond part there is always a possibility of these values '00000' to '09999' occurring. These are obviously NOT octal but decimal with leading zeros so......
......I used 'expr' to remove leading zeros otherwise there is a major shell error, thus I can't see how your modification works.
The maths method does not remove those leading zeros...

Well the C source does not compile on a current update, 19-01-2020, Linux Mint 19 using gcc 7.4.0 so here is a completely simplified and modified version for gcc 4.2.1 and 7.4.0...
The printf function will need to edited for it to work.
It is set up for Linux, (Mint 19), using gcc 7.4.0.
You can still leave to original as it is for OSX 10.14.6+ as it won't affect the performance at all, but you can edit this in instead and have both capable with the minor alteration shown...

/* 'epoch_microsecs.c' */
/* Thanks to Perderabo for this little snippet modified to suit my needs. */
/* It saved me the bother of working it out by myself. */
/* https://www.unix.com/programming/1991-time-microseconds-2.html */

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>

int main(void)
{
    struct timeval tv;
    gettimeofday(&tv, NULL);

    /* Line below for gcc version 4.2.1, OSX 10.14.6... */
    /* printf("%ld.%06d", tv.tv_sec, tv.tv_usec); */

    /* Line below for gcc version 7.4.0, Linux Mint 19... */
    printf("%ld.%06ld", tv.tv_sec, tv.tv_usec);

    exit(0);
}

I see. Here is how to strip leading zeros with Posix-shell-builtins:

 num=${TEST_VAL#??}
 lz=${num%%[1-9]*}
 TEST_VAL=${num#$lz}

Don't know if this is really faster than expr.

Hi MadeInGermany...
Although there is a workaround this is an edge case:

#!/usr/local/bin/dash
# lead_zeros.sh

# This is a gotcha...
# There will always be 6 zeros sooner or later, 000000.
 
# Only the last 6 digits are really needed... 
TEST_VAL=0.000000
TEST_NUM=123

num=${TEST_VAL#??}
echo "${num}"
lz=${num%%[1-9]*}
echo "${lz}"
TEST_VAL=${num#$lz}

echo "Test value = ${TEST_VAL}..."

TEST_VAL=$(( TEST_VAL * TEST_NUM ))

echo "Required integer = ${TEST_VAL}..."

Results:

Last login: Mon Jan 20 21:18:42 on ttys000
AMIGA:amiga~> cd Desktop/Code/Shell
AMIGA:amiga~/Desktop/Code/Shell> chmod 755 lead_zeros.sh
AMIGA:amiga~/Desktop/Code/Shell> ./lead_zeros.sh
000000
000000
Test value = ...
./lead_zeros.sh: 19: ./lead_zeros.sh: Illegal number: 
AMIGA:amiga~/Desktop/Code/Shell> _

EDIT:
This is a workaround...
TEST_VAL=$(( (${TEST_VAL} + 0 ) * TEST_NUM ))

Sorry for this beeing an off-topic quote but I never had the feeling that this forum was for elitists only.
Well, at least not back in 2016 up to my last activity here.

When I had joined the forum here, I didnt consider myself experienced, but I did feel welcomed.
All I can say is thank you (team & everyone here) and keep it up!

2 Likes

Good to know. Thanks for the feedback.

That has been one of my #1 objectives, since forming the site for users, decades ago.

1 Like