第13章「文字」

概要

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

文字とは何か

計算機における「文字」とは、日常で使っている自然言語の文字とは異なる特徴があります。
  • 文字は「集合」に属する形で定められます。計算機は自分の手で新しい文字を書くことはできないので、あらかじめ規格として定められた種類の文字のみを扱うことができます。そのような文字の集まりを「文字集合( character set )」と呼び、代表的な日本語の文字は JIS X 0280 で定められています。
  • 文字には「番号」が割り振られています。人間は文字を見れば区別できますが、計算機は文字を見ることはできません。そのため、文字集合における各要素の文字にはそれぞれ番号( character code point )を割り振っておき、その番号で文字を区別します。文字から番号へ、番号から文字へという相互変換が可能になります。
  • 文字に割り振られた番号を「符号化」する方法が定められています。計算機は最終的にはビットの情報、すなわちオン・オフしか区別できないため、割り振られた番号をどのようなビット列にするかを決めておく必要があります。その方法は符号化方式( character encoding scheme )と呼ばれ、文字集合とセットになっている場合もあれば、独立している場合もあります。英数字を定めた最も著名な文字集合である ASCII のように7bitで表すことができる場合は、単に番号を1オクテットの2進数に変換するだけのシンプルな符号化方式で足りますが、マルチバイト文字列(9bit以上を必要とする文字集合)の場合の符号化方式は複雑になります。
文字集合、文字番号、文字符号化方式という3つの特徴を持って初めて、文字を計算機で扱うことが可能になります。しかし、これらの組み合わせは国や地域よって様々な組み合わせがあり、また、時代によっても変わってきます。そのため、ANSI Common Lispでは第13章「文字」の多くをあえて「処理系依存」として、変化に対応できるようにしています。現代では代表的な処理系の多くが日本語も含めた Unicode という文字集合を使っているため、英数字とギリシャ文字と漢字の違いなどの自然言語上の違いを気にすることなく、文字を使うことができます。

このページでは、ANSI Common Lispの標準に定められた文字に関するオペレータを中心に紹介します。実行結果などは処理系やオペレーティングシステムの環境によって異なる場合がありますので、自分の処理系でも適宜試してみてください。

文字と番号と名前

Common Lispにおいて、文字は他のデータと同様にオブジェクトです。文字オブジェクトはcharacterクラスかそのサブクラスのインスタンスとして生成されます。

文字オブジェクトの最も重要な役割は、印字されることです。文字は計算機と人間の間の相互意思伝達手段ですから、基本的には文字そのものが印字されて、通常はその列として複数の文字が印字されて、特定のメッセージを伝達する目的で使われます。

しかし、ANSI Common Lispでは他にも文字列オブジェクトのスロットを参照することができるように定められています。この節ではそれらを紹介します。

文字定数

文字は#\を付けて表すことで定数、すなわち自己評価値として認識されます。
#\A
; => #\A

#\a
; => #\a

#\1
; => #\1

#\あ
; => #\HIRAGANA_LETTER_A

#\λ
; => #\GREEK_SMALL_LETTER_LAMDA

上の3つは文字をそのまま自己評価値として評価して返していますが、下の二つは文字に付けられた名前が帰ってきます。しかし、文字そのものと文字の名前には、本質的な違いはありません。
(eq #\あ #\HIRAGANA_LETTER_A)
; => T

(eq #\λ #\GREEK_SMALL_LETTER_LAMDA)
; => T

Common Lispでは文字オブジェクトは数と同様に基本的なオブジェクトの一つであり、一般的にはベクトルにして「文字列」の形で扱うことが一般的です。文字列の各要素は文字定数になります。
(aref "abc" 1)
; => #\b

(aref "あいうえお" 2)
; => #\HIRAGANA_LETTER_U

前節で説明したような計算機上の文字の実装面はCommon Lispによって隠蔽(抽象化)されており、プログラマは文字を一般的な文字と同様に自然に扱うことができます。

文字と番号: char-code, code-char

このようにCommon Lispにおける文字は文字オブジェクトの形で実装され、使うことができるように準備されていますが、そのオブジェクトの最も重要なスロットを参照するのがchar-code関数です。この関数はANSI Common Lispでも文字集合における全ての文字に必要なスロットへのアクセッサとして定められています。
(char-code #\A)
; => 65

(format t "~x" 65)
; 41
; => NIL

多くの代表的な処理系が文字集合として持っている Unicode では全ての文字に統一的な文字番号を与えています。この文字番号は4桁の16進数で定められており、英文字の#\AU+0041 として定められています。16進数における#x0041は10進数の65なので、#\Aという文字オブジェクトの文字番号をchar-codeで調べると65という値が返されます。
(char-code #\あ)
; => 12354

(format t "~x" *)
; 3042
; => NIL

日本語のひらがなである#\あUnicode では U+3042 と定められており、16進数の#x3042は10進数で12354に該当します。
(char-code #\greek_small_letter_lamda)
; => 955

(format t "~x" *)
; 3BB
; => NIL

ギリシャ文字の小文字のλは Unicode では U+03BB と定められており、16進数の#x03BBは10進数で955に該当します。

このように、char-code関数を使えば文字に割り振られている番号を調べることができます。

逆に番号から文字に変換するにはcode-char関数を使います。
(code-char 65)
; => #\A

(code-char #x3042)
; => #\HIRAGANA_LETTER_A

(code-char #x3bb)
; => #\GREEK_SMALL_LETTER_LAMDA

Unixode は16進数で定められているので、#xリーダーマクロを使って16進数の文字番号をそのまま指定した方が楽でしょう。
(loop for i from #x3041 to #x304A do (format t "~c " (code-char i)))
; ぁ あ ぃ い ぅ う ぇ え ぉ お 
; => NIL

文字と名前: char-name, name-char

一部の文字については、文字の名前が定められています。

ANSI Common Lispの仕様で定められている文字名は2つあります。
(format t "~c" #\newline)
; 
; => NIL

(format t "A~cB" #\space)
; A B
; => NIL

一つは#\Newlineで改行を意味します。この文字は自然言語には存在しませんが、制御文字と呼ばれる重要な文字です。もう一つは#\Spaceで、一般には半角スペースを意味します。改行も空白も文字の実体を入力することは可能ですが、何も内容に見えるので、文字定数をそのまま使うのではなく、文字の名前を使うことが推奨されます。
;; 改行を文字の名前ではなくそのまま定数として表記する例
(eq #\newline #\
)
; => T

;; 空白を同様に扱う例
(eq #\space #\ )
; => T

その他に、標準に準じると定められている文字の名前がいくつかあります。#\Tab#\Return, #\Linefeedなどです。これらは環境や処理系によって扱いが異なってきます。
(format t "A~cB" #\tab)
; A    B
; => NIL

これらの制御文字に関わるANSI Common Lisp標準仕様の他に、すでに挙げた例のように文字集合に合わせて普通の文字にも名前が割り振られていることがあります。それらは、char-nameで参照することができます。
(char-name #\A)
; => "LATIN_CAPITAL_LETTER_A"

(char-name #\あ)
; => "HIRAGANA_LETTER_A"

(char-name #\λ)
; => "GREEK_SMALL_LETTER_LAMDA"

これらの名前は文字オブジェクトから取得するだけでなく、文字オブジェクトを生成するためにも使うことができます。
(format t "~c" (name-char "GREEK_SMALL_LETTER_LAMDA"))
; λ
; => NIL

文字の名前は#\リーダーマクロに続けて記述すればそのまま文字定数となりますので、name-charはプログラムの中で使用する場合などに限られるかもしれません。

これらの「文字の名前」の定義は処理系によっても異なりますので、複数の処理系で動作することを期待するプログラムの場合は使用しないか、処理系に合わせた名前を採用するように配慮すべきでしょう。

なお、本章とは関係がありませんが、処理系に合わせた処理を記述するには#+, #-リーダーマクロが役に立ちます。CLISP, SBCL, CCLの3つの処理系に対応しながら、全角スペースを使うには以下のようにすると正常に動作します。
(defconstant +zenkaku_space+
  #+(or clisp sbcl) #\ideographic_space
  #+ccl #\U+3000)
; => +ZENKAKU_SPACE+

(format t "あ~cい" +zenkaku_space+)
; あ い
; => NIL

文字の生成

文字の生成で最も簡便な方法は#\リーダーマクロを使ってリテラル表記することです。この表記は、続くのが1文字ならばその文字オブジェクトを生成し、2文字以上なら対応する文字の名前を持つ文字オブジェクトを生成します。1文字の場合は大文字と小文字が区別されますが、2文字以上の場合は全て大文字に変換されてから文字の名前が検索されます。

前節で紹介したcode-char関数は文字の番号から文字を生成する方法であり、name-char関数は文字の名前から文字を生成する方法でした。この節では、それ以外の文字の生成方法を紹介します。

character関数

文字を生成する際の基本的な関数としてcharacterが用意されています。これは文字指定子を引数に取り、文字を返します。

文字指定子とは以下のようなオブジェクトです。
  • 文字定数そのもの
  • 1文字の文字列
  • 1文字のシンボル
以下がサンプルです。
(character #\A)
; => #\A

(character "A")
; => #\A

(character 'A)
; => #\A

シンボルの場合は大文字と小文字の違いに注意してください。
(character #\a)
; => #\a

;; 文字列は小文字から大文字への変換を行わない
(character "a")
; => #\a

;; シンボルは自動で大文字に変換する
(character 'a)
; => #\A

;; 小文字のシンボルを使うときは | | で囲む
(character '|a|)
; => #\a

;; 1文字の小文字のシンボルの場合は \x の形式でも可能
(character '\a)
; => #\a

;; シンボルのパッケージはなんでも良い
(character '#:|a|)
; => #\a

ただ、文字オブジェクトを作るために文字列やシンボルを作らなければならないため、この関数は文字の生成ではなく型の変換として使われることを意図しています。

大文字・小文字の変換: char-upcase, char-downcase

アルファベットにおいて大文字と小文字の変換は非常に頻繁に行われる処理です。シンボルの例を見ても、標準では小文字を大文字に変換してからシンボルを生成します。

小文字を大文字に変換するにはchar-upcaseを使用します。
(char-upcase #\a)
; => #\A

文字列の要素全てがアルファベットであるときに全てを変換するにはmap関数を使用します。
(map 'string #'char-upcase "common-lisp")
; => "COMMON-LISP"
stringという型はcharacter型のベクトルと等しいので、このように記述しても同じです。
(map '(vector character) #'char-upcase "common-lisp")
; => "COMMON-LISP"

逆に大文字を小文字に変換するにはchar-downcaseを使用します。
(char-downcase #\A)
; => #\a

参考: ビット操作による大文字・小文字の変換

ANSI Common Lispには標準で関数が定められているので使うことはないですが、前章「」で扱ったビット単位の操作を使うと、大文字・小文字の変換は簡単に行うことができます。

まず、英語のアルファベット文字は Unicode においても ASCII においても、文字集合において同じ文字番号が割り振られています。そして、大文字と小文字は常に32ずつ差があるように番号が割り振られています。そのため、大文字の文字番号に32を足せば小文字になり、逆に小文字の文字番号から32を引けば大文字になります。
32という数が2進数で複雑になればビット操作を使う意味はないのですが、32は2進数で6桁目が1で、残りが0というシンプルなビット列で表すことができます。
(format t "~b" 32)
; 100000
; => NIL

このことを利用すれば、32を足すという操作は6桁目のビットを1にするだけでよく、逆に32を引くという操作は6桁目のビットを0にするだけでいいのです。

大文字・小文字の変換をフラグの操作に見立てると、以下のようにそれぞれ1行ずつで関数を定義できます。
(defun my-upcase (c)
  (code-char (logand (char-code c) (lognot #b100000))))
; => MY-UPCASE

(defun my-downcase (c)
  (code-char (logior (char-code c) #b100000)))
; => MY-DOWNCASE

ただし、これはなんら例外的状況への対処を含んでいないため、実際にはANSI Common Lisp標準のchar-upcase関数とchar-downcase関数を使用してください。

文字の判定

この節では文字を比較したり、同一性を判定したりする関数を紹介します。

等しさの判定: char=, char/=, char-equal, char-not-equal

文字の同一性を比較する場合は、基本的にはchar=関数またはchar/=関数を使用してください。
(char= #\A #\a)
; => NIL
(char= #\A #\A)
; => T

(char/= #\A #\a)
; => T
(char/= #\A #\A)
; => NIL

また、場合によっては大文字と小文字を区別したくない場合もあるかと思います。char-equal関数とchar-not-equal関数は大文字・小文字を区別しませんので、こちらを使うこともできます。
(char-equal #\A #\a)
; => T

印字文字の判定: alphanumericp, alpha-char-p

制御文字ではなく、印字可能な文字であるかどうかを判定するにはalphanumericp関数が使えます。
(alphanumericp #\a)
; => T
(alphanumericp #\あ)
; => T
(alphanumericp #\λ)
; => T

(alphanumericp #\newline)
; => NIL
(alphanumericp #\space)
; => NIL

この関数は英数字ではなくてもいわゆる自然言語の文字であればTになりますが、記号文字はNILになるので注意してください。
(alphanumericp #+)
; => NIL

(map 'list #'alphanumericp "0120-123-456")
; => (T T T T NIL T T T NIL T T T)
alphanumericp関数の判定範囲のうち、数を除いたものはalpha-char-p関数です。
(map 'list #'alpha-char-p "abcd1234")
; => (T T T T NIL NIL NIL NIL)

これらはANSI Common Lisp標準の関数ですが、正規表現を扱うCommon Lispのサードパーティ製ライブラリとして、CL-PPCREというデファクトスタンダードが存在します。これはPerl互換の正規表現ライブラリですが、非常に高速であり、依存ライブラリも少ないため一般的に利用されています。文字のカテゴリを使うプログラムであれば、こちらのライブラリの使用も検討してみてください。

大文字・小文字の判定: upper-case-p, lower-case-p, both-case-p

ある文字が大文字・小文字の区別をする文字であるとき、both-case-p関数は真になります。
(both-case-p #\a)
; => T
(both-case-p #\あ)
; => NIL

また、大文字であるかどうかはupper-case-p関数で、小文字であるかどうかはlower-case-pで調べることができます。
(map 'list #'upper-case-p "Sample")
; => (T NIL NIL NIL NIL NIL)

(map 'list #'lower-case-p "Sample")
; => (NIL T T T T T)
upper-case-plower-case-pも、both-case-pとは独立して評価されるため、大文字・小文字の区別をしない文字の場合は、常にNILになります。
(both-case-p #\space)
; => NIL
(upper-case-p #\space)
; => NIL
(lower-case-p #\space)
; => NIL

1 件のコメント :

  1. 大変参考になりました。
    ありがとうございます。

    返信削除