fr.quatrevieux.araknemu.util.Splitter   A
last analyzed

Complexity

Total Complexity 34

Size/Duplication

Total Lines 268
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 76
dl 0
loc 268
ccs 58
cts 58
cp 1
rs 9.68
c 1
b 0
f 0
wmc 34

17 Methods

Rating   Name   Duplication   Size   Complexity  
A nextSplit(char) 0 2 1
A nextPositiveInt() 0 2 1
A hasNext() 0 2 1
A nextNonNegativeInt() 0 2 1
A Splitter(String,char) 0 3 1
A value() 0 2 1
A nextNonNegativeIntOrDefault(int) 0 8 3
A nextInt() 0 2 1
A nextPart() 0 18 3
A nextInt(int) 0 2 1
A nextPartOrDefault(String) 0 6 2
A nextNonNegativeOrNegativeOneInt() 0 2 1
A nextIntOrDefault(int) 0 8 3
A nextNonNegativeOrNegativeOneIntOrDefault(int) 0 8 3
A nextPositiveIntOrDefault(int) 0 8 3
A nextNonNegativeInt(int) 0 2 1
B toIntArray() 0 30 7
1
/*
2
 * This file is part of Araknemu.
3
 *
4
 * Araknemu is free software: you can redistribute it and/or modify
5
 * it under the terms of the GNU Lesser General Public License as published by
6
 * the Free Software Foundation, either version 3 of the License, or
7
 * (at your option) any later version.
8
 *
9
 * Araknemu is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public License
15
 * along with Araknemu.  If not, see <https://www.gnu.org/licenses/>.
16
 *
17
 * Copyright (c) 2017-2022 Vincent Quatrevieux
18
 */
19
20
package fr.quatrevieux.araknemu.util;
21
22
import org.checkerframework.checker.index.qual.GTENegativeOne;
23
import org.checkerframework.checker.index.qual.NonNegative;
24
import org.checkerframework.checker.index.qual.Positive;
25
import org.checkerframework.common.value.qual.IntRange;
26
27
import java.util.NoSuchElementException;
28
import java.util.StringTokenizer;
29
30
/**
31
 * Split string parts separated by a character, and provide method for parsing parts to java primitives
32
 * This class can be overridden for provide extra parsing methods
33
 *
34
 * Unlike {@link StringTokenizer} :
35
 * - empty string are not skipped
36
 * - separator can only be a single character
37
 *
38
 * Usage:
39
 * <pre>{@code
40
 * // For "fixed" string format :
41
 * final Splitter splitter = new Splitter("foo,bar;15;fa,fc;;0", ';'); // Split string using ';'
42
 *
43
 * final Splitter begin = splitter.nextSplit(','); // Sub-split the first part
44
 *
45
 * begin.nextPart(); // "foo"
46
 * begin.nextPart(); // "bar"
47
 *
48
 * splitter.nextInt(); // 15
49
 *
50
 * final Splitter range = splitter.nextSplit(',');
51
 *
52
 * range.nextNonNegativeInt(16); // 250
53
 * range.nextNonNegativeInt(16); // 252
54
 *
55
 * splitter.nextPart(); // "" : do not skip empty strings
56
 * splitter.nextPart(); // "0"
57
 * splitter.nextPartOrDefault("###"); // "###" : use default value because there is not more parts
58
 *
59
 * // For "iterator style" parsing of dynamic string format :
60
 *
61
 * final Splitter splitter = new Splitter(myString, ';');
62
 *
63
 * // Will the end is not reached
64
 * while (splitter.hasNext()) {
65
 *     final Splitter parts = splitter.nextSplit(','); // Process the current part
66
 *
67
 *     parts.nextInt();
68
 *     parts.nextPart();
69
 *     // ...
70
 * }
71
 * }</pre>
72
 *
73
 * @see StringTokenizer Inspired by this class
74
 */
75
public final class Splitter {
76
    private final String string;
77
    private final char delimiter;
78
79 1
    private @NonNegative int currentPosition = 0;
80
81
    /**
82
     * @param string String to split
83
     * @param delimiter The delimiter
84
     */
85 1
    public Splitter(String string, char delimiter) {
86 1
        this.string = string;
87 1
        this.delimiter = delimiter;
88 1
    }
89
90
    /**
91
     * Get the inner string value
92
     */
93
    public String value() {
94 1
        return string;
95
    }
96
97
    /**
98
     * Get the next packet part
99
     * Unlike {@link StringTokenizer#nextToken()} empty parts are not skipped,
100
     * so an empty string can be returned by this method
101
     *
102
     * @throws NoSuchElementException When no more parts is available (reach end of the string)
103
     */
104
    public String nextPart() {
105 1
        final int currentPosition = this.currentPosition;
0 ignored issues
show
Comprehensibility introduced by
The variable currentPositionshadows a field with the same name declared in line 79. Consider renaming it.
Loading history...
106 1
        final String packet = this.string;
107
108 1
        if (currentPosition > packet.length()) {
109 1
            throw new NoSuchElementException();
110
        }
111
112 1
        int nextPosition = packet.indexOf(delimiter, currentPosition);
113
114 1
        if (nextPosition == -1) {
115 1
            nextPosition = packet.length();
116
        }
117
118 1
        final String token = packet.substring(currentPosition, nextPosition);
119 1
        this.currentPosition = nextPosition + 1;
120
121 1
        return token;
122
    }
123
124
    /**
125
     * There is more parts on the string
126
     */
127
    public boolean hasNext() {
128 1
        return currentPosition <= string.length();
129
    }
130
131
    /**
132
     * Get the next part if exists, or return the default value
133
     *
134
     * The default value is only used if the part is missing, not if the part is an empty string.
135
     * So the code : {@code new Splitter("").nextPartOrDefault("foo")} will return "", and not "foo"
136
     *
137
     * @param defaultValue The used value if the next part is missing
138
     */
139
    public String nextPartOrDefault(String defaultValue) {
140 1
        if (!hasNext()) {
141 1
            return defaultValue;
142
        }
143
144 1
        return nextPart();
145
    }
146
147
    /**
148
     * Parse next part as integer
149
     *
150
     * @throws NoSuchElementException When no more parts is available (reach end of the packet)
151
     * @throws NumberFormatException When the number format is invalid
152
     */
153
    public int nextInt() {
154 1
        return Integer.parseInt(nextPart());
155
    }
156
157
    /**
158
     * Parse next part as integer
159
     *
160
     * @param base Number encoding base
161
     *
162
     * @throws NoSuchElementException When no more parts is available (reach end of the packet)
163
     * @throws NumberFormatException When the number format is invalid
164
     */
165
    public int nextInt(@IntRange(from = Character.MIN_RADIX, to = Character.MAX_RADIX) int base) {
166 1
        return Integer.parseInt(nextPart(), base);
167
    }
168
169
    /**
170
     * Parse next part as integer if exists, or get the default value
171
     * Unlike {@link Splitter#nextPartOrDefault(String)} the default value will be used
172
     * if the next part is an empty string
173
     *
174
     * @param defaultValue Default value to use if the next part is missing or empty
175
     *
176
     * @throws NumberFormatException When the number format is invalid
177
     */
178
    public int nextIntOrDefault(int defaultValue) {
179 1
        if (!hasNext()) {
180 1
            return defaultValue;
181
        }
182
183 1
        final String part = nextPart();
184
185 1
        return part.isEmpty() ? defaultValue : Integer.parseInt(part);
186
    }
187
188
    /**
189
     * Parse next part as non negative or -1 (i.e. >= -1) integer
190
     *
191
     * @throws NoSuchElementException When no more parts is available (reach end of the packet)
192
     * @throws NumberFormatException When the number format is invalid
193
     * @throws IllegalArgumentException When the number is too low
194
     */
195
    public @GTENegativeOne int nextNonNegativeOrNegativeOneInt() {
196 1
        return ParseUtils.parseNonNegativeOrNegativeOneInt(nextPart());
197
    }
198
199
    /**
200
     * Parse next part as non negative or -1 (i.e. >= -1) integer if exists, or get the default value
201
     * Unlike {@link Splitter#nextPartOrDefault(String)} the default value will be used
202
     * if the next part is an empty string
203
     *
204
     * @param defaultValue Default value to use if the next part is missing or empty
205
     *
206
     * @throws NumberFormatException When the number format is invalid
207
     * @throws IllegalArgumentException When the number is too low
208
     */
209
    public @GTENegativeOne int nextNonNegativeOrNegativeOneIntOrDefault(@GTENegativeOne int defaultValue) {
210 1
        if (!hasNext()) {
211 1
            return defaultValue;
212
        }
213
214 1
        final String part = nextPart();
215
216 1
        return part.isEmpty() ? defaultValue : ParseUtils.parseNonNegativeOrNegativeOneInt(part);
217
    }
218
219
    /**
220
     * Parse next part as non negative (i.e. >= 0) integer
221
     *
222
     * @param base Number encoding base
223
     *
224
     * @throws NoSuchElementException When no more parts is available (reach end of the packet)
225
     * @throws NumberFormatException When the number format is invalid
226
     * @throws IllegalArgumentException When the number is too low
227
     */
228
    public @NonNegative int nextNonNegativeInt(@IntRange(from = Character.MIN_RADIX, to = Character.MAX_RADIX) int base) {
229 1
        return ParseUtils.parseNonNegativeInt(nextPart(), base);
230
    }
231
232
    /**
233
     * Parse next part as non negative (i.e. >= 0) integer
234
     *
235
     * @throws NoSuchElementException When no more parts is available (reach end of the packet)
236
     * @throws NumberFormatException When the number format is invalid
237
     * @throws IllegalArgumentException When the number is too low
238
     */
239
    public @NonNegative int nextNonNegativeInt() {
240 1
        return ParseUtils.parseNonNegativeInt(nextPart());
241
    }
242
243
    /**
244
     * Parse next part as non negative (i.e. >= 0) integer if exists, or get the default value
245
     * Unlike {@link Splitter#nextPartOrDefault(String)} the default value will be used
246
     * if the next part is an empty string
247
     *
248
     * @param defaultValue Default value to use if the next part is missing or empty
249
     *
250
     * @throws NumberFormatException When the number format is invalid
251
     * @throws IllegalArgumentException When the number is too low
252
     */
253
    public @NonNegative int nextNonNegativeIntOrDefault(@NonNegative int defaultValue) {
254 1
        if (!hasNext()) {
255 1
            return defaultValue;
256
        }
257
258 1
        final String part = nextPart();
259
260 1
        return part.isEmpty() ? defaultValue : ParseUtils.parseNonNegativeInt(part);
261
    }
262
263
    /**
264
     * Parse next part as positive (i.e. >= 1) integer
265
     *
266
     * @throws NoSuchElementException When no more parts is available (reach end of the packet)
267
     * @throws NumberFormatException When the number format is invalid
268
     * @throws IllegalArgumentException When the number is too low
269
     */
270
    public @Positive int nextPositiveInt() {
271 1
        return ParseUtils.parsePositiveInt(nextPart());
272
    }
273
274
    /**
275
     * Parse next part as positive (i.e. >= 1) integer if exists, or get the default value
276
     * Unlike {@link Splitter#nextPartOrDefault(String)} the default value will be used
277
     * if the next part is an empty string
278
     *
279
     * @param defaultValue Default value to use if the next part is missing or empty
280
     *
281
     * @throws NumberFormatException When the number format is invalid
282
     * @throws IllegalArgumentException When the number is too low
283
     */
284
    public @Positive int nextPositiveIntOrDefault(@Positive int defaultValue) {
285 1
        if (!hasNext()) {
286 1
            return defaultValue;
287
        }
288
289 1
        final String part = nextPart();
290
291 1
        return part.isEmpty() ? defaultValue : ParseUtils.parsePositiveInt(part);
292
    }
293
294
    /**
295
     * Split the next token using given delimiter
296
     *
297
     * This method is equivalent to `new Splitter(splitter.nextPart(), delimiter);`
298
     */
299
    public Splitter nextSplit(char delimiter) {
300 1
        return new Splitter(nextPart(), delimiter);
301
    }
302
303
    /**
304
     * Parse all remaining parts to an array of int
305
     * If the value is empty, an array of size 0 is returned
306
     * After calling
307
     *
308
     * Note: this method do not skip empty parts, which will cause {@link NumberFormatException}
309
     *
310
     * @throws NoSuchElementException When the end is already reached
311
     * @throws NumberFormatException If a part is non-well formatted
312
     */
313
    public int[] toIntArray() {
314 1
        if (currentPosition > string.length()) {
315 1
            throw new NoSuchElementException();
316
        }
317
318 1
        final String str = currentPosition == 0 ? string : string.substring(currentPosition);
319 1
        final char delimiter = this.delimiter;
0 ignored issues
show
Comprehensibility introduced by
The variable delimitershadows a field with the same name declared in line 77. Consider renaming it.
Loading history...
320
321 1
        if (str.isEmpty()) {
322 1
            ++currentPosition; // Invalidate splitter for next call
323 1
            return new int[0];
324
        }
325
326
        // Start count at 1 : number of parts is always 1 + delimiters count
327 1
        int count = 1;
328
329
        // Count delimiters
330 1
        for (int i = 0; i < str.length(); ++i) {
331 1
            if (str.charAt(i) == delimiter) {
332 1
                ++count;
333
            }
334
        }
335
336 1
        final int[] values = new int[count];
337
338 1
        for (int i = 0; i < count; ++i) {
339 1
            values[i] = nextInt();
340
        }
341
342 1
        return values;
343
    }
344
}
345