February 03, 2005
CL-DIFFLIB

I wrote CL-DIFFLIB, which is a library for computing differences between pairs of sequences. It is nearly a transcription of Python's difflib module, which contains the following description of its algorithm:

The basic algorithm predates, and is a little fancier than, an algorithm published in the late 1980's by Ratcliff and Obershelp under the hyperbolic name “gestalt pattern matching”. The basic idea is to find the longest contiguous matching subsequence that contains no “junk” elements (R-O doesn't address junk). The same idea is then applied recursively to the pieces of the sequences to the left and to the right of the matching subsequence. This does not yield minimal edit sequences, but does tend to yield matches that “look right” to people.

Simple enough, I guess.

Anyway, it's got the unified diff:

CL-USER> (unified-diff *standard-output*
                       '("one" "two" "three" "four" "five" "six")
                       '("one" "three" "four" "seven" "six")
                       :test-function #'equal)
---  
+++  
@@ -1,6 +1,5 @@
 one
-two
 three
 four
-five
+seven
 six
; No value

And it's got the context diff:

CL-USER> (context-diff *standard-output*
                       '("one" "two" "three" "four" "five" "six")
                       '("one" "three" "four" "seven" "six")
                       :test-function #'equal)
***  
---  
***************
*** 1,6 ***
  one
- two
  three
  four
! five
  six
--- 1,5 ----
  one
  three
  four
! seven
  six
; No value

It should work on any sequence (as long as the elements can be properly kept track of in a hash table--that's what the :test-function argument is for):

CL-USER> (context-diff *standard-output*
               #(1 2 3 4 5 6)
               #(1 3 4 7 6)
               :from-file "original.fake"
               :from-file-date "Mon Sep 27 14:13:57 2004"
               :to-file "modified.fake"
               :to-file-date "Thu Jan 13 15:55:05 2005")
*** original.fake Mon Sep 27 14:13:57 2004
--- modified.fake Thu Jan 13 15:55:05 2005
***************
*** 1,6 ***
  1
- 2
  3
  4
! 5
  6
--- 1,5 ----
  1
  3
  4
! 7
  6
; No value

It's got some similarity measures:

CL-USER> (defparameter *lisp-symbols* '())
*LISP-SYMBOLS*
CL-USER> (do-external-symbols (sym "COMMON-LISP")
           (push (symbol-name sym) *lisp-symbols*))
NIL
CL-USER> (get-close-matches "PRING" *lisp-symbols*)
("PRINC" "PRINT" "PRIN1")
CL-USER> (get-close-matches "SPROING" *lisp-symbols*)
("STRING" "PROG" "STRINGP")
CL-USER> (get-close-matches "WITH-HASHING-TABLE" *lisp-symbols*)
("HASH-TABLE" "WITH-HASH-TABLE-ITERATOR" "HASH-TABLE-P")

And it's got some lower level building blocks:

CL-USER> (let ((m (make-instance 'sequence-matcher
                                 :a '("one" "two" "three" "four" "five" "six")
                                 :b '("one" "three" "four" "seven" "six")
                                 :test-function #'equal)))
           (pprint (get-opcodes m)))

(#<OPCODE :EQUAL 0 1 0 1> #<OPCODE :DELETE 1 2 1 1> #<OPCODE :EQUAL 2 4 1 3>
 #<OPCODE :REPLACE 4 5 3 4> #<OPCODE :EQUAL 5 6 4 5>)
; No value

Some things it doesn't have: Python difflib's Differ class, and the ndiff or restore functions (maybe someday). It also hasn't been performance tuned. I guess it's also got some ugly bits of code that look a little Pythonesque. I did change a few things to be more Lispy, but most of the documentation for the Python module should still be applicable.

CL-DIFFLIB is pretty simple, but the diffs it generates look decent. It should be completely portable Lisp. And, best of all, you can download it via ASDF-INSTALL.

(Nathan Froyd has some diff code that looks more complicated than this.)

Posted by jjwiseman at February 03, 2005 12:04 AM
Comments

Just what I need, but why doesn't unified-diff return the diff as a string when I pass nil as the first parameter?

I'm a CL noob, please advice. :)

Posted by: Vetle on December 20, 2007 01:19 PM

Nevermind, found it!

(with-output-to-string (stream)
(dolist (char '(#\Z #\a #\p #\p #\a #\, #\Space))
(princ char stream))
(difflib:unified-diff stream "foobar" "foozot"))

Posted by: Vetle on December 20, 2007 02:21 PM
Post a comment
Name:


Email Address:


URL:




Unless you answer this question, your comment will be classified as spam and will not be posted.
(I'll give you a hint: the answer is “lisp”.)

Comments:


Remember info?