ブログ

ご注意:
この情報は過去に書かれたブログ記事のキャッシュになります。最新の情報ではありませんのでご注意ください。
また、公開当時のキャッシュされたテキストのみを読めるようにした内容になっております。 公開当時の画像やデザイン情報がありませんので、デザイン上読みにくい部分がありますことをご了承ください。 なお、最新のブログはGitHubにて公開しています。

日付0000/00/00

4D v12は,これまでのバージョンとは違い,全面的にUnicodeを採用しています。デベロッパは,その及ぼす影響を想像しただけでも,とても不安な思いになり,バージョンの移行に対してかなり消極的になるかもしれません。それは,長年,4Dを扱ってきたベテランの開発者であっても同じです。


心の叫び
Shift_JISを前提に書かれた既存のプログラム,半角/全角を考慮した位置の算定,2バイト文字の改行,固定長文字列,パケット通信,CSV書き出し,苦労して書いたメソッドの数々は,すべて書き直さなければならないのでしょうか。ものすごい数になるので,絶対に無理です。何故,Japan Packプラグインは無くなってしまったのですか。それに代わるものはないのですか?時間がないので,Unicodeとか正規表現を勉強しろなどと言われても困ります。なんとかしてください。

v12への道
たいていのプログラマーは,理論をあれこれレクチャーされるよりも,コードを自分で研究したほうが,実際的な技術をものにできると感じています。そのようなわけで,この記事では,4D 2004コードと,v12のコードを並記することにしました。まったく書き換えを必要としなかったものもあれば,ほとんど書き直されたものもあります。たいていの場合,新しいメソッドのほうがオリジナルよりもずっとシンプルです。また,書き換えが特に必要なくても,v12でブラッシュアップできるものは,そのことが指摘されています。いずれにしても,それぞれの例から移行を成功させるためのヒントがたくさんみつけられるはずです。

STUDY #1
PositionInBlob ($Target;$PointerBlobText)
目的:Blobの中のPositionを返します。

オリジナルコード
$Loop:=1 // 見つかったかどうかのフラグ
$Offset:=0
$SizeBlob:=BLOB size($PointerBlobText->)
$LengthTarget:=Length($Target)
$Position:=-1
While ($Loop=1)
$OldOffSet:=$Offset
$Text:=BLOB to text($PointerBlobText->;Mac text without length;$Offset;$LengthTarget)
If ($Text=$Target)
// 見つかった
$Loop:=0
$Position:=$OldOffSet
Else
If ($Offset>=$SizeBlob)
$Loop:=0
Else
$Offset:=$Offset-$LengthTarget+1
End if
End if
End while
$0:=$Position

概要BLOBの中にある特定の文字列(暗示的にShift_JISのバイト配列)の出現する位置を調べるメソッドです。何故,このようなメソッドが必要だったかというと,4D 2004にはテキストに32,000バイトというサイズ上限[1]があり,それを超えるサイズのテキストは,BLOBに収録する必要があったからです。Positionという標準コマンドは,BLOBには使用できない[2]ので,BLOBを32,000バイト以下の断片に変換(BLOB to text)しながらループでPositionを実行する,ということが必要でした。

[1] コンパイルモード。インタプリタモードでは32,767でした。
[2] 4D Write,API Packなどのプラグインには,BLOBのサーチコマンドが用意されています。

変換の影響
BLOB size,BLOB to textは,ともにこれまでと同じように動作します。定数のText without lengthがMac text without lengthと変わりましたが,内容的には同じものです。「Mac」という接頭辞は,「4Dの内部文字コード」,つまりUnicodeではない,という意味があります。これは,英語版の4DがMacRomanエンコーディングをWindowsとMacの両方で採用していたことに由来しています。日本語版の4Dは,Windows-31Jあるいはコードページ932と呼ばれるShift_JISの亜種を採用しています。したがって,Mac text without lengthとは,要するに,コードページ932のことです。

ただ,オリジナルのメソッドは,バイト単位の位置を計算するのに,Lengthを使用しています。Lengthは,Unicodeモードでバイト数ではなく文字数を返すので,振る舞いが以前とは違います。書き換えが必要です。

書き換え例
$Text:=Convert to text($PointerBlobText->;"windows-31j")
$Position:=Position($Target;$Text;*)
C_BLOB($TextData)
CONVERT FROM TEXT(Substring($Text;1;$Position-1);"windows-31j";$TextData)
$0:=BLOB size($TextData)+Num($Position>0)

解説
Positionが使用できるように,BLOBをテキストに変換するという発想は同じですが,旧コマンドのBLOB to textではなく,CONVERT FROM TEXTを使用しています。BLOB to textは,一度に変換できるBLOBのサイズが32,000バイトに限定されていますが,新コマンドのほうは全体を一気に変換することができ,その後,ループする必要がないからです。また,新コマンドのほうは,BLOBに収録されたテキストの文字コードが指定できるようになっていますが,前述したように,以前と同じ解釈をするのであれば,これは"Windows-31J"となります[1]。

ひとたびテキストに変換できれば,標準のPositionで位置を調べることができます。このとき,関数に任意の引数アスタリスクを渡していることに注目してください。この引数が渡されたとき,コマンドは半角と全角,ひらがなとカタカナ,大文字と小文字(っ,ゃ,etc)を区別します。つまり,より厳密に文字列を評価します。一方,この引数が渡されなかったときは,半角と全角,ひらがなとカタカナ,大文字と小文字を区別しません。濁音・半濁音と清音は,どちらの用法でも区別します[2]。

Positionから返される値は,文字数で数えた位置なのでこれをバイト単位の位置に換算するためには,その位置までのSubstringをBLOBに変換し,BLOB sizeで調べます。みつかった場合,その位置はサイズ+1,みつからなかった場合,その位置は0です。

[1] "Shift_JIS"とすることもできますが,その場合,Macでは,拡張文字が処理できません。Shift_JISは,機種に依存しない,各社共通の文字だけが登録されているからです。Windowsでは,Shift_JISとWindows-31Jが同じコードページ932を指します。なお,4Dは2003までMacでx-Mac-Japaneseを採用していました。その当時の変換テーブルを再現したいのであれば,"x-Mac-Japanese"でコマンドを呼び出すこともできます。

[2] データベース設定で「現在のデータ言語」が日本語に設定されていることが必要です。そうでない場合,濁音・半濁音と清音は区別されません。その場合,「㍿」と「株式会社」,❶と1など,記号化された単語と文字列も比較演算で同等とみなされます。データベース設定には「旧バージョン互換の文字列比較を使用する」という項目もあります。これは,有効にされている場合,「あー」などの長音を「ああ」と同一視しないという設定です。基本的に,デフォルトの値(データ言語=日本語,旧バージョン互換=有効)がもっとも自然な設定です。

ポイント
BLOBからテキストに変換するときはCONVERT FROM TEXT。
エンコーデングには"windows-31j"を指定してする。
Position(アスタリスク付き)で文字列の位置を調べる。
BLOB sizeでバイト位置に換算する。

STUDY #2
$countLines=Lines($srcText)
目的:テキストの行数を数える

オリジナルコード
C_LONGINT($0;$Lines)
C_TEXT($1;$Text)
$Text:=$1
// -------------------------------------------------------------------------------
// 最後が改行でなければ改行を加える
If (Substring($Text;Length($Text);1)#Char(13))
$Text:=$Text+Char(13)
End if
// 行数を数える
$Lines:=Length($Text)-Length(Replace string($Text;Char(13);""))
// -------------------------------------------------------------------------------
$0:=$Lines

概要
改行コード(復帰/キャリッジリターン)が含まれたテキストの行数を調べるメソッドです。画面または印刷用紙に文章を整形して出力する必要のある業務アプリケーションなどでは日常的な処理かもしれません。ここでは,改行コードを空の文字列で置換し,元のテキストとのサイズの差で行数を割り出すというやり方を採用しています。

変換の影響
Substring,Length,Char,Replace stringなどの文字列関数は,すべてUnicodeモードの新しい振る舞いに切り替わります。つまり,文字コードはASCII/Shift_JISではなく,Unicodeの番号,文字の位置や長さはバイト数ではなく,文字数単位となります。大々的な見直しが必要かと思いきや,実際に試してみると,何の問題もなくメソッドが期待されたとおりに動作します。理由は,あるコマンドから返された値がそのまま別のコマンドに渡されているため,ロジックが全体として合致しているからです。

このように,文字列コマンドから受け取った値を直接,別の文字列コマンドに渡している限り,文字コードの違いがロジックを崩すことはありません。問題が発生するのは,コマンドから返された値ではなく,固定値(1, 2, 128などのリテラル値)がコマンドに渡されているような場合に限られいます。この例では,Char(13),および最初のSubstring(...;...;1)で参照されている"1"がそれに相当しますが,改行コードはUnicodeでもASCIIと同じ13 (U+000D)であり,そのサイズはASCIIのバイト数/Unicodeの文字数がともに1なので,特に問題にはなりませんでした。Unicodeで127(0x7F)以下の文字や制御文字は,ASCIIと完全に同一だからです[1]。もし,これが全角文字であり,Char(33440)とか,Substring(...;...;2)などであったならば,コードの書き換えが必要となるところでした。いずれにしても,書き換えは必要ありません。

文字列コマンドから受け取った値を直接,別の文字列コマンドに渡している限り,そのままUnicodeに移行できるという原則は,全角文字にも適用されます。つまり,Char(33440),これはShift_JISの"あ"ですが,これはUnicodeモードでは違う文字を指します。また,2,これはその文字のサイズ(Length)ですが,これもUnicodeモードでは間違った値になります。対照的に,Ascii("あ")とか,Length("あ")のように値を関数で評価しているコードは,4D 2004ではShift_JISの値,v12ではUnicodeの値がコマンドから返されるので,その値はそのまま別の文字列コマンドに渡すことができ,特にメソッドを書き換える必要がないということです。

[1] 日本ではバックスラッシュ記号(0x5C)に通貨の円記号を割り当てているなど,若干の違いが存在します。

ポイント
文字の番号,位置,サイズは,関数からの返り値をそのまま使用すれば良い。
127(0x7F)以下の文字や制御文字は,Unicodeモードでも変わらない。

STUDY #3
$Result:=IsNumberString ($Target;$Option)
目的: 数字だけの文字列かどうかを判断する

オリジナルコード
$Result:=True
// 小数点がある場合
If ($AcceptDecimalPoint=True) & (Position(".";$String)>0)
// 2つ以上あってはいけない
If (Length($String)-Length(Replace string($String;".";""))>1)
$Result:=False
Else
$String:=Replace string($String;".";"")
End if
End if
$n:=Length($String)
For ($i;1;$n)
$ASCII:=Character code(Substring($String;$i;1))
If ($ASCII<48) | ($ASCII>57)
$Result:=False
End if
End for
// --------------------------------------------------------------------------------
$0:=$Result

概要
4Dのユーザーインタフェース(フォームオブジェクト)には,日付,時間,数値の入力に特化されたものが用意されていますが,さまざまな理由により,オブジェクトには汎用的な文字タイプを使用し,アプリケーション側で入力値を処理する,といった手法もそれなりに有用です。このメソッドは,そうした入力エリアにタイプされた文字列が有効な数値であるかどうかを判別するためのものです。

変換の影響
ひとつ前のSTUDY #2と同じように,文字列コマンド全般は新しいモードに切り替わりますが,128より上位の文字には言及していないため,書き換えは必要ありません。

書き換え例
If ($AcceptDecimalPoint=True)
$0:=Match regex("-?\\p{Nd}+\\.?\\p{Nd}+";$Target)
Else
$0:=Match regex("-?\\p{Nd}+";$Target)
End if

解説
オリジナルのままでも問題はないのですが,良い機会なので,これを正規表現と呼ばれるプログラムで書き換えてみましょう。以前のように,ループの中で一文字ずつ解析し,何段階かの条件分岐を記述する必要がなくなり,事実上,一行にメソッドを短縮することができました。

正規表現になじみがないと,いったい何故,これだけのコードで本来の処理(数値だけの文字列かどうか,小数点は頭および末尾以外の場所に0個あるいは1個だけ許容する,etc)がループなしで実現するのか不思議に感じるかもしれません。正規表現(Match regex)は,最初の敷居が少し高いのが難点ですが,文字列解析のコードをこの上なくシンプルにできるたいへん便利で強力なコマンドです。

今回の例では,"-?\\p{Nd}+\\.?\\p{Nd}+"という正規表現(合致パターン)を使用しました。一見,宇宙語のような文字列ですが,各部分に分解してみれば,以外にシンプルな構造であることが分かります。

-?
これは-(マイナス記号)が0個あるいは1個,という意味です。文字の"-"に続く"?"は「あれば欲しいけど,ある?」という控え目な要件を指しています。もし,"?"がなければ,「この文字は必須です」という話になります。

\\p{Nd}+
これは数字を1個以上,という意味です。10万を超えるUnicodeの文字は,それぞれ個別に文字の性質を説明するプロパティが定義されています。たとえば,0, 1, 2などの数字には,N(数字全般),Nd(Number decimal,10進数の数字)というふたつのプロパティが割り当てられています。ちなみに1(全角)や❶は,N(数字全般),No(Number other,その他の数字)というプロパティであり,標準の数字とは区別されています。\\p{Nd}は,Ndというプロパティを有する文字,という意味で,それに続く"+"は1文字以上,できるだけ多数,という要件を指しています。バックスラッシュ+pは,そのプロパティを有する,という意味です。バックスラッシュ+Pは,その反対,つまりそのプロパティを有さない,という意味になります。

\\.?
これは前出のマイナス記号と同じで,小数点が0個あるいは1個,という意味ですが,正規表現でピリオドは本来,ワイルドカード(改行以外の文字)を指す特殊な記号なので,これを文字どおりの小数点として解釈させるために,バックスラッシュで特殊な意味合いをキャンセル(エスケープしています)。正規表現としては,バックスラッシュはひとつでじゅうぶんなのですが,今度は4Dのメソッドエディターが単独のバックスラッシュを特別な意味に解釈してしまうので,ふたつめのバックスラッシュが必要となっています。

Match regexコマンドは,渡される引数の組み合わせにより,6とおりの用法がありますが,今回,使用したのは引数をふたつ(条件と文字列)だけ指定するもっとも簡単な使い方で,「文字列が全体として(つまり冒頭から末尾まで評価して)この条件に適うか」を調べるものでした。さらに高度な使い方としては,文字の途中で条件を満たす箇所をみつけ,その位置と長さを取得するものもあります。こちらのほうは,文字列の抽出などで威力を発揮します。

今回の正規表現は,冒頭のマイナス(なくても可),数字,途中の小数点を条件としたものでした。正規表現を洗練してゆけば,桁区切りや,全角の可否,指数など,さらに条件を広げたり狭めたりすることもできます。いずれにしても,書き換えるのはコマンドに渡されるパターン定義の文字列だけであり,ループを増やしたり,条件分岐を再構成する必要はないのが正規表現のよいところです。

さらにいえば,正規表現は多数のプログラミング言語で採用されており,いろいろなタイプの文字列に合致するパターン定義の資料を書籍やインターネットなどから容易に入手することができ,4D言語でわざわざ編み出さなくても良いという点も注目に値します。正規表現は,知らなくても困ることはありませんが,知っていれば非常に楽ができるテクニックなのです。

ポイント
Match regexを使用すれば,文字列の解析や抽出がもっとシンプルになる。

STUDY #4
$New:=ReplaceString ($Old;$From;$To;$CaseSensitive)
目的:文字列を変換する 1バイトと2バイトは区別する
$CaseSensitiveがTrueの時は、大文字と小文字も区別する

オリジナルコード
C_LONGINT($i;$n;$Length)
$New:=""
$n:=Length($Old)
$Length:=Length($From)
For ($i;1;$n)
If ($CaseSensitive=False)
//【警告】Unicodeでは,半角も全角もLengthは1なので,それで判別することはできない
If (Substring($Old;$i;$Length)=$From) & (Length(Substring($Old;$i;$Length))=$Length)
$New:=$New+$To
$i:=$i+$Length-1
Else
$New:=$New+Substring($Old;$i;1)
End if
Else
If (CompareString (Substring($Old;$i;$Length);$From;True)=True)
$New:=$New+$To
$i:=$i+$Length
Else
$New:=$New+Substring($Old;$i;1)
End if
End if
End for
// -------------------------------------------------------------------------------
$0:=$New

概要
文字列を置換(変換)する手段としては,標準コマンドのReplace stringがありますが,こちらは4Dの文字列比較で同等とみなされた文字列を置換してゆくので,半角と全角が区別されることはなく,また大文字と小文字(っ,ぁ,etc)も同じように変換されてしまうので,より厳格な文字列置換には不向きです。そのため,このメソッドのように,自前の変換ルーチンが作られることがあります。

このメソッドでは,まず文字列の等価性(対照)を標準の比較演算子(=記号)で実施し,合わせてLengthから返される文字列で半角と全角を区別しています。"あ"="ア"ですが,Length("あ")#Length(" ア")というわけです。鋭い読者は気づかれたと思いますが,Length("ガ")=Length("ガ")というパターンがあることは見過ごされています。いずれにしても,全角と半角を判別にはLengthを使用しているのが要点です。

メソッドは,さらに一段階上の文字列比較モードも有しており,これには,標準の比較演算子ではなく,自前の比較ルーチンCompareStringを呼び出しています。このメソッドには,文字列を1バイトずつ抽出し,そのアスキーコード値を比較するコードが記述されています。

変換の影響
Unicodeモードでは,半角文字も全角文字も,ともにLength=1となるので,それを手がかりに半角と全角を区別することはできません。つまり,このメソッドは書き換えが必要です。

書き換え例(抜粋)
$Substring:=Substring($Old;$i;$Length)
If ($Substring=$From) & \
((Match regex("[:East Asian Width=Halfwidth:]+";$Substring)=\
Match regex("[:East Asian Width=Halfwidth:]+";$From)) & \
(Match regex("[:East Asian Width=Fullwidth:]+";$Substring)=\
Match regex("[:East Asian Width=Fullwidth:]+";$From)))
//元のコード
//If (Substring($Old;$i;$Length)=$From) & (Length(Substring($Old;$i;$Length))=$Length)

解説
Lengthで全角・半角を判別する代わりに,正規表現で半角かどうかを判断するようにしました。ここでもMatch regexが文字列の解析に一役買っています。Unicodeの半角カタカナは,文字コード一覧のU+FF61からU+FF91が割り当てられていますので,一文字単位の評価であれば,その値で判別できないこともありませんが,長い文字列,さらには濁音半濁音にも対応しようとなると,かなり複雑なコードを書かなければなりません。正規表現を使用しれば,非常にシンプルなコードにすることができ,文字単位のループ処理を実行しなくても済みます。

[:East Asian Width=Halfwidth:]+
STUDY #4で登場したプロパティ(N,Nd)は有無どちらかの状態しかありませんでしたが,East Asian Widthプロパティのように,Wide(全角),Halfwidth(半角)など,いくつかの値を取り得るプロパティもUnicodeには存在します。そのようなプロパティの値で文字列を照合するには,\\p{East Asian Width=Halfwidth},または上記のような正規表現を記述します。どちらの書き方であっても意味は同じです。

漢字,全角ひらがな,全角カタカナはWide,全角のアルファベットはFullwidth,半角アルファベットはNarrowというプロパティ値が割り当てられています。したがって,Halfwidth(かWide)とFullwidth(かNarrow)のプロパティ値が両方とも一致しなければ,文字列比較演算子では同等とされていても,一方が全角で他方が半角だということが分かります。それぞれのパターンでMatch regexを呼び出しているのは,半角文字にはカタカナと英数があり得るからです。

なお,この例では,オリジナルコードの中でUnicodeに対応していない箇所,つまり全角と半角の判別をするロジックに着目し,その部分を修正するには正規表現が効的であるという点を際立たせるために紹介しました。「文字列を変換する/1バイトと2バイトは区別する/大文字と小文字も区別する」ことが目的であれば,もっと簡単なメソッドに書き換えることもできました。

書き換え例
If ($CaseSensitive)
$0:=Replace string($Old;$From;$To;*)
End if

Replace stringに任意の引数(アスタリスク)が渡されていることに注目してください。この引数が渡されたとき,コマンドは半角と全角,ひらがなとカタカナ,大文字と小文字(っ,ゃ,etc)を区別します。この引数が渡されなかったときは,半角と全角,ひらがなとカタカナ,大文字と小文字を区別しません。濁音・半濁音と清音は,どちらの用法でも区別します。つまり,STUDY #1で登場したPositionとまったく同じ仕様です。

このメソッドの場合,デフォルトのモード(半角は区別するが,ひらがなとカタカナ,大文字と小文字は区別しない。)では,正規表現を使用して文字列を解析し,厳格モードでは,ループそのものを実施せず単純にReplace stringをアスタリスクで呼び出せば良いということになります。

ポイント
Replace string(アスタリスク付き)で文字列を置換する。
Lengthで半角文字は判別できない。
半角文字は正規表現で判別できる。

STUDY #5
$Result:=CompareString ($Text1;$Text2;True)
目的:文字列を比較する
$CaseSensitiveがTrueの時は、大文字と小文字も区別する

オリジナルコード
If (Length($Text1)=Length($Text2))
$n:=Length($Text1)
For ($i;1;$n)
If ($CaseSensitive=True) | ((Character code($Text1[[$i]])=Character code("@")) | (Character code($Text2[[$i]])=Character code("@")))
If (Character code($Text1[[$i]])#Character code($Text2[[$i]]))
$Result:=False
$i:=$n+1
End if
Else
If ($Text1[[$i]]#$Text2[[$i]])
$Result:=False
$i:=$n+1
End if
End if
End for
Else
$Result:=False
End if
// ----------------------------------------------------------------------
$0:=$Result

概要
今回,取り上げる最後の例は,先程のメソッドからもサブルーチンとして呼び出されていた自前の文字列比較メソッドです。通常,文字列の等価性は比較演算子の=/#で判別します。検索(クエリ)でもおなじみのこの演算子は,バイナリレベルで完全に一致する文字列同士はもちろんのこと,日常言語的に同等とみなされるべき(主観的ですね)文字列も「等価」と判断します[1]。またワイルドカード記号(半角@)も考慮します。

そのような「緩い」判断基準は,検索エンジンの振る舞いとして便利(半角で全角がヒットし,ひらがなでカタカナがヒットする)なものですが,ある種のデータは,そのように処理されては困ることもあるかもしれません。そのようなわけで,上記のメソッドでは,文字列を一文字ずつループ(n文字目を抽出するために$Text[[n]]表記を使用)し,その文字コード(旧称Ascii,現Character code)を比較しています。また,ワイルドカードに惑わされないための処置も施されています。

[1] 4D SQLは,外部DBからのアクセスが想定されていることもあり,クエリとは別の文字列比較ルールが持てるようになっています。データベース設定,あるいはSET DATABASE PARAMETERで「文字種を区別した文字列比較」を有効にすることができ,実際,これがデフォルトの設定です。文字種を区別した文字列比較」とは,PositionやReplace stringにアスタリスクが渡されたときと同じ基準で,文字コード単位で一致する文字列だけを等価をみなします。便宜上,Case Sensitive(大文字と小文字を区別する)と表現されることもありますが,実際にはもっと厳格な文字列比較の基準です。

変換の影響
$Text[[n]]表記は,Unicodeモードでも引き続き有効です。もっとも,返されるのは,n番目のバイトではなく,n番目の文字ですが,いずれにしてもその総数はLengthと同じであり,ループは問題なく実行することができます。Character codeを比較するという手法も引き続き有効です。つまり,書き換えは必要ありません。

書き換え例
If ($CaseSensitive)
$0:=(Length($Text1)=Length($Text2)) & (Position($Text2;$Text1;*)=1)
Else
$0:=(Length($Text1)=Length($Text2)) & (Position($Text2;$Text1)=1)
End if

解説
すでに取り上げたように,コードレベルでの同等性は,自前でループ処理を実行して文字列を解析するまでもなく,Position関数にアスタリスクを渡すことで事足ります。つまり,みつかった位置が1であり,文字列のサイズが同じなのであれば,両者は同じ文字列であるので,上記のようにPosiitioとLengthだけで書き換えることができます。(ドキュメントに記載されているとおり,Positionはもともとワイルドカードの影響を受けません。)このように,コマンドの特性を活用すれば,たいへんシンプルなコードを記述することができます。

ポイント
PositionとLengthと合わせて使用すれば文字列の完全一致が判別できる。