diff --git a/android/guava-tests/test/com/google/common/base/StringsTest.java b/android/guava-tests/test/com/google/common/base/StringsTest.java index 8b174a02922f..a544889ed341 100644 --- a/android/guava-tests/test/com/google/common/base/StringsTest.java +++ b/android/guava-tests/test/com/google/common/base/StringsTest.java @@ -211,6 +211,25 @@ public void testValidSurrogatePairAt() { assertFalse(Strings.validSurrogatePairAt("\uD8ABx", 0)); } + public void testLenientFormat() { + assertEquals("%s", Strings.lenientFormat("%s")); + assertEquals("5", Strings.lenientFormat("%s", 5)); + assertEquals("foo [5]", Strings.lenientFormat("foo", 5)); + assertEquals("foo [5, 6, 7]", Strings.lenientFormat("foo", 5, 6, 7)); + assertEquals("%s 1 2", Strings.lenientFormat("%s %s %s", "%s", 1, 2)); + assertEquals(" [5, 6]", Strings.lenientFormat("", 5, 6)); + assertEquals("123", Strings.lenientFormat("%s%s%s", 1, 2, 3)); + assertEquals("1%s%s", Strings.lenientFormat("%s%s%s", 1)); + assertEquals("5 + 6 = 11", Strings.lenientFormat("%s + 6 = 11", 5)); + assertEquals("5 + 6 = 11", Strings.lenientFormat("5 + %s = 11", 6)); + assertEquals("5 + 6 = 11", Strings.lenientFormat("5 + 6 = %s", 11)); + assertEquals("5 + 6 = 11", Strings.lenientFormat("%s + %s = %s", 5, 6, 11)); + assertEquals("null [null, null]", Strings.lenientFormat("%s", null, null, null)); + assertEquals("null [5, 6]", Strings.lenientFormat(null, 5, 6)); + assertEquals("null", Strings.lenientFormat("%s", (Object) null)); + assertEquals("(Object[])null", Strings.lenientFormat("%s", (Object[]) null)); + } + @GwtIncompatible // NullPointerTester public void testNullPointers() { NullPointerTester tester = new NullPointerTester(); diff --git a/android/guava/src/com/google/common/base/Strings.java b/android/guava/src/com/google/common/base/Strings.java index 998d1a50bedd..6eb7f6626b4e 100644 --- a/android/guava/src/com/google/common/base/Strings.java +++ b/android/guava/src/com/google/common/base/Strings.java @@ -210,6 +210,72 @@ public static String commonSuffix(CharSequence a, CharSequence b) { return a.subSequence(a.length() - s, a.length()).toString(); } + /** + * Returns the given {@code template} string with each occurrence of {@code "%s"} replaced with + * the corresponding argument value from {@code args}; or, if the placeholder and argument counts + * do not match, returns a best-effort form of that string. Will not throw an exception under any + * circumstances (as long as all arguments' {@code toString} methods successfully return). + * + *

Note: For most string-formatting needs, use {@link String#format}, {@link + * PrintWriter#format}, and related methods. These support the full range of {@linkplain + * Formatter#syntax format specifiers}, and alert you to usage errors by throwing {@link + * InvalidFormatException}. + * + *

In certain cases, such as outputting debugging information or constructing a message to be + * used for another unchecked exception, an exception during string formatting would serve little + * purpose except to supplant the real information you were trying to provide. These are the cases + * this method is made for; it instead generates a best-effort string with all supplied argument + * values present. This method is also useful in environments such as GWT where {@code + * String.format} is not available. As an example, method implementations of the {@link + * Preconditions} class use this formatter, for both of the reasons just discussed. + * + *

Warning: Only the exact two-character placeholder sequence {@code "%s"} is + * recognized. + * + * @param template a string containing zero or more {@code "%s"} placeholder sequences. {@code + * null} is treated as the four-character string {@code "null"}. + * @param args the arguments to be substituted into the message template. The first argument + * specified is substituted for the first occurrence of {@code "%s"} in the template, and so + * forth. A {@code null} argument is converted to the four-character string {@code "null"}; + * non-null values are converted to strings using {@link Object#toString()}. + * @since NEXT + */ + // TODO(diamondm) consider using Arrays.toString() for array parameters + // TODO(diamondm) capture exceptions thrown from arguments' toString methods + public static String lenientFormat(@NullableDecl String template, @NullableDecl Object... args) { + template = String.valueOf(template); // null -> "null" + + args = args == null ? new Object[] {"(Object[])null"} : args; + + // start substituting the arguments into the '%s' placeholders + StringBuilder builder = new StringBuilder(template.length() + 16 * args.length); + int templateStart = 0; + int i = 0; + while (i < args.length) { + int placeholderStart = template.indexOf("%s", templateStart); + if (placeholderStart == -1) { + break; + } + builder.append(template, templateStart, placeholderStart); + builder.append(args[i++]); + templateStart = placeholderStart + 2; + } + builder.append(template, templateStart, template.length()); + + // if we run out of placeholders, append the extra args in square braces + if (i < args.length) { + builder.append(" ["); + builder.append(args[i++]); + while (i < args.length) { + builder.append(", "); + builder.append(args[i++]); + } + builder.append(']'); + } + + return builder.toString(); + } + /** * True when a valid surrogate pair starts at the given {@code index} in the given {@code string}. * Out-of-range indexes return false. diff --git a/guava-gwt/test/com/google/common/base/StringsTest_gwt.java b/guava-gwt/test/com/google/common/base/StringsTest_gwt.java index 68ebbb6c6522..1f45e5522f07 100644 --- a/guava-gwt/test/com/google/common/base/StringsTest_gwt.java +++ b/guava-gwt/test/com/google/common/base/StringsTest_gwt.java @@ -38,6 +38,11 @@ public void testIsNullOrEmpty() throws Exception { testCase.testIsNullOrEmpty(); } +public void testLenientFormat() throws Exception { + com.google.common.base.StringsTest testCase = new com.google.common.base.StringsTest(); + testCase.testLenientFormat(); +} + public void testNullToEmpty() throws Exception { com.google.common.base.StringsTest testCase = new com.google.common.base.StringsTest(); testCase.testNullToEmpty(); diff --git a/guava-tests/test/com/google/common/base/StringsTest.java b/guava-tests/test/com/google/common/base/StringsTest.java index 8b174a02922f..a544889ed341 100644 --- a/guava-tests/test/com/google/common/base/StringsTest.java +++ b/guava-tests/test/com/google/common/base/StringsTest.java @@ -211,6 +211,25 @@ public void testValidSurrogatePairAt() { assertFalse(Strings.validSurrogatePairAt("\uD8ABx", 0)); } + public void testLenientFormat() { + assertEquals("%s", Strings.lenientFormat("%s")); + assertEquals("5", Strings.lenientFormat("%s", 5)); + assertEquals("foo [5]", Strings.lenientFormat("foo", 5)); + assertEquals("foo [5, 6, 7]", Strings.lenientFormat("foo", 5, 6, 7)); + assertEquals("%s 1 2", Strings.lenientFormat("%s %s %s", "%s", 1, 2)); + assertEquals(" [5, 6]", Strings.lenientFormat("", 5, 6)); + assertEquals("123", Strings.lenientFormat("%s%s%s", 1, 2, 3)); + assertEquals("1%s%s", Strings.lenientFormat("%s%s%s", 1)); + assertEquals("5 + 6 = 11", Strings.lenientFormat("%s + 6 = 11", 5)); + assertEquals("5 + 6 = 11", Strings.lenientFormat("5 + %s = 11", 6)); + assertEquals("5 + 6 = 11", Strings.lenientFormat("5 + 6 = %s", 11)); + assertEquals("5 + 6 = 11", Strings.lenientFormat("%s + %s = %s", 5, 6, 11)); + assertEquals("null [null, null]", Strings.lenientFormat("%s", null, null, null)); + assertEquals("null [5, 6]", Strings.lenientFormat(null, 5, 6)); + assertEquals("null", Strings.lenientFormat("%s", (Object) null)); + assertEquals("(Object[])null", Strings.lenientFormat("%s", (Object[]) null)); + } + @GwtIncompatible // NullPointerTester public void testNullPointers() { NullPointerTester tester = new NullPointerTester(); diff --git a/guava/src/com/google/common/base/Strings.java b/guava/src/com/google/common/base/Strings.java index 20d82488bcc7..095c7a3cfb44 100644 --- a/guava/src/com/google/common/base/Strings.java +++ b/guava/src/com/google/common/base/Strings.java @@ -209,6 +209,73 @@ public static String commonSuffix(CharSequence a, CharSequence b) { return a.subSequence(a.length() - s, a.length()).toString(); } + /** + * Returns the given {@code template} string with each occurrence of {@code "%s"} replaced with + * the corresponding argument value from {@code args}; or, if the placeholder and argument counts + * do not match, returns a best-effort form of that string. Will not throw an exception under any + * circumstances (as long as all arguments' {@code toString} methods successfully return). + * + *

Note: For most string-formatting needs, use {@link String#format}, {@link + * PrintWriter#format}, and related methods. These support the full range of {@linkplain + * Formatter#syntax format specifiers}, and alert you to usage errors by throwing {@link + * InvalidFormatException}. + * + *

In certain cases, such as outputting debugging information or constructing a message to be + * used for another unchecked exception, an exception during string formatting would serve little + * purpose except to supplant the real information you were trying to provide. These are the cases + * this method is made for; it instead generates a best-effort string with all supplied argument + * values present. This method is also useful in environments such as GWT where {@code + * String.format} is not available. As an example, method implementations of the {@link + * Preconditions} class use this formatter, for both of the reasons just discussed. + * + *

Warning: Only the exact two-character placeholder sequence {@code "%s"} is + * recognized. + * + * @param template a string containing zero or more {@code "%s"} placeholder sequences. {@code + * null} is treated as the four-character string {@code "null"}. + * @param args the arguments to be substituted into the message template. The first argument + * specified is substituted for the first occurrence of {@code "%s"} in the template, and so + * forth. A {@code null} argument is converted to the four-character string {@code "null"}; + * non-null values are converted to strings using {@link Object#toString()}. + * @since NEXT + */ + // TODO(diamondm) consider using Arrays.toString() for array parameters + // TODO(diamondm) capture exceptions thrown from arguments' toString methods + public static String lenientFormat( + @Nullable String template, @Nullable Object @Nullable... args) { + template = String.valueOf(template); // null -> "null" + + args = args == null ? new Object[] {"(Object[])null"} : args; + + // start substituting the arguments into the '%s' placeholders + StringBuilder builder = new StringBuilder(template.length() + 16 * args.length); + int templateStart = 0; + int i = 0; + while (i < args.length) { + int placeholderStart = template.indexOf("%s", templateStart); + if (placeholderStart == -1) { + break; + } + builder.append(template, templateStart, placeholderStart); + builder.append(args[i++]); + templateStart = placeholderStart + 2; + } + builder.append(template, templateStart, template.length()); + + // if we run out of placeholders, append the extra args in square braces + if (i < args.length) { + builder.append(" ["); + builder.append(args[i++]); + while (i < args.length) { + builder.append(", "); + builder.append(args[i++]); + } + builder.append(']'); + } + + return builder.toString(); + } + /** * True when a valid surrogate pair starts at the given {@code index} in the given {@code string}. * Out-of-range indexes return false.