Fortune
April 5, 2011
It is possible to read the entire fortunes file into an array of lines, then select one line at random, but instead we choose a streaming algorithm: choose the first line with probability 1/1, then replace it with the second line with probability 1/2, then replace it with the third line with probability 1/3, and so on, so that when all the lines have been read each line will have been chosen with probability 1/n, where n is the number of lines in the file. Here’s the code:
(define (fortune file-name)
(with-input-from-file file-name
(lambda ()
(let loop ((line (read-line)) (n 1) (fort ""))
(if (eof-object? line) fort
(loop (read-line) (+ n 1)
(if (< (rand) (/ n)) line fort)))))))
The key is the expression (< (rand) (/ n))
. If the random fraction is less then 1/n, fort
is replaced by the current line
.
We need to reseed the random number generator so that every run of the program produces a different aphorism. We use the current date and time, which Chez Scheme returns in the standard Unix date
format Sat Apr 2 12:06:22 2011
when calling (date-and-time)
; other Scheme systems have other conventions. We also have to check if the user entered a filename on the command line. Here’s the main program:
(define (get-digits str)
(string->number (list->string (filter char-numeric? (string->list str)))))
(rand (get-digits (date-and-time)))
(display (fortune
(if (null? (cdr (command-line)))
"/usr/games/lib/fortunes"
(cadr (command-line)))))
(newline)
We used filter, read-line and rand from the Standard Prelude. You can see the entire program, complete with the Chez Scheme header that invokes a program from the command line, at http://programmingpraxis.codepad.org/X3dRp0va.
The original C source code of the Unix V7 fortune
command is shown below:
#include <stdio.h>
char line[500];
char bline[500];
main()
{
double p;
register char * l;
long t;
FILE *f;
f = fopen("/usr/games/lib/fortunes", "r");
if (f == NULL) {
printf("Memory fault -- core dumped\n");
exit(1);
}
time(&t);
srand(getpid() + (int)((t>>16) + t));
p = 1.;
for(;;) {
l = fgets(line, 500, f);
if(l == NULL)
break;
if(rand() < 32768./p)
strcpy(bline, line);
p += 1.;
}
fputs(bline, stdout);
return(0);
}
[…] today’s Programming Praxis exercise, our goal is to implement the fortune Unix command line tool. […]
My Haskell solution (see http://bonsaicode.wordpress.com/2011/04/05/programming-praxis-fortune/ for a version with comments):
A Python solution:
Beauty of awk :
My first solution is similar to Dave Webb’s, but I also wrote up an iterative
function that will use less resources if the fortune file is large.
Woops, line 14 should read
i = 0 if randrange(n) >= 1 else 1
After writing my above solution, I took this as an opportunity to play
with pylint a sourcecode analyzer.
My new version is available here.
Another variation on solutions posted by Dave and Graham.
Nice one Mike; I need to learn more about the itertools module.
Quick question, though:
randint(0, n)
uniformly picks a random integer from[0, n]
(inclusive), so doesn’t this use the probability
1/(n + 1)
instead of1/n
?@Graham. Yes, my program calculates the probability as 1/(n+1). However, n starts at 0.
So, for fortune lines 0, 1, 2, …, the probabilities are 1/1, 1/2, 1/3, ….
This is the same as n starting at 1 and calculating the probability as 1/n.
An version in Common Lisp:
and a completely golfed command line Perl version
perl -ne 'rand($.)<1&&($l=$_)}{print$l'
just because the awk version was still readable.@Mike: My mistake! Thanks for clearing that up.
In F#,
[…] reimplemented a basic version of Fortune. This version prints to the standard output a randomly chosen line from a text file. You can […]