Lisp/基本からさらに一歩進んで/文字列

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

Common Lisp での文字列の扱いについて知らなければならない最も重要なことは、文字列は配列であり、したがって連続している順序でもあるということです。これは配列と順序に適用される全ての概念が文字列に利用することも出来る、ということを意味します。もし、特定の文字列操作関数が見つからなければ、必ずもっと一般的な配列か順序に関する関数を探した方がいいでしょう。ここでは文字列に対して出来ることをほんの少しだけ取り扱います。

部分文字列の取り出し[編集]

文字列は連続しているので、部分文字列を取り出すには SUBSEQ 関数を使用します。文字列の添え字(index)は常に 0 から始まります。三番目の引数は任意で、ここから部分文字列を構成しない最初の文字の添え字です。部分文字列の終端の1個後の添え字と憶えてもいいかもしれません。部分文字列の長さを指定するものではないので注意しましょう。

(defparameter *my-string* (string "Groucho Marx"))
 *MY-STRING*
(subseq *my-string* 8)
 "Marx"
(subseq *my-string* 0 7)
 "Groucho"
(subseq *my-string* 1 5)
 "rouc"

SETF と SUBSEQ を一緒に使えば部分文字列を操作することも出来ます。

(defparameter *my-string* (string "Harpo Marx"))
 *MY-STRING*
(subseq *my-string* 0 5)
 "Harpo"
(setf (subseq *my-string* 0 5) "Chico")
 "Chico"
*my-string*
 "Chico Marx"

しかし、文字列は "伸縮可能" ではないということには注意してください。 HyperSpec から引用すると、 "部分列と新しい列が等しい長さでなければ、短い方が入れ替えられた列の要素数を決定する" ということになります。例を挙げると以下のようになります。

(defparameter *my-string* (string "Karl Marx"))
 *MY-STRING*
(subseq *my-string* 0 4)
 "Karl"
(setf (subseq *my-string* 0 4) "Harpo")
 "Harpo"
*my-string*
 "Harp Marx"
(subseq *my-string* 4)
 " Marx"
(setf (subseq *my-string* 4) "o Marx")
 "o Marx"
*my-string*
 "Harpo Mar"

個別の文字の取り出し[編集]

文字列の個別の文字を取り出すために、関数 CHAR を使用することができます。 CHAR は SETF と連結して使うことができます。

(defparameter *my-string* (string "Groucho Marx"))
 *MY-STRING*
(char *my-string* 11)
 #\x
(char *my-string* 7)
 #\Space
(char *my-string* 6)
 #\o
(setf (char *my-string* 6) #\y)
 #\y
*my-string*
 "Grouchy Marx"

SCHAR もあるということにも注意です。効率が重要なら、 SCHAR を適切に配置すれば少しだけ高速になります。

文字列は配列であり、したがって連続した順序でもあるので、もっと包括的な関数である AREF や ELT を使用することもできます(これは CHAR がより効率的に利用されるのに対して、より一般的ということになります。)

(defparameter *my-string* (string "Groucho Marx"))
 *MY-STRING*
(aref *my-string* 3)
 #\u
(elt *my-string* 8)
 #\M

文字列の部分の操作[編集]

文字列操作に使われる関数はたくさんありますので、ここではいくつかのサンプルを示すにとどめます。これ以上は HyperSpec のシーケンス辞典を参照してください。

(remove #\o "Harpo Marx")
 "Harp Marx"
(remove #\a "Harpo Marx")
 "Hrpo Mrx"
(remove #\a "Harpo Marx" :start 2)
 "Harpo Mrx"
(remove-if #'upper-case-p "Harpo Marx")
 "arpo arx"
(substitute #\u #\o "Groucho Marx")
 "Gruuchu Marx"
(substitute-if #\_ #'upper-case-p "Groucho Marx")
 "_roucho _arx"
(defparameter *my-string* (string "Zeppo Marx"))
 *MY-STRING*
(replace *my-string* "Harpo" :end1 5)
 "Harpo Marx"
*my-string*
 "Harpo Marx"

他の頻繁に使用される関数は replace-all です(ただしANSI規格標準ではありません)この関数は、文字列の 検索/置換 操作の簡単な機能を提供しています。文字列の全ての '部分' の存在が '代替部分' と置き換えられた新しい文字列が返り値として返ります。

(replace-all "Groucho Marx Groucho" "Groucho" "ReplacementForGroucho")
 "ReplacementForGroucho Marx ReplacementForGroucho"

replace-all の実装は以下のようなものになります。

(defun replace-all (string part replacement &key (test #'char=))
"Returns a new string in which all the occurences of the part 
is replaced with replacement."
    (with-output-to-string (out)
      (loop with part-length = (length part)
            for old-pos = 0 then (+ pos part-length)
            for pos = (search part string
                              :start2 old-pos
                              :test test)
            do (write-string string out
                             :start old-pos
                             :end (or pos (length string)))
            when pos do (write-string replacement out)
            while pos)))

しかし、上記のコードは長い文字列については能率的に利用されていないということを憶えておいてください。もし非常に長い文字列やファイルなどの操作を実行しようとするのなら、ぜひ、非常に最適化された cl-ppcre の正規表現と文字列操作ライブラリの使用を検討してください。

文字列の連結[編集]

名前が全てを示すとおり、 CONCATENATE はあなたの友達となります。これは一般的な手続き関数なので、返り値の型を最初の引数として与えなければなりません。

(concatenate 'string "Karl" " " "Marx")
 "Karl Marx"
(concatenate 'list "Karl" " " "Marx")
 (#\K #\a #\r #\l #\Space #\M #\a #\r #\x)

もしたくさんの部分から文字列を生成しなければならないとしたら、いつも CONCATENATE を呼び出すのは非効率になるでしょう。あなたのデータが何であるかによって、少なくとも他に3つの文字列の断片を生成する良い方法があります。もし一回につき一文字ずつ文字列を作るのなら、調節可能な VECTOR(1次元の配列)型で 0 の fill-pointer の文字を作成し、それから VECTOR-PUSH-EXTEND を使用すると良いでしょう。これで文字列がどれくらいの長さになるかの概算が可能かどうか、システムに対してヒントを与えることも出来るようになります(VECTOR-PUSH-EXTEND の任意の三番目の引数を見てください)

(defparameter *my-string* (make-array 0
                                      :element-type 'character
                                      :fill-pointer 0
                                      :adjustable t))
 *MY-STRING*
*my-string*
 ""
(dolist (char '(#\Z #\a #\p #\p #\a))
  (vector-push-extend char *my-string*))
 NIL
*my-string*
 "Zappa"

もし文字列がシンボルや数値、文字、文字列など任意のオブジェクト(かつプリントされた表現)で構成されるなら、出力ストリームの引数を NIL にした FORMAT を使用することができます。これは文字列としての出力で表示された返り値を直接 FORMAT します。

(format nil "This is a string with a list ~A in it"
        '(1 2 3))
 "This is a string with a list (1 2 3) in it"

FORMAT のミニ言語としての Loop 構造を用いて CONCATENATE の動作を模倣することが出来ます。

(format nil "The Marx brothers are:~{ ~A~}."
        '("Groucho" "Harpo" "Chico" "Zeppo" "Karl"))
 "The Marx brothers are: Groucho Harpo Chico Zeppo Karl."

FORMAT ではもっとたくさんの処理が可能なのですが、しかし相対的に難解な構文をしています。最後の例の後で、 CLHS の項でフォーマット出力についての詳細を確認できるでしょう。

(format nil "The Marx brothers are:~{ ~A~^,~}."
        '("Groucho" "Harpo" "Chico" "Zeppo" "Karl"))
 "The Marx brothers are: Groucho, Harpo, Chico, Zeppo, Karl."

様々なオブジェクトによってプリント表示される文字列を作成するまた別の方法は WITH-OUTPUT-TO-STRING です。このお手軽なマクロの値は、マクロの記述部分を伴った文字列ストリームに出力された全てを含む文字列です。これはつまりあなたの采配において FORMAT の全能力を使えるということを意味します。これは必ず必要なるでしょう。

(with-output-to-string (stream)
  (dolist (char '(#\Z #\a #\p #\p #\a #\, #\Space))
    (princ char stream))
  (format stream "~S - ~S" 1940 1993))
 "Zappa, 1940 - 1993"

区切り文字で、文字列を連結[編集]

前の項ではこれをするための十分なヒントが与えられているのですが、どのように区切り文字を使って文字列を連結するかを強調するのはここがちょうどいいタイミングかもしれません。 (192 168 1 1) あるいは ("192" "168" "1" "1") のような数値か文字列のリストを持っていると考えてください。そして別の文字列を作成するために、それらを区切り文字 "." あるいは ";" で連結したいとします。以下にいくつかの例を表してみました。

(defparameter *my-list* '(192 168 1 1))
 *MY-LIST*
(defparameter *my-string-list* '("192" "168" "1" "1"))
 *MY-STRING-LIST*
(setf *result-string* (format nil "~{~a~^.~}" *my-list*))
 "192.168.1.1"
*result-string*
 "192.168.1.1"
(setf *result-string* (format nil "~{~a~^.~}" *my-string-list*))
 "192.168.1.1"
*result-string*
 "192.168.1.1"
(setf *result-string* (format nil "~{~a~^;~}" *my-list*))
 "192;168;1;1"
*result-string*
 "192;168;1;1"

文字列を一回に一文字ずつ処理する[編集]

文字列を一回に一文字ずつ処理するには MAP 関数を使用します。

(defparameter *my-string* (string "Groucho Marx"))
 *MY-STRING*
(map 'string #'(lambda (c) (print c)) *my-string*)
 #\G 
#\r 
#\o 
#\u 
#\c 
#\h 
#\o 
#\Space 
#\M 
#\a 
#\r 
#\x 
"Groucho Marx"

あるいは LOOP でも同じことが出来ます。

(loop for char across "Zeppo"
      collect char)
 (#\Z #\e #\p #\p #\o)

単語か文字での文字列の反転[編集]

文字で文字列を反転させるのは組み込みの REVERSE 関数を使用すれば簡単です。あるいは同じ機能ですが破壊的作用を持つ NREVERSE でも可能です。

(defparameter *my-string* (string "DSL"))
 *MY-STRING*
(reverse *my-string*)
 "LSD"

Common Lisp では Perl の split と join で実現できるような、単語での文字列の反転を一行で書ける機能はありません。実現するには外部ライブラリの SPLIT-SEQUENCE を使うか、あるいは自分で解決法を実装するしかありません。以下の例で実装を試みています。

(defun split-by-one-space (string)
    "空白文字一文字で分割された文字列の部分文字列のリストを返します。
2つの連続した空白文字があると、
間に空の文字列が存在するものとみなされることに注意してください。"
    (loop for i = 0 then (1+ j)
          as j = (position #\Space string :start i)
          collect (subseq string i j)
          while j))
 SPLIT-BY-ONE-SPACE
(split-by-one-space "Singing in the rain")
 ("Singing" "in" "the" "rain")
(split-by-one-space "Singing in the  rain")
 ("Singing" "in" "the" "" "rain")
(split-by-one-space "Cool")
 ("Cool")
(split-by-one-space " Cool ")
 ("" "Cool" "")
(defun join-string-list (string-list)
    "文字列のリストを連結します。
それぞれの単語の間に空白文字を挿入します。"
    (format nil "~{~A~^ ~}" string-list))
 JOIN-STRING-LIST
(join-string-list '("We" "want" "better" "examples"))
 "We want better examples"
(join-string-list '("Really"))
 "Really"
(join-string-list '())
 ""
(join-string-list
   (nreverse
    (split-by-one-space
     "Reverse this sentence by word")))
 "word by sentence this Reverse"

大文字、小文字の操作[編集]

Common Lisp では文字列の大文字、小文字を操作する関数がいくつもあります。

(string-upcase "cool")
 "COOL"
(string-upcase "Cool")
 "COOL"
(string-downcase "COOL")
 "cool"
(string-downcase "Cool")
 "cool"
(string-capitalize "cool")
 "Cool"
(string-capitalize "cool example")
 "Cool Example"

これらの関数は :START や :END のようなキーワード引数をとることが出来ます。これを使用して任意で文字列の一部分だけを操作することが出来ます。これらの関数は全て、関数名の最初に "N" をつけると、同じ機能の破壊的な作用を持つ関数となります。

(string-capitalize "cool example" :start 5)
 "cool Example"
(string-capitalize "cool example" :end 5)
 "Cool example"
(defparameter *my-string* (string "BIG"))
 *MY-STRING*
(defparameter *my-downcase-string* (nstring-downcase *my-string*))
 *MY-DOWNCASE-STRING*
*my-downcase-string*
 "big"
*my-string*
 "big"

HyperSpec によると "STRING-UPCASE や STRING-DOWNCASE, STRING-CAPITALIZE では文字列は変更されません。しかし、文字列のどの文字も変更の必要が無いということならば、実装の裁量によっては、結果はその文字列そのものか、文字列のコピーということになるでしょう。" ということになるそうです。これは次の例の最後の返り値は実装によっては "BIG" にも "BUG" にもなるということです。もし結果を確実にしたいなら、 COPY-SEQ を利用してください。

(defparameter *my-string* (string "BIG"))
 *MY-STRING*
(defparameter *my-upcase-string* (string-upcase *my-string*))
 *MY-UPCASE-STRING*
(setf (char *my-string* 1) #\U)
 #\U
*my-string*
 "BUG"
*my-upcase-string*
 "BIG"

文字列の端から空白を取り除く[編集]

空白文字を取り除くことが出来るだけではなく、任意の文字列を取り除くことが出来ます。 STRING-TRIM, STRING-LEFT-TRIM そして STRING-RIGHT-TRIM は第2引数の最初、あるいは最後、もしくは両方から、第1引数で指定した文字を全て取り除いた部分文字列を返します。第1引数はどんな連続した文字列でもかまいません。

(string-trim " " " trim me ")
 "trim me"
(string-trim " et" " trim me ")
 "rim m"
(string-left-trim " et" " trim me ")
 "rim me "
(string-right-trim " et" " trim me ")
 " trim m"
(string-right-trim '(#\Space #\e #\t) " trim me ")
 " trim m"
(string-right-trim '(#\Space #\e #\t #\m) " trim me ")
 " tri"

大文字、小文字の操作の項で言及されていた警告はここでも適用されます。

シンボルと文字列の変換[編集]

関数 INTERN は文字列をシンボルに変換します。実際にはこの関数は第一引数の文字列として表されるシンボルが既にパッケージの中でアクセス可能かどうかをチェックし、必要であれば入力します。任意の第二引数はデフォルトで現在使用しているパッケージが設定されています。この関数の全てのコンセプトを説明し、二番目の返り値について言及するのはこの章の範囲を超えますので、 パッケージの詳細については、CLHS の章を参照してください。

この文字列のケースの関連性に注目してください。

(in-package "COMMON-LISP-USER")
 #<The COMMON-LISP-USER package, 35/44 internal, 0/9 external>
(intern "MY-SYMBOL")
 MY-SYMBOL
 NIL
(intern "MY-SYMBOL")
MY-SYMBOL
 :INTERNAL
(export 'MY-SYMBOL)
 T
(intern "MY-SYMBOL")
MY-SYMBOL
 :EXTERNAL
(intern "My-Symbol")
|My-Symbol|
 NIL
(intern "MY-SYMBOL" "KEYWORD")
:MY-SYMBOL
 NIL
(intern "MY-SYMBOL" "KEYWORD")
:MY-SYMBOL
 :EXTERNAL

反対に、シンボルから文字列に変換するには SYMBOL-NAME か STRING を使用します。

(symbol-name 'MY-SYMBOL)
 "MY-SYMBOL"
(symbol-name 'my-symbol)
 "MY-SYMBOL"
(symbol-name '|my-symbol|)
 "my-symbol"
(string 'howdy)
 "HOWDY"

文字と文字列の変換[編集]

関数 COERCE を使用することで長さ1の文字列を文字に変換することが出来ます。 COERCE ではどんな連続した文字でも文字列に変換することも出来ます。しかし、 COERCE を使って一つの文字を文字列に変換することは出来ません。それには STRING を使用しなければなりません。

(coerce "a" 'character)
 #\a
(coerce (subseq "cool" 2 3) 'character)
 #\o
(coerce "cool" 'list)
 (#\c #\o #\o #\l)
(coerce '(#\h #\e #\y) 'string)
 "hey"
(coerce (nth 2 '(#\h #\e #\y)) 'character)
 #\y
(defparameter *my-array* (make-array 5 :initial-element #\x))
 *MY-ARRAY*
*my-array*
 #(#\x #\x #\x #\x #\x)
(coerce *my-array* 'string)
 "xxxxx"
(string 'howdy)
 "HOWDY"
(string #\y)
 "y"
(coerce 'string #\y)
 Type-error in KERNEL::OBJECT-NOT-TYPE-ERROR-HANDLER:
     #\y is not of type (OR CONS CLASS SYMBOL)

文字列の中の要素を検索する[編集]

関数 FIND, POSITION そして、ほぼ同じ機能を持った FIND-IF, POSITION-IF を使用することで文字列中の文字を検索することが出来ます。

(find #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equal)
 #\t
(find #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
 #\T
(find #\z "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
 NIL
(find-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks.")
 #\1
(find-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks." :from-end t)
 #\0
(position #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equal)
 17
(position #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
 0
(position-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks.")
 37
(position-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks." :from-end t)
 43

あるいは COUNT や COUNT-IF を使用することで文字列中の文字を数えることが出来ます。

(count #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equal)
 2
(count #\t "The Hyperspec contains approximately 110,000 hyperlinks." :test #'equalp)
 3
(count-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks.")
 6
(count-if #'digit-char-p "The Hyperspec contains approximately 110,000 hyperlinks." :start 38)
 5

文字列の中の部分文字列を検索する[編集]

関数 SEARCH は文字列中の部分文字列を検索することが出来ます。

(search "we" "If we can't be free we can at least be cheap")
 3
(search "we" "If we can't be free we can at least be cheap" :from-end t)
 20
(search "we" "If we can't be free we can at least be cheap" :start2 4)
 20
(search "we" "If we can't be free we can at least be cheap" :end2 5 :from-end t)
 3
(search "FREE" "If we can't be free we can at least be cheap")
 NIL
(search "FREE" "If we can't be free we can at least be cheap" :test #'char-equal)
 15

文字列を数値に変換する[編集]

Common Lisp は文字列で表された数字を、対応する数値の値に変換する PARSE-INTEGER という関数があります。二番目の返り値は文字列の解析が止まった場所の添え字となります。

(parse-integer "42")
42
 2
(parse-integer "42" :start 1)
2
 2
(parse-integer "42" :end 1)
4
 1
(parse-integer "42" :radix 8)
34
 2
(parse-integer " 42 ")
42
 3
(parse-integer " 42 is forty-two" :junk-allowed t)
42
 3
(parse-integer " 42 is forty-two")

 Error in function PARSE-INTEGER:
     There's junk in this string: " 42 is forty-two".

PARSE-INTEGER は #X のような基数の指定子を解釈しませんし、他の数値型を解析する組み込み関数もありません。このようなケースでは READ-FROM-STRING を使用することができますが、この関数を使用する場合は十全な読み込み解釈が有効になっていることに注意してください。

(read-from-string "#X23")
35
 4
(read-from-string "4.5")
4.5
 3
(read-from-string "6/8")
3/4
 3
(read-from-string "#C(6/8 1)")
#C(3/4 1)
 9
(read-from-string "1.2e2")
120.00001
 5
(read-from-string "symbol")
SYMBOL
 6
(defparameter *foo* 42)
 *FOO*
(read-from-string "#.(setq *foo* \"gotcha\")")
"gotcha"
 23
*foo*
 "gotcha"

数値を文字列に変換する[編集]

Common Lisp では PRINC-TO-STRING や PRIN1-TO-STRING のような数値を文字列に変換する関数が提供されています。文字列と数値を連結したいなら PRINC-TO-STRING や PRIN1-TO-STRING を以下の例のように使用することができます。

(concatenate 'string "9" (princ-to-string 8))
 "98"
(concatenate 'string "9" (prin1-to-string 8))
 "98"
(concatenate 'string "9" 8)

The value 8 is not of type SEQUENCE.
   [Condition of type TYPE-ERROR]

文字列の比較[編集]

一般的な関数の EQUAL と EQUALP は二つの文字列が等しいかどうかのテストに使用することもできます。文字列は大文字、小文字を区別する方法の EQUAL か、区別しない方法の EQUALP のどちらかで、それぞれの要素ごとに比較されていきます。他にもたくさんの文字列比較に特化した関数があります。実装で定義された文字の属性を活用するならこれらの関数を使いたくなるでしょう。その場合はベンダーのドキュメントをあたってください。

以下にいくつかのサンプルを示してみました。不一致の検査をした全ての関数が、最初のミスマッチが発生した位置で一般的な真偽値を返すことに注意して下さい。もっと多彩な操作を行いたい場合は、一般的な手続き関数の MISMATCH を使用することができます。

(string= "Marx" "Marx")
 T
(string= "Marx" "marx")
 NIL
(string-equal "Marx" "marx")
 T
(string< "Groucho" "Zeppo")
 0
(string< "groucho" "Zeppo")
 NIL
(string-lessp "groucho" "Zeppo")
 0
(mismatch "Harpo Marx" "Zeppo Marx" :from-end t :test #'char=)
 3

文字列の分割[編集]

SPLIT-SEQUENCE は http://www.cliki.net/SPLIT-SEQUENCE で利用可能な、 Common Lisp のユーティリティーコレクションの一部です。

(split-sequence:SPLIT-SEQUENCE #\Space "Please split this string.") 
 ("Please" "split" "this" "string.")

Copyright © 2002-2007 The Common Lisp Cookbook Project [1]