IBM i においては、シフト文字という概念が存在します。
全角文字と半角文字が切り替わる際にシフトアウト(X’0E’)・シフトイン(X’0F’)が
それぞれ入るということは、ご存じの方も多いでしょう。
このシフト文字は内部的に各1バイトを使用するため、
たとえばOタイプフィールドで全角文字を満々に詰めた状態で
SQLを使って更新しようとすると、シフト文字の分だけ桁あふれが発生してしまいます。
このようなエラーを回避するには、更新する文字列について、
シフト文字の分も考慮してデータベースの桁数におさまるように調整する必要があります。
<案1>MaxLengthによる制御
上図のように入力項目の各Editの値をSQLのパラメータ等に渡して更新する場合、
そのEditにMaxLengthプロパティを設定して入力桁数を制御することは可能です。
しかし、このプロパティはバイト数ではなく文字数でカウントするため、
Aタイプ(全て半角)のフィールドでしか本領を発揮できません。
また全角と半角が入り混じった文字列では更新後に入るシフト文字も増えるため、
Editのプロパティ側で対処するのは困難です。
※OnExitイベントで「MaxLengthの半分の文字数まででカット」といった処理を入れた場合でも、
文字列によっては桁あふれしてしまいます。
※Delphi/400 V2007以前はMaxLengthをバイト数でカウントしますが、シフト文字までは考慮されません。
<案2>シフト文字を考慮した文字長による制御
そこで考えられる対策としては、
シフト文字を考慮した文字長を取得する制御方法となります。
具体的には、
①文字列の長さがシフト文字を考慮した文字長を超えていたらエラーにする
②文字列の後ろをカットして、桁数に収まる範囲のみを更新する
のいずれかとなります。
まずは以下のロジックを使って、
シフト文字を考慮した文字長(バイト数)を取得します。
※uses節に「System.AnsiStrings」が必要
※バイト数を取得するためにANSIにキャストしているため、UNICODE依存文字が含まれないことが前提
{*******************************************************************************
目的: 文字長取得処理
引数: AText - 該当文字列
戻値: シフト文字を含む文字長
*******************************************************************************}
function GetCharLength(AText: AnsiString): Integer;
var
i: integer;
InDBCS: Boolean;
begin
InDBCS := False;
Result := Length(AText);
// 文字長取得処理
for i := 1 to Length(AText) do
begin
case System.AnsiStrings.ByteType(AText, i) of
mbSingleByte: // ASCII 文字もしくは半角カタカナ
InDBCS := False;
mbLeadByte: // 2バイト文字の1バイト目
if InDBCS = False then
begin
InDBCS := True;
Result := Result + 2;
end;
mbTrailByte: // 2バイト文字の2バイト目
;
end;
end;
end;
①の場合は対象フィールドの桁数と比較し、
桁あふれ(戻り値がフィールドの桁数を超過)していればエラーにして処理中断させます。
②の場合はさらに以下のロジックを使って、あふれる分の後方文字をカットします。
※上記の「GetCharLength」を呼び出しており、2つの関数を組み合わせて使う前提
{*******************************************************************************
目的: 指定長文字列取得処理
引数: AText - 対象文字列
AMaxLength - 取得文字バイト数(シフト文字込)
戻値: バイト数に収まる文字列
*******************************************************************************}
function GetLengthText(AText: AnsiString; AMaxLength: Integer): AnsiString;
begin
if (AMaxLength <= 0) or (GetCharLength(AText) <= AMaxLength) then
Result := AText
else
begin
repeat
AText := Copy(WideString(AText), 1, Length(WideString(AText)) - 1);
until (GetCharLength(AText) <= AMaxLength);
Result := AText;
end;
end;
これらの関数「GetCharLength」「GetLengthText」を適用すれば、
冒頭で示した図のような場合、
①では更新SQLの発行より前にエラーにすることができます。
②では後方をカットした「デルファイ40」までが更新されるようになります。
データモジュール等の共通ユニットに記述して
各画面から呼び出せるようにしておくと汎用性が向上します。
また、カスタムコンポーネントを作成できる場合は
OnExitイベントに相当する箇所で後方の桁あふれ部分をカットすることも可能です。