001    /*
002     * Licensed to the Apache Software Foundation (ASF) under one or more
003     * contributor license agreements.  See the NOTICE file distributed with
004     * this work for additional information regarding copyright ownership.
005     * The ASF licenses this file to You under the Apache License, Version 2.0
006     * (the "License"); you may not use this file except in compliance with
007     * the License.  You may obtain a copy of the License at
008     * 
009     *      http://www.apache.org/licenses/LICENSE-2.0
010     * 
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    package org.apache.commons.lang;
018    
019    import java.io.Serializable;
020    import java.util.Iterator;
021    import java.util.NoSuchElementException;
022    
023    import org.apache.commons.lang.text.StrBuilder;
024    
025    /**
026     * <p>A contiguous range of characters, optionally negated.</p>
027     * 
028     * <p>Instances are immutable.</p>
029     *
030     * <p>#ThreadSafe#</p>
031     * @author Apache Software Foundation
032     * @author Chris Feldhacker
033     * @author Gary Gregory
034     * @since 1.0
035     * @version $Id: CharRange.java 1057072 2011-01-10 01:55:57Z niallp $
036     */
037    public final class CharRange implements Serializable {
038    
039        /**
040         * Required for serialization support. Lang version 2.0. 
041         * 
042         * @see java.io.Serializable
043         */
044        private static final long serialVersionUID = 8270183163158333422L;
045        
046        /** The first character, inclusive, in the range. */
047        private final char start;
048        /** The last character, inclusive, in the range. */
049        private final char end;
050        /** True if the range is everything except the characters specified. */
051        private final boolean negated;
052        
053        /** Cached toString. */
054        private transient String iToString;
055    
056        // Static
057        //-----------------------------------------------------------------------
058        /**
059         * <p>Constructs a <code>CharRange</code> over a single character.</p>
060         *
061         * @param ch  only character in this range
062         * @return the new CharRange object
063         * @see CharRange#CharRange(char, char, boolean)
064         * @since 2.5
065         */
066        public static CharRange is(char ch) {
067            return new CharRange(ch, ch, false);
068        }
069    
070        /**
071         * <p>Constructs a negated <code>CharRange</code> over a single character.</p>
072         *
073         * @param ch  only character in this range
074         * @return the new CharRange object
075         * @see CharRange#CharRange(char, char, boolean)
076         * @since 2.5
077         */
078        public static CharRange isNot(char ch) {
079            return new CharRange(ch, ch, true);
080        }
081    
082        /**
083         * <p>Constructs a <code>CharRange</code> over a set of characters.</p>
084         *
085         * @param start  first character, inclusive, in this range
086         * @param end  last character, inclusive, in this range
087         * @return the new CharRange object
088         * @see CharRange#CharRange(char, char, boolean)
089         * @since 2.5
090         */
091        public static CharRange isIn(char start, char end) {
092            return new CharRange(start, end, false);
093        }
094    
095        /**
096         * <p>Constructs a negated <code>CharRange</code> over a set of characters.</p>
097         *
098         * @param start  first character, inclusive, in this range
099         * @param end  last character, inclusive, in this range
100         * @return the new CharRange object
101         * @see CharRange#CharRange(char, char, boolean)
102         * @since 2.5
103         */
104        public static CharRange isNotIn(char start, char end) {
105            return new CharRange(start, end, true);
106        }
107    
108        //-----------------------------------------------------------------------
109        /**
110         * <p>Constructs a <code>CharRange</code> over a single character.</p>
111         *
112         * @param ch  only character in this range
113         */
114        public CharRange(char ch) {
115            this(ch, ch, false);
116        }
117    
118        /**
119         * <p>Constructs a <code>CharRange</code> over a single character,
120         * optionally negating the range.</p>
121         *
122         * <p>A negated range includes everything except the specified char.</p>
123         *
124         * @param ch  only character in this range
125         * @param negated  true to express everything except the range
126         */
127        public CharRange(char ch, boolean negated) {
128            this(ch, ch, negated);
129        }
130    
131        /**
132         * <p>Constructs a <code>CharRange</code> over a set of characters.</p>
133         *
134         * @param start  first character, inclusive, in this range
135         * @param end  last character, inclusive, in this range
136         */
137        public CharRange(char start, char end) {
138            this(start, end, false);
139        }
140    
141        /**
142         * <p>Constructs a <code>CharRange</code> over a set of characters,
143         * optionally negating the range.</p>
144         *
145         * <p>A negated range includes everything except that defined by the
146         * start and end characters.</p>
147         * 
148         * <p>If start and end are in the wrong order, they are reversed.
149         * Thus <code>a-e</code> is the same as <code>e-a</code>.</p>
150         *
151         * @param start  first character, inclusive, in this range
152         * @param end  last character, inclusive, in this range
153         * @param negated  true to express everything except the range
154         */
155        public CharRange(char start, char end, boolean negated) {
156            super();
157            if (start > end) {
158                char temp = start;
159                start = end;
160                end = temp;
161            }
162            
163            this.start = start;
164            this.end = end;
165            this.negated = negated;
166        }
167    
168        // Accessors
169        //-----------------------------------------------------------------------
170        /**
171         * <p>Gets the start character for this character range.</p>
172         * 
173         * @return the start char (inclusive)
174         */
175        public char getStart() {
176            return this.start;
177        }
178    
179        /**
180         * <p>Gets the end character for this character range.</p>
181         * 
182         * @return the end char (inclusive)
183         */
184        public char getEnd() {
185            return this.end;
186        }
187    
188        /**
189         * <p>Is this <code>CharRange</code> negated.</p>
190         * 
191         * <p>A negated range includes everything except that defined by the
192         * start and end characters.</p>
193         *
194         * @return <code>true</code> is negated
195         */
196        public boolean isNegated() {
197            return negated;
198        }
199    
200        // Contains
201        //-----------------------------------------------------------------------
202        /**
203         * <p>Is the character specified contained in this range.</p>
204         *
205         * @param ch  the character to check
206         * @return <code>true</code> if this range contains the input character
207         */
208        public boolean contains(char ch) {
209            return (ch >= start && ch <= end) != negated;
210        }
211    
212        /**
213         * <p>Are all the characters of the passed in range contained in
214         * this range.</p>
215         *
216         * @param range  the range to check against
217         * @return <code>true</code> if this range entirely contains the input range
218         * @throws IllegalArgumentException if <code>null</code> input
219         */
220        public boolean contains(CharRange range) {
221            if (range == null) {
222                throw new IllegalArgumentException("The Range must not be null");
223            }
224            if (negated) {
225                if (range.negated) {
226                    return start >= range.start && end <= range.end;
227                }
228                return range.end < start || range.start > end;
229            }
230            if (range.negated) {
231                return start == 0 && end == Character.MAX_VALUE;
232            }
233            return start <= range.start && end >= range.end;
234        }
235    
236        // Basics
237        //-----------------------------------------------------------------------
238        /**
239         * <p>Compares two CharRange objects, returning true if they represent
240         * exactly the same range of characters defined in the same way.</p>
241         * 
242         * @param obj  the object to compare to
243         * @return true if equal
244         */
245        public boolean equals(Object obj) {
246            if (obj == this) {
247                return true;
248            }
249            if (obj instanceof CharRange == false) {
250                return false;
251            }
252            CharRange other = (CharRange) obj;
253            return start == other.start && end == other.end && negated == other.negated;
254        }
255    
256        /**
257         * <p>Gets a hashCode compatible with the equals method.</p>
258         * 
259         * @return a suitable hashCode
260         */
261        public int hashCode() {
262            return 83 + start + 7 * end + (negated ? 1 : 0);
263        }
264        
265        /**
266         * <p>Gets a string representation of the character range.</p>
267         * 
268         * @return string representation of this range
269         */
270        public String toString() {
271            if (iToString == null) {
272                StrBuilder buf = new StrBuilder(4);
273                if (isNegated()) {
274                    buf.append('^');
275                }
276                buf.append(start);
277                if (start != end) {
278                    buf.append('-');
279                    buf.append(end);
280                }
281                iToString = buf.toString();
282            }
283            return iToString;
284        }
285    
286        // Expansions
287        //-----------------------------------------------------------------------
288        /**
289         * <p>Returns an iterator which can be used to walk through the characters described by this range.</p>
290         *
291         * <p>#NotThreadSafe# the iterator is not threadsafe</p>
292         * @return an iterator to the chars represented by this range
293         * @since 2.5
294         */
295        public Iterator iterator() {
296            return new CharacterIterator(this);
297        }
298    
299        /**
300         * Character {@link Iterator}.
301         * <p>#NotThreadSafe#</p>
302         */
303        private static class CharacterIterator implements Iterator {
304            /** The current character */
305            private char current;
306    
307            private final CharRange range;
308            private boolean hasNext;
309    
310            /**
311             * Construct a new iterator for the character range.
312             *
313             * @param r The character range
314             */
315            private CharacterIterator(CharRange r) {
316                range = r;
317                hasNext = true;
318    
319                if (range.negated) {
320                    if (range.start == 0) {
321                        if (range.end == Character.MAX_VALUE) {
322                            // This range is an empty set
323                            hasNext = false;
324                        } else {
325                            current = (char) (range.end + 1);
326                        }
327                    } else {
328                        current = 0;
329                    }
330                } else {
331                    current = range.start;
332                }
333            }
334    
335            /**
336             * Prepare the next character in the range.
337             */
338            private void prepareNext() {
339                if (range.negated) {
340                    if (current == Character.MAX_VALUE) {
341                        hasNext = false;
342                    } else if (current + 1 == range.start) {
343                        if (range.end == Character.MAX_VALUE) {
344                            hasNext = false;
345                        } else {
346                            current = (char) (range.end + 1);
347                        }
348                    } else {
349                        current = (char) (current + 1);
350                    }
351                } else if (current < range.end) {
352                    current = (char) (current + 1);
353                } else {
354                    hasNext = false;
355                }
356            }
357    
358            /**
359             * Has the iterator not reached the end character yet?
360             *
361             * @return <code>true</code> if the iterator has yet to reach the character date
362             */
363            public boolean hasNext() {
364                return hasNext;
365            }
366    
367            /**
368             * Return the next character in the iteration
369             *
370             * @return <code>Character</code> for the next character
371             */
372            public Object next() {
373                if (hasNext == false) {
374                    throw new NoSuchElementException();
375                }
376                char cur = current;
377                prepareNext();
378                return new Character(cur);
379            }
380    
381            /**
382             * Always throws UnsupportedOperationException.
383             *
384             * @throws UnsupportedOperationException
385             * @see java.util.Iterator#remove()
386             */
387            public void remove() {
388                throw new UnsupportedOperationException();
389            }
390        }
391    }