• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

amaembo / streamex / #683

09 Aug 2025 05:15PM UTC coverage: 99.725% (+0.05%) from 99.673%
#683

push

amaembo
Update workflow versions in test.yml

5804 of 5820 relevant lines covered (99.73%)

1.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.89
/src/main/java/one/util/streamex/Joining.java
1
/*
2
 * Copyright 2015, 2024 StreamEx contributors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16
package one.util.streamex;
17

18
import org.jspecify.annotations.NullMarked;
19

20
import java.text.BreakIterator;
21
import java.util.ArrayList;
22
import java.util.Collections;
23
import java.util.List;
24
import java.util.Set;
25
import java.util.function.*;
26
import java.util.stream.Collector;
27
import java.util.stream.Collectors;
28

29
import static one.util.streamex.Internals.alwaysTrue;
30
import static one.util.streamex.Internals.checkNonNegative;
31

32
/**
33
 * An advanced implementation of joining {@link Collector}. This collector is
34
 * capable to join the input {@code CharSequence} elements with given delimiter
35
 * optionally wrapping into given prefix and suffix and optionally limiting the
36
 * length of the resulting string (in Unicode code units, code points or
37
 * grapheme clusters) adding the specified ellipsis sequence. This collector
38
 * supersedes the standard JDK
39
 * {@link Collectors#joining(CharSequence, CharSequence, CharSequence)}
40
 * collectors family.
41
 * 
42
 * <p>
43
 * This collector is <a
44
 * href="package-summary.html#ShortCircuitReduction">short-circuiting</a> when
45
 * the string length is limited in either of ways. Otherwise it's not
46
 * short-circuiting.
47
 * 
48
 * <p>
49
 * Every specific collector represented by this class is immutable, so you can
50
 * share it. A bunch of methods is provided to create a new collector based on
51
 * this one.
52
 * 
53
 * <p>
54
 * To create {@code Joining} collector use {@link #with(CharSequence)} static
55
 * method and specify the delimiter. For further setup use specific instance
56
 * methods which return new {@code Joining} objects like this:
57
 * 
58
 * <pre>{@code
59
 * StreamEx.of(source).collect(Joining.with(", ").wrap("[", "]")
60
 *         .maxCodePoints(100).cutAtWord());
61
 * }</pre>
62
 * 
63
 * <p>
64
 * The intermediate accumulation type of this collector is the implementation
65
 * detail and not exposed to the API. If you want to cast it to
66
 * {@code Collector} type, use ? as accumulator type variable:
67
 * 
68
 * <pre>{@code
69
 * Collector<CharSequence, ?, String> joining = Joining.with(", ");
70
 * }</pre>
71
 * 
72
 * @author Tagir Valeev
73
 * @since 0.4.1
74
 */
75
@NullMarked
76
public class Joining extends CancellableCollector<CharSequence, Joining.Accumulator, String> {
77
    static final class Accumulator {
1✔
78
        final List<CharSequence> data = new ArrayList<>();
1✔
79
        int chars = 0, count = 0;
1✔
80
    }
81

82
    private static final int CUT_ANYWHERE = 0;
83
    private static final int CUT_CODEPOINT = 1;
84
    private static final int CUT_GRAPHEME = 2;
85
    private static final int CUT_WORD = 3;
86
    private static final int CUT_BEFORE_DELIMITER = 4;
87
    private static final int CUT_AFTER_DELIMITER = 5;
88

89
    private static final int LENGTH_CHARS = 0;
90
    private static final int LENGTH_CODEPOINTS = 1;
91
    private static final int LENGTH_GRAPHEMES = 2;
92
    private static final int LENGTH_ELEMENTS = 3;
93

94
    private final String delimiter, ellipsis, prefix, suffix;
95
    private final int cutStrategy, lenStrategy, maxLength;
96
    private int limit, delimCount = -1;
1✔
97

98
    private Joining(String delimiter, String ellipsis, String prefix, String suffix, int cutStrategy, int lenStrategy,
99
            int maxLength) {
1✔
100
        this.delimiter = delimiter;
1✔
101
        this.ellipsis = ellipsis;
1✔
102
        this.prefix = prefix;
1✔
103
        this.suffix = suffix;
1✔
104
        this.cutStrategy = cutStrategy;
1✔
105
        this.lenStrategy = lenStrategy;
1✔
106
        this.maxLength = maxLength;
1✔
107
    }
1✔
108

109
    private void init() {
110
        if (delimCount == -1) {
1✔
111
            limit = maxLength - length(prefix, false) - length(suffix, false);
1✔
112
            delimCount = length(delimiter, false);
1✔
113
        }
114
    }
1✔
115

116
    private int length(CharSequence s, boolean content) {
117
        switch (lenStrategy) {
1✔
118
            case LENGTH_CHARS:
119
                return s.length();
1✔
120
            case LENGTH_CODEPOINTS:
121
                if (s instanceof String)
1✔
122
                    return ((String) s).codePointCount(0, s.length());
1✔
123
                return (int) s.codePoints().count();
1✔
124
            case LENGTH_GRAPHEMES:
125
                BreakIterator bi = BreakIterator.getCharacterInstance();
1✔
126
                bi.setText(s.toString());
1✔
127
                int count = 0;
1✔
128
                for (int end = bi.next(); end != BreakIterator.DONE; end = bi.next())
1✔
129
                    count++;
1✔
130
                return count;
1✔
131
            case LENGTH_ELEMENTS:
132
                return content ? 1 : 0;
1✔
133
            default:
134
                throw new InternalError();
×
135
        }
136
    }
137

138
    private static int copy(char[] buf, int pos, String str) {
139
        str.getChars(0, str.length(), buf, pos);
1✔
140
        return pos + str.length();
1✔
141
    }
142

143
    private int copyCut(char[] buf, int pos, String str, int limit, int cutStrategy) {
144
        if (limit <= 0)
1✔
145
            return pos;
1✔
146
        int endPos = str.length();
1✔
147
        switch (lenStrategy) {
1✔
148
            case LENGTH_CHARS:
149
                if (limit < str.length())
1✔
150
                    endPos = limit;
1✔
151
                break;
152
            case LENGTH_CODEPOINTS:
153
                if (limit < str.codePointCount(0, str.length()))
1✔
154
                    endPos = str.offsetByCodePoints(0, limit);
1✔
155
                break;
156
            case LENGTH_GRAPHEMES:
157
                BreakIterator bi = BreakIterator.getCharacterInstance();
1✔
158
                bi.setText(str);
1✔
159
                int count = limit, end;
1✔
160
                while (true) {
161
                    end = bi.next();
1✔
162
                    if (end == BreakIterator.DONE)
1✔
163
                        break;
1✔
164
                    if (--count == 0) {
1✔
165
                        endPos = end;
1✔
166
                        break;
1✔
167
                    }
168
                }
169
                break;
170
            case LENGTH_ELEMENTS:
171
                break;
×
172
            default:
173
                throw new InternalError();
×
174
        }
175
        if (endPos < str.length()) {
1✔
176
            BreakIterator bi;
177
            switch (cutStrategy) {
1✔
178
            case CUT_BEFORE_DELIMITER:
179
            case CUT_AFTER_DELIMITER:
180
                endPos = 0;
1✔
181
                break;
1✔
182
            case CUT_WORD:
183
                bi = BreakIterator.getWordInstance();
1✔
184
                bi.setText(str);
1✔
185
                endPos = bi.preceding(endPos + 1);
1✔
186
                break;
1✔
187
            case CUT_GRAPHEME:
188
                bi = BreakIterator.getCharacterInstance();
1✔
189
                bi.setText(str);
1✔
190
                endPos = bi.preceding(endPos + 1);
1✔
191
                break;
1✔
192
            case CUT_ANYWHERE:
193
                break;
1✔
194
            case CUT_CODEPOINT:
195
                if (Character.isHighSurrogate(str.charAt(endPos - 1)) && Character.isLowSurrogate(str.charAt(endPos)))
1✔
196
                    endPos--;
1✔
197
                break;
198
            default:
199
                throw new InternalError();
×
200
            }
201
        }
202
        str.getChars(0, endPos, buf, pos);
1✔
203
        return pos + endPos;
1✔
204
    }
205

206
    private String finisherNoOverflow(Accumulator acc) {
207
        char[] buf = new char[acc.chars + prefix.length() + suffix.length()];
1✔
208
        int size = acc.data.size();
1✔
209
        int pos = copy(buf, 0, prefix);
1✔
210
        for (int i = 0; i < size; i++) {
1✔
211
            if (i > 0) {
1✔
212
                pos = copy(buf, pos, delimiter);
1✔
213
            }
214
            pos = copy(buf, pos, acc.data.get(i).toString());
1✔
215
        }
216
        copy(buf, pos, suffix);
1✔
217
        return new String(buf);
1✔
218
    }
219

220
    private Joining withLimit(int lenStrategy, int maxLength) {
221
        checkNonNegative("Length", maxLength);
1✔
222
        return new Joining(delimiter, ellipsis, prefix, suffix, cutStrategy, lenStrategy, maxLength);
1✔
223
    }
224

225
    private Joining withCut(int cutStrategy) {
226
        return new Joining(delimiter, ellipsis, prefix, suffix, cutStrategy, lenStrategy, maxLength);
1✔
227
    }
228

229
    /**
230
     * Returns a {@code Collector} that concatenates the input elements,
231
     * separated by the specified delimiter, in encounter order.
232
     * 
233
     * <p>
234
     * This collector is similar to {@link Collectors#joining(CharSequence)},
235
     * but can be further set up in a flexible way, for example, specifying the
236
     * maximal allowed length of the resulting {@code String}.
237
     *
238
     * @param delimiter the delimiter to be used between each element
239
     * @return A {@code Collector} which concatenates CharSequence elements,
240
     *         separated by the specified delimiter, in encounter order
241
     * @see Collectors#joining(CharSequence)
242
     */
243
    public static Joining with(CharSequence delimiter) {
244
        return new Joining(delimiter.toString(), "...", "", "", CUT_GRAPHEME, LENGTH_CHARS, -1);
1✔
245
    }
246

247
    /**
248
     * Returns a {@code Collector} which behaves like this collector, but
249
     * additionally wraps the result with the specified prefix and suffix.
250
     * 
251
     * <p>
252
     * The collector returned by
253
     * {@code Joining.with(delimiter).wrap(prefix, suffix)} is equivalent to
254
     * {@link Collectors#joining(CharSequence, CharSequence, CharSequence)}, but
255
     * can be further set up in a flexible way, for example, specifying the
256
     * maximal allowed length of the resulting {@code String}.
257
     * 
258
     * <p>
259
     * If length limit is specified for the collector, the prefix length and the
260
     * suffix length are also counted towards this limit. If the length of the
261
     * prefix and the suffix exceed the limit, the resulting collector will not
262
     * accumulate any elements and produce the same output. For example,
263
     * {@code stream.collect(Joining.with(",").wrap("prefix", "suffix").maxChars(9))}
264
     * will produce {@code "prefixsuf"} string regardless of the input stream
265
     * content.
266
     * 
267
     * <p>
268
     * You may wrap several times:
269
     * {@code Joining.with(",").wrap("[", "]").wrap("(", ")")} is equivalent to
270
     * {@code Joining.with(",").wrap("([", "])")}.
271
     * 
272
     * @param prefix the sequence of characters to be used at the beginning of
273
     *        the joined result
274
     * @param suffix the sequence of characters to be used at the end of the
275
     *        joined result
276
     * @return a new {@code Collector} which wraps the result with the specified
277
     *         prefix and suffix.
278
     */
279
    public Joining wrap(CharSequence prefix, CharSequence suffix) {
280
        return new Joining(delimiter, ellipsis, prefix.toString().concat(this.prefix), this.suffix.concat(suffix
1✔
281
                .toString()), cutStrategy, lenStrategy, maxLength);
1✔
282
    }
283

284
    /**
285
     * Returns a {@code Collector} which behaves like this collector, but uses
286
     * the specified ellipsis {@code CharSequence} instead of default
287
     * {@code "..."} when the string limit (if specified) is reached.
288
     * 
289
     * @param ellipsis the sequence of characters to be used at the end of the
290
     *        joined result to designate that not all of the input elements are
291
     *        joined due to the specified string length restriction.
292
     * @return a new {@code Collector} which will use the specified ellipsis
293
     *         instead of current setting.
294
     */
295
    public Joining ellipsis(CharSequence ellipsis) {
296
        return new Joining(delimiter, ellipsis.toString(), prefix, suffix, cutStrategy, lenStrategy, maxLength);
1✔
297
    }
298

299
    /**
300
     * Returns a {@code Collector} which behaves like this collector, but sets
301
     * the maximal length of the resulting string to the specified number of
302
     * UTF-16 characters (or Unicode code units). This setting overwrites any
303
     * limit previously set by {@code maxChars(int)},
304
     * {@link #maxCodePoints(int)}, {@link #maxGraphemes(int)} or
305
     * {@link #maxElements(int)} call.
306
     * 
307
     * <p>
308
     * The {@code String} produced by the resulting collector is guaranteed to
309
     * have {@link String#length() length} which does not exceed the specified
310
     * limit. An ellipsis sequence (by default {@code "..."}) is used to
311
     * designate whether the limit was reached. Use
312
     * {@link #ellipsis(CharSequence)} to set custom ellipsis sequence.
313
     * 
314
     * <p>
315
     * The collector returned by this method is <a
316
     * href="package-summary.html#ShortCircuitReduction">short-circuiting</a>:
317
     * it may not process all the input elements if the limit is reached.
318
     * 
319
     * @param limit the maximal number of UTF-16 characters in the resulting
320
     *        String.
321
     * @return a new {@code Collector} which will produce String no longer than
322
     *         given limit.
323
     */
324
    public Joining maxChars(int limit) {
325
        return withLimit(LENGTH_CHARS, limit);
1✔
326
    }
327

328
    /**
329
     * Returns a {@code Collector} which behaves like this collector, but sets
330
     * the maximal number of Unicode code points of the resulting string. This
331
     * setting overwrites any limit previously set by {@link #maxChars(int)},
332
     * {@code maxCodePoints(int)}, {@link #maxGraphemes(int)} or
333
     * {@link #maxElements(int)} call.
334
     * 
335
     * <p>
336
     * The {@code String} produced by the resulting collector is guaranteed to
337
     * have no more code points than the specified limit. An ellipsis sequence
338
     * (by default {@code "..."}) is used to designate whether the limit was
339
     * reached. Use {@link #ellipsis(CharSequence)} to set custom ellipsis
340
     * sequence.
341
     * 
342
     * <p>
343
     * The collector returned by this method is <a
344
     * href="package-summary.html#ShortCircuitReduction">short-circuiting</a>:
345
     * it may not process all the input elements if the limit is reached.
346
     * 
347
     * @param limit the maximal number of code points in the resulting String.
348
     * @return a new {@code Collector} which will produce String no longer than
349
     *         given limit.
350
     */
351
    public Joining maxCodePoints(int limit) {
352
        return withLimit(LENGTH_CODEPOINTS, limit);
1✔
353
    }
354

355
    /**
356
     * Returns a {@code Collector} which behaves like this collector, but sets
357
     * the maximal number of grapheme clusters. This setting overwrites any
358
     * limit previously set by {@link #maxChars(int)}, {@link #maxCodePoints(int)},
359
     * {@code maxGraphemes(int)} or {@link #maxElements(int)} call.
360
     * 
361
     * <p>
362
     * The grapheme cluster is defined in <a
363
     * href="http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries"
364
     * >Unicode Text Segmentation</a> technical report. Basically, it counts
365
     * base character and the following combining characters as single object.
366
     * The {@code String} produced by the resulting collector is guaranteed to
367
     * have no more grapheme clusters than the specified limit. An ellipsis
368
     * sequence (by default {@code "..."}) is used to designate whether the
369
     * limit was reached. Use {@link #ellipsis(CharSequence)} to set custom
370
     * ellipsis sequence.
371
     * 
372
     * <p>
373
     * The collector returned by this method is <a
374
     * href="package-summary.html#ShortCircuitReduction">short-circuiting</a>:
375
     * it may not process all the input elements if the limit is reached.
376
     * 
377
     * @param limit the maximal number of grapheme clusters in the resulting
378
     *        String.
379
     * @return a new {@code Collector} which will produce String no longer than
380
     *         given limit.
381
     */
382
    public Joining maxGraphemes(int limit) {
383
        return withLimit(LENGTH_GRAPHEMES, limit);
1✔
384
    }
385

386
    /**
387
     * Returns a {@code Collector} which behaves like this collector, but sets
388
     * the maximal number of elements to join. This setting overwrites any limit
389
     * previously set by {@link #maxChars(int)}, {@link #maxCodePoints(int)} or
390
     * {@link #maxGraphemes(int)} or {@code maxElements(int)} call.
391
     * <p>
392
     * The {@code String} produced by the resulting collector is guaranteed to
393
     * have no more input elements than the specified limit. An ellipsis
394
     * sequence (by default {@code "..."}) is used to designate whether the
395
     * limit was reached. Use {@link #ellipsis(CharSequence)} to set custom
396
     * ellipsis sequence. The cutting strategy is mostly irrelevant for this
397
     * mode except {@link #cutBeforeDelimiter()}.
398
     * <p>
399
     * The collector returned by this method is
400
     * <a href="package-summary.html#ShortCircuitReduction">short-circuiting</a>:
401
     * it may not process all the input elements if the limit is reached.
402
     *
403
     * @param limit the maximal number of input elements in the resulting String.
404
     * @return a new {@code Collector} which will produce String no longer than
405
     *         given limit.
406
     * @since 0.6.7
407
     */
408
    public Joining maxElements(int limit) {
409
        return withLimit(LENGTH_ELEMENTS, limit);
1✔
410
    }
411

412
    /**
413
     * Returns a {@code Collector} which behaves like this collector, but cuts
414
     * the resulting string at any point when limit is reached.
415
     * 
416
     * <p>
417
     * The resulting collector will produce {@code String} which length is
418
     * exactly equal to the specified limit if the limit is reached. If used
419
     * with {@link #maxChars(int)}, the resulting string may be cut in the
420
     * middle of surrogate pair.
421
     * 
422
     * @return a new {@code Collector} which cuts the resulting string at any
423
     *         point when limit is reached.
424
     */
425
    public Joining cutAnywhere() {
426
        return withCut(CUT_ANYWHERE);
1✔
427
    }
428

429
    /**
430
     * Returns a {@code Collector} which behaves like this collector, but cuts
431
     * the resulting string between any code points when limit is reached.
432
     * 
433
     * <p>
434
     * The resulting collector will not split the surrogate pair when used with
435
     * {@link #maxChars(int)} or {@link #maxCodePoints(int)}. However it may
436
     * remove the combining character which may result in incorrect rendering of
437
     * the last displayed grapheme.
438
     * 
439
     * @return a new {@code Collector} which cuts the resulting string between
440
     *         code points.
441
     */
442
    public Joining cutAtCodePoint() {
443
        return withCut(CUT_CODEPOINT);
1✔
444
    }
445

446
    /**
447
     * Returns a {@code Collector} which behaves like this collector, but cuts
448
     * the resulting string at grapheme cluster boundary when limit is reached.
449
     * This is the default behavior.
450
     * 
451
     * <p>
452
     * The grapheme cluster is defined in <a
453
     * href="http://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries"
454
     * >Unicode Text Segmentation</a> technical report. Thus the resulting
455
     * collector will not split the surrogate pair and will preserve any
456
     * combining characters or remove them with the base character.
457
     * 
458
     * @return a new {@code Collector} which cuts the resulting string at
459
     *         grapheme cluster boundary.
460
     */
461
    public Joining cutAtGrapheme() {
462
        return withCut(CUT_GRAPHEME);
1✔
463
    }
464

465
    /**
466
     * Returns a {@code Collector} which behaves like this collector, but cuts
467
     * the resulting string at word boundary when limit is reached.
468
     * 
469
     * <p>
470
     * The beginning and end of every input stream element or delimiter is
471
     * always considered as word boundary, so the stream of
472
     * {@code "one", "two three"} collected with
473
     * {@code Joining.with("").maxChars(n).ellipsis("").cutAtWord()} may produce
474
     * the following strings depending on {@code n}:
475
     * 
476
     * <pre>{@code
477
     * ""
478
     * "one"
479
     * "onetwo"
480
     * "onetwo "
481
     * "onetwo three"
482
     * }</pre>
483
     * 
484
     * @return a new {@code Collector} which cuts the resulting string at word
485
     *         boundary.
486
     */
487
    public Joining cutAtWord() {
488
        return withCut(CUT_WORD);
1✔
489
    }
490

491
    /**
492
     * Returns a {@code Collector} which behaves like this collector, but cuts
493
     * the resulting string before the delimiter when limit is reached.
494
     * 
495
     * @return a new {@code Collector} which cuts the resulting string at before
496
     *         the delimiter.
497
     */
498
    public Joining cutBeforeDelimiter() {
499
        return withCut(CUT_BEFORE_DELIMITER);
1✔
500
    }
501

502
    /**
503
     * Returns a {@code Collector} which behaves like this collector, but cuts
504
     * the resulting string after the delimiter when limit is reached.
505
     * 
506
     * @return a new {@code Collector} which cuts the resulting string at after
507
     *         the delimiter.
508
     */
509
    public Joining cutAfterDelimiter() {
510
        return withCut(CUT_AFTER_DELIMITER);
1✔
511
    }
512

513
    @Override
514
    public Supplier<Accumulator> supplier() {
515
        return Accumulator::new;
1✔
516
    }
517

518
    @Override
519
    public BiConsumer<Accumulator, CharSequence> accumulator() {
520
        if (maxLength == -1)
1✔
521
            return (acc, str) -> {
1✔
522
                if (!acc.data.isEmpty())
1✔
523
                    acc.chars += delimiter.length();
1✔
524
                acc.chars += str.length();
1✔
525
                acc.data.add(str);
1✔
526
            };
1✔
527
        init();
1✔
528
        return (acc, str) -> {
1✔
529
            if (acc.count <= limit) {
1✔
530
                if (!acc.data.isEmpty()) {
1✔
531
                    acc.chars += delimiter.length();
1✔
532
                    acc.count += delimCount;
1✔
533
                }
534
                acc.chars += str.length();
1✔
535
                acc.count += length(str, true);
1✔
536
                acc.data.add(str);
1✔
537
            }
538
        };
1✔
539
    }
540

541
    @Override
542
    public BinaryOperator<Accumulator> combiner() {
543
        if (maxLength == -1)
1✔
544
            return (acc1, acc2) -> {
1✔
545
                if (acc1.data.isEmpty())
1✔
546
                    return acc2;
1✔
547
                if (acc2.data.isEmpty())
1✔
548
                    return acc1;
1✔
549
                acc1.chars += delimiter.length() + acc2.chars;
1✔
550
                acc1.data.addAll(acc2.data);
1✔
551
                return acc1;
1✔
552
            };
553
        init();
1✔
554
        BiConsumer<Accumulator, CharSequence> accumulator = accumulator();
1✔
555
        return (acc1, acc2) -> {
1✔
556
            if (acc1.data.isEmpty())
1✔
557
                return acc2;
1✔
558
            if (acc2.data.isEmpty())
1✔
559
                return acc1;
1✔
560
            int len = acc1.count + acc2.count + delimCount;
1✔
561
            if (len <= limit) {
1✔
562
                acc1.count = len;
1✔
563
                acc1.chars += delimiter.length() + acc2.chars;
1✔
564
                acc1.data.addAll(acc2.data);
1✔
565
            } else {
566
                for (CharSequence s : acc2.data) {
1✔
567
                    if (acc1.count > limit)
1✔
568
                        break;
1✔
569
                    accumulator.accept(acc1, s);
1✔
570
                }
1✔
571
            }
572
            return acc1;
1✔
573
        };
574
    }
575

576
    @Override
577
    public Function<Accumulator, String> finisher() {
578
        if (maxLength == -1) {
1✔
579
            return this::finisherNoOverflow;
1✔
580
        }
581
        init();
1✔
582
        if (limit <= 0 && lenStrategy != LENGTH_ELEMENTS) {
1✔
583
            char[] buf = new char[prefix.length() + suffix.length()];
1✔
584
            int pos = copyCut(buf, 0, prefix, maxLength, cutStrategy);
1✔
585
            pos = copyCut(buf, pos, suffix, maxLength - length(prefix, false), cutStrategy);
1✔
586
            String result = new String(buf, 0, pos);
1✔
587
            return acc -> result;
1✔
588
        }
589
        return acc -> {
1✔
590
            if (acc.count <= limit)
1✔
591
                return finisherNoOverflow(acc);
1✔
592
            char[] buf = new char[acc.chars + prefix.length() + suffix.length()];
1✔
593
            int size = acc.data.size();
1✔
594
            int pos = copy(buf, 0, prefix);
1✔
595
            int ellipsisCount = length(ellipsis, false);
1✔
596
            int rest = limit - ellipsisCount;
1✔
597
            if (rest < 0) {
1✔
598
                pos = copyCut(buf, pos, ellipsis, limit, CUT_ANYWHERE);
1✔
599
            } else {
600
                for (int i = 0; i < size; i++) {
1✔
601
                    String s = acc.data.get(i).toString();
1✔
602
                    int count = length(s, true);
1✔
603
                    if (i > 0) {
1✔
604
                        if (cutStrategy == CUT_BEFORE_DELIMITER && delimCount + count > rest) {
1✔
605
                            break;
1✔
606
                        }
607
                        if (delimCount > rest) {
1✔
608
                            pos = copyCut(buf, pos, delimiter, rest, cutStrategy);
1✔
609
                            break;
1✔
610
                        }
611
                        rest -= delimCount;
1✔
612
                        pos = copy(buf, pos, delimiter);
1✔
613
                    }
614
                    if (cutStrategy == CUT_AFTER_DELIMITER && delimCount + count > rest) {
1✔
615
                        break;
1✔
616
                    }
617
                    if (count > rest) {
1✔
618
                        pos = copyCut(buf, pos, s, rest, cutStrategy);
1✔
619
                        break;
1✔
620
                    }
621
                    pos = copy(buf, pos, s);
1✔
622
                    rest -= count;
1✔
623
                }
624
                pos = copy(buf, pos, ellipsis);
1✔
625
            }
626
            pos = copy(buf, pos, suffix);
1✔
627
            return new String(buf, 0, pos);
1✔
628
        };
629
    }
630

631
    @Override
632
    public Set<Characteristics> characteristics() {
633
        init();
1✔
634
        if (limit <= 0)
1✔
635
            return Collections.singleton(Characteristics.UNORDERED);
1✔
636
        return Collections.emptySet();
1✔
637
    }
638

639
    @Override
640
    Predicate<Accumulator> finished() {
641
        if (maxLength == -1)
1✔
642
            return null;
1✔
643
        init();
1✔
644
        if (limit <= 0 && lenStrategy != LENGTH_ELEMENTS)
1✔
645
            return alwaysTrue();
1✔
646
        return acc -> acc.count > limit;
1✔
647
    }
648
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc