[VBA]環境依存文字⇒常用漢字の変換(サロゲートペア対応)

やりたいこと

名前の環境依存文字を常用漢字に置換する処理の実装。
Shift-JISに存在しない(Unicodeだけに存在する)環境依存文字も対象に取る。

事前知識

Shift-JISの構造

Wikiからの参照
「全角文字は2バイト固定」で「バイトの先頭は81~9FまたはE0~EF」である。

JIS X 0201を1バイトで、JIS X 0208を2バイトで符号化する可変幅文字符号化方式。2バイト文字は、第1バイトに8116-9F16またはE016-EF16の47通り、第2バイトに4016-7E16または8016-FC16の188通りを用いる。

UTF-16の構造

こちらもWikiからの参照

  • UTF-16は2バイト、または4バイトの値を取りうる。
  • 2バイトの場合、バイトの先頭は「00~D7」または「E0~FF」
    コードポイントは4桁の16進数で表される。(YYYY)
  • 4バイトの場合、1,2バイト目の先頭は「D8~DB」、3,4バイト目の「DC~DF」
    コードポイントは5桁の16進数(XYYYY)
    2文字を組み合わせて表現する手法をサロゲートペアと呼ぶ

BMPに含まれるU+0000U+D7FFU+E000U+FFFFは、そのまま符号単位1つで表す。BMP以外のU+10000U+10FFFFは、表のようにビットを配分して、符号単位2つで表す。

このとき使われる、U+D800U+DFFF の符号位置を、代用符号位置(Surrogate Code Point)と呼び、BMP外の1つの符号位置を表す連続した2つの代用符号位置のペアをサロゲートペアと呼ぶ。代用符号位置に使うため、BMPのこの領域には文字が収録されておらず、UTF-16以外のUTF-8UTF-32では使用されない。

次に、4バイト文字(サロゲートペア)からなる漢字の一覧は以下の通り。
珍しい苗字もわずかながらに含まれる。

ソースコード

' 対応表のシート名および開始行
Private Const SHEET_NAME = "Master"
Private Const BEGIN_ROW As Long = 3

' 常用漢字と異体字の対応表
Private dic As Dictionary

' 対応表を初期化する
' 前提条件:A列に異体字、B列に常用漢字を入力したシートの用意
Private Sub InitMapping()
    Dim sh As Worksheet
    Set sh = ThisWorkbook.Worksheets(SHEET_NAME)
    Dim last_row As Long
    last_row = sh.Range("A" & BEGIN_ROW).End(xlDown).Row
    
    Set dic = New Dictionary
    Dim i As Long
    For i = BEGIN_ROW To last_row
        Dim c As String
        Dim code As Long
        c = sh.Cells(i, 1)
        code = WorksheetFunction.Unicode(c)
        Call dic.Add(code, sh.Cells(i, 2))
    Next
End Sub
 
' 異体字を対応する常用漢字へ置換する
Public Function ConvertCommonKanji(name As String) As String

    Const TEMP_FILL_CHAR = &H0

    If (dic Is Nothing) Then
        Call InitMapping
    End If

    Dim i As Long
    For i = 1 To Len(name)
        Dim c As String
        Dim code As Long
        c = Mid(name, i, 1)
        code = AscW(c)
                
        ' 4バイト文字である場合 (上位サロゲートで判定)
        If (&HD800 <= code And code <= &HDBFF) Then
            ' 4バイト文字の場合、AscW()では対応できないため別関数を使用する
            code = WorksheetFunction.Unicode(Mid(name, i, 2))
            ' 2文字置換する
            If (dic.Exists(code)) Then
                ' 文字数の一致する代入しかできないため、あとでReplaceで取り除く
                Mid(name, i, 2) = dic.Item(code) & chr(TEMP_FILL_CHAR)
            End If
            i = i + 1
        ' 2バイト文字である場合
        Else
            ' 1文字置換する
            If (dic.Exists(code)) Then
                Mid(name, i, 1) = dic.Item(code)
            End If
        End If
    Next
    name = Replace(name, chr(TEMP_FILL_CHAR), "")
    ConvertCommonKanji = name
End Function

コード解説

AscWの代替

事前準備のところで、バイト数について言及したのには理由がある。

Shift-JISは2バイト固定だが、UTF-16の場合は4バイトを取る場合がある。
AscWを使うとUnicodeのコードポイントを取得することができるが、引数には -32768 から 32767までの65535通り(2バイトの範囲)でしか文字を取得できない。
そのため、例えば2バイトを超えた範囲-32900や40000のような値を渡すと実行時エラーが発生する。

一方で、シート関数で用意されているUnicode()であれば4バイト文字にも対応している。
そのため、4バイト文字のコードポイントを求める場面ではWorksheetFunction.Unicode()を使用している。

4バイト文字の判定

エクセルではセルの文字列を取得する際、4バイト文字は自動的に2文字に分割して参照される。そのため、2バイトであれば1文字置換を、4バイトであれば2文字置換をと条件分岐が必要になる。

UTF-16の仕様として、先頭バイト「D8~DB」で判定を取ることができる。

実行速度(おまけ)

  • 対応表の件数:300件
    人名漢字の置換を想定しており、環境依存文字を全て網羅するわけでない。
  • 入力件数:3,000,000行
  • 入力文字長:10桁 / 行(置換される文字を必ず1桁持つ)

実行速度は3,000,000行で約10秒。
数百~数千程度であれば全然動く。