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.

Advertisement

Pages: 1 2 3

14 Responses to “Fortune”

  1. […] today’s Programming Praxis exercise, our goal is to implement the fortune Unix command line tool. […]

  2. 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"]) =<< getArgs
    
  3. Dave Webb said

    A 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)),
    
  4. arturasl said

    Beauty of awk :

    #!/bin/awk
    BEGIN{srand()}
    {if(rand()<=1/++n)l=$0}
    END{print l}
    
  5. Graham said

    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)
    
  6. Graham said

    Woops, line 14 should read i = 0 if randrange(n) >= 1 else 1

  7. Graham said

    After writing my above solution, I took this as an opportunity to play
    with pylint a sourcecode analyzer.
    My new version is available here.

  8. Mike said

    Another variation on solutions posted by Dave and Graham.

    from collections import deque
    from itertools import count, compress
    from random import randint
    from sys import argv
    
    FORTUNES = 'fortunes.txt'
    
    with open(argv[1] if argv[1:] else FORTUNES, 'rt') as f:
       print deque(compress(f, (not randint(0,n) for n in count())), maxlen=1).pop()
    
  9. Graham said

    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 of 1/n?

  10. Mike said

    @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.

  11. Ross said

    An version in Common Lisp:

    (defun fortune (filename)
      "Return a Unix V7 fortune"
      (with-open-file (s filename :direction :input)
        (loop :with fortune
              :for line = (read-line s nil)
              :while line
              :counting line :into counter
              :do (when (< (random counter) 1) (setq fortune line))
              :finally (return fortune))))

    and a completely golfed command line Perl version perl -ne 'rand($.)<1&&($l=$_)}{print$l' just because the awk version was still readable.

  12. Graham said

    @Mike: My mistake! Thanks for clearing that up.

  13. Khanh Nguyen said

    In F#,

    open System
    let rnd = new Random()
    let lines = System.IO.File.ReadAllLines("fortunes.txt")
    Seq.nth (rnd.Next(Seq.length lines)) lines
    
  14. […] reimplemented a basic version of Fortune. This version prints to the standard output a randomly chosen line from a text file. You can […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: