{"id":45,"date":"2013-05-04T14:19:00","date_gmt":"2013-05-04T14:19:00","guid":{"rendered":"https:\/\/sandrock.co.za\/carl\/2013\/05\/04\/word-count-program-comparison\/"},"modified":"2013-05-04T14:19:00","modified_gmt":"2013-05-04T14:19:00","slug":"word-count-program-comparison","status":"publish","type":"post","link":"https:\/\/sandrock.co.za\/carl\/2013\/05\/word-count-program-comparison\/","title":{"rendered":"Word count program comparison"},"content":{"rendered":"<p>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:<\/p>\n<div>\n<\/div>\n<div>\n<div>\n<i>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, &#8220;in the&#8221; and &#8220;at most&#8221; are words that are frequently found together.<\/i><\/div>\n<div>\n<i><br \/><\/i><\/div>\n<div>\n<i>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.<\/i><\/div>\n<\/div>\n<div>\n<i><br \/><\/i><\/div>\n<div>\nFirst, I downloaded these books and converted them to unix line-endings, then I wrote some programs to do the same job.<\/p>\n<h3>\nShell<\/h3>\n<p>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 <span style=\"font-family: Courier New, Courier, monospace\">sort | uniq -c<\/span><\/div>\n<div>\nis a really idiomatic way of counting things.<\/p>\n<\/div>\n<div>\n<pre>for f in pg*.txt; do\n    echo $f\n    echo \"Words: 1\"\n    tr -s \"t \" \"n\" &lt; $f | sort | uniq -c | sort -nr | head\n    echo \"Words: 2\"\n    tr -s \"t \" \"n\" &lt; $f | awk 'NR&gt;1 {print w\" \"$0} {w=$0}' | sort | uniq -c | sort -nr | head\ndone<\/pre>\n<h3>\nPython<\/h3>\n<p>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.<\/p>\n<p><\/p>\n<pre>import glob\nfrom collections import defaultdict\nfrom operator import itemgetter\n\ntopN = 10 # Display this number of elements in the report\n\nfor filename in glob.glob('pg*.txt'):\n    # Parse\n    previousword = None\n    # Create a single-word and double-word counter\n    counters = [defaultdict(int), defaultdict(int)]\n    for word in open(filename, 'r').read().split():\n        counters[0][word] += 1\n        if previousword: counters[1][previousword + ' ' + word] += 1\n        previousword = word\n\n    # Report\n    print \"File:\", filename\n    for i, c in enumerate(counters):\n        print \"Length\", i+1\n        # Sort the counters by their counts, reverse order, take topN elements\n        for item in sorted(c.items(), reverse=True, key=itemgetter(1))[:topN]:\n            print \"%-20s%5i\" % item\n<\/pre>\n<div>\n<\/p>\n<h3>\nC++<\/h3>\n<p>The C++ version follows the same strategy as the Python program, using the C++ standard library equivalents of Python&#8217;s dictionaries and lists. I&#8217;ve left out the globbing, as the C++ STL doesn&#8217;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&#8217;re SOL. This program didn&#8217;t take that long to write, but it took ages to debug due to the heavy use of templates.<\/p>\n<pre>#include &lt;iostream&gt;\n#include &lt;map&gt;\n#include &lt;string&gt;\n#include &lt;cmath&gt;\n#include &lt;fstream&gt;\n#include &lt;iterator&gt;\n#include &lt;vector&gt;\n#include &lt;algorithm&gt;\n\nusing namespace std;\n\nbool value(const pair&lt;string, int&gt;&amp; a, const pair&lt;string, int&gt;&amp; b)\n{ \n  return a.second &gt; b.second;\n}\n\n\nint main (int argc, char *argv[]) {\n  for (int i=1; i&lt;argc; i++) {\n    ifstream infile(argv[i]);\n    map&lt;string, int&gt; counter[2];\n\n    cout &lt;&lt; argv[i] &lt;&lt; endl;\n\n    \/\/ read file\n    string word, previousword;\n    bool firstword=true;\n\n    while (infile &gt;&gt; word) {\n      counter[0][word]++;\n      if (!firstword) {\n        counter[1][previousword + \" \" + word]++;\n        previousword = word;\n      } else {\n        firstword = false;\n      }\n    }\n\n    for (int c=0; c&lt;2; c++) {\n      cout &lt;&lt; \"Words:\" &lt;&lt; c+1 &lt;&lt; endl;\n\n      \/\/ sort\n      vector&lt;pair&lt;string,int&gt; &gt; wordlist;\n      copy(counter[c].begin(), counter[c].end(), \n           back_inserter&lt;vector&lt;pair&lt;string, int&gt; &gt; &gt;(wordlist));\n      sort(wordlist.begin(), wordlist.end(), &amp;value);\n    \n      \/\/ write output\n      for (int j=0; j &lt; 10; j++) {\n        cout &lt;&lt; wordlist[j].first &lt;&lt; \": \" &lt;&lt; wordlist[j].second &lt;&lt; endl;\n      }\n    }\n  }\n}<\/pre>\n<h3>\nJava<\/h3>\n<p>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.<\/p>\n<pre>import java.io.*;\nimport java.util.*;\n\npublic class ngrammer {\n\n  public static void main(String[] args) throws FileNotFoundException {\n\n    class WordCounter extends HashMap&lt;String, Integer&gt; {\n\n      class ValueComparator implements Comparator&lt;Map.Entry&lt;String, Integer&gt;&gt; {\n        \/\/ Comparator to allow sorting on the value of the HashMap elements\n        @Override\n        public int compare(Map.Entry&lt;String, Integer&gt; o1, Map.Entry&lt;String, Integer&gt; o2) {\n          return o2.getValue() - o1.getValue();\n        }\n      }\n\n      void add(String word) {\n        if (containsKey(word)) {\n          put(word, get(word)+1);\n        } else {\n          put(word, 1);\n        }\n      }\n        \n      void printN(Integer N) {\n        \/\/ Prints the top N elements of the counter\n        \/\/ Make a list of the counter entries\n        List&lt;Map.Entry&lt;String,Integer&gt;&gt; entries = new LinkedList&lt;Map.Entry&lt;String,Integer&gt;&gt;(entrySet());\n        \/\/ Sort it on the value\n        Collections.sort(entries, new ValueComparator());\n        \/\/ Print out at most N items       \n        int i=N;\n        for (Map.Entry&lt;String,Integer&gt; entry: entries) {\n          System.out.println(entry.getKey() + \": \" + entry.getValue());\n          if (--i == 0) break;\n        }\n      }\n    }\n        \n    final int topN = 10;\n    \n    \/\/ Process each file in the input arguments\n    for (int fi = 1; fi &lt; args.length; fi++) {\n      File file = new File(args[fi]);\n      \/\/ Initialise new counters\n      WordCounter[] counter = {new WordCounter(), new WordCounter()};\n      String word, previousword=\"\";\n      \/\/ Tokenise file\n      Scanner scanner = new Scanner(file);\n      while (scanner.hasNext()) {\n        word = scanner.next();\n        counter[0].add(word);\n        if (!previousword.isEmpty()) \n          counter[1].add(previousword + \" \" + word);\n        previousword = word;\n      }\n      \/\/ Report\n      System.out.println(\"Filename:\" + file.getAbsolutePath());\n      for (int i = 0; i &lt; counter.length; i++) {\n        System.out.println(\"Words: \" + (i+1));\n        counter[i].printN(topN);\n      }\n    }\n  }\n}\n<\/pre>\n<h2>\nC#<\/h2>\n<p>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.<\/p>\n<pre>using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\n\nclass MainClass\n{\n\n    class WordCounter : Dictionary&lt;string, int&gt;\n    {\n        public void CountWord (string word)\n        {\n            if (ContainsKey (word)) {\n                this [word]++;\n            } else {\n                this [word] = 1;\n            }\n        }\n\n        public void Report (int N)\n        {\n            var list = this.ToList ();\n       \n            list.Sort ((a, b) =&gt; { return b.Value.CompareTo (a.Value); });\n            foreach (var item in list.Take(N)) {\n                Console.WriteLine(item.Key + \": \" + item.Value);\n            }\n        }\n    }\n    \n    public static void Main (string[] args)\n    {\n      const int Ntop=10;\n\n      foreach (string f in args) {\n         WordCounter[] wordcounter = {new WordCounter(), new WordCounter()};\n\n            string lastword = \"\";\n            foreach (var word in File.ReadAllText(f).Split(' ')) {\n                wordcounter[0].CountWord(word);\n                if (lastword.Length &gt; 0) {\n                    wordcounter[1].CountWord (lastword + \" \" + word);\n                }\n                lastword = word;\n            }\n\n         Console.WriteLine(f);\n         for (int i=1; i&lt;2; i++) {\n             Console.WriteLine (\"Words: \" + (i+1));\n             wordcounter[i].Report (Ntop);\n         }\n      }\n    }\n}\n<\/pre>\n<\/div>\n<\/div>\n<p><\/p>\n<h2>\nHaskell<\/h2>\n<div>\nHere&#8217;s another short one. Haskell makes this problem almost solve itself.<\/p>\n<\/div>\n<div>\n<pre>module Main where\n\nimport System.Environment (getArgs)\nimport Data.List (sort, group)\n\ncountgroup xs = (length xs, head xs)\n\ntwowords (a, b) = a ++ \" \" ++ b\n\ncount = reverse . sort . map countgroup . group . sort \n\nformat (a, b) = b ++ \": \" ++ show a\n\nreport = mapM_ putStrLn . map format . take 10 . count\n\ncountfile f = do \n  putStrLn f\n  contents &lt;- readFile f\n  let thewords = words contents\n  putStrLn \"Words: 1\"\n  report $ thewords\n  putStrLn \"Words: 2\"\n  report . map twowords $ zip thewords (tail thewords)\n\nmain = getArgs &gt;&gt;= mapM_ countfile\n\n<\/pre>\n<p><\/div>\n<h2>\nClojure<\/h2>\n<div>\nLet&#8217;s call this a late entry. Clojure is a Lisp that targets a couple of platforms.<\/p>\n<\/div>\n<div>\n<pre>(use '[clojure.string :only (split)])\n\n(defn twowords [words]\n  (apply str (interpose \" \" words)))\n\n(defn bigrams [words]\n  (map twowords (map list words (rest words))))\n\n(defn report [N, words]\n  (take N (reverse (sort-by second (frequencies words)))))\n\n(defn dofile [N, filename]\n  (let [words (split (slurp filename) #\"s+\")]\n    (dorun (map println\n  (concat [filename]\n   [\"Words: 1\"]\n   (report N words)\n   [\"Words: 2\"]\n   (report N (bigrams words)))))))\n\n(dorun (map (partial dofile 10) (rest *command-line-args*)))\n\n<\/pre>\n<\/div>\n<div>\n<div>\n<\/div>\n<\/div>\n<h2>\nResults<\/h2>\n<p>For what it&#8217;s worth, here&#8217;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&#8217;t make you win). I also ran the programs on the three files mentioned in the problem statement.<\/p>\n<table>\n<tbody>\n<tr>\n<th>Language<\/th>\n<th>Noncomment Lines<\/th>\n<th>Gzipped bytes<\/th>\n<th>Runtime \/ s<\/th>\n<\/tr>\n<tr>\n<td>Shell<\/td>\n<td><i>7<\/i><\/td>\n<td>151<\/td>\n<td>0.88<\/td>\n<\/tr>\n<tr>\n<td>Python<\/td>\n<td>16<\/td>\n<td>360<\/td>\n<td>0.48<\/td>\n<\/tr>\n<tr>\n<td>C++<\/td>\n<td>40 &#8211; 7 = 23<sup>\u2020<\/sup><\/td>\n<td>484<\/td>\n<td>0.70<\/td>\n<\/tr>\n<tr>\n<td>Java<\/td>\n<td>49 &#8211; 12 = 37<sup>\u2020<\/sup><\/td>\n<td>680<\/td>\n<td>1.92<\/td>\n<\/tr>\n<tr>\n<td>C#<\/td>\n<td>46 &#8211; 21 = 25<sup>\u2020<\/sup><\/td>\n<td>548<\/td>\n<td><i>0.33<\/i><\/td>\n<\/tr>\n<tr>\n<td>Haskell<\/td>\n<td>17<\/td>\n<td>308<\/td>\n<td>3.5<\/td>\n<\/tr>\n<tr>\n<td>Clojure<\/td>\n<td>16<\/td>\n<td>287<\/td>\n<td>4.2<sup>\u2021<\/sup><\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p><sup>\u2020<\/sup> I have subtracted the lines occupied by braces on their own line.<br \/>\n<sup>\u2021<\/sup> Includes compile time<\/p>\n<div>\nI 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.<\/p>\n<p>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.<\/p>\n<p>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 &#8220;easy to use&#8221;.<\/p><\/div>\n","protected":false},"excerpt":{"rendered":"<p>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 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false},"categories":[1],"tags":[],"class_list":["post-45","post","type-post","status-publish","format-standard","hentry","category-uncategorized"],"jetpack_sharing_enabled":true,"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/posts\/45","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/comments?post=45"}],"version-history":[{"count":0,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/posts\/45\/revisions"}],"wp:attachment":[{"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/media?parent=45"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/categories?post=45"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/sandrock.co.za\/carl\/wp-json\/wp\/v2\/tags?post=45"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}