Year: 2013

  • Printing offset page ranges

    I was printing a document the other day that contained some colour pages. Being a cheapskate, I printed the whole document to the black-and-white laser printer down the hall, then printed the colour pages on the more expensive colour printer.

    Unfortunately, I had just written down the ranges of the pages to print using the page numbers on the document, but I had forgotten that the page number had started again from page 12 in the actual document. This meant I had printed all the ranges 12 pages shifted on the colour document. It wasn’t a complete loss, as some of the pages overlapped. How to print only the pages which hadn’t been printed already? Unix shell to the rescue!

    I came up with the following one-liner which saved me literally minutes of menial counting:

    echo {46..50} {52..56} {59..68} 70 
    | tr " " "n" 
    | awk '{printed[$1+12]++; printed[$i]--} END {for (i in printed) if (printed[i]==1) print i}' 
    | sort | tr "n" "," 

    This yields a nice comma separated list of pages that I can print:

    58,71,72,73,74,75,76,77,78,79,80,82

    I had also previously written a range parsing class in Python, which looks like this:

    class Range(object):
        """ class containing the idea of non-contiguous ranges """
        def __init__(self, s):
            """ Parse range from string """
            self.string = s
            self.items = []
            for group in s.split(','): # 1-2,5-7,9
                if '-' in group:
                    [start, stop] = [int(e) for e in group.split('-')]
                else:
                    start = int(group)
                    stop = start
                items = range(start, stop+1)
                self.items = self.items + items
        def __iter__(self):
            return iter(self.items)
           
        def __str__(self):
            return self.string
            
        def __repr__(self):
            return "Range('" + str(self) + "')" 
    

    With that defined, I could also solve the problem like this in a python console:

    wantedtoprint = set(Range('46-50,52-56,59-68,70').items)
    printed = set(i+12 for i in wantedtoprint)
    sorted(printed.difference(wantedtoprint))
    
    => [58, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 82]
    

    This is why I always have a terminal open.

  • Higher order tool use

    Humans are tool-users. It is difficult to make it through the day without using one. Even if you are cast into the wild, naked, you will find yourself using stones to break things, sticks for leverage and so on. I’ve been thinking about tool use a lot lately, especially in the context of learning.

    Our world is ever more equipped with useful tools. You are reading this post on a vastly powerful computational tool (the computer), which has been fitted with additional functionality in the form of programmes or applications. In the “real” world, there are also a multitude of specialised tools to solve almost every physical problem. Much of problem solving has become a question of finding the correct tool, rather than having to work out how to solve it “yourself”. This has made each one of us more productive, but I believe it is also making higher-order tool use less common.

    I think of first-order tool use as something like using a drill to make holes or a hammer for hitting nails. By “higher order” tool use I mean the idea of using other tools to make a tool to solve your problem. I suppose second-order would be to make a machine to make screws and third order would be building a factory which produced these tools. Somewhere in between is sequential tool use, like constructing a table by using a number of tools like saws, drills and screw drivers in sequence.

    It appears to me that there are so many useful tools around that it does not occur to many people to construct their own tools anymore. I see this with students who don’t construct jigs to help them with positioning parts, and I see it often in people using computers. I suppose it could be argued that building a spreadsheet is second-order tool use, as the same spreadsheet can solve different problems when the numbers are changed, but the activity feels like first order tool use, as mostly you are solving the problem you are working on now.

    I have been inspired by this talk by Richard Hamming, specifically his approach of solving problems in the general form. I quote the relevant part here, the emphasis is my own:

    I was solving one problem after another after another; a fair number were successful and there were a few failures. I went home one Friday after finishing a problem, and curiously enough I wasn’t happy; I was depressed. I could see life being a long sequence of one problem after another after another. After quite a while of thinking I decided, “No, I should be in the mass production of a variable product. I should be concerned with all of next year’s problems, not just the one in front of my face.” By changing the question I still got the same kind of results or better, but I changed things and did important work. I attacked the major problem – How do I conquer machines and do all of next year’s problems when I don’t know what they are going to be? How do I prepare for it? How do I do this one so I’ll be on top of it? How do I obey Newton’s rule? He said, “If I have seen further than others, it is because I’ve stood on the shoulders of giants.” These days we stand on each other’s feet! 

    You should do your job in such a fashion that others can build on top of it, so they will indeed say, “Yes, I’ve stood on so and so’s shoulders and I saw further.” The essence of science is cumulative. By changing a problem slightly you can often do great work rather than merely good work. Instead of attacking isolated problems, I made the resolution that I would never again solve an isolated problem except as characteristic of a class.

    This seems to me the driver behind second-order tool use: the realisation that building a tool and then using it to solve your problem could be better than simply solving your problem with the tools you have to hand. Higher order tool use is an example of generalisation.

    Thinking like this also leads you to choose your tools differently. You end up with a box full of general purpose tools that are good for building other tools rather than single purpose tools that simply solve a particular problem.

  • Word count program comparison

    Recently one of my favourite topics came up over dinner: the relative power of programming languages, in the guise of a programming challenge the person talked about as an interview question. I asked him to forward it on to me, and here it is:

    Pride and Prejudice, A Christmas Carol and A Tale of Two Cities can be downloaded from http://www.gutenberg.org/browse/scores/top. Write a program in your favourite language to count the number of times every word in each book is used. Also calculate the top 10 pairs of words in each book. For example, “in the” and “at most” are words that are frequently found together.

    Send back a list of the top 10 words in each book, the top 10 words in each book that do not appear in the other two and lastly the top 10 word pairs that are found together in each book. Include in each list the number of times each word or word pair appeared in the book. Clean up your source code and add comments to it, so that it is easy to understand, and send that along too.

    First, I downloaded these books and converted them to unix line-endings, then I wrote some programs to do the same job.

    Shell

    I wrote the following script in almost no time at all. This is the kind of task that the Unix shell really excels at, with much of the heavy lifting being done in the uniq routine. The sort | uniq -c

    is a really idiomatic way of counting things.

    for f in pg*.txt; do
        echo $f
        echo "Words: 1"
        tr -s "t " "n" < $f | sort | uniq -c | sort -nr | head
        echo "Words: 2"
        tr -s "t " "n" < $f | awk 'NR>1 {print w" "$0} {w=$0}' | sort | uniq -c | sort -nr | head
    done

    Python

    My next try was Python. This took a little longer to write, but appeared to run a bit faster. This is not surprising as the above code traverses each text file twice and contains four sorts. The Python code uses the really fast dictionary object to count the words, rather than using the sort and count approach in the shell code.

    import glob
    from collections import defaultdict
    from operator import itemgetter
    
    topN = 10 # Display this number of elements in the report
    
    for filename in glob.glob('pg*.txt'):
        # Parse
        previousword = None
        # Create a single-word and double-word counter
        counters = [defaultdict(int), defaultdict(int)]
        for word in open(filename, 'r').read().split():
            counters[0][word] += 1
            if previousword: counters[1][previousword + ' ' + word] += 1
            previousword = word
    
        # Report
        print "File:", filename
        for i, c in enumerate(counters):
            print "Length", i+1
            # Sort the counters by their counts, reverse order, take topN elements
            for item in sorted(c.items(), reverse=True, key=itemgetter(1))[:topN]:
                print "%-20s%5i" % item
    

    C++

    The C++ version follows the same strategy as the Python program, using the C++ standard library equivalents of Python’s dictionaries and lists. I’ve left out the globbing, as the C++ STL doesn’t have functions for that. The typical use of this kind of commandline function would rely on the shell for globbing as I have here. Of course, if your shell is CMD.EXE you’re SOL. This program didn’t take that long to write, but it took ages to debug due to the heavy use of templates.

    #include <iostream>
    #include <map>
    #include <string>
    #include <cmath>
    #include <fstream>
    #include <iterator>
    #include <vector>
    #include <algorithm>
    
    using namespace std;
    
    bool value(const pair<string, int>& a, const pair<string, int>& b)
    { 
      return a.second > b.second;
    }
    
    
    int main (int argc, char *argv[]) {
      for (int i=1; i<argc; i++) {
        ifstream infile(argv[i]);
        map<string, int> counter[2];
    
        cout << argv[i] << endl;
    
        // read file
        string word, previousword;
        bool firstword=true;
    
        while (infile >> word) {
          counter[0][word]++;
          if (!firstword) {
            counter[1][previousword + " " + word]++;
            previousword = word;
          } else {
            firstword = false;
          }
        }
    
        for (int c=0; c<2; c++) {
          cout << "Words:" << c+1 << endl;
    
          // sort
          vector<pair<string,int> > wordlist;
          copy(counter[c].begin(), counter[c].end(), 
               back_inserter<vector<pair<string, int> > >(wordlist));
          sort(wordlist.begin(), wordlist.end(), &value);
        
          // write output
          for (int j=0; j < 10; j++) {
            cout << wordlist[j].first << ": " << wordlist[j].second << endl;
          }
        }
      }
    }

    Java

    The guy had said that a typical Java program to do this was about 100 lines. Here is my attempt. Again, I have tried to keep the basic philosophy the same as with the Python program, using the Java standard library for maps and sorting rather than getting clever. Interestingly, this Java program is both the largest and the slowest of the lot. The Eclipse IDE cut a little of the time off writing this, and the Java generics are much easier to debug than C++ templates.

    import java.io.*;
    import java.util.*;
    
    public class ngrammer {
    
      public static void main(String[] args) throws FileNotFoundException {
    
        class WordCounter extends HashMap<String, Integer> {
    
          class ValueComparator implements Comparator<Map.Entry<String, Integer>> {
            // Comparator to allow sorting on the value of the HashMap elements
            @Override
            public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
              return o2.getValue() - o1.getValue();
            }
          }
    
          void add(String word) {
            if (containsKey(word)) {
              put(word, get(word)+1);
            } else {
              put(word, 1);
            }
          }
            
          void printN(Integer N) {
            // Prints the top N elements of the counter
            // Make a list of the counter entries
            List<Map.Entry<String,Integer>> entries = new LinkedList<Map.Entry<String,Integer>>(entrySet());
            // Sort it on the value
            Collections.sort(entries, new ValueComparator());
            // Print out at most N items       
            int i=N;
            for (Map.Entry<String,Integer> entry: entries) {
              System.out.println(entry.getKey() + ": " + entry.getValue());
              if (--i == 0) break;
            }
          }
        }
            
        final int topN = 10;
        
        // Process each file in the input arguments
        for (int fi = 1; fi < args.length; fi++) {
          File file = new File(args[fi]);
          // Initialise new counters
          WordCounter[] counter = {new WordCounter(), new WordCounter()};
          String word, previousword="";
          // Tokenise file
          Scanner scanner = new Scanner(file);
          while (scanner.hasNext()) {
            word = scanner.next();
            counter[0].add(word);
            if (!previousword.isEmpty()) 
              counter[1].add(previousword + " " + word);
            previousword = word;
          }
          // Report
          System.out.println("Filename:" + file.getAbsolutePath());
          for (int i = 0; i < counter.length; i++) {
            System.out.println("Words: " + (i+1));
            counter[i].printN(topN);
          }
        }
      }
    }
    

    C#

    Note that this is my least fluent language, so this may be inelegant. Also, before you say it, I did have a version that used Linq. This was much more succinct, but it was really dog slow. I was really surprised that this program was the fastest of all the ones on this page, especially since I was running it on Mono.

    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    
    class MainClass
    {
    
        class WordCounter : Dictionary<string, int>
        {
            public void CountWord (string word)
            {
                if (ContainsKey (word)) {
                    this [word]++;
                } else {
                    this [word] = 1;
                }
            }
    
            public void Report (int N)
            {
                var list = this.ToList ();
           
                list.Sort ((a, b) => { return b.Value.CompareTo (a.Value); });
                foreach (var item in list.Take(N)) {
                    Console.WriteLine(item.Key + ": " + item.Value);
                }
            }
        }
        
        public static void Main (string[] args)
        {
          const int Ntop=10;
    
          foreach (string f in args) {
             WordCounter[] wordcounter = {new WordCounter(), new WordCounter()};
    
                string lastword = "";
                foreach (var word in File.ReadAllText(f).Split(' ')) {
                    wordcounter[0].CountWord(word);
                    if (lastword.Length > 0) {
                        wordcounter[1].CountWord (lastword + " " + word);
                    }
                    lastword = word;
                }
    
             Console.WriteLine(f);
             for (int i=1; i<2; i++) {
                 Console.WriteLine ("Words: " + (i+1));
                 wordcounter[i].Report (Ntop);
             }
          }
        }
    }
    

    Haskell

    Here’s another short one. Haskell makes this problem almost solve itself.

    module Main where
    
    import System.Environment (getArgs)
    import Data.List (sort, group)
    
    countgroup xs = (length xs, head xs)
    
    twowords (a, b) = a ++ " " ++ b
    
    count = reverse . sort . map countgroup . group . sort 
    
    format (a, b) = b ++ ": " ++ show a
    
    report = mapM_ putStrLn . map format . take 10 . count
    
    countfile f = do 
      putStrLn f
      contents <- readFile f
      let thewords = words contents
      putStrLn "Words: 1"
      report $ thewords
      putStrLn "Words: 2"
      report . map twowords $ zip thewords (tail thewords)
    
    main = getArgs >>= mapM_ countfile
    
    

    Clojure

    Let’s call this a late entry. Clojure is a Lisp that targets a couple of platforms.

    (use '[clojure.string :only (split)])
    
    (defn twowords [words]
      (apply str (interpose " " words)))
    
    (defn bigrams [words]
      (map twowords (map list words (rest words))))
    
    (defn report [N, words]
      (take N (reverse (sort-by second (frequencies words)))))
    
    (defn dofile [N, filename]
      (let [words (split (slurp filename) #"s+")]
        (dorun (map println
      (concat [filename]
       ["Words: 1"]
       (report N words)
       ["Words: 2"]
       (report N (bigrams words)))))))
    
    (dorun (map (partial dofile 10) (rest *command-line-args*)))
    
    

    Results

    For what it’s worth, here’s a table of metrics. I counted the non-comment non-blank lines, then ran the program through gzip to get an idea of the inherent token-complexity of the program (so that shorter variable names don’t make you win). I also ran the programs on the three files mentioned in the problem statement.

    Language Noncomment Lines Gzipped bytes Runtime / s
    Shell 7 151 0.88
    Python 16 360 0.48
    C++ 40 – 7 = 23 484 0.70
    Java 49 – 12 = 37 680 1.92
    C# 46 – 21 = 25 548 0.33
    Haskell 17 308 3.5
    Clojure 16 287 4.2

    I have subtracted the lines occupied by braces on their own line.
    Includes compile time

    I would be the first to agree that this kind of test is not a good place to check performance, but I am quite pleased at how well Python compares to the other options here. I am also very surprised at how fast the shell solution is. I will admit that I am more comfortable with the first two languages than the second two, so perhaps I have missed some performance problems, but I suspect any additional gains in performance will be obtained at the cost of more lines and less succinct expression.

    I would also like to comment on the idea that this is an unrealistic, non-real-world problem. I am a lecturer and have required similar programs several times when working with class lists. Questions routinely arise about which students have been in more than one class, or who needs to do a particular activity given the ones they have already done. Often the fastest way to get the answer us by a process similar to the word counting problem we have here. In fact, this is why I was able to bang out the shell solution so quickly.

    Another aside: It has taken me longer to get the code properly input in this blog than it took me to write all the programs. And Blogger is supposed to be “easy to use”.

  • Five years or five minutes

    A friend of mine posted a link on Facebook to this story about an old lady who spent five years compiling a list of three-letter words for Scrabble. I thought this was a perfect example of applicability vs avoidability.

    Scripting to the rescue 

    I happen to have dedicated a bit of time to the study of Scrabble. I wrote a crossword-setting program for my computer-science project in Matric, and have always enjoyed word games of all kinds. Scrabble also happens to be the thing that got me onto Facebook. The fact that these combinatorial problems are ideally suited to computer analysis is obvious to me, although many people think it would be cheating. This is why I know that there are easily downloadable official scrabble wordlists. They all have names like CSWyy.txt, with yy a two-digit year. There are ’06, ’07 and ’12 versions, which you can easily download from several sources, for instance (from the Zyzzyva website):
    $ wget http://www.zyzzyva.net/lexicons/CSW{06,07,12}.txt

    A little bit of regular expression knowledge will help you to do the following:

    $ grep "^...$" CSW12.txt

    This gives you a full list of all the three letter words in the wordlist.

    Of course, now we want to be able to figure out the scores of the words. The letter scores of the words are easily found on Wikipedia:

    • 1 pointE ×12, A ×9, I ×9, O ×8, N ×6, R ×6, T ×6, L ×4, S ×4, U ×4
    • 2 pointsD ×4, G ×3
    • 3 pointsB ×2, C ×2, M ×2, P ×2
    • 4 pointsF ×2, H ×2, V ×2, W ×2, Y ×2
    • 5 pointsK ×1
    • 8 pointsJ ×1, X ×1
    • 10 pointsQ ×1, Z ×1

    I wrote a quick Awk program to calculate the word scores, taking advantage of a couple of cool Awk features (like automatic conversion of strings to numbers):

    BEGIN {score="0D1G1B2C2M2P2F3H3V3W3Y3K4J7X7Q9Z9"} # score table
    /^...$/ { # Match three-letter words
       s=0; # accumulator for scores
       for(i=1;i<=length($0);i++) {
          # Find the current character in the score table
          # move one down for the score and add one
          s += 1+substr(score, 1+index(score, substr($1, i, 1)))}; 
          print s, $1
       }
    }
    

    The version above has been commented, but it started out as a one-liner, which can be used as follows:

    $ awk 'BEGIN {score="0D1G1B2C2M2P2F3H3V3W3Y3K4J7X7Q9Z9"} /^...$/ {s=0; for(i=1;i<=length($0);i++) {s+=1+substr(score, 1+index(score, substr($1, i, 1)))}; print s, $1}' CSW12.txt | sort -nr | head

    This gives us my version of the table in the article:

    30 ZZZ
    21 ZUZ
    21 ZIZ
    19 ZEX
    19 ZAX
    19 JIZ
    16 ZEK
    15 ZHO
    15 WIZ
    15 PYX

    Notice that she missed out quite a few high-scoring words, and spent five years on this, while it took me a couple of minutes to bash out the script.

    My observations are first, that it is clearly possible to compile a list like the article describes without using programming, but it would have taken her a lot less time to learn programming and then compile the list than it probably took her to compile it by hand. In this case, programming is avoidable, but definitely applicable.

    I suppose she could also just have downloaded Zyzzyva (a scrabble word-lookup GUI program), but to be honest, I couldn’t get the list above as quickly, because the word scores weren’t in the program by default and I couldn’t figure out how to get them there.

    Other implementations

    I did a quick Python implementation as well, slightly less suited for one-liners on the commandline, but perhaps a little more legible:

    table = {'A': 1, 'B': 3, 'C': 3, 'D': 2, 'E': 1, 'F': 4, 'G': 2, 'H': 4,
             'I': 1, 'J': 8, 'K': 5, 'L': 1, 'M': 3, 'N': 1, 'O': 1, 'P': 3,
             'Q': 10, 'R': 1, 'S': 1, 'T': 1, 'U': 1, 'V': 4, 'W': 4, 'X': 8,
             'Y': 4, 'Z': 1,
             }
    
    wordsandscores = [(sum(table[c] for c in word), word) for word in
                      [w.strip() for w in open('CSW12.txt')] if len(word)==3]
    wordsandscores.sort(reverse=True)
    
    for s, w in wordsandscores[:10]:
        print s, w
    

    I’m also very keen to have someone post the solution to this problem using Windows Powershell.