Fortune
April 5, 2011
We have today another in our occasional series of re-implementations of Unix V7 commands. The fortune command prints a random aphorism on the user’s terminal; many people put the fortune command in their login script so they get a new aphorism every time they login. Here’s the man page:
NAME
fortune
SYNOPSIS
fortune [ file ]
DESCRIPTION
Fortune prints a one-line aphorism chosen at random. If a file is specified, the sayings are taken from that file; otherwise they are selected from /usr/games/lib/fortunes.
FILES
/usr/games/lib/fortunes - sayings
The fortunes are stored in a file, one fortune per line. The original Unix V7 fortunes file, available from http://minnie.tuhs.org/cgi-bin/utree.pl?file=V7/usr/games/lib/fortunes, is reproduced at the end of the exercise.
Your task is to implement the Unix V7 fortune program. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below.
[…] 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):
import Control.Monad import System.Environment import System.Random chance :: Int -> Int -> a -> a -> IO a chance x y a b = fmap (\r -> if r < x then a else b) $ randomRIO (0, y-1) fortune :: [a] -> IO a fortune = foldM (\a (n,x) -> chance 1 n x a) undefined . zip [1..] main :: IO () main = putStrLn =<< fortune . lines =<< readFile . head . (++ ["fortunes.txt"]) =<< getArgsA Python solution:
import random,sys filename = sys.argv[1] if len(sys.argv) > 1 else '/usr/games/lib/fortunes' with open(filename) as fortunes: print random.choice(list(fortunes)),Beauty of awk :
#!/bin/awk BEGIN{srand()} {if(rand()<=1/++n)l=$0} END{print l}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.
#!/usr/bin/env python from random import choice, randrange, seed from sys import argv def whole_file(f): # easily written, good if f is smallish return choice(f.readlines()) def iter_file(f): # good if f is large curr_line, n = None, 0 for line in f: n += 1 i = 0 if randrange(n) > 1 else 1 curr_line = [curr_line, line][i] return curr_line def main(f, func=iter_file): print func(f) return None if __name__ == "__main__": seed() f_name = argv[1] if len(argv) > 1 else "fortunes.txt" with open(f_name) as f: main(f)Woops, line 14 should read
i = 0 if randrange(n) >= 1 else 1After 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#,
open System let rnd = new Random() let lines = System.IO.File.ReadAllLines("fortunes.txt") Seq.nth (rnd.Next(Seq.length lines)) lines[…] reimplemented a basic version of Fortune. This version prints to the standard output a randomly chosen line from a text file. You can […]