【Common Lisp】
文字列と文字の基本の使い方
(string, char)

  • 文字列は "" で作り、lengthcharsubseq で読む。
    loop for c across で全文字を走査できる。
  • 文字列は element-type 'character のベクタなので、aref でアクセスでき、mapremove-ifevery などのシーケンス関数がそのまま使える。
  • 文字は #\a 記法で表し、char-code / code-char で数値と相互変換できる。
  • 文字列固有の関数として string=string<searchstring-upcasestring-trim などがある。
  • format nil で文字列を組み立て、read-line で標準入力から1行読む。
    どちらも文字列の主な生成手段として make-string と並んで使われる。
  • parse-integerwrite-to-string で数値と文字列を相互変換する。

関連記事

1. 文字列を作る・読む・ループする

1.1. リテラル

文字列リテラルは "" で囲むだけで生成できます。

"hello"
;=> "hello"

"Common Lisp"
;=> "Common Lisp"Code language: Lisp (lisp)

1.2. length・char で読む

length は文字列の長さを返します。
char は 0 始まりのインデックスで文字を取り出します。

(length "hello")  ;=> 5
(char "hello" 0)  ;=> #\h
(char "hello" 4)  ;=> #\oCode language: Lisp (lisp)

1.3. 文字列の比較(string=)

string= は2つの文字列が等しいかどうかを返します。
大文字小文字を区別し、:start1 :end1 :start2 :end2 で比較範囲を部分指定できます。

コマンドライン引数の照合に使う例です。

(defun dispatch-command (cmd)
  (cond ((string= cmd "start") 
         (format t "starting...~%"))
        ((string= cmd "stop")  
         (format t "stopping...~%"))
        (t                     
         (format t "unknown: ~a~%" cmd))))

(dispatch-command "start")  ; starting...
(dispatch-command "STOP")   ; unknown: STOPCode language: Lisp (lisp)

文字列の比較に equal も使えますが、string= は文字列専用で、文字列以外を渡すとエラーになる点で意図が明確です1

辞書順の比較には string<string>string<=string>= を使います。
string<nil か整数を返します。
整数は最初に異なる位置のインデックスで、ifwhen の条件にそのまま渡せます2

(defun sort-words (words)
  (sort (copy-list words) #'string<))

(sort-words '("banana" "apple" "cherry" "date"))
;=> ("apple" "banana" "cherry" "date")Code language: Lisp (lisp)

1.4. ケースインセンシティブ

大文字小文字を無視するには string-equal を使います。

(defun dispatch-command-ci (cmd)
  (cond ((string-equal cmd "start")
     (format t "starting...~%"))
        ((string-equal cmd "stop")
     (format t "stopping...~%"))
        (t
     (format t "unknown: ~a~%" cmd))))

(dispatch-command-ci "STOP")  ; stopping...Code language: Lisp (lisp)

2. 文字列は文字のベクタである

文字列は文字のベクタである “hello” #\h #\e #\l #\l #\o 0 1 2 3 4 char 専用アクセサ aref 汎用アクセサ (char “hello” 1) → #\e (stringp “hello”) → T (vectorp “hello”) → T (aref “hello” 1) → #\e (arrayp “hello”) → T 文字列以外でもOK

2.1. aref でアクセスできる

Common Lisp の文字列は element-type 'character のベクタです。
chararef はどちらも同じ要素を返します。

(char  "hello" 1)  ;=> #\e
(aref  "hello" 1)  ;=> #\eCode language: Lisp (lisp)

char は文字列専用のアクセサで、文字列以外を渡すとエラーになります。
aref はベクタ全般に使える汎用アクセサです。
文字列専用のコードでは char を使う方が意図が明確になります3

文字列であることを確認するには stringp を使います。
vectorpt を返します4

(stringp "hello")  ;=> T
(vectorp "hello")  ;=> T
(arrayp  "hello")  ;=> TCode language: Lisp (lisp)

2.2. loop across で走査する

loopacross 節で全文字を順に取り出せます。
リストに対する for x in に対応します。

(defun find-diff-index (a b)
  "2つの文字列が最初に異なる位置を返す。同じなら nil。"
  (loop for i from 0
        for ca across a
        for cb across b
        when (char/= ca cb)
          return i))

(find-diff-index "abcde" "abXde")  ;=> 2
(find-diff-index "abc"   "abc")    ;=> NILCode language: Lisp (lisp)

インデックスと文字を同時に扱うには for i from 0 と組み合わせます。

3. 文字と文字コード

文字は #\ に続けて文字を書いて表します。

文字と文字コード 文字 #\a リテラル表記 char-code 数値 97 ASCII コード code-char 主な文字コード #\a → 97 #\A → 65 #\0 → 48 #\space→ 32 特殊文字:#\space #\newline #\tab #\nul 応用例:Caesar 暗号 (mod (+ (- (char-code c) base) n) 26) “Hello” + 3 → “Khoor”

スペースや改行などの特殊文字には名前があります。

#\a         ; 小文字 a
#\A         ; 大文字 A
#\0         ; 数字の 0
#\space     ; スペース
#\newline   ; 改行(LF)
#\tab       ; タブ
#\return    ; キャリッジリターン(CR)
#\nul       ; NUL 文字(ASCII 0)
#\escape    ; ESC(ASCII 27)
#\delete    ; DEL(ASCII 127)
#\backspace ; BS(ASCII 8)
#\page      ; フォームフィード(FF、ASCII 12)Code language: Lisp (lisp)

3.1. 文字の比較(char=)

文字の比較には char=char< などを使います。

大文字小文字を無視するには char-equalchar-lessp などを使います5

(char= #\a #\a)      ;=> T
(char< #\a #\b)      ;=> T
(char-equal #\a #\A) ;=> T   ; 大小無視Code language: Lisp (lisp)

3.2. char-codeとcode-char

文字コードを数値として演算するには、char-codeを使います。

(char-code #\a)        ;=> 97
(char-code #\A)        ;=> 65
(char-code #\0)        ;=> 48
(char-code #\space)    ;=> 32
(char-code #\newline)  ;=> 10

(code-char 97)         ;=> #\aCode language: Lisp (lisp)

char-code で文字コード(整数)を得て、code-char で逆変換します6

英小文字を 0〜25 のインデックスに変換するときは char-code の差を使います。
頻度カウントや Caesar 暗号の実装でよく出るパターンです。

(defun caesar-shift (s n)
  "文字列を Caesar 暗号で n 文字シフトする。"
  (map 'string
       (lambda (c)
         (if (alpha-char-p c)
             (let* ((base (if (upper-case-p c) (char-code #\A) (char-code #\a)))
                    (shifted (mod (+ (- (char-code c) base) n) 26)))
               (code-char (+ base shifted)))
             c))
       s))

(caesar-shift "Hello, World!" 3)   ;=> "Khoor, Zruog!"
(caesar-shift "Khoor, Zruog!" -3)  ;=> "Hello, World!"Code language: Lisp (lisp)

mod は Common Lisp では常に非負の値を返します7

3.3. 文字の判定・比較(alphanumericp)

文字の種類を調べる関数です。

(alpha-char-p #\a)      ;=> T    ; アルファベットか
(alpha-char-p #\1)      ;=> NIL
(digit-char-p #\5)      ;=> 5    ; 数字か(数値を返す)
(digit-char-p #\a)      ;=> NIL
(alphanumericp #\a)     ;=> T    ; アルファベットか数字か
(alphanumericp #\space) ;=> NILCode language: Lisp (lisp)
8

大文字小文字の判定です。

(upper-case-p #\A)  ;=> T
(lower-case-p #\a)  ;=> T
(upper-case-p #\a)  ;=> NILCode language: Lisp (lisp)

4. シーケンス関数をそのまま使う

文字列はベクタの一種なので、mapremove-ifeverysomecount-if などのシーケンス関数がリストやベクタと同じように使えます。

4.1. map で変換する

第1引数に 'string を渡すと文字列が返ります。

識別子の正規化など、全文字を変換したいときに使います9

(defun normalize-id (s)
  "スペースをアンダースコアに置き換えて小文字にする。"
  (string-downcase
   (map 'string (lambda (c) (if (char= c #\space) #\_ c)) s)))

(normalize-id "Hello World")  ;=> "hello_world"
(normalize-id "User Name")    ;=> "user_name"Code language: Lisp (lisp)

全文字を変換した結果を文字列にまとめるには map が簡潔です。
ROT13 エンコードの例です。

(defun encode-rot13 (s)
  (map 'string
       (lambda (c)
         (cond ((and (char<= #\a c) (char<= c #\z))
                (code-char (+ (char-code #\a)
                              (mod (+ (- (char-code c) (char-code #\a)) 13) 26))))
               ((and (char<= #\A c) (char<= c #\Z))
                (code-char (+ (char-code #\A)
                              (mod (+ (- (char-code c) (char-code #\A)) 13) 26))))
               (t c)))
       s))

(encode-rot13 "Hello, World!")  ;=> "Uryyb, Jbeyq!"
(encode-rot13 "Uryyb, Jbeyq!") ;=> "Hello, World!"Code language: Lisp (lisp)

4.2. remove-if でフィルタする

(defun extract-digits (s)
  (remove-if-not #'digit-char-p s))

(extract-digits "Tel: 090-1234-5678")
;=> "09012345678"Code language: Lisp (lisp)
(defun strip-punctuation (s)
  (remove-if (lambda (c) (not (alphanumericp c))) s))

(strip-punctuation "Hello, World! 123")
;=> "HelloWorld123"Code language: Lisp (lisp)

4.3. every・some で条件を判定する

(defun strong-password-p (s)
  "8文字以上、大文字・小文字・数字をそれぞれ1つ以上含むか判定する。
"
  (and (>= (length s) 8)
       (some #'upper-case-p s)
       (some #'lower-case-p s)
       (some #'digit-char-p s)))

(strong-password-p "Passw0rd")  ;=> T
(strong-password-p "password")  ;=> NIL
(strong-password-p "Pass1")     ;=> NILCode language: Lisp (lisp)
10

4.4. count-if で数える

(defun count-vowels (s)
  (count-if (lambda (c) (find c "aeiouAEIOU")) s))

(count-vowels "hello world")
;=> 3Code language: Lisp (lisp)
11

5. 文字列を比較・検索する

5.1. search で部分文字列を探す

search は第1引数のシーケンスが第2引数の中に最初に現れる位置を返します。

見つからなければ nil を返します12

(defun https-url-p (url)
  (and (search "https://" url) t))

(https-url-p "https://example.com")  ;=> T
(https-url-p "http://example.com")   ;=> NILCode language: Lisp (lisp)

大文字小文字を無視するには :test #'char-equal を渡します。

(search "hello" "Say Hello World" :test #'char-equal)
;=> 4Code language: Lisp (lisp)

5.2. position と position-if で文字を探す

position は文字が最初に現れるインデックスを返します。
メールアドレスをローカル部とドメインに分割する例です。

(defun parse-email (addr)
  (let ((at (position #\@ addr)))
    (when at
      (list (subseq addr 0 at)
            (subseq addr (1+ at))))))

(parse-email "alice@example.com")
;=> ("alice" "example.com")Code language: Lisp (lisp)

position-if は条件関数を受け取ります。
CSS の値などで先頭の数字部分だけを取り出す例です。

(defun parse-leading-int (s)
  "先頭の数字列を整数として返す。
数字がなければ nil。
"
  (let ((end (or (position-if-not #'digit-char-p s) (length s))))
    (when (> end 0)
      (parse-integer (subseq s 0 end)))))

(parse-leading-int "42px")   ;=> 42
(parse-leading-int "100%")   ;=> 100
(parse-leading-int "auto")   ;=> NILCode language: Lisp (lisp)

5.3. subseqとposition

subseq は開始インデックスから終了インデックスの手前までを切り出して新しい文字列を返します。
終了インデックスを省略すると末尾まで取り出します。

positionと組み合わせると、ファイルパスから拡張子を取り出すことができます。

(defun file-extension (path)
  (let ((dot (position #\. path :from-end t)))
    (when dot
      (subseq path (1+ dot)))))

(file-extension "report.pdf")     ;=> "pdf"
(file-extension "archive.tar.gz") ;=> "gz"
(file-extension "Makefile")       ;=> NILCode language: Lisp (lisp)

拡張子を除いたベース名も同様に切り出せます。

(defun file-basename (path)
  (let ((dot (position #\. path :from-end t)))
    (if dot
        (subseq path 0 dot)
        path)))

(file-basename "report.pdf")  ;=> "report"
(file-basename "Makefile")    ;=> "Makefile"Code language: Lisp (lisp)

6. 文字列を加工する

6.1. string-trim で端の文字を取り除く

string-trim は文字列の両端から、指定した文字集合に含まれる文字を取り除いた新しい文字列を返します。
第1引数に取り除く文字のリストか文字列を渡します13

標準入力から読んだ行末の空白や改行を取り除く使い方が典型的です。

(defun read-trimmed-line ()
  (string-trim '(#\space #\tab #\newline #\return) (read-line)))Code language: Lisp (lisp)

片側だけ取り除くには string-left-trimstring-right-trim を使います。
先頭ゼロを除く例です。

(defun strip-leading-zeros (s)
  (let ((result (string-left-trim "0" s)))
    (if (string= result "") "0" result)))

(strip-leading-zeros "007")  ;=> "7"
(strip-leading-zeros "000")  ;=> "0"
(strip-leading-zeros "100")  ;=> "100"Code language: Lisp (lisp)

6.2. 大文字・小文字を変換する

string-upcase は全文字を大文字に、string-downcase は小文字に変換した新しい文字列を返します。元の文字列は変更しません14

HTTP ヘッダー名は大文字小文字を問わないので、小文字に正規化してからハッシュテーブルのキーにする例です。

(defun make-header-table (pairs)
  (let ((table (make-hash-table :test #'equal)))
    (loop for (k . v) in pairs
          do (setf (gethash (string-downcase k) table) v))
    table))

(defparameter *headers*
  (make-header-table '(("Content-Type" . "application/json")
                       ("X-Request-ID" . "abc123"))))

(gethash "content-type" *headers*)
;=> "application/json"Code language: Lisp (lisp)

string-capitalize は単語の先頭を大文字にします。

(string-capitalize "hello world")
;=> "Hello World"Code language: Lisp (lisp)

7. 汎用文字列整形 format

文字列の整形には便利な format があります。

汎用文字列整形 format (format t 標準出力 / nil 文字列を生成 “~a, ~,2f” テンプレート arg …) 結果 “alice 85.0” 主なディレクティブ ~a ~s 引用符付 ~% 改行 ~d 整数 ~x 16進 ~,2f 小数桁指定 ~:[~;~] bool分岐 ~{~} ループ ~R 英語変換 ループ (format nil “~{~a~^, ~}” ‘(“a” “b” “c”)) → “a, b, c” bool分岐 (format nil “~:[off~;on~]” t) → “on” (format nil “~R / ~:R” 3 3) → “three / third”  ~R = 英語基数  ~:R = 序数

formatは、第1引数を t にすると、C言語のprintfのように標準出力に書き出し、nilだとsprintfのように文字列を作ります。

(defun format-result (name score total)
  (format nil "~a: ~a/~a (~,1f%)"
          name score total (* 100.0 (/ score total))))

(format-result "alice" 85 100)
;=> "alice: 85/100 (85.0%)"Code language: Lisp (lisp)

テンプレートに値を埋め込んで文字列を生成するのが典型的な使い方です15

format"~a: ~a/~a (~,1f%)"は、
C言語での "%s: %d/%d (%.1f%%)"と同じようなテンプレート文字列です。

#include <stdio.h>

void format_result(char *buf, const char *name, int score, int total)
{
    sprintf(buf, "%s: %d/%d (%.1f%%)",
             name, score, total, 100.0 * (double)score / (double)total);
}Code language: Arduino (arduino)

7.1. formatの基本的なディレクティブ

小数点の桁数や16進数など、値をどのような文字列に整形するかは、~のあとのディレクティブで指定します。

(format nil "~a"   42)      ;=> "42"        ; 値をそのまま
(format nil "~%")           ;=> "\n"        ; 改行

(format nil "~s"   "hi")    ;=> "\"hi\""    ; print 形式(引用符付き)
(format nil "~d"   255)     ;=> "255"       ; 10進整数
(format nil "~b"   10)      ;=> "1010"      ; 2進数
(format nil "~x"   255)     ;=> "FF"        ; 16進数
(format nil "~f"   3.14)    ;=> "3.14"      ; 浮動小数点
(format nil "~,2f" 3.14159) ;=> "3.14"      ; 小数点以下2桁Code language: Lisp (lisp)

7.2. 値による条件分岐(~:[~;~]

~:[~;~] はブール分岐です。
値が nil なら最初の節、それ以外なら2番目の節を出力します。

(defun format-status (activep)
  (format nil "status: ~:[inactive~;active~]" activep))

(format-status t)    ;=> "status: active"
(format-status nil)  ;=> "status: inactive"Code language: Lisp (lisp)

7.3. 数値による条件分岐(~[~;~]

~[~;~] は数値による条件分岐です。

0 から順に ~; で区切られた選択肢が対応します16

(format nil "~[zero~;one~;two~;other~]" 1)
;=> "one"

(defun format-rank (n)
  (format nil "~[金~;銀~;銅~;入賞~]メダル" n))

(format-rank 0)  ;=> "金メダル"
(format-rank 2)  ;=> "銅メダル"Code language: Lisp (lisp)

7.4. 数値を英語の基数に変換する(~R)

~R は整数を英語の基数に変換します。~:R は序数になります17

(format nil "~R"  42)  ;=> "forty-two"
(format nil "~:R"  3)  ;=> "third"
(format nil "~:R"  1)  ;=> "first"Code language: Lisp (lisp)

8. リストを整形するループディレクティブ(~{~}

format には、ループディレクティブ ~{~} があり、リストを文字列にすることもできます。

~{~} はリストの各要素に対して内側のディレクティブを繰り返し適用します18

(format nil "~{~a~^, ~}" '("alice" "bob" "carol"))
;=> "alice, bob, carol"

(format nil "~{~a~^ / ~}" '("usr" "local" "bin"))
;=> "usr / local / bin"Code language: Lisp (lisp)

~^ は残り要素がないときに繰り返しを打ち切るディレクティブです。
これがないと末尾にも区切り文字が付いてしまいます。

; ~^ なしだと末尾にカンマが残る
(format nil "~{~a, ~}" '("alice" "bob" "carol"))
;=> "alice, bob, carol, "Code language: Lisp (lisp)

区切り文字なしで全要素を並べるだけなら ~^ は不要です。

(format nil "~{~a~}" '(1 2 3 4 5))
;=> "12345"Code language: Lisp (lisp)

8.1. ループディレクティブの回数指定(~n{~}

~n{~} は繰り返し回数に上限を設けます。
リストが長くても最大 n 件だけ出力します。

(format nil "~3{~a ~}" '(1 2 3 4 5))
;=> "1 2 3 "Code language: Lisp (lisp)

8.2. 残りの引数をリストとしてループ(~@{~}

~@{~} はリストではなく残りの引数を走査します。
リストに変換せず可変引数をそのまま渡したいときに使います。

(format nil "~@{~a ~}" 1 2 3)
;=> "1 2 3 "Code language: Lisp (lisp)

9. そのほかの文字列の作り方

9.1. read-line で1行読む

read-line は標準入力から1行読んで文字列として返します。
末尾の改行は含まれません。

(defun read-n-lines (n)
  (loop repeat n collect (read-line)))

; 入力:
; hello
; world
; (read-n-lines 2) ;=> ("hello" "world")Code language: Lisp (lisp)

行数が不定の入力を末尾まで読み切るには、

(defun read-all-lines ()
  (loop for line = (read-line *standard-input* nil nil)
        while line
        collect line))Code language: Lisp (lisp)

EOF に達すると read-line は第2戻り値として t を返します19

9.2. make-string

配列でのmake-arrayに対応した文字列用の生成関数に、make-string があります。

第1引数が長さ、:initial-element で埋める文字を指定し、文字列オブジェクトそのものを作ります20

(defun make-padding (n char)
  (make-string n :initial-element char))

(make-padding 5 #\*)
;=> "*****"

(make-padding 3 #\space)
;=> "   "Code language: Lisp (lisp)

9.3. concatenate で連結する

concatenate を使うと複数の文字列を連結して新しい文字列を返すことができます。

第1引数に返り値の型として 'string を指定します。

文字列のリストを区切り文字で結合する例です。

(defun join-with (separator words)
  (if (null words)
      ""
      (reduce (lambda (a b) 
                 (concatenate 'string a separator b))
              words)))

(join-with ", " '("alice" "bob" "carol"))
;=> "alice, bob, carol"

(join-with "/" '("usr" "local" "bin"))
;=> "usr/local/bin"Code language: Lisp (lisp)

9.4. parse-integer と write-to-string で数値と文字列を変換する

parse-integer は文字列を整数に変換します。
変換できない文字が含まれる場合はエラーになります21

(parse-integer "42")   ;=> 42
(parse-integer "-10")  ;=> -10Code language: Lisp (lisp)

:junk-allowed t を渡すと、数字以外の文字が混じっていてもエラーにならず、読めた範囲までを返します。

(parse-integer "42abc" :junk-allowed t)
;=> 42
;   2    ; 読み終えた位置も返すCode language: Lisp (lisp)

write-to-string は値を文字列に変換します。整数以外も含めて汎用的に変換できます22

(write-to-string 42)     ;=> "42"
(write-to-string -10)    ;=> "-10"
(write-to-string 3.14)   ;=> "3.14"
(write-to-string '(1 2)) ;=> "(1 2)"Code language: Lisp (lisp)

9.5. coerce でリストと相互変換する

coerce でリストと文字列を相互変換できます。

ただし remove-if のようなシーケンス関数は文字列に直接使えるので、変換が必要な場面は限られます23

(remove #\l "hello world")
;=> "heo word"

(coerce "abc" 'list)
;=> (#\a #\b #\c)

(coerce '(#\h #\i) 'string)
;=> "hi"

(coerce "abc" 'vector)
;=> #(#\a #\b #\c)Code language: Lisp (lisp)

10. 実践パターン

10.1. 回文判定

(defun palindrome-p (s)
  (string= s (reverse s)))

(palindrome-p "racecar")  ;=> T
(palindrome-p "hello")    ;=> NILCode language: Lisp (lisp)

大文字小文字を無視して判定するなら string-downcase で正規化します。

(defun palindrome-ignore-case-p (s)
  (let ((normalized (string-downcase s)))
    (string= normalized (reverse normalized))))

(palindrome-ignore-case-p "Racecar")  ;=> TCode language: Lisp (lisp)

10.2. 単語に分割する

Common Lisp には標準の split 関数がありません。loopposition で書けます24

(defun split (s delimiter)
  (loop for start = 0 then (1+ end)
        for end = (position delimiter s :start start)
        collect (subseq s start end)
        while end))

(split "hello world foo" #\space)
;=> ("hello" "world" "foo")

(split "a,b,c,d" #\,)
;=> ("a" "b" "c" "d")Code language: Lisp (lisp)

10.3. 文字の出現頻度を数える

char-code でインデックスに変換して配列に集計します。

(defun count-lowercase-freq (s)
  (let ((freq (make-array 26 :initial-element 0)))
    (loop for c across s
          when (and (char<= #\a c) (char<= c #\z))
            do (incf (aref freq (- (char-code c) (char-code #\a)))))
    freq))

(count-lowercase-freq "abracadabra")
;=> #(5 2 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 2 0 0 0 0 0 0 0 0)
;    a  b  c  d ...                      rCode language: Lisp (lisp)

文字種が事前にわからないときはハッシュテーブルで集計します。

(defun char-frequency (s)
  (let ((freq (make-hash-table)))
    (loop for c across s
          do (incf (gethash c freq 0)))
    freq))

(defun top-chars (s n)
  (let* ((freq (char-frequency s))
         (pairs (loop for k being each hash-key of freq
                      using (hash-value v)
                      collect (cons k v))))
    (subseq (sort pairs #'> :key #'cdr) 0 n)))

(top-chars "abracadabra" 3)
;=> ((#\a . 5) (#\b . 2) (#\r . 2))Code language: Lisp (lisp)

10.4. 数値リストを1行で読む

競技プログラミングで頻出のパターンです。
スペース区切りの数値を1行で読んで整数のリストにします。

(defun read-int-list ()
  (mapcar #'parse-integer
          (split (read-line) #\space)))

; 入力: 1 2 3 4 5
; (read-int-list) ;=> (1 2 3 4 5)Code language: Lisp (lisp)
  1. The Common Lisp HyperSpec – string=
  2. string< が整数を返す仕様はANSIで定義されています。「最初に異なる位置」を返すことで、どこが違うかの情報も同時に得られます。真偽値としてのみ使うなら (not (string< a b)) のように明示するとコードの意図が伝わりやすくなります。 – The Common Lisp HyperSpec – string<
  3. schar という専用アクセサもあります。simple-string(調整不可能な固定長文字列)に特化しており、処理系によっては char より高速になる場合があります。ただし現代の SBCL では char との差はほぼなく、可読性の点から char が一般的です。 – The Common Lisp Cookbook – Strings
  4. Common Lisp の文字列はミュータブルで、(setf (char s i) #\x) で要素を書き換えられます。ただし """hello" のようなリテラル文字列への書き換えは動作未定義です。list で作ったリストと同様、書き換えを前提とする場合は copy-seq でコピーしてから操作するのが安全です。 – The Common Lisp Cookbook – Strings
  5. 文字単体の大文字小文字変換には char-upcasechar-downcase があります。(char-upcase #\a)#\A を返します。string-upcase が文字列全体を変換するのに対し、こちらは1文字単位で使います。map と組み合わせると (map 'string #'char-upcase s) で文字列全体を変換できます。 – The Common Lisp HyperSpec – char-upcase
  6. char-code の返す値はANSI仕様上は処理系依存ですが、SBCLは :SB-UNICODE 機能を持ち、char-code の値がUnicodeコードポイントと一致します。全1,114,112文字を扱えます。(code-char 200)#\LATIN_CAPITAL_LETTER_E_WITH_GRAVE を返すなど、ASCII範囲外の文字も名前付きで参照できます。 – Character and String Types – SBCL Internals
  7. Common Lisp の mod は数学的なモジュロ演算で、除数と同じ符号の結果を返します。(mod -1 26)25 になるため、負のシフト値でも正しくラップアラウンドします。C言語の % 演算子は被除数の符号に従うため -1 % 26-1 になり、同じコードを書けません。 – The Common Lisp HyperSpec – mod
  8. digit-char-p は第2引数に基数を取れます。(digit-char-p #\a 16)10 を返し、#\a が16進数の桁として有効であることを示します。基数は2から36まで指定できます。逆に digit-char を使うと数値から文字に変換できます。(digit-char 10 16)#\A を返します。 – The Common Lisp HyperSpec – digit-char-p
  9. 'list を渡すと文字のリストが返ります。(map 'list #'char-upcase "hello")(#\H #\E #\L #\L #\O) になります。'nil を渡すと副作用だけ実行して戻り値を返しません。mapc のベクタ・文字列版として使えます。 – The Common Lisp HyperSpec – map
  10. everysome は短絡評価を行います。every は条件を満たさない要素が見つかった時点で走査を打ち切り nil を返します。some は条件を満たす要素が見つかった時点で打ち切り、その要素の値を返します。長い文字列でも無駄なく動作します。notany は1つも条件を満たさないとき tnotevery は少なくとも1つが条件を満たさないとき t を返します。 – The Common Lisp HyperSpec – every
  11. count-if:key オプションで、各要素に適用してから条件を判定する関数を指定できます。また :start:end で検索範囲を絞れます。count は値を直接指定して一致する要素を数えます。(count #\l "hello world")3 を返します。 – The Common Lisp HyperSpec – count-if
  12. :from-end t を渡すと末尾側から検索して最後に一致した位置を返します。また :start2 :end2 で検索範囲を絞れます。:test#'char-equal を渡すと大文字小文字を無視した検索になります。 – The Common Lisp HyperSpec – search
  13. 第1引数はリストでも文字列でもよく、文字のシーケンスであれば何でも受け付けます。たとえば (string-trim "abc" "abcxyzabc")"xyz" を返します。ただし指定した文字の「集合」として扱うので順序は無関係です。 – The Common Lisp HyperSpec – string-trim
  14. HyperSpec によると、変換不要な文字が含まれない場合、戻り値が元の文字列と同一オブジェクトになるかどうかは実装依存です。変換結果を安全に独立させたい場合は copy-seq してから使います。破壊的に変換する nstring-upcase / nstring-downcase も標準で用意されています。 – The Common Lisp Cookbook – Strings
  15. 文字列バッファを段階的に構築したい場合は with-output-to-string が使えます。(with-output-to-string (s) (format s "hello") (format s " world")) のように、同一ストリームに複数回書き込んでから文字列として受け取れます。 – The Common Lisp Cookbook – Strings
  16. 範囲外の値に対するデフォルト節を設けるには ~:; を使います。(format nil "~[zero~;one~:;other~]" 5)"other" を返します。また ~:[~;~] と異なり ~[~;~] は引数として整数を消費するため、同じ引数を複数のディレクティブで使いたい場合は ~* で引数を戻す操作が必要になります。 – Practical Common Lisp – A Few FORMAT Recipes
  17. パラメータを付けると任意基数の出力になります。(format nil "~8R" 255) は8進数の "377" を返します。~2R(2進数)、~16R(16進数)のように使え、~b~x の汎用版として機能します。英語変換は引数なしの ~R のみで、パラメータ付きでは数値基数変換になります。 – Practical Common Lisp – A Few FORMAT Recipes
  18. ~{~} は入れ子にもできます。(format nil "~{~{~a/~a~}~^ ~}" '((1 2) (3 4))) のようにリストのリストを処理できます。また ~@{~} と組み合わせて引数の消費を制御することも可能です。 – Practical Common Lisp – A Few FORMAT Recipes
  19. read-line の完全なシグネチャは (read-line &optional stream eof-error-p eof-value recursive-p) です。第2引数 eof-error-pnil にすると EOF でエラーにならず、第3引数 eof-value を返します。nil を渡してループを打ち切るのが慣用句です。なお read-line と異なり read はトークン単位で読むため、空白・改行をデリミタとして扱います。 – The Common Lisp HyperSpec – read-line
  20. :initial-element を省略したときの初期値は実装依存です。SBCL では #\Nul になりますが、移植性を保つためには常に明示するのが安全です。 – The Common Lisp HyperSpec – make-string
  21. :radix で基数を指定できます。(parse-integer "FF" :radix 16)255 を返します。基数は2から36まで指定可能です。浮動小数点数を含む文字列をパースしたい場合は標準には関数がなく、parse-number ライブラリが使えます。 – The Common Lisp HyperSpec – parse-integer
  22. :base キーワードで出力基数を変更できます。(write-to-string 255 :base 16)"FF" を返します。より簡潔な変換関数として princ-to-string もあります。write-to-string がエスケープ付き("hello""\"hello\"" )なのに対し、princ-to-string はエスケープなし("hello""hello")で人間向けの出力に向いています。 – The Common Lisp Cookbook – Strings
  23. シンボルや1文字から文字列を作るには string 関数が便利です。(string 'hello)"HELLO"(string #\a)"a" を返します。coerce でも同じことができますが string の方が簡潔です。 – The Common Lisp Cookbook – Strings
  24. split-sequence ライブラリを使うと (split-sequence:split-sequence #\space "hello world foo") のように書けます。Quicklisp 経由でインストールできます。Alexandria はポータビリティを重視する設計方針から split-sequence を含まず、別ライブラリとして独立しています。Serapeum はこの split-sequence を再エクスポートしています。 – GitHub – sharplispers/split-sequence