Lisp/基本からさらに一歩進んで/CLOS/Example 1

出典: フリー教科書『ウィキブックス(Wikibooks)』

映画の字幕を含んでいる .src フォーマットのファイルを持っているとしましょう。例えば以下のようなものです。


00:00:33,657 --> 00:00:35,852
Michael Rennie was ill

2
00:00:36,097 --> 00:00:39,055
The day the earth stood still

3
00:00:39,297 --> 00:00:44,132
But he told us where we stand

4
00:00:44,377 --> 00:00:46,447
And Flash Gordon was there

5
00:00:46,697 --> 00:00:49,609
In silver underwear

しかしこの字幕はあまりうまくありません。なぜならあなたの持っている映画の版では何かの理由で映画の最初に 10.532 秒の静止時間が含まれるからです。全ての字幕の表示時間を手作業で変えるのは不可能です。しかも、表示時間を変更するための道具もありません。ということで Common Lisp (かあるいは他の何か) のスクリプトをコーディングします。では始めましょう。

(defclass srt-time ()
  ((hr :initarg :hr :initform 0 :accessor hr)
   (mi :initarg :mi :initform 0 :accessor mi)
   (se :initarg :se :initform 0 :accessor se)
   (ms :initarg :ms :initform 0 :accessor ms))
  (:documentation "Time format for srt"))

(defgeneric display (what)
  (:documentation "Returns string that represents the object"))

(defgeneric normalise (time)
  (:documentation "Fix overflow of fields"))

(defmethod normalise ((time srt-time))
  (with-slots (hr mi se ms) time 
    (loop until (< ms 1000) do (decf ms 1000) (incf se))
    (loop until (< se 60) do (decf se 60) (incf mi))
    (loop until (< mi 60) do (decf mi 60) (incf hr)))
  time)

(defmethod display ((time srt-time))
  (normalise time)
  (with-slots (hr mi se ms) time 
    (format nil "~2,'0d:~2,'0d:~2,'0d,~3,'0d" hr mi se ms)))

(defun make-srt-time (arglist)
  (destructuring-bind (hr mi se ms) arglist
    (make-instance 'srt-time :hr hr :mi mi :se se :ms ms)))

display メソッドは srt-time オブジェクトの本文の表示を返します。 normalize は補助関数で、スロットの全ての "オーバーフロー" を修正します。(60秒以上になったりすることがないようにです。) make-srt-timemake-instance を囲むラッパーで srt-time オブジェクトの作成を容易にします。

ここで、時間を加算するための二つのメソッドを追加しました。

(defgeneric add (t1 t2))

(defmethod add ((t1 srt-time) (t2 srt-time))
  "Adds two srt-times"
  (normalise 
   (make-srt-time 
    (mapcar #'+ (list (hr t1) (mi t1) (se t1) (ms t1)) 
		(list (hr t2) (mi t2) (se t2) (ms t2))))))

(defmethod add ((t1 srt-time) (t2 integer))
  "Adds some number of seconds"
  (normalise (make-srt-time (list (hr t1) (mi t1) (+ (se t1) t2) (ms t1)))))

時間を加算する二番目の方法を追加したのは、よくないように見えるかもしれません。しかし、加算で呼び出される全ての関数は第二引数に srt-time の代わりに数値を渡していることを考慮してください。後で見るように、この関数的拡張はプログラムの上方のレイヤに伝播し、これはユーザーに呼び出されることを意図した関数も含まれます。

では我々の課題の二番目の部分を考えてみましょう。テキスト文字列を与えられ、我々はタイムスタンプのインスタンスを修正されたタイムスタンプに交換しなければなりません。幸いなことに、 CL-PPCRE ではまさにそれが可能になっています。われわれには適当な正規表現が必要です。正規表現はこの wikibooks の範囲ではありませんが、正規表現にあまり親しんでない場合は、正規表現の概念を学ぶことが出来る良いサイトがたくさんあるので参照してみてください。ここではただこのケースに合う正規表現を記すにとどめます。: "([0-9]{2,}):([0-9]{2}):([0-9]{2}),([0-9]{3})" これは例えば 00:00:44,132 のような特定のタイムスタンプに一致するか判定しようとするものです。 "(" と ")" の間にある正規表現の部分にマッチしたものが CL-PRCRE に記憶されるということに注意してください。これを活用することになります。では正規表現に一致する scanner を生成します。

(defparameter *find-time* (cl-ppcre:create-scanner 
                           "([0-9]{2,}):([0-9]{2}):([0-9]{2}),([0-9]{3})"))

この scanner は実際にコンパイルされた関数ですが、意図した働きをしているのなら、実装の詳細を知る必要はありません。次のステップでは scanner で文字列の部分文字列を検索し、入れ替えるために使います。

(defun modify-times (str fun)
  "Modify all instances of srt-time being hidden in the given string
   using a given function"
  (cl-ppcre:regex-replace-all *find-time* str fun :simple-calls t))

この関数は任意の文字列をと任意の関数を引数に取り、この関数を使用し、 scanner の *find-time* が文字列で見つけた全てのタイムスタンプを変換します。では modify-times に適切な関数を与える関数を書きましょう。

(defun apply-line-add (str delta)
  (labels ((adder (match hr mi se ms)
	     (declare (ignore match)) ;;match is needed for CL-PPCRE
	     (display
	      (add (make-srt-time (mapcar #'parse-integer (list hr mi se ms)))
		   delta))))
    (modify-times str #'adder)))

それほど面白くはなかったですか?まだユーザーがどれだけの時間を加算したいのかわからないので、ランタイムに必要となる関数を作成しました。 regex-replace-alladder を5つの引数と共に呼び出します。最初の引数の match は全体一致のためのもので、今回の我々には必要ありません。今回必要なのは部分一致です。(括弧で囲まれた部分です。)それらは時、分、秒、そしてミリセカンドと一致するものです。これらを文字列から数値に変換するのが parse-integer です。それで、 srt-time オブジェクトはこれらの数値から作られ、 delta がこの数値に加算されます。(delta は sort-time か数値、どちらでも可能だということに注意してください。知らないなら知らないでかまわないことでもありますが。)そして、 display メソッドを使用することで結果が文字列に返還されます。これこそが CL-PRCRE がこの関数から必要としていたもので、もうここで CL-PRCRE について忘れて、他の事に集中してもかまいません。

次の関数の mapline はファイルを複数の行に切り分けます。これらの切り分けられた行に関数を与え、それらの行を出力ファイルに出力します。

(defun mapline (fun input output)
  "Applies function to lines of input file and outputs the result" 
  (with-open-file (in input)
    (with-open-file (out output :direction :output :if-exists :supersede)
      (loop for str = (read-line in nil nil)
            while str
            do (princ (funcall fun str) out) (terpri out)))))

with-open-file はお嫌いですか?簡潔で格好の良いコードです。

さていよいよ、最後の関数です。 maplinemodify-times の力を掛け合わせます。

(defun delay (delay input output)
  "Adjusts all srt-times in file by adding delay to them. Delay can be
  either integer (number of seconds) or srt-time instance."
  (mapline (lambda (str) (apply-line-add str delay)) input output))

最後となりますが、どうしてこの example が CLOS の説明に配置されているのでしょうか?それは、どうして CLOS が優れているかを示すためです。 CLOS を使用することであなたのプログラムを大規模に実現可能なものにします。それでは、最後に練習として、少数秒だけ遅らせることが出来る機能を追加したいとします。この課題に合うような add メソッドを書いてみましょう。