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コアAPI数の遷移をChart.jsでグラフ化

Javaはデフォルトで多数のAPIクラスを持っていてコーディングを楽にしてくれます。その数は増え続けているんでしょうか。 バージョンが上がるごとにどう変化しているか調べてchart.jsでグラフ化しま …

優秀なJava開発者になる為の10のステップ

プログラミングで生計を立てるなら企業ニーズの多いJavaはリターンの多い言語ですから使えるようになっておいて損はありません。 Javaはこの先まだまだ隆盛を誇るでしょうから、優秀なJava開発者になれ …

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

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

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

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

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

前回の続きです。One IT ThingIVS対応フォント「IPAmj明朝」で使えるフォントをWeb上に一覧表示してみる(1)https://one-it-thing.com/2098IPAmj明朝フ …

 

shingo.nakanishi
 

東京在勤、1977年生まれ、IT職歴2n年、生涯現役技術者を目指しています。健康第一。