android.text.TextUtils のメソッドを調べる

# 概要
勉強のため、[android.text.TextUtils](https://developer.android.com/reference/android/text/TextUtils.html) について、どんなメソッドがあるのかを調べてみました。

## 何ができるクラスなのか？
文字列処理で便利なメソッドが揃っています。null の際の比較を柔軟にできるメソッドがいくつかある(equals, isEmpty 等)のが特徴的です。

## なぜ commons-lang3 の [StringUtils](https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html) があるのにそちらを使わないのか？
あくまで個人の見解ですが、Android アプリには 64kのメソッド数制限があるので、
メソッド数が多くなる commons-lang3 を気軽に使えないという事情があるのではないかと思います。

なお、皆無というわけではなく、commons-lang3 を採用している Android アプリもあるようです。

参考： https://docs.google.com/presentation/d/11pCsJmahMIP9Gldt39nPnd0XX4lrvycpuNibPmDQ-SM/pub?start=false&loop=false&delayms=3000#slide=id.p6

## 注意点
このクラスは Android SDK のものであるため、これを用いたクラスの JUnit テストコードを書く際は [Robolectric](http://robolectric.org/) が必要となります。

```groovy:app/build.gradle
dependencies {
    // dependencies...

    testCompile 'junit:junit:4.12'
    testCompile "org.robolectric:robolectric:3.4.2"
}
```

```java
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public class UsingTextUtilsClassTest {
// ...
```


## 参照したコード
今回は GitHub リポジトリにあった 8.0.0_RC4 のコードを参照しました。

https://github.com/android/platform_frameworks_base/blob/android-8.0.0_r4/core/java/android/text/TextUtils.java

## 実行環境

| 項目 | 値
|:---|:---
| Android Studio | 2.3.3
| compileSdkVersion | 25
| targetSdkVersion  | 25
| Java | 1.8.0_131


----

# メソッド

## static CharSequence concat(CharSequence... text)
渡された複数の文字列を連結した結果を返します。

### 例
以下の場合

```java
TextUtils.concat("a", "b", "c")
```

"abc" という文字列が得られます。

可変長引数なので1つも引数を渡さないで呼び出すことが可能です。その場合は空文字列が返ります。

```java
TextUtils.concat();
```

まあ、何の意味もないので明示的にこのような呼び出しをすることはないでしょう。

## static boolean equals(CharSequence a, CharSequence b)
渡された2つの文字列が等しいなら true を返します。このメソッドを使うと片方が null なら false を返すだけで、「うっかり null の方から equals 呼び出して NullPointerException 出しちゃった！」という Java あるあるを回避できます。
また、よくある Java のイディオムで、絶対 null にならない方(主に定数)から equals を呼び出す([ヨーダ記法](https://ja.wikipedia.org/wiki/%E3%83%A8%E3%83%BC%E3%83%80%E8%A8%98%E6%B3%95)というらしいです)際にありがちな可読性・美しさの低下を避けることもできます。

ちなみにこのメソッド、単にnullチェックしてから equals を呼んでいるだけかと思ったら、ちゃんとしっかり比較処理を作っているようです。

```java
    public static boolean equals(CharSequence a, CharSequence b) {
        if (a == b) return true;
        int length;
        if (a != null && b != null && (length = a.length()) == b.length()) {
            if (a instanceof String && b instanceof String) {
                return a.equals(b);
            } else {
                for (int i = 0; i < length; i++) {
                    if (a.charAt(i) != b.charAt(i)) return false;
                }
                return true;
            }
        }
        return false;
    }
```

## expandTemplate(CharSequence template, CharSequence... values)
毎回 Formatter オブジェクトを生成する String#format を使いがちなところで、このメソッドを使うとよさそうに思えます。

### 例
第1引数の文字列で ```^番号``` の形式でテンプレートを指定し、続く可変長引数で入れ込む値を指定します。

```java
TextUtils.expandTemplate("^1 is ^2.", "Ice cream", "cool"); // Ice cream is cool.
```

テンプレート内は必ずしも1から順に置く必要はありません。

```java
TextUtils.expandTemplate("^2 is ^1.", "Ice cream", "cool"); // cool is Ice cream.
```

ただ、ややこしくなるので通常は順序通りに指定した方がよいかと思います。

同じ番号を複数回用いることもできます。

```Java
TextUtils.expandTemplate("^1 is ^1.", "Ice cream", "cool");
```

第2引数は可変長引数であるため、何も渡さないことができます。その場合は第1引数の文字列がそのまま返されます。

```java
TextUtils.expandTemplate("Ice cream is cool."));
```

この場合、第1引数の "Ice cream is cool." が返ってきます。何の意味もないのでやる必要はないでしょう。

### 注意

#### 個数
渡した個数が2なのにテンプレートで3以上の数字を使うと例外 [IllegalArgumentException](https://developer.android.com/reference/java/lang/IllegalArgumentException.html) が発生します。

```java
TextUtils.expandTemplate("^3 is ^1.", "Ice cream", "cool");
```

#### 9個まで
可変長引数ですが、サポートしているのは9個までで、10個以上渡すと [IllegalArgumentException](https://developer.android.com/reference/java/lang/IllegalArgumentException.html) が発生します。

## static int getLayoutDirectionFromLocale(Locale locale)
APIレベル17 で追加されました。設定言語ごとの文字列の方向を int 定数で返すメソッドです。

| 方向 | 定数名 | 定数値
|:---|:---|:---
| 左から右 | LAYOUT_DIRECTION_LTR | 0 (0x00000000)
| 右から左 | LAYOUT_DIRECTION_RTL | 1 (0x00000001)

## static int getTrimmedLength(CharSequence s) 
Trim された状態での文字列の長さを返してくれるメソッドです。

### 例

```java
TextUtils.getTrimmedLength("a  ");      // 1
TextUtils.getTrimmedLength("a  b");     // 4
TextUtils.getTrimmedLength("  a  b  "); // 4
TextUtils.getTrimmedLength("");         // 0
```

### 注意
null を渡すと NullPointerException が発生します。

```java
TextUtils.getTrimmedLength(null);       // NullPointerException
```

## static String htmlEncode(String s)
HTML で困る &lt や &gt を &amp;lt や &amp;gt に置換してくれるメソッドです。

### 例

```java
TextUtils.htmlEncode("<a>"); // "&lt;a&gt;"
TextUtils.htmlEncode("");    // ""
```

### 注意
null を渡すと NullPointerException が発生します。

```java
assertEquals(null, TextUtils.htmlEncode(null));
```

## static int indexOf(CharSequence s, char ch)
文字列から対象の1文字が含まれる位置を返すメソッドで、いくつかオーバーロードされています。

```java
static int indexOf(CharSequence s, char ch, int start, int end)
static int indexOf(CharSequence s, CharSequence needle, int start, int end)
static int indexOf(CharSequence s, CharSequence needle, int start)
static int indexOf(CharSequence s, CharSequence needle)
static int indexOf(CharSequence s, char ch, int start)
```

## static boolean isDigitsOnly(CharSequence str)
文字列の中が数字だけであれば true を返すメソッドです。

### 例

```java
TextUtils.isDigitsOnly("123456"); // true
TextUtils.isDigitsOnly("123g");   // false
TextUtils.isDigitsOnly("");       // false
```

### 注意
あまりこういうメソッドを濫用しすぎると、文字列で何でもやってしまう癖がつくので気を付けたいところです。

null を渡すと NullPointerException が発生します。

```java
TextUtils.isDigitsOnly(null);     // NullPointerException!!!
```

## static boolean isEmpty(CharSequence str)
文字列が null または長さ0の時に true を返すメソッドです。 NullPointerException が発生しません。

なお、 Apache の StringUtils と異なり、 [isNotEmpty](https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html#isNotEmpty-java.lang.CharSequence-) (Empty でないなら true)
や [isBlank](https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html#isBlank-java.lang.CharSequence-) (Emptyまたは空白のみの文字列なら true)
や [isNoneEmpty](https://commons.apache.org/proper/commons-lang/apidocs/org/apache/commons/lang3/StringUtils.html#isNoneEmpty-java.lang.CharSequence...-) (引数すべてが Empty でないなら true)は一切ありません。

## static String join(CharSequence delimiter, Iterable tokens)
指定した delimiter を使って Iterable(List や Set等) の要素の toString() した文字列を連結し、その結果を返します。
StringBuilder を使って実装しているようです。この手のコードはよく実装するので手間を省けます。

### 例
第1引数の delimiter に null を渡してもそのまま連結できます。

```java
TextUtils.join("", new String[]{"a", "b", "c", "d", "e"});   // "abcde"
TextUtils.join(",", new String[]{"a", "b", "c", "d", "e"});  // "a,b,c,d,e"
TextUtils.join(null, new String[]{"a", "b", "c", "d", "e"}); // "anullbnullcnulldnulle"
```

第2引数 tokens に null を渡すと ```Ambiguous method call.``` でコンパイルエラーになります。

## static String join(CharSequence delimiter, Object[] tokens)
同名メソッドの配列版です。拡張 for 文とフラグを使って実装しているようです。

## static int lastIndexOf(CharSequence s, char ch)
文字列の中で指定した文字列が最後に現れる index を返します。

```
static int lastIndexOf(CharSequence s, char ch, int last)
static int lastIndexOf(CharSequence s, char ch, int start, int last)
```

## static CharSequence replace(CharSequence template, String[] sources, CharSequence[] destinations)
対象文字列 template の、sources に一致する文字列を destinations で置き換えます。sources と destinations は同じ index の文字列が対応します。ただ、sources と destinations の数は一致している必要はありませんし、一致しなくてもエラーにはならずそのまま実行されます。

### 例
以下の場合、対象文字列 "abc" に対し、"a" -> "c", "c" -> "i" の置換が実施されます。

```java
TextUtils.replace("abc", new String[]{"a", "c"}, new CharSequence[]{"c", "i"});
```

この結果は "cbi" となります。


```java
TextUtils.replace("abcda", new String[]{"a", "c"}, new CharSequence[]{"c", "i"});
TextUtils.replace("abcda", new String[]{"c", "a"}, new CharSequence[]{"i", "c"});
TextUtils.replace("abcda", new String[]{"c", "a"}, new CharSequence[]{"i", "c", "e"});
TextUtils.replace("abcda", new String[]{"c", "a", "e"}, new CharSequence[]{"i", "c"});
```

上記はいずれも "cbida" が返されます。

## static String[] split(String text, Pattern pattern)
文字列を正規表現で String 配列に分割します。第1引数が空文字列の場合は空の String 配列が返ります。
null の場合は NullPointerException が発生します。

text の長さをチェックして0なら空の配列を返し、そうでない場合は text から split メソッドを呼び出す実装でした。

## static String[] split(String text, String expression)
文字列を正規表現で String 配列に分割します。第1引数が空文字列の場合は空の String 配列が返ります。
null の場合は NullPointerException が発生します。

text の長さをチェックして0なら空の配列を返し、そうでない場合は pattern から split メソッドを呼び出す実装でした。

### 例

```java
TextUtils.split("to/be/or/not/to", "/"); // [to, be, or, not, to]
```

### 注意
第2引数 expression に null を渡すと ```Ambiguous method call.``` でコンパイルエラーになります。

```java
TextUtils.split("to/be/or/not/to", null);
```

## static String substring(CharSequence source, int start, int end)
元の文字列から新たな部分文字列を生成して取得します。

### 例

```java
TextUtils.substring("tomato", 1, 3); // "om"
```

### 注意
source が null だと NullPointerException が発生します。

```java
TextUtils.substring(null, 1, 3); // NullPointerException!!!
```

マイナスや文字列長より大きな数字を指定すると java.lang.StringIndexOutOfBoundsException が発生します。

```java
TextUtils.substring("tomato", -1, 3);
```

----
# クラス

## [TextUtils.SimpleStringSplitter](https://developer.android.com/reference/android/text/TextUtils.SimpleStringSplitter.html)
文字列を指定の区切り文字で分割するクラスです。

### 例
文字列を '-' で区切りたい時は下記のようにインスタンスを取得します。

```java
TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter('-');
```

この splitter を文字列 "a-b-c" に対して使用すると

```
splitter.setString("a-b-c");
```

splitter 内部にて分割した文字列へのイテレータが生成され使用できるようになります。

```java
splitter.setString("a-b-c");
for (;splitter.hasNext();) {
    System.out.println(splitter.next());
}
```

この出力結果は下記の通りです。

```
a
b
c
```

区切り文字がない場合は要素1のイテレータが取得できます。

```
splitter.setString("9");
```

### 注意
setString に null を渡すと NullPointerException が発生します。

```java
splitter.setString(null);
```

----

# まとめ
Android SDK の android.text.TextUtils について調べてみました。知っておくと無用の再実装を防げる便利なメソッドが結構あるので、自分で StringUtils を作る前にドキュメントとソースコードを眺めておくとよいかもしれません。

## 参考

### 今回の検証用コード(Gist)
https://gist.github.com/toastkidjp/7230227dd16b80bd4eea9da652be3637

----

# ほか
Deprecated になっているメソッドは飛ばしたのですが、気になるメソッドが1つあったので以下に書きます。

## [getReverse(CharSequence source, int start, int end)](https://developer.android.com/reference/android/text/TextUtils.html#getReverse(java.lang.CharSequence,%20int,%20int))
面白そうなメソッドですが、 APIレベル24で Deprecated になったようです。
「使うな」とだけ書いてあって理由や代替案がない投げっぷりが何とも言えません。

> This method was deprecated in API level 24. Do not use. 

```java
    private static class Reverser
    implements CharSequence, GetChars
    {
        public Reverser(CharSequence source, int start, int end) {
            mSource = source;
            mStart = start;
            mEnd = end;
        }

        public int length() {
            return mEnd - mStart;
        }

        public CharSequence subSequence(int start, int end) {
            char[] buf = new char[end - start];

            getChars(start, end, buf, 0);
            return new String(buf);
        }

        @Override
        public String toString() {
            return subSequence(0, length()).toString();
        }

        public char charAt(int off) {
            return (char) UCharacter.getMirror(mSource.charAt(mEnd - 1 - off));
        }

        @SuppressWarnings("deprecation")
        public void getChars(int start, int end, char[] dest, int destoff) {
            TextUtils.getChars(mSource, start + mStart, end + mStart,
                               dest, destoff);
            AndroidCharacter.mirror(dest, 0, end - start);

            int len = end - start;
            int n = (end - start) / 2;
            for (int i = 0; i < n; i++) {
                char tmp = dest[destoff + i];

                dest[destoff + i] = dest[destoff + len - i - 1];
                dest[destoff + len - i - 1] = tmp;
            }
        }

        private CharSequence mSource;
        private int mStart;
        private int mEnd;
    }
```