// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.lib.syntax;
import static java.util.stream.Collectors.joining;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Ordering;
import com.google.devtools.build.lib.collect.nestedset.NestedSet;
import com.google.devtools.build.lib.events.Event;
import com.google.devtools.build.lib.events.Location;
import com.google.devtools.build.lib.skylarkinterface.Param;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModule;
import com.google.devtools.build.lib.skylarkinterface.SkylarkModuleCategory;
import com.google.devtools.build.lib.skylarkinterface.SkylarkSignature;
import com.google.devtools.build.lib.syntax.EvalUtils.ComparisonException;
import com.google.devtools.build.lib.syntax.SkylarkList.MutableList;
import com.google.devtools.build.lib.syntax.SkylarkList.Tuple;
import com.google.devtools.build.lib.syntax.Type.ConversionException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** A helper class containing built in functions for the Skylark language. */
public class MethodLibrary {
private MethodLibrary() {}
// Emulate Python substring function
// It converts out of range indices, and never fails
private static String pythonSubstring(String str, int start, Object end, String msg)
throws ConversionException {
if (start == 0 && EvalUtils.isNullOrNone(end)) {
return str;
}
start = EvalUtils.clampRangeEndpoint(start, str.length());
int stop;
if (EvalUtils.isNullOrNone(end)) {
stop = str.length();
} else {
stop = EvalUtils.clampRangeEndpoint(Type.INTEGER.convert(end, msg), str.length());
}
if (start >= stop) {
return "";
}
return str.substring(start, stop);
}
// supported string methods
@SkylarkSignature(name = "join", objectType = StringModule.class, returnType = String.class,
doc = "Returns a string in which the string elements of the argument have been "
+ "joined by this string as a separator. Example: "
+ "
\"|\".join([\"a\", \"b\", \"c\"]) == \"a|b|c\"
",
parameters = {
@Param(name = "self", type = String.class, doc = "This string, a separator."),
@Param(name = "elements", type = SkylarkList.class, doc = "The objects to join.")})
private static final BuiltinFunction join = new BuiltinFunction("join") {
public String invoke(String self, SkylarkList> elements) throws ConversionException {
return Joiner.on(self).join(elements);
}
};
@SkylarkSignature(name = "lower", objectType = StringModule.class, returnType = String.class,
doc = "Returns the lower case version of this string.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string, to convert to lower case.")})
private static final BuiltinFunction lower = new BuiltinFunction("lower") {
public String invoke(String self) {
return self.toLowerCase();
}
};
@SkylarkSignature(name = "upper", objectType = StringModule.class, returnType = String.class,
doc = "Returns the upper case version of this string.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string, to convert to upper case.")})
private static final BuiltinFunction upper = new BuiltinFunction("upper") {
public String invoke(String self) {
return self.toUpperCase();
}
};
/**
* For consistency with Python we recognize the same whitespace characters as they do over the
* range 0x00-0xFF. See https://hg.python.org/cpython/file/3.6/Objects/unicodetype_db.h#l5738
* This list is a consequence of Unicode character information.
*
* Note that this differs from Python 2.7, which uses ctype.h#isspace(), and from
* java.lang.Character#isWhitespace(), which does not recognize U+00A0.
*/
private static final String LATIN1_WHITESPACE = (
"\u0009"
+ "\n"
+ "\u000B"
+ "\u000C"
+ "\r"
+ "\u001C"
+ "\u001D"
+ "\u001E"
+ "\u001F"
+ "\u0020"
+ "\u0085"
+ "\u00A0"
);
private static String stringLStrip(String self, String chars) {
CharMatcher matcher = CharMatcher.anyOf(chars);
for (int i = 0; i < self.length(); i++) {
if (!matcher.matches(self.charAt(i))) {
return self.substring(i);
}
}
return ""; // All characters were stripped.
}
private static String stringRStrip(String self, String chars) {
CharMatcher matcher = CharMatcher.anyOf(chars);
for (int i = self.length() - 1; i >= 0; i--) {
if (!matcher.matches(self.charAt(i))) {
return self.substring(0, i + 1);
}
}
return ""; // All characters were stripped.
}
@SkylarkSignature(
name = "lstrip",
objectType = StringModule.class,
returnType = String.class,
doc =
"Returns a copy of the string where leading characters that appear in chars"
+ "are removed."
+ "
"
+ "\"abcba\".lstrip(\"ba\") == \"cba\""
+ "
",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(
name = "chars",
type = String.class,
noneable = true,
doc = "The characters to remove, or all whitespace if None.",
defaultValue = "None"
)
}
)
private static final BuiltinFunction lstrip =
new BuiltinFunction("lstrip") {
public String invoke(String self, Object charsOrNone) {
String chars = charsOrNone != Runtime.NONE ? (String) charsOrNone : LATIN1_WHITESPACE;
return stringLStrip(self, chars);
}
};
@SkylarkSignature(
name = "rstrip",
objectType = StringModule.class,
returnType = String.class,
doc =
"Returns a copy of the string where trailing characters that appear in chars"
+ "are removed."
+ "
"
+ "\"abcbaa\".rstrip(\"ab\") == \"abc\""
+ "
",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(
name = "chars",
type = String.class,
noneable = true,
doc = "The characters to remove, or all whitespace if None.",
defaultValue = "None"
)
}
)
private static final BuiltinFunction rstrip =
new BuiltinFunction("rstrip") {
public String invoke(String self, Object charsOrNone) {
String chars = charsOrNone != Runtime.NONE ? (String) charsOrNone : LATIN1_WHITESPACE;
return stringRStrip(self, chars);
}
};
@SkylarkSignature(
name = "strip",
objectType = StringModule.class,
returnType = String.class,
doc =
"Returns a copy of the string where trailing characters that appear in chars"
+ "are removed."
+ "
"
+ "\"aabcbcbaa\".strip(\"ab\") == \"cbc\""
+ "
",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(
name = "chars",
type = String.class,
noneable = true,
doc = "The characters to remove, or all whitespace if None.",
defaultValue = "None"
)
}
)
private static final BuiltinFunction strip =
new BuiltinFunction("strip") {
public String invoke(String self, Object charsOrNone) {
String chars = charsOrNone != Runtime.NONE ? (String) charsOrNone : LATIN1_WHITESPACE;
return stringLStrip(stringRStrip(self, chars), chars);
}
};
@SkylarkSignature(
name = "replace",
objectType = StringModule.class,
returnType = String.class,
doc =
"Returns a copy of the string in which the occurrences "
+ "of old have been replaced with new, optionally "
+ "restricting the number of replacements to maxsplit.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "old", type = String.class, doc = "The string to be replaced."),
@Param(name = "new", type = String.class, doc = "The string to replace with."),
@Param(
name = "maxsplit",
type = Integer.class,
noneable = true,
defaultValue = "None",
doc = "The maximum number of replacements."
)
},
useLocation = true
)
private static final BuiltinFunction replace =
new BuiltinFunction("replace") {
public String invoke(
String self, String oldString, String newString, Object maxSplitO, Location loc)
throws EvalException {
StringBuffer sb = new StringBuffer();
Integer maxSplit =
Type.INTEGER.convertOptional(
maxSplitO, "'maxsplit' argument of 'replace'", /*label*/ null, Integer.MAX_VALUE);
try {
Matcher m = Pattern.compile(oldString, Pattern.LITERAL).matcher(self);
for (int i = 0; i < maxSplit && m.find(); i++) {
m.appendReplacement(sb, Matcher.quoteReplacement(newString));
}
m.appendTail(sb);
} catch (IllegalStateException e) {
throw new EvalException(loc, e.getMessage() + " in call to replace");
}
return sb.toString();
}
};
@SkylarkSignature(
name = "split",
objectType = StringModule.class,
returnType = MutableList.class,
doc =
"Returns a list of all the words in the string, using sep as the separator, "
+ "optionally limiting the number of splits to maxsplit.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sep", type = String.class, doc = "The string to split on."),
@Param(
name = "maxsplit",
type = Integer.class,
noneable = true,
defaultValue = "None",
doc = "The maximum number of splits."
)
},
useEnvironment = true,
useLocation = true
)
private static final BuiltinFunction split =
new BuiltinFunction("split") {
public MutableList invoke(
String self, String sep, Object maxSplitO, Location loc, Environment env)
throws EvalException {
int maxSplit =
Type.INTEGER.convertOptional(
maxSplitO, "'split' argument of 'split'", /*label*/ null, -2);
// + 1 because the last result is the remainder. The default is -2 so that after +1,
// it becomes -1.
String[] ss = Pattern.compile(sep, Pattern.LITERAL).split(self, maxSplit + 1);
return MutableList.of(env, ss);
}
};
@SkylarkSignature(
name = "rsplit",
objectType = StringModule.class,
returnType = MutableList.class,
doc =
"Returns a list of all the words in the string, using sep as the separator, "
+ "optionally limiting the number of splits to maxsplit. "
+ "Except for splitting from the right, this method behaves like split().",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sep", type = String.class, doc = "The string to split on."),
@Param(
name = "maxsplit",
type = Integer.class,
noneable = true,
defaultValue = "None",
doc = "The maximum number of splits."
)
},
useEnvironment = true,
useLocation = true
)
private static final BuiltinFunction rsplit =
new BuiltinFunction("rsplit") {
@SuppressWarnings("unused")
public MutableList invoke(
String self, String sep, Object maxSplitO, Location loc, Environment env)
throws EvalException {
int maxSplit =
Type.INTEGER.convertOptional(maxSplitO, "'split' argument of 'split'", null, -1);
try {
return stringRSplit(self, sep, maxSplit, env);
} catch (IllegalArgumentException ex) {
throw new EvalException(loc, ex);
}
}
};
/**
* Splits the given string into a list of words, using {@code separator} as a delimiter.
*
*
At most {@code maxSplits} will be performed, going from right to left.
*
* @param input The input string.
* @param separator The separator string.
* @param maxSplits The maximum number of splits. Negative values mean unlimited splits.
* @return A list of words
* @throws IllegalArgumentException
*/
private static MutableList stringRSplit(
String input, String separator, int maxSplits, Environment env) {
if (separator.isEmpty()) {
throw new IllegalArgumentException("Empty separator");
}
if (maxSplits <= 0) {
maxSplits = Integer.MAX_VALUE;
}
LinkedList result = new LinkedList<>();
String[] parts = input.split(Pattern.quote(separator), -1);
int sepLen = separator.length();
int remainingLength = input.length();
int splitsSoFar = 0;
// Copies parts from the array into the final list, starting at the end (because
// it's rsplit), as long as fewer than maxSplits splits are performed. The
// last spot in the list is reserved for the remaining string, whose length
// has to be tracked throughout the loop.
for (int pos = parts.length - 1; (pos >= 0) && (splitsSoFar < maxSplits); --pos) {
String current = parts[pos];
result.addFirst(current);
++splitsSoFar;
remainingLength -= sepLen + current.length();
}
if (splitsSoFar == maxSplits && remainingLength >= 0) {
result.addFirst(input.substring(0, remainingLength));
}
return new MutableList<>(result, env);
}
@SkylarkSignature(name = "partition", objectType = StringModule.class,
returnType = Tuple.class,
doc = "Splits the input string at the first occurrence of the separator "
+ "sep and returns the resulting partition as a three-element "
+ "tuple of the form (substring_before, separator, substring_after).",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sep", type = String.class,
defaultValue = "\" \"", doc = "The string to split on, default is space (\" \").")},
useEnvironment = true,
useLocation = true)
private static final BuiltinFunction partition = new BuiltinFunction("partition") {
@SuppressWarnings("unused")
public Tuple invoke(String self, String sep, Location loc, Environment env)
throws EvalException {
return partitionWrapper(self, sep, true, loc);
}
};
@SkylarkSignature(name = "rpartition", objectType = StringModule.class,
returnType = Tuple.class,
doc = "Splits the input string at the last occurrence of the separator "
+ "sep and returns the resulting partition as a three-element "
+ "tuple of the form (substring_before, separator, substring_after).",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sep", type = String.class,
defaultValue = "\" \"", doc = "The string to split on, default is space (\" \").")},
useEnvironment = true,
useLocation = true)
private static final BuiltinFunction rpartition = new BuiltinFunction("rpartition") {
@SuppressWarnings("unused")
public Tuple invoke(String self, String sep, Location loc, Environment env)
throws EvalException {
return partitionWrapper(self, sep, false, loc);
}
};
/**
* Wraps the stringPartition() method and converts its results and exceptions
* to the expected types.
*
* @param self The input string
* @param separator The string to split on
* @param forward A flag that controls whether the input string is split around
* the first ({@code true}) or last ({@code false}) occurrence of the separator.
* @param loc The location that is used for potential exceptions
* @return A list with three elements
*/
private static Tuple partitionWrapper(
String self, String separator, boolean forward, Location loc) throws EvalException {
try {
return Tuple.copyOf(stringPartition(self, separator, forward));
} catch (IllegalArgumentException ex) {
throw new EvalException(loc, ex);
}
}
/**
* Splits the input string at the {first|last} occurrence of the given separator and returns the
* resulting partition as a three-tuple of Strings, contained in a {@code MutableList}.
*
*
If the input string does not contain the separator, the tuple will consist of the original
* input string and two empty strings.
*
*
This method emulates the behavior of Python's str.partition() and str.rpartition(),
* depending on the value of the {@code forward} flag.
*
* @param input The input string
* @param separator The string to split on
* @param forward A flag that controls whether the input string is split around the first ({@code
* true}) or last ({@code false}) occurrence of the separator.
* @return A three-tuple (List) of the form [part_before_separator, separator,
* part_after_separator].
*/
private static List stringPartition(String input, String separator, boolean forward) {
if (separator.isEmpty()) {
throw new IllegalArgumentException("Empty separator");
}
int partitionSize = 3;
ArrayList result = new ArrayList<>(partitionSize);
int pos = forward ? input.indexOf(separator) : input.lastIndexOf(separator);
if (pos < 0) {
for (int i = 0; i < partitionSize; ++i) {
result.add("");
}
// Following Python's implementation of str.partition() and str.rpartition(),
// the input string is copied to either the first or the last position in the
// list, depending on the value of the forward flag.
result.set(forward ? 0 : partitionSize - 1, input);
} else {
result.add(input.substring(0, pos));
result.add(separator);
// pos + sep.length() is at most equal to input.length(). This worst-case
// happens when the separator is at the end of the input string. However,
// substring() will return an empty string in this scenario, thus making
// any additional safety checks obsolete.
result.add(input.substring(pos + separator.length()));
}
return result;
}
@SkylarkSignature(
name = "capitalize",
objectType = StringModule.class,
returnType = String.class,
doc =
"Returns a copy of the string with its first character capitalized and the rest "
+ "lowercased. This method does not support non-ascii characters.",
parameters = {@Param(name = "self", type = String.class, doc = "This string.")}
)
private static final BuiltinFunction capitalize =
new BuiltinFunction("capitalize") {
@SuppressWarnings("unused")
public String invoke(String self) throws EvalException {
if (self.isEmpty()) {
return self;
}
return Character.toUpperCase(self.charAt(0)) + self.substring(1).toLowerCase();
}
};
@SkylarkSignature(name = "title", objectType = StringModule.class,
returnType = String.class,
doc =
"Converts the input string into title case, i.e. every word starts with an "
+ "uppercase letter while the remaining letters are lowercase. In this "
+ "context, a word means strictly a sequence of letters. This method does "
+ "not support supplementary Unicode characters.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string.")})
private static final BuiltinFunction title = new BuiltinFunction("title") {
@SuppressWarnings("unused")
public String invoke(String self) throws EvalException {
char[] data = self.toCharArray();
boolean previousWasLetter = false;
for (int pos = 0; pos < data.length; ++pos) {
char current = data[pos];
boolean currentIsLetter = Character.isLetter(current);
if (currentIsLetter) {
if (previousWasLetter && Character.isUpperCase(current)) {
data[pos] = Character.toLowerCase(current);
} else if (!previousWasLetter && Character.isLowerCase(current)) {
data[pos] = Character.toUpperCase(current);
}
}
previousWasLetter = currentIsLetter;
}
return new String(data);
}
};
/**
* Common implementation for find, rfind, index, rindex.
* @param forward true if we want to return the last matching index.
*/
private static int stringFind(boolean forward,
String self, String sub, int start, Object end, String msg)
throws ConversionException {
String substr = pythonSubstring(self, start, end, msg);
int subpos = forward ? substr.indexOf(sub) : substr.lastIndexOf(sub);
start = EvalUtils.clampRangeEndpoint(start, self.length());
return subpos < 0 ? subpos : subpos + start;
}
@SkylarkSignature(name = "rfind", objectType = StringModule.class, returnType = Integer.class,
doc = "Returns the last index where sub is found, "
+ "or -1 if no such index exists, optionally restricting to "
+ "[start:end], "
+ "start being inclusive and end being exclusive.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to find."),
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Restrict to search from this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
doc = "optional position before which to restrict to search.")})
private static final BuiltinFunction rfind = new BuiltinFunction("rfind") {
public Integer invoke(String self, String sub, Integer start, Object end)
throws ConversionException {
return stringFind(false, self, sub, start, end, "'end' argument to rfind");
}
};
@SkylarkSignature(name = "find", objectType = StringModule.class, returnType = Integer.class,
doc = "Returns the first index where sub is found, "
+ "or -1 if no such index exists, optionally restricting to "
+ "[start:end], "
+ "start being inclusive and end being exclusive.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to find."),
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Restrict to search from this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
doc = "optional position before which to restrict to search.")})
private static final BuiltinFunction find = new BuiltinFunction("find") {
public Integer invoke(String self, String sub, Integer start, Object end)
throws ConversionException {
return stringFind(true, self, sub, start, end, "'end' argument to find");
}
};
@SkylarkSignature(
name = "rindex",
objectType = StringModule.class,
returnType = Integer.class,
doc =
"Returns the last index where sub is found, "
+ "or raises an error if no such index exists, optionally restricting to "
+ "[start:end], "
+ "start being inclusive and end being exclusive.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to find."),
@Param(
name = "start",
type = Integer.class,
defaultValue = "0",
doc = "Restrict to search from this position."
),
@Param(
name = "end",
type = Integer.class,
noneable = true,
defaultValue = "None",
doc = "optional position before which to restrict to search."
)
},
useLocation = true
)
private static final BuiltinFunction rindex =
new BuiltinFunction("rindex") {
public Integer invoke(String self, String sub, Integer start, Object end, Location loc)
throws EvalException {
int res = stringFind(false, self, sub, start, end, "'end' argument to rindex");
if (res < 0) {
throw new EvalException(loc, Printer.format("substring %r not found in %r", sub, self));
}
return res;
}
};
@SkylarkSignature(
name = "index",
objectType = StringModule.class,
returnType = Integer.class,
doc =
"Returns the first index where sub is found, "
+ "or raises an error if no such index exists, optionally restricting to "
+ "[start:end], "
+ "start being inclusive and end being exclusive.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to find."),
@Param(
name = "start",
type = Integer.class,
defaultValue = "0",
doc = "Restrict to search from this position."
),
@Param(
name = "end",
type = Integer.class,
noneable = true,
defaultValue = "None",
doc = "optional position before which to restrict to search."
)
},
useLocation = true
)
private static final BuiltinFunction index =
new BuiltinFunction("index") {
public Integer invoke(String self, String sub, Integer start, Object end, Location loc)
throws EvalException {
int res = stringFind(true, self, sub, start, end, "'end' argument to index");
if (res < 0) {
throw new EvalException(loc, Printer.format("substring %r not found in %r", sub, self));
}
return res;
}
};
@SkylarkSignature(name = "splitlines", objectType = StringModule.class,
returnType = SkylarkList.class,
doc =
"Splits the string at line boundaries ('\\n', '\\r\\n', '\\r') "
+ "and returns the result as a list.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "keepends", type = Boolean.class, defaultValue = "False",
doc = "Whether the line breaks should be included in the resulting list.")})
private static final BuiltinFunction splitLines = new BuiltinFunction("splitlines") {
@SuppressWarnings("unused")
public SkylarkList invoke(String self, Boolean keepEnds) throws EvalException {
List result = new ArrayList<>();
Matcher matcher = SPLIT_LINES_PATTERN.matcher(self);
while (matcher.find()) {
String line = matcher.group("line");
String lineBreak = matcher.group("break");
boolean trailingBreak = lineBreak.isEmpty();
if (line.isEmpty() && trailingBreak) {
break;
}
if (keepEnds && !trailingBreak) {
result.add(line + lineBreak);
} else {
result.add(line);
}
}
return SkylarkList.createImmutable(result);
}
};
private static final Pattern SPLIT_LINES_PATTERN =
Pattern.compile("(?.*)(?(\\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 there is "
+ "at least one character.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string.")})
private static final BuiltinFunction isalpha = new BuiltinFunction("isalpha") {
@SuppressWarnings("unused") // Called via Reflection
public Boolean invoke(String self) throws EvalException {
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.",
parameters = {@Param(name = "self", type = String.class, doc = "This string.")})
private static final 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.",
parameters = {@Param(name = "self", type = String.class, doc = "This string.")})
private static final 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.",
parameters = {@Param(name = "self", type = String.class, doc = "This string.")})
private static final 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.",
parameters = {@Param(name = "self", type = String.class, doc = "This string.")})
private static final 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.",
parameters = {@Param(name = "self", type = String.class, doc = "This string.")})
private static final 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).",
parameters = {@Param(name = "self", type = String.class, doc = "This string.")})
private static final 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 sub in "
+ "string, optionally restricting to [start:end], "
+ "start being inclusive and end being exclusive.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to count."),
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Restrict to search from this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
doc = "optional position before which to restrict to search.")})
private static final BuiltinFunction count = new BuiltinFunction("count") {
public Integer invoke(String self, String sub, Integer start, Object end)
throws ConversionException {
String str = pythonSubstring(self, start, end, "'end' operand of 'find'");
if (sub.isEmpty()) {
return str.length() + 1;
}
int count = 0;
int index = -1;
while ((index = str.indexOf(sub)) >= 0) {
count++;
str = str.substring(index + sub.length());
}
return count;
}
};
@SkylarkSignature(name = "endswith", objectType = StringModule.class, returnType = Boolean.class,
doc = "Returns True if the string ends with sub, "
+ "otherwise False, optionally restricting to [start:end], "
+ "start being inclusive and end being exclusive.",
parameters = {
@Param(name = "self", type = String.class, doc = "This string."),
@Param(name = "sub", type = String.class, doc = "The substring to check."),
@Param(name = "start", type = Integer.class, defaultValue = "0",
doc = "Test beginning at this position."),
@Param(name = "end", type = Integer.class, noneable = true, defaultValue = "None",
doc = "optional position at which to stop comparing.")})
private static final BuiltinFunction endswith = new BuiltinFunction("endswith") {
public Boolean invoke(String self, String sub, Integer start, Object end)
throws ConversionException {
return pythonSubstring(self, start, end, "'end' operand of 'endswith'").endsWith(sub);
}
};
// In Python, formatting is very complex.
// We handle here the simplest case which provides most of the value of the function.
// https://docs.python.org/3/library/string.html#formatstrings
@SkylarkSignature(
name = "format",
objectType = StringModule.class,
returnType = String.class,
doc =
"Perform string interpolation. Format strings contain replacement fields "
+ "surrounded by curly braces {}. Anything that is not contained "
+ "in braces is considered literal text, which is copied unchanged to the output."
+ "If you need to include a brace character in the literal text, it can be "
+ "escaped by doubling: {{ and }}"
+ "A replacement field can be either a name, a number, or empty. Values are "
+ "converted to strings using the str function."
+ "