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