Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

java.text.BreakIteratorによる文字数(grapheme)カウント

JIS X 0213など、シフトJISマイクロソフト コードページ932以外の文字をプログラム上で紙に印刷する場合には、入力された文字列を枠内に確実に収めるため、文字数を正しくカウントする必要があります。

JIS X 0213では複数のコードポイントで1文字を表す文字(合字)があります。このような場合、見た目の「1文字」を「grapheme」(書記素)と呼びます。印刷においては、書記素ひとつを「1文字」としてカウントする必要があるわけです。

さて、以下のサイト「文字数をカウントする7つの方法」では、java.text.BreakIteratorを使って書記素をカウントする方法を示しています。どうやらLINEで使われているカウントの方法らしい。 engineering.linecorp.com

ところが調べてみると、そのまま使うにはコーナーケースをカバーしきれていないようです……

https://repl.it/@satob/BreakIteratorTest

import java.text.BreakIterator;

class Main {
  public static void main(String[] args) {
    // (a) 一般的な非漢字「あ」
    //     (1書記素=1ユニット=1コードポイント)
    System.out.println("\u3042");
    // 1書記素とカウントすべきところ、正しく1書記素とカウントされる
    System.out.println(getGraphemeLength("\u3042")); // -> 1

    // (b) サロゲートペア「𠮷」
    //     (1書記素=2ユニット=1コードポイント)
    System.out.println("\ud842\udfb7");
    // 1書記素とカウントすべきところ、正しく1書記素とカウントされる
    System.out.println(getGraphemeLength("\ud842\udfb7")); // -> 1

    // (c) 異体字セレクタつき文字「侮」
    //     (1書記素=2ユニット=2コードポイント)
    //     ただし2コードポイント目は異体字セレクタ(文字ではない)。
    System.out.println("\u4fae\ufe00");
    // 1書記素とカウントすべきところ、正しく1書記素とカウントされる
    System.out.println(getGraphemeLength("\u4fae\ufe00")); // -> 1

    // (d) JIS X 0213合字「˥˩」
    //     (1書記素=2ユニット=2コードポイント)
    //     1文字目も2文字目も単独で書記素として意味をなす。
    System.out.println("\u02e5\u02e9");
    // 1書記素とカウントすべきところ、誤って2書記素とカウントされる
    System.out.println(getGraphemeLength("\u02e5\u02e9")); // -> 2

    // (e) JIS X 0213合字「カ゚」
    //     (1書記素=2ユニット=2コードポイント)
    //     2コードポイント目は合字用文字で、
    //     JIS X 0213非漢字に定義されている正しい合字の組み合わせ。
    System.out.println("\u30ab\u309a");
    // 1書記素とカウントすべきところ、正しく1書記素とカウントされる
    System.out.println(getGraphemeLength("\u30ab\u309a")); // -> 1

    // (f) (e)の1コードポイント目と2コードポイント目を逆にしたもの
    //     JIS X 0213非漢字に定義されていない組み合わせ。
    //     2ユニット、2コードポイントだがJIS X 0213の文字としては不当。
    //     1コードポイント目は合字用文字だが、組み合わせる文字がないので
    //     表示時には合わせて2書記素ぶんの幅をとる。
    System.out.println("\u309a\u30ab");
    // 正しいは判断つかないが、ともかく2書記素とカウントされる
    System.out.println(getGraphemeLength("\u309a\u30ab")); // -> 2

    // (g) JIS X 0213にない合字「『ま』にマル」
    //     2ユニット、2コードポイントだがJIS X 0213の文字としては不当。
    //     フォントの実装上1書記素の幅で表示されるが、JIS X 0213の規格としては
    //     2書記素ぶんになるのでは?
    System.out.println("\u307e\u309a");
    // 正しいは判断つかないが、ともかく1書記素とカウントされる
    System.out.println(getGraphemeLength("\u307e\u309a")); // -> 1
    
    // (h) (e)の2コードポイント目だけを単独でカウント
    //     1ユニット、1コードポイントだがJIS X 0213の文字としては不当。
    //     (f)の11コードポイント目と同様、表示時には1書記素ぶんの幅をとる。
    System.out.println("\u309a");
    // 正しいは判断つかないが、ともかく1書記素とカウントされる
    System.out.println(getGraphemeLength("\u309a")); // -> 1

    // (i) (おまけ)ZWJを使った絵文字「👨<200d>👩<200d>👦」
    //     8ユニット、5コードポイント、1書記素。
    System.out.println("\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66");
    System.out.println(getGraphemeLength("\uD83D\uDC68\u200D\uD83D\uDC69\u200D\uD83D\uDC66")); // -> 5
  }

  public static int getGraphemeLength(String value) {
    BreakIterator it = BreakIterator.getCharacterInstance();
    it.setText(value);
    int count = 0;
    while (it.next() != BreakIterator.DONE) {
        count++;
    }
    return count;
  }
}

まとめると、JIS X 0213の範囲では以下のような挙動をするようです。2.がキツいな……

  1. サロゲートペア、IVSは正しく処理されている
  2. 1コードポイント目・2コードポイント目とも書記素として正当な組み合わせの合字が2書記素とカウントされてしまう
  3. JIS X 0213として不正な合字でも無理やりカウントしてしまう
  4. (おまけ)ZWJは1書記素としてカウントされてしまう

対策としては以下のような感じかと思います。

  • 文字列長チェックの前に、JIS X 0213として不正なコードポイントの並びがないか確認する
  • 1コードポイント目・2コードポイント目とも書記素として正当な組み合わせの合字が含まれている場合、文字列長から引き算する

学校で習う漢字の書き順はどう決まったのか

私の母は「自分(母)が学校で習った内容とは、漢字の書き順からして違ってたから、変なこと教えちゃいけないと思って」私に勉強を教えることはなかった。じゃぁ学校で習う漢字の書き順はどう決まったのか?という話。

学校で習う漢字の書き順は、文部省「筆順指導の手引き」(昭和33年3月, 博文堂出版)で定められたんだけど、その内容を読んでみて気づいたこと。原典にあたるって大事ね。

  • もともと、書家の先生の間でも漢字の筆順は一致しないことがあった(そのために文部省が筆順を定めた)。 例: http://d.hatena.ne.jp/kuzan/20080509/1210319941
  • 「この書き順がいちばんきれいに書ける」とは言っていない。「書写指導の教育的な観点」から筆順を一貫させるべき、と言っている。
  • 「ここに取りあげなかった筆順についても、これを謝りとするものでもなく、また否定しようとするものでもない」と明示されている。
  • 標準的な筆順で文字が書けることを目標にするのは3年生から。
  • 筆順は手書きの毛筆書体で示されているので、現代の基準では先生に直されてしまうような文字(点の位置がズレてる、とか)が多く見られる。
  • 筆順を示しているだけで、画数は示されていない。紙面の都合もあってか、一部の書き順は省略されている(たとえば「字」の4画目と5画目はまとめられている)。

「おサイダー」の話

日本人の知らない日本語」に、外来語の名詞で頭に「お」がついて丁寧語になるもの……と聞かれて「おビール」と答えるくだりがあった。「おビール」は水商売でよく使う言葉だからやめなさい……みたいな話だったと記憶している。妻に同じ質問をしてみて、帰ってきた答えは「おトイレ」。 ビールとトイレの他に、そんな語はそうないよな……と思っていたのだが、古川緑波「ロッパの悲食記」にある「清涼飲料」の話にこんなくだりがあった。

ビールや、サイダーに、「お」の字を附けたのは、何時の頃からであろうか。

https://www.aozora.gr.jp/cards/001558/files/52328_46436.html

どうやら昔は「おサイダー」という言い方もあったらしい。今はググっても能登の「しおサイダー」しか引っかからない……

カール・ワイクとトム・ケリーの「ヴュジャデ」

ジェームズ・R・チャイルズ「最悪の事故が起こるまで人は何をしていたのか」には、カール・ワイクが提示した「ヴュジャデ」という語が出てくる。体感すると「巨大で意味のない恐怖におそわれる」らしい。

調べたところ、Karl E. Weick,“The Collapse of Sensemaking in Organizations: The Mann Gulch Disaster”(1993)*1が出展で、”I have never been here before, have no idea where I am, and no idea who can help me”という感覚なんだそうだ。

一方で、Googleで「ヴュジャデ」を検索すると、IDEOのトム・ケリーが「イノベーションの達人!」で示した「見慣れたものを初めて見るように見ること」という意味ばかり出てきて、カール・ワイクの話はぜんぜん出てこない。

こういう「元々あった語の意味が、ぜんぜん別のカテゴリーの、ぜんぜん別の意味で、偶然にも?上書きされてしまう現象」に名前ってついてないんですかね。

robocopy finished with no error, with erroneous result on OneDrive for Business

Background:

  • You are using OneDrive for Bueiness.
  • You are trying to copy files to OneDrive with robocopy.

Problem:

robocopy had finished with no error (with ERRORLEVEL 0), but the the size of the files on OneDrive is 0byte.

Cause:

It seems the administrators of your OneDrive for Business activate storage quota for file size.

Solution:

You should ask the administrators to remove the quota, or you can split the files with utilities like 7-zip.