Skylark: implemented more string functions (isalnum, isdigit, etc).

--
MOS_MIGRATED_REVID=110261986
diff --git a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
index f43f714..a199ab4 100644
--- a/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
+++ b/src/main/java/com/google/devtools/build/lib/syntax/MethodLibrary.java
@@ -674,27 +674,147 @@
       Pattern.compile("(?<line>.*)(?<break>(\\r\\n|\\r|\\n)?)");
 
   @SkylarkSignature(name = "isalpha", objectType = StringModule.class, returnType = Boolean.class,
-    doc = "Returns True if all characters in the string are alphabetic ([a-zA-Z]) and it "
-        + "contains at least one character.",
+    doc = "Returns True if all characters in the string are alphabetic ([a-zA-Z]) and there is "
+        + "at least one character.",
     mandatoryPositionals = {
         @Param(name = "self", type = String.class, doc = "This string.")})
   private static BuiltinFunction isalpha = new BuiltinFunction("isalpha") {
+    @SuppressWarnings("unused") // Called via Reflection
     public Boolean invoke(String self) throws EvalException {
-      int length = self.length();
-      if (length < 1) {
-        return false;
-      }
-      for (int index = 0; index < length; index++) {
-        char character = self.charAt(index);
-        if (!((character >= 'A' && character <= 'Z')
-            || (character >= 'a' && character <= 'z'))) {
-          return false;
-        }
-      }
-      return true;
+      return MethodLibrary.matches(self, MethodLibrary.ALPHA, false);
     }
   };
 
+  @SkylarkSignature(name = "isalnum", objectType = StringModule.class, returnType = Boolean.class,
+      doc =
+      "Returns True if all characters in the string are alphanumeric ([a-zA-Z0-9]) and there is "
+      + "at least one character.",
+      mandatoryPositionals = {@Param(name = "self", type = String.class, doc = "This string.")})
+  private static BuiltinFunction isAlnum = new BuiltinFunction("isalnum") {
+    @SuppressWarnings("unused") // Called via Reflection
+    public Boolean invoke(String self) throws EvalException {
+      return MethodLibrary.matches(self, MethodLibrary.ALNUM, false);
+    }
+  };
+
+  @SkylarkSignature(name = "isdigit", objectType = StringModule.class, returnType = Boolean.class,
+      doc =
+      "Returns True if all characters in the string are digits ([0-9]) and there is "
+      + "at least one character.",
+      mandatoryPositionals = {@Param(name = "self", type = String.class, doc = "This string.")})
+  private static BuiltinFunction isDigit = new BuiltinFunction("isdigit") {
+    @SuppressWarnings("unused") // Called via Reflection
+    public Boolean invoke(String self) throws EvalException {
+      return MethodLibrary.matches(self, MethodLibrary.DIGIT, false);
+    }
+  };
+
+  @SkylarkSignature(name = "isspace", objectType = StringModule.class, returnType = Boolean.class,
+      doc =
+      "Returns True if all characters are white space characters and the string "
+      + "contains at least one character.",
+      mandatoryPositionals = {@Param(name = "self", type = String.class, doc = "This string.")})
+  private static BuiltinFunction isSpace = new BuiltinFunction("isspace") {
+    @SuppressWarnings("unused") // Called via Reflection
+    public Boolean invoke(String self) throws EvalException {
+      return MethodLibrary.matches(self, MethodLibrary.SPACE, false);
+    }
+  };
+
+  @SkylarkSignature(name = "islower", objectType = StringModule.class, returnType = Boolean.class,
+      doc =
+      "Returns True if all cased characters in the string are lowercase and there is "
+      + "at least one character.",
+      mandatoryPositionals = {@Param(name = "self", type = String.class, doc = "This string.")})
+  private static BuiltinFunction isLower = new BuiltinFunction("islower") {
+    @SuppressWarnings("unused") // Called via Reflection
+    public Boolean invoke(String self) throws EvalException {
+      // Python also accepts non-cased characters, so we cannot use LOWER.
+      return MethodLibrary.matches(self, MethodLibrary.UPPER.negate(), true);
+    }
+  };
+
+  @SkylarkSignature(name = "isupper", objectType = StringModule.class, returnType = Boolean.class,
+      doc =
+      "Returns True if all cased characters in the string are uppercase and there is "
+      + "at least one character.",
+      mandatoryPositionals = {@Param(name = "self", type = String.class, doc = "This string.")})
+  private static BuiltinFunction isUpper = new BuiltinFunction("isupper") {
+    @SuppressWarnings("unused") // Called via Reflection
+    public Boolean invoke(String self) throws EvalException {
+      // Python also accepts non-cased characters, so we cannot use UPPER.
+      return MethodLibrary.matches(self, MethodLibrary.LOWER.negate(), true);
+    }
+  };
+
+  @SkylarkSignature(name = "istitle", objectType = StringModule.class, returnType = Boolean.class,
+      doc =
+      "Returns True if the string is in title case and it contains at least one character. "
+      + "This means that every uppercase character must follow an uncased one (e.g. whitespace) "
+      + "and every lowercase character must follow a cased one (e.g. uppercase or lowercase).",
+      mandatoryPositionals = {@Param(name = "self", type = String.class, doc = "This string.")})
+  private static BuiltinFunction isTitle = new BuiltinFunction("istitle") {
+    @SuppressWarnings("unused") // Called via Reflection
+    public Boolean invoke(String self) throws EvalException {
+      if (self.isEmpty()) {
+        return false;
+      }
+      // From the Python documentation: "uppercase characters may only follow uncased characters
+      // and lowercase characters only cased ones".
+      char[] data = self.toCharArray();
+      CharMatcher matcher = CharMatcher.ANY;
+      char leftMostCased = ' ';
+      for (int pos = data.length - 1; pos >= 0; --pos) {
+        char current = data[pos];
+        // 1. Check condition that was determined by the right neighbor.
+        if (!matcher.matches(current)) {
+          return false;
+        }
+        // 2. Determine condition for the left neighbor.
+        if (LOWER.matches(current)) {
+          matcher = CASED;
+        } else if (UPPER.matches(current)) {
+          matcher = CASED.negate();
+        } else {
+          matcher = CharMatcher.ANY;
+        }
+        // 3. Store character if it is cased.
+        if (CASED.matches(current)) {
+          leftMostCased = current;
+        }
+      }
+      // The leftmost cased letter must be uppercase. If leftMostCased is not a cased letter here,
+      // then the string doesn't have any cased letter, so UPPER.test will return false.
+      return UPPER.matches(leftMostCased);
+    }
+  };
+
+  private static boolean matches(
+      String str, CharMatcher matcher, boolean requiresAtLeastOneCasedLetter) {
+    if (str.isEmpty()) {
+      return false;
+    } else if (!requiresAtLeastOneCasedLetter) {
+      return matcher.matchesAllOf(str);
+    }
+    int casedLetters = 0;
+    for (char current : str.toCharArray()) {
+      if (!matcher.matches(current)) {
+        return false;
+      } else if (requiresAtLeastOneCasedLetter && CASED.matches(current)) {
+        ++casedLetters;
+      }
+    }
+    return casedLetters > 0;
+  }
+
+  private static final CharMatcher DIGIT = CharMatcher.javaDigit();
+  private static final CharMatcher LOWER = CharMatcher.inRange('a', 'z');
+  private static final CharMatcher UPPER = CharMatcher.inRange('A', 'Z');
+  private static final CharMatcher ALPHA = LOWER.or(UPPER);
+  private static final CharMatcher ALNUM = ALPHA.or(DIGIT);
+  private static final CharMatcher CASED = ALPHA;
+  private static final CharMatcher SPACE = CharMatcher.WHITESPACE;
+
   @SkylarkSignature(name = "count", objectType = StringModule.class, returnType = Integer.class,
       doc = "Returns the number of (non-overlapping) occurrences of substring <code>sub</code> in "
           + "string, optionally restricting to [<code>start</code>:<code>end</code>], "
diff --git a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
index d1e307a..de20fc2 100644
--- a/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
+++ b/src/test/java/com/google/devtools/build/lib/syntax/MethodLibraryTest.java
@@ -193,6 +193,79 @@
   }
 
   @Test
+  public void testStringIsAlnum() throws Exception {
+    new SkylarkTest()
+        .testStatement("''.isalnum()", false)
+        .testStatement("'a0 33'.isalnum()", false)
+        .testStatement("'1'.isalnum()", true)
+        .testStatement("'a033'.isalnum()", true);
+  }
+
+  @Test
+  public void testStringIsDigit() throws Exception {
+    new SkylarkTest()
+        .testStatement("''.isdigit()", false)
+        .testStatement("' '.isdigit()", false)
+        .testStatement("'a'.isdigit()", false)
+        .testStatement("'0234325.33'.isdigit()", false)
+        .testStatement("'1'.isdigit()", true)
+        .testStatement("'033'.isdigit()", true);
+  }
+
+  @Test
+  public void testStringIsSpace() throws Exception {
+    new SkylarkTest()
+        .testStatement("''.isspace()", false)
+        .testStatement("'a'.isspace()", false)
+        .testStatement("'1'.isspace()", false)
+        .testStatement("'\\ta\\n'.isspace()", false)
+        .testStatement("' '.isspace()", true)
+        .testStatement("'\\t\\n'.isspace()", true);
+  }
+
+  @Test
+  public void testStringIsLower() throws Exception {
+    new SkylarkTest()
+        .testStatement("''.islower()", false)
+        .testStatement("' '.islower()", false)
+        .testStatement("'1'.islower()", false)
+        .testStatement("'Almost'.islower()", false)
+        .testStatement("'abc'.islower()", true)
+        .testStatement("' \\nabc'.islower()", true)
+        .testStatement("'abc def\\n'.islower()", true)
+        .testStatement("'\\ta\\n'.islower()", true);
+  }
+
+  @Test
+  public void testStringIsUpper() throws Exception {
+    new SkylarkTest()
+        .testStatement("''.isupper()", false)
+        .testStatement("' '.isupper()", false)
+        .testStatement("'1'.isupper()", false)
+        .testStatement("'aLMOST'.isupper()", false)
+        .testStatement("'ABC'.isupper()", true)
+        .testStatement("' \\nABC'.isupper()", true)
+        .testStatement("'ABC DEF\\n'.isupper()", true)
+        .testStatement("'\\tA\\n'.isupper()", true);
+  }
+
+  @Test
+  public void testStringIsTitle() throws Exception {
+    new SkylarkTest()
+        .testStatement("''.istitle()", false)
+        .testStatement("' '.istitle()", false)
+        .testStatement("'134'.istitle()", false)
+        .testStatement("'almost Correct'.istitle()", false)
+        .testStatement("'1nope Nope Nope'.istitle()", false)
+        .testStatement("'NO Way'.istitle()", false)
+        .testStatement("'T'.istitle()", true)
+        .testStatement("'Correct'.istitle()", true)
+        .testStatement("'Very Correct! Yes\\nIndeed1X'.istitle()", true)
+        .testStatement("'1234Ab Ab'.istitle()", true)
+        .testStatement("'\\tA\\n'.istitle()", true);
+  }
+
+  @Test
   public void testStackTraceLocation() throws Exception {
     new SkylarkTest().testIfErrorContains(
         "Traceback (most recent call last):\n\t"