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とPerlでUDPソケット通信

目次1 はじめに2 対象読者3 実装3.1 UDPサーバ(Perl)3.2 UDPクライアント(Java)3.3 実行4 まとめ はじめに 本棚を整理していたらラクダ本が出てきました。うわー懐かしい、 …

Spring&JSPの検証環境を速攻で作る

2019年現在、オワコン風潮の強いJSPですが使っているプロジェクトもまだまだあり、枯れた技術を好む官公庁系のプロジェクトでは根強いシェアを誇っています。実装検証をする為に環境を作る機会があったりする …

CentOS7にOpenJDK11をインストール、alternatives後の再ログインでJAVA_HOMEも自動変更

OpenJDKはCentOS-Baseリポジトリのupdatesに登録されているので新規yumリポジトリ追加は不要です。 [root@spock tmp]# yumdb search from_rep …

SpringBootアプリにBootstrap4を追加(WebJars使用)

SpringBootにCSSやJSを追加する場合は概ね以下のパターンがあるんじゃないかと思います。(bowerはもう使わない方向で) CDNで外部から読み込む<script src=&#8221 …

Maven環境別ビルド時、プロファイルの違いでdependencyを変える

MavenのResourceFilteringを使い、production、staging、developmentとかで環境別ビルドしている時、ある環境ビルドの時だけ特定のライブラリを追加する、をmv …


shingo nakanishi。東京で消耗中の職歴20年越え中年ITエンジニアです。「生涯現役プログラマを楽しむ」ことができる働き方探しをライフワークにしています。

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