001// Generated by delombok at Sat Feb 25 17:58:34 UTC 2023
002package io.cui.tools.string;
003
004import static io.cui.tools.base.Preconditions.checkArgument;
005import static io.cui.tools.string.MoreStrings.isEmpty;
006import static io.cui.tools.string.MoreStrings.requireNotEmpty;
007import static java.util.Objects.requireNonNull;
008import java.util.Collections;
009import java.util.List;
010import java.util.regex.Pattern;
011import io.cui.tools.collect.CollectionBuilder;
012import io.cui.tools.logging.CuiLogger;
013import lombok.NonNull;
014
015/**
016 * Derived from Googles Splitter.
017 * <p>
018 * It uses internally the {@link String#split(String)} implementation
019 * of java and provides a guava like wrapper. This results in an implicit splitting of the whole
020 * String compared to the lazy / deferred splitting of guava. It focuses and RegEx-based splitting
021 * and omits the fixedLength and Map based variants.
022 * </p>
023 * <h2>Migrating from Guava</h2>
024 * <p>
025 * In order to migrate for most case you only need to replace the package name on the import. A
026 * major different is that the split method provided is {@link #splitToList(String)}, the variant
027 * split() is replaced by it completely.
028 * </p>
029 * <h2>Changes to Guavas-Splitter</h2>
030 * <p>
031 * It is quite similar to the guava variant but behaves a little
032 * different in certain details, especially in the context of {@link Splitter#limit(int)} and
033 * {@link Splitter#trimResults()}, {@link Splitter#omitEmptyStrings()}.
034 * While guavas version will filter the limit elements after the
035 * application of omit / trim, this version will do it the other way round, resulting in a different
036 * result compared to the guava version.
037 * </p>
038 *
039 * @author Oliver Wolff
040 */
041public final class Splitter {
042    private static final CuiLogger log = new CuiLogger(Splitter.class);
043    @NonNull
044    private final SplitterConfig splitterConfig;
045
046    /**
047     * Returns a splitter that uses the given fixed string as a separator. For example, {@code
048     * Splitter.on(", ").split("foo, bar,baz")} returns an iterable containing {@code ["foo",
049     * "bar,baz"]}.
050     *
051     * @param separator the literal, nonempty string to recognize as a separator
052     *
053     * @return a splitter, with default settings, that recognizes that separator
054     */
055    public static Splitter on(final String separator) {
056        requireNotEmpty(separator);
057        return new Splitter(SplitterConfig.builder().separator(separator).build());
058    }
059
060    /**
061     * Returns a splitter that uses the given fixed string as a separator. For example, {@code
062     * Splitter.on(", ").split("foo, bar,baz")} returns an iterable containing {@code ["foo",
063     * "bar,baz"]}.
064     *
065     * @param separator the literal, nonempty string to recognize as a separator
066     *
067     * @return a splitter, with default settings, that recognizes that separator
068     */
069    public static Splitter on(final char separator) {
070        requireNonNull(separator);
071        return new Splitter(SplitterConfig.builder().separator(String.valueOf(separator)).build());
072    }
073
074    /**
075     * Returns a splitter that behaves equivalently to {@code this} splitter, but automatically
076     * omits empty strings from the results. For example, {@code
077     * Splitter.on(',').omitEmptyStrings().split(",a,,,b,c,,")} returns an iterable containing only
078     * {@code ["a", "b", "c"]}.
079     *
080     * <p>
081     * If either {@code trimResults} option is also specified when creating a splitter, that
082     * splitter always trims results first before checking for emptiness. So, for example, {@code
083     * Splitter.on(':').omitEmptyStrings().trimResults().split(": : : ")} returns an empty iterable.
084     * <p>
085     *
086     * @return a splitter with the desired configuration
087     */
088    public Splitter omitEmptyStrings() {
089        return new Splitter(splitterConfig.copy().omitEmptyStrings(true).build());
090    }
091
092    /**
093     * Usually the separator will be pre-processed before being passed to
094     * {@link String#split(String)}. This is needed to mask special characters that will harm
095     * {@link Pattern#compile(String)}. If you want to disable this behavior and take care for your
096     * self you can change this method by calling this method.
097     *
098     * @return a splitter with the desired configuration
099     */
100    public Splitter doNotModifySeparatorString() {
101        return new Splitter(splitterConfig.copy().doNotModifySeparatorString(true).build());
102    }
103
104    /**
105     * Returns a splitter that behaves equivalently to {@code this} splitter, but automatically
106     * removes leading and trailing whitespace from each
107     * returned substring. For example,
108     * {@code Splitter.on(',').trimResults().split(" a, b ,c ")} returns an iterable containing
109     * {@code ["a", "b", "c"]}.
110     *
111     * @return a splitter with the desired configuration
112     */
113    public Splitter trimResults() {
114        return new Splitter(splitterConfig.copy().trimResults(true).build());
115    }
116
117    /**
118     * Returns a splitter that behaves equivalently to {@code this} splitter but stops splitting
119     * after it reaches the limit. The limit defines the maximum number of items returned by the
120     * iterator, or the maximum size of the list returned by {@link #splitToList}.
121     *
122     * <p>
123     * For example, {@code Splitter.on(',').limit(3).split("a,b,c,d")} returns an iterable
124     * containing {@code ["a", "b", "c,d"]}. When omitting empty strings, the omitted strings do not
125     * count. Hence, {@code Splitter.on(',').limit(3).omitEmptyStrings().split("a,,,b,,,c,d")}
126     * returns an iterable containing {@code ["a", "b", "c,d"}. When trim is requested, all entries
127     * are trimmed, including the last. Hence
128     * {@code Splitter.on(',').limit(3).trimResults().split(" a , b
129     * , c , d ")} results in {@code ["a", "b", "c , d"]}.
130     *
131     * @param maxItems the maximum number of items returned
132     * @return a splitter with the desired configuration
133     */
134    public Splitter limit(int maxItems) {
135        checkArgument(maxItems > 0, "must be greater than zero: %s");
136        return new Splitter(splitterConfig.copy().maxItems(maxItems).build());
137    }
138
139    /**
140     * Splits {@code sequence} into string components and returns them as an immutable list.
141     *
142     * @param sequence the sequence of characters to split
143     *
144     * @return an immutable list of the segments split from the parameter
145     */
146    public List<String> splitToList(String sequence) {
147        log.trace("Splitting String {} with configuration {}", sequence, splitterConfig);
148        if (isEmpty(sequence)) {
149            return Collections.emptyList();
150        }
151        var splitted = sequence.split(handleSplitCharacter(splitterConfig.getSeparator()), splitterConfig.getMaxItems());
152        if (null == splitted || 0 == splitted.length) {
153            log.trace("No content to be returned for input {} and configuration {}", sequence, splitterConfig);
154            return Collections.emptyList();
155        }
156        var builder = new CollectionBuilder<String>();
157        for (String element : splitted) {
158            addIfApplicable(builder, element);
159        }
160        return builder.toImmutableList();
161    }
162
163    private String handleSplitCharacter(String separator) {
164        if (splitterConfig.isDoNotModifySeparatorString()) {
165            return separator;
166        }
167        return Pattern.quote(separator);
168    }
169
170    private void addIfApplicable(CollectionBuilder<String> builder, String element) {
171        if (null == element) {
172            return;
173        }
174        var toDo = element;
175        if (splitterConfig.isTrimResults()) {
176            toDo = toDo.trim();
177        }
178        if (!splitterConfig.isOmitEmptyStrings()) {
179            builder.add(toDo);
180            return;
181        }
182        if (!toDo.isEmpty()) {
183            // Omit empty strings
184            builder.add(toDo);
185        }
186    }
187
188    @java.lang.SuppressWarnings("all")
189    @lombok.Generated
190    Splitter(@NonNull final SplitterConfig splitterConfig) {
191        if (splitterConfig == null) {
192            throw new java.lang.NullPointerException("splitterConfig is marked non-null but is null");
193        }
194        this.splitterConfig = splitterConfig;
195    }
196}