※追記(2014/02/212015/03/24)
最新のHSPには文字列置換のための命令 strrep が追加されています。
実際に使う場合はこれを利用してください。


第三回:文字列を置換してみよう

開発とかの関係で置換の実験をしてみたので、記憶が新しいうちに書くことにしました。
今書かないと、記憶が飛びそうなんで……


疑問1:置換とは?

おそらく、この疑問を持つ人は少ないでしょう。
スクリプトエディタに優秀な置換機能がついているので、その恩恵を受けている人が多いかと思います。

一応説明しておきますと、文字列の中から特定の単語を抜き出して、それを別の文字列に変更することを、置換といいます。
これは非常に便利なので、利用できるようにしましょう。

疑問2:どうやるの?

方法は、いくらでもあります。
今回はいくつもの方法で、やっていくことにしましょう。


置き換えるところだけ連結し直す

おそらく、もっともシンプルなやり方です。
とりあえず、読み替え法とでも言いましょう。(適当に命名)
こんなものは、ちゃちゃっと作ってしまいます。

#module
#defcfunc replace str p1, str target, str after
	dim	len, 2
		len = strlen(p1), strlen(target)
	
	sdim string, len + 1		// 置換前文字列を格納する変数を用意
	string = p1			// instr するので、いったん変数に移す
	
	i = 0	// リセット
	repeat
		n = instr(string, i, target)
		if ( n < 0 ) { break }
		i += n
		string = strmid(string, 0, i) + after + strmid(string, i + len(1), len(0) - i)
		i += len(1)
	loop
	return string
	
#global

使用例:

	buf = "ABCDEFG ABCDEFG"
	mes replace(buf, "ABC", "あいう")

ずいぶん適当ですが、しっかり置換できているようですね。

この関数がしている置換方法は、以下の通りです。

  1. 検索語を、i から検索する
  2. インデックス( i )を見つけたところまで進める
  3. 変数 string の、
    A : 始めから i まで
    B : 置換語
    C : 検索語を飛ばしたところから最後まで
    を連結して格納する。

しかし、この関数には問題があります。
とにかく、元の文字列が長くなると、極端に遅くなるのです。
原因は、string = strmid + after + strmid の式で、見て分かるとおり、同じ文字列をなんども string に格納しています。
一回だけで十分のはずなのに……。何回もするのは無駄でしかありません。
さて、次は、すべての文字を一回づつコピーしていきます。


ちょっと変えてコピーする

やり方を説明していきましょう。
基本的に読み替え法と同じですが、今回は作業バッファが必要です。
※作業バッファとは、読んで字の如く「作業の為に使う領域」です。

文字列の先頭から、instr() 関数で検索し、見つけた場所までをいったん、作業バッファにコピーします。
次に、作業バッファに置換語を付け加えます。
その後、元の文字列の「検索語を飛ばしたところ」から、次を検索します。
それで、見つかった場所までをコピー。あとは繰り返しです。
今回もちゃちゃっと組んじゃいましょう。

#module
#defcfunc replace str p1, str target, str after
	dim	len, 2
		len = strlen(p1), strlen(target)
	
	sdim string, len + 1		// 置換前文字列を格納する変数を用意
	string = p1			// instr するので、いったん変数に移す
	sdim buf, len + 1		// 作業バッファ
	
	i = 0		// リセット
	wrote = 0
	repeat
		n = instr(string, i, target)
		
		memexpand buf, wrote + n + 16	// 拡張する。
		
		if ( n < 0 ) {
			// 即終了ではなく、元の文字列の残りをコピーする
			// そうしないと、後ろが切れることになります
			poke buf, wrote, strmid(string, i, len(0) - i)
			break
		}
		poke buf, wrote, strmid(string, i, n) + after
		i += n + len(1)
	loop
	return buf	// 作業バッファを返す
	
#global

特に説明すべきことはありませんが、一つだけ。
この関数は、文字列を後から付けくわえる時、+= 演算子ではなく、poke 命令を使っています。
この命令は、+= のように自動的にバッファを確保してくれない代わりに、より早く動作します。
ちなみに、バッファを確保するのには memexpand 命令を用います。
本当は、確保し直さなくても大丈夫なときにするんですけどね。この方法。


消して、書き加える

さっきの方法でも、普通なら十分な程度の性能でした。
しかし、あれでもまだ「無駄」はあるのでしょう?。
そう、関係ない部分をコピーする必要は無いはずです!!

今回はわかりやすい題にもあるとおり、検索語を「消して」、置換語を「書き加え」るという方法で置換しようと思います。

とりあえず、今回の方法には文字列を挿入する命令と、文字列を削除する命令が必要です。
HSP3.1 にはないので、マクロを使いましょう。

// 文字列操作マクロ
#define global StrDel(%1, %2 = 0, %3 = 0) \
	memcpy (%1), (%1), strlen((%1)) - ((%2) + (%3)), (%2), (%2) + (%3) :\
	memset (%1), 0, (%3), (%2) + (%3)
#define global StrInsert(%1, %2 = "", %3 = 0) \
	_StrInsert_len@ = strlen(%2) :\
	sdim _StrInsert_temp@, (_StrInsert_len@ + 2) : _StrInsert_temp@ = (%2) :\
	memexpand (%1), strlen((%1)) + _StrInsert_len@ + 1 :\
	memcpy (%1), (%1), strlen(%1) - (%3), (%3) + _StrInsert_len@, (%3) :\
	memcpy (%1), _StrInsert_temp@, _StrInsert_len@, %3, 0

StrDel  マクロ:消す部分を、その後に続く文字列を前にずらして上書きしている。
StrInsert マクロ:挿入する部分を広げて、空いたところにに文字列をコピーしている。
この二つのマクロを使って置換します。
#命令にした方がいいかもしれない。

#module

// 文字列操作マクロ
#define StrDel(%1,%2=0,%3=0) memcpy %1,%1,strlen(%1)-((%2)+(%3)),(%2),(%2)+(%3):memset %1,0,%3,(%2)+(%3)
#define StrInsert(%1,%2="",%3=0) _StrInsert_len@ = strlen(%2):\
	sdim _StrInsert_temp@,_StrInsert_len@+2:_StrInsert_temp@=%2:\
	memexpand %1,strlen(%1)+_StrInsert_len@+1:\
	memcpy %1,%1,strlen(%1)-(%3),(%3)+_StrInsert_len@,%3:\
	memcpy %1,_StrInsert_temp@,_StrInsert_len@,%3,0

#defcfunc replace str p1, str target, str after
	dim	len, 2
		len = strlen(target), strlen(after)
	
	sdim string, len + 1
	string = p1
	
	i = 0	// リセット
	repeat
		n = instr(string, i, target)
		if ( n < 0 ) { break }
		StrDel    string, i + n, len(0)		// 削除
		StrInsert string, after, i + n		// 挿入
		i += n + len(1)
	loop
	return string
	
#global

とりあえず作りました。とってもシンプルですね。


上書きする

さて、次に作る関数には、重大な欠点があります。
検索語と置換語の長さが同じでないと、使えません
どんなものか、作ってみましょう。

#module
#defcfunc replace str p1, str target, str p3
	
	len = strlen(p3)
	
	if ( strlen(target) != len ) { return p1 }
	
	sdim string, strlen(p1) + 1
	string = p1
	sdim after, len + 1
	after  = p3
	
	i = 0	// リセット
	repeat
		n = instr(string, i, target)
		if ( n < 0 ) { break }
		
		memcpy string, after, len, i + n, 0
		i += n + len
	loop
	return string
	
#global

とりあえず実行してみたところ、今までのものよりずいぶん速く置換できます。
memcpy は速い、と覚えておいてください。
( memcpy 命令は、メモリに直接書き込んでいるので、高速です。)
しかし、検索語と置換語が同じ長さなんて、たまにしか起こらないので、この関数は使い道が無いわけです。
関数というのは、もっと汎用的であるべきでしょう。
というわけで、次で克服してみます。


長さを調整した上で上書きする

今回も、この前のマクロを使います。
やり方はこうです。
検索語と置換語の長さを調べ、
検索語の方が長ければ、StrDel で、不要な部分を削り、
置換語の方が長ければ、StrInsert で、書き込める領域をのばします。
そうすれば、memcpy を使って上書きすることが可能です。

#module
#defcfunc replace str p1, str target, str p3
	dim len, 2
	dim num, 2
	
	len = strlen(target), strlen(p3)
	if ( len(0) > len(1) ) {
		num = len(0) - len(1), 1
	} else {
		num = len(1) - len(0), 2
		sdim tmp, num + 1		// 挿入する文字列を作成
		repeat num
			poke tmp, cnt, 0x20	// 半角スペース
		loop
	}
	
	sdim string, strlen(p1) + 1
		string = p1
	sdim after, len(1) + 1
		after  = p3
	
	i = 0	// リセット
	repeat
		n = instr(string, i, target)
		if ( n < 0 ) { break }
		
		if ( num(1) == 1 ) {				// 1 なら Del
			StrDel string, i + n, num
		} else : if ( num(1) == 2 ) {		// 2 なら Insert
			StrInsert string, tmp, i + n
		}
		
		memcpy string, after, len(1), i + n, 0
		i += n + len(1)
	loop
	return string
	
#global

後半がひどくテキトーな気がしますが、ここでお開きです。

では、また次回。