1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 package org.apache.commons.lang3;
18
19 import java.io.IOException;
20 import java.io.InvalidObjectException;
21 import java.io.ObjectInputStream;
22 import java.io.Serializable;
23 import java.util.Iterator;
24 import java.util.NoSuchElementException;
25 import java.util.Objects;
26
27 /**
28 * A contiguous range of characters, optionally negated.
29 *
30 * <p>
31 * Instances are immutable.
32 * </p>
33 *
34 * <p>
35 * #ThreadSafe#
36 * </p>
37 *
38 * @since 1.0
39 * @since 3.21.0 {@code serialVersionUID} changed from {@code 8270183163158333422L} to {@code 2L}.
40 */
41 // TODO: This is no longer public and will be removed later as CharSet is moved
42 // to depend on Range.
43 final class CharRange implements Iterable<Character>, Serializable {
44
45 /**
46 * Character {@link Iterator}.
47 * <p>
48 * #NotThreadSafe#
49 * </p>
50 */
51 private static final class CharacterIterator implements Iterator<Character> {
52
53 /** The current character */
54 private char current;
55
56 private final CharRange range;
57 private boolean hasNext;
58
59 /**
60 * Constructs a new iterator for the character range.
61 *
62 * @param r The character range.
63 */
64 private CharacterIterator(final CharRange r) {
65 range = r;
66 hasNext = true;
67 if (range.isEmpty()) {
68 // This range is an empty set
69 hasNext = false;
70 } else if (range.negated) {
71 if (range.isStartMin()) {
72 current = (char) (range.end + 1);
73 } else {
74 current = Character.MIN_VALUE;
75 }
76 } else {
77 current = range.start;
78 }
79 }
80
81 /**
82 * Tests whether this iterator reached the end character.
83 *
84 * @return {@code true} if the iterator has yet to reach the character date.
85 */
86 @Override
87 public boolean hasNext() {
88 return hasNext;
89 }
90
91 /**
92 * Returns the next character in the iteration.
93 *
94 * @return {@link Character} for the next character.
95 */
96 @Override
97 public Character next() {
98 if (!hasNext) {
99 throw new NoSuchElementException();
100 }
101 final char cur = current;
102 prepareNext();
103 return Character.valueOf(cur);
104 }
105
106 /**
107 * Prepares the next character in the range.
108 */
109 private void prepareNext() {
110 if (range.negated) {
111 if (current == Character.MAX_VALUE) {
112 hasNext = false;
113 } else if (current + 1 == range.start) {
114 if (range.isEndMax()) {
115 hasNext = false;
116 } else {
117 current = (char) (range.end + 1);
118 }
119 } else {
120 current = (char) (current + 1);
121 }
122 } else if (current < range.end) {
123 current = (char) (current + 1);
124 } else {
125 hasNext = false;
126 }
127 }
128
129 /**
130 * Always throws {@link UnsupportedOperationException}.
131 *
132 * @throws UnsupportedOperationException Thrown because this operation is unsupported.
133 * @see java.util.Iterator#remove()
134 */
135 @Override
136 public void remove() {
137 throw new UnsupportedOperationException();
138 }
139 }
140
141 /**
142 * Required for serialization support. Lang version 2.0.
143 *
144 * @see java.io.Serializable
145 * @since 3.21.0 {@code serialVersionUID} changed from {@code 8270183163158333422L} to {@value}.
146 */
147 private static final long serialVersionUID = 2L;
148
149 /** Empty array. */
150 static final CharRange[] EMPTY_ARRAY = {};
151
152 /**
153 * Constructs a {@link CharRange} over a single character.
154 *
155 * @param ch only character in this range.
156 * @return The new CharRange object.
157 * @since 2.5
158 */
159 public static CharRange is(final char ch) {
160 return new CharRange(ch, ch, false);
161 }
162
163 /**
164 * Constructs a {@link CharRange} over a set of characters.
165 *
166 * <p>
167 * If start and end are in the wrong order, they are reversed.
168 * Thus {@code a-e} is the same as {@code e-a}.
169 * </p>
170 *
171 * @param start first character, inclusive, in this range.
172 * @param end last character, inclusive, in this range.
173 * @return The new CharRange object.
174 * @since 2.5
175 */
176 public static CharRange isIn(final char start, final char end) {
177 return new CharRange(start, end, false);
178 }
179
180 /**
181 * Constructs a negated {@link CharRange} over a single character.
182 *
183 * <p>
184 * A negated range includes everything except that defined by the
185 * single character.
186 * </p>
187 *
188 * @param ch only character in this range.
189 * @return The new CharRange object.
190 * @since 2.5
191 */
192 public static CharRange isNot(final char ch) {
193 return new CharRange(ch, ch, true);
194 }
195
196 /**
197 * Constructs a negated {@link CharRange} over a set of characters.
198 *
199 * <p>
200 * A negated range includes everything except that defined by the
201 * start and end characters.
202 * </p>
203 *
204 * <p>
205 * If start and end are in the wrong order, they are reversed.
206 * Thus {@code a-e} is the same as {@code e-a}.
207 * </p>
208 *
209 * @param start first character, inclusive, in this range.
210 * @param end last character, inclusive, in this range.
211 * @return The new CharRange object.
212 * @since 2.5
213 */
214 public static CharRange isNotIn(final char start, final char end) {
215 return new CharRange(start, end, true);
216 }
217
218 /** The first character, inclusive, in the range. */
219 private final char start;
220
221 /** The last character, inclusive, in the range. */
222 private final char end;
223
224 /** True if the range is everything except the characters specified. */
225 private final boolean negated;
226
227 /** Cached toString. */
228 private transient String iToString;
229
230 /**
231 * Constructs a {@link CharRange} over a set of characters,
232 * optionally negating the range.
233 *
234 * <p>
235 * A negated range includes everything except that defined by the
236 * start and end characters.
237 * </p>
238 *
239 * <p>
240 * If start and end are in the wrong order, they are reversed.
241 * Thus {@code a-e} is the same as {@code e-a}.
242 * </p>
243 *
244 * @param start first character, inclusive, in this range.
245 * @param end last character, inclusive, in this range.
246 * @param negated true to express everything except the range.
247 */
248 private CharRange(char start, char end, final boolean negated) {
249 if (start > end) {
250 final char temp = start;
251 start = end;
252 end = temp;
253 }
254
255 this.start = start;
256 this.end = end;
257 this.negated = negated;
258 }
259
260 /**
261 * Is the character specified contained in this range.
262 *
263 * @param ch The character to check.
264 * @return {@code true} if this range contains the input character.
265 */
266 public boolean contains(final char ch) {
267 return (ch >= start && ch <= end) != negated;
268 }
269
270 /**
271 * Are all the characters of the passed in range contained in
272 * this range.
273 *
274 * @param range The range to check against.
275 * @return {@code true} if this range entirely contains the input range.
276 * @throws NullPointerException Thrown if {@code null} input.
277 */
278 public boolean contains(final CharRange range) {
279 Objects.requireNonNull(range, "range");
280 if (negated) {
281 if (range.negated) {
282 return start >= range.start && end <= range.end;
283 }
284 return range.end < start || range.start > end;
285 }
286 if (range.negated) {
287 // range denotes [0, range.start - 1] union [range.end + 1, Character.MAX_VALUE]
288 if (range.isEmpty()) {
289 return true; // range denotes the empty set
290 }
291 if (range.isStartMin()) {
292 // range denotes [range.end + 1, Character.MAX_VALUE]
293 return isEndMax() && start <= range.end + 1;
294 }
295 if (range.isEndMax()) {
296 // range denotes [0, range.start - 1]
297 return isStartMin() && end + 1 >= range.start;
298 }
299 return isStartMin() && isEndMax();
300 }
301 return start <= range.start && end >= range.end;
302 }
303
304 /**
305 * Compares two CharRange objects, returning true if they represent
306 * exactly the same range of characters defined in the same way.
307 *
308 * @param obj The object to compare to.
309 * @return true if equal.
310 */
311 @Override
312 public boolean equals(final Object obj) {
313 if (obj == this) {
314 return true;
315 }
316 if (!(obj instanceof CharRange)) {
317 return false;
318 }
319 final CharRange other = (CharRange) obj;
320 return start == other.start && end == other.end && negated == other.negated;
321 }
322
323 /**
324 * Gets the end character for this character range.
325 *
326 * @return The end char (inclusive).
327 */
328 public char getEnd() {
329 return this.end;
330 }
331
332 /**
333 * Gets the start character for this character range.
334 *
335 * @return The start char (inclusive).
336 */
337 public char getStart() {
338 return this.start;
339 }
340
341 /**
342 * Gets a hashCode compatible with the equals method.
343 *
344 * @return A suitable hashCode.
345 */
346 @Override
347 public int hashCode() {
348 return Objects.hash(end, negated, start);
349 }
350
351 /**
352 * Tests whether this range denotes the empty set.
353 *
354 * <p>
355 * A plain (non-negated) range always contains at least one character and is
356 * therefore never empty. A negated range is empty if and only if it excludes the
357 * entire character space, i.e. if it was created via
358 * {@code isNotIn(Character.MIN_VALUE, Character.MAX_VALUE)}.
359 * </p>
360 *
361 * @return {@code true} if this range contains no characters, {@code false} otherwise.
362 */
363 boolean isEmpty() {
364 return negated && isStartMin() && isEndMax();
365 }
366
367 private boolean isEndMax() {
368 return end == Character.MAX_VALUE;
369 }
370
371 /**
372 * Tests whether this {@link CharRange} is negated.
373 *
374 * <p>
375 * A negated range includes everything except that defined by the
376 * start and end characters.
377 * </p>
378 *
379 * @return {@code true} if negated.
380 */
381 public boolean isNegated() {
382 return negated;
383 }
384
385 private boolean isStartMin() {
386 return start == Character.MIN_VALUE;
387 }
388
389 /**
390 * Returns an iterator which can be used to walk through the characters described by this range.
391 *
392 * <p>
393 * #NotThreadSafe# the iterator is not thread-safe
394 * </p>
395 *
396 * @return An iterator to the chars represented by this range
397 * @since 2.5
398 */
399 @Override
400 public Iterator<Character> iterator() {
401 return new CharacterIterator(this);
402 }
403
404 /**
405 * Re-asserts the {@code start <= end} invariant after default deserialization. The constructor reverses reversed endpoints, so a legitimately serialized
406 * instance always has {@code start <= end}; a stream that violates this did not come from the constructor and is rejected.
407 *
408 * @param in See {@link Serializable}.
409 * @throws IOException Thrown as described in {@link Serializable}.
410 * @throws ClassNotFoundException Thrown as described in {@link Serializable}.
411 * @throws InvalidObjectException Thrown if {@code start} is greater than {@code end}.
412 */
413 private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
414 in.defaultReadObject();
415 if (start > end) {
416 throw new InvalidObjectException("CharRange start is greater than end.");
417 }
418 }
419
420 /**
421 * Gets a string representation of the character range.
422 *
423 * @return string representation of this range.
424 */
425 @Override
426 public String toString() {
427 if (iToString == null) {
428 final StringBuilder buf = new StringBuilder(4);
429 if (isNegated()) {
430 buf.append('^');
431 }
432 buf.append(start);
433 if (start != end) {
434 buf.append('-');
435 buf.append(end);
436 }
437 iToString = buf.toString();
438 }
439 return iToString;
440 }
441 }