One IT Thing

IT業界で飯を食う為の学習系雑記

java

IVS、サロゲートペアが混じった文字列をJavaのWebアプリでワードカウントする

投稿日:2019年7月7日 更新日:

(結論:java.text.BreakIteratorは「サロゲートペア文字 + IVS」の8バイト文字も1文字としてカウントしてくれる)

先日、IPAmj明朝フォントを使ってIVSを含んだUnicode文字を一覧表示してみました。

一覧表示したフォントが持つ文字コードサイズには以下の種類があるはずです。

  1. Unicode文字(2Byte)= 2Byte文字
  2. サロゲート文字(4Byte)= 4Byte文字
  3. Unicode文字(2Byte) + IVS(4Byte)= 6Byte文字
  4. サロゲート文字(4Byte) + IVS(4Byte) = 8Byte文字

これらバラバラのサイズの文字コードを持つ文字の、見た目上の文字数を正確に数えられるかJavaで試しました。

調査対象の文字列

6万文字をカウントするのは効率が悪いので、MJ文字情報一覧表の.xls から前述の4パターンを全て含む10文字が並ぶ区画、MJ060378~MJ067953を選んで文字数をカウントしてみます。

Webで表示する用にエスケープしてみるとこんな感じの10文字コードです。0x10000以上はサロゲートペアにエンコードして4バイトになるのでカッコ内のバイト数になります。

&#x9F8F&#xE0104  (6Byte)
&#x2EBDB          (4Byte)
&#x2EBE0          (4Byte)
&#x2D380&#xE0101 (8Byte)
&#x6E5B&#xE0104  (6Byte)
&#x6F9A&#xE0102  (6Byte)
&#x8352&#xE0108  (6Byte)
&#x2B751&#xE0102 (8Byte)
&#x2D4FD          (4Byte)
&#x3407           (2Byte)

10文字で計54バイトですね。String.length()は2バイトバウンダリで数えるので27文字と回答することが今から予想されます。

JavaのWebアプリで数えてみる

プロジェクトは前回まで使っていたSpringBootです。

こんな流れで。

wordCount.jsp

/word_countにアクセスすると表示され、前述の10文字をコントローラから貰い、input type=”text”に表示します。

文字化けせずに表示出来るようにfont-familyにはIPAmj明朝を指定しておきます。(CSS分けた方がいいですが、分かり易いので取りあえずstyleに直書きしてます)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
<head>
<%@ include file="/WEB-INF/jsp/head.jsp"%>
</head>
<body>
    <header class="sticky-top">
        <h3>文字数チェック</h3>
    </header>

    <main class="container">
        <form:form action="/word_count_result" method="POST" modelAttribute="wordCountForm">
            <div class="form-group">
                <form:input path="inputValue" class="form-control"
                    style="font-family: IPAmj明朝;  height: 96px;" value="${codePoints}" htmlEscape="false" />

                <input type="submit" value="文字列をワードカウント" class="btn btn-primary" />
            </div>
        </form:form>
    </main>
</body>
</html>

WordCountForm.java

/word_count_resultに渡すフォーム。inputValueには10文字分の文字コードが入ります。(バイト数は54Byteだけど)

package com.example.demo;

public class WordCountForm {
    private String inputValue;

    public String getInputValue() {
        return inputValue;
    }

    public void setInputValue(String inputValue) {
        this.inputValue = inputValue;
    }
}

wordCountResult.jsp

数えた文字数をコントローラから貰って表示するだけです。

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<!DOCTYPE html>
<html>
<head>
<%@ include file="/WEB-INF/jsp/head.jsp"%>
</head>
<body>
    <header class="sticky-top">
        <h3>文字数チェック結果</h3>
    </header>

    <main class="container">
        <p style="font-size: 32px;">
            String.length()で図った文字数 : ${length1}
        </p>
        <p style="font-size: 32px;">
            BreakIteratorで計った文字数 : ${length2}
        </p>
    </main>
</body>
</html>

WordCountService.java

ワードカウントを行う処理をServiceに切り分けておきます。BreakIteratorで文字列をパースするとcurrent()で現在パース中のバイト配列インデックスが返却されます。

バイト配列中のどこからどこまでが一文字か分かったら、String文字列からその部分をコードポイント配列として抜き出しています。

package com.example.demo;

import java.text.BreakIterator;
import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;

@Service
public class WordCountService {

    // 引数inputを一文字ずつコードポイント配列にして返却
    public char[][] toUnicodeArray(String input) {
        List<char[]> utf16List = new ArrayList<char[]>();

        BreakIterator bi = BreakIterator.getCharacterInstance();
        bi.setText(input);

        int start = 0;
        int cnt = 0;
        while (bi.next() != BreakIterator.DONE) {
            
            // サロゲートペアを考慮した現在パース中の
            // バイト配列インデックスを返してくれる
            int current = bi.current();
            
            // BreakIteratorが1文字として検知した分のサイズを
            // String文字列からコードポイント配列化
            char[] codePointArray = toCodePointArray(input, start, current);
            utf16List.add(codePointArray);

            start = current;
        }

        char[][] ret = utf16List.toArray(new char[utf16List.size()][]);

        return ret;
    }

    // BreakIteratorが1文字と判定した範囲をコードポイント配列化
    public char[] toCodePointArray(String input, int start, int end) {
        int len = end - start;
        char[] ret = new char[len];

        for (int i = 0; start < end; start++, i++) {
            ret[i] = input.charAt(start);
        }
        return ret;
    }
}

WordCountController.java

/word_countにアクセスされるとwordCountResult.jspを返却、/word_count_resultにアクセスされると文字数を数えて結果をwordCountResult.jspで表示します。

package com.example.demo;

import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class WordCountController {

    private WordCountService wordCountService;

    @ModelAttribute
    public WordCountForm setUpForm() {
        return new WordCountForm();
    }

    public WordCountController(WordCountService wordCountService) {
        this.wordCountService = wordCountService;
    }

    @RequestMapping(path = "/word_count", method = { RequestMethod.GET })
    public String wordCount(Model model) {

        List<String> codeList = new ArrayList<String>();
        codeList.add("&#x9F8F&#xE0104");
        codeList.add("&#x2EBDB");
        codeList.add("&#x2EBE0");
        codeList.add("&#x2D380&#xE0101");
        codeList.add("&#x6E5B&#xE0104");
        codeList.add("&#x6F9A&#xE0102");
        codeList.add("&#x8352&#xE0108");
        codeList.add("&#x2B751&#xE0102");
        codeList.add("&#x2D4FD");
        codeList.add("&#x3407");

        String codePoints = String.join("", codeList);
        model.addAttribute("codePoints", codePoints);

        return "wordCount";
    }

    @RequestMapping(path = "/word_count_result", method = { RequestMethod.POST })
    public String wordCountResult(WordCountForm form, Model model) {
        char[][] codePoints = 
            this.wordCountService.toUnicodeArray(form.getInputValue());

        model.addAttribute("length1", form.getInputValue().length());
        model.addAttribute("length2", codePoints.length);

        return "wordCountResult";
    }
}

実装完了です。

動作確認

SpringBootを実行してブラウザでhttp://localhost:8080/word_countにアクセス。「font-family: IPAmj明朝;」が効いて文字化けせずに期待通りの文字が表示されています。

文字数チェックの為にボタンを押してみると・・・期待通りの回答が返ってきました。

String.length()はやはり54Byte / 2Byte = 27文字として返却し、IVSを含めたサロゲートをBreakIteratorで考慮した場合は正解の10文字でした。

Unicodeの文字数カウントはBreakIteratorに任せて安心

2、4、6、8Byteの文字が混在していてもjava.text.BreakIteraterさんはコードポイントを判断して正しい文字数を返してくれる素敵なAPIでした。

-java
-, , ,

執筆者:

関連記事

JavaでRSA暗号を使う際にCRYPTREC暗号リストに足元をすくわれる可能性を回避する

標準的な暗号しか使わないケースでもJavaでRSAを使う時はBouncyCastleを入れておいた方が無難、という話です。 “ECB”という文字列がプログラム中にあると監査に引 …

Javascript(暗号化JSライブラリ「Forge」)とp12ファイルで署名値を作成、Javaで検証する

前回、送信データの改ざんを検知する為、簡易的なセキュリティトークンであるPKCS#12形式のファイルを作成しました。  One IT Thing開発用のPKCS#12ファイルをOpenSSL …

JUnitコードの自動生成も出来る古の神Eclipseプラグイン「CodeProAnalytix」は今でも使える

デスマーチPM「各社のPG進捗が遅れていてユニットテストを書く暇が全く無いんですよ、なにかいい手は有りませんかね・・・? 因みに本プロジェクトはカバレッジ100%で請け負っています!(キリッ)」 何故 …

Doxygenでspring-frameworkをドキュメント化

以前にspring-frameworkのソースリーディングが出来る環境をOpenGrokで作りました。 One IT ThingOpenGrokをインストールしてソースリーディング環境を作る …

IVS対応フォント「IPAmj明朝」で使えるフォントをWeb上に一覧表示してみる(1)

目次1 IPAmj明朝フォントとは2 日本における漢字のコンピュータ表示課題3 サロゲートペアが対応策だが4 IVS(ideographic variation sequence) とは5 そこでIP …

 

shingo.7k24
 

東京在勤、職歴20年越え中年ITエンジニアです。まだ開発現場で頑張っています。

19歳(1996年)から書き始めた個人日記が5,000日を超え、残りの人生は発信をして行きたいと思い、令和元日からこのサイトを開始しました。勉強と試行錯誤をしながら、自分が経験したIT関連情報を投稿しています。

私と同じく、今後IT業界で生計を立てて行きたいと考えている方や、技術共有したいけどフリーランスで孤独、といった方と一緒に成長、知識共有して行けたら楽しいな、と思っています。