住所の区切りを自動化する
上記の印字例の住所部分を下図の現状に示しました。AdjustStringクラスでの改行は見やすくありません。"相模原市"の後と、"三上ハイツ"の前で区切りたいところです。
この場合は"市"のあとで区切る、" "(スペース)の後で区切るなどの処理をすればよいと考えられます。この他にも"県"とか"丁目"、"番地"などいろいろ候補は挙げられますが、"市"というのはうまくいかない場合があります。市原市や四日市市、市浦村(青森県)などの問題です。このため改行候補は複数にして優先順位を印刷するデータに合わせて変更するなどの対処が必要なことも考えられます。
また、全角半角のスペースは特別な存在です。ここで改行することになった場合は印字しなくて良い文字だからです。
住所の自動区切りプログラム
前項の方針でAdjustStringのようなプログラムをつくります。まずプログラムリストを示し、使い方を説明し、その後で動作原理を説明します。
/**
文字列を指定幅に改行しながら配置する
改行位置の候補文字列(文字列の末尾で改行)を配列で持つ。
候補文字が全・半角空白の場合はこれを印刷しない。
2019-11-18
@author Adachi Junichi
@version 3.1
*/
import java.awt.Graphics2D;
import java.awt.FontMetrics;
public class AfureString {
String zstr, str;
int nextjbgn;
int zslen;
float wdmm;
float remm = 0f;
Graphics2D g2;
FontMetrics fm;
float mm2pt = 72/25.4f;
float pt2mm = 25.4f/72;
String[] afure = {" ","丁目","大字","字","郡","市","番地"};
boolean debug = true;
/** string の内、areawidth に入る最初の部分をstrに保持。 */
public AfureString(Graphics2D g2, String string, float areawidth) {
this(g2,string,areawidth,null,false);
}
/** string の内、areawidth に入る最初の部分をstrに保持。改行候補をafureにする */
public AfureString(Graphics2D g2, String string, float areawidth, String[] afure) {
this(g2,string,areawidth,afure,false);
}
/** string の内、areawidth に入る最初の部分をstrに保持。debugをtrueにできる */
public AfureString(Graphics2D g2, String string, float areawidth, boolean debug){
this(g2,string,areawidth,null,debug);
}
/** string の内、areawidth に入る最初の部分をstrに保持。改行候補をafureにし、debugをtrueにできる */
public AfureString(Graphics2D g2, String string, float areawidth, String[] afure, boolean debug){
if(afure!=null)this.afure=afure;
this.debug = debug;
this.g2 = g2;
fm = g2.getFontMetrics();
zstr = (string!=null) ? string : ""; //nullなら""
wdmm = (areawidth>=0f) ? areawidth : 0f; //負なら0
zslen = zstr.length();
nextjbgn = 0;
if(debug) System.out.println("constr:'"+zstr+"',"+wdmm);
nextLine();
}
/** 次の行に書ける部分をstrに保持。 */
public void nextLine(){
//溢れ改行文字での改行はcpに踏み込まなくてもよい
int jbgn = nextjbgn;
str = zstr.substring(jbgn,zslen);
remm = wdmm - fm.stringWidth(str)*pt2mm;
int afi = 0;
nextjbgn = -1;
if(debug) System.out.printf("AfreBegn:%s %3d %3d %3.1f\n",str,jbgn,nextjbgn,remm);
while(0>remm && afure.length>afi) {
if (str.indexOf(afure[afi])>=0){
if (afure[afi].equals(" ") ){
nextjbgn = str.indexOf(afure[afi])+jbgn;
str = zstr.substring(jbgn,nextjbgn);
nextjbgn++;
}else{
nextjbgn = str.indexOf(afure[afi])+jbgn+afure[afi].length();
str = zstr.substring(jbgn,nextjbgn);
}
remm = wdmm - fm.stringWidth(str)*pt2mm;
}
afi++;
}
if(debug) System.out.printf("Afre2dec:%s %3d %3d %3.1f\n",str,jbgn,nextjbgn,remm);
while(0>remm && !str.isEmpty()) {
int newendidx = str.offsetByCodePoints(str.length(), -1); //一つ前のindexを求める
str = str.substring(0,newendidx);
remm = wdmm - fm.stringWidth(str)*pt2mm;
nextjbgn = jbgn + str.length();
}
if(debug) System.out.printf("output--:%s %3d %3d %3.1f\n",str,jbgn,nextjbgn,remm);
}
/** 書くべき文字が残っている時trueを返す */
public boolean hasNext(){
return nextjbgn>=0;
}
public void drawLeft(float x, float y) {
g2.drawString(str,x*mm2pt,y*mm2pt);
}
public void drawRight(float x, float y) {
g2.drawString(str,(x+remm)*mm2pt,y*mm2pt);
}
public void drawCenter(float x, float y) {
g2.drawString(str,(x+remm/2)*mm2pt,y*mm2pt);
}
public void drawKintou(float x, float y) {
float hpp = 0;
int strlen = str.length();
int cpct = str.codePointCount(0,strlen);
if (cpct!=1){
float aidamm = remm/(cpct-1);
int i=0;
int nexti = 0;
int ct = 0;
while (strlen>i){
nexti = str.offsetByCodePoints(i,1);
g2.drawString(str.substring(i,nexti),(x+aidamm*ct)*mm2pt+hpp,y*mm2pt);
hpp = fm.stringWidth(str.substring(0,nexti));
i=nexti;
ct++;
}
}else{
drawCenter( x, y);
}
}
}
AfureString.javaの使い方
上に記した「PrintableWithPreview01.java」を改変して、住所部分をAdjustStringでなくてAfureStringを使うように書き換えます。
AfureString afs;
afs = new AfureString(g2,addr,waddr);
afs.drawLeft(xaddr, yaddr);
int ct = 0;
while(afs.hasNext()){
afs.nextLine();
afs.drawRight(xaddr, yaddr + ypch*(++ct));
}
最初のインスタンスの生成で1行目をセットしておいて、drawLeft()で左寄せに印字し、その後hasNext()である間、nextLine()で次の行の文字列をセットさせ、drawRight()で右寄せに印字します。
AdjustStringに比べ、文字列をどこまで書いたかの情報はインスタンス内に持つので、開始位置を指定する必要はありません。
改行候補は、指定しなければデフォルトの候補を使用します。必要に応じてコンストラクタに引数として与えて一時的に変更できます。改行候補はインスタンスの生成時に一度使われることに注意してください。
改行候補は文字列の配列です。デフォルトでは次のように設定されています。
String[] afure = {" ","丁目","大字","字","市","番地"}
配列の格納順に改行候補になります。この項目と順番は扱う地域の特性により変えたほうが良いかもしれません。"市"が下位にあるのは、"市原市"や"市浦村"など、地域名に"市"が入っている場合に不都合だからです。同じ理由で"町"や"村"は除外してあります。
改行候補は文字列を許しますから、"市原市"がよく出てくるデータを扱う場合にはあえて、"市原市"を上位の候補に加え、その後に"市"を配置するという手もありです。
改行候補でうまく切れない場合は、AdjustStringのように入るところまでという所で切るようになっています。もしどうしてもうまくいかないなら、住所データ内に" "を挿入して調整する手もあります。
AfureString.javaを使うように変更した「PrintableWithPreview01.java」のプレビューの様子です。
AfureStringの仕組み
改行候補で区切って指定幅に入りきる最初の文字列をstrに入れ、次の行に書く文字列の開始位置(元の文字列で)をnextjbgnに入れるという過程の解説です。
サロゲートペアが含まれていても(それが改行候補内であっても)問題なく動きます。
public void nextLine(){
//改行候補文字での改行はcpに踏み込まなくてもよい
int jbgn = nextjbgn;
str = zstr.substring(jbgn,zslen);
remm = wdmm - fm.stringWidth(str)*pt2mm;
int afi = 0;
nextjbgn = -1;
if(debug) System.out.printf("Afre:%s %3d %3d %3.1f\n",str,jbgn,nextjbgn,remm);
while(0>remm && afure.length>afi) {
if (str.indexOf(afure[afi])>=0){
if (afure[afi].equals(" ") ){
nextjbgn = str.indexOf(afure[afi])+jbgn;
str = zstr.substring(jbgn,nextjbgn);
nextjbgn++;
}else{
nextjbgn = str.indexOf(afure[afi])+jbgn+afure[afi].length();
str = zstr.substring(jbgn,nextjbgn);
}
remm = wdmm - fm.stringWidth(str)*pt2mm;
}
afi++;
}
...この後、remmがまだマイナスならAdjustStringの手法で一文字ずつ減らしていく。
上の実行例での計算過程を図示すると次のようになります。
最初の候補" "で区切っても入り切りません。次の"丁目","大字","字",は住所に含まれませんからスキップします。"市"で区切ると入ります。これが1回目。次は" "で区切ると入ります。これが2回目です。最後は区切らなくても入ります。これが3回目です。
- jはjbgnの値。これは元の文字列zstr内のindexの値
- iはstr.indexOf()の値。zstrから切り出したstr内のindexであることに注意
- nはnextjbgnの値。次の行を求める時のzstr内の開始位置。次の呼び出しの最初でjbgnに代入される。候補が全角半角のスペースでなければ、「jbgn+i+候補の長さ」と計算される
