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.Serializable;
20  import java.util.Iterator;
21  import java.util.NoSuchElementException;
22  import java.util.Objects;
23  
24  /**
25   * A contiguous range of characters, optionally negated.
26   *
27   * <p>Instances are immutable.</p>
28   *
29   * <p>#ThreadSafe#</p>
30   *
31   * @since 1.0
32   * @since 3.21.0 {@code serialVersionUID} changed from {@code 8270183163158333422L} to {@code 2L}.
33   */
34  // TODO: This is no longer public and will be removed later as CharSet is moved
35  // to depend on Range.
36  final class CharRange implements Iterable<Character>, Serializable {
37  
38      /**
39       * Character {@link Iterator}.
40       * <p>#NotThreadSafe#</p>
41       */
42      private static final class CharacterIterator implements Iterator<Character> {
43  
44          /** The current character */
45          private char current;
46  
47          private final CharRange range;
48          private boolean hasNext;
49  
50          /**
51           * Constructs a new iterator for the character range.
52           *
53           * @param r The character range.
54           */
55          private CharacterIterator(final CharRange r) {
56              range = r;
57              hasNext = true;
58  
59              if (range.negated) {
60                  if (range.start == 0) {
61                      if (range.end == Character.MAX_VALUE) {
62                          // This range is an empty set
63                          hasNext = false;
64                      } else {
65                          current = (char) (range.end + 1);
66                      }
67                  } else {
68                      current = 0;
69                  }
70              } else {
71                  current = range.start;
72              }
73          }
74  
75          /**
76           * Tests whether this iterator reached the end character.
77           *
78           * @return {@code true} if the iterator has yet to reach the character date.
79           */
80          @Override
81          public boolean hasNext() {
82              return hasNext;
83          }
84  
85          /**
86           * Returns the next character in the iteration.
87           *
88           * @return {@link Character} for the next character.
89           */
90          @Override
91          public Character next() {
92              if (!hasNext) {
93                  throw new NoSuchElementException();
94              }
95              final char cur = current;
96              prepareNext();
97              return Character.valueOf(cur);
98          }
99  
100         /**
101          * Prepares the next character in the range.
102          */
103         private void prepareNext() {
104             if (range.negated) {
105                 if (current == Character.MAX_VALUE) {
106                     hasNext = false;
107                 } else if (current + 1 == range.start) {
108                     if (range.end == Character.MAX_VALUE) {
109                         hasNext = false;
110                     } else {
111                         current = (char) (range.end + 1);
112                     }
113                 } else {
114                     current = (char) (current + 1);
115                 }
116             } else if (current < range.end) {
117                 current = (char) (current + 1);
118             } else {
119                 hasNext = false;
120             }
121         }
122 
123         /**
124          * Always throws UnsupportedOperationException.
125          *
126          * @throws UnsupportedOperationException Always thrown.
127          * @see java.util.Iterator#remove()
128          */
129         @Override
130         public void remove() {
131             throw new UnsupportedOperationException();
132         }
133     }
134 
135     /**
136      * Required for serialization support. Lang version 2.0.
137      *
138      * @see java.io.Serializable
139      * @since 3.21.0 {@code serialVersionUID} changed from {@code 8270183163158333422L} to {@value}.
140      */
141     private static final long serialVersionUID = 2L;
142 
143     /** Empty array. */
144     static final CharRange[] EMPTY_ARRAY = {};
145 
146     /**
147      * Constructs a {@link CharRange} over a single character.
148      *
149      * @param ch  only character in this range.
150      * @return the new CharRange object.
151      * @since 2.5
152      */
153     public static CharRange is(final char ch) {
154         return new CharRange(ch, ch, false);
155     }
156 
157     /**
158      * Constructs a {@link CharRange} over a set of characters.
159      *
160      * <p>If start and end are in the wrong order, they are reversed.
161      * Thus {@code a-e} is the same as {@code e-a}.</p>
162      *
163      * @param start  first character, inclusive, in this range.
164      * @param end  last character, inclusive, in this range.
165      * @return the new CharRange object.
166      * @since 2.5
167      */
168     public static CharRange isIn(final char start, final char end) {
169         return new CharRange(start, end, false);
170     }
171 
172     /**
173      * Constructs a negated {@link CharRange} over a single character.
174      *
175      * <p>A negated range includes everything except that defined by the
176      * single character.</p>
177      *
178      * @param ch  only character in this range.
179      * @return the new CharRange object.
180      * @since 2.5
181      */
182     public static CharRange isNot(final char ch) {
183         return new CharRange(ch, ch, true);
184     }
185 
186     /**
187      * Constructs a negated {@link CharRange} over a set of characters.
188      *
189      * <p>A negated range includes everything except that defined by the
190      * start and end characters.</p>
191      *
192      * <p>If start and end are in the wrong order, they are reversed.
193      * Thus {@code a-e} is the same as {@code e-a}.</p>
194      *
195      * @param start  first character, inclusive, in this range.
196      * @param end  last character, inclusive, in this range.
197      * @return the new CharRange object.
198      * @since 2.5
199      */
200     public static CharRange isNotIn(final char start, final char end) {
201         return new CharRange(start, end, true);
202     }
203 
204     /** The first character, inclusive, in the range. */
205     private final char start;
206 
207     /** The last character, inclusive, in the range. */
208     private final char end;
209 
210     /** True if the range is everything except the characters specified. */
211     private final boolean negated;
212 
213     /** Cached toString. */
214     private transient String iToString;
215 
216     /**
217      * Constructs a {@link CharRange} over a set of characters,
218      * optionally negating the range.
219      *
220      * <p>A negated range includes everything except that defined by the
221      * start and end characters.</p>
222      *
223      * <p>If start and end are in the wrong order, they are reversed.
224      * Thus {@code a-e} is the same as {@code e-a}.</p>
225      *
226      * @param start  first character, inclusive, in this range.
227      * @param end  last character, inclusive, in this range.
228      * @param negated  true to express everything except the range.
229      */
230     private CharRange(char start, char end, final boolean negated) {
231         if (start > end) {
232             final char temp = start;
233             start = end;
234             end = temp;
235         }
236 
237         this.start = start;
238         this.end = end;
239         this.negated = negated;
240     }
241 
242     /**
243      * Is the character specified contained in this range.
244      *
245      * @param ch  the character to check.
246      * @return {@code true} if this range contains the input character.
247      */
248     public boolean contains(final char ch) {
249         return (ch >= start && ch <= end) != negated;
250     }
251 
252     /**
253      * Are all the characters of the passed in range contained in
254      * this range.
255      *
256      * @param range  the range to check against.
257      * @return {@code true} if this range entirely contains the input range.
258      * @throws NullPointerException if {@code null} input.
259      */
260     public boolean contains(final CharRange range) {
261         Objects.requireNonNull(range, "range");
262         if (negated) {
263             if (range.negated) {
264                 return start >= range.start && end <= range.end;
265             }
266             return range.end < start || range.start > end;
267         }
268         if (range.negated) {
269             return start == 0 && end == Character.MAX_VALUE;
270         }
271         return start <= range.start && end >= range.end;
272     }
273 
274     /**
275      * Compares two CharRange objects, returning true if they represent
276      * exactly the same range of characters defined in the same way.
277      *
278      * @param obj  the object to compare to.
279      * @return true if equal.
280      */
281     @Override
282     public boolean equals(final Object obj) {
283         if (obj == this) {
284             return true;
285         }
286         if (!(obj instanceof CharRange)) {
287             return false;
288         }
289         final CharRange other = (CharRange) obj;
290         return start == other.start && end == other.end && negated == other.negated;
291     }
292 
293     /**
294      * Gets the end character for this character range.
295      *
296      * @return the end char (inclusive).
297      */
298     public char getEnd() {
299         return this.end;
300     }
301 
302     /**
303      * Gets the start character for this character range.
304      *
305      * @return the start char (inclusive).
306      */
307     public char getStart() {
308         return this.start;
309     }
310 
311     /**
312      * Gets a hashCode compatible with the equals method.
313      *
314      * @return a suitable hashCode.
315      */
316     @Override
317     public int hashCode() {
318         return Objects.hash(end, negated, start);
319     }
320 
321     /**
322      * Is this {@link CharRange} negated.
323      *
324      * <p>A negated range includes everything except that defined by the
325      * start and end characters.</p>
326      *
327      * @return {@code true} if negated.
328      */
329     public boolean isNegated() {
330         return negated;
331     }
332 
333     /**
334      * Returns an iterator which can be used to walk through the characters described by this range.
335      *
336      * <p>#NotThreadSafe# the iterator is not thread-safe</p>
337      *
338      * @return an iterator to the chars represented by this range
339      * @since 2.5
340      */
341     @Override
342     public Iterator<Character> iterator() {
343         return new CharacterIterator(this);
344     }
345 
346     /**
347      * Gets a string representation of the character range.
348      *
349      * @return string representation of this range.
350      */
351     @Override
352     public String toString() {
353         if (iToString == null) {
354             final StringBuilder buf = new StringBuilder(4);
355             if (isNegated()) {
356                 buf.append('^');
357             }
358             buf.append(start);
359             if (start != end) {
360                 buf.append('-');
361                 buf.append(end);
362             }
363             iToString = buf.toString();
364         }
365         return iToString;
366     }
367 }