第16章「文字列」

概要

ANSI Common Lispの第16章「文字列」を説明します。

文字列の概要

文字列はその名の通り、文字の列です。ANSI Common Lispでは文字のベクトルとして定義されており、ベクトルに対して可能な操作は文字列に対しても適用可能です。さらに、ベクトルはシーケンスなので、シーケンス一般に適用可能な処理も可能です。

そのため、文字列のみに関する仕様は非常に簡素に定義されています。

実際にプログラミングの中で文字列を使うときは、CL-PPCREなどの正規表現ライブラリを用いることが多いと思います。CL-PPCREはPerl互換で高速であり、様々なライブラリからも用いられているため、事実上の標準として広く利用されています。

このページでは、サードパーティ製のライブラリですがsplit-sequenceだけANSI Common Lispの標準仕様と共に紹介します。このライブラリはパブリックドメインで古くから利用されており、文字列の分割を行う目的で広く利用されています。

文字列の生成と参照

この節では文字列の生成と参照の方法について説明します。

文字列を生成する最も簡単な方法は、ダブルクォートで囲んでリテラル表記することです。
"abc"
; => "abc"

(equal "abc" 
       (make-array 3 :element-type 'character 
                     :initial-contents '(#\a #\b #\c)))
; => T

リテラル表記された文字列は、character型もしくはそのサブタイプに要素の型を限定したベクトルとして生成されます。

ANSI Common Lispでは明示的にmake-array関数で文字列を生成する以外の方法が定められています。

生成: make-string

make-string関数は、指定された長さの文字列(文字ベクトル)を生成します。
defparameter *str* (make-string 3))
; => *STR*
*str*
; => ""
(length *str*)
; => 3
(type-of *str*)
; => (SIMPLE-BASE-STRING 3)
(aref *str* 0)
; => #\Null
make-string関数がどの文字でベクトルの要素を埋めるかは処理系依存です。GNU CLISPではヌル文字#\NULLが使われています。

文字の章でも説明しましたが、Unicode に対応した処理系では英数字も日本語も区別なく扱うことができます。
(setf (aref *str* 0) #\あ)
; => #\HIRAGANA_LETTER_A
(setf (aref *str* 1) #\い)
; => #\HIRAGANA_LETTER_I
(setf (aref *str* 2) #\う)
; => #\HIRAGANA_LETTER_U
*str*
; => "あいう"

変換: string, write-to-string

文字列を生成する別の方法は、他のオブジェクトから変換することです。変換の方法は2通りあります。

変換前のオブジェクトが文字またはシンボルの場合、string関数を使うことができます。文字の場合は、その文字を要素とする長さ1の文字列を、シンボルの場合はそのsymbol-nameを返します。
(string #\a)
; => "a"
(string 'symbol)
; => "SYMBOL"

数を文字列に変換する場合は、write-to-string関数を用います。
(write-to-string 10)
; => "10"
(write-to-string 3.14d0)
; => "3.14d0"

もう少し複雑な変換を行いたい場合はformat関数の第1引数にnilを指定します。
(format nil "~:d" 10000)
; => "10,000"
(format nil "~,5f" 3.14d0)
; => "3.14000"
(format nil "~10,5f" 3.14d0)
; => "   3.14000"

参照: char, schar

文字列はベクトルの一種なので、arefアクセッサでアクセスすることもできますが、ベクトルの型がcharacterかそのサブタイプに限定されているので、型を指定すると少し高速にアクセスできる可能性があります。
(aref (the string "abc") 1)
; => #\b

しかし、このアクセス方法は冗長なので、charというアクセッサが独立して定められています。
(char "abc" 1)
; => #\b

また、文字列の中には可変長またはフィルポインタ付のベクトルを基礎とするものもありますが、可変長でもなく、フィルポインタもないsimple-stringもあります。その場合はscharアクセッサを使うことで、最も高速にアクセスできる可能性があります。
(schar "abc" 1)
; => #\b

ただし、文字列はsvrefアクセッサではアクセスできないことに注意してください。
(svref "abc" 1)
; *** - SVREF: "abc" is not a SIMPLE-VECTOR
svrefアクセッサはsimple-vector型に限定した汎参照ですが、simple-vectorは要素の型がtと決まっており、特定の型に限定することはできません。simple-stringsimple-base-stringなどの文字列はそれぞれcharacter, base-characterを型とするベクトルなので、simple-vectorではありません。

文字列に関するオペレータ

この節では文字列に関するオペレータを紹介します。

判定: stringp, string=, string/=

あるオブジェクトが文字列であるかどうかを判定するにはstringp述語関数を用います。
(stringp 10)
; => NIL
(stringp "10")
; => T
stringpは型を調べる述語関数なので、typepと同じです。
(typep "10" 'string)
; => T

また、文字列の一致を確認するにはstring=述語関数を用います。
(string= "abc" "ABC")
; => NIL
(string= "ABC" "ABC")
; => T
string/=関数は一見すると(not (string= ...))のように見えますが、実は述語関数ではありません。この関数は文字列同士を比較し、不一致になった文字のインデックスを返します。一致していればnilが返り値になります。
(string/= "abc" "ABC")
; => 0
(string/= "ABc" "ABC")
; => 2
(string/= "ABC" "ABC")
; => NIL

空白の除去: string-trim

文字列の中から空白文字を除去するにはstring-trim関数を使います。この関数の第1引数は少し曲者で、ANSI Common Lispでは「文字バッグ( character-bag )」として定められています。

最もシンプルに空白を除去したい場合は、以下のように指定します。
(string-trim " " "   abc   ")
; => "abc"

第1引数の文字バッグとは、文字のシーケンスとして定められているため、文字のベクトルである文字列を指定するだけでなく、文字を要素にしたリストでも指定することができます。
(string-trim '(#\space) "   abc   ")
; => "abc"
string-trim関数は第1引数の文字バッグ(シーケンス)に入っている文字を削除しますが、全ての位置の文字を削除する訳ではなく、両端のみ削除します。
(string-trim " " " a b c ")
; => "a b c"

もし全ての空白を除去したい場合、少し冗長ですが以下のようにsubstitute関数を使うと実現できます。
(substitute #\null #\space " a b c ")
; => "abc"
string-trim関数は両端しか文字を削除しないので、空白の除去に限定されている訳ではありませんが、実用的には空白除去が中心的な役割になると思います。

大文字・小文字: string-upcase, string-downcase

文字列を大文字または小文字に変換するにはstring-upcase関数またはstring-downcase関数を用います。
(string-upcase "abc")
; => "ABC"

(string-downcase "AbC")
; => "abc"

対象の文字列の中にすでに変換後の文字が含まれている場合もエラー等は通知せず、そのまま処理してくれます。

参考: 連結: format

文字列を連結したい場合はかなり多いと思います。そのような時はformat関数が便利です。
以下のようにstring-join関数を自分で定義すれば、簡単に文字列連結関数を用意することができます。
(defun string-join (strings &optional (sep " "))
  (format nil (concatenate 'string "~{~a~^" sep "~}")
          strings))
; => STRING-JOIN
format関数は~{~}で囲まれた部分について、繰り返し処理を行います。繰り返しの対象はリストなので、連結したい文字列のリストを用意して繰り返しながらフォーマットを行います。ただ、どのような文字または文字列で連結するかは場合によって異なるため、オプショナル引数で指定できるようにしています。
(string-join '("abc" "def" "ghi"))
; => "abc def ghi"
(string-join '("abc" "def" "ghi") ", ")
; => "abc, def, ghi"
(string-join '("abc" "def" "ghi") " | ")
; => "abc | def | ghi"

参考: 分割: split-sequence

文字列の分割はANSI Common Lispの標準関数だけで自分で定義するのではなく、split-sequenceを使うのが一般的です。これはパブリックドメインなので、Quicklisp などからダウンロードしてもいいですし、split-sequence.lispからダウンロードして使っても構いません。ASDFを利用している場合は、split-sequence.asdを使ってロードしてください。

使い方は簡単で、第1引数に分割の基準とするデリミタを、第2引数に分割したい文字列を指定します。
(asdf:load-system :split-sequence)
; => T
(use-package :split-sequence)
; => T

(split-sequence #\, "abc,def,ghi")
; => ("abc" "def" "ghi") ;
;    11

0 件のコメント :

コメントを投稿