001/*
002 * ============================================================================
003 * Copyright © 20002-2026 by Thomas Thrien.
004 * All Rights Reserved.
005 * ============================================================================
006 * Licensed to the public under the agreements of the GNU Lesser General Public
007 * License, version 3.0 (the "License"). You may obtain a copy of the License at
008 *
009 *      http://www.gnu.org/licenses/lgpl.html
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
013 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
014 * License for the specific language governing permissions and limitations
015 * under the License.
016 */
017
018package org.tquadrat.foundation.util;
019
020import static java.lang.Character.charCount;
021import static java.lang.Character.isISOControl;
022import static java.lang.Character.isValidCodePoint;
023import static java.lang.Character.isWhitespace;
024import static java.lang.Character.toChars;
025import static java.lang.Character.toLowerCase;
026import static java.lang.Character.toTitleCase;
027import static java.lang.Character.toUpperCase;
028import static java.lang.Integer.min;
029import static java.net.URLDecoder.decode;
030import static java.net.URLEncoder.encode;
031import static java.text.Normalizer.Form.NFD;
032import static java.text.Normalizer.normalize;
033import static java.util.regex.Pattern.DOTALL;
034import static java.util.regex.Pattern.compile;
035import static org.apiguardian.api.API.Status.STABLE;
036import static org.tquadrat.foundation.lang.CommonConstants.CHAR_ELLIPSIS;
037import static org.tquadrat.foundation.lang.CommonConstants.EMPTY_STRING;
038import static org.tquadrat.foundation.lang.CommonConstants.NULL_CHAR;
039import static org.tquadrat.foundation.lang.CommonConstants.UTF8;
040import static org.tquadrat.foundation.lang.Objects.isNull;
041import static org.tquadrat.foundation.lang.Objects.nonNull;
042import static org.tquadrat.foundation.lang.Objects.requireNonNullArgument;
043import static org.tquadrat.foundation.lang.Objects.requireNotEmptyArgument;
044import static org.tquadrat.foundation.util.CharSetUtils.escapeCharacter;
045import static org.tquadrat.foundation.util.StringUtils.Clipping.CLIPPING_ABBREVIATE;
046import static org.tquadrat.foundation.util.StringUtils.Clipping.CLIPPING_ABBREVIATE_MIDDLE;
047import static org.tquadrat.foundation.util.StringUtils.Clipping.CLIPPING_CUT;
048import static org.tquadrat.foundation.util.StringUtils.Clipping.CLIPPING_NONE;
049import static org.tquadrat.foundation.util.StringUtils.Padding.PADDING_CENTER;
050import static org.tquadrat.foundation.util.StringUtils.Padding.PADDING_LEFT;
051import static org.tquadrat.foundation.util.StringUtils.Padding.PADDING_RIGHT;
052import static org.tquadrat.foundation.util.internal.Entities.HTML50;
053import static org.tquadrat.foundation.util.internal.Entities.XML;
054
055import java.io.IOException;
056import java.io.UnsupportedEncodingException;
057import java.util.ArrayList;
058import java.util.Arrays;
059import java.util.Collection;
060import java.util.List;
061import java.util.Optional;
062import java.util.SequencedCollection;
063import java.util.function.Supplier;
064import java.util.regex.Pattern;
065import java.util.regex.PatternSyntaxException;
066import java.util.stream.Stream;
067import java.util.stream.Stream.Builder;
068
069import org.apiguardian.api.API;
070import org.tquadrat.foundation.annotation.ClassVersion;
071import org.tquadrat.foundation.annotation.UtilityClass;
072import org.tquadrat.foundation.exception.CharSequenceTooLongException;
073import org.tquadrat.foundation.exception.EmptyArgumentException;
074import org.tquadrat.foundation.exception.ImpossibleExceptionError;
075import org.tquadrat.foundation.exception.NullArgumentException;
076import org.tquadrat.foundation.exception.PrivateConstructorForStaticClassCalledError;
077import org.tquadrat.foundation.exception.ValidationException;
078import org.tquadrat.foundation.lang.Objects;
079
080/**
081 *  Library of utility methods that are useful when dealing with Strings. <br>
082 *  <br>Parts of the code were adopted from the class
083 *  <code>org.apache.commons.lang.StringUtils</code> and modified to match the
084 *  requirements of this project. In particular, these are the methods
085 *  <ul>
086 *  <li>{@link #abbreviate(CharSequence, int) abbreviate()}</li>
087 *  <li>{@link #capitalize(CharSequence) capitalize()}</li>
088 *  <li>{@link #escapeHTML(CharSequence) escapeHTML()} in both versions</li>
089 *  <li>{@link #isEmpty(CharSequence) isEmpty()}</li>
090 *  <li>{@link #isNotEmpty(CharSequence) isNotEmpty()}</li>
091 *  <li>{@link #repeat(CharSequence, int) repeat()}</li>
092 *  <li>{@link #unescapeHTML(CharSequence) unescapeHTML()} in both versions</li>
093 *  </ul>
094 *
095 *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
096 *  @version $Id: StringUtils.java 1186 2026-04-06 11:24:14Z tquadrat $
097 *  @since 0.0.3
098 *
099 *  @UMLGraph.link
100 */
101@SuppressWarnings( {"ClassWithTooManyMethods", "OverlyComplexClass"} )
102@ClassVersion( sourceVersion = "$Id: StringUtils.java 1186 2026-04-06 11:24:14Z tquadrat $" )
103@UtilityClass
104public final class StringUtils
105{
106        /*---------------*\
107    ====** Inner Classes **====================================================
108        \*---------------*/
109    /**
110     *  The clipping mode that is used for the method
111     *  {@link StringUtils#pad(CharSequence,int,char,Padding,Clipping)}
112     *
113     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
114     *  @version $Id: StringUtils.java 1186 2026-04-06 11:24:14Z tquadrat $
115     *  @since 0.0.3
116     *
117     *  @UMLGraph.link
118     */
119    @SuppressWarnings( "InnerClassTooDeeplyNested" )
120    @ClassVersion( sourceVersion = "$Id: StringUtils.java 1186 2026-04-06 11:24:14Z tquadrat $" )
121    @API( status = STABLE, since = "0.0.5" )
122    public static enum Clipping
123    {
124            /*------------------*\
125        ====** Enum Declaration **=============================================
126            \*------------------*/
127        /**
128         *  If an input String is already longer than the target length, it
129         *  will be returned unchanged.
130         */
131        CLIPPING_NONE
132        {
133            /**
134             *  {@inheritDoc}
135             */
136            @Override
137            protected final String clip( final CharSequence input, final int length ) { return input.toString(); }
138        },
139
140        /**
141         *  If an input String is longer than the target length, it will be
142         *  just shortened to that length.
143         */
144        CLIPPING_CUT
145        {
146            /**
147             *  {@inheritDoc}
148             */
149            @Override
150            protected final String clip( final CharSequence input, final int length )
151            {
152                final var retValue = (
153                    input.length() > length ? input.subSequence( 0, length ) :
154                    input).toString();
155
156                //---* Done *--------------------------------------------------
157                return retValue;
158            }   //  clip()
159        },
160
161        /**
162         *  If an input String is longer than the target length, it will be
163         *  abbreviated to that length, by calling
164         *  {@link StringUtils#abbreviate(CharSequence, int)}
165         *  with that String. The minimum length for the padded String is 4.
166         */
167        CLIPPING_ABBREVIATE
168        {
169            /**
170             *  {@inheritDoc}
171             */
172            @Override
173            protected final String clip( final CharSequence input, final int length ) { return abbreviate( input, length ); }
174        },
175
176        /**
177         *  If an input String is longer than the target length, it will be
178         *  abbreviated to that length, by calling
179         *  {@link StringUtils#abbreviateMiddle(CharSequence, int)}
180         *  with that String. The minimum length for the padded String is 5.
181         */
182        CLIPPING_ABBREVIATE_MIDDLE
183        {
184            /**
185             *  {@inheritDoc}
186             */
187            @Override
188            protected final String clip( final CharSequence input, final int length ) { return abbreviateMiddle( input, length ); }
189        };
190
191            /*---------*\
192        ====** Methods **======================================================
193            \*---------*/
194        /**
195         *  Clips the given input String.
196         *
197         *  @param  input   The input String.
198         *  @param  length  The target length.
199         *  @return The result String.
200         */
201        protected abstract String clip( final CharSequence input, final int length );
202    }
203    //  enum Clipping
204
205    /**
206     *  The padding mode that is used for the methods
207     *  {@link StringUtils#pad(CharSequence,int,char,Padding,boolean)}
208     *  and
209     *  {@link StringUtils#pad(CharSequence,int,char,Padding,Clipping)}
210     *
211     *  @extauthor Thomas Thrien - thomas.thrien@tquadrat.org
212     *  @version $Id: StringUtils.java 1186 2026-04-06 11:24:14Z tquadrat $
213     *  @since 0.0.5
214     *
215     *  @UMLGraph.link
216     */
217    @SuppressWarnings( "InnerClassTooDeeplyNested" )
218    @ClassVersion( sourceVersion = "$Id: StringUtils.java 1186 2026-04-06 11:24:14Z tquadrat $" )
219    @API( status = STABLE, since = "0.0.5" )
220    public static enum Padding
221    {
222            /*------------------*\
223        ====** Enum Declaration **=============================================
224            \*------------------*/
225        /**
226         *  The pad characters are distributed evenly at begin and end of the
227         *  string.
228         */
229        PADDING_CENTER
230        {
231            /**
232             *  {@inheritDoc}
233             */
234            @Override
235            protected final String pad( final CharSequence input, final int padSize, final char c )
236            {
237                final var rightSize = padSize / 2;
238                final var leftSize = padSize - rightSize;
239                final var retValue = padding( leftSize, c ) + input.toString() + padding( rightSize, c );
240
241                //---* Done *--------------------------------------------------
242                return retValue;
243            }   //  pad()
244        },
245
246        /**
247         *  The pad characters are added at the beginning of the string
248         *  (prefixing it).
249         */
250        PADDING_LEFT
251        {
252            /**
253             *  {@inheritDoc}
254             */
255            @Override
256            protected final String pad( final CharSequence input, final int padSize, final char c )
257            {
258                return padding( padSize, c ) + input.toString();
259            }   //  pad()
260        },
261
262        /**
263         *  The pad characters are added the end of the string (as a suffix).
264         */
265        PADDING_RIGHT
266        {
267            /**
268             *  {@inheritDoc}
269             */
270            @Override
271            protected final String pad( final CharSequence input, final int padSize, final char c )
272            {
273                return input.toString() + padding( padSize, c );
274            }   //  pad()
275        };
276
277            /*---------*\
278        ====** Methods **======================================================
279            \*---------*/
280        /**
281         *  Pads the given input String.
282         *
283         *  @param  input   The input String.
284         *  @param  padSize The pad size.
285         *  @param  c   The pad character.
286         *  @return The result String.
287         */
288        protected abstract String pad( final CharSequence input, final int padSize, final char c );
289
290        /**
291         *  <p>{@summary Returns padding using the specified pad character repeated to the
292          *  given length.}</p>
293         *  <br><code>
294         *  Padding.padding(&nbsp;0,&nbsp;'e'&nbsp;)&nbsp;&rArr;&nbsp;""<br>
295         *  Padding.padding(&nbsp;3,&nbsp;'e'&nbsp;)&nbsp;&rArr;&nbsp;"eee"<br>
296         *  Padding.padding(&nbsp;-2,&nbsp;'e'&nbsp;)&nbsp;&rArr;&nbsp;IndexOutOfBoundsException<br>
297         *  </code>
298         *
299         *  @param  repeat  Number of times to repeat {@code padChar}; must be
300         *      0 or greater.
301         *  @param  padChar Character to repeat.
302         *  @return String with repeated {@code padChar} character, or the
303         *      empty String if {@code repeat} is 0.
304         *  @throws IndexOutOfBoundsException {@code repeat} is less than 0.
305         *
306         *  @see StringUtils#repeat(int,int)
307         */
308        private static String padding( final int repeat, final char padChar ) throws IndexOutOfBoundsException
309        {
310            if( repeat < 0 ) throw new IndexOutOfBoundsException( MSG_PadNegative.formatted( repeat ) );
311
312            final var retValue = Character.toString( padChar ).repeat( repeat ).intern();
313
314            //---* Done *------------------------------------------------------
315            return retValue;
316        }   //  padding()
317    }
318    //  enum Padding
319
320        /*-----------*\
321    ====** Constants **========================================================
322        \*-----------*/
323    /**
324     *  <p>{@summary The regular expression for an HTML or XML comment:
325     *  {@value}.}</p>
326     *  <p>This pattern is used by the
327     *  {@link #stripXMLComments(CharSequence)}
328     *  method.</p>
329     *
330     *  @since 0.0.5
331     */
332    @API( status = STABLE, since = "0.0.5" )
333    public static final String COMMENTREMOVAL_PATTERN = "<!--.+?-->";
334
335    /**
336     *  The message text indicating that the given value for the abbreviation
337     *  target length is too short.
338     */
339    private static final String MSG_AbbrTooShort = "The minimum abbreviation width is %d";
340
341    /**
342     *  The message indicating that the give size for padding is negative.
343     */
344    private static final String MSG_PadNegative = "Cannot pad a negative amount: %d";
345
346    /**
347     *  The maximum size to which the padding constant(s) can expand: {@value}.
348     *
349     *  @see #repeat(CharSequence,int) repeat() for String
350     *  @see #repeat(char,int) repeat() for char
351     */
352    @SuppressWarnings( "unused" )
353    private static final int PAD_LIMIT = 8192;
354
355    /**
356     *  The regular expression for an HTML or XML tag: {@value}.<br>
357     *  <br>This pattern is used by the
358     *  {@link #stripTags(CharSequence)}
359     *  method.<br>
360     *  <br>As HTML/XML comments may contain a &quot;greater than&quot; sign
361     *  ('&gt;' or '&amp;gt;'), it is necessary to treat comments
362     *  separately.<br>
363     *  <br>Just as a reminder: several sources recommend using the following
364     *  idiom for embedded JavaScript:<pre><code>&lt;script&gt;
365     *  &lt;!--
366     *  <i>JavaScript code </i>
367     *  --&gt;
368     *  &lt;/script&gt;</code></pre>
369     *
370     *  @since 0.0.5
371     */
372    @SuppressWarnings( "RegExpUnnecessaryNonCapturingGroup" )
373    @API( status = STABLE, since = "0.0.5" )
374    public static final String TAGREMOVAL_PATTERN = "(?:<!--.+?-->)|(?:<[^>]+?>)";
375
376        /*------------------------*\
377    ====** Static Initialisations **===========================================
378        \*------------------------*/
379    /**
380     *  The pattern that is used to identify an HTML or XML comment.
381     *
382     *  @see #stripXMLComments(CharSequence)
383     *  @see #COMMENTREMOVAL_PATTERN
384     */
385    private static final Pattern m_CommentRemovalPattern;
386
387    /**
388     *  The pattern that is used to identify an HTML or XML tag.
389     *
390     *  @see #stripTags(CharSequence)
391     *  @see #TAGREMOVAL_PATTERN
392     */
393    private static final Pattern m_TagRemovalPattern;
394
395    static
396    {
397        //---* The regex patterns *--------------------------------------------
398        try
399        {
400            m_CommentRemovalPattern = compile( COMMENTREMOVAL_PATTERN, DOTALL );
401            m_TagRemovalPattern = compile( TAGREMOVAL_PATTERN, DOTALL );
402        }
403        catch( final PatternSyntaxException e )
404        {
405            throw new ImpossibleExceptionError( "The patterns are constant values that have been tested", e );
406        }
407    }
408
409        /*--------------*\
410    ====** Constructors **=====================================================
411        \*--------------*/
412    /**
413     *  No instance of this class is allowed.
414     */
415    private StringUtils() { throw new PrivateConstructorForStaticClassCalledError( StringUtils.class ); }
416
417        /*---------*\
418    ====** Methods **==========================================================
419        \*---------*/
420    /**
421     *  <p>{@summary Abbreviates a String using ellipses (Unicode HORIZONTAL
422     *  ELLIPSIS, 0x2026).} This will turn &quot;<i>Now is the time for all
423     *  good men</i>&quot; into &quot;<i>Now is the time for&hellip;</i>&quot;.</p>
424     *  <p>Specifically:</p>
425     *  <ul>
426     *  <li>If {@code text} is less than {@code maxWidth} characters long,
427     *  return it unchanged.</li>
428     *  <li>Else abbreviate it to <code>(substring( text, 0, max - 1 ) +
429     *  &quot;&hellip;&quot; )</code>.</li>
430     *  <li>If {@code maxWidth} is less than 4, throw an
431     *  {@link ValidationException}.</li>
432     *  <li>In no case it will return a String of length greater than
433     *  {@code maxWidth}.</li>
434     *  </ul>
435     *  <p>Some samples:</p>
436     *  <pre><code>
437     *  StringUtils.abbreviate( null, * )      = null
438     *  StringUtils.abbreviate( "", 4 )        = ""
439     *  StringUtils.abbreviate( "abc", 4 )     = "abc"
440     *  StringUtils.abbreviate( "abcd", 4 )    = "abcd;"
441     *  StringUtils.abbreviate( "abcdefg", 4 ) = "abc&hellip;"
442     *  StringUtils.abbreviate( "abcdefg", 7 ) = "abcdefg"
443     *  StringUtils.abbreviate( "abcdefg", 8 ) = "abcdefg"
444     *  StringUtils.abbreviate( "abcdefg", 3 ) = IllegalArgumentException
445     *  </code></pre>
446     *
447     *  @param  text    The String to abbreviate, can be {@code null}.
448     *  @param  maxWidth    The maximum length of result String, must be at
449     *      least 4.
450     *  @return The abbreviated String, or {@code null} if the input was
451     *      already {@code null}.
452     *  @throws ValidationException The value for {@code maxWidth} was less
453     *      than 4.
454     *
455     *  @since 0.0.5
456     */
457    @API( status = STABLE, since = "0.0.5" )
458    public static final String abbreviate( final CharSequence text, final int maxWidth ) throws ValidationException
459    {
460        return abbreviate( text, 0, maxWidth );
461    }   //  abbreviate()
462
463    /**
464     *  Abbreviates a String using ellipses (Unicode HORIZONTAL ELLIPSIS,
465     *  0x2026). This will turn &quot;<i>Now is the time for all good
466     *  men</i>&quot; into &quot;<i>&hellip;is the time
467     *  for&hellip;</i>&quot;.<br>
468     *  <br>Works like
469     *  {@link #abbreviate(CharSequence, int)},
470     *  but allows to specify a &quot;left edge&quot; offset. Note that this
471     *  left edge is not necessarily going to be the leftmost character in the
472     *  result, or the first character following the ellipses, but it will
473     *  appear somewhere in the result. An offset less than 0 will be treated
474     *  as 0, a value greater than {@code maxWidth} will be ignored.<br>
475     *  <br>In no case will it return a String of length greater than
476     *  {@code maxWidth}.<br>
477     *  <br>Some samples:<br>
478     *  <pre>
479     *  StringUtils.abbreviate( null, *, * )                = null
480     *  StringUtils.abbreviate( "", 0, 4 )                  = ""
481     *  StringUtils.abbreviate( "abcdefghijklmno", -1, 10 ) = "abcdefghi&hellip;"
482     *  StringUtils.abbreviate( "abcdefghijklmno", 0, 10 )  = "abcdefghi&hellip;"
483     *  StringUtils.abbreviate( "abcdefghijklmno", 1, 10 )  = "abcdefghi&hellip;"
484     *  StringUtils.abbreviate( "abcdefghijklmno", 4, 10 )  = "&hellip;efghijkl&hellip;"
485     *  StringUtils.abbreviate( "abcdefghijklmno", 5, 10 )  = "&hellip;fghijklm&hellip;"
486     *  StringUtils.abbreviate( "abcdefghijklmno", 6, 10 )  = "&hellip;ghijklmno"
487     *  StringUtils.abbreviate( "abcdefghijklmno", 8, 10 )  = "&hellip;ghijklmno"
488     *  StringUtils.abbreviate( "abcdefghijklmno", 10, 10 ) = "&hellip;ghijklmno"
489     *  StringUtils.abbreviate( "abcdefghijklmno", 12, 10 ) = "&hellip;ghijklmno"
490     *  StringUtils.abbreviate( "abcdefghij", 0, 3 )        = IllegalArgumentException
491     *  StringUtils.abbreviate( "abcdefghij", 5, 6 )        = IllegalArgumentException
492     *  </pre>
493     *
494     *  @param  text    The String to process, can be {@code null}.
495     *  @param  offset  The left edge of the source String; this value will not
496     *      be checked.
497     *  @param  maxWidth    The maximum length of result String, must be at
498     *      least 4.
499     *  @return The abbreviated String, or {@code null} if the input was
500     *      already {@code null}.
501     *  @throws ValidationException The value for {@code maxWidth} was less
502     *      than 4.
503     *
504     *  @since 0.0.5
505     */
506    @API( status = STABLE, since = "0.0.5" )
507    public static final String abbreviate( final CharSequence text, final int offset, final int maxWidth ) throws ValidationException
508    {
509        final var ellipsis = Character.toString( CHAR_ELLIPSIS ).intern();
510
511        String retValue = null;
512        if( nonNull( text ) )
513        {
514            if( maxWidth < 4 ) throw new ValidationException( String.format( MSG_AbbrTooShort, 4 ) );
515
516            final var len = text.length();
517            if( len > maxWidth )
518            {
519                var effectiveOffset = min( offset, len);
520                if( (len - effectiveOffset) < (maxWidth - 1))
521                {
522                    effectiveOffset = len - (maxWidth - 1);
523                }
524                if( effectiveOffset <= 1 )
525                {
526                    retValue = text.subSequence( 0, maxWidth - 1 ) + ellipsis;
527                }
528                else
529                {
530                    if( ((effectiveOffset + maxWidth) - 1) < len )
531                    {
532                        retValue = ellipsis + abbreviate( text.subSequence( effectiveOffset, len ), maxWidth - 1 );
533                    }
534                    else
535                    {
536                        retValue = ellipsis + text.subSequence( len - (maxWidth - 1), len );
537                    }
538                }
539            }
540            else
541            {
542                retValue = text.toString();
543            }
544        }
545
546        //---* Done *----------------------------------------------------------
547        return retValue;
548    }   //  abbreviate()
549
550    /**
551     *  <p>{@summary Abbreviates a String using ellipses (Unicode HORIZONTAL
552     *  ELLIPSIS, 0x2026) in the middle of the returned text.} This will turn
553     *  &quot;<i>Now is the time for all good men</i>&quot; into &quot;<i>Now
554     *  is &hellip; good men</i>&quot;.</p>
555     *  <p>Works like
556     *  {@link #abbreviate(CharSequence, int)}.</p>
557     *  <p>In no case will it return a String of length greater than
558     *  {@code maxWidth}.</p>
559     *  <p>Some samples:</p>
560     *  <pre>
561     *  StringUtils.abbreviateMiddle(null, *)      = null
562     *  StringUtils.abbreviateMiddle("", 5)        = ""
563     *  StringUtils.abbreviateMiddle("abcdefgh", 5) = "ab&hellip;gh"
564     *  StringUtils.abbreviateMiddle("abcdefgh", 7) = "ab&hellip;gh"
565     *  StringUtils.abbreviateMiddle("abcdefgh", 8) = "abcdefgh"
566     *  StringUtils.abbreviateMiddle("abcdefgh", 4) = IllegalArgumentException
567     *  </pre>
568     *
569     *  @param  input   The String to check, may be {@code null}.
570     *  @param  maxWidth    The maximum length of result String, must be at
571     *      least 5.
572     *  @return The abbreviated String, or {@code null} if the input was
573     *      already {@code null}.
574     *  @throws ValidationException The value for {@code maxWidth} was less
575     *      than 5.
576     *
577     *  @since 0.0.5
578     */
579    @API( status = STABLE, since = "0.0.5" )
580    public static final String abbreviateMiddle( final CharSequence input, final int maxWidth )
581    {
582        final var ellipsis = Character.toString( CHAR_ELLIPSIS ).intern();
583
584        String retValue = null;
585        if( nonNull( input ) )
586        {
587            if( maxWidth < 5 ) throw new ValidationException( String.format( MSG_AbbrTooShort, 5 ) );
588
589            final var len = input.length();
590            if( len > maxWidth )
591            {
592                final var suffixLength = (maxWidth - 1) / 2;
593                final var prefixLength = maxWidth - 1 - suffixLength;
594                final var suffixStart = len - suffixLength;
595                retValue = input.subSequence( 0, prefixLength ) + ellipsis + input.subSequence( suffixStart, suffixStart + suffixLength );
596            }
597            else
598            {
599                retValue = input.toString();
600            }
601        }
602
603        //---* Done *----------------------------------------------------------
604        return retValue;
605    }   //  abbreviateMiddle()
606
607    /**
608     *  <p>{@summary Breaks a long string into chunks of the given length.}</p>
609     *  <p>This method returns an instance of
610     *  {@link Stream} that can be easily converted into an array or a
611     *  collection.</p>
612     *  <p>To array:</p>
613     *  <div class="source-container"><pre>breakString( &lt;<i>string</i>&gt;, &lt;<i>chunk</i>&gt; ).toArray( String []::new )</pre></div>
614     *  <p>To collection (here: a
615     *  {@link List}):</p>
616     *  <div class="source-container"><pre>breakString( &lt;<i>string</i>&gt;, &lt;<i>chunk</i>&gt; ).collect( Collectors.toList() )</pre></div>
617     *
618     *  @param  input   The string.
619     *  @param  chunk   The chunk size.
620     *  @return The chunks from the string; the last chunk could be shorter
621     *      than the others.
622     *
623     *  @see Stream#toArray(java.util.function.IntFunction)
624     *  @see Stream#collect(java.util.stream.Collector)
625     *  @see java.util.stream.Collectors#toList()
626     *
627     *  @since 0.0.5
628     */
629    @API( status = STABLE, since = "0.0.5" )
630    public static final Stream<String> breakString( final CharSequence input, final int chunk )
631    {
632        if( chunk < 1 ) throw new ValidationException( "Chunk size must not be zero or a negative number: %d".formatted( chunk ) );
633
634        final Builder<String> builder = Stream.builder();
635        final var len = requireNonNullArgument( input, "input" ).length();
636        var pos = 0;
637        while( (pos + chunk) < len )
638        {
639            builder.add( input.subSequence( pos, pos + chunk ).toString() );
640            pos += chunk;
641        }
642        if( pos < len ) builder.add( input.subSequence( pos, len ).toString() );
643
644        final var retValue = builder.build();
645
646        //---* Done *----------------------------------------------------------
647        return retValue;
648    }   //  breakString()
649
650    /**
651     *  <p>{@summary Breaks a text into lines of the given length, but
652     *  different from
653     *  {@link #breakString(CharSequence, int)},
654     *  it will honour whitespace.}</p>
655     *  <p>This method returns an instance of
656     *  {@link Stream} that can be easily converted into an array, a String, or
657     *  a collection.</p>
658     *  <p>To array:</p>
659     *  <div class="source-container"><pre>breakText( &lt;<i>text</i>&gt;, &lt;<i>len</i>&gt; ).toArray( String []::new )</pre></div>
660     *  <p>To String:</p>
661     *  <div class="source-container"><pre>breakText( &lt;<i>text</i>&gt;, &lt;<i>len</i>&gt; ).collect( Collectors.joining() )</pre></div>
662     *  <p>To collection (here: a
663     *  {@link List}):</p>
664     *  <div class="source-container"><pre>breakText( &lt;<i>text</i>&gt;, &lt;<i>len</i>&gt; ).collect( Collectors.toList() )</pre></div>
665     *
666     *  @param  text    The text.
667     *  @param  lineLength  The length of a line.
668     *  @return The lines; if a word is longer than the given line length, a
669     *      line containing only that word can be longer that the given line
670     *      length.
671     *
672     *  @see Stream#toArray(java.util.function.IntFunction)
673     *  @see Stream#collect(java.util.stream.Collector)
674     *  @see java.util.stream.Collectors#joining()
675     *  @see java.util.stream.Collectors#joining(CharSequence)
676     *  @see java.util.stream.Collectors#joining(CharSequence, CharSequence, CharSequence)
677     *  @see java.util.stream.Collectors#toList()
678     *
679     *  @since 0.0.5
680     */
681    @API( status = STABLE, since = "0.0.5" )
682    public static final Stream<String> breakText( final CharSequence text, final int lineLength )
683    {
684        if( lineLength < 1 ) throw new ValidationException( "Line length size must not be zero or a negative number: %d".formatted( lineLength ) );
685
686        final Builder<String> builder = Stream.builder();
687
688        for( final var line : splitString( requireNonNullArgument( text, "text" ), '\n' ) )
689        {
690            if( isEmptyOrBlank( line ) )
691            {
692                builder.add( EMPTY_STRING );
693            }
694            else
695            {
696                final var buffer = new StringBuilder();
697                final var chunks = line.split( "\\s" );
698                SplitLoop: for( final var chunk : chunks )
699                {
700                    if( chunk.isEmpty() ) continue SplitLoop;
701                    if( (buffer.length() + 1 + chunk.length()) < lineLength )
702                    {
703                        if( isNotEmpty( buffer) ) buffer.append( ' ' );
704                    }
705                    else
706                    {
707                        if( isNotEmpty( buffer ) )
708                        {
709                            builder.add( buffer.toString() );
710                            buffer.setLength( 0 );
711                        }
712                    }
713                    buffer.append( chunk );
714                }   //  SplitLoop:
715                if( isNotEmpty( buffer ) ) builder.add( buffer.toString() );
716            }
717        }
718
719        final var retValue = builder.build();
720
721        //---* Done *----------------------------------------------------------
722        return retValue;
723    }   //  breakText()
724
725    /**
726     *  <p>{@summary <i>Capitalises</i> a String, meaning changing the first
727     *  letter to upper case as per
728     *  {@link Character#toUpperCase(char)}.} No other letters are changed.</p>
729     *  <p>A {@code null} input String returns {@code null}.</p>
730     *  <p>Samples:</p>
731     *  <pre><code>
732     *  StringUtils.capitalize( null )            == null;
733     *  StringUtils.capitalize( &quot;&quot; )    == &quot;&quot;;
734     *  StringUtils.capitalize( &quot;cat&quot; ) == &quot;Cat&quot;;
735     *  StringUtils.capitalize( &quot;cAt&quot; ) == &quot;CAt&quot;;</code></pre>
736     *  <p>Use this function to create a getter or setter name from the name of
737     *  the attribute.</p>
738     *  <p>This method does not recognise the
739     *  {@linkplain java.util.Locale#getDefault() default locale}.
740     *  This means that &quot;istanbul&quot; will become &quot;Istanbul&quot;
741     *  even for the locale {@code tr_TR} (although &quot;&#x130;stanbul&quot;
742     *  would be correct).</p>
743     *
744     *  @param input    The String to capitalise, can be {@code null}.
745     *  @return The capitalised String, or {@code null} if the argument
746     *      was already {@code null}.
747     *
748     *  @see #decapitalize(CharSequence)
749     *
750     *  @since 0.0.5
751     */
752    @API( status = STABLE, since = "0.0.5" )
753    public static final String capitalize( final CharSequence input )
754    {
755        String retValue = null;
756        if( isNotEmpty( input ) )
757        {
758            final var str = input.toString();
759            final var firstCodePoint = str.codePointAt( 0 );
760            final var newCodePoint = toUpperCase( firstCodePoint );
761            if( firstCodePoint == newCodePoint )
762            {
763                retValue = str;
764            }
765            else
766            {
767                final var strLen = str.length();
768                final var newCodePoints = new int [strLen];
769                var outOffset = 0;
770                newCodePoints [outOffset++] = newCodePoint;
771                //noinspection ForLoopWithMissingComponent
772                for( var inOffset = charCount( firstCodePoint ); inOffset < strLen; )
773                {
774                    final var codePoint = str.codePointAt( inOffset );
775                    newCodePoints [outOffset++] = codePoint;
776                    inOffset += charCount( codePoint );
777                }
778                retValue = new String( newCodePoints, 0, outOffset );
779            }
780        }
781        else if( nonNull( input ) )
782        {
783            retValue = EMPTY_STRING;
784        }
785
786        //---* Done *----------------------------------------------------------
787        return retValue;
788    }   //  capitalize()
789
790    /**
791     *  <p>{@summary <i>Capitalises</i> a String, meaning changing the first
792     *  letter to upper case as per
793     *  {@link Character#toTitleCase(char)}.} No other letters are changed.</p>
794     *  <p>A {@code null} input String returns {@code null}.</p>
795     *  <p>Samples:</p>
796     *  <pre><code>
797     *  StringUtils.capitalize( null )            == null;
798     *  StringUtils.capitalize( &quot;&quot; )    == &quot;&quot;;
799     *  StringUtils.capitalize( &quot;cat&quot; ) == &quot;Cat&quot;;
800     *  StringUtils.capitalize( &quot;cAt&quot; ) == &quot;CAt&quot;;</code></pre>
801     *  <p>Use this function to create a getter or setter name from the name of
802     *  the attribute.</p>
803     *  <p>This method does not recognise the
804     *  {@linkplain java.util.Locale#getDefault() default locale}.
805     *  This means that &quot;istanbul&quot; will become &quot;Istanbul&quot;
806     *  even for the locale {@code tr_TR} (although &quot;&#x130;stanbul&quot;
807     *  would be correct).</p>
808     *
809     *  @param input    The String to capitalise, can be {@code null}.
810     *  @return The capitalised String, or {@code null} if the argument
811     *      was already {@code null}.
812     *
813     *  @see #capitalize(CharSequence)
814     *  @see #decapitalize(CharSequence)
815     *
816     *  @since 0.4.8
817     */
818    @API( status = STABLE, since = "0.4.8" )
819    public static final String capitalizeToTitle( final CharSequence input )
820    {
821        String retValue = null;
822        if( isNotEmpty( input ) )
823        {
824            final var str = input.toString();
825            final var firstCodePoint = str.codePointAt( 0 );
826            final var newCodePoint = toTitleCase( firstCodePoint );
827            if( firstCodePoint == newCodePoint )
828            {
829                retValue = str;
830            }
831            else
832            {
833                final var strLen = str.length();
834                final var newCodePoints = new int [strLen];
835                var outOffset = 0;
836                newCodePoints [outOffset++] = newCodePoint;
837                //noinspection ForLoopWithMissingComponent
838                for( var inOffset = charCount( firstCodePoint ); inOffset < strLen; )
839                {
840                    final var codePoint = str.codePointAt( inOffset );
841                    newCodePoints [outOffset++] = codePoint;
842                    inOffset += charCount( codePoint );
843                }
844                retValue = new String( newCodePoints, 0, outOffset );
845            }
846        }
847        else if( nonNull( input ) )
848        {
849            retValue = EMPTY_STRING;
850        }
851
852        //---* Done *----------------------------------------------------------
853        return retValue;
854    }   //  capitalizeToTitle()
855
856    /**
857     *  Tests if the given text is not {@code null}, not empty and not
858     *  longer than the given maximum length. Use this to check whether a
859     *  String that is provided as an argument to a method is longer than
860     *  expected.
861     *
862     *  @param  name    The name that should appear in the exception if one
863     *      will be thrown. Usually this is the name of the argument to
864     *      validate.
865     *  @param  text    The text to check.
866     *  @param  maxLength   The maximum length.
867     *  @return Always the contents of <code>text</code> as a String; if the
868     *      argument fails any of the tests, an
869     *      {@link IllegalArgumentException}
870     *      or an exception derived from that will be thrown.
871     *  @throws CharSequenceTooLongException    {@code text} is longer than
872     *      {@code maxLength}.
873     *  @throws EmptyArgumentException Either {@code name} or {@code text} is
874     *      the empty String.
875     *  @throws NullArgumentException   Either {@code name} or {@code text} is
876     *      {@code null}.
877     *
878     *  @since 0.0.5
879     */
880    @API( status = STABLE, since = "0.0.5" )
881    public static final String checkTextLen( final String name, final CharSequence text, final int maxLength ) throws CharSequenceTooLongException, EmptyArgumentException, NullArgumentException
882    {
883        if( requireNotEmptyArgument( text, requireNotEmptyArgument( name, "name" ) ).length() > maxLength )
884        {
885            throw new CharSequenceTooLongException( name, maxLength );
886        }
887
888        //---* Done *----------------------------------------------------------
889        return text.toString();
890    }   //  checkTextLen()
891
892    /**
893     *  Tests if the given text is not longer than the given maximum length;
894     *  different from
895     *  {@link #checkTextLen(String, CharSequence, int)},
896     *  it may be {@code null} or empty.
897     *
898     *  @param  name    The name that should appear in the exception if one
899     *      will be thrown.
900     *  @param  text    The text to check; may be {@code null}.
901     *  @param  maxLength   The maximum length.
902     *  @return Always the contents of {@code text} as a String, {@code null}
903     *      if {@code text} was {@code null}; if the argument fails any of the
904     *      tests, an
905     *      {@link IllegalArgumentException}
906     *      or an exception derived from that will be thrown.
907     *  @throws CharSequenceTooLongException    {@code text} is longer than
908     *      {@code maxLength}.
909     *  @throws EmptyArgumentException  {@code name} is empty.
910     *  @throws NullArgumentException   {@code name} is {@code null}.
911     *
912     *  @since 0.0.5
913     */
914    @API( status = STABLE, since = "0.0.5" )
915    public static final String checkTextLenNull( final String name, final CharSequence text, final int maxLength ) throws CharSequenceTooLongException
916    {
917        requireNotEmptyArgument( name, "name" );
918
919        String retValue = null;
920        if( nonNull( text ) )
921        {
922            if( text.length() > maxLength )
923            {
924                throw new CharSequenceTooLongException( name, maxLength );
925            }
926            retValue = text.toString();
927        }
928
929        //---* Done *----------------------------------------------------------
930        return retValue;
931    }   //  checkTextLenNull()
932
933    /**
934     *  <p>{@summary Changes the first letter of the given String to lower case
935     *  as per
936     *  {@link Character#toLowerCase(char)}.}
937     *  No other letters are changed. A {@code null} input String returns
938     *  {@code null}.</p>
939     *  <p>Samples:</p>
940     *  <pre><code>
941     *  StringUtils.decapitalize( null )           = null;
942     *  StringUtils.decapitalize(&quot;&quot;)     = &quot;&quot;;
943     *  StringUtils.decapitalize(&quot;Cat&quot;)  = &quot;cat&quot;;
944     *  StringUtils.decapitalize(&quot;CAT&quot;)  = &quot;cAT&quot;;</code></pre>
945     *  <p>Basically, this is the complementary method to
946     *  {@link #capitalize(CharSequence)}.</p>
947     *  <p>Use this method to normalise the name of bean attributes.</p>
948     *
949     *  @param  input   The String to <i>decapitalise</i>, may be {@code null}.
950     *  @return The <i>decapitalised</i> String, {@code null} if the argument
951     *      was {@code null}.
952     *  @see #capitalize(CharSequence)
953     *
954     *  @since 0.0.5
955     */
956    @API( status = STABLE, since = "0.1.0" )
957    public static final String decapitalize( final CharSequence input )
958    {
959        String retValue = null;
960        if( isNotEmpty( input ) )
961        {
962            final var str = input.toString();
963            final var firstCodePoint = str.codePointAt( 0 );
964            final var newCodePoint = toLowerCase( firstCodePoint );
965            if( firstCodePoint == newCodePoint )
966            {
967                retValue = str;
968            }
969            else
970            {
971                final var strLen = str.length();
972                final var newCodePoints = new int [strLen];
973                var outOffset = 0;
974                newCodePoints [outOffset++] = newCodePoint;
975                //noinspection ForLoopWithMissingComponent
976                for( var inOffset = charCount( firstCodePoint ); inOffset < strLen; )
977                {
978                    final var codePoint = str.codePointAt( inOffset );
979                    newCodePoints [outOffset++] = codePoint;
980                    inOffset += charCount( codePoint );
981                }
982                retValue = new String( newCodePoints, 0, outOffset );
983            }
984        }
985        else if( nonNull( input ) )
986        {
987            retValue = EMPTY_STRING;
988        }
989
990        //---* Done *----------------------------------------------------------
991        return retValue;
992    }   //  decapitalize()
993
994    /**
995     *  <p>{@summary Escapes the non-ASCII and special characters in a
996     *  {@code String} so that the result can be used in the context of HTML.}
997     *  Wherever possible, the method will return the respective HTML&nbsp;5
998     *  entity; only when there is no matching entity, it will use the Unicode
999     *  escape.</p>
1000     *  <p>So if you call the method with the argument
1001     *  &quot;<i>S&uuml;&szlig;e</i>&quot;, it will return
1002     *  &quot;<code>S&amp;uuml;&amp;szlig;e</code>&quot;.</p>
1003     *  <p>If the input will be, for example, a Chinese text like this:
1004     *  &quot;<i>球体</i>&quot; (means "Ball"), you may get back something like
1005     *  this: &quot;<code>&amp;#x7403;&amp;#x4F53;</code>&quot;, as there are
1006     *  no entities defined for (any) Chinese letters.</p>
1007     *  <p>The method supports all known HTML&nbsp;5.0 entities, including
1008     *  funky accents. But it will not escape several commonly used characters
1009     *  like the full stop ('.'), the comma (','), the colon (':'), or the
1010     *  semicolon (';'), although they will be handled properly by
1011     *  {@link #unescapeHTML(CharSequence)}.</p>
1012     *  <p>Note that the commonly used apostrophe escape character
1013     *  (&amp;apos;) that was not a legal entity for HTML before HTML&nbsp;5 is
1014     *  now supported.</p>
1015     *
1016     *  @param  input   The {@code String} to escape, may be {@code null}.
1017     *  @return A new escaped {@code String}, or {@code null} if the
1018     *      argument was already {@code null}.
1019     *
1020     *  @see #unescapeHTML(CharSequence)
1021     *  @see <a href="http://hotwired.lycos.com/webmonkey/reference/special_characters/">ISO Entities</a>
1022     *  @see <a href="http://www.w3.org/TR/REC-html32#latin1">HTML 3.2 Character Entities for ISO Latin-1</a>
1023     *  @see <a href="http://www.w3.org/TR/REC-html40/sgml/entities.html">HTML 4.0 Character entity references</a>
1024     *  @see <a href="http://www.w3.org/TR/html401/charset.html#h-5.3">HTML 4.01 Character References</a>
1025     *  @see <a href="http://www.w3.org/TR/html401/charset.html#code-position">HTML 4.01 Code positions</a>
1026     *
1027     *  @since 0.0.5
1028     */
1029    /*
1030     *  For some unknown reasons, Javadoc will not accept the entities &#x7403;
1031     *  and &#x4F53; (for '球' and '体'), therefore it was required to add the
1032     *  Chinese characters directly into the comment above.
1033     */
1034    @API( status = STABLE, since = "0.0.5" )
1035    public static final String escapeHTML( final CharSequence input )
1036    {
1037        final var retValue = nonNull( input ) ? HTML50.escape( input ) : null;
1038
1039        //---* Done *----------------------------------------------------------
1040        return retValue;
1041    }   //  escapeHTML()
1042
1043    /**
1044     *  Escapes the characters in a {@code String} using HTML entities and
1045     *  writes them to an
1046     *  {@link Appendable}.
1047     *  For details, refer to
1048     *  {@link #escapeHTML(CharSequence)}.
1049     *
1050     *  @param  appendable  The appendable object receiving the escaped string.
1051     *  @param  input   The {@code String} to escape, may be {@code null}.
1052     *  @throws NullArgumentException   The appendable is {@code null}.
1053     *  @throws IOException when {@code Appendable} passed throws the exception
1054     *      from calls to the
1055     *      {@link Appendable#append(char)}
1056     *      method.
1057     *
1058     *  @see #escapeHTML(CharSequence)
1059     *  @see #unescapeHTML(CharSequence)
1060     *  @see <a href="http://hotwired.lycos.com/webmonkey/reference/special_characters/">ISO Entities</a>
1061     *  @see <a href="http://www.w3.org/TR/REC-html32#latin1">HTML 3.2 Character Entities for ISO Latin-1</a>
1062     *  @see <a href="http://www.w3.org/TR/REC-html40/sgml/entities.html">HTML 4.0 Character entity references</a>
1063     *  @see <a href="http://www.w3.org/TR/html401/charset.html#h-5.3">HTML 4.01 Character References</a>
1064     *  @see <a href="http://www.w3.org/TR/html401/charset.html#code-position">HTML 4.01 Code positions</a>
1065     *
1066     *  @since 0.0.5
1067     */
1068    @API( status = STABLE, since = "0.0.5" )
1069    public static final void escapeHTML( final Appendable appendable, final CharSequence input ) throws IOException
1070    {
1071        requireNonNullArgument( appendable, "appendable" );
1072
1073        if( nonNull( input ) ) HTML50.escape( appendable, input );
1074    }   //  escapeHTML()
1075
1076    /**
1077     *  Formats the given {@code String} for the output into JSONText. This
1078     *  means that the input sequence will be surrounded by double quotes, and
1079     *  backslash sequences are put into all the right places.<br>
1080     *  <br>&lt; and &gt; will be inserted as their Unicode values, allowing
1081     *  JSON text to be delivered in HTML.<br>
1082     *  <br>In JSON text, a string cannot contain a control character or an
1083     *  unescaped quote or backslash, so these are translated to Unicode
1084     *  escapes also.
1085     *
1086     *  @param  input   The string to escape to the JSON format; it may be
1087     *      empty, but not {@code null}.
1088     *  @return A string correctly formatted for insertion in a JSON text.
1089     *
1090     *  @since 0.0.5
1091     */
1092    @SuppressWarnings( "OverlyComplexMethod" )
1093    @API( status = STABLE, since = "0.0.5" )
1094    public static final String escapeJSON( final CharSequence input )
1095    {
1096        var retValue = "\"\""; // The JSON empty string.
1097        final var len = requireNonNullArgument( input, "input" ).length();
1098        if( len > 0 )
1099        {
1100            final var buffer = new StringBuilder( len * 2 ).append( '"' );
1101            char c;
1102            for( var i = 0; i < len; ++i )
1103            {
1104                c = input.charAt( i );
1105                switch( c )
1106                {
1107                    case '\\', '"', '<', '>', '&' -> buffer.append( escapeCharacter( c ) );
1108
1109                    case '\b' -> buffer.append( "\\b" );
1110
1111                    case '\t' -> buffer.append( "\\t" );
1112
1113                    case '\n'-> buffer.append( "\\n" );
1114
1115                    case '\f' -> buffer.append( "\\f" );
1116
1117                    case '\r' -> buffer.append( "\\r" );
1118
1119                    default ->
1120                    {
1121                        //noinspection OverlyComplexBooleanExpression,CharacterComparison,UnnecessaryUnicodeEscape
1122                        if( (c < ' ')
1123                            || ((c >= '\u0080') && (c < '\u00a0'))
1124                            || ((c >= '\u2000') && (c < '\u2100')) )
1125                        {
1126                            buffer.append( escapeCharacter( c ) );
1127                        }
1128                        else
1129                        {
1130                            buffer.append( c );
1131                        }
1132                    }
1133                }
1134            }
1135            buffer.append( '"' );
1136            retValue = buffer.toString();
1137        }
1138
1139        //---* Done *----------------------------------------------------------
1140        return retValue;
1141    }   //  escapeJSON()
1142
1143    /**
1144     *  Escapes the given character using Regex escapes and writes them to a
1145     *  {@link Appendable}.
1146     *
1147     *  @param  appendable  The appendable receiving the escaped string.
1148     *  @param  c   The character to escape.
1149     *  @throws NullArgumentException   The appendable is {@code null}.
1150     *  @throws IOException when {@code Appendable} passed throws the exception
1151     *      from calls to the
1152     *      {@link Appendable#append(CharSequence)}
1153     *      method.
1154     *
1155     *  @since 0.0.5
1156     */
1157    @SuppressWarnings( "SwitchStatementWithTooManyBranches" )
1158    @API( status = STABLE, since = "0.0.5" )
1159    public static final void escapeRegex( final Appendable appendable, final char c ) throws IOException
1160    {
1161        requireNonNullArgument( appendable, "appendable" );
1162
1163        TestSwitch: switch( c )
1164        {
1165            case '\\' -> appendable.append( "\\" );
1166            case '[', ']', '{', '}', '(', ')', '^', '$', '&', '*', '.', '+', '|', '?' -> appendable.append( "\\" ).append( c );
1167            case '\t' -> appendable.append( "\\t" );
1168            case '\n' -> appendable.append( "\\n" );
1169            case '\r' -> appendable.append( "\\r" );
1170            case '\f' -> appendable.append( "\\f" );
1171            case '\u0007' -> appendable.append( "\\a" );
1172            case '\u001B' -> appendable.append( "\\e" ); // ESC
1173            default -> appendable.append( c );
1174        }   //  TestSwitch:
1175    }   //  escapeRegex()
1176
1177    /**
1178     *  Escapes the given character using Regex escapes.
1179     *
1180     *  @param  c   The character to escape.
1181     *  @return A {@code String} with the escaped character.
1182     *
1183     *  @since 0.0.5
1184     */
1185    @API( status = STABLE, since = "0.0.5" )
1186    public static final String escapeRegex( final char c )
1187    {
1188        final var retValue = new StringBuilder();
1189        try
1190        {
1191            escapeRegex( retValue, c );
1192        }
1193        catch( final IOException e )
1194        {
1195            /*
1196             * We append to a StringBuilder, and StringBuilder.append() does
1197             * not define an IOException.
1198             */
1199            throw new ImpossibleExceptionError( e );
1200        }
1201
1202        //---* Done *----------------------------------------------------------
1203        return retValue.toString();
1204    }   //  escapeRegex()
1205
1206    /**
1207     *  Escapes the characters in a {@code String} using Regex escapes.
1208     *
1209     *  @param  input   The {@code String} to escape, may be {@code null}.
1210     *  @return A new escaped {@code String}, or {@code null} if the argument
1211     *      was already {@code null}.
1212     *
1213     *  @since 0.0.5
1214     */
1215    @API( status = STABLE, since = "0.0.5" )
1216    public static final String escapeRegex( final CharSequence input )
1217    {
1218        String retValue = null;
1219        if( nonNull( input ) )
1220        {
1221            final var len = input.length();
1222            if( len > 0 )
1223            {
1224                final var buffer = new StringBuilder( (len * 12) / 10 );
1225                try
1226                {
1227                    escapeRegex( buffer, input );
1228                }
1229                catch( final IOException e )
1230                {
1231                    /*
1232                     * We append to a StringBuilder, and StringBuilder.append() does
1233                     * not define an IOException.
1234                     */
1235                    throw new ImpossibleExceptionError( e );
1236                }
1237                retValue = buffer.toString();
1238            }
1239            else
1240            {
1241                retValue = EMPTY_STRING;
1242            }
1243        }
1244
1245        //---* Done *----------------------------------------------------------
1246        return retValue;
1247    }   //  escapeRegex()
1248
1249    /**
1250     *  Escapes the characters in a {@code String} using Regex escapes and
1251     *  writes them to a
1252     *  {@link Appendable}.
1253     *
1254     *  @param  appendable  The appendable receiving the escaped string.
1255     *  @param  input   The {@code String} to escape. If {@code null} or the empty
1256     *      String, nothing will be put to the appendable.
1257     *  @throws NullArgumentException   The appendable is {@code null}.
1258     *  @throws IOException when {@code Appendable} passed throws the exception
1259     *      from calls to the
1260     *      {@link Appendable#append(CharSequence)}
1261     *      method.
1262     *
1263     *  @since 0.0.5
1264     */
1265    @API( status = STABLE, since = "0.0.5" )
1266    public static final void escapeRegex( final Appendable appendable, final CharSequence input ) throws IOException
1267    {
1268        requireNonNullArgument( appendable, "appendable" );
1269
1270        if( isNotEmpty( input ) )
1271        {
1272            ScanLoop: for( var i = 0; i < input.length(); ++i )
1273            {
1274                escapeRegex( appendable, input.charAt( i ) );
1275            }   //  ScanLoop:
1276        }
1277    }   //  escapeRegex()
1278
1279    /**
1280     *  <p>{@summary Escapes the characters in a {@code String} using XML
1281     *  entities.}</p>
1282     *  <p>For example:</p>
1283     *  <p>{@code "bread" & "butter"}</p>
1284     *  <p>becomes:</p>
1285     *  <p><code>&amp;quot;bread&amp;quot; &amp;amp;
1286     *  &amp;quot;butter&amp;quot;</code>.</p>
1287     *
1288     *  @param  input   The {@code String} to escape, may be null.
1289     *  @return A new escaped {@code String}, or {@code null} if the
1290     *      argument was already {@code null}.
1291     *
1292     *  @see #unescapeXML(CharSequence)
1293     */
1294    @API( status = STABLE, since = "0.0.5" )
1295    public static final String escapeXML( final CharSequence input )
1296    {
1297        final var retValue = nonNull( input ) ? XML.escape( input ) : null;
1298
1299        //---* Done *----------------------------------------------------------
1300        return retValue;
1301    }   //  escapeXML()
1302
1303    /**
1304     *  <p>{@summary Escapes the characters in a {@code String} using XML
1305     *  entities and writes them to an
1306     *  {@link Appendable}.}</p>
1307     *  <p>For example:</p>
1308     *  <p>{@code "bread" & "butter"}</p>
1309     *  <p>becomes:</p>
1310     *  <p><code>&amp;quot;bread&amp;quot; &amp;amp;
1311     *  &amp;quot;butter&amp;quot;</code>.</p>
1312     *
1313     *  @param  appendable  The appendable object receiving the escaped string.
1314     *  @param  input   The {@code String} to escape, may be {@code null}.
1315     *  @throws NullArgumentException   The appendable is {@code null}.
1316     *  @throws IOException when {@code Appendable} passed throws the exception
1317     *      from calls to the
1318     *      {@link Appendable#append(char)}
1319     *      method.
1320     *
1321     *  @see #escapeXML(CharSequence)
1322     *  @see #unescapeXML(CharSequence)
1323     *
1324     *  @since 0.0.5
1325     */
1326    @API( status = STABLE, since = "0.0.5" )
1327    public static final void escapeXML( final Appendable appendable, final CharSequence input ) throws IOException
1328    {
1329        requireNonNullArgument( appendable, "appendable" );
1330
1331        if( nonNull( input ) ) XML.escape( appendable, input );
1332    }   //  escapeXML()
1333
1334    /**
1335     *  Tests if the given String is {@code null} or the empty String.
1336     *
1337     *  @param  input   The String to test.
1338     *  @return {@code true} if the given String reference is
1339     *      {@code null} or the empty String.
1340     *
1341     *  @since 0.0.5
1342     */
1343    @API( status = STABLE, since = "0.0.5" )
1344    public static final boolean isEmpty( final CharSequence input ) { return isNull( input ) || input.isEmpty(); }
1345
1346    /**
1347     *  Tests if the given String is {@code null}, the empty String, or just
1348     *  containing whitespace.
1349     *
1350     *  @param  input   The String to test.
1351     *  @return {@code true} if the given String reference is not
1352     *      {@code null} and not the empty String.
1353     *
1354     *  @see String#isBlank()
1355     *
1356     *  @since 0.0.5
1357     */
1358    @API( status = STABLE, since = "0.0.5" )
1359    public static final boolean isEmptyOrBlank( final CharSequence input )
1360    {
1361        final var retValue = isNull( input ) || input.toString().isBlank();
1362
1363        //---* Done *----------------------------------------------------------
1364        return retValue;
1365    }   //  isEmptyOrBlank()
1366
1367    /**
1368     *  Tests if the given String is not {@code null} and not the empty
1369     *  String.
1370     *
1371     *  @param  input   The String to test.
1372     *  @return {@code true} if the given String reference is not
1373     *      {@code null} and not the empty String.
1374     *
1375     *  @since 0.0.5
1376     */
1377    @API( status = STABLE, since = "0.0.5" )
1378    public static final boolean isNotEmpty( final CharSequence input ) { return nonNull( input ) && !input.isEmpty(); }
1379
1380    /**
1381     *  Tests if the given String is not {@code null}, not the empty String,
1382     *  and that it contains other characters than just whitespace.
1383     *
1384     *  @param  input   The String to test.
1385     *  @return {@code true} if the given String reference is not
1386     *      {@code null} and not the empty String, and it contains other
1387     *      characters than just whitespace.
1388     *
1389     *  @see String#isBlank()
1390     *
1391     *  @since 0.0.5
1392     */
1393    @API( status = STABLE, since = "0.0.5" )
1394    public static final boolean isNotEmptyOrBlank( final CharSequence input )
1395    {
1396        final var retValue = nonNull( input ) && !input.toString().isBlank();
1397
1398        //---* Done *----------------------------------------------------------
1399        return retValue;
1400    }   //  isNotEmptyOrBlank()
1401
1402    /**
1403     *  <p>{@summary Returns the given replacement value if the given String is
1404     *  {@code null} or empty.} Otherwise the original String is returned.</p>
1405     *
1406     *  @param  input   The String to test.
1407     *  @param  replacement The replacement; can be {@code null}.
1408     *  @return Either the {@code input} or the {@code replacment} in case the
1409     *      input is {@code null} or empty.
1410     *
1411     *  @see #isEmpty(CharSequence)
1412     *  @see Objects#mapFromNull(Object,Object)
1413     *
1414     *  @since 0.25.2
1415     */
1416    @API( status = STABLE, since = "0.25.2" )
1417    public static final String mapFromEmpty( final CharSequence input, final String replacement )
1418    {
1419        final var retValue = isNull( input ) || input.isEmpty() ? replacement : input.toString();
1420
1421        //---* Done *----------------------------------------------------------
1422        return retValue;
1423    }   //  mapFromEmpty()
1424
1425    /**
1426     *  <p>{@summary Returns the replacement provided by the given supplier if
1427     *  the given String is {@code null} or empty.} Otherwise the original
1428     *  String is returned.</p>
1429     *
1430     *  @param  input   The String to test.
1431     *  @param  replacementSupplier Provides a replacement for the empty input.
1432     *  @return Either the {@code input} or the replacement value in case the
1433     *      input is {@code null} or empty.
1434     *
1435     *  @see #isEmpty(CharSequence)
1436     *  @see Objects#mapFromNull(Object, Supplier)
1437     *
1438     *  @since 0.25.2
1439     */
1440    @API( status = STABLE, since = "0.25.2" )
1441    public static final String mapFromEmpty( final CharSequence input, final Supplier<? extends CharSequence> replacementSupplier )
1442    {
1443        requireNonNullArgument( replacementSupplier, "replacementSupplier" );
1444        final var charSequence = isNull( input ) || input.isEmpty() ? replacementSupplier.get() : input;
1445        final var retValue = nonNull( charSequence ) ? charSequence.toString() : null;
1446
1447        //---* Done *----------------------------------------------------------
1448        return retValue;
1449    }   //  mapFromEmpty()
1450
1451    /**
1452     *  Determines the maximum length over all Strings provided in the given
1453     *  {@link Stream}.
1454     *
1455     *  @param  stream  The strings.
1456     *  @return The length of the longest string in the list; -1 if all values
1457     *      in the given {@code stream} are {@code null}, and
1458     *      {@link Integer#MIN_VALUE}
1459     *      if the given {@code stream} is empty.
1460     *
1461     *  @since 0.0.5
1462     */
1463    @API( status = STABLE, since = "0.0.5" )
1464    public static final int maxContentLength( final Stream<? extends CharSequence> stream )
1465    {
1466        final var retValue = requireNonNullArgument( stream, "stream" )
1467            .mapToInt( string -> nonNull( string ) ? string.length() : -1 )
1468            .max()
1469            .orElse( Integer.MIN_VALUE );
1470
1471        //---* Done *----------------------------------------------------------
1472        return retValue;
1473    }   //  maxContentLength()
1474
1475    /**
1476     *  Determines the maximum length over all strings provided in the given
1477     *  {@link Collection}.
1478     *
1479     *  @param  list    The strings.
1480     *  @return The length of the longest string in the list; -1 if all values
1481     *      in the given {@code list} are {@code null}, and
1482     *      {@link Integer#MIN_VALUE}
1483     *      if the given {@code list} is empty.
1484     *
1485     *  @since 0.0.5
1486     */
1487    @API( status = STABLE, since = "0.0.5" )
1488    public static final int maxContentLength( final Collection<? extends CharSequence> list )
1489    {
1490        final var retValue = maxContentLength( requireNonNullArgument( list, "list" ).stream() );
1491
1492        //---* Done *----------------------------------------------------------
1493        return retValue;
1494    }   //  maxContentLength()
1495
1496    /**
1497     *  Determines the maximum length over all strings provided in the given
1498     *  array.
1499     *
1500     *  @param  a   The strings.
1501     *  @return The length of the longest string in the list; -1 if all values
1502     *      in the array are {@code null}, and
1503     *      {@link Integer#MIN_VALUE}
1504     *      if the given array has zero length.
1505     *
1506     *  @since 0.0.5
1507     */
1508    @API( status = STABLE, since = "0.0.5" )
1509    public static final int maxContentLength( final CharSequence... a )
1510    {
1511        final var retValue = maxContentLength( Arrays.stream( requireNonNullArgument( a, "a" ) ) );
1512
1513        //---* Done *----------------------------------------------------------
1514        return retValue;
1515    }   //  maxContentLength()
1516
1517    /**
1518     *  <p>{@summary Normalizes the given String to a pure ASCII String.} This
1519     *  replaces 'ß' by 'ss' and replaces all diacritical characters by their
1520     *  base form (that mean that 'ü' gets 'u' and so on). For the normalising
1521     *  of a search criteria, this should be sufficient, although it may cause
1522     *  issues for non-latin scripts, as for these the input can be mapped to
1523     *  the empty String.
1524     *
1525     *  @note   The Scandinavian letters 'ø' and 'Ø' are not diacritical
1526     *      letters, nevertheless they will be replaced.
1527     *
1528     *  @param  input   The input string.
1529     *  @return The normalised String, only containing ASCII characters; it
1530     *      could be empty.
1531     *
1532     *  TODO Check the implementation and the results!! 2022-12-10
1533     */
1534    public static final String normalizeToASCII( final CharSequence input )
1535    {
1536        final var str = requireNonNullArgument( input, "s" ).toString()
1537            .replace( "ß", "ss" )
1538            .replace( 'ø', 'o' )
1539            .replace( 'Ø', 'O' );
1540        final var retValue = normalize( str, NFD )
1541            .replaceAll( "[^\\p{ASCII}]", EMPTY_STRING );
1542
1543        //---* Done *----------------------------------------------------------
1544        return retValue;
1545    }   //  normalizeToASCII()
1546
1547    /**
1548     *  Brings the given string to the given length and uses the provided
1549     *  padding character to fill up the string.
1550     *
1551     *  @param  input   The string to format.
1552     *  @param  length  The desired length; if 0 or less, the given string is
1553     *      returned, regardless of {@code clip}.
1554     *  @param  c   The pad character.
1555     *  @param  mode    The
1556     *      {@linkplain StringUtils.Padding pad mode}.
1557     *  @param  clip    {@code true} if the input string should be cut in case
1558     *      it is longer than {@code length}, {@code false} if it has to be
1559     *      returned unchanged .
1560     *  @return The re-formatted string.
1561     *
1562     *  @since 0.0.5
1563     */
1564    @API( status = STABLE, since = "0.0.5" )
1565    public static final String pad( final CharSequence input, final int length, final char c, final Padding mode, final boolean clip )
1566    {
1567        return pad( input, length, c, mode, clip ? CLIPPING_CUT : CLIPPING_NONE );
1568    }   //  pad()
1569
1570    /**
1571     *  Brings the given string to the given length and uses the provided
1572     *  padding character to fill up the string.
1573     *
1574     *  @param  input   The string to format.
1575     *  @param  length  The desired length; if 0 or less, the given string is
1576     *      returned, regardless of {@code clip}.
1577     *  @param  c   The pad character.
1578     *  @param  mode    The
1579     *      {@linkplain StringUtils.Padding pad mode}.
1580     *  @param  clip    The
1581     *      {@linkplain StringUtils.Clipping clipping mode}.
1582     *  @return The re-formatted string.
1583     *
1584     *  @since 0.0.5
1585     */
1586    @API( status = STABLE, since = "0.0.5" )
1587    public static final String pad( final CharSequence input, final int length, final char c, final Padding mode, final Clipping clip )
1588    {
1589        //noinspection OverlyComplexBooleanExpression
1590        if( ((requireNonNullArgument( clip, "clip" ) == CLIPPING_ABBREVIATE) && (length < 4)) || ((clip == CLIPPING_ABBREVIATE_MIDDLE) && (length < 5)) )
1591        {
1592            throw new ValidationException( "Length %d is too short for clipping mode %s".formatted( length, clip.toString() ) );
1593        }
1594        requireNonNullArgument( mode, "mode" );
1595
1596        final String retValue;
1597        final var currentLength = requireNonNullArgument( input, "input" ).length();
1598
1599        if( (length > 0) && (length != currentLength) )
1600        {
1601            if( currentLength > length )
1602            {
1603                retValue = clip.clip( input, length );
1604            }
1605            else
1606            {
1607                final var padSize = length - currentLength;
1608                retValue = mode.pad( input, padSize, c );
1609            }
1610        }
1611        else
1612        {
1613            retValue = input.toString();
1614        }
1615
1616        //---* Done *----------------------------------------------------------
1617        return retValue;
1618    }   //  pad()
1619
1620    /**
1621     *  <p>{@summary Fills up the given string to the given length by adding
1622     *  blanks on both sides; will abbreviate the string if it is longer than
1623     *  the given length.} The minimum length is 5.</p>
1624     *  <p>This is a shortcut to a call to
1625     *  {@link #pad(CharSequence,int,char,Padding,Clipping) pad( input, length, ' ', PADDING_CENTER, CLIPPING_ABBREVIATE_MIDDLE ) }.</p>
1626     *
1627     *  @param  input   The string to format.
1628     *  @param  length  The desired length; minimum value is 5.
1629     *  @return The re-formatted string.
1630     *
1631     *  @see Padding#PADDING_CENTER
1632     *  @see Clipping#CLIPPING_ABBREVIATE_MIDDLE
1633     *
1634     *  @since 0.0.5
1635     */
1636    @API( status = STABLE, since = "0.0.5" )
1637    public static final String padCenter( final CharSequence input, final int length ) { return pad( input, length, ' ', PADDING_CENTER, CLIPPING_ABBREVIATE_MIDDLE ); }
1638
1639    /**
1640     *  <p>{@summary Fills up the given string to the given length by adding
1641     *  blanks on the left side;  will abbreviate the string if it is longer
1642     *  than the given length.} The minimum length is 4.</p>
1643     *  <p>This is a shortcut to a call to
1644     *  {@link #pad(CharSequence,int,char,Padding,Clipping) pad( input, length, ' ', PADDING_LEFT, CLIPPING_ABBREVIATE ) }.</p>
1645     *
1646     *  @param  input   The string to format.
1647     *  @param  length  The desired length; the minimum value is 4.
1648     *  @return The re-formatted string.
1649     *
1650     *  @see Padding#PADDING_LEFT
1651     *  @see Clipping#CLIPPING_ABBREVIATE
1652     *
1653     *  @since 0.0.5
1654     */
1655    @API( status = STABLE, since = "0.0.5" )
1656    public static final String padLeft( final CharSequence input, final int length ) { return pad( input, length, ' ', PADDING_LEFT, CLIPPING_ABBREVIATE ); }
1657
1658    /**
1659     *  <p>{@summary Fills up the given string to the given length by adding
1660     *  blanks on the right side; will abbreviate the string if it is longer
1661     *  than the given length.} The minimum length is 4.</p>
1662     *  <p>This is a shortcut to a call to
1663     *  {@link #pad(CharSequence,int,char,Padding,Clipping) pad( input, length, ' ', PADDING_RIGHT, CLIPPING_ABBREVIATE ) }.</p>
1664     *
1665     *  @param  input   The string to format.
1666     *  @param  length  The desired length; the minimum value is 4.
1667     *  @return The re-formatted string.
1668     *
1669     *  @see Padding#PADDING_RIGHT
1670     *  @see Clipping#CLIPPING_ABBREVIATE
1671     *
1672     *  @since 0.0.5
1673     */
1674    @API( status = STABLE, since = "0.0.5" )
1675    public static final String padRight( final CharSequence input, final int length ) { return pad( input, length, ' ', PADDING_RIGHT, CLIPPING_ABBREVIATE ); }
1676
1677    /**
1678     *  <p>{@summary Surrounds the given String with double-quotes
1679     *  (&quot;, &amp;#34;).}</p>
1680     *  <p>When the double-quote is needed in a String constant, it has to be
1681     *  escaped with a backslash:</p>
1682     *  <pre><code>&quot;\&quot;…\&quot;&quot;</code></pre>
1683     *  <p>Sometimes, this is just ugly, and there this method comes into
1684     *  play.</p>
1685     *
1686     *  @param  input   The String to surround; can be {@code null}.
1687     *  @return The quoted String; will be {@code null} if the argument was
1688     *      {@code null} already.
1689     */
1690    public static final String quote( final CharSequence input )
1691    {
1692        final var retValue = isNull( input ) ? null : String.format( "\"%s\"", input );
1693
1694        //---* Done *----------------------------------------------------------
1695        return retValue;
1696    }   //  quote()
1697
1698    /**
1699     *  <p>{@summary This method replaces all diacritical characters in the
1700     *  input String by their base form.} That means that 'ü' gets 'u', `È'
1701     *  gets 'E' and so on.</p>
1702     *  <p>This differs from
1703     *  {@link #normalizeToASCII(CharSequence)}
1704     *  as this method still allows non-ASCII characters in the output.</p>
1705     *
1706     *  @note   The Scandinavian letters 'ø' and 'Ø' are not diacritical
1707     *      letters, meaning they will not be replaced.
1708     *
1709     *  @param  input   The input string.
1710     *  @return The normalised String, not containing any diacritical
1711     *      characters.
1712     *
1713     *  TODO Check the implementation and the results!! 2022-12-10
1714     */
1715    public static final String removeDiacriticalMarks( final CharSequence input )
1716    {
1717        final var retValue = normalize( requireNonNullArgument( input, "input" ), NFD )
1718            .replaceAll("\\p{InCombiningDiacriticalMarks}+", EMPTY_STRING );
1719
1720        //---* Done *----------------------------------------------------------
1721        return retValue;
1722    }   //  removeDiacriticalMarks()
1723
1724    /**
1725     *  Repeats the given char {@code repeat} to form a new String. The table
1726     *  below shows the various  result for some argument combinations.<br>
1727     *  <br><code>
1728     *  StringUtils.repeat(&nbsp;'a',&nbsp;0&nbsp;)&nbsp;&rArr;&nbsp;""<br>
1729     *  StringUtils.repeat(&nbsp;'a',&nbsp;3&nbsp;)&nbsp;&rArr;&nbsp;"aaa"<br>
1730     *  StringUtils.repeat(&nbsp;'a',&nbsp;-2&nbsp;)&nbsp;&rArr;&nbsp;""<br>
1731     *  </code>
1732     *
1733     *  @param  c   The character to repeat.
1734     *  @param  count   The number of times to repeat {@code c}; a negative
1735     *      value will be treated as zero.
1736     *  @return A new String consisting of the given character repeated
1737     *      {@code count} times, or the empty String if {@code count} was 0
1738     *      or negative.
1739     *
1740     *  @see String#repeat(int)
1741     *
1742     *  @since 0.0.5
1743     */
1744    @API( status = STABLE, since = "0.0.5" )
1745    public static final String repeat( final char c, final int count )
1746    {
1747        final var retValue = ( count > 0 ? Character.toString( c ).repeat( count ) : EMPTY_STRING).intern();
1748
1749        //---* Done *----------------------------------------------------------
1750        return retValue;
1751    }   //  repeat()
1752
1753    /**
1754     *  Repeats the given char {@code repeat}, identified by its code point, to
1755     *  form a new String. The
1756     *  table below shows the various  result for some argument
1757     *  combinations.<br>
1758     *  <br><code>
1759     *  StringUtils.repeat(&nbsp;'a',&nbsp;0&nbsp;)&nbsp;&rArr;&nbsp;""<br>
1760     *  StringUtils.repeat(&nbsp;'a',&nbsp;3&nbsp;)&nbsp;&rArr;&nbsp;"aaa"<br>
1761     *  StringUtils.repeat(&nbsp;'a',&nbsp;-2&nbsp;)&nbsp;&rArr;&nbsp;""<br>
1762     *  </code>
1763     *
1764     *  @param  codePoint   The character to repeat.
1765     *  @param  count   The number of times to repeat {@code c}; a negative
1766     *      value will be treated as zero.
1767     *  @return A new String consisting of the given character repeated
1768     *      {@code count} times, or the empty String if {@code count} was 0
1769     *      or negative, or {@code null} if the code point is invalid.
1770     *
1771     *  @see Character#isValidCodePoint(int)
1772     *  @see String#repeat(int)
1773     *
1774     *  @since 0.0.5
1775     */
1776    @API( status = STABLE, since = "0.0.5" )
1777    public static final String repeat( final int codePoint, final int count )
1778    {
1779        final var retValue = (count > 0)
1780            ? isValidCodePoint( codePoint )
1781                ? Character.toString( codePoint ).repeat( count ).intern()
1782                : null
1783            : EMPTY_STRING;
1784
1785        //---* Done *----------------------------------------------------------
1786        return retValue;
1787    }   //  repeat()
1788
1789    /**
1790     *  Repeats the given String {@code repeat} times to form a new String. The
1791     *  table below shows the various  result for some argument
1792     *  combinations.<br>
1793     *  <br><code>
1794     *  StringUtils.repeat(&nbsp;null,&nbsp;2&nbsp;)&nbsp;&rArr;&nbsp;null<br>
1795     *  StringUtils.repeat(&nbsp;"",&nbsp;0&nbsp;)&nbsp;&rArr;&nbsp;""<br>
1796     *  StringUtils.repeat(&nbsp;"",&nbsp;2&nbsp;)&nbsp;&rArr;&nbsp;""<br>
1797     *  StringUtils.repeat(&nbsp;"a",&nbsp;3&nbsp;)&nbsp;&rArr;&nbsp;"aaa"<br>
1798     *  StringUtils.repeat(&nbsp;"ab",&nbsp;2&nbsp;)&nbsp;&rArr;&nbsp;"abab"<br>
1799     *  StringUtils.repeat(&nbsp;"a",&nbsp;-2&nbsp;)&nbsp;&rArr;&nbsp;""<br>
1800     *  </code>
1801     *
1802     *  @param  input The String to repeat, may be {@code null}.
1803     *  @param  count   The number of times to repeat {@code str}; a negative
1804     *      value will be treated as zero.
1805     *  @return A new String consisting of the original String repeated,
1806     *      {@code count} times, the empty String if {@code count} was 0
1807     *      or negative, or {@code null} if the input String was
1808     *      {@code null}, too.
1809     *
1810     *  @see String#repeat(int)
1811     *
1812     *  @since 0.0.5
1813     */
1814    @API( status = STABLE, since = "0.0.5" )
1815    public static final String repeat( final CharSequence input, final int count )
1816    {
1817        final var retValue =
1818            nonNull( input )
1819                ? (count > 0) && !input.isEmpty()
1820                    ? input.toString().repeat( count )
1821                    : EMPTY_STRING
1822                : null;
1823
1824        //---* Done *----------------------------------------------------------
1825        return retValue;
1826    }   //  repeat()
1827
1828    /**
1829     *  <p>{@summary Splits a String by the given separator character and
1830     *  returns an array of all parts.} In case a separator character is
1831     *  immediately followed by another separator character, an empty String
1832     *  will be placed to the array.</p>
1833     *  <p>Beginning and end of the String are treated as separators. If the
1834     *  first character of the String is a separator, the returned array will
1835     *  start with an empty String, as it will end with an empty String if the
1836     *  last character is a separator.</p>
1837     *  <p>In case the String is empty, the return value will be an array
1838     *  containing just the empty String. It will not be empty.</p>
1839     *
1840     *  @param  input  The String to split.
1841     *  @param  separator   The separator character.
1842     *  @return The parts of the String.
1843     *
1844     *  @since 0.0.5
1845     */
1846    @API( status = STABLE, since = "0.0.5" )
1847    public static final String [] splitString( final CharSequence input, final char separator )
1848    {
1849        return splitString( input, (int) separator );
1850    }   //  splitString()
1851
1852    /**
1853     *  <p>{@summary Splits a String by the given separator character,
1854     *  identified by its Unicode code point, and returns an array of all
1855     *  parts.} In case a separator character is immediately followed by
1856     *  another separator character, an empty String will be placed to the
1857     *  array.</p>
1858     *  <p>Beginning and end of the String are treated as separators, so if the
1859     *  first character of the String is a separator, the returned array will
1860     *  start with an empty String, as it will end with an empty String if the
1861     *  last character is a separator.</p>
1862     *  <p>In case the String is empty, the return value will be an array
1863     *  containing just the empty String. It will not be empty.</p>
1864     *
1865     *  @param  input  The String to split.
1866     *  @param  separator   The code point for the separator character.
1867     *  @return The parts of the String.
1868     *
1869     *  @since 0.0.5
1870     */
1871    @API( status = STABLE, since = "0.0.5" )
1872    public static final String [] splitString( final CharSequence input, final int separator )
1873    {
1874        final var retValue = stream( input, separator ).toArray( String []::new );
1875
1876        //---* Done *----------------------------------------------------------
1877        return retValue;
1878    }   //  splitString()
1879
1880    /**
1881     *  <p>{@summary Splits a String by the given separator sequence and
1882     *  returns an array of all parts.} In case a separator sequence is
1883     *  immediately followed by another separator sequence, an empty String
1884     *  will be placed to the array.</p>
1885     *  <p>Beginning and end of the String are treated as separators, so if the
1886     *  first part of the String equals the separator sequence, the returned
1887     *  array will start with an empty String, as it will end with an empty
1888     *  String if the last part would equal the separator sequence.</p>
1889     *  <p>In case the String is empty, the return value will be an array
1890     *  containing just the empty String. It will not be empty.</p>
1891     *
1892     *  @param  input  The String to split.
1893     *  @param  separator   The separator sequence.
1894     *  @return The parts of the String.
1895     *
1896     *  @since 0.0.5
1897     */
1898    @API( status = STABLE, since = "0.0.5" )
1899    public static final String [] splitString( final CharSequence input, final CharSequence separator )
1900    {
1901        final var retValue = stream( input, separator).toArray( String []::new );
1902
1903        //---* Done *----------------------------------------------------------
1904        return retValue;
1905    }   //  splitString()
1906
1907    /**
1908     *  <p>{@summary Splits a String by the given separator character and
1909     *  returns an instance of
1910     *  {@link Stream}
1911     *  providing all parts.} In case a separator character is immediately
1912     *  followed by another separator character, an empty String will be put to
1913     *  the {@code Stream}.</p>
1914     *  <p>Beginning and end of the String are treated as separators, so if the
1915     *  first character of the String is a separator, the returned
1916     *  {@code Stream} will start with an empty String, as it will end with an
1917     *  empty String if the last character is a separator.</p>
1918     *  <p>In case the String is empty, the return value will be a
1919     *  {@code Stream} containing just the empty String. It will not be
1920     *  empty.</p>
1921     *
1922     *  @param  input  The String to split.
1923     *  @param  separator   The separator character.
1924     *  @return A {@code Stream} instance with the parts of the String.
1925     *
1926     *  @since 0.0.7
1927     */
1928    @API( status = STABLE, since = "0.0.7" )
1929    public static final Stream<String> stream( final CharSequence input, final char separator )
1930    {
1931        return stream( input, (int) separator );
1932    }   //  stream()
1933
1934    /**
1935     *  <p>{@summary Splits a String by the given separator character, identified by its
1936     *  Unicode code point, and returns a
1937     *  {@link Stream}
1938     *  of all parts.} In case a separator character is immediately followed by
1939     *  another separator char, an empty String will be put to the
1940     *  {@code Stream}.</p>
1941     *  <p>Beginning and end of the String are treated as
1942     *  separators, so if the first character of the String is a separator, the
1943     *  returned {@code Stream} will start with an empty String, as it will end
1944     *  with an empty String if the last character is a separator.</p>
1945     *  <p>In case the String is empty, the return value will be a
1946     *  {@code Stream} containing just the empty String. It will not be
1947     *  empty.</p>
1948     *
1949     *  @param  input  The String to split.
1950     *  @param  separator   The code point for the separator character.
1951     *  @return A {@code Stream} instance with the parts of the String.
1952     *
1953     *  @since 0.0.7
1954     */
1955    @API( status = STABLE, since = "0.0.7" )
1956    public static final Stream<String> stream( final CharSequence input, final int separator )
1957    {
1958        //---* Process the string *--------------------------------------------
1959        final var codepoints = requireNonNullArgument( input, "input" ).codePoints().toArray();
1960        final var builder = Stream.<String>builder();
1961        var begin = -1;
1962        for( var i = 0 ; i < codepoints.length; ++i )
1963        {
1964            if( begin == -1 )
1965            {
1966                begin = i;
1967            }
1968            if( codepoints [i] == separator )
1969            {
1970                builder.add( new String( codepoints, begin, i - begin ).intern() );
1971                begin = -1;
1972            }
1973        }
1974
1975        //---* Add the rest *--------------------------------------------------
1976        if( begin >= 0 )
1977        {
1978            builder.add( new String( codepoints, begin, codepoints.length - begin ).intern() );
1979        }
1980        if( (codepoints.length == 0) || (codepoints [codepoints.length - 1] == separator) )
1981        {
1982            builder.add( EMPTY_STRING );
1983        }
1984
1985        //---* Create the return value *---------------------------------------
1986        final var retValue = builder.build();
1987
1988        //---* Done *----------------------------------------------------------
1989        return retValue;
1990    }   //  stream()
1991
1992    /**
1993     *  <p>{@summary Splits a String by the given separator sequence and
1994     *  returns an instance of
1995     *  {@link Stream}
1996     *  containing all parts.} In case a separator sequence is immediately
1997     *  followed by another separator sequence, an empty String will be put to
1998     *  the {@code Stream}.</p>
1999     *  <p>Beginning and end of the String are treated as separators, so if the
2000     *  first part of the String equals the separator sequence, the returned
2001     *  {@code Stream} will start with an empty string, as it will end with an
2002     *  empty String if the last part would equal the separator sequence.</p>
2003     *  <p>In case the String is empty, the return value will be a
2004     *  {@code Stream} containing just the empty String. It will not be
2005     *  empty.</p>
2006     *
2007     *  @param  input   The String to split.
2008     *  @param  separator   The separator sequence.
2009     *  @return The parts of the String.
2010     *
2011     *  @since 0.0.7
2012     */
2013    @API( status = STABLE, since = "0.0.7" )
2014    public static final Stream<String> stream( final CharSequence input, final CharSequence separator )
2015    {
2016        //---* Process the string *--------------------------------------------
2017        var s = requireNonNullArgument( input, "input" ).toString();
2018        final var t = requireNotEmptyArgument( separator, "separator" ).toString();
2019
2020        final var builder = Stream.<String>builder();
2021        var pos = Integer.MAX_VALUE;
2022        while( isNotEmpty( s ) && (pos >= 0) )
2023        {
2024            pos = s.indexOf( t );
2025            switch( Integer.signum( pos ) )
2026            {
2027                case 0 -> /* String starts with separator */
2028                    {
2029                        builder.add( EMPTY_STRING );
2030                        s = s.substring( t.length() );
2031                    }
2032                case 1 -> /* String contains a separator somewhere */
2033                    {
2034                        builder.add( s.substring( 0, pos ) );
2035                        s = s.substring( pos + t.length() );
2036                    }
2037                default -> { /* Just leave the loop */ }
2038            }   //  ResultHandlerSwitch:
2039        }
2040
2041        //---* Add the rest *--------------------------------------------------
2042        builder.add( s );
2043
2044        //---* Create the return value *---------------------------------------
2045        final var retValue = builder.build();
2046
2047        //---* Done *----------------------------------------------------------
2048        return retValue;
2049    }   //  stream()
2050
2051    /**
2052     *  <p>{@summary Splits a String using the given regular expression and
2053     *  returns an instance of
2054     *  {@link Stream}
2055     *  providing all parts.} In case a separator sequence is immediately
2056     *  followed by another separator sequence, an empty String will be put to
2057     *  the {@code Stream}.</p>
2058     *  <p>Beginning and end of the String are treated as separators, so if the
2059     *  first part of the String equals the separator sequence, the returned
2060     *  {@code Stream} will start with an empty string, as it will end with an
2061     *  empty String if the last part would equal the separator sequence.</p>
2062     *  <p>In case the String is empty, the return value will be a
2063     *  {@code Stream} containing just the empty String. It will not be
2064     *  empty.</p>
2065     *
2066     *  @note This method behaves different from
2067     *      {@link String#split(String)}
2068     *      as it will return trailing empty Strings.
2069     *
2070     *  @param  input  The String to split.
2071     *  @param  pattern The separator sequence.
2072     *  @return The parts of the String.
2073     *
2074     *  @see String#split(String)
2075     *  @see Pattern#split(CharSequence)
2076     *
2077     *  @since 0.0.7
2078     */
2079    @API( status = STABLE, since = "0.0.7" )
2080    public static final Stream<String> stream( final CharSequence input, final Pattern pattern )
2081    {
2082        requireNonNullArgument( pattern, "pattern" );
2083
2084        //---* Process the string *--------------------------------------------
2085        final var builder = Stream.<String>builder();
2086        if( isEmpty( requireNonNullArgument( input, "s" ) ) )
2087        {
2088            builder.add( EMPTY_STRING );
2089        }
2090        else
2091        {
2092            final var parts = pattern.split( input );
2093            for( final var part : parts )
2094            {
2095                builder.add( part );
2096            }
2097            final var matcher = pattern.matcher( input );
2098            var count = 0;
2099            while( matcher.find() ) ++count;
2100            //noinspection ForLoopWithMissingComponent
2101            for( ; count >= parts.length; --count )
2102            {
2103                builder.add( EMPTY_STRING );
2104            }
2105        }
2106
2107        //---* Create the return value *---------------------------------------
2108        final var retValue = builder.build();
2109
2110        //---* Done *----------------------------------------------------------
2111        return retValue;
2112    }   //  stream()
2113
2114    /**
2115     *  <p>{@summary Strips HTML or XML tags from the given String, without
2116     *  touching other entities (like {@code &amp;} or {@code &nbsp;}).} The
2117     *  result would be the effective text, stripped from all other whitespace
2118     *  (except single blanks).</p>
2119     *  <p>This means that the result for</p>
2120     *  <div class="source-container"><pre>stripTags( &quot;&quot;&quot;
2121     *    &lt;html&gt;
2122     *      &lt;head&gt;
2123     *        &hellip;
2124     *      &lt;/head&gt;
2125     *      &lt;body&gt;
2126     *        &lt;a href='&hellip;'&gt;       Simple          &lt;br&gt;
2127     *          &lt;br&gt;           Text       &lt;/a&gt;
2128     *      &lt;/body&gt;
2129     *    &lt;/html&gt;&quot;&quot;&quot; )</pre></div> would be just
2130     *  <p>&quot;{@code Simple Text}&quot;.</p>
2131     *  <p>Comments will be stripped as well, and {@code <pre>} tags are not
2132     *  interpreted, with the consequence that any formatting with whitespace
2133     *  gets lost. {@code CDATA} elements are stripped, too.</p>
2134     *
2135     *  @param  input   The HTML/XML string.
2136     *  @return The string without the tags.
2137     *
2138     *  @since 0.0.7
2139     */
2140    @API( status = STABLE, since = "0.0.5" )
2141    public static final String stripTags( final CharSequence input )
2142    {
2143        final var retValue = new StringBuilder();
2144        if( isNotEmptyOrBlank( requireNonNullArgument( input, "input" ) ) )
2145        {
2146            final var matcher = m_TagRemovalPattern.matcher( input );
2147            final var buffer = matcher.replaceAll( " " ).trim().codePoints().toArray();
2148            int lastChar = NULL_CHAR;
2149            ScanLoop: for( final var codePoint : buffer )
2150            {
2151                if( isWhitespace( codePoint ) )
2152                {
2153                    //---* Consecutive whitespace detected *-------------------
2154                    if( isWhitespace( lastChar ) ) continue ScanLoop;
2155
2156                    //---* All resulting whitespace have to be blanks *--------
2157                    retValue.append( " " );
2158                }
2159                else
2160                {
2161                    //---* Write the character *-------------------------------
2162                    retValue.append( toChars( codePoint ) );
2163                }
2164                lastChar = codePoint;
2165            }   //  ScanLoop:
2166        }
2167
2168        //---* Done *----------------------------------------------------------
2169        return retValue.toString();
2170    }   //  stripTags()
2171
2172    /**
2173     *  <p>{@summary Strips characters from the given input that are not
2174     *  allowed (or should be at least avoided) for a file or folder name on
2175     *  most or all operating systems.}</p>
2176     *  <p>The following characters will be stripped:</p>
2177     *  <dl>
2178     *  <dt><b>:</b> (colon)</dt><dd>On Windows systems it is used to separate
2179     *  the drive letter from the path and file name; on Unix-like operating
2180     *  systems (including MacOS) it would be valid, but it can cause issues on
2181     *  the {@code PATH} and {@code CLASSPATH} variables on these operating
2182     *  systems.</dd>
2183     *  <dt><b>\</b> (backslash)</dt><dd>On Windows systems it is used as the
2184     *  path separator, while on Unix-like operating systems it is problematic
2185     *  in other ways. For example, it is used to escape blanks in not-quoted
2186     *  file or folder names.</dd>
2187     *  <dt><b>/</b> (slash or forward slash)</dt><dd>The path separator on
2188     *  Unix-like operating systems, but Java will use it that way on Windows
2189     *  systems, too.</dd>
2190     *  <dt><b>;</b> (semicolon)</dt><dd>It can cause issues on the {@code PATH}
2191     *  and {@code CLASSPATH} variables on Windows.</dd>
2192     *  <dt><b>*</b> (asterisk)</dt><dd>The asterisk is often used as wild card
2193     *  character in shell programs to find groups of files; using it in a file
2194     *  name can cause funny effects.</dd>
2195     *  <dt><b>?</b> (question mark)</dt><dd>The question mark is used on
2196     *  Windows as a wild card for a single character; similar to the asterisk,
2197     *  it can cause funny effects when used in a file name.</dd>
2198     *  <dt><b>&quot;</b> (double quotes)</dt>
2199     *  <dt><b>'</b> (single quotes)</dt><dd>Both have some potential to
2200     *  confuse the various shell programs of all operating systems.</dd>
2201     *  <dt><b>@</b> ('at'-sign)</dt><dd>Although it is allowed for file and
2202     *  folder names, it causes issues when used in the URL for that respective
2203     *  file.</dd>
2204     *  <dt><b>|</b> (pipe symbol)</dt><dd>Similar to the '*' (asterisk), the
2205     *  pipe-symbol has – as the name already indicates - a meaning on most
2206     *  shells that would make it difficult to manage files that contains this
2207     *  character in their names.</dd>
2208     *  <dt><b>&lt;</b> (less than)</dt>
2209     *  <dt><b>&gt;</b> (greater than)</dt><dd>Like the pipe, these two have a
2210     *  meaning on most shells that would make it difficult to manage files
2211     *  that contains one of these characters in their names.</dd>
2212     *  <dt>Whitespace</dt><dd>Only blanks will remain, any other whitespace
2213     *  characters are stripped.</dd>
2214     *  </dl>
2215     *  <p>Finally, the method will strip all leading and trailing blanks;
2216     *  although blanks are usually allowed, they are confusing when not
2217     *  surrounded by some visible characters.</p>
2218     *  <p>Especially regarding the characters that are critical for shells
2219     *  ('*', '?', '&quot;', ''', '|', '&lt;', and '&gt;') this method is
2220     *  over-cautious, as most shells could handle them after proper escaping
2221     *  the offending characters or quoting the file name.</p>
2222     *  <p>This method furthermore assumes that any other Unicode character is
2223     *  valid for a file or folder name; unfortunately, there are filesystems
2224     *  where this is not true.</p>
2225     *
2226     *  @note   This method will not take care about the length of the returned
2227     *      String; this means the result to a call to this method may still be
2228     *      invalid as a file or folder name because it is too long.
2229     *
2230     *  @param  input   The input String, denoting a file or folder name -
2231     *      <i>not</i> a full path.
2232     *  @return The String without the characters that are invalid for a file
2233     *      name. This value will never be {@code null} or empty.
2234     *  @throws NullArgumentException   The input is {@code null}.
2235     *  @throws EmptyArgumentException  The input is the empty String.
2236     *  @throws ValidationException After stripping the invalid characters the
2237     *      return value would be empty.
2238     *
2239     *  @since 0.0.5
2240     */
2241    @SuppressWarnings( "SwitchStatementWithTooManyBranches" )
2242    @API( status = STABLE, since = "0.0.5" )
2243    public static final String stripToFilename( final CharSequence input ) throws ValidationException
2244    {
2245        final var len = requireNotEmptyArgument( input, "input" ).length();
2246        final var buffer = new StringBuilder( len );
2247        ScanLoop: for( var i = 0; i < len; ++i )
2248        {
2249            final var currentCharacter = input.charAt( i );
2250            Selector:
2251            //noinspection SwitchStatementWithTooManyBranches,EnhancedSwitchMigration
2252            switch( currentCharacter )
2253            {
2254                case ':':
2255                case '\\':
2256                case '/':
2257                case ';':
2258                case '*':
2259                case '"':
2260                case '\'':
2261                case '@':
2262                case '|':
2263                case '?':
2264                case '<':
2265                case '>':
2266                    continue ScanLoop;
2267
2268                default:
2269                {
2270                    if( (currentCharacter == ' ') || (!isISOControl( currentCharacter ) && !isWhitespace( currentCharacter )) )
2271                    {
2272                        buffer.append( currentCharacter );
2273                    }
2274                    break Selector;
2275                }
2276            }   //  Selector:
2277        }   //  ScanLoop:
2278
2279        final var retValue = buffer.toString().trim();
2280        if( retValue.isEmpty() )
2281        {
2282            throw new ValidationException( "After stripping the invalid characters from '%1$s' there do not remain enough characters for a valid file name".formatted(  input.toString() ) );
2283        }
2284
2285        //---* Done *----------------------------------------------------------
2286        return retValue;
2287    }   //  stripToFilename()
2288
2289    /**
2290     *  Strips HTML or XML comments from the given String.
2291     *
2292     *  @param  input   The HTML/XML string.
2293     *  @return The string without the comments.
2294     *
2295     *  @since 0.0.5
2296     */
2297    @API( status = STABLE, since = "0.0.5" )
2298    public static final String stripXMLComments( final CharSequence input )
2299    {
2300        final var matcher = m_CommentRemovalPattern.matcher( requireNonNullArgument( input, "input" ) );
2301        final var retValue = matcher.replaceAll( EMPTY_STRING );
2302
2303        //---* Done *----------------------------------------------------------
2304        return retValue;
2305    }   //  stripXMLComments()
2306
2307    /**
2308     *  <p>{@summary Gets the String that is nested in between two Strings.}
2309     *  Only the first match is returned.</p>
2310     *  <p>A {@code null} input String returns {@code null}. A {@code null}
2311     *  open/close returns {@code null} (no match). An empty (&quot;&quot;)
2312     *  open and close returns an empty string.</p>
2313     *  <pre><code>
2314     *  substringBetween( "wx[b]yz", "[", "]" )    = "b"
2315     *  substringBetween( null, *, * )             = Optional.empty()
2316     *  substringBetween( *, null, * )             = Optional.empty()
2317     *  substringBetween( *, *, null )             = Optional.empty()
2318     *  substringBetween( "", "", "" )             = ""
2319     *  substringBetween( "", "", "]" )            = Optional.empty()
2320     *  substringBetween( "", "[", "]" )           = Optional.empty()
2321     *  substringBetween( "yabcz", "", "" )        = ""
2322     *  substringBetween( "yabcz", "y", "z" )      = "abc"
2323     *  substringBetween( "yabczyabcz", "y", "z" ) = "abc"
2324     *  </code></pre>
2325     *
2326     *  @inspired Apache Commons Lang
2327     *
2328     *  @param  input   The String containing the substring, may be
2329     *      {@code null}.
2330     *  @param  open    The String before the substring, may be {@code null}.
2331     *  @param  close   The String after the substring, may be {@code null}.
2332     *  @return An instance of
2333     *      {@link Optional}
2334     *      that holds the found substring; will be
2335     *      {@linkplain Optional#empty() empty} if no match
2336     *
2337     *  @since 0.4.8
2338     */
2339    @API( status = STABLE, since = "0.4.8" )
2340    public static final Optional<String> substringBetween( final String input, final String open, final String close )
2341    {
2342        String found = null;
2343
2344        if( Stream.of( input, open, close ).allMatch( Objects::nonNull ) )
2345        {
2346            if( open.isEmpty() && close.isEmpty() )
2347            {
2348                found = EMPTY_STRING;
2349            }
2350            else
2351            {
2352                final var start = input.indexOf(open);
2353                if( start >= 0 )
2354                {
2355                    final var end = input.indexOf( close, start + open.length() );
2356                    if( end > 0 )
2357                    {
2358                        found = input.substring( start + open.length(), end );
2359                    }
2360                }
2361            }
2362        }
2363        final var retValue = Optional.ofNullable( found );
2364
2365        //---* Done *----------------------------------------------------------
2366        return retValue;
2367    }   //  substringBetween()
2368
2369    /**
2370     *  <p>{@summary Searches a String for substrings delimited by a start and
2371     *  end tag, returning all matching substrings in a
2372     *  {@link java.util.SequencedCollection Collection}.} That collection is
2373     *  empty if no match was found.</p>
2374     *  <p>No match can be found in a {@code null} input String; same for a
2375     *  {@code null} or an empty (&quot;&quot;) open or close.</p>
2376     *  <pre><code>
2377     *  substringsBetween( "[a][b][c]", "[", "]" ) = ["a","b","c"]
2378     *  substringsBetween( null, *, * )            = []
2379     *  substringsBetween( *, null, * )            = []
2380     *  substringsBetween( *, *, null )            = []
2381     *  substringsBetween( "", "[", "]" )          = []
2382     *  </code></pre>
2383     *
2384     *  @param  input   The String containing the substrings, may be
2385     *      {@code null}.
2386     *  @param  open    The String identifying the start of the substring, may
2387     *      be {@code null}.
2388     *  @param  close   The String identifying the end of the substring, may be
2389     *      {@code null}.
2390     *  @return A
2391     *      {@link SequencedCollection Collection}
2392     *      with the found substrings, in the sequence they have in the input
2393     *      String. The collection is mutable.
2394     *
2395     *  @since 0.4.8
2396     */
2397    @API( status = STABLE, since = "0.4.8" )
2398    public static final SequencedCollection<String> substringsBetween( final String input, final String open, final String close)
2399    {
2400        final SequencedCollection<String> retValue = new ArrayList<>();
2401
2402        if( Stream.of( input, open, close ).allMatch( StringUtils::isNotEmpty ) )
2403        {
2404            final var strLen = input.length();
2405            final var closeLen = close.length();
2406            final var openLen = open.length();
2407            var pos = 0;
2408            ScanLoop: while( pos < strLen - closeLen )
2409            {
2410                var start = input.indexOf( open, pos );
2411                if( start < 0 ) break ScanLoop;
2412                start += openLen;
2413                final var end = input.indexOf( close, start );
2414                if (end < 0) break ScanLoop;
2415                retValue.add( input.substring( start, end ) );
2416                pos = end + closeLen;
2417            }   //  ScanLoop:
2418        }
2419
2420        //---* Done *----------------------------------------------------------
2421        return retValue;
2422    }   //  substringsBetween()
2423
2424    /**
2425     *  Unescapes a string containing entity escapes to a string containing the
2426     *  actual Unicode characters corresponding to the escapes. Supports HTML
2427     *  5.0 entities.<br>
2428     *  <br>For example, the string
2429     *  &quot;&amp;lt;Fran&amp;ccedil;ais&amp;gt;&quot; will become
2430     *  &quot;&lt;Fran&ccedil;ais&gt;&quot;.<br>
2431     *  <br>If an entity is unrecognised, it is left alone, and inserted
2432     *  verbatim into the result string. e.g. &quot;&amp;gt;&amp;zzzz;x&quot;
2433     *  will become &quot;&gt;&amp;zzzz;x&quot;.
2434     *
2435     *  @param  input   The {@code String} to unescape, may be {@code null}.
2436     *  @return A new unescaped {@code String}, {@code null} if the given
2437     *      string was already {@code null}.
2438     *
2439     *  @see #escapeHTML(CharSequence)
2440     *  @see #escapeHTML(Appendable,CharSequence)
2441     *
2442     *  @since 0.0.5
2443     */
2444    @API( status = STABLE, since = "0.0.5" )
2445    public static final String unescapeHTML( final CharSequence input )
2446    {
2447        final var retValue = nonNull( input ) ? HTML50.unescape( input ) : null;
2448
2449        //---* Done *----------------------------------------------------------
2450        return retValue;
2451    }   //  unescapeHTML()
2452
2453    /**
2454     *  Unescapes a string containing entity escapes to a string containing the
2455     *  actual Unicode characters corresponding to the escapes and writes it to
2456     *  the given
2457     *  {@link Appendable}.
2458     *  Supports HTML 4.0 entities.<br>
2459     *  <br>For example, the string
2460     *  &quot;&amp;lt;Fran&amp;ccedil;ais&amp;gt;&quot; will become
2461     *  &quot;&lt;Fran&ccedil;ais&gt;&quot;.<br>
2462     *  <br>If an entity is unrecognised, it is left alone, and inserted
2463     *  verbatim into the result string. e.g. &quot;&amp;gt;&amp;zzzz;x&quot;
2464     *  will become &quot;&gt;&amp;zzzz;x&quot;.
2465     *
2466     *  @param  appendable  The appendable receiving the unescaped string.
2467     *  @param  input   The {@code String} to unescape, may be {@code null}.
2468     *  @throws NullArgumentException   The appendable is {@code null}.
2469     *  @throws IOException An IOException occurred.
2470     *
2471     *  @see #escapeHTML(CharSequence)
2472     *
2473     *  @since 0.0.5
2474     */
2475    @API( status = STABLE, since = "0.0.5" )
2476    public static final void unescapeHTML( final Appendable appendable, final CharSequence input ) throws IOException
2477    {
2478        requireNonNullArgument( appendable, "appendable" );
2479
2480        if( nonNull( input ) ) HTML50.unescape( appendable, input );
2481    }   //  unescapeHTML()
2482
2483    /**
2484     *  <p>{@summary Unescapes an XML string containing XML entity escapes to a
2485     *  string containing the actual Unicode characters corresponding to the
2486     *  escapes.}</p>
2487     *  <p>If an entity is unrecognised, it is left alone, and inserted
2488     *  verbatim into the result string. e.g. &quot;&amp;gt;&amp;zzzz;x&quot;
2489     *  will become &quot;&gt;&amp;zzzz;x&quot;.</p>
2490     *
2491     *  @param  input   The {@code String} to unescape, may be {@code null}.
2492     *  @return A new unescaped {@code String}, {@code null} if the given
2493     *      string was already {@code null}.
2494     *
2495     *  @see #escapeXML(CharSequence)
2496     *  @see #escapeXML(Appendable,CharSequence)
2497     *
2498     *  @since 0.0.5
2499     */
2500    @API( status = STABLE, since = "0.0.5" )
2501    public static final String unescapeXML( final CharSequence input )
2502    {
2503        final var retValue = nonNull( input ) ? XML.unescape( input ) : null;
2504
2505        //---* Done *----------------------------------------------------------
2506        return retValue;
2507    }   //  unescapeXML()
2508
2509    /**
2510     *  <p>{@summary Unescapes an XML String containing XML entity escapes to a
2511     *  String containing the actual Unicode characters corresponding to the
2512     *  escapes and writes it to the given
2513     *  {@link Appendable}.}</p>
2514     *  <p>If an entity is unrecognised, it is left alone, and inserted
2515     *  verbatim into the result string. e.g. &quot;&amp;gt;&amp;zzzz;x&quot;
2516     *  will become &quot;&gt;&amp;zzzz;x&quot;.</p>
2517     *
2518     *  @param  appendable  The appendable receiving the unescaped string.
2519     *  @param  input   The {@code String} to unescape, may be {@code null}.
2520     *  @throws NullArgumentException   The writer is {@code null}.
2521     *  @throws IOException An IOException occurred.
2522     *
2523     *  @see #escapeXML(CharSequence)
2524     *
2525     *  @since 0.0.5
2526     */
2527    @API( status = STABLE, since = "0.0.5" )
2528    public static final void unescapeXML( final Appendable appendable, final CharSequence input ) throws IOException
2529    {
2530        requireNonNullArgument( appendable, "appendable" );
2531
2532        if( nonNull( input ) ) XML.unescape( appendable, input );
2533    }   //  unescapeXML()
2534
2535    /**
2536     *  Returns the given URL encoded String in its decoded form, using the
2537     *  UTF-8 character encoding.<br>
2538     *  <br>Internally, this method and
2539     *  {@link #urlEncode(CharSequence)}
2540     *  make use of the methods from
2541     *  {@link java.net.URLDecoder}
2542     *  and
2543     *  {@link java.net.URLEncoder}, respectively. The methods here were
2544     *  introduced to simplify the handling, as first only the UTF-8 encoding
2545     *  should be used - making the second argument for the methods
2546     *  {@link java.net.URLDecoder#decode(String, String) decode()}/
2547     *  {@link java.net.URLEncoder#encode(String, String) encode()}
2548     *  obsolete - and second, they could throw an
2549     *  {@link UnsupportedEncodingException} - although this should never occur
2550     *  when UTF-8 encoding is used.
2551     *
2552     *  @param  input   The input String.
2553     *  @return The decoded result.
2554     *
2555     *  @see java.net.URLDecoder#decode(String, String)
2556     *
2557     *  @since 0.0.5
2558     */
2559    @API( status = STABLE, since = "0.0.5" )
2560    public static final String urlDecode( final CharSequence input )
2561    {
2562        final var retValue = decode( requireNonNullArgument( input, "input" ).toString(), UTF8 );
2563
2564        //---* Done *----------------------------------------------------------
2565        return retValue;
2566    }   //  urlDecode()
2567
2568    /**
2569     *  Returns the given String in its URL encoded form, using the
2570     *  UTF-8 character encoding.
2571     *
2572     *  @param  input   The input String.
2573     *  @return The URL encoded result.
2574     *
2575     *  @see java.net.URLEncoder#encode(String, String)
2576     *  @see #urlDecode(CharSequence)
2577     *
2578     *  @since 0.0.5
2579     */
2580    @API( status = STABLE, since = "0.0.5" )
2581    public static final String urlEncode( final CharSequence input )
2582    {
2583        final var retValue = encode( requireNonNullArgument( input, "input" ).toString(), UTF8 );
2584
2585        //---* Done *----------------------------------------------------------
2586        return retValue;
2587    }   //  urlEncode()
2588}
2589//  class StringUtils
2590
2591/*
2592 *    End of File
2593 */