文字列をPHPのsubstr関数風にSubstringする

役に立つかどうかよくわからないTipsシリーズ

今回は `substring` すなわち「文字列の部分取得」です

`String` には

- `substringToIndex(_:)`
- `substringFromIndex(_:)`
- `substringWithRange(_:)`

といった部分取得を行えるメソッドが用意されていますが

特に `substringWithRange(_:)` の場合は
引数に `Range<Index>` を取ります

`NSString` である場合は `NSRange` を `NSMakeRange()` してやることで
簡単に範囲指定できましたが
この `Range<Index>` は個人的には何だか直感的にわかりづらいというか
なんだか無駄な実装が増えてしまって困りものなのです

そこで、もっとシンプルに文字列の部分取得ができるように
一工夫してみようかと思います

## substringとその仕様

自分は元々PHPerであるので、
PHPの `substr関数` を模倣した仕様で使えると助かるのです

PHPの `substr関数` のドキュメント [PHP: substr - Manual](http://php.net/manual/ja/function.substr.php)  には
こんな風に仕様が書かれています

```php
// 関数定義
string substr ( string $string , int $start [, int $length ] )
```

```php
// 引数 string
入力文字列。最低 1 文字以上を指定しなければなりません。
```
```php
// 引数 start
start が正の場合、返される文字列は、
string の 0 から数えて start番目から始まる文字列となります。
例えば、文字列'abcdef'において位置 0にある文字は、'a'であり、 位置2には'c'があります。

start が負の場合、返される文字列は、
stringの後ろから数えて start番目から始まる文字列となります。

string の長さが start 文字より短い場合は FALSE が返されます。
```
```php
// 引数 length (省略可能)
length が指定され、かつ正である場合、
返される文字列は start (string の長さに依存します) から数えてlength文字数分となります。

length が指定され、かつ負である場合、
string の終端からその文字数分の文字が省略されます
(start が負の場合は、 開始位置を算出したあとで)
もし start が切り出し位置を超える場合、 FALSE が返されます。

length が指定され、かつ 0、NULL、もしくは FALSE であれば、
空の文字が返されます。

length を省略した場合は、 
start の位置から文字列の最後までの部分文字列を返します。
```
```php
// 戻り値
string の一部を返します
失敗した場合に FALSE を返します。あるいは空文字列を返します。
```

基本的にはこのルールを踏襲するのですが
PHPと何もかもすべて同じというわけにも行かないので
少しずつ変えたいと思います

**「FALSEを返します」**
こう書かれた部分は、空文字列が返るように実装します
オプショナルの戻り値にして `nil` を返すということも考えましたが
空文字で返ってきたほうが使う時に使いやすいかと思うのでそういう仕様にします
このあたりは好みで使い分けてもらえればいいかなと

**「length が指定され、かつ 0、NULL、もしくは FALSE であれば、空の文字が返されます」**
PHPと違ってswiftは型に厳しいので、`Int` を渡すところに `false` を渡すことはしません
なので、この仕様はドロップです
代わりに下記のような仕様になるようになります

- `length` が 0 の場合は、負の数を渡されたものとして扱う
- `length` に `nil` を渡すと、返される文字は「`start` から終端まで」とする

この辺りだけPHPの挙動とは微妙に変わると思いますのでご注意を

## 開始位置と終了位置で部分取得する

```swift
extension String {

    /// 文字列の部分取得を行う
    private func substring(startIndex start: Int, endIndex end: Int) -> String {
        let max = self.length - 1
        var s = start, e = end
        if max < 0 || e < s || max < s {
            return ""
        } else if s < 0 {
            s = 0
        } else if e < 0 {
            e = 0
        } else if max < e {
            e = max
        }
        let range = Range(self.startIndex.advancedBy(s)...self.startIndex.advancedBy(e))
        return self.substringWithRange(range)
    }
}
```

最初に挙げたPHPの`substr関数`とは定義が違いますが
`substring(startIndex:endIndex:)` というメソッドを`String`に追加しました

文字列の中から開始位置と終了位置を指定することで
その部分だけを抜き取るメソッドです

```swift
"abcdef".substring(startIndex: 2, endIndex: 4)
```
例えばこうすると
`"abcdef"`のインデックス2の`"c"`から、インデックス4の`"e"`までの
`"cde"`を取得します

メソッドの仕様として
開始位置が終了位置を上回ると、空文字を返すようにしています
これが意外と後々でミソになります

ちなみに
ここに出てくる`self.length`というのは、オリジナルプロパティです
これについては [Stringの文字列長の取得はextensionでラップしておく](http://qiita.com/nakau1/items/4f2f39a6adbaa3ab4e2b) を参照ください

もうひとつちなみに・・・ですが
このメソッドは `private` で定義しています
これは単純に個人的にですが、このメソッド単独で使うシーンがないので隠蔽しました
もちろんこれだけでも充分使えるメソッドなので
`public` で宣言しても差し支えは無いと思います

## 本題のsubstringをする

下準備のメソッドができたので
最初に書いたPHPの `substr関数` を模倣した `substring` メソッドを作成します

```swift
public func substring(location location: Int, length: Int? = nil) -> String {
    return ""
}
```
このように定義しました
(とりあえずエラーにならないように空文字を返してます)

引数のラベル名は、 `substr関数` よりもむしろ `NSRange` に合わせたほうが
iOS開発者には馴染み深いと思い
`start:length:` ではなく、`location:length:` にしています
`length` は `substr関数` と同じく省略可能です

さて、実装をする前に
このメソッドをどう使って何が返ってくるかを整理しておいたほうがいいですね

もういちど [PHP: substr - Manual](http://php.net/manual/ja/function.substr.php) の戻り値の箇所を確認します

```php
<?php
echo substr('abcdef', 1);     // bcdef
echo substr('abcdef', 1, 3);  // bcd
echo substr('abcdef', 0, 4);  // abcd
echo substr('abcdef', 0, 8);  // abcdef
echo substr('abcdef', -1, 1); // f
?>
```
```php
<?php
$rest = substr("abcdef", -1);    // "f" を返す
$rest = substr("abcdef", -2);    // "ef" を返す
$rest = substr("abcdef", -3, 1); // "d" を返す
?>
```
```php
<?php
$rest = substr("abcdef", 0, -1);  // "abcde" を返す
$rest = substr("abcdef", 2, -1);  // "cde" を返す
$rest = substr("abcdef", 4, -4);  // false を返す
$rest = substr("abcdef", -3, -1); // "de" を返す
?>
```

これを、先ほど定義したメソッドを使ってswift的に再現するなら
こんな感じです

```swift
"abcdef".substring(location:  1)             // bcdef
"abcdef".substring(location:  1, length: 3)  // bcd
"abcdef".substring(location:  0, length: 4)  // abcd
"abcdef".substring(location:  0, length: 8)  // abcdef
"abcdef".substring(location: -1, length: 1)  // f

"abcdef".substring(location: -1)             // f
"abcdef".substring(location: -2)             // ef
"abcdef".substring(location: -3, length: 1)  // d

"abcdef".substring(location:  0, length: -1) // abcde
"abcdef".substring(location:  2, length: -1) // cde
"abcdef".substring(location:  4, length: -4) // (false) => 空文字
"abcdef".substring(location: -3, length: -1) // de
```

今からこのコメント通りの戻り値が返るように実装していきます
いくつかのブロックに分けながら説明を書きます

```swift
        let strlen = self.length
        let max = strlen - 1, min = strlen * -1
```

メソッドの仕様に基づくのであれば
`location`　は文字列長の範囲を超えてはいけません

たとえば

```swift
"abc".substring(location: 10)
```
だと開始位置が不明なのでNGですね

この考えから`"abc"`という文字列ならば`-3〜2` が
`"abcdef"`という文字列ならば`-6〜5` が範囲ということになります

次に

```swift
        var s = location
        
        if s < min || max < s {
            return ""
        } else if s < 0 {
            s = strlen + s
        }
```
変数 `s` は開始位置インデックスです
正の数であれば `location` と同値のはずです
ただし負の数であれば、メソッドの仕様に基づいて「後ろから数えた数」です

範囲外の開始位置インデックスであれば
メソッドの仕様に基づき、空文字を返して処理を抜けます

次に

```swift
        let len = length ?? strlen
        var e = 0
        
        if len > 0 {
            e = s + (len - 1)
            e = (e > max) ? max : e
        } else {
            e = max + len
        }
```
変数 `e` は終了位置インデックスです
原則として開始位置から `length` 分の位置が終了位置になりますが
終了位置は範囲を超えることはありえませんから
もし超えたら「終了位置は範囲の最大インデックス」という風にしておきます

`length` が負の値の場合のメソッドの仕様として
> length が指定され、かつ負である場合
> string の終端からその文字数分の文字が省略されます
> (start が負の場合は、 開始位置を算出したあとで)
> もし start が切り出し位置を超える場合、 FALSE が返されます

というものがありました

複雑な仕様に見えますが、
実は最大インデックスに負の数の`length` を足すことで終了位置が取得できます

ここまでで、開始と終了位置がとれました
試しに開始と終了位置を出力してみます

```swift
print("start:\(s), end:\(e)")
```

```swift
"abcdef".substring(location:  1))              // start:1, end:5 // bcdef
"abcdef".substring(location:  1, length: 3))   // start:1, end:3 // bcd
"abcdef".substring(location:  0, length: 4))   // start:0, end:3 // abcd
"abcdef".substring(location:  0, length: 8))   // start:0, end:5 // abcdef
"abcdef".substring(location: -1, length: 1))   // start:5, end:5 // f
"abcdef".substring(location: -1))              // start:5, end:5 // f
"abcdef".substring(location: -2))              // start:4, end:5 // ef
"abcdef".substring(location: -3, length: 1))   // start:3, end:3 // d
"abcdef".substring(location:  0, length: -1))  // start:0, end:4 // abcde
"abcdef".substring(location:  2, length: -1))  // start:2, end:4 // cde
"abcdef".substring(location:  4, length: -4))  // start:4, end:1 //
"abcdef".substring(location: -3, length: -1))  // start:3, end:4 // de
```

渡している引数に対して、見比べてもらえると
おおよそ開始位置と終了位置があっているかと思います

唯一合わないのが下から2番目の`start:4, end:1`というものです
開始位置が終了位置を上回ってしまっています

ここで前項で「ミソ」と言っていたところが効いてきます
開始位置が終了位置を上回ったら空文字を返すようにしたのはこれをするためでした

最後にこのメソッドは、前項で作ったメソッドを使って文字列取得します

```swift
        return self.substring(startIndex: s, endIndex: e)
```

PHPのドキュメントと同じ結果が返されると思います

## おさらい
今回のソースコードです

```swift
extension String {

    /// 文字列長
    public var length: Int { return self.characters.count }
    
    /// 文字列の部分取得を行う
    ///
    ///     "abcdef".substring(location: 1)              // bcdef
    ///     "abcdef".substring(location: 1, length: 3)   // bcd
    ///     "abcdef".substring(location: 0, length: 4)   // abcd
    ///     "abcdef".substring(location: 0, length: 8)   // abcdef
    ///     "abcdef".substring(location: -1, length: 1)  // f
    ///     "abcdef".substring(location: -1)             // f
    ///     "abcdef".substring(location: -2)             // ef
    ///     "abcdef".substring(location: -3, length: 1)  // d
    ///     "abcdef".substring(location: 0, length: -1)  // abcde
    ///     "abcdef".substring(location: 2, length: -1)  // cde
    ///     "abcdef".substring(location: 4, length: -4)  //
    ///     "abcdef".substring(location: -3, length: -1) // de
    /// - parameter location: 開始インデックス
    /// - parameter length: 文字列長
    /// - returns: 部分取得された文字列
    public func substring(location location: Int, length: Int? = nil) -> String {
        let strlen = self.length
        let max = strlen - 1, min = strlen * -1
        
        var s = location
        
        if s < min || max < s {
            return ""
        } else if s < 0 {
            s = strlen + s
        }
        
        let len = length ?? strlen
        var e = 0
        
        if len > 0 {
            e = s + (len - 1)
            e = (e > max) ? max : e
        } else {
            e = max + len
        }
        
        return self.substring(startIndex: s, endIndex: e)
    }
    
    /// 文字列の部分取得を行う
    ///
    ///     "abcdef".substring(startIndex: 2, endIndex: 4) // cde
    /// - parameter startIndex: 開始インデックス
    /// - parameter endIndex: 終了インデックス
    /// - returns: 部分取得された文字列
    private func substring(startIndex start: Int, endIndex end: Int) -> String {
        let max = self.length - 1
        var s = start, e = end
        if max < 0 || e < s || max < s {
            return ""
        } else if s < 0 {
            s = 0
        } else if e < 0 {
            e = 0
        } else if max < e {
            e = max
        }
        let range = Range(self.startIndex.advancedBy(s)...self.startIndex.advancedBy(e))
        return self.substringWithRange(range)
    }
}
```