/*
 * StringOperation class.
 *
 * Copyright (C) 2007 SATOH Takayuki All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package ts.util.text;

/**
 * 基本的な文字列操作を実行するためのクラス。
 * <br>
 * Unicodeの補助文字の対応(JSR-204)により、文字と<code>char</code>変数が必ず
 * しも対応しなくなり、文字列操作において文字に関する考慮が複雑になった。
 * このクラスでは、その文字に関する考慮を簡単にするため、以下の方針で文字及び
 * 文字列を扱うことができるように、メソッドを用意している：
 * <ul>
 * <li><code>char</code>変数の使用を極力避け、文字を取得する場合でも{@link
 * java.lang.String String}オブジェクトを使用する。</li>
 * <li>文字の長さやインデックスは、<code>char</code>配列のサイズやインデックス
 * ではなく、コード・ポイント単位で換算した値を使用する。</li>
 * <li>文字を順番に取得するなどの繰り返し操作を行う場合は、インデックスを使用
 * する代わりに、{@link ts.util.text.StringSequence StringSequence}クラスを使用
 * する。</li>
 * </ul>
 *
 * @author  佐藤隆之.
 * @version $Revision: 1.2 $, $Date: 2010-10-16 09:35:14 $
 */
public final class StringOperation
{
  /**
   * デフォルトコンストラクタ。
   */
  protected StringOperation()
  {}

  /**
   * 指定された文字列の長さを取得する。
   * <br>
   * なお、ここでいう長さとは、コード・ポイント単位の値をいう。
   *
   * @param  str 文字列。
   * @return 文字列長。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static int length(String str)
  {
    assert (str != null) : "@param:str is null.";

    return str.codePointCount(0, str.length());
  }

  /**
   * 指定された文字列が空文字列かどうかを判定する。
   *
   * @param  str 文字列。
   * @return 空文字列の場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static boolean isEmpty(String str)
  {
    assert (str != null) : "@param:str is null.";

    return (str.length() == 0) ? true : false;
  }

  /**
   * 指定された文字列が空白文字だけで構成されているかどうかを判定する。
   * <br>
   * 引数が空文字列の場合は<tt>false</tt>を返す。
   *
   * @param  str 文字列。
   * @return 空白文字だけで構成されている場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static boolean isWhitespaces(String str)
  {
    assert (str != null) : "@param:str is null.";

    if (str.length() == 0) {
      return false;
    }

    for (int i=0; i<str.length(); i=offsetByCodePoints(str, i, 1)) {
      if (! Character.isWhitespace(str.codePointAt(i))) {
        return false;
      }
    }

    return true;
  }

  /**
   * 指定された文字列内の文字が全て指定されたUnicode文字ブロックに含まれている
   * かどうかを判定する。
   * <br>
   * 引数が空文字列の場合は<tt>false</tt>を返す。
   *
   * @param  str 文字列。
   * @param  unicodeBlock Unicode文字ブロック。
   * @return 全ての文字が指定されたUnicode文字ブロックの場合は<tt>true</tt>を
   * 返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static boolean isUnicodeBlock(
    String str, Character.UnicodeBlock unicodeBlock)
  {
    assert (str != null) : "@param:str is null.";
    assert (unicodeBlock != null) : "@param:unicodeBlock is null.";

    if (str.length() == 0) {
      return false;
    }

    for (int i=0; i<str.length(); i=offsetByCodePoints(str, i, 1)) {
      Character.UnicodeBlock ub = Character.UnicodeBlock.of(str.codePointAt(i));
      if (! ub.equals(unicodeBlock)) {
        return false;
      }
    }

    return true;
  }

  /**
   * 指定された文字列が数字だけで構成されているかどうかを判定する。
   * <br>
   * 引数が空文字列の場合は<tt>false</tt>を返す。
   *
   * @param  str 文字列。
   * @return 数字だけで構成されている場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static boolean isDigits(String str)
  {
    assert (str != null) : "@param:str is null.";

    if (str.length() == 0) {
      return false;
    }

    for (int i=0; i<str.length(); i=offsetByCodePoints(str, i, 1)) {
      if (! Character.isDigit(str.codePointAt(i))) {
        return false;
      }
    }

    return true;
  }

  /**
   * 指定されたインデックスの文字を取得する。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイントの先頭からの順番（0から
   * 開始）である。
   *
   * @param  str 文字列。
   * @param  codePointIndex コード・ポイント・インデックス。
   * @return 指定されたインデックスの文字。
   * @throws IndexOutOfBoundsException 指定されたインデックスが範囲外の場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static String characterAt(String str, int codePointIndex)
    throws IndexOutOfBoundsException
  {
    assert (str != null) : "@param:str is null.";

    int bgnInd = offsetByCodePoints(str, 0, codePointIndex);
    int endInd = offsetByCodePoints(str, bgnInd, 1);
    return str.substring(bgnInd, endInd);
  }

  /**
   * 指定されたインデックスを先頭とする部分文字列を取得する。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイントの先頭からの順番（0から
   * 開始）である。
   *
   * @param  str 文字列。
   * @param  beginCodePointIndex 開始インデックス。
   * @return 指定されたインデックスから開始する部分文字列。
   * @throws IndexOutOfBoundsException 指定されたインデックスが範囲外の場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static String substring(String str, int beginCodePointIndex)
    throws IndexOutOfBoundsException
  {
    assert (str != null) : "@param:str is null.";

    return str.substring(offsetByCodePoints(str, 0, beginCodePointIndex));
  }

  /**
   * 指定されたインデックスを先頭及び末尾とする部分文字列を取得する。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイントの先頭からの順番（0から
   * 開始）である。
   *
   * @param  str 文字列。
   * @param  beginCodePointIndex 開始インデックス。
   * @param  endCodePointIndex 終了インデックス。
   * @return 部分文字列。
   * @throws IndexOutOfBoundsException 指定されたインデックスが範囲外の場合。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static String substring(String str,
    int beginCodePointIndex, int endCodePointIndex)
      throws IndexOutOfBoundsException
  {
    assert (str != null) : "@param:str is null.";

    int count = endCodePointIndex - beginCodePointIndex;
    int bgnInd = offsetByCodePoints(str, 0, beginCodePointIndex);
    int endInd = offsetByCodePoints(str, bgnInd, count);
    return str.substring(bgnInd, endInd);
  }

  /**
   * 指定された文字列において、探索文字列が最初に現れるインデックスを取得する。
   * <br>
   * 探索文字列が見つからなかった場合は、負の値を返す。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイントの先頭からの順番（0から
   * 開始）である。
   *
   * @param  str 文字列。
   * @param  searched 探索文字列。
   * @return 探索される文字列が最初に現れたインデックス。見つからなかった場合は
   *           負の値を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static int indexOf(String str, String searched)
  {
    return indexOf(str, searched, 0);
  }

  /**
   * 指定された文字列において、探索文字列が開始インデックス以降で最初に現れる
   * インデックスを取得する。
   * <br>
   * 開始インデックスの範囲については制約はなく、負の値や文字列の長さを超える
   * インデックスが指定された場合でも判定が行われる。負の値が指定された場合は、
   * ゼロを指定された時と同じ結果を返す。
   * <br>
   * 探索文字列が開始インデックス以降で見つからなかった場合は、負の値を返す。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイントの先頭からの順番（0から
   * 開始）である。
   *
   * @param  str 文字列。
   * @param  searched 探索文字列。
   * @param  fromCodePointIndex 探索を開始するインデックス。
   * @return 探索される文字列が最初に現れたインデックス。見つからなかった場合は
   *           負の値を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static int indexOf(String str, String searched, int fromCodePointIndex)
  {
    assert (str != null) : "@param:str is null.";
    assert (searched != null) : "@param:searched is null.";

    int cpind = Math.max(fromCodePointIndex, 0);

    try {
      int bgnInd = offsetByCodePoints(str, 0, cpind);

      int n = str.length() - searched.length();
      for (int i=bgnInd; i<=n; i=offsetByCodePoints(str, i, 1), cpind++) {
        if (str.startsWith(searched, i)) {
          return cpind;
        }
      }
      return -1;
    }
    catch (IndexOutOfBoundsException e) {
      return -1;
    }
  }

  /**
   * 指定された文字列において、探索文字列が最後に現れるインデックスを取得する。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイントの先頭からの順番（0から
   * 開始）である。
   *
   * @param  str 文字列。
   * @param  searched 探索文字列。
   * @return 探索される文字列が最後に現れたインデックス。見つからなかった場合は
   *           負の値を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static int lastIndexOf(String str, String searched)
  {
    return lastIndexOf(str, searched, str.length());
  }

  /**
   * 指定された文字列において、探索文字列が指定されたインデックス以前で最後に
   * 現れるインデックスを取得する。
   * <br>
   * 開始インデックスの範囲については制約はなく、負の値や文字列の長さを超える
   * インデックスが指定された場合でも判定が行われる。文字列の長さを超える値が
   * 指定された場合は、最終インデックスを指定された時と同じ結果を返す。
   * <br>
   * なお、ここでいうインデックスとは、コード・ポイントの先頭からの順番（0から
   * 開始）である。
   *
   * @param  str 文字列。
   * @param  searched 探索文字列。
   * @param  fromCodePointIndex 探索を行う最終インデックス。
   * @return 探索される文字列が最後に現れたインデックス。見つからなかった場合は
   *           負の値を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static int lastIndexOf(String str, String searched,
    int fromCodePointIndex)
  {
    assert (str != null) : "@param:str is null.";
    assert (searched != null) : "@param:searched is null.";

    int cpind = Math.min(fromCodePointIndex,
      Math.max(StringOperation.length(str), 0));

    try {
      int bgnInd = offsetByCodePoints(str, 0, cpind);
      for (int i=bgnInd; i>=0; i=offsetByCodePoints(str, i,-1), cpind--) {
        if (str.startsWith(searched, i)) {
          return cpind;
        }
      }
    }
    catch (IndexOutOfBoundsException e) {}

    return -1;
  }

  /**
   * 指定された文字列の開始が、指定されたプリフィックスと一致しているかどうか
   * を判定する。
   *
   * @param  str 文字列。
   * @param  prefix プリフィックス。
   * @return 文字列の開始がプリフィックスと一致する場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static boolean startsWith(String str, String prefix)
  {
    assert (str != null) : "@param:str is null.";
    assert (prefix != null) : "@param:prefix is null.";

    return str.startsWith(prefix);
  }

  /**
   * 指定された文字列の末尾が、指定されたサフィックスと一致しているかどうかを
   * 判定する。
   *
   * @param  str 文字列。
   * @param  suffix サフィックス。
   * @return 文字列の開始がサフィックスと一致する場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static boolean endsWith(String str, String suffix)
  {
    assert (str != null) : "@param:str is null.";
    assert (suffix != null) : "@param:suffix is null.";

    return str.endsWith(suffix);
  }

  /**
   * 指定された文字列に、指定された探索文字列が含まれているかどうかを判定する。
   *
   * @param  str 文字列。
   * @param  searched 探索文字列。
   * @return 探索文字列が含まれている場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public static boolean contains(String str, String searched)
  {
    assert (str != null) : "@param:str is null.";
    assert (searched != null) : "@param:searched is null.";

    return str.contains(searched);
  }

  /**
   * 指定されたインデックスからコード・ポイント・オブセット分だけ移動した
   * インデックスを返す。
   * <br>
   * ここでいうインデックスは、文字列内の<code>char</code>配列のインデックス
   * である。
   * <br>
   * このメソッドは、JDK1.5の不具合（Bug ID:6242664）への対策のために作成
   * された。
   *
   * @param  index インデックス。
   * @param  codePointOffset コード・ポイント・オフセット。
   * @return 文字列内のインデックス。
   * @throws IndexOutOfBoundsException 引数のインデックスが負の値か文字列長を
   *           超える場合、又はコード・ポイント・インデックスが負の値の場合、
   *           移動した後のインデックスが文字列長を超える場合。
   */
  protected static int offsetByCodePoints(
    String str, int index, int codePointOffset
  ) throws IndexOutOfBoundsException
  {
    if (System.getProperty("java.version").startsWith("1.5.")) {
      return new String(str).offsetByCodePoints(index, codePointOffset);
    } else {
      return str.offsetByCodePoints(index, codePointOffset);
    }
  }
}

