住所の自動分割印刷

あふれ改行

ここであふれ改行とは、指定幅に文字列を書ききれなかった時に、残りを次の行に持ち越す機能という意味で使います。特に、文字列の途中に直後で改行することが好ましい文字を見つけてその直後で改行することを考えようという事です。

住所等は1行に書ききれない時には、見やすい場所で改行して配置を考えます。宛名印刷のソフトではこのために住所を2分割して入れるようになっていたりします。これを自動でできるようにします。

葉書に書く住所は縦書きが多いですが、まず数字の変換などを必要としない横書きにして、処理方法を考えてみます。

javaで帳票印刷 文章を印刷の所定枠に文章を印刷する方法では、書ききれない分を次の行に左寄せで書くようにしていました。

印字例1
青森県弘前市大字赤松原3丁
目456番地の78

これを右寄せで書けば住所らしくなるでしょう。

印字例2
青森県弘前市大字赤松原3丁
目456番地の78

しかし、印字幅によっては、都合の悪い場所で改行される心配があります。

印字例3
青森県弘前市大字赤松原3丁目45
6番地の78

これはぜひこうなって欲しいでしょう

印字例4
青森県弘前市大字赤松原3丁目 
456番地の78

"丁目","大字","市","郡"などの大きな切れ目になりがちな文字を順番に捜してその後ろで改行します

" "," "の半角、全角文字の方が優先で改行したいですが、"丁目"などと異なり、文字自体を削除して欄内に入るかどうかを確認する必要があり、別扱いをします。

注意すべきは、市川市など"市"の文字を含む地名が若干あること。青森県でも町村合併前には"市浦村"という村が存在しました。

あふれ改行文字

候補となる文字(列)をいくつか上げておき、順番に含まれるかテストしていくことにします。どんな文字(列)を選択しどんな順に試みるかは利用する地域により異なりますから、試してみるのが良いでしょう。

青森県弘前市周辺で試行錯誤した結果では、次のようにすると落ち着きました。弘前市では正式には町名の前に"大字"がつきます。歴史的な経緯なのだろうと思いますが、弘前市大字青山1丁目という住所はちょっと違和感があります。

String[] afure = {" "," ","丁目","大字","字","市","番地"};

AfureJushoの使い方

AjustStringと同様、AfureJushoというクラスを作成します。

AfureJusho af = new AfureJusho(Graphics2D g2, String jusho, float horwidth);

これでhorwidthの幅(単位はmm)をもつ枠に入る文字列をafインスタンス内に用意します。

この文字列は

af.drawLeft(float x, float y);

というメソッドで(x,y)を始点とした幅horwidthの領域に書きだされます。

もし文字列が残っていたら

af.hasNext()

がtrueを返すので、

af.nextLine();

で、残りの文字列に対し同じ事をおこなって、2行目の文字列を用意します。

その後

af.drawRight(float x, float y);

というメソッドで(x,y)を始点とした幅horwidthの領域に右寄せで書きだされます。yの値は前の行より1行分以上大きな値にしておきます。

これを最初の文字列が全部書き出されるまで繰り返します。

プログラム使用例

ちょうど"丁目"で改行されるよう幅などを調整しています。

ZipJusho.javaの住所印字部分

Font font10 = new Font(Font.SERIF, Font.PLAIN, 10);
AfureJusho af;
float x = 35f;
float y = 40f;
float horwidth = 55f;
g2.setPaint(Color.black);
g2.setFont(font10);
af = new AfureJusho(g2, jusho, horwidth);
af.drawLeft(x,y);
while(af.hasNext()){
    af.nextLine();
    y += 4.8f;
    af.drawRight(x,y);
}

AfureJusho.javaのコンストラクタ

まずフィールド部分。変数はBASIC版であったころからの習慣を引き継いでいる...

AfureJusho.java のフィールド部分

String kp;  //書くべき住所
String nbf; //次に書く1行
int jbgn;  //kpのindex
int nextjbgn; //kpのnbfの次のindex
int kplen; //kpの長さ(文字数)
float fmwm;  //枠の幅(mm)
float remm ; //nbfを書いた時のあまり(mm)
float aidamm;//nbfの文字間に使う隙間
Graphics2D g;
FontMetrics fm;
float mm2pt = 72/25.4f; //mm->ptへ換算
float pt2mm = 25.4f/72; //pt->mmへ換算
String[] afure = {" "," ","丁目","大字","字","市","番地"};

コンストラクタ

nextjbgnを先頭にしてnextLine()を呼び出す。

AfureJusho.java のコンストラクタ

public AfureJusho(Graphics2D g, String mojiretsu, float formatWidthMm) {
    this.g = g;
    kp = mojiretsu;
    kplen = kp.length();
    fmwm = formatWidthMm;
    fm = g.getFontMetrics();
    nextjbgn = 0;
    //System.out.println("constr:'"+kp+"',"+fmwm); //test
    nextLine();
}

AfureJusho.javaのキモ

nextLine()前半はあふれ改行文字の候補を使った分割です。

AfureJusho.java のnextLine()の前半

public void nextLine(){
    jbgn = nextjbgn;
    nbf = kp.substring(jbgn,kplen);
    remm = fmwm - fm.stringWidth(nbf)*pt2mm;
    int afi = 0;
    nextjbgn = -1;
    while(0>remm && afure.length>afi)  {
        if (nbf.indexOf(afure[afi])>=0){
            if (afure[afi].equals(" ") || afure[afi].equals(" ") ){
                nextjbgn = nbf.indexOf(afure[afi])+jbgn;
                nbf = kp.substring(jbgn,nextjbgn);
                nextjbgn++;
            }else{
                nextjbgn = nbf.indexOf(afure[afi])+jbgn+afure[afi].length();
                nbf = kp.substring(jbgn,nextjbgn);
            }
            remm = fmwm - fm.stringWidth(nbf)*pt2mm;
        }
        afi++;
    }

実際の動作例

コンストラクタから呼ばれるときの値。

nextjbgn:最初はコンストラクタから0として呼ばれる
kp      :"青森県弘前市大字赤松原3丁目456番地の78"
kplen   :22

これから1行目を準備する。

jbgn    :0
nbf     :"青森県弘前市大字赤松原3丁目456番地の78"
remm    :nbfを書くと余りは何mmか
afi     :afure[]の添字の初期値
nextjbgn:-1は残りの文字がないことを表す

whileの 0>remm は入りきらないことを示す

あふれ改行{" "," ","丁目","大字","字","市","番地"}を順に試す

もしもあふれ改行文字(列)があったら

全角半角の空白の場合はその直前までをnbfとし、空白の次を次の開始位置とする

そうでないときは、その文字(列)までをnbfとし、その次を次の開始位置とする

remmを計算しなおし、改行文字(列)を1つ進めておく

例では空白はないので、"丁目"で合致する。

nextjbgn:12+0+2->14
nbf     :"青森県弘前市大字赤松原3丁目"

これで枠内におさまるならnextLine()を終わる。

次にZipJusho.javaからaf.drawLeft(x,y);により、nbfが印字される

次にaf.hasNext()でtrueを返すと、

af.nextLine();でもう一度呼び出される。

再び呼ばれる時の値。

nextjbgn:14
kp      :"青森県弘前市大字赤松原3丁目456番地の78"
kplen   :22

これから2行目を準備する。

jbgn    :14
nbf     :"456番地の78"
remm    :nbfを書くと余りは何mmか
afi     :afure[]の添字の初期値を0に戻す
nextjbgn:-1は残りの文字がないことを表す

今回はremmが正の値なのでwhile内は実行しない

次にZipJusho.javaからaf.drawRight(x,y);により、nbfが印字される

次にaf.hasNext()でfalseが返る。

終了

入りきらなければ3行に

上に示した例は2行ですが、入りきらなければ3行になります。

青森県弘前市大字赤松原3丁目456番地の78 市営住宅A-1234

を45mmに表示します

大字と半角空白で切っています。

afureの候補は順番に試されます。つまり、候補Aでnbfが作られたにも関わらず、入りきらない時は、次の候補Bを試します。

このとき、kpに戻らず、入りきらなかったnbfに次の候補を適用して切る場所を探します。これは、候補Aの後ろにBが見つかったとしても、入りきらないのがわかりきっているからです。

それでもだめなら1文字ずつ

nextLine()の後半です。

前半で全ての候補を試して入りきらない時は、一文字ずつ入るまで削っていきます。

数字の途中で改行してしまう可能性もありますが、プレビューなどで確認し、改行文字の候補を追加するか、適当な場所に半角スペースを入れて登録することにします。

後半のプログラムは他の手段が至らなかった時の最終的な保険と考えています。

さて、スタートの文字列は、kpではなく、改行候補で短くなったnbfです。

ここで必要になるのがコードポイントの考慮です。一般的にはサロゲートペア対応と言ったほうがわかりやすいでしょうか。

サロゲートペアは普通の文字の2文字分を必要とする文字です。この文字を1文字減らす時に2文字分減らさなければならないのです。サロゲートペアを考慮した1文字をJavaではコードポイントと呼びます。Javaの都合上の文字はStringの実態であるchar配列の1要素です。このプログラムではnbf中のコードポイントの数を nbfcplen に格納してこれを1減させ、この文字列の配列の要素数を求めて、nbflenに格納しています。

nbfcplenという変数を介さずに値を渡すことも可能ですが、わかりやすさのためにこのようにしています。

AfureJusho.java のnextLine()の後半

    int nbflen = nbf.length();
    while(0>remm){
        nbf = kp.substring(jbgn,jbgn+nbflen);
        remm = fmwm - fm.stringWidth(nbf)*pt2mm;
        nextjbgn = jbgn + nbflen; 
        int nbfcplen = nbf.codePointCount(0, nbflen);
        nbfcplen--;
        nbflen = nbf.offsetByCodePoints(0, nbfcplen);
    }
}

サロゲートペアで表現される文字には、次のようなものがあります。

𡈽𠀋𠮟𡋽𡉕𣷓𩸽𨺉𥐮𪀯𠝏

(どれも JIS X 0213:2004 にある文字ですが、フォント(環境)によっては表示されないかも知れません)

これを適当に加えた住所で、正しく動いているか検証してみます。

うまく行っているようです。サロゲートペアの文字を下に示します。

その他のメソッド

目的から言って、drawKintou()は不要かと思うが、AjustStringの歴史から、ここにも加えてある。

AfureJusho.java の一部

public boolean hasNext(){
    return nextjbgn>=0;
}
public void drawLeft(float hm, float vm) {
    g.drawString(nbf,hm*mm2pt,vm*mm2pt);
}
public void drawRight(float hm, float vm) {
    g.drawString(nbf,(hm+remm)*mm2pt,vm*mm2pt);
}
public void drawCenter(float hm, float vm) {
    g.drawString(nbf,(hm+remm/2)*mm2pt,vm*mm2pt);
}
public void drawKintou(float hm, float vm) {
    float hpp = 0;
    int nbflen = nbf.length();
    int nbfcplen = nbf.codePointCount(0,nbflen);
    if (nbfcplen!=1){
        aidamm = remm/(nbfcplen-1);
        int i=0;
        int nexti = 0;
        int cpct = 0;
        while (nbflen>i){ 
            nexti = nbf.offsetByCodePoints(i,1);
            //System.out.println("'"+nbf.substring(i,nexti)+"',"+hpp); //test
            g.drawString(nbf.substring(i,nexti),(hm+aidamm*cpct)*mm2pt+hpp,vm*mm2pt);
            hpp = fm.stringWidth(nbf.substring(0,nexti)); //pt when 0?
            i=nexti;
            cpct++;
        }
    }else{
        drawCenter( hm, vm);
    }
}

プログラムリストへのリンク

住所の自動分割印刷 プログラムリスト:ZipJusho.java, AfreJusho.java