View Javadoc
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 }