/*
 * Copyright (C) 2008 The Guava Authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.common.base;

import static com.google.common.base.CharMatcher.BREAKING_WHITESPACE;
import static com.google.common.base.CharMatcher.WHITESPACE;
import static com.google.common.base.CharMatcher.anyOf;
import static com.google.common.base.CharMatcher.forPredicate;
import static com.google.common.base.CharMatcher.inRange;
import static com.google.common.base.CharMatcher.is;
import static com.google.common.base.CharMatcher.isNot;
import static com.google.common.base.CharMatcher.noneOf;

import com.google.common.annotations.GwtCompatible;
import com.google.common.collect.Sets;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
 * Unit test for {@link CharMatcher}.
 *
 * @author Kevin Bourrillion
 */
@GwtCompatible(emulated = true)
public class CharMatcherTest extends TestCase {

  private static final CharMatcher WHATEVER = new CharMatcher() {
    @Override public boolean matches(char c) {
      throw new AssertionFailedError(
          "You weren't supposed to actually invoke me!");
    }
  };

  public void testAnyAndNone_logicalOps() throws Exception {
    // These are testing behavior that's never promised by the API, but since
    // we're lucky enough that these do pass, it saves us from having to write
    // more excruciating tests! Hooray!

    assertSame(CharMatcher.ANY, CharMatcher.NONE.negate());
    assertSame(CharMatcher.NONE, CharMatcher.ANY.negate());

    assertSame(WHATEVER, CharMatcher.ANY.and(WHATEVER));
    assertSame(CharMatcher.ANY, CharMatcher.ANY.or(WHATEVER));

    assertSame(CharMatcher.NONE, CharMatcher.NONE.and(WHATEVER));
    assertSame(WHATEVER, CharMatcher.NONE.or(WHATEVER));
  }

  // The rest of the behavior of ANY and DEFAULT will be covered in the tests for
  // the text processing methods below.

  public void testWhitespaceBreakingWhitespaceSubset() throws Exception {
    for (int c = 0; c <= Character.MAX_VALUE; c++) {
      if (BREAKING_WHITESPACE.apply((char) c)) {
        assertTrue(Integer.toHexString(c), WHITESPACE.apply((char) c));
      }
    }
  }

  // The next tests require ICU4J and have, at least for now, been sliced out
  // of the open-source view of the tests.

  // Omitting tests for the rest of the JAVA_* constants as these are defined
  // as extremely straightforward pass-throughs to the JDK methods.

  // We're testing the is(), isNot(), anyOf(), noneOf() and inRange() methods
  // below by testing their text-processing methods.

  // The organization of this test class is unusual, as it's not done by
  // method, but by overall "scenario". Also, the variety of actual tests we
  // do borders on absurd overkill. Better safe than sorry, though?

  public void testEmpty() throws Exception {
    doTestEmpty(CharMatcher.ANY);
    doTestEmpty(CharMatcher.NONE);
    doTestEmpty(is('a'));
    doTestEmpty(isNot('a'));
    doTestEmpty(anyOf(""));
    doTestEmpty(anyOf("x"));
    doTestEmpty(anyOf("xy"));
    doTestEmpty(anyOf("CharMatcher"));
    doTestEmpty(noneOf("CharMatcher"));
    doTestEmpty(inRange('n', 'q'));
    doTestEmpty(forPredicate(Predicates.equalTo('c')));
  }

  private void doTestEmpty(CharMatcher matcher) throws Exception {
    reallyTestEmpty(matcher);
    reallyTestEmpty(matcher.negate());
    reallyTestEmpty(matcher.precomputed());
  }

  private void reallyTestEmpty(CharMatcher matcher) throws Exception {
    assertEquals(-1, matcher.indexIn(""));
    assertEquals(-1, matcher.indexIn("", 0));
    try {
      matcher.indexIn("", 1);
      fail();
    } catch (IndexOutOfBoundsException expected) {
    }
    try {
      matcher.indexIn("", -1);
      fail();
    } catch (IndexOutOfBoundsException expected) {
    }
    assertEquals(-1, matcher.lastIndexIn(""));
    assertFalse(matcher.matchesAnyOf(""));
    assertTrue(matcher.matchesAllOf(""));
    assertTrue(matcher.matchesNoneOf(""));
    assertEquals("", matcher.removeFrom(""));
    assertEquals("", matcher.replaceFrom("", 'z'));
    assertEquals("", matcher.replaceFrom("", "ZZ"));
    assertEquals("", matcher.trimFrom(""));
    assertEquals(0, matcher.countIn(""));
  }

  public void testNoMatches() {
    doTestNoMatches(CharMatcher.NONE, "blah");
    doTestNoMatches(is('a'), "bcde");
    doTestNoMatches(isNot('a'), "aaaa");
    doTestNoMatches(anyOf(""), "abcd");
    doTestNoMatches(anyOf("x"), "abcd");
    doTestNoMatches(anyOf("xy"), "abcd");
    doTestNoMatches(anyOf("CharMatcher"), "zxqy");
    doTestNoMatches(noneOf("CharMatcher"), "ChMa");
    doTestNoMatches(inRange('p', 'x'), "mom");
    doTestNoMatches(forPredicate(Predicates.equalTo('c')), "abe");
    doTestNoMatches(inRange('A', 'Z').and(inRange('F', 'K').negate()), "F1a");
    doTestNoMatches(CharMatcher.DIGIT, "\tAz()");
    doTestNoMatches(CharMatcher.JAVA_DIGIT, "\tAz()");
    doTestNoMatches(CharMatcher.DIGIT.and(CharMatcher.ASCII), "\tAz()");
    doTestNoMatches(CharMatcher.SINGLE_WIDTH, "\u05bf\u3000");
  }

  private void doTestNoMatches(CharMatcher matcher, String s) {
    reallyTestNoMatches(matcher, s);
    reallyTestAllMatches(matcher.negate(), s);
    reallyTestNoMatches(matcher.precomputed(), s);
    reallyTestAllMatches(matcher.negate().precomputed(), s);
    reallyTestAllMatches(matcher.precomputed().negate(), s);
    reallyTestNoMatches(forPredicate(matcher), s);

    reallyTestNoMatches(matcher, new StringBuilder(s));
  }

  public void testAllMatches() {
    doTestAllMatches(CharMatcher.ANY, "blah");
    doTestAllMatches(isNot('a'), "bcde");
    doTestAllMatches(is('a'), "aaaa");
    doTestAllMatches(noneOf("CharMatcher"), "zxqy");
    doTestAllMatches(anyOf("x"), "xxxx");
    doTestAllMatches(anyOf("xy"), "xyyx");
    doTestAllMatches(anyOf("CharMatcher"), "ChMa");
    doTestAllMatches(inRange('m', 'p'), "mom");
    doTestAllMatches(forPredicate(Predicates.equalTo('c')), "ccc");
    doTestAllMatches(CharMatcher.DIGIT, "0123456789\u0ED0\u1B59");
    doTestAllMatches(CharMatcher.JAVA_DIGIT, "0123456789");
    doTestAllMatches(CharMatcher.DIGIT.and(CharMatcher.ASCII), "0123456789");
    doTestAllMatches(CharMatcher.SINGLE_WIDTH, "\t0123ABCdef~\u00A0\u2111");
  }

  private void doTestAllMatches(CharMatcher matcher, String s) {
    reallyTestAllMatches(matcher, s);
    reallyTestNoMatches(matcher.negate(), s);
    reallyTestAllMatches(matcher.precomputed(), s);
    reallyTestNoMatches(matcher.negate().precomputed(), s);
    reallyTestNoMatches(matcher.precomputed().negate(), s);
    reallyTestAllMatches(forPredicate(matcher), s);

    reallyTestAllMatches(matcher, new StringBuilder(s));
  }

  private void reallyTestNoMatches(CharMatcher matcher, CharSequence s) {
    assertFalse(matcher.matches(s.charAt(0)));
    assertEquals(-1, matcher.indexIn(s));
    assertEquals(-1, matcher.indexIn(s, 0));
    assertEquals(-1, matcher.indexIn(s, 1));
    assertEquals(-1, matcher.indexIn(s, s.length()));
    try {
      matcher.indexIn(s, s.length() + 1);
      fail();
    } catch (IndexOutOfBoundsException expected) {
    }
    try {
      matcher.indexIn(s, -1);
      fail();
    } catch (IndexOutOfBoundsException expected) {
    }
    assertEquals(-1, matcher.lastIndexIn(s));
    assertFalse(matcher.matchesAnyOf(s));
    assertFalse(matcher.matchesAllOf(s));
    assertTrue(matcher.matchesNoneOf(s));

    assertEquals(s.toString(), matcher.removeFrom(s));
    assertEquals(s.toString(), matcher.replaceFrom(s, 'z'));
    assertEquals(s.toString(), matcher.replaceFrom(s, "ZZ"));
    assertEquals(s.toString(), matcher.trimFrom(s));
    assertEquals(0, matcher.countIn(s));
  }

  private void reallyTestAllMatches(CharMatcher matcher, CharSequence s) {
    assertTrue(matcher.matches(s.charAt(0)));
    assertEquals(0, matcher.indexIn(s));
    assertEquals(0, matcher.indexIn(s, 0));
    assertEquals(1, matcher.indexIn(s, 1));
    assertEquals(-1, matcher.indexIn(s, s.length()));
    assertEquals(s.length() - 1, matcher.lastIndexIn(s));
    assertTrue(matcher.matchesAnyOf(s));
    assertTrue(matcher.matchesAllOf(s));
    assertFalse(matcher.matchesNoneOf(s));
    assertEquals("", matcher.removeFrom(s));
    assertEquals(Strings.repeat("z", s.length()),
        matcher.replaceFrom(s, 'z'));
    assertEquals(Strings.repeat("ZZ", s.length()),
        matcher.replaceFrom(s, "ZZ"));
    assertEquals("", matcher.trimFrom(s));
    assertEquals(s.length(), matcher.countIn(s));
  }

  public void testGeneral() {
    doTestGeneral(is('a'), 'a', 'b');
    doTestGeneral(isNot('a'), 'b', 'a');
    doTestGeneral(anyOf("x"), 'x', 'z');
    doTestGeneral(anyOf("xy"), 'y', 'z');
    doTestGeneral(anyOf("CharMatcher"), 'C', 'z');
    doTestGeneral(noneOf("CharMatcher"), 'z', 'C');
    doTestGeneral(inRange('p', 'x'), 'q', 'z');
  }

  private void doTestGeneral(CharMatcher matcher, char match, char noMatch) {
    doTestOneCharMatch(matcher, "" + match);
    doTestOneCharNoMatch(matcher, "" + noMatch);
    doTestMatchThenNoMatch(matcher, "" + match + noMatch);
    doTestNoMatchThenMatch(matcher, "" + noMatch + match);
  }

  private void doTestOneCharMatch(CharMatcher matcher, String s) {
    reallyTestOneCharMatch(matcher, s);
    reallyTestOneCharNoMatch(matcher.negate(), s);
    reallyTestOneCharMatch(matcher.precomputed(), s);
    reallyTestOneCharNoMatch(matcher.negate().precomputed(), s);
    reallyTestOneCharNoMatch(matcher.precomputed().negate(), s);
  }

  private void doTestOneCharNoMatch(CharMatcher matcher, String s) {
    reallyTestOneCharNoMatch(matcher, s);
    reallyTestOneCharMatch(matcher.negate(), s);
    reallyTestOneCharNoMatch(matcher.precomputed(), s);
    reallyTestOneCharMatch(matcher.negate().precomputed(), s);
    reallyTestOneCharMatch(matcher.precomputed().negate(), s);
  }

  private void doTestMatchThenNoMatch(CharMatcher matcher, String s) {
    reallyTestMatchThenNoMatch(matcher, s);
    reallyTestNoMatchThenMatch(matcher.negate(), s);
    reallyTestMatchThenNoMatch(matcher.precomputed(), s);
    reallyTestNoMatchThenMatch(matcher.negate().precomputed(), s);
    reallyTestNoMatchThenMatch(matcher.precomputed().negate(), s);
  }

  private void doTestNoMatchThenMatch(CharMatcher matcher, String s) {
    reallyTestNoMatchThenMatch(matcher, s);
    reallyTestMatchThenNoMatch(matcher.negate(), s);
    reallyTestNoMatchThenMatch(matcher.precomputed(), s);
    reallyTestMatchThenNoMatch(matcher.negate().precomputed(), s);
    reallyTestMatchThenNoMatch(matcher.precomputed().negate(), s);
  }

  private void reallyTestOneCharMatch(CharMatcher matcher, String s) {
    assertTrue(matcher.matches(s.charAt(0)));
    assertTrue(matcher.apply(s.charAt(0)));
    assertEquals(0, matcher.indexIn(s));
    assertEquals(0, matcher.indexIn(s, 0));
    assertEquals(-1, matcher.indexIn(s, 1));
    assertEquals(0, matcher.lastIndexIn(s));
    assertTrue(matcher.matchesAnyOf(s));
    assertTrue(matcher.matchesAllOf(s));
    assertFalse(matcher.matchesNoneOf(s));
    assertEquals("", matcher.removeFrom(s));
    assertEquals("z", matcher.replaceFrom(s, 'z'));
    assertEquals("ZZ", matcher.replaceFrom(s, "ZZ"));
    assertEquals("", matcher.trimFrom(s));
    assertEquals(1, matcher.countIn(s));
  }

  private void reallyTestOneCharNoMatch(CharMatcher matcher, String s) {
    assertFalse(matcher.matches(s.charAt(0)));
    assertFalse(matcher.apply(s.charAt(0)));
    assertEquals(-1, matcher.indexIn(s));
    assertEquals(-1, matcher.indexIn(s, 0));
    assertEquals(-1, matcher.indexIn(s, 1));
    assertEquals(-1, matcher.lastIndexIn(s));
    assertFalse(matcher.matchesAnyOf(s));
    assertFalse(matcher.matchesAllOf(s));
    assertTrue(matcher.matchesNoneOf(s));

    assertSame(s, matcher.removeFrom(s));
    assertSame(s, matcher.replaceFrom(s, 'z'));
    assertSame(s, matcher.replaceFrom(s, "ZZ"));
    assertSame(s, matcher.trimFrom(s));
    assertSame(0, matcher.countIn(s));
  }

  private void reallyTestMatchThenNoMatch(CharMatcher matcher, String s) {
    assertEquals(0, matcher.indexIn(s));
    assertEquals(0, matcher.indexIn(s, 0));
    assertEquals(-1, matcher.indexIn(s, 1));
    assertEquals(-1, matcher.indexIn(s, 2));
    assertEquals(0, matcher.lastIndexIn(s));
    assertTrue(matcher.matchesAnyOf(s));
    assertFalse(matcher.matchesAllOf(s));
    assertFalse(matcher.matchesNoneOf(s));
    assertEquals(s.substring(1), matcher.removeFrom(s));
    assertEquals("z" + s.substring(1), matcher.replaceFrom(s, 'z'));
    assertEquals("ZZ" + s.substring(1), matcher.replaceFrom(s, "ZZ"));
    assertEquals(s.substring(1), matcher.trimFrom(s));
    assertEquals(1, matcher.countIn(s));
  }

  private void reallyTestNoMatchThenMatch(CharMatcher matcher, String s) {
    assertEquals(1, matcher.indexIn(s));
    assertEquals(1, matcher.indexIn(s, 0));
    assertEquals(1, matcher.indexIn(s, 1));
    assertEquals(-1, matcher.indexIn(s, 2));
    assertEquals(1, matcher.lastIndexIn(s));
    assertTrue(matcher.matchesAnyOf(s));
    assertFalse(matcher.matchesAllOf(s));
    assertFalse(matcher.matchesNoneOf(s));
    assertEquals(s.substring(0, 1), matcher.removeFrom(s));
    assertEquals(s.substring(0, 1) + "z", matcher.replaceFrom(s, 'z'));
    assertEquals(s.substring(0, 1) + "ZZ", matcher.replaceFrom(s, "ZZ"));
    assertEquals(s.substring(0, 1), matcher.trimFrom(s));
    assertEquals(1, matcher.countIn(s));
  }

  /**
   * Checks that expected is equals to out, and further, if in is
   * equals to expected, then out is successfully optimized to be
   * identical to in, i.e. that "in" is simply returned.
   */
  private void assertEqualsSame(String expected, String in, String out) {
    if (expected.equals(in)) {
      assertSame(in, out);
    } else {
      assertEquals(expected, out);
    }
  }

  // Test collapse() a little differently than the rest, as we really want to
  // cover lots of different configurations of input text
  public void testCollapse() {
    // collapsing groups of '-' into '_' or '-'
    doTestCollapse("-", "_");
    doTestCollapse("x-", "x_");
    doTestCollapse("-x", "_x");
    doTestCollapse("--", "_");
    doTestCollapse("x--", "x_");
    doTestCollapse("--x", "_x");
    doTestCollapse("-x-", "_x_");
    doTestCollapse("x-x", "x_x");
    doTestCollapse("---", "_");
    doTestCollapse("--x-", "_x_");
    doTestCollapse("--xx", "_xx");
    doTestCollapse("-x--", "_x_");
    doTestCollapse("-x-x", "_x_x");
    doTestCollapse("-xx-", "_xx_");
    doTestCollapse("x--x", "x_x");
    doTestCollapse("x-x-", "x_x_");
    doTestCollapse("x-xx", "x_xx");
    doTestCollapse("x-x--xx---x----x", "x_x_xx_x_x");

    doTestCollapseWithNoChange("");
    doTestCollapseWithNoChange("x");
    doTestCollapseWithNoChange("xx");
  }

  private void doTestCollapse(String in, String out) {
    // Try a few different matchers which all match '-' and not 'x'
    // Try replacement chars that both do and do not change the value.
    for (char replacement : new char[] { '_', '-' }) {
      String expected = out.replace('_', replacement);
      assertEqualsSame(expected, in, is('-').collapseFrom(in, replacement));
      assertEqualsSame(expected, in, is('-').collapseFrom(in, replacement));
      assertEqualsSame(expected, in, is('-').or(is('#')).collapseFrom(in, replacement));
      assertEqualsSame(expected, in, isNot('x').collapseFrom(in, replacement));
      assertEqualsSame(expected, in, is('x').negate().collapseFrom(in, replacement));
      assertEqualsSame(expected, in, anyOf("-").collapseFrom(in, replacement));
      assertEqualsSame(expected, in, anyOf("-#").collapseFrom(in, replacement));
      assertEqualsSame(expected, in, anyOf("-#123").collapseFrom(in, replacement));
    }
  }

  private void doTestCollapseWithNoChange(String inout) {
    assertSame(inout, is('-').collapseFrom(inout, '_'));
    assertSame(inout, is('-').or(is('#')).collapseFrom(inout, '_'));
    assertSame(inout, isNot('x').collapseFrom(inout, '_'));
    assertSame(inout, is('x').negate().collapseFrom(inout, '_'));
    assertSame(inout, anyOf("-").collapseFrom(inout, '_'));
    assertSame(inout, anyOf("-#").collapseFrom(inout, '_'));
    assertSame(inout, anyOf("-#123").collapseFrom(inout, '_'));
    assertSame(inout, CharMatcher.NONE.collapseFrom(inout, '_'));
  }

  public void testCollapse_any() {
    assertEquals("", CharMatcher.ANY.collapseFrom("", '_'));
    assertEquals("_", CharMatcher.ANY.collapseFrom("a", '_'));
    assertEquals("_", CharMatcher.ANY.collapseFrom("ab", '_'));
    assertEquals("_", CharMatcher.ANY.collapseFrom("abcd", '_'));
  }

  public void testTrimFrom() {
    // trimming -
    doTestTrimFrom("-", "");
    doTestTrimFrom("x-", "x");
    doTestTrimFrom("-x", "x");
    doTestTrimFrom("--", "");
    doTestTrimFrom("x--", "x");
    doTestTrimFrom("--x", "x");
    doTestTrimFrom("-x-", "x");
    doTestTrimFrom("x-x", "x-x");
    doTestTrimFrom("---", "");
    doTestTrimFrom("--x-", "x");
    doTestTrimFrom("--xx", "xx");
    doTestTrimFrom("-x--", "x");
    doTestTrimFrom("-x-x", "x-x");
    doTestTrimFrom("-xx-", "xx");
    doTestTrimFrom("x--x", "x--x");
    doTestTrimFrom("x-x-", "x-x");
    doTestTrimFrom("x-xx", "x-xx");
    doTestTrimFrom("x-x--xx---x----x", "x-x--xx---x----x");
    // additional testing using the doc example
    assertEquals("cat", anyOf("ab").trimFrom("abacatbab"));
  }

  private void doTestTrimFrom(String in, String out) {
    // Try a few different matchers which all match '-' and not 'x'
    assertEquals(out, is('-').trimFrom(in));
    assertEquals(out, is('-').or(is('#')).trimFrom(in));
    assertEquals(out, isNot('x').trimFrom(in));
    assertEquals(out, is('x').negate().trimFrom(in));
    assertEquals(out, anyOf("-").trimFrom(in));
    assertEquals(out, anyOf("-#").trimFrom(in));
    assertEquals(out, anyOf("-#123").trimFrom(in));
  }

  public void testTrimLeadingFrom() {
    // trimming -
    doTestTrimLeadingFrom("-", "");
    doTestTrimLeadingFrom("x-", "x-");
    doTestTrimLeadingFrom("-x", "x");
    doTestTrimLeadingFrom("--", "");
    doTestTrimLeadingFrom("x--", "x--");
    doTestTrimLeadingFrom("--x", "x");
    doTestTrimLeadingFrom("-x-", "x-");
    doTestTrimLeadingFrom("x-x", "x-x");
    doTestTrimLeadingFrom("---", "");
    doTestTrimLeadingFrom("--x-", "x-");
    doTestTrimLeadingFrom("--xx", "xx");
    doTestTrimLeadingFrom("-x--", "x--");
    doTestTrimLeadingFrom("-x-x", "x-x");
    doTestTrimLeadingFrom("-xx-", "xx-");
    doTestTrimLeadingFrom("x--x", "x--x");
    doTestTrimLeadingFrom("x-x-", "x-x-");
    doTestTrimLeadingFrom("x-xx", "x-xx");
    doTestTrimLeadingFrom("x-x--xx---x----x", "x-x--xx---x----x");
    // additional testing using the doc example
    assertEquals("catbab", anyOf("ab").trimLeadingFrom("abacatbab"));
  }

  private void doTestTrimLeadingFrom(String in, String out) {
    // Try a few different matchers which all match '-' and not 'x'
    assertEquals(out, is('-').trimLeadingFrom(in));
    assertEquals(out, is('-').or(is('#')).trimLeadingFrom(in));
    assertEquals(out, isNot('x').trimLeadingFrom(in));
    assertEquals(out, is('x').negate().trimLeadingFrom(in));
    assertEquals(out, anyOf("-#").trimLeadingFrom(in));
    assertEquals(out, anyOf("-#123").trimLeadingFrom(in));
  }

  public void testTrimTrailingFrom() {
    // trimming -
    doTestTrimTrailingFrom("-", "");
    doTestTrimTrailingFrom("x-", "x");
    doTestTrimTrailingFrom("-x", "-x");
    doTestTrimTrailingFrom("--", "");
    doTestTrimTrailingFrom("x--", "x");
    doTestTrimTrailingFrom("--x", "--x");
    doTestTrimTrailingFrom("-x-", "-x");
    doTestTrimTrailingFrom("x-x", "x-x");
    doTestTrimTrailingFrom("---", "");
    doTestTrimTrailingFrom("--x-", "--x");
    doTestTrimTrailingFrom("--xx", "--xx");
    doTestTrimTrailingFrom("-x--", "-x");
    doTestTrimTrailingFrom("-x-x", "-x-x");
    doTestTrimTrailingFrom("-xx-", "-xx");
    doTestTrimTrailingFrom("x--x", "x--x");
    doTestTrimTrailingFrom("x-x-", "x-x");
    doTestTrimTrailingFrom("x-xx", "x-xx");
    doTestTrimTrailingFrom("x-x--xx---x----x", "x-x--xx---x----x");
    // additional testing using the doc example
    assertEquals("abacat", anyOf("ab").trimTrailingFrom("abacatbab"));
  }

  private void doTestTrimTrailingFrom(String in, String out) {
    // Try a few different matchers which all match '-' and not 'x'
    assertEquals(out, is('-').trimTrailingFrom(in));
    assertEquals(out, is('-').or(is('#')).trimTrailingFrom(in));
    assertEquals(out, isNot('x').trimTrailingFrom(in));
    assertEquals(out, is('x').negate().trimTrailingFrom(in));
    assertEquals(out, anyOf("-#").trimTrailingFrom(in));
    assertEquals(out, anyOf("-#123").trimTrailingFrom(in));
  }

  public void testTrimAndCollapse() {
    // collapsing groups of '-' into '_' or '-'
    doTestTrimAndCollapse("", "");
    doTestTrimAndCollapse("x", "x");
    doTestTrimAndCollapse("-", "");
    doTestTrimAndCollapse("x-", "x");
    doTestTrimAndCollapse("-x", "x");
    doTestTrimAndCollapse("--", "");
    doTestTrimAndCollapse("x--", "x");
    doTestTrimAndCollapse("--x", "x");
    doTestTrimAndCollapse("-x-", "x");
    doTestTrimAndCollapse("x-x", "x_x");
    doTestTrimAndCollapse("---", "");
    doTestTrimAndCollapse("--x-", "x");
    doTestTrimAndCollapse("--xx", "xx");
    doTestTrimAndCollapse("-x--", "x");
    doTestTrimAndCollapse("-x-x", "x_x");
    doTestTrimAndCollapse("-xx-", "xx");
    doTestTrimAndCollapse("x--x", "x_x");
    doTestTrimAndCollapse("x-x-", "x_x");
    doTestTrimAndCollapse("x-xx", "x_xx");
    doTestTrimAndCollapse("x-x--xx---x----x", "x_x_xx_x_x");
  }

  private void doTestTrimAndCollapse(String in, String out) {
    // Try a few different matchers which all match '-' and not 'x'
    for (char replacement : new char[] { '_', '-' }) {
      String expected = out.replace('_', replacement);
      assertEqualsSame(expected, in, is('-').trimAndCollapseFrom(in, replacement));
      assertEqualsSame(expected, in, is('-').or(is('#')).trimAndCollapseFrom(in, replacement));
      assertEqualsSame(expected, in, isNot('x').trimAndCollapseFrom(in, replacement));
      assertEqualsSame(expected, in, is('x').negate().trimAndCollapseFrom(in, replacement));
      assertEqualsSame(expected, in, anyOf("-").trimAndCollapseFrom(in, replacement));
      assertEqualsSame(expected, in, anyOf("-#").trimAndCollapseFrom(in, replacement));
      assertEqualsSame(expected, in, anyOf("-#123").trimAndCollapseFrom(in, replacement));
    }
  }

  public void testReplaceFrom() {
    assertEquals("yoho", is('a').replaceFrom("yaha", 'o'));
    assertEquals("yh", is('a').replaceFrom("yaha", ""));
    assertEquals("yoho", is('a').replaceFrom("yaha", "o"));
    assertEquals("yoohoo", is('a').replaceFrom("yaha", "oo"));
    assertEquals("12 &gt; 5", is('>').replaceFrom("12 > 5", "&gt;"));
  }

  public void testPrecomputedOptimizations() {
    // These are testing behavior that's never promised by the API.
    // Some matchers are so efficient that it is a waste of effort to
    // build a precomputed version.
    CharMatcher m1 = is('x');
    assertSame(m1, m1.precomputed());
    assertSame(m1.toString(), m1.precomputed().toString());

    CharMatcher m2 = anyOf("Az");
    assertSame(m2, m2.precomputed());
    assertSame(m2.toString(), m2.precomputed().toString());

    CharMatcher m3 = inRange('A', 'Z');
    assertSame(m3, m3.precomputed());
    assertSame(m3.toString(), m3.precomputed().toString());

    assertSame(CharMatcher.NONE, CharMatcher.NONE.precomputed());
    assertSame(CharMatcher.ANY, CharMatcher.ANY.precomputed());
  }

  static void checkExactMatches(CharMatcher m, char[] chars) {
    Set<Character> positive = Sets.newHashSetWithExpectedSize(chars.length);
    for (int i = 0; i < chars.length; i++) {
      positive.add(chars[i]);
    }
    for (int c = 0; c <= Character.MAX_VALUE; c++) {
      assertFalse(positive.contains(new Character((char) c)) ^ m.matches((char) c));
    }
  }

  static char[] randomChars(Random rand, int size) {
    Set<Character> chars = new HashSet<Character>(size);
    for (int i = 0; i < size; i++) {
      char c;
      while (true) {
        c = (char) rand.nextInt(Character.MAX_VALUE - Character.MIN_VALUE + 1);
        if (!chars.contains(c)) {
          break;
        }
      }
      chars.add(c);
    }
    char[] retValue = new char[chars.size()];
    int i = 0;
    for (char c : chars) {
      retValue[i++] = c;
    }
    Arrays.sort(retValue);
    return retValue;
  }

  public void testToString() {
    assertToStringWorks("CharMatcher.NONE", CharMatcher.anyOf(""));
    assertToStringWorks("CharMatcher.is('\\u0031')", CharMatcher.anyOf("1"));
    assertToStringWorks("CharMatcher.isNot('\\u0031')", CharMatcher.isNot('1'));
    assertToStringWorks("CharMatcher.anyOf(\"\\u0031\\u0032\")", CharMatcher.anyOf("12"));
    assertToStringWorks("CharMatcher.anyOf(\"\\u0031\\u0032\\u0033\")",
        CharMatcher.anyOf("321"));
    assertToStringWorks("CharMatcher.inRange('\\u0031', '\\u0033')",
        CharMatcher.inRange('1', '3'));
  }

  private static void assertToStringWorks(String expected, CharMatcher matcher) {
    assertEquals(expected, matcher.toString());
    assertEquals(expected, matcher.precomputed().toString());
    assertEquals(expected, matcher.negate().negate().toString());
    assertEquals(expected, matcher.negate().precomputed().negate().toString());
    assertEquals(expected, matcher.negate().precomputed().negate().precomputed().toString());
  }
}

