質問で提示されたコードのコメントから、外部関数 getCode()、putCode() の仕様はわかりました。
getCode()で入力ファイルハンドルからUTF-8の1文字分を読み込んでデコードし、putCode()でUTF-8にエンコードして出力ファイルハンドルに書き込むのですから、単純に考えるとコードは次のようなものになることでしょう。
C
1#include <stdio.h>
2#include "UTF8io.h"
3
4int main(int argc, char *argv[]) {
5 unicode input;
6
7 while ((input = getCode(stdin)) != UTF8NUL) { // EOFかエラーになるまで1文字ずつ読む
8 if (読んだ文字を無視したい)
9 continue;
10
11 if (読んだ文字の直前で改行したい)
12 putCode(0x000A, stdout);
13
14 putCode(input, stdout); // 読んだ文字を出力
15
16 if (読んだ文字の直後で改行したい)
17 putCode(0x000A, stdout);
18 }
19
20 return 0;
21}
それぞれのif文の括弧に入る条件が実際にどんなコードになるかはご自分でお考えください。
しかし、これだけでは質問で挙げられているそれぞれの「STEP」を実現できません。以下、各STEPで検討すべき事項について、気づいた点を述べます。
#0 前提
-
レパートリの選定
ユニコードに収載された全ての文字を対象にすることは現実的ではありませんので、ここでは日本語の文書に現れる文字を処理の前提とすることにします。このような文字集合の公的規格としてはJIS X 0213があります。以降の説明では扱う文字の範囲をこれに準拠します。
リンク先を見てわかるとおり、「日本語の文書」には伝統的な日本語の文字 (平仮名、片仮名、漢字) 以外にもラテン文字、ギリシャ文字、キリル文字、アイヌ語用片仮名、発音記号、その他のさまざまな記号などが含まれます。
-
改行の扱い
入力されるテキストによって、改行のコードが "\n"
、"\r\n"
、"\r"
などである可能性があります。したがってなんらかの方法で改行の正規化が必要です (何らかの前処理をしてもいいでしょうし、テキストを読み込むコードの中で行ってもいいでしょう)。以降の説明では入力テキストの改行はUnixの標準的な改行コード "\n"
に正規化ずみであるとします。
#1 入力に含まれる改行は無視 / 決まった文字数ご とに改行を入れて整形
これは上のコードで可能ですね。改行文字 U+000A ("\n"
) を読み込んだら無視すればいいです。
しかし、後述するように「欧文の単語を分割しない」という処理を想定するなら、逆に入力されるデータでは欧文の単語同士の間に改行が入っている可能性を考える必要があるのではないでしょうか。例 (こちらより):
このように母音をまったく含まないチェコ語の文の例としては他にもVlk
zmrzl, zhltl hrst zrn(凍った狼が一掴みの穀物を飲み込む)など
がある。
「Vlk」と「zmrzl」は別の単語ですから、単に改行を無視するのではなくU+0020 (空白) に置き換えるべきでしょう。いっぽう「ど」と「が」の間の改行は無視しなければなりません。
決まった文字数ごとに改行を入れるには、読み込んだ文字数によって改行を入れればよい わけではありません。複数のユニコード文字で文字が構成される場合があります。たとえば「チタタㇷ゚」の「ㇷ゚」は U+31F7 U+309Aという2つのユニコード文字の組み合わせでひとつの文字になっています。つまりこの語はユニコード文字としては5文字ですが、文字数を数えると4文字です。
U+309Aは先行する文字に半濁点をつける結合文字 (単独では使われず必ず他の文字の後につくユニコード文字) です。JIS X 0213に収載されている文字に使われる結合文字 としてはこのほかにU+0300 (鈍アクセントをつける) やU+0301 (鋭アクセントをつける) があります。 でこれ以外のものは U+0300 〜 U+0361 の範囲にありますから、これらのコード範囲にあるものは結合文字として処理していいでしょう。〔2024-05-24訂正〕
#2 文字幅を 2 : 1 (全角と半角) に
これは、STEP 1のように文字数で改行位置を決めるのではなく、文字の幅を基準にして一定幅で改行するようにするということですね。個々の文字の文字幅の情報が与えられたとしてどのようなコードを書けばいいかは、ご自分で考えてください。
全角と半角の区別は歴史的な事情によるもので、全ての文字がこの区別を持っているわけではないのですが、大まかに言うとASCII文字など欧文の文字・記号は半角、和文のそれは全角とされ、違う幅で表示されます。ユニコードではこの幅に関連して全ての文字を6種類に分類しています (「東アジアの文字幅」を参照)。
全角と半角を区別して処理する場合、これらのうち特性F (全角) とW (広) は全角として2桁の幅、特性H (半角)、Na (狭)、N (中立) を半角として1桁の幅とすればよいでしょう。
やっかいなのが特性A (曖昧) に分類される文字で、欧文では全角ではない (欧文にはもともと全角の文字はないので) のに、日本語など東アジアの言語の等幅フォントではしばしば全角で作られてきたものです。そのため表示環境によって全角に見えたり半角に見えたりすることがあります。たとえば次のようなものです。
- ギリシャ文字やキリル文字。テキスト中にギリシャ語やロシア語などの単語が出てくるのなら全角で表示するのは好ましくないですが、そのように表示されている例もよく見かけます。
- 6/9形の引用符 “……” (U+201C, U+201D)、‘……’ (U+2018, U+2019) も悩むところです。日本語の等幅フォントなどでは全角の文字として表示されますが、欧文ではそうではありません。形も微妙に変わります (全角のときは左や右に空きが取られ、引用符が字面の端に寄った形になります)。
これら特性Aの文字は実際の表示環境に合わせて (想定環境を定められない場合は何らかの基準を設けて) 全角か半角かに決める必要があります。
それぞれのユニコード文字に割り当てられた特性値の情報はこちらからダウンロードできます (テキストファイル、約200kB。全てのユニコード文字についての情報なのでJIS X 0213に収載されていない文字も含みます)。
#3 行頭に「。」や「、」などが来ないように、改行位置を調整
行頭禁則や行末禁則の処理ですね。
一般に閉じ括弧、句読点、音引き、小書き仮名などは行頭に来ないようにします。つまりこれらの直前では改行して行を分割することは禁止されます。同様に開き括弧などは行末に来ないようにします。つまりこれらの直後では改行して行を分割することは禁止されます。さらに、分割禁止のルールとしては、欧文の単語の途中では改行してはならないといったものもあります。
これらを考えてみると、例えば次のような場合 (かなりわざとらしい例ですが)
では、「e」と「だ」の間でしか行を分割できません。他の箇所には改行を入れられないことになります。
ここへ来て、冒頭に示した一文字ずつ読んですぐ出力するコードでは処理がうまくいかなくなります。読んだものをある程度メモリ上のバッファに置いておき、改行できる位置が見つかったら出力するようなコードが必要です。——ただ、質問者さんがそのようなコードを書き始めるのはどんなに早くても数ヶ月は先の話でしょうから、今からコード例などを示して予断を与えるようなことは控えます。
少しだけ説明します。#1 で挙げたテキストを例にすると、改行可能な位置は次のようになります。
こ|の|よ|う|に|母|音|を|まっ|た|く|含|ま|な|い|チェ|コ|語|の|文|の|例|と|し|て|は|他|に|も|Vlk‖zmrzl,‖zhltl‖hrst‖zrn|(凍っ|た|狼|が|一|掴|み|の|穀|物|を|飲|み|込|む)|な|ど|が|あ|る。
「|」と「‖」で改行可能な位置を示しました。「‖」の箇所はU+0020 (空白) があるため、ここで改行した場合は1桁減ります。このようにして改行可能な位置を見つけていき、適切な箇所で改行します。
日本語の禁則処理の詳細については下記を参照してください。
以下は余談です。
-
#1 で、欧文の単語の間の改行について「単に改行を無視するのではなくU+0020 (空白) に置き換えるべきでしょう」と述べました。どういう場合に改行を空白に置き換えるべきかは、分割可否ルールから導くことができます。
つまり、「Vlk」と「zmrzl」の間にもともと何もなかったのであればkとzの間での分割は禁止されるので、改行することはできなかったはずです。したがって分割禁止にならないように改行に代えて空白を挿入する必要があります。いっぽう「ど」と「が」の間は分割禁止ではないので、単に改行を無視できます。
-
#0 のウィキペディアの記事でも、上のJLREQでも、文字の一覧にいわゆる「全角英数」や「半角仮名」が挙げられていないのに気づかれたかもしれません。JIS X 0213のような文字コード規格ではこれらのものは過去の遺産であり、今後は基本的に使うべきではないという立場です (完全に禁止しているわけではないですが)。そのため一覧に挙がっていません。
- 全角形の文字の分割可否ルールについては、次のいずれかの方法があります。
a. 対応するASCIIの文字と同じルールにする。
b. 漢字などのいつでも前後どちらでも分割可能な文字と同じルールにする。
- 半角形の文字の分割可否ルールについては、次のいずれかの方法があります。
a. 対応する「全角」の片仮名や句読点と同じルールにする。
b. 欧文の文字と同様、互いに分離禁止にする。
ただしいずれの場合も、U+FF9E「゙」(半角濁点) とU+FF9F「゚」(半角半濁点) の直前は分割禁止とします。
雑多な補足
- 結合文字の処理について。結合文字は必ず他の文字の後につきますが、これがついても元の文字の文字幅が増えることはありませんし、分割可否ルールは変化しません。一方で結合文字と先行する文字との間は絶対に分割禁止です。つまり、入力に結合文字が現れたらそのまま出力しなければなりませんが、文字幅の計算や改行の可否判断はあたかもその結合文字が存在しないかのようにして行います (ちなみに結合文字はひとつの文字に複数つく場合もあります)。
- ユニコードではひとつの文字を何通りかの方法で表せる場合があるので、一つの表しかたに揃える「正規化」という前処理を行うことが推奨されます。よく使われる正規化の形式は正規化形式C (NFC) というものです。「Unicode正規化」を参照。
- 禁則処理への対応として「ぶら下げ組み」が行われることがあります。JLREQではこれについて触れていませんが、おそらく商業出版の書籍で縦書きのもの (漫画は除く) のほとんどがぶら下げ組みを採用しています。ご質問で想定されているようなプレインテキストでの改行処理でも、行末禁則の効果で行の右端が削れてしまうことをぶら下げ組みによって減らせるので、検討してみてもよいかもしれません。
最後に、ご質問の課題はおそらく自分で実装することに意義があるのだと思われますのでこれは関係ないかと思いますが、以上説明してきたようなさまざまな処理 (文字幅の特性の判別、改行処理、正規化処理など) を行うライブラリパッケージもあります。ICUはよく使われます。