From 30c975b7b982475eb9ea3acb3942d511ab72404e Mon Sep 17 00:00:00 2001 From: xingyutangyuan <147447743+xingyutangyuan@users.noreply.github.com> Date: Sat, 23 Dec 2023 20:02:47 -0800 Subject: [PATCH 1/2] Extract the StringFormat instance methods into AbstractStringFormat. This makes it easier to create alternative flavors of StringFormat using different placeholder styles such as , %, etc. --- mug/BUILD | 4 +- .../google/mu/util/AbstractStringFormat.java | 779 ++++++++++++++++++ .../java/com/google/mu/util/StringFormat.java | 771 +---------------- 3 files changed, 788 insertions(+), 766 deletions(-) create mode 100644 mug/src/main/java/com/google/mu/util/AbstractStringFormat.java diff --git a/mug/BUILD b/mug/BUILD index 496cd5e6df..daec5241bc 100644 --- a/mug/BUILD +++ b/mug/BUILD @@ -7,7 +7,7 @@ java_library( "src/main/java/com/google/mu/function/*.java", "src/main/java/com/google/mu/collect/*.java", ], - exclude = ["src/main/java/com/google/mu/util/StringFormat.java"]), + exclude = glob(["src/main/java/com/google/mu/util/*StringFormat.java"])), ) java_library( @@ -27,7 +27,7 @@ java_library( java_library( name = "format", visibility = ["//visibility:public"], - srcs = ["src/main/java/com/google/mu/util/StringFormat.java"], + srcs = glob(["src/main/java/com/google/mu/util/*StringFormat.java"]), deps = [":base"], exported_plugins = ["//mug-errorprone:plugin"], ) diff --git a/mug/src/main/java/com/google/mu/util/AbstractStringFormat.java b/mug/src/main/java/com/google/mu/util/AbstractStringFormat.java new file mode 100644 index 0000000000..4300dcfce2 --- /dev/null +++ b/mug/src/main/java/com/google/mu/util/AbstractStringFormat.java @@ -0,0 +1,779 @@ +package com.google.mu.util; + +import static com.google.mu.util.InternalCollectors.toImmutableList; +import static com.google.mu.util.Optionals.optional; +import static com.google.mu.util.Substring.before; +import static com.google.mu.util.Substring.first; +import static com.google.mu.util.Substring.suffix; +import static com.google.mu.util.stream.MoreCollectors.combining; +import static com.google.mu.util.stream.MoreCollectors.onlyElement; +import static java.util.Collections.unmodifiableList; +import static java.util.Objects.requireNonNull; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Stream; + +import com.google.mu.function.Quarternary; +import com.google.mu.function.Quinary; +import com.google.mu.function.Senary; +import com.google.mu.function.Ternary; +import com.google.mu.util.stream.MoreStreams; + +/** + * The API of StringFormat. Allows different subclasses to use different placeholder styles. + */ +abstract class AbstractStringFormat { + private final String format; + final List fragments; // The string literals between placeholders + private final List toCapture; + private final int numCapturingPlaceholders; + + AbstractStringFormat(String format, Substring.RepeatingPattern placeholdersPattern, String wildcard) { + Stream.Builder delimiters = Stream.builder(); + Stream.Builder toCapture = Stream.builder(); + placeholdersPattern.split(format).forEachOrdered( + literal -> { + delimiters.add(literal.toString()); + toCapture.add(!literal.isFollowedBy(wildcard)); + }); + this.format = format; + this.fragments = delimiters.build().collect(toImmutableList()); + this.toCapture = chop(toCapture.build().collect(toImmutableList())); + this.numCapturingPlaceholders = + this.fragments.size() - 1 - (int) this.toCapture.stream().filter(c -> !c).count(); + } + + /** + * Parses {@code input} and applies the {@code mapper} function with the single placeholder value + * in this string format. + * + *

For example:

{@code
+   * new StringFormat("Job failed (job id: {job_id})").parse(input, jobId -> ...);
+   * }
+ * + * @return the return value of the {@code mapper} function if not null. Returns empty if + * {@code input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if or the format string doesn't have exactly one placeholder. + */ + public Optional parse(String input, Function mapper) { + return parseExpecting(1, input, onlyElement(mapper)); + } + + /** + * Parses {@code input} and applies {@code mapper} with the two placeholder values + * in this string format. + * + *

For example:

{@code
+   * new StringFormat("Job failed (job id: '{id}', error code: {code})")
+   *     .parse(input, (jobId, errorCode) -> ...);
+   * }
+ * + * @return the return value of the {@code mapper} function if not null. Returns empty if + * {@code input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if or the format string doesn't have exactly two placeholders. + */ + public Optional parse( + String input, BiFunction mapper) { + return parseExpecting(2, input, combining(mapper)); + } + + /** + * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code + * mapper} with the 3 placeholder values in this string format. + * + *

For example:

{@code
+   * new StringFormat("Job failed (job id: '{job_id}', error code: {code}, error details: {details})")
+   *     .parse(input, (jobId, errorCode, errorDetails) -> ...);
+   * }
+ * + * @return the return value of the {@code mapper} function if not null. Returns empty if + * {@code input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if or the format string doesn't have exactly 3 placeholders. + */ + public Optional parse(String input, Ternary mapper) { + return parseExpecting(3, input, combining(mapper)); + } + + /** + * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code + * mapper} with the 4 placeholder values in this string format. + * + * @return the return value of the {@code mapper} function if not null. Returns empty if + * {@code input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if or the format string doesn't have exactly 4 placeholders. + */ + public Optional parse(String input, Quarternary mapper) { + return parseExpecting(4, input, combining(mapper)); + } + + /** + * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code + * mapper} with the 5 placeholder values in this string format. + * + * @return the return value of the {@code mapper} function if not null. Returns empty if + * {@code input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if or the format string doesn't have exactly 5 placeholders. + */ + public Optional parse(String input, Quinary mapper) { + return parseExpecting(5, input, combining(mapper)); + } + + /** + * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code + * mapper} with the 6 placeholder values in this string format. + * + * @return the return value of the {@code mapper} function if not null. Returns empty if + * {@code input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if or the format string doesn't have exactly 6 placeholders. + */ + public Optional parse(String input, Senary mapper) { + return parseExpecting(6, input, combining(mapper)); + } + + /** + * Parses {@code input} against the pattern. + * + *

Returns an immutable list of placeholder values in the same order as {@link #placeholders}, + * upon success; otherwise returns empty. + * + *

The {@link Substring.Match} result type allows caller to inspect the characters around each + * match, or to access the raw index in the input string. + */ + public Optional> parse(String input) { + return internalParse(input, fragments, toCapture); + } + + private Optional> internalParse( + String input, List fragments, List toCapture) { + checkUnformattability(); + if (!input.startsWith(fragments.get(0))) { // first literal is the prefix + return Optional.empty(); + } + List builder = new ArrayList<>(numCapturingPlaceholders); + int inputIndex = fragments.get(0).length(); + int numPlaceholders = numPlaceholders(); + for (int i = 1; i <= numPlaceholders; i++) { + // subsequent delimiters are searched left-to-right; last literal is the suffix. + Substring.Pattern trailingLiteral = + i < numPlaceholders ? first(fragments.get(i)) : suffix(fragments.get(i)); + Substring.Match placeholder = before(trailingLiteral).in(input, inputIndex).orElse(null); + if (placeholder == null) { + return Optional.empty(); + } + if (toCapture.get(i - 1)) { + builder.add(placeholder); + } + inputIndex = placeholder.index() + placeholder.length() + fragments.get(i).length(); + } + return optional(inputIndex == input.length(), unmodifiableList(builder)); + } + + /** + * Parses {@code input} and applies {@code mapper} with the single placeholder value in this + * format string. + * + *

For example: + * + *

{@code
+   * new StringFormat("Job failed (job id: {job_id})").parseOrThrow(input, jobId -> ...);
+   * }
+ * + *

Unlike {@link #parse(String, Function)}, {@code IllegalArgumentException} is thrown if the + * input string doesn't match the string format. The error message will include both the input + * string and the format string for ease of debugging, but is otherwise generic. If you need a + * different exception type, or need to customize the error message, consider using {@link + * parse(String, Function)} instead and call {@link Optional#orElseThrow} explicitly. + * + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if the input string doesn't match the string format, or if the + * format string doesn't have exactly one placeholder + * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. + * @since 7.0 + */ + public R parseOrThrow(String input, Function mapper) { + return parseOrThrowExpecting(1, input, onlyElement(mapper)); + } + + /** + * Parses {@code input} and applies {@code mapper} with the two placeholder values in this format + * string. + * + *

For example: + * + *

{@code
+   * new StringFormat("Job failed (job id: '{job_id}', error code: {error_code})")
+   *     .parseOrThrow(input, (jobId, errorCode) -> ...);
+   * }
+ * + *

Unlike {@link #parse(String, BiFunction)}, {@code IllegalArgumentException} is thrown if the + * input string doesn't match the string format. The error message will include both the input + * string and the format string for ease of debugging, but is otherwise generic. If you need a + * different exception type, or need to customize the error message, consider using {@link + * parse(String, BiFunction)} instead and call {@link Optional#orElseThrow} explicitly. + * + * @return the return value of the {@code mapper} function applied on the extracted placeholder + * value. + * @throws IllegalArgumentException if the input string doesn't match the string format, or if the + * format string doesn't have exactly two placeholders + * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. + * @since 7.0 + */ + public R parseOrThrow(String input, BiFunction mapper) { + return parseOrThrowExpecting(2, input, combining(mapper)); + } + + /** + * Similar to {@link #parseOrThrow(String, BiFunction)}, but parses {@code input} and applies + * {@code mapper} with the 3 placeholder values in this format string. + * + *

For example: + * + *

{@code
+   * new StringFormat("Job failed (id: '{job_id}', code: {error_code}, error details: {details})")
+   *     .parseOrThrow(input, (jobId, errorCode, errorDetails) -> ...);
+   * }
+ * + *

Unlike {@link #parse(String, Ternary)}, {@code IllegalArgumentException} is thrown if the + * input string doesn't match the string format. The error message will include both the input + * string and the format string for ease of debugging, but is otherwise generic. If you need a + * different exception type, or need to customize the error message, consider using {@link + * parse(String, Ternary)} instead and call {@link Optional#orElseThrow} explicitly. + * + * @return the return value of the {@code mapper} function applied on the extracted placeholder + * values. + * @throws IllegalArgumentException if the input string doesn't match the string format, or if the + * format string doesn't have exactly 3 placeholders + * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. + * @since 7.0 + */ + public R parseOrThrow(String input, Ternary mapper) { + return parseOrThrowExpecting(3, input, combining(mapper)); + } + + /** + * Similar to {@link #parseOrThrow(String, BiFunction)}, but parses {@code input} and applies + * {@code mapper} with the 4 placeholder values in this string format. + * + *

Unlike {@link #parse(String, Quarternary)}, {@code IllegalArgumentException} is thrown if the + * input string doesn't match the string format. The error message will include both the input + * string and the format string for ease of debugging, but is otherwise generic. If you need a + * different exception type, or need to customize the error message, consider using {@link + * parse(String, Quarternary)} instead and call {@link Optional#orElseThrow} explicitly. + * + * @return the return value of the {@code mapper} function applied on the extracted placeholder + * values. + * @throws IllegalArgumentException if the input string doesn't match the string format, or if the + * format string doesn't have exactly 4 placeholders + * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. + * @since 7.0 + */ + public R parseOrThrow(String input, Quarternary mapper) { + return parseOrThrowExpecting(4, input, combining(mapper)); + } + + /** + * Similar to {@link #parseOrThrow(String, BiFunction)}, but parses {@code input} and applies + * {@code mapper} with the 5 placeholder values in this string format. + * + *

Unlike {@link #parse(String, Quinary)}, {@code IllegalArgumentException} is thrown if the + * input string doesn't match the string format. The error message will include both the input + * string and the format string for ease of debugging, but is otherwise generic. If you need a + * different exception type, or need to customize the error message, consider using {@link + * parse(String, Quinary)} instead and call {@link Optional#orElseThrow} explicitly. + * + * @return the return value of the {@code mapper} function applied on the extracted placeholder + * values. + * @throws IllegalArgumentException if the input string doesn't match the string format, or if the + * format string doesn't have exactly 5 placeholders + * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. + * @since 7.0 + */ + public R parseOrThrow(String input, Quinary mapper) { + return parseOrThrowExpecting(5, input, combining(mapper)); + } + + /** + * Similar to {@link #parseOrThrow(String, BiFunction)}, but parses {@code input} and applies + * {@code mapper} with the 6 placeholder values in this string format. + * + *

Unlike {@link #parse(String, MapFrom6)}, {@code IllegalArgumentException} is thrown if the + * input string doesn't match the string format. The error message will include both the input + * string and the format string for ease of debugging, but is otherwise generic. If you need a + * different exception type, or need to customize the error message, consider using {@link + * parse(String, MapFrom6)} instead and call {@link Optional#orElseThrow} explicitly. + * + * @return the return value of the {@code mapper} function applied on the extracted placeholder + * values. + * @throws IllegalArgumentException if the input string doesn't match the string format, or if the + * format string doesn't have exactly 6 placeholders + * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. + * @since 7.0 + */ + public R parseOrThrow(String input, Senary mapper) { + return parseOrThrowExpecting(6, input, combining(mapper)); + } + + /** + * Similar to {@link #parse(String, Function)}, parses {@code input} and applies {@code mapper} + * with the single placeholder value in this format string, but matches the placeholders backwards + * from the end to the beginning of the input string. + * + *

For unambiguous strings, it's equivalent to {@link #parse(String, Function)}, but if for + * example you are parsing "a/b/c" against the pattern of "{parent}/{...}", {@code parse("a/b/c", + * parent -> parent)} results in "a", while {@code parseGreedy("a/b/c", parent -> parent)} results + * in "a/b". + * + *

This is also equivalent to allowing the left placeholder to match greedily, while still + * requiring the remaining placeholder(s) to be matched. + * + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if the format string doesn't have exactly one placeholder. + * @since 7.0 + */ + public final Optional parseGreedy( + String input, Function mapper) { + return parseGreedyExpecting(1, input, onlyElement(mapper)); + } + + /** + * Similar to {@link #parse(String, BiFunction)}, parses {@code input} and applies {@code mapper} + * with the two placeholder values in this format string, but matches the placeholders backwards + * from the end to the beginning of the input string. + * + *

For unambiguous strings, it's equivalent to {@link #parse(String, BiFunction)}, but if for + * example you are parsing "a/b/c" against the pattern of "{parent}/{child}", {@code + * parse("a/b/c", (parent, child) -> ...)} parses out "a" as parent and "b/c" as child, while + * {@code parseGreedy("a/b/c", (parent, child) -> ...)} parses "a/b" as parent and "c" as child. + * + *

This is also equivalent to allowing the left placeholder to match greedily, while still + * requiring the remaining placeholder(s) to be matched. + * + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if the format string doesn't have exactly two placeholders. + * @since 7.0 + */ + public final Optional parseGreedy( + String input, BiFunction mapper) { + return parseGreedyExpecting(2, input, combining(mapper)); + } + + /** + * Similar to {@link #parse(String, Ternary)}, parses {@code input} and applies {@code mapper} + * with the 3 placeholder values in this format string, but matches the placeholders backwards + * from the end to the beginning of the input string. + * + *

This is also equivalent to allowing the left placeholder to match greedily, while still + * requiring the remaining placeholder(s) to be matched. + * + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if the format string doesn't have exactly 3 placeholders. + * @since 7.0 + */ + public final Optional parseGreedy( + String input, Ternary mapper) { + return parseGreedyExpecting(3, input, combining(mapper)); + } + + /** + * Similar to {@link #parse(String, Quarternary)}, parses {@code input} and applies {@code mapper} + * with the 3 placeholder values in this format string, but matches the placeholders backwards + * from the end to the beginning of the input string. + * + *

This is also equivalent to allowing the left placeholder to match greedily, while still + * requiring the remaining placeholder(s) to be matched. + * + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if the format string doesn't have exactly 4 placeholders. + * @since 7.0 + */ + public final Optional parseGreedy( + String input, Quarternary mapper) { + return parseGreedyExpecting(4, input, combining(mapper)); + } + + /** + * Similar to {@link #parse(String, Quinary)}, parses {@code input} and applies {@code mapper} + * with the 5 placeholder values in this format string, but matches the placeholders backwards + * from the end to the beginning of the input string. + * + *

This is also equivalent to allowing the left placeholder to match greedily, while still + * requiring the remaining placeholder(s) to be matched. + * + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. + * @throws IllegalArgumentException if the format string doesn't have exactly 5 placeholders. + * @since 7.0 + */ + public final Optional parseGreedy( + String input, Quinary mapper) { + return parseGreedyExpecting(5, input, combining(mapper)); + } + + /** + * Returns true if this format matches {@code input} entirely. + * + * @since 7.0 + */ + public boolean matches(String input) { + return parse(input).isPresent(); + } + + /** + * Scans the {@code input} string and extracts all matched placeholders in this string format. + * + *

unlike {@link #parse(String)}, the input string isn't matched entirely: + * the pattern doesn't have to start from the beginning, and if there are some remaining + * characters that don't match the pattern any more, the stream stops. In particular, if there + * is no match, empty stream is returned. + */ + public Stream> scan(String input) { + requireNonNull(input); + if (format.isEmpty()) { + return Stream.generate(() -> Collections.emptyList()) + .limit(input.length() + 1); + } + int numPlaceholders = numPlaceholders(); + return MoreStreams.whileNotNull( + new Supplier>() { + private int inputIndex = 0; + private boolean done = false; + + @Override + public List get() { + if (done) { + return null; + } + inputIndex = input.indexOf(fragments.get(0), inputIndex); + if (inputIndex < 0) { + return null; + } + inputIndex += fragments.get(0).length(); + List builder = new ArrayList<>(numCapturingPlaceholders); + for (int i = 1; i <= numPlaceholders; i++) { + String literal = fragments.get(i); + // Always search left-to-right. The last placeholder at the end of format is suffix. + Substring.Pattern literalLocator = + i == numPlaceholders && fragments.get(i).isEmpty() + ? Substring.END + : first(fragments.get(i)); + Substring.Match placeholder = before(literalLocator).match(input, inputIndex); + if (placeholder == null) { + return null; + } + if (toCapture.get(i - 1)) { + builder.add(placeholder); + } + inputIndex = placeholder.index() + placeholder.length() + literal.length(); + } + if (inputIndex == input.length()) { + done = true; + } + return unmodifiableList(builder); + } + }); + } + + /** + * Scans the {@code input} string and extracts all matches of this string format. + * Returns the lazy stream of non-null results from passing the single placeholder values to + * the {@code mapper} function for each iteration, with null results skipped. + * + *

For example:

{@code
+   * new StringFormat("/home/usr/myname/{file_name}\n")
+   *     .scan(multiLineInput, fileName -> ...);
+   * }
+ * + *

unlike {@link #parse(String, Function)}, the input string isn't matched + * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining + * characters that don't match the pattern any more, the stream stops. In particular, if there + * is no match, empty stream is returned. + * + *

By default, placeholders are allowed to be matched against an empty string. If the + * placeholder isn't expected to be empty, consider filtering it out by returning null from + * the {@code mapper} function, which will then be ignored in the result stream. + */ + public Stream scan(String input, Function mapper) { + requireNonNull(input); + requireNonNull(mapper); + checkPlaceholderCount(1); + return scanAndCollect(input, onlyElement(mapper)); + } + + /** + * Scans the {@code input} string and extracts all matches of this string format. + * Returns the lazy stream of non-null results from passing the two placeholder values to + * the {@code mapper} function for each iteration, with null results skipped. + * + *

For example:

{@code
+   * new StringFormat("[key={key}, value={value}]")
+   *     .repeatedly()
+   *     .parse(input, (key, value) -> ...);
+   * }
+ * + *

unlike {@link #parse(String, BiFunction)}, the input string isn't matched + * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining + * characters that don't match the pattern any more, the stream stops. In particular, if there + * is no match, empty stream is returned. + * + *

By default, placeholders are allowed to be matched against an empty string. If a certain + * placeholder isn't expected to be empty, consider filtering it out by returning null from + * the {@code mapper} function, which will then be ignored in the result stream. + */ + public Stream scan( + String input, BiFunction mapper) { + requireNonNull(input); + requireNonNull(mapper); + checkPlaceholderCount(2); + return scanAndCollect(input, combining(mapper)); + } + + /** + * Scans the {@code input} string and extracts all matches of this string format. + * Returns the lazy stream of non-null results from passing the 3 placeholder values to + * the {@code mapper} function for each iteration, with null results skipped. + * + *

For example:

{@code
+   * new StringFormat("[{lhs} + {rhs} = {result}]")
+   *     .repeatedly()
+   *     .parse(input, (lhs, rhs, result) -> ...);
+   * }
+ * + *

unlike {@link #parse(String, Ternary)}, the input string isn't matched + * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining + * characters that don't match the pattern any more, the stream stops. In particular, if there + * is no match, empty stream is returned. + * + *

By default, placeholders are allowed to be matched against an empty string. If a certain + * placeholder isn't expected to be empty, consider filtering it out by returning null from + * the {@code mapper} function, which will then be ignored in the result stream. + */ + public Stream scan(String input, Ternary mapper) { + requireNonNull(input); + requireNonNull(mapper); + checkPlaceholderCount(3); + return scanAndCollect(input, combining(mapper)); + } + + /** + * Scans the {@code input} string and extracts all matches of this string format. + * Returns the lazy stream of non-null results from passing the 4 placeholder values to + * the {@code mapper} function for each iteration, with null results skipped. + * + *

unlike {@link #parse(String, Quarternary)}, the input string isn't matched + * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining + * characters that don't match the pattern any more, the stream stops. In particular, if there + * is no match, empty stream is returned. + * + *

By default, placeholders are allowed to be matched against an empty string. If a certain + * placeholder isn't expected to be empty, consider filtering it out by returning null from + * the {@code mapper} function, which will then be ignored in the result stream. + */ + public Stream scan(String input, Quarternary mapper) { + requireNonNull(input); + requireNonNull(mapper); + checkPlaceholderCount(4); + return scanAndCollect(input, combining(mapper)); + } + + /** + * Scans the {@code input} string and extracts all matches of this string format. + * Returns the lazy stream of non-null results from passing the 5 placeholder values to + * the {@code mapper} function for each iteration, with null results skipped. + * + *

unlike {@link #parse(String, Quinary)}, the input string isn't matched + * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining + * characters that don't match the pattern any more, the stream stops. In particular, if there + * is no match, empty stream is returned. + * + *

By default, placeholders are allowed to be matched against an empty string. If a certain + * placeholder isn't expected to be empty, consider filtering it out by returning null from + * the {@code mapper} function, which will then be ignored in the result stream. + */ + public Stream scan(String input, Quinary mapper) { + requireNonNull(input); + requireNonNull(mapper); + checkPlaceholderCount(5); + return scanAndCollect(input, combining(mapper)); + } + + /** + * Scans the {@code input} string and extracts all matches of this string format. + * Returns the lazy stream of non-null results from passing the 6 placeholder values to + * the {@code mapper} function for each iteration, with null results skipped. + * + *

unlike {@link #parse(String, Senary)}, the input string isn't matched + * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining + * characters that don't match the pattern any more, the stream stops. In particular, if there + * is no match, empty stream is returned. + * + *

By default, placeholders are allowed to be matched against an empty string. If a certain + * placeholder isn't expected to be empty, consider filtering it out by returning null from + * the {@code mapper} function, which will then be ignored in the result stream. + */ + public Stream scan(String input, Senary mapper) { + requireNonNull(input); + requireNonNull(mapper); + checkPlaceholderCount(6); + return scanAndCollect(input, combining(mapper)); + } + + /** + * Returns the string formatted with placeholders filled using {@code args}. + * This is the reverse operation of the {@code parse(...)} methods. For example: + * + *

{@code
+   * new StringFormat("Hello {who}").format("world")
+   *     => "Hello world"
+   * }
+ * + * @throws IllegalArgumentException if the number of arguments doesn't match that of the placeholders + */ + public String format(Object... args) { + checkFormatArgs(args); + StringBuilder builder = new StringBuilder().append(fragments.get(0)); + for (int i = 0; i < args.length; i++) { + builder.append(args[i]).append(fragments.get(i + 1)); + } + return builder.toString(); + } + + /** Returns the string format. */ + @Override public String toString() { + return format; + } + + private Optional parseGreedyExpecting( + int cardinality, String input, Collector collector) { + requireNonNull(input); + checkPlaceholderCount(cardinality); + // To match backwards, we reverse the input as well as the format string. + // After the matching is done, reverse the results back. + return internalParse( + reverse(input), + reverse(fragments).stream().map(s -> reverse(s)).collect(toImmutableList()), + reverse(toCapture)) + .map( + captured -> + reverse(captured).stream() + .map( + sub -> { // Return the original (unreversed) substring + int forwardIndex = input.length() - (sub.index() + sub.length()); + return input.substring(forwardIndex, forwardIndex + sub.length()); + }) + .collect(collector)); + } + + private Optional parseExpecting(int cardinality, String input, Collector collector) { + requireNonNull(input); + checkPlaceholderCount(cardinality); + return parse(input).map(values -> values.stream().map(Substring.Match::toString).collect(collector)); + } + + /** + * Parses {@code input} with the number of placeholders equal to {@code cardinality}, then + * collects the placeholder values using {@code collector}. + * + * @throws IllegalArgumentException if input fails parsing + */ + private R parseOrThrowExpecting( + int cardinality, String input, Collector collector) { + requireNonNull(input); + checkPlaceholderCount(cardinality); + List values = + parse(input) + .orElseThrow( + () -> + new IllegalArgumentException( + new StringFormat("input '{input}' doesn't match format string '{format}'") + .format(input, format))); + R result = values.stream().map(Substring.Match::toString).collect(collector); + if (result == null) { + throw new NullPointerException( + String.format( + "mapper function returned null when matching input '%s' against format string '%s'", + input, format)); + } + return result; + } + + private Stream scanAndCollect(String input, Collector collector) { + return scan(input) + .map(values -> values.stream().map(Substring.Match::toString).collect(collector)) + .filter(v -> v != null); + } + + private int numPlaceholders() { + return fragments.size() - 1; + } + + private void checkUnformattability() { + for (int i = 1; i < numPlaceholders(); i++) { + if (this.fragments.get(i).isEmpty()) { + throw new IllegalArgumentException("Placeholders cannot be next to each other: " + format); + } + } + } + + private void checkPlaceholderCount(int expected) { + if (numCapturingPlaceholders != expected) { + throw new IllegalArgumentException( + String.format( + "format string has %s placeholders; %s expected.", + numCapturingPlaceholders, + expected)); + } + } + + + final void checkFormatArgs(Object[] args) { + if (args.length != numPlaceholders()) { + throw new IllegalArgumentException( + String.format( + "format string expects %s placeholders, %s provided", + numPlaceholders(), + args.length)); + } + } + + static String reverse(String s) { + if (s.length() <= 1) { + return s; + } + StringBuilder builder = new StringBuilder(s.length()); + for (int i = s.length() - 1; i >= 0; i--) { + builder.append(s.charAt(i)); + } + return builder.toString(); + } + + static List reverse(List list) { + if (list.size() <= 1) { + return list; + } + return new AbstractList() { + @Override public int size() { + return list.size(); + } + @Override public T get(int i) { + return list.get(list.size() - 1 - i); + } + }; + } + + private static List chop(List list) { + return list.subList(0, list.size() - 1); + } +} diff --git a/mug/src/main/java/com/google/mu/util/StringFormat.java b/mug/src/main/java/com/google/mu/util/StringFormat.java index 54412bc7d1..cb484a4c08 100644 --- a/mug/src/main/java/com/google/mu/util/StringFormat.java +++ b/mug/src/main/java/com/google/mu/util/StringFormat.java @@ -1,35 +1,15 @@ package com.google.mu.util; import static com.google.mu.util.InternalCollectors.toImmutableList; -import static com.google.mu.util.Optionals.optional; -import static com.google.mu.util.Substring.before; -import static com.google.mu.util.Substring.first; -import static com.google.mu.util.Substring.suffix; import static com.google.mu.util.Substring.BoundStyle.INCLUSIVE; -import static com.google.mu.util.stream.MoreCollectors.combining; -import static com.google.mu.util.stream.MoreCollectors.onlyElement; -import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; -import java.util.AbstractList; -import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; -import java.util.Optional; -import java.util.function.BiFunction; import java.util.function.Function; -import java.util.function.Supplier; -import java.util.stream.Collector; -import java.util.stream.Stream; -import com.google.mu.function.Quarternary; -import com.google.mu.function.Quinary; -import com.google.mu.function.Senary; -import com.google.mu.function.Ternary; import com.google.mu.util.stream.BiStream; -import com.google.mu.util.stream.MoreStreams; /** * A string parser to extract placeholder values from input strings according to a format string. @@ -84,15 +64,11 @@ * * @since 6.6 */ -public final class StringFormat { +public final class StringFormat extends AbstractStringFormat { private static final Substring.RepeatingPattern PLACEHOLDERS = Substring.consecutive(c -> c != '{' && c != '}') // Find the inner-most pairs of curly braces. .immediatelyBetween("{", INCLUSIVE, "}", INCLUSIVE) .repeatedly(); - private final String format; - private final List fragments; // The string literals between placeholders - private final List toCapture; - private final int numCapturingPlaceholders; /** * Returns a {@link Substring.Pattern} spanning the substring matching {@code format}. For @@ -227,622 +203,7 @@ public String toString() { * (e.g. a placeholder immediately followed by another placeholder) */ public StringFormat(String format) { - Stream.Builder delimiters = Stream.builder(); - Stream.Builder toCapture = Stream.builder(); - PLACEHOLDERS.split(format).forEachOrdered( - literal -> { - delimiters.add(literal.toString()); - toCapture.add(!format.startsWith("...}", literal.index() + literal.length() + 1)); - }); - this.format = format; - this.fragments = delimiters.build().collect(toImmutableList()); - this.toCapture = chop(toCapture.build().collect(toImmutableList())); - this.numCapturingPlaceholders = - this.fragments.size() - 1 - (int) this.toCapture.stream().filter(c -> !c).count(); - } - - /** - * Parses {@code input} and applies the {@code mapper} function with the single placeholder value - * in this string format. - * - *

For example:

{@code
-   * new StringFormat("Job failed (job id: {job_id})").parse(input, jobId -> ...);
-   * }
- * - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if or the format string doesn't have exactly one placeholder. - */ - public Optional parse(String input, Function mapper) { - return parseExpecting(1, input, onlyElement(mapper)); - } - - /** - * Parses {@code input} and applies {@code mapper} with the two placeholder values - * in this string format. - * - *

For example:

{@code
-   * new StringFormat("Job failed (job id: '{id}', error code: {code})")
-   *     .parse(input, (jobId, errorCode) -> ...);
-   * }
- * - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if or the format string doesn't have exactly two placeholders. - */ - public Optional parse( - String input, BiFunction mapper) { - return parseExpecting(2, input, combining(mapper)); - } - - /** - * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code - * mapper} with the 3 placeholder values in this string format. - * - *

For example:

{@code
-   * new StringFormat("Job failed (job id: '{job_id}', error code: {code}, error details: {details})")
-   *     .parse(input, (jobId, errorCode, errorDetails) -> ...);
-   * }
- * - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if or the format string doesn't have exactly 3 placeholders. - */ - public Optional parse(String input, Ternary mapper) { - return parseExpecting(3, input, combining(mapper)); - } - - /** - * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code - * mapper} with the 4 placeholder values in this string format. - * - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if or the format string doesn't have exactly 4 placeholders. - */ - public Optional parse(String input, Quarternary mapper) { - return parseExpecting(4, input, combining(mapper)); - } - - /** - * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code - * mapper} with the 5 placeholder values in this string format. - * - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if or the format string doesn't have exactly 5 placeholders. - */ - public Optional parse(String input, Quinary mapper) { - return parseExpecting(5, input, combining(mapper)); - } - - /** - * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code - * mapper} with the 6 placeholder values in this string format. - * - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if or the format string doesn't have exactly 6 placeholders. - */ - public Optional parse(String input, Senary mapper) { - return parseExpecting(6, input, combining(mapper)); - } - - /** - * Parses {@code input} against the pattern. - * - *

Returns an immutable list of placeholder values in the same order as {@link #placeholders}, - * upon success; otherwise returns empty. - * - *

The {@link Substring.Match} result type allows caller to inspect the characters around each - * match, or to access the raw index in the input string. - */ - public Optional> parse(String input) { - return internalParse(input, fragments, toCapture); - } - - private Optional> internalParse( - String input, List fragments, List toCapture) { - checkUnformattability(); - if (!input.startsWith(fragments.get(0))) { // first literal is the prefix - return Optional.empty(); - } - List builder = new ArrayList<>(numCapturingPlaceholders); - int inputIndex = fragments.get(0).length(); - int numPlaceholders = numPlaceholders(); - for (int i = 1; i <= numPlaceholders; i++) { - // subsequent delimiters are searched left-to-right; last literal is the suffix. - Substring.Pattern trailingLiteral = - i < numPlaceholders ? first(fragments.get(i)) : suffix(fragments.get(i)); - Substring.Match placeholder = before(trailingLiteral).in(input, inputIndex).orElse(null); - if (placeholder == null) { - return Optional.empty(); - } - if (toCapture.get(i - 1)) { - builder.add(placeholder); - } - inputIndex = placeholder.index() + placeholder.length() + fragments.get(i).length(); - } - return optional(inputIndex == input.length(), unmodifiableList(builder)); - } - - /** - * Parses {@code input} and applies {@code mapper} with the single placeholder value in this - * format string. - * - *

For example: - * - *

{@code
-   * new StringFormat("Job failed (job id: {job_id})").parseOrThrow(input, jobId -> ...);
-   * }
- * - *

Unlike {@link #parse(String, Function)}, {@code IllegalArgumentException} is thrown if the - * input string doesn't match the string format. The error message will include both the input - * string and the format string for ease of debugging, but is otherwise generic. If you need a - * different exception type, or need to customize the error message, consider using {@link - * parse(String, Function)} instead and call {@link Optional#orElseThrow} explicitly. - * - * @return the return value of the {@code mapper} function if not null. Returns empty if {@code - * input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if the input string doesn't match the string format, or if the - * format string doesn't have exactly one placeholder - * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. - * @since 7.0 - */ - public R parseOrThrow(String input, Function mapper) { - return parseOrThrowExpecting(1, input, onlyElement(mapper)); - } - - /** - * Parses {@code input} and applies {@code mapper} with the two placeholder values in this format - * string. - * - *

For example: - * - *

{@code
-   * new StringFormat("Job failed (job id: '{job_id}', error code: {error_code})")
-   *     .parseOrThrow(input, (jobId, errorCode) -> ...);
-   * }
- * - *

Unlike {@link #parse(String, BiFunction)}, {@code IllegalArgumentException} is thrown if the - * input string doesn't match the string format. The error message will include both the input - * string and the format string for ease of debugging, but is otherwise generic. If you need a - * different exception type, or need to customize the error message, consider using {@link - * parse(String, BiFunction)} instead and call {@link Optional#orElseThrow} explicitly. - * - * @return the return value of the {@code mapper} function applied on the extracted placeholder - * value. - * @throws IllegalArgumentException if the input string doesn't match the string format, or if the - * format string doesn't have exactly two placeholders - * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. - * @since 7.0 - */ - public R parseOrThrow(String input, BiFunction mapper) { - return parseOrThrowExpecting(2, input, combining(mapper)); - } - - /** - * Similar to {@link #parseOrThrow(String, BiFunction)}, but parses {@code input} and applies - * {@code mapper} with the 3 placeholder values in this format string. - * - *

For example: - * - *

{@code
-   * new StringFormat("Job failed (id: '{job_id}', code: {error_code}, error details: {details})")
-   *     .parseOrThrow(input, (jobId, errorCode, errorDetails) -> ...);
-   * }
- * - *

Unlike {@link #parse(String, Ternary)}, {@code IllegalArgumentException} is thrown if the - * input string doesn't match the string format. The error message will include both the input - * string and the format string for ease of debugging, but is otherwise generic. If you need a - * different exception type, or need to customize the error message, consider using {@link - * parse(String, Ternary)} instead and call {@link Optional#orElseThrow} explicitly. - * - * @return the return value of the {@code mapper} function applied on the extracted placeholder - * values. - * @throws IllegalArgumentException if the input string doesn't match the string format, or if the - * format string doesn't have exactly 3 placeholders - * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. - * @since 7.0 - */ - public R parseOrThrow(String input, Ternary mapper) { - return parseOrThrowExpecting(3, input, combining(mapper)); - } - - /** - * Similar to {@link #parseOrThrow(String, BiFunction)}, but parses {@code input} and applies - * {@code mapper} with the 4 placeholder values in this string format. - * - *

Unlike {@link #parse(String, Quarternary)}, {@code IllegalArgumentException} is thrown if the - * input string doesn't match the string format. The error message will include both the input - * string and the format string for ease of debugging, but is otherwise generic. If you need a - * different exception type, or need to customize the error message, consider using {@link - * parse(String, Quarternary)} instead and call {@link Optional#orElseThrow} explicitly. - * - * @return the return value of the {@code mapper} function applied on the extracted placeholder - * values. - * @throws IllegalArgumentException if the input string doesn't match the string format, or if the - * format string doesn't have exactly 4 placeholders - * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. - * @since 7.0 - */ - public R parseOrThrow(String input, Quarternary mapper) { - return parseOrThrowExpecting(4, input, combining(mapper)); - } - - /** - * Similar to {@link #parseOrThrow(String, BiFunction)}, but parses {@code input} and applies - * {@code mapper} with the 5 placeholder values in this string format. - * - *

Unlike {@link #parse(String, Quinary)}, {@code IllegalArgumentException} is thrown if the - * input string doesn't match the string format. The error message will include both the input - * string and the format string for ease of debugging, but is otherwise generic. If you need a - * different exception type, or need to customize the error message, consider using {@link - * parse(String, Quinary)} instead and call {@link Optional#orElseThrow} explicitly. - * - * @return the return value of the {@code mapper} function applied on the extracted placeholder - * values. - * @throws IllegalArgumentException if the input string doesn't match the string format, or if the - * format string doesn't have exactly 5 placeholders - * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. - * @since 7.0 - */ - public R parseOrThrow(String input, Quinary mapper) { - return parseOrThrowExpecting(5, input, combining(mapper)); - } - - /** - * Similar to {@link #parseOrThrow(String, BiFunction)}, but parses {@code input} and applies - * {@code mapper} with the 6 placeholder values in this string format. - * - *

Unlike {@link #parse(String, MapFrom6)}, {@code IllegalArgumentException} is thrown if the - * input string doesn't match the string format. The error message will include both the input - * string and the format string for ease of debugging, but is otherwise generic. If you need a - * different exception type, or need to customize the error message, consider using {@link - * parse(String, MapFrom6)} instead and call {@link Optional#orElseThrow} explicitly. - * - * @return the return value of the {@code mapper} function applied on the extracted placeholder - * values. - * @throws IllegalArgumentException if the input string doesn't match the string format, or if the - * format string doesn't have exactly 6 placeholders - * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. - * @since 7.0 - */ - public R parseOrThrow(String input, Senary mapper) { - return parseOrThrowExpecting(6, input, combining(mapper)); - } - - /** - * Similar to {@link #parse(String, Function)}, parses {@code input} and applies {@code mapper} - * with the single placeholder value in this format string, but matches the placeholders backwards - * from the end to the beginning of the input string. - * - *

For unambiguous strings, it's equivalent to {@link #parse(String, Function)}, but if for - * example you are parsing "a/b/c" against the pattern of "{parent}/{...}", {@code parse("a/b/c", - * parent -> parent)} results in "a", while {@code parseGreedy("a/b/c", parent -> parent)} results - * in "a/b". - * - *

This is also equivalent to allowing the left placeholder to match greedily, while still - * requiring the remaining placeholder(s) to be matched. - * - * @return the return value of the {@code mapper} function if not null. Returns empty if {@code - * input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if the format string doesn't have exactly one placeholder. - * @since 7.0 - */ - public final Optional parseGreedy( - String input, Function mapper) { - return parseGreedyExpecting(1, input, onlyElement(mapper)); - } - - /** - * Similar to {@link #parse(String, BiFunction)}, parses {@code input} and applies {@code mapper} - * with the two placeholder values in this format string, but matches the placeholders backwards - * from the end to the beginning of the input string. - * - *

For unambiguous strings, it's equivalent to {@link #parse(String, BiFunction)}, but if for - * example you are parsing "a/b/c" against the pattern of "{parent}/{child}", {@code - * parse("a/b/c", (parent, child) -> ...)} parses out "a" as parent and "b/c" as child, while - * {@code parseGreedy("a/b/c", (parent, child) -> ...)} parses "a/b" as parent and "c" as child. - * - *

This is also equivalent to allowing the left placeholder to match greedily, while still - * requiring the remaining placeholder(s) to be matched. - * - * @return the return value of the {@code mapper} function if not null. Returns empty if {@code - * input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if the format string doesn't have exactly two placeholders. - * @since 7.0 - */ - public final Optional parseGreedy( - String input, BiFunction mapper) { - return parseGreedyExpecting(2, input, combining(mapper)); - } - - /** - * Similar to {@link #parse(String, Ternary)}, parses {@code input} and applies {@code mapper} - * with the 3 placeholder values in this format string, but matches the placeholders backwards - * from the end to the beginning of the input string. - * - *

This is also equivalent to allowing the left placeholder to match greedily, while still - * requiring the remaining placeholder(s) to be matched. - * - * @return the return value of the {@code mapper} function if not null. Returns empty if {@code - * input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if the format string doesn't have exactly 3 placeholders. - * @since 7.0 - */ - public final Optional parseGreedy( - String input, Ternary mapper) { - return parseGreedyExpecting(3, input, combining(mapper)); - } - - /** - * Similar to {@link #parse(String, Quarternary)}, parses {@code input} and applies {@code mapper} - * with the 3 placeholder values in this format string, but matches the placeholders backwards - * from the end to the beginning of the input string. - * - *

This is also equivalent to allowing the left placeholder to match greedily, while still - * requiring the remaining placeholder(s) to be matched. - * - * @return the return value of the {@code mapper} function if not null. Returns empty if {@code - * input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if the format string doesn't have exactly 4 placeholders. - * @since 7.0 - */ - public final Optional parseGreedy( - String input, Quarternary mapper) { - return parseGreedyExpecting(4, input, combining(mapper)); - } - - /** - * Similar to {@link #parse(String, Quinary)}, parses {@code input} and applies {@code mapper} - * with the 5 placeholder values in this format string, but matches the placeholders backwards - * from the end to the beginning of the input string. - * - *

This is also equivalent to allowing the left placeholder to match greedily, while still - * requiring the remaining placeholder(s) to be matched. - * - * @return the return value of the {@code mapper} function if not null. Returns empty if {@code - * input} doesn't match the format, or {@code mapper} returns null. - * @throws IllegalArgumentException if the format string doesn't have exactly 5 placeholders. - * @since 7.0 - */ - public final Optional parseGreedy( - String input, Quinary mapper) { - return parseGreedyExpecting(5, input, combining(mapper)); - } - - /** - * Returns true if this format matches {@code input} entirely. - * - * @since 7.0 - */ - public boolean matches(String input) { - return parse(input).isPresent(); - } - - /** - * Scans the {@code input} string and extracts all matched placeholders in this string format. - * - *

unlike {@link #parse(String)}, the input string isn't matched entirely: - * the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. - */ - public Stream> scan(String input) { - requireNonNull(input); - if (format.isEmpty()) { - return Stream.generate(() -> Collections.emptyList()) - .limit(input.length() + 1); - } - int numPlaceholders = numPlaceholders(); - return MoreStreams.whileNotNull( - new Supplier>() { - private int inputIndex = 0; - private boolean done = false; - - @Override - public List get() { - if (done) { - return null; - } - inputIndex = input.indexOf(fragments.get(0), inputIndex); - if (inputIndex < 0) { - return null; - } - inputIndex += fragments.get(0).length(); - List builder = new ArrayList<>(numCapturingPlaceholders); - for (int i = 1; i <= numPlaceholders; i++) { - String literal = fragments.get(i); - // Always search left-to-right. The last placeholder at the end of format is suffix. - Substring.Pattern literalLocator = - i == numPlaceholders && fragments.get(i).isEmpty() - ? Substring.END - : first(fragments.get(i)); - Substring.Match placeholder = before(literalLocator).match(input, inputIndex); - if (placeholder == null) { - return null; - } - if (toCapture.get(i - 1)) { - builder.add(placeholder); - } - inputIndex = placeholder.index() + placeholder.length() + literal.length(); - } - if (inputIndex == input.length()) { - done = true; - } - return unmodifiableList(builder); - } - }); - } - - /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the single placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. - * - *

For example:

{@code
-   * new StringFormat("/home/usr/myname/{file_name}\n")
-   *     .scan(multiLineInput, fileName -> ...);
-   * }
- * - *

unlike {@link #parse(String, Function)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. - * - *

By default, placeholders are allowed to be matched against an empty string. If the - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. - */ - public Stream scan(String input, Function mapper) { - requireNonNull(input); - requireNonNull(mapper); - checkPlaceholderCount(1); - return scanAndCollect(input, onlyElement(mapper)); - } - - /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the two placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. - * - *

For example:

{@code
-   * new StringFormat("[key={key}, value={value}]")
-   *     .repeatedly()
-   *     .parse(input, (key, value) -> ...);
-   * }
- * - *

unlike {@link #parse(String, BiFunction)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. - * - *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. - */ - public Stream scan( - String input, BiFunction mapper) { - requireNonNull(input); - requireNonNull(mapper); - checkPlaceholderCount(2); - return scanAndCollect(input, combining(mapper)); - } - - /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the 3 placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. - * - *

For example:

{@code
-   * new StringFormat("[{lhs} + {rhs} = {result}]")
-   *     .repeatedly()
-   *     .parse(input, (lhs, rhs, result) -> ...);
-   * }
- * - *

unlike {@link #parse(String, Ternary)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. - * - *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. - */ - public Stream scan(String input, Ternary mapper) { - requireNonNull(input); - requireNonNull(mapper); - checkPlaceholderCount(3); - return scanAndCollect(input, combining(mapper)); - } - - /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the 4 placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. - * - *

unlike {@link #parse(String, Quarternary)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. - * - *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. - */ - public Stream scan(String input, Quarternary mapper) { - requireNonNull(input); - requireNonNull(mapper); - checkPlaceholderCount(4); - return scanAndCollect(input, combining(mapper)); - } - - /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the 5 placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. - * - *

unlike {@link #parse(String, Quinary)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. - * - *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. - */ - public Stream scan(String input, Quinary mapper) { - requireNonNull(input); - requireNonNull(mapper); - checkPlaceholderCount(5); - return scanAndCollect(input, combining(mapper)); - } - - /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the 6 placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. - * - *

unlike {@link #parse(String, Senary)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. - * - *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. - */ - public Stream scan(String input, Senary mapper) { - requireNonNull(input); - requireNonNull(mapper); - checkPlaceholderCount(6); - return scanAndCollect(input, combining(mapper)); - } - - /** - * Returns the string formatted with placeholders filled using {@code args}. - * This is the reverse operation of the {@code parse(...)} methods. For example: - * - *

{@code
-   * new StringFormat("Hello {who}").format("world")
-   *     => "Hello world"
-   * }
- * - * @throws IllegalArgumentException if the number of arguments doesn't match that of the placeholders - */ - public String format(Object... args) { - checkFormatArgs(args); - StringBuilder builder = new StringBuilder().append(fragments.get(0)); - for (int i = 0; i < args.length; i++) { - builder.append(args[i]).append(fragments.get(i + 1)); - } - return builder.toString(); - } - - /** Returns the string format. */ - @Override public String toString() { - return format; + super(format, PLACEHOLDERS, "{...}"); } /** @@ -860,7 +221,11 @@ public interface To { public abstract String toString(); } - /** A functional SPI interface for custom interpolation. */ + /** + * A functional SPI interface for custom interpolation. + * + * @since 7.0 + */ public interface Interpolator { /** * Interpolates with {@code fragments} of size {@code N + 1} and {@code placeholders} of size @@ -870,126 +235,4 @@ public interface Interpolator { */ T interpolate(List fragments, BiStream placeholders); } - - private Optional parseGreedyExpecting( - int cardinality, String input, Collector collector) { - requireNonNull(input); - checkPlaceholderCount(cardinality); - // To match backwards, we reverse the input as well as the format string. - // After the matching is done, reverse the results back. - return internalParse( - reverse(input), - reverse(fragments).stream().map(s -> reverse(s)).collect(toImmutableList()), - reverse(toCapture)) - .map( - captured -> - reverse(captured).stream() - .map( - sub -> { // Return the original (unreversed) substring - int forwardIndex = input.length() - (sub.index() + sub.length()); - return input.substring(forwardIndex, forwardIndex + sub.length()); - }) - .collect(collector)); - } - - private Optional parseExpecting(int cardinality, String input, Collector collector) { - requireNonNull(input); - checkPlaceholderCount(cardinality); - return parse(input).map(values -> values.stream().map(Substring.Match::toString).collect(collector)); - } - - /** - * Parses {@code input} with the number of placeholders equal to {@code cardinality}, then - * collects the placeholder values using {@code collector}. - * - * @throws IllegalArgumentException if input fails parsing - */ - private R parseOrThrowExpecting( - int cardinality, String input, Collector collector) { - requireNonNull(input); - checkPlaceholderCount(cardinality); - List values = - parse(input) - .orElseThrow( - () -> - new IllegalArgumentException( - new StringFormat("input '{input}' doesn't match format string '{format}'") - .format(input, format))); - R result = values.stream().map(Substring.Match::toString).collect(collector); - if (result == null) { - throw new NullPointerException( - String.format( - "mapper function returned null when matching input '%s' against format string '%s'", - input, format)); - } - return result; - } - - private Stream scanAndCollect(String input, Collector collector) { - return scan(input) - .map(values -> values.stream().map(Substring.Match::toString).collect(collector)) - .filter(v -> v != null); - } - - private int numPlaceholders() { - return fragments.size() - 1; - } - - private void checkUnformattability() { - for (int i = 1; i < numPlaceholders(); i++) { - if (this.fragments.get(i).isEmpty()) { - throw new IllegalArgumentException("Placeholders cannot be next to each other: " + format); - } - } - } - - private void checkPlaceholderCount(int expected) { - if (numCapturingPlaceholders != expected) { - throw new IllegalArgumentException( - String.format( - "format string has %s placeholders; %s expected.", - numCapturingPlaceholders, - expected)); - } - } - - - private void checkFormatArgs(Object[] args) { - if (args.length != numPlaceholders()) { - throw new IllegalArgumentException( - String.format( - "format string expects %s placeholders, %s provided", - numPlaceholders(), - args.length)); - } - } - - static String reverse(String s) { - if (s.length() <= 1) { - return s; - } - StringBuilder builder = new StringBuilder(s.length()); - for (int i = s.length() - 1; i >= 0; i--) { - builder.append(s.charAt(i)); - } - return builder.toString(); - } - - static List reverse(List list) { - if (list.size() <= 1) { - return list; - } - return new AbstractList() { - @Override public int size() { - return list.size(); - } - @Override public T get(int i) { - return list.get(list.size() - 1 - i); - } - }; - } - - private static List chop(List list) { - return list.subList(0, list.size() - 1); - } } From 2c6239f0f3588d25a69d4c919d1625075c986dd8 Mon Sep 17 00:00:00 2001 From: xingyutangyuan <147447743+xingyutangyuan@users.noreply.github.com> Date: Sat, 23 Dec 2023 20:07:33 -0800 Subject: [PATCH 2/2] Mark all public methods in AbstractStringFormat final --- .../google/mu/util/AbstractStringFormat.java | 231 ++++++++++-------- 1 file changed, 124 insertions(+), 107 deletions(-) diff --git a/mug/src/main/java/com/google/mu/util/AbstractStringFormat.java b/mug/src/main/java/com/google/mu/util/AbstractStringFormat.java index 4300dcfce2..7f1a191a6c 100644 --- a/mug/src/main/java/com/google/mu/util/AbstractStringFormat.java +++ b/mug/src/main/java/com/google/mu/util/AbstractStringFormat.java @@ -36,7 +36,8 @@ abstract class AbstractStringFormat { private final List toCapture; private final int numCapturingPlaceholders; - AbstractStringFormat(String format, Substring.RepeatingPattern placeholdersPattern, String wildcard) { + AbstractStringFormat( + String format, Substring.RepeatingPattern placeholdersPattern, String wildcard) { Stream.Builder delimiters = Stream.builder(); Stream.Builder toCapture = Stream.builder(); placeholdersPattern.split(format).forEachOrdered( @@ -55,32 +56,36 @@ abstract class AbstractStringFormat { * Parses {@code input} and applies the {@code mapper} function with the single placeholder value * in this string format. * - *

For example:

{@code
+   * 

For example: + * + *

{@code
    * new StringFormat("Job failed (job id: {job_id})").parse(input, jobId -> ...);
    * }
* - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. * @throws IllegalArgumentException if or the format string doesn't have exactly one placeholder. */ - public Optional parse(String input, Function mapper) { + public final Optional parse(String input, Function mapper) { return parseExpecting(1, input, onlyElement(mapper)); } /** - * Parses {@code input} and applies {@code mapper} with the two placeholder values - * in this string format. + * Parses {@code input} and applies {@code mapper} with the two placeholder values in this string + * format. + * + *

For example: * - *

For example:

{@code
+   * 
{@code
    * new StringFormat("Job failed (job id: '{id}', error code: {code})")
    *     .parse(input, (jobId, errorCode) -> ...);
    * }
* - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. * @throws IllegalArgumentException if or the format string doesn't have exactly two placeholders. */ - public Optional parse( + public final Optional parse( String input, BiFunction mapper) { return parseExpecting(2, input, combining(mapper)); } @@ -89,16 +94,18 @@ public Optional parse( * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code * mapper} with the 3 placeholder values in this string format. * - *

For example:

{@code
+   * 

For example: + * + *

{@code
    * new StringFormat("Job failed (job id: '{job_id}', error code: {code}, error details: {details})")
    *     .parse(input, (jobId, errorCode, errorDetails) -> ...);
    * }
* - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. * @throws IllegalArgumentException if or the format string doesn't have exactly 3 placeholders. */ - public Optional parse(String input, Ternary mapper) { + public final Optional parse(String input, Ternary mapper) { return parseExpecting(3, input, combining(mapper)); } @@ -106,11 +113,12 @@ public Optional parse(String input, Ternary * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code * mapper} with the 4 placeholder values in this string format. * - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. * @throws IllegalArgumentException if or the format string doesn't have exactly 4 placeholders. */ - public Optional parse(String input, Quarternary mapper) { + public final Optional parse( + String input, Quarternary mapper) { return parseExpecting(4, input, combining(mapper)); } @@ -118,11 +126,11 @@ public Optional parse(String input, Quarternary5 placeholder values in this string format. * - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. * @throws IllegalArgumentException if or the format string doesn't have exactly 5 placeholders. */ - public Optional parse(String input, Quinary mapper) { + public final Optional parse(String input, Quinary mapper) { return parseExpecting(5, input, combining(mapper)); } @@ -130,11 +138,11 @@ public Optional parse(String input, Quinary * Similar to {@link #parse(String, BiFunction)}, but parses {@code input} and applies {@code * mapper} with the 6 placeholder values in this string format. * - * @return the return value of the {@code mapper} function if not null. Returns empty if - * {@code input} doesn't match the format, or {@code mapper} returns null. + * @return the return value of the {@code mapper} function if not null. Returns empty if {@code + * input} doesn't match the format, or {@code mapper} returns null. * @throws IllegalArgumentException if or the format string doesn't have exactly 6 placeholders. */ - public Optional parse(String input, Senary mapper) { + public final Optional parse(String input, Senary mapper) { return parseExpecting(6, input, combining(mapper)); } @@ -147,7 +155,7 @@ public Optional parse(String input, Senary m *

The {@link Substring.Match} result type allows caller to inspect the characters around each * match, or to access the raw index in the input string. */ - public Optional> parse(String input) { + public final Optional> parse(String input) { return internalParse(input, fragments, toCapture); } @@ -199,7 +207,7 @@ private Optional> internalParse( * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. * @since 7.0 */ - public R parseOrThrow(String input, Function mapper) { + public final R parseOrThrow(String input, Function mapper) { return parseOrThrowExpecting(1, input, onlyElement(mapper)); } @@ -227,7 +235,8 @@ public R parseOrThrow(String input, Function mapper) { * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. * @since 7.0 */ - public R parseOrThrow(String input, BiFunction mapper) { + public final R parseOrThrow( + String input, BiFunction mapper) { return parseOrThrowExpecting(2, input, combining(mapper)); } @@ -255,7 +264,7 @@ public R parseOrThrow(String input, BiFunction R parseOrThrow(String input, Ternary mapper) { + public final R parseOrThrow(String input, Ternary mapper) { return parseOrThrowExpecting(3, input, combining(mapper)); } @@ -263,8 +272,8 @@ public R parseOrThrow(String input, Ternary mapper) { * Similar to {@link #parseOrThrow(String, BiFunction)}, but parses {@code input} and applies * {@code mapper} with the 4 placeholder values in this string format. * - *

Unlike {@link #parse(String, Quarternary)}, {@code IllegalArgumentException} is thrown if the - * input string doesn't match the string format. The error message will include both the input + *

Unlike {@link #parse(String, Quarternary)}, {@code IllegalArgumentException} is thrown if + * the input string doesn't match the string format. The error message will include both the input * string and the format string for ease of debugging, but is otherwise generic. If you need a * different exception type, or need to customize the error message, consider using {@link * parse(String, Quarternary)} instead and call {@link Optional#orElseThrow} explicitly. @@ -276,7 +285,7 @@ public R parseOrThrow(String input, Ternary mapper) { * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. * @since 7.0 */ - public R parseOrThrow(String input, Quarternary mapper) { + public final R parseOrThrow(String input, Quarternary mapper) { return parseOrThrowExpecting(4, input, combining(mapper)); } @@ -297,7 +306,7 @@ public R parseOrThrow(String input, Quarternary mapper) { * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. * @since 7.0 */ - public R parseOrThrow(String input, Quinary mapper) { + public final R parseOrThrow(String input, Quinary mapper) { return parseOrThrowExpecting(5, input, combining(mapper)); } @@ -318,7 +327,7 @@ public R parseOrThrow(String input, Quinary mapper) { * @throws NullPointerException if any of the parameter is null or {@code mapper} returns null. * @since 7.0 */ - public R parseOrThrow(String input, Senary mapper) { + public final R parseOrThrow(String input, Senary mapper) { return parseOrThrowExpecting(6, input, combining(mapper)); } @@ -427,19 +436,19 @@ public final Optional parseGreedy( * * @since 7.0 */ - public boolean matches(String input) { + public final boolean matches(String input) { return parse(input).isPresent(); } /** * Scans the {@code input} string and extracts all matched placeholders in this string format. * - *

unlike {@link #parse(String)}, the input string isn't matched entirely: - * the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. + *

unlike {@link #parse(String)}, the input string isn't matched entirely: the pattern doesn't + * have to start from the beginning, and if there are some remaining characters that don't match + * the pattern any more, the stream stops. In particular, if there is no match, empty stream is + * returned. */ - public Stream> scan(String input) { + public final Stream> scan(String input) { requireNonNull(input); if (format.isEmpty()) { return Stream.generate(() -> Collections.emptyList()) @@ -487,25 +496,27 @@ public List get() { } /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the single placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. + * Scans the {@code input} string and extracts all matches of this string format. Returns the lazy + * stream of non-null results from passing the single placeholder values to the {@code mapper} + * function for each iteration, with null results skipped. + * + *

For example: * - *

For example:

{@code
+   * 
{@code
    * new StringFormat("/home/usr/myname/{file_name}\n")
    *     .scan(multiLineInput, fileName -> ...);
    * }
* - *

unlike {@link #parse(String, Function)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. + *

unlike {@link #parse(String, Function)}, the input string isn't matched entirely: the + * pattern doesn't have to start from the beginning, and if there are some remaining characters + * that don't match the pattern any more, the stream stops. In particular, if there is no match, + * empty stream is returned. * *

By default, placeholders are allowed to be matched against an empty string. If the - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. + * placeholder isn't expected to be empty, consider filtering it out by returning null from the + * {@code mapper} function, which will then be ignored in the result stream. */ - public Stream scan(String input, Function mapper) { + public final Stream scan(String input, Function mapper) { requireNonNull(input); requireNonNull(mapper); checkPlaceholderCount(1); @@ -513,26 +524,28 @@ public Stream scan(String input, Function ma } /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the two placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. + * Scans the {@code input} string and extracts all matches of this string format. Returns the lazy + * stream of non-null results from passing the two placeholder values to the {@code mapper} + * function for each iteration, with null results skipped. + * + *

For example: * - *

For example:

{@code
+   * 
{@code
    * new StringFormat("[key={key}, value={value}]")
    *     .repeatedly()
    *     .parse(input, (key, value) -> ...);
    * }
* - *

unlike {@link #parse(String, BiFunction)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. + *

unlike {@link #parse(String, BiFunction)}, the input string isn't matched entirely: the + * pattern doesn't have to start from the beginning, and if there are some remaining characters + * that don't match the pattern any more, the stream stops. In particular, if there is no match, + * empty stream is returned. * *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. + * placeholder isn't expected to be empty, consider filtering it out by returning null from the + * {@code mapper} function, which will then be ignored in the result stream. */ - public Stream scan( + public final Stream scan( String input, BiFunction mapper) { requireNonNull(input); requireNonNull(mapper); @@ -541,26 +554,28 @@ public Stream scan( } /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the 3 placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. + * Scans the {@code input} string and extracts all matches of this string format. Returns the lazy + * stream of non-null results from passing the 3 placeholder values to the {@code mapper} function + * for each iteration, with null results skipped. * - *

For example:

{@code
+   * 

For example: + * + *

{@code
    * new StringFormat("[{lhs} + {rhs} = {result}]")
    *     .repeatedly()
    *     .parse(input, (lhs, rhs, result) -> ...);
    * }
* - *

unlike {@link #parse(String, Ternary)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. + *

unlike {@link #parse(String, Ternary)}, the input string isn't matched entirely: the pattern + * doesn't have to start from the beginning, and if there are some remaining characters that don't + * match the pattern any more, the stream stops. In particular, if there is no match, empty stream + * is returned. * *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. + * placeholder isn't expected to be empty, consider filtering it out by returning null from the + * {@code mapper} function, which will then be ignored in the result stream. */ - public Stream scan(String input, Ternary mapper) { + public final Stream scan(String input, Ternary mapper) { requireNonNull(input); requireNonNull(mapper); checkPlaceholderCount(3); @@ -568,20 +583,20 @@ public Stream scan(String input, Ternary map } /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the 4 placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. + * Scans the {@code input} string and extracts all matches of this string format. Returns the lazy + * stream of non-null results from passing the 4 placeholder values to the {@code mapper} function + * for each iteration, with null results skipped. * - *

unlike {@link #parse(String, Quarternary)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. + *

unlike {@link #parse(String, Quarternary)}, the input string isn't matched entirely: the + * pattern doesn't have to start from the beginning, and if there are some remaining characters + * that don't match the pattern any more, the stream stops. In particular, if there is no match, + * empty stream is returned. * *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. + * placeholder isn't expected to be empty, consider filtering it out by returning null from the + * {@code mapper} function, which will then be ignored in the result stream. */ - public Stream scan(String input, Quarternary mapper) { + public final Stream scan(String input, Quarternary mapper) { requireNonNull(input); requireNonNull(mapper); checkPlaceholderCount(4); @@ -589,20 +604,20 @@ public Stream scan(String input, Quarternary } /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the 5 placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. + * Scans the {@code input} string and extracts all matches of this string format. Returns the lazy + * stream of non-null results from passing the 5 placeholder values to the {@code mapper} function + * for each iteration, with null results skipped. * - *

unlike {@link #parse(String, Quinary)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. + *

unlike {@link #parse(String, Quinary)}, the input string isn't matched entirely: the pattern + * doesn't have to start from the beginning, and if there are some remaining characters that don't + * match the pattern any more, the stream stops. In particular, if there is no match, empty stream + * is returned. * *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. + * placeholder isn't expected to be empty, consider filtering it out by returning null from the + * {@code mapper} function, which will then be ignored in the result stream. */ - public Stream scan(String input, Quinary mapper) { + public final Stream scan(String input, Quinary mapper) { requireNonNull(input); requireNonNull(mapper); checkPlaceholderCount(5); @@ -610,20 +625,20 @@ public Stream scan(String input, Quinary map } /** - * Scans the {@code input} string and extracts all matches of this string format. - * Returns the lazy stream of non-null results from passing the 6 placeholder values to - * the {@code mapper} function for each iteration, with null results skipped. + * Scans the {@code input} string and extracts all matches of this string format. Returns the lazy + * stream of non-null results from passing the 6 placeholder values to the {@code mapper} function + * for each iteration, with null results skipped. * - *

unlike {@link #parse(String, Senary)}, the input string isn't matched - * entirely: the pattern doesn't have to start from the beginning, and if there are some remaining - * characters that don't match the pattern any more, the stream stops. In particular, if there - * is no match, empty stream is returned. + *

unlike {@link #parse(String, Senary)}, the input string isn't matched entirely: the pattern + * doesn't have to start from the beginning, and if there are some remaining characters that don't + * match the pattern any more, the stream stops. In particular, if there is no match, empty stream + * is returned. * *

By default, placeholders are allowed to be matched against an empty string. If a certain - * placeholder isn't expected to be empty, consider filtering it out by returning null from - * the {@code mapper} function, which will then be ignored in the result stream. + * placeholder isn't expected to be empty, consider filtering it out by returning null from the + * {@code mapper} function, which will then be ignored in the result stream. */ - public Stream scan(String input, Senary mapper) { + public final Stream scan(String input, Senary mapper) { requireNonNull(input); requireNonNull(mapper); checkPlaceholderCount(6); @@ -631,17 +646,18 @@ public Stream scan(String input, Senary mapp } /** - * Returns the string formatted with placeholders filled using {@code args}. - * This is the reverse operation of the {@code parse(...)} methods. For example: + * Returns the string formatted with placeholders filled using {@code args}. This is the reverse + * operation of the {@code parse(...)} methods. For example: * *

{@code
    * new StringFormat("Hello {who}").format("world")
    *     => "Hello world"
    * }
* - * @throws IllegalArgumentException if the number of arguments doesn't match that of the placeholders + * @throws IllegalArgumentException if the number of arguments doesn't match that of the + * placeholders */ - public String format(Object... args) { + public final String format(Object... args) { checkFormatArgs(args); StringBuilder builder = new StringBuilder().append(fragments.get(0)); for (int i = 0; i < args.length; i++) { @@ -676,7 +692,8 @@ private Optional parseGreedyExpecting( .collect(collector)); } - private Optional parseExpecting(int cardinality, String input, Collector collector) { + private Optional parseExpecting( + int cardinality, String input, Collector collector) { requireNonNull(input); checkPlaceholderCount(cardinality); return parse(input).map(values -> values.stream().map(Substring.Match::toString).collect(collector));