Solidity Stringutilsで文字列操作

Yuya Sugano
16 min readFeb 8, 2019

--

Solidityで便利な文字列比較・操作のライブラリがあるので使ってみました。オブジェクト志向型で文字列をオブジェクトとしてメソッド操作できる言語と異なりSolidityでの文字列はただのバイト列。stringutilsを使用するとSolidityでも文字列をオブジェクトのように扱えます。

Old script

いままでCryptoZombiesなどで出てきた文字列比較の例は、SHA3の関数であるkeccak256関数を利用して文字列のハッシュ値が同じであるかどうかを確認していました。以下のコードはsolidity 0.4.23で書いております。

function compareString(string _comparee, string _comparer) external pure returns (bool) {
if (keccak256(abi.encodePacked(_comparee)) == keccak256(abi.encodePacked(_comparer))) {
return true;
} else {
return false;
}
}

このコントラクトをtruffle developでデプロイして確認してみます。文字列が同じかどうかの比較はできました。

truffle(develop)> StringUtil = StringUtil.at("0x345ca3e014aaf5dca488057592ee47305d9b3e10")...
truffle(develop)> StringUtil.compareString("Lorem","Lorem")
true
truffle(develop)> StringUtil.compareString("Lorem","Ipsum")
false

ところが文字列をスライスしたり、コピーしたりといった関数が与えられているわけではありません。rubyをはじめとし文字列がObject型の言語設計で、組み込みメソッドが最初から定義されているわけではないので当然といえば当然です。

文字列操作について調べていたところstringutilsという便利ライブラリを発見しました。仕組みとしてはsliceという構造体で文字列を表現して、その構造体を操作することで様々な文字列操作を実現しています。

試し方。ライブラリを読み込みます。

import "./strings.sol";contract StringUtil {  using strings for *;
..

基本的な操作としてtoSlice()という関数とtoString()という関数を使用します。toSlice()はstrings.slice型へ変換、toString()はstrings.sliceからstring型へ変換できます。以下は引数をtoSlice()でstrings.slice型へ変更して何も処理せずtoString()でstring型で返す関数の例。

この関数ではなにもしていませんが、以下のようにtoSlice()でstrings.slice型へ変換しておいて処理をしてtoString()で通常のstring型へ戻す、という処理が基本となります。

function nothingString(string _strings) external pure returns (string) {
strings.slice memory nothing = _strings.toSlice();
return nothing.toString();
}

よく使うメソッドを以下確認してみました。

文字列の長さ

string型の文字列に対してtoSlice()を呼んでlen()で文字数を返します。

Getting the character length of a string

var len = "Unicode snowman ☃".toSlice().len(); // 17

Solidityでの関数実装例。

function lengthString(string _strings) external pure returns (uint) {
return _strings.toSlice().len();
}

文字列の分割

デリミタとなる文字をtoSlice()で一旦strings.sliceにする必要があります。split関数でデリミタを指定することでデリミタで分割した前節の文字を取り出し可能です。

Splitting a string around a delimiter

var s = "foo bar baz".toSlice();
var foo = s.split(" ".toSlice());

After the above code executes, s is now "bar baz", and foo is now "foo".

Solidityでの関数実装例。splitString関数はデリミタも引数として渡せるようにしてみました。

function sliceString(string _strings) external pure returns (string) {
strings.slice memory slicee = _strings.toSlice();
return slicee.split(" ".toSlice()).toString();
}
function splitString(string _strings, string _delimiter) external pure returns (string) {
strings.slice memory slicee = _strings.toSlice();
strings.slice memory delim = _delimiter.toSlice();
return slicee.split(delim).toString();
}

文字列を分割して配列化

これもよくあります。デリミタで文字列を分割し、分割したそれぞれの文字を配列に格納したいケースです。

Splitting a string into an array

var s = "www.google.com".toSlice();
var delim = ".".toSlice();
var parts = new string[](s.count(delim) + 1);
for(uint i = 0; i < parts.length; i++) {
parts[i] = s.split(delim).toString();
}

Solidityでの関数実装例。count関数は指定したstrings.sliceの出現回数を返します。ここではデリミタの出現回数から割り当てに必要な配列数を計算しています。split関数で個々の分割した要素を配列に順に格納します。

function stringToArray(string _strings) external pure returns (string[]) {
strings.slice memory slicee = _strings.toSlice();
strings.slice memory delim = ".".toSlice();
string[] memory parts = new string[](slicee.count(delim) + 1);
for (uint i = 0; i < parts.length; i++) {
parts[i] = slicee.split(delim).toString();
}
return parts;
}

文字列の結合

結合するだけでもまずはtoSlice()を呼んで結合前に個々の文字列をstrings.slice型に変換します。

Concatenating strings

var s = "abc".toSlice().concat("def".toSlice()); // "abcdef"

Solidityでの関数実装例。

function concatenateString(string _stringa, string _stringb) external pure returns (string) {
return _stringa.toSlice().concat(_stringb.toSlice());
}

なぜかtoString()を呼ぶ必要がありませんでした。concat関数の返り値がすでにstring型のようです。以下はtruffle developでの実行例でabcとdefを結合してみました。

truffle(develop)> StringUtil = StringUtil.at("0x345ca3e014aaf5dca488057592ee47305d9XXXXX")
truffle(develop)> StringUtil.concatenateString("abc","def")
'abcdef'

joinを利用すれば文字列とstrings.sliceの配列を結合できます。

join(slice self, slice[] parts) internal returns (string)
Joins an array of slices, using self as a delimiter, returning a newly allocated string.
Arguments:
self The delimiter to use.
parts A list of slices to join.
Returns A newly allocated string containing all the slices in parts, joined with self.

文字列比較

stringutilsの文字列比較にはcompareとequalsという2つの文字列比較関数があります。最初に示したような2つの文字列が同じものかどうかを比較し、boolで返すにはequalsを利用します。Solidityでの関数実装例です。

function equalString(string _self, string _other) external pure returns (bool) {
return strings.equals(_self.toSlice(), _other.toSlice());
}

以下はtruffle developでの実行例です。比較文字列同士が同じであればbool型でtrue、異なればfalseを返します。

truffle(develop)> StringUtil = StringUtil.at("0x345ca3e014aaf5dca488057592ee47305d9XXXXX")
truffle(develop)> StringUtil.equalsString("abc","abc")
true
truffle(develop)> StringUtil.equalsString("abc","def")
false

compareはbool型ではなく、数値を返します。比較文字列同士が同じときに0、比較対象が辞書順であれば負数が返ります。以下Solidityでの関数実装例です。

function compareStrings(string _self, string _other) external pure returns (int) {
return strings.compare(_self.toSlice(), _other.toSlice());
}

比較対象文字列の順が辞書通りだと負数が返り、同じだと0が、逆であると正数が返ります。

truffle(develop)> StringUtil = StringUtil.at("0x345ca3e014aaf5dca488057592ee47305d9XXXXX")
truffle(develop)> StringUtil.compareStrings("abc","def")
BigNumber {
s: -1,
e: 75,
c:
[ 136225,
97921831746898,
84677340377,
677934743781,
80187278714238,
68769281245184 ] }
truffle(develop)> StringUtil.compareStrings("abc","abc")
BigNumber { s: 1, e: 0, c: [ 0 ] }
truffle(develop)> StringUtil.compareStrings("def","abc")
BigNumber {
s: 1,
e: 75,
c:
[ 136225,
97921831746898,
84677340377,
677934743781,
80187278714238,
68769281245184 ] }

接頭・接尾語関連

いわゆるプレフィックスとサフィックスの走査や処理の方法です。startsWith関数およびendsWith関数で接頭・接尾に対象の語があるかを、beyond関数およびuntil関数で接頭・接尾語を切り落として文字列を修正できます。

Prefix and suffix matching

var s = "A B C B D".toSlice();
s.startsWith("A".toSlice()); // True
s.endsWith("D".toSlice()); // True
s.startsWith("B".toSlice()); // False

Removing a prefix or suffix

var s = "A B C B D".toSlice();
s.beyond("A ".toSlice()).until(" D".toSlice()); // "B C B"

beyond modifies s to contain the text after its argument; until modifies s to contain the text up to its argument. If the argument isn't found, s is unmodified.

Solidityでの関数実装例です。

function startsWithString(string _strings, string _other) external pure returns (bool) {
strings.slice memory slicee = _strings.toSlice();
return slicee.startsWith(_other.toSlice());
}
function endsWithString(string _strings, string _other) external pure returns (bool) {
strings.slice memory slicee = _strings.toSlice();
return slicee.endsWith(_other.toSlice());
}

truffle developでの実行例。startsWithでは接頭語が対象の文字列である場合にはtrue、そうでない場合にはfalseを返します。endsWithでは接尾語が対応の文字列である場合にはtrue、そうでない場合にはfalseを返します。

truffle(develop)> StringUtil = StringUtil.at("0x345ca3e014aaf5dca488057592ee47305d9XXXXX")
truffle(develop)> StringUtil.startsWithString("A B C B D", "A")
true
truffle(develop)> StringUtil.startsWithString("A B C B D", "B")
false
truffle(develop)> StringUtil.endsWithString("A B C B D", "A")
false
truffle(develop)> StringUtil.endsWithString("A B C B D", "D")
true

beyond関数およびuntil関数の使用例。

function beyondString(string _strings, string _target) external pure returns (string) {
strings.slice memory slicee = _strings.toSlice();
return slicee.beyond(_target.toSlice()).toString();
}
function untilString(string _strings, string _target) external pure returns (string) {
strings.slice memory slicee = _strings.toSlice();
return slicee.until(_target.toSlice()).toString();
}

こちらは実行例です。beyondでは指定した文字列以降を対象文字列から返し、untilでは指定した文字列以前を対象文字列から返します。

truffle(develop)> StringUtil = StringUtil.at("0x345ca3e014aaf5dca488057592ee47305d9XXXXX")
truffle(develop)> StringUtil.beyondString("A B C B D", "A ")
'B C B D'
truffle(develop)> StringUtil.untilString("A B C B D", " D")
'A B C B'

その他にも使用事例や各関数が詳細に掲載されています。

まとめ

  • stringutilsを使用するとSolidityでも文字列をオブジェクトのように扱える
  • 文字列をtoSlice()関数でstrings.slice型へ変換しないと操作が行えない
  • 処理した後はtoString()関数でstring型へ戻すことができる
  • string.sliceはライブラリの中で定義されている構造体である

References

--

--

Yuya Sugano
Yuya Sugano

Written by Yuya Sugano

Cloud Architect and Blockchain Enthusiast, techflare.blog, Vinyl DJ, Backpacker. ブロックチェーン・クラウド(AWS/Azure)関連の記事をパブリッシュ。バックパッカーとしてユーラシア大陸を陸路横断するなど旅が趣味。

No responses yet