日本語対応の文字列関数、そしてUNICODE

文字列とリスト

AutoCAD 2021 以降の日本語処理

AutoCAD 2021 から日本語を含むマルチバイト文字は UNICODE で表示したり処理されるようになりました。したがって、以降で説明している SHIFT_JIS を扱うテクニックは AutoCAD 2020 まで応用が効くもので、UNICODE の場合は特に考慮することなく日本語を正しく扱えるようになりました。

しかし、ここで使う文字列をリストとして処理するテクニックは UNICODE になっても応用が効くものなので、あらましを理解されることをお勧めします。そして、最後にこのことを踏まえて AutoCAD 2021 以降でこの辺りがどう変わったかを補足します。

AutoLISP 標準の文字列用の関数は SHIFT_JIS といった 2 バイトで一文字を表す日本語に対してはエラーにこそなりませんが対応していません。AutoCAD 用のコマンドをちょっと作る分には、文字列の操作をしようという需要は少なそうなので問題が表面化することも少なく、大した事ではないと考えるかもしれません。しかし、ユーザーの操作以外に設定ファイルやデータファイルといった外部のテキストファイルを扱う必要が出てくると、そこに日本語を含めると誤作動する可能性があります。エラーの原因は 2 バイト文字の二文字目に 1 バイト文字の「¥」といったエスケープコードを含むものがあるので、運悪くそういった漢字やカナを使うと頭を悩ませて時間を浪費することになります。また、グローバルな現代のプログラムと比べるとあまりにも原始的すぎますので、プログラムを組む意欲も減退します。ここではテキスト処理にしっかりと対応するために SHIFT_JIS に対応した文字列関数を作成してみます。

ここで文字列を扱うにあたってリストに変換して操作します。AutoLISP 標準の関数で vl-string->list 関数や vl-list->string 関数がありました。文字列をリストに変換することで AutoLISP から文字列を扱いやすくすることが可能になります。

vl-string->list 関数 は文字列を文字コードを表す整数のリストに変換する関数でした。

"巨大生物の名はGodzilla"→(139 144 145 229 144 182 149 168 130 204 150 188 130 205 71 111 100 122 105 108 108 97)

これを SHIFT_JIS の 2 バイト文字ではペアのリストにまとめて、1 バイトの ASCII 文字と区別できるように構造化します。その変換を行う関数は以下のようになります。SHIFT_JIS の 2 バイト文字は、最初の 1 バイト目を調べることによって、アルファベットなどの ASCII 文字と区別できます。

(defun string->list:SHIFT_JIS (buffer)
  (if buffer
    (progn (if (or (and (<= 128 (car buffer)) (<= (car buffer) 159))
                   (and (<= 224 (car buffer)) (<= (car buffer) 255))
               )
             (setq result (cons (list (car buffer) (cadr buffer)) result)
                   buffer (cdr buffer)
             )
             (setq result (cons (car buffer) result))
           )
           (string->list:SHIFT_JIS (cdr buffer))
    )
  )
)

(defun string->list (string / buffer result)
  (setq buffer (vl-string->list string))
  (reverse (progn (string->list:SHIFT_JIS buffer) result))
)

使用例は以下の通りです。

_$ (string->list "巨大生物の名はGodzilla")⏎
((139 144) (145 229) (144 182) (149 168) (130 204) (150 188) (130 205) 71 111 100 122 105 108 108 97)

リストの要素がアトムの整数ならば ASCII 文字、リストならマルチバイト文字と区別できるようになりましたので、後の操作は格段に楽になります。さらに、リストを元の文字列に戻す関数も用意しておきましょう。

(defun list->string:sub (alist)
  (if alist
    (progn (setq buffer (if (vl-consp (car alist))
                          (append (reverse (car alist)) buffer)
                          (cons (car alist) buffer)
                        )
           )
           (list->string:sub (cdr alist))
    )
  )
)

(defun list->string (alist / buffer)
  (vl-list->string (progn (list->string:sub alist) (reverse buffer)))
)

また、リストから部分的に要素を取り出す関数を用意します。Common LISP の同名の関数の仕様に倣って subseq 関数は一番目の引数のリストの、二番目の引数のインデックスの要素から三番目の引数のインデックスの要素の直前までを抽出するサポート関数です。インデックスは 0 から始まります。

(defun default (value onNull)
  (if value
    value
    (eval onNull)
  )
)

(defun subseq:sub (alist counter)
  (if (< counter end)
    (if (<= start counter)
      (cons (car alist) (subseq:sub (cdr alist) (1+ counter)))
      (subseq:sub (cdr alist) (1+ counter))
    )
  )
)

(defun subseq (alist start end)
  (setq start (default start 0)
        end   (min (default end (length alist)) (length alist))
  )
  (subseq:sub alist 0)
)

subseq 関数の使用例は以下の通りです。三番目の引数が示すインデックスの直前までが抽出される点に注意してください。

_$ (subseq '(0 1 2 3 4 5 6) 1 4)⏎
(1 2 3)

文字列の長さ

SHIFT_JIS の 2 バイト文字を含む文字列の長さを調べるのは、先ほどの構造化されたリストの長さを求めるのと同じ問題となり、次のような関数になります。

(defun strlen2 (string) (length (string->list string)))
_$ (strlen2 "巨大生物の名はGodzilla")⏎
15

部分文字列の抽出

部分文字列を抽出する substr 関数の SHIFT_JIS 対応版は次のようになります。文字のインデックスは substr 関数に倣って一文字目を 1 とします。

(defun substr2 (string start strlength)
  (list->string
    (subseq (string->list string)
            (1- start)
            (if strlength
              (1- (+ start strlength))
            )
    )
  )
)
_$ (substr2 "巨大生物の名はGodzilla" 3 8)⏎
"生物の名はGod"

文字列の検索

vl-string-search 関数の SHIFT_JIS 対応版は次のようになります。vl-string-search 関数の仕様に倣って、戻り値のインデックスは 0 から始まります。

(defun string-search:headmatch-p (pattern-list string-list)
  (and (<= (length pattern-list) (length string-list))
       (vl-every (function (lambda (char1 char2) (equal char1 char2)))
                 pattern-list
                 string-list
       )
  )
)

(defun string-search:headpos ()
  (if (< pos sLength)
    (if (equal (car plist) (nth pos slist))
      pos
      (progn (setq pos (1+ pos)) (string-search:headpos))
    )
  )
)

(defun string-search (pattern string start-pos / plist slist sLength pos result flag)
  (setq start-pos (if start-pos start-pos 0)
        plist     (string->list pattern)
        slist     (subseq (string->list string) start-pos nil)
        sLength   (length slist)
        pos       0
  )
  (while (null flag)
    (if (setq pos (string-search:headpos))
      (if (string-search:headmatch-p plist (subseq slist pos nil))
        (setq result (+ start-pos pos)
              flag   T
        )
        (setq pos (1+ pos))
      )
      (setq flag T)
    )
  )
  result
)
_$ (string-search "生物" "巨大生物の名はGodzilla" nil)⏎
2

文字列の置換

vl-string-subst 関数の SHIFT_JIS 対応版は次のようになります。

(defun string-subst (new-string pattern string start-pos / pos slist)
  (if (setq pos (string-search pattern string start-pos))
    (progn (setq slist (string->list string))
           (list->string
             (append (subseq slist nil pos)
                     (string->list new-string)
                     (subseq slist (+ pos (strlen2 pattern)) nil)
             )
           )
    )
    string
  )
)
_$ (string-subst "怪獣" "生物" "巨大生物の名はGodzilla"nil)⏎
"巨大怪獣の名はGodzilla"

以上のように、文字列をリストに変換することで操作が容易になることが見て取れたかと思います。AutoLISP や元となった XLISP の技術的な記述が見当たらないのではっきりとは言えませんが、Common LISP においては文字列はベクタという配列の一種です。そして配列とリストはシークエンスという仲間になります。つまり、文字列とリストは近しい親戚であり、今回の場合に限らず文字列をリストで処理すると操作が容易になることがあることをご理解ください。

さて日本語の処理にめどが付きましたが、コードの中に日本語の文字列を含めると、ソース時点でのテストは通りますが、コンパイラが通らない事があるということが判っています。これを完璧に避けるには、コンパイルしないでソースコードのままで使用するか、日本語を含む文字列の情報を外部のテキストファイルに移してコンパイラを通すコードには日本語を含めないという手段をとることになます。プログラムの多言語化までを考えると、本来は言語依存の部分を外部にもった構造のプログラムとするべきですが、その仕組みの構築には一手間二手間かかります。コンパイルをする場合は、残念ながらコード中の文字列には日本語は使わずに英語などで代用するのが手っ取り早い安全な方法です。

AutoCAD 2021 以降の UNICODE

AutoCAD 2021 以降は UNICODE をベースとした文字列の扱いになりました。AutoCAD で文字列を表示するのも UNICODE、AutoLISP のプログラムを書くのも文字列を処理するのも UNICODE で、逆に従前の SHIFT_JIS は扱えません。

UNICODE でプログラミングする場合は、新しいシステム変数 LISPSYS を 1 または 2 とします。このシステム変数を 0 以外にすると AutoLISP の開発環境が Visual LISP から Visual Studio Code に変更になります。再起動が必要です。

UNICODE とは世界中の文字を一つの統一した規格の中で扱えるように制定されたもので、SHIFT_JIS などが 2 バイトだったのに対して仕様的には最大 4 バイトで表現される文字コードを使って文字を表します。なお、現在実際使われているのは 3 バイトまでです。4 バイトは 32 ビットで AutoLISP の整数は 32 ビットでしたので、整数がひとつあればどのようなものでも UNICODE の文字を一つ表せます。

これをふまえて、文字列を文字コードのリストに変換する vl-string->list 関数の動作を見てみます。

(vl-string->list "aあ")  ⇒  (97 12354)

「a」などというアルファベットは、ASCII コードと変わりません。日本語では「あ」が、SHIFT_JIS の 2 バイトに対応する 2 つの整数に分かれることなく、ひとつの整数で表されています。ascii 関数を含め他の文字コードを扱う関数も同じように処理されます。これにより特に考慮しなくても正しく日本語を扱えるようになりました。

UNICODE で文字化けする場合は

UNICODE は UTF-8 や UTF-16 とバイト単位で扱う際のエンコードの種類がありますが、AutoCAD ではどれを選んでも受け付けるようです。しかし、時には文字化けが発生することもあります。その際はエンコード情報が頭に付加された「UTF-8 with BOM」を試してみてください。特に DCL ファイルは BOM 付きをお勧めします。

しかし、注意しておかなければならないことも新たに生まれました。システム変数 LISPSYS が 0 のときに旧来の開発環境 Visual LISP が使われるとともに、文字コード関係の関数は AutoCAD 2020 以前と同じ動作になります。具体的には SHIFT_JIS の "aあ" をファイルから読み込んでリストに変換すると (97 130 160) となります。そして、UNICODE の "aあ"は (97 227 129 130) になります。SHIFT_JIS はもはや AutoCAD で表示も入力も出来なくなったので UNICODE に切り替えていけば良いのですが、同じ UNICODE だったとしても別の文字コードに変換される可能性があるのです。そして稀なケースになるでしょうが AutoCAD のエンドユーザーがシステム変数 LISPSYS を 0 にしていることもありえるのです。文字コードをあつかうルーチンを使っている場合は、LISPSYS が 0 以外になっているか確認し、そうでなければユーザーにシステム変数を変更して AutoCAD アプリケーションを再起動してもらうようにしておかないと、確実な動作が見込めません。正直、これは困った仕様です。残念です。

余談になりますが、UNICODE には日本語に加えて絵文字や世界中の見慣れない文字が含まれています。したがって、次のように文字の表現の幅も広がります。

AutoCAD UNICODE AUtoLISP