指定幅に入るかを調べるために
仕組みはいらないからすぐに使いたいという方は、4ページの「文字列配置プログラムAdjustString.java」に進んでください。
まず必要なのが指定幅に入るかどうかをどうやって判断するかということです。
これには、FontMetricsのメソッドである、stringWidth(String)が使えます。 戻値はポイントを単位とするfloat値です。FontMetricsのインスタンスはGraphics2DのインスタンスからgetFontMetrics()で得られます。
//Graphics2D g2, String mojiretsuとして、 FontMetrics fm = g2.getFontMetrics(); float widthinpt = fm.stringWidth(mojiretsu);
この値はGraphics2Dに設定されているフォントから、サイズとプロポーショナルかどうかも考慮されて実際の印字幅が計算されます。
文字列を1文字ずつ分解するために
次に必要になるのは、均等割付けのために文字列を一文字ずつばらばらにする機能です。これにはサロゲートペアへの対応が必要です。
Javaの文字はもともとUnicodeに基づき、全部の文字を1文字あたり2バイト(正確には16ビット)で表すように設計されました。charは2バイト、Stringはcharの配列になっています。配列の1要素をとれば1文字に相当するはずでした。しかし、当然2バイトでは全世界の文字を登録するには不足で、Unicodeが拡張されます。いくつかのやり方がありますが、拡張部分の文字を4バイトつまり2バイトのペアで表すのがサロゲートペアです。Javaではこれを配列の2つの要素で一文字とするように拡張しました。最低限の対応という感じがします。なかなか不便です。
WindowsもVistaから(Linuxではもう少し前から)JIS X 0213で追加された文字に対応するようになりました。この中にサロゲートペアが必要な文字が若干含まれていますので、考慮する必要があります。
例えば、普通の文字"東西南北"にサロケートペアになってしまう"【丈`】"(注1)の文字を挟んだもので試してみます。
注1
CodeZineのサイトではCMSの仕様でサロゲートペアの文字が扱えないため、「U+2000b」の文字を【丈`】(実際は1文字)と表記しています。
| 文字 | 東 | 西 | 【丈`】 | 南 | 北 | |
|---|---|---|---|---|---|---|
| コードポイント値 | U+6771 | U+897f | U+2000b | U+5357 | U+5317 | |
| String内部のCharの配列 | 6771 | 897f | d840 | dc0b | 5357 | 5317 |
| 配列のindex | 0 | 1 | 2 | 3 | 4 | 5 |
| UTF-8(参考) | e6 9d b1 | e8 a5 bf | f0 a0 80 8b | e5 8d 97 | e5 8c 97 | |
コードポイント値とはUnicodeに登録された文字の集まりに付けられた番号です。文字集合内の文字の位置を指し示す番号というイメージです。この数値は以前は0~FFFFの16ビットでしたが、0~10FFFFの21ビットに拡張されたわけです。U+を頭につけて16進数で表現します。Javaにはこのコードポイント値をintとして扱う方法も若干用意されています。
charの値の実体は16ビットの符号なし整数で、通常の文字はコードポイント値がそのまま入っています。"東西南北"はそうなっているのが分かります。"【丈`】"はサロゲートペアで表現されています。これは21ビットを決められた方法で分けて、U+D800〜U+DBFFの範囲の値とU+DC00〜U+DFFFの範囲の値の組み合わせとして表現したものです。この2つの範囲はサロゲートペア用として予約されていて他の文字と混同されることはありません。
Javaにはサロケートペアを扱うメソッドはいくつかありますが、次のようにして、一文字ずつ切り出すことができます。
String str = "東西【丈`】南北";
int stlen = str.length();
int cpct = str.codePointCount(0,stlen);
System.out.println(str+": 配列要素数="+stlen+": 文字数="+cpct);
int i=0;
int nexti = 0;
while (stlen>i){
nexti = str.offsetByCodePoints(i,1);
System.out.print("("+i+","+nexti+")"+str.substring(i,nexti)+"-");
i=nexti;
}
System.out.println();
実行結果です。
東西【丈`】南北: 配列要素数=6: 文字数=5 (0,1)東- (1,2)西- (2,4)【丈`】- (4,5)南- (5,6)北-
Javaにとってサロゲートペア対応は後から追加された仕様なので、分かりやすいとは言えませんが、ここで使った2つのメソッドを組み合わせるのが最も簡単な方法だと思います。文中のindexは文字列を構成する配列の添字を指します。CodePointは1文字を指し示す数値のことですからコードポイント数は文字数のことです。
冒頭のintは、戻値の型です。
int codePointCount(int beginIndex, int endIndex)
このStringの指定されたテキスト範囲のUnicodeコードポイントの数を返します。要するにsubString(int beginIndex, int endIndex)内の文字数です。
int offsetByCodePoints(int index, int codePointOffset)
このString内で、指定されたindexからcodePointOffsetコードポイント分だけオフセットされた位置のインデックスを返します。要するにsubString(int Index, int endIndex)を使って、indexから、codePointOffset個の文字を切り出すためにはendIndexの値をいくつにすればよいかを求めます。
サロゲートペアの取扱を試すためには
ここで紹介されているプログラムはCodeZineのサイトのCMSの制限から、サロゲートペアを必要とする文字を、普通の文字に置き換えて掲載していますので、掲載してある実行結果と同じにはなりません。
掲載のままでも、文字列を1文字ずつ分解する様子は確認できます。
東西【丈`】南北: 配列要素数=8: 文字数=8 (0,1)東- (1,2)西- (2,3)【- (3,4)丈- (4,5)`- (5,6)】- (6,7)南- (7,8)北-
サロゲートペアの部分でも1文字ずつ分解できることを確認するには、【丈`】の部分をサロゲートペアを必要とする文字に書き換える必要があります。ただし、もう一つのハードルがあってシステムがUTF-8を採用しているなど、サロゲートペアを扱える環境になっていなければなりません。
Linuxではかなり前からシステムのデフォルトがUTF-8で、仮想端末でのコマンドもUTF-8での入出力になっています。JavaコンパイラはOSの設定に合わせてソースコードを読みますから問題を生じません。
この場合、唯一の問題は文字の入力です。【丈`】は丈とおなじく「じょう」で漢字変換できるかもしれませんが、なかなか出てこないでしょう。どこかに表示させることができればコピー・アンド・ペーストで普通の文字同様にプログラム中にリテラルで挿入して使えます。
それが面倒な場合は、コードポイント値の整数配列から文字列を生成する方法が使えます。
int[] cps = {0x6771,0x897f,0x2000b,0x5357,0x5317};
String str = new String(cps,0,cps.length);
実は0x2000bだけが問題なので、次のように書いても同じです。
String str="東西"+(new String(new int[]{0x2000b},0,1))+"南北";
いわゆるShift_JIS環境の問題
WindowsではOS本体がUnicode化されたときにも、仮想端末はShift_JIS(正確にはWindows-31JまたはMS932、以降MS932と記述)がデフォルトのままでした。つまり、Windowsでは特に指示しなければエディタはMS932で保存し、JavaコンパイラもソースファイルをMS932として読んで、テキスト出力もデフォルトはMS932でした。そしてWindows10になっても、ほとんどそのままです。
しかし、サロゲートペアの文字はMS932では表現できません。この環境でサロゲートペアの文字の扱いを確認するには3つの問題があります。
(1)ソースコードをUTF-8で保存する
最近になって今更ですが、メモ帳がUTF-8での保存がデフォルトになりました。多くの方が他のエディタをお使いでしょうが、適当なものがなければこれで保存できるようになりました。
ただし、UTF-8で保存できても、サロゲートペアに対応していないエディタを筆者は知っています。このエディタは内部コードがMS932のままでした。お気をつけください。
もちろんサロゲートペアの文字の扱いができればUTF-8以外でも構いませんが(2)と合致することが必要です。
ちなみに、UTF-8とはコードポイント値をそのまま使わずに、1バイトから4バイトまでのバイト列に変換して使うUnicodeのエンコード方式のひとつです。上の表に参考として掲載しました。拡張されたコードポイント値で10000~10FFFFの部分はF0〜F7で始まる4バイトのコードが割り当てられています。UTF-8にはサロゲートペアの概念はありません。「UTF-8でサロゲートペア文字を扱う」というのは正確ではありません。ほんとうは「UTF-8で、UTF-16というエンコード方式でのサロゲートペア文字を扱う」、または、「UTF-8で基本多言語面外の文字を扱う」といった方が良いでしょう。
(2)コンパイラにソースコードがUTF-8であることを指定する
JavaコンパイラはWindowsではソースコードがMS932であることを前提にしています。次のようにしてUTF-8を指定します。もちろんHogeHoge.javaはターゲットとなるソースファイルの例です。
javac -encoding UTF-8 HogeHoge.java
最近はJavaのバージョンが上がる速度も早くなり、また、JDKも様々なところで作られるようになり、この指定が必要がなくなるときが来るかもしれません。
(3)仮想端末への文字出力の問題
これが一番の難問です。コマンドプロンプトやPowerShellのウィンドウ(これを仮想端末と言っておきます)へのサロゲートペア文字の出力は多分文字化けします。2020年6月現在のWindows10では、うまく行きませんでした。
PowerShellウィンドウへのコードページ指定(chcp 65001 でUTF-8)、Java実行時の-Dfile.encoding=UTF-8オプション、フォントの指定変更などを組み合わせましたが、サロゲートペアどころか日本語の文字が全滅したり、出力がまったくなかったりと原因を探ることができません。PowerShellより古くからあるコマンドプロンプトはもう少しまともでUTF-8の出力が表示できますが、サロゲートペアはだめです。
これを解決するには次のような方法をとるのが良さそうです。
(3a)出力にサロゲートペアの文字を出さないように工夫をし、文字コードなど変換して表示する
(3b)出力をファイルにUTF-8で書き出してUTF-8扱えるエディタなどで確認する
(3c)出力をGUIで表示する。
GUIで試すプログラム
問題の多い仮想端末への出力の代わりにswingのshowMessageDialog()を使うプログラムです。リストに溜めた文字列を最後にダイアログとして出します。プログラムリスト中にもサロゲートペアを必要とする文字を直接書き込んでいないのでMS932の環境でもコンパイルすることができます。GUI環境でUnicodeに対応できていてフォントが用意されていれば一文字ずつ取り出す様子が確認できます。
import java.util.List;
import java.util.ArrayList;
public class ChkSurrogGUI {
public static void main(String[] args){
List<String> lines = new ArrayList<>();
//String str = "東西【丈`】南北";
String str="東西"+(new String(new int[]{0x2000b},0,1))+"南北";
int stlen = str.length();
int cpct = str.codePointCount(0,stlen);
String s = str+": 配列要素数="+stlen+": 文字数="+cpct;
//System.out.println(s);
lines.add(s);
int i=0;
int nexti = 0;
while (stlen>i){
nexti = str.offsetByCodePoints(i,1);
s = "("+i+","+nexti+")"+str.substring(i,nexti)+"-";
//System.out.println(s);
lines.add(s);
i=nexti;
}
javax.swing.JOptionPane.showMessageDialog(
null,
String.join("\n",lines),
"一文字ずつ",
javax.swing.JOptionPane.PLAIN_MESSAGE
);
}
}
ダイアログの中には、次のようにサロゲート文字も出力できます。
