2012年12月13日木曜日

正規表現を利用して文字列を作成する

( Lisp Advent Calendar 2012 の13日目の記事です)

テストなどのために適当なデータを作成したいことがあります。
この時、データをランダムに作成できると、楽ができる上に 自分の考えていなかった入力パターンから問題を見つけることができて良さそうです。

作成したいデータが数値なら、疑似乱数生成用の関数(rand,randomなど)を使えば事足ります。
文字列を作成したい場合は、正規表現を利用すれば楽ができそうです。

Clojure には、正規表現からその正規表現にマッチするような文字列を作成してくれる re-rand という ライブラリがあります。

[re-rand "0.1.0"]
user> (require [re-rand :as r])

user> (repeatedly 3 #(r/re-rand #"\d{4}/\d{2}/\d{2} \d\d:\d\d:\d\d"))
("7511/27/85 13:37:43" "6728/87/88 96:68:15" "1536/63/37 90:23:02")

user> (repeatedly 3 #(r/re-rand #"[0-9零一二三四五六七八九]{1,10}"))
("六二零七" "1七8" "二三七八一2八1")

Common Lisp には・・・と思って調べて見ましたが、見つけることができませんでした。 仕方ないので自分で作りましょう。

幸い、一番大変そうな正規表現のパースは cl-ppcre の parse-string 関数で行えます。

(asdf:load-system :cl-ppcre)

(defpackage :random-string
  (:use :cl)
  (:export random-string
           *repeat-limit*
           *charset-every*
           *charset-digit*
           *charset-word*
           *charset-whitespace*))

(in-package :random-string)

(defvar *repeat-limit* 64)

(defvar *charset-everything*
  (lambda () (code-char (random 256) )))

(defvar *charset-digit* "0123456789")

(defvar *charset-word*
  (concatenate 'string
   "abcdefghijklmnopqrstuvwxyz"
   "ABCEFGHJIJKLMNOPQRSTUVWXYZ"
   "!\"#$%'()-=^~\\|[]{};:+*,.<>/?_"))

(defvar *charset-whitespace*
  (format nil "%c%c%c" #\space #\newline #\tab))

(defun one-of (obj)
  (if (functionp obj)
      (funcall obj)
      (let ((count (length obj)))
        (elt obj (random count)))))

(defun random-string (regexp)
  (generate (ppcre:parse-string regexp)))

(defmethod generate ((obj string))
  obj)

(defmethod generate ((obj character))
  (string obj))

(defmethod generate ((obj list))
  (generate/list (first obj) (rest obj)))

(defmethod generate ((obj (eql :digit-class)))
  (string (one-of *charset-digit*)))

(defmethod generate ((obj (eql :everything)))
  (string (one-of *charset-everything*)))

(defmethod generate ((obj (eql :whitespace-char-class)))
  (string (one-of *charset-whitespace*)))

(defmethod generate ((obj (eql :word-char-class)))
  (string (one-of *charset-word*)))

(defmethod generate/list ((cls (eql :sequence)) rest)
  (apply #'concatenate 'string (mapcar #'generate rest)))

(defmethod generate/list ((cls (eql :char-class)) rest)
  (string (one-of (mapcar #'generate rest))))

(defmethod generate/list ((cls (eql :range)) rest)
  (string
   (destructuring-bind (from to) rest
     (code-char
      (+ (char-code from)
         (random (1+ (- (char-code to) (char-code from)))))))))

(defmethod generate/list ((cls (eql :INVERTED-CHAR-CLASS)) rest)
  (error "unsupported expression: INVERTED-CHAR-CLASS"))

(defmethod generate/list ((cls (eql :greedy-repetition)) rest)
  (destructuring-bind (min max gen-cls) rest
    (let ((max (or max (+ min *repeat-limit*))))
      (apply #'concatenate 'string
             (loop :repeat (+ min (random (1+ (- max min))))
                :collect (generate gen-cls))))))

(defmethod generate/list ((cls (eql :register)) rest)
  (apply #'concatenate 'string (mapcar #'generate rest)))

(defmethod generate/list ((cls (eql :alternation)) rest)
  (generate (one-of rest)))

CL-USER> (random-string:random-string "b(an)+a")
"banananananananananananananananana"

CL-USER> (dotimes (_ 3)
           (print (random-string:random-string "[012]\\d{3}-((0[1-9])|(1[12]))-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z")))
"2056-09-58T98:80:10Z" 
"2057-02-82T36:65:58Z" 
"1077-11-18T77:72:22Z"

CL-USER> (dotimes (_ 3)
           (print (random-string:random-string "[\\d零一二三四五六七八九]{1,5}")))

"二二四四" 
"五68" 
"二七"

文字集合の選択などで偏りが出てしまいますが、適当なデータを作成する程度なら十分利用できるかなぁ、と思います。

今回書いたコードでは、やりたいことの9割くらいを正規表現ライブラリのcl-ppcreが行ってくれています。
Lisp使いは「全部自分で書く」みたいなイメージがありますが、 leiningen や Quicklisp を使うとライブラリのインストールは簡単ですし、 やりたいことにマッチするライブラリがあったら、ありがたく使わせてもらいましょう。

0 件のコメント:

コメントを投稿