001/*
002 * Copyright (C) 2009-2011 Mathias Doenitz
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 * http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.parboiled.support;
018
019import org.parboiled.common.StringUtils;
020import org.parboiled.common.Preconditions;
021import org.parboiled.common.StringUtils;
022
023import java.util.Arrays;
024
025import static org.parboiled.common.Preconditions.checkArgNotNull;
026
027/**
028 * An immutable, set-like aggregation of (relatively few) characters that allows for an inverted semantic
029 * ("all chars except these few").
030 */
031public class Characters {
032
033    private static final char[] NO_CHARS = new char[0];
034
035    /**
036     * The empty Characters set
037     */
038    public static final Characters NONE = new Characters(false, NO_CHARS);
039
040    /**
041     * The Characters set including all character.
042     */
043    public static final Characters ALL = new Characters(true, NO_CHARS);
044
045    // if the set is subtractive its semantics change from "includes all characters in the set" to
046    // "includes all characters not in the set"
047    private final boolean subtractive;
048    private final char[] chars;
049
050    private Characters(boolean subtractive, char[] chars) {
051        this.subtractive = subtractive;
052        this.chars = Preconditions.checkArgNotNull(chars, "chars").clone();   // wilbur: clone and then sort for fast lookup
053        Arrays.sort(this.chars);
054    }
055
056    /**
057     * @return true if the set is subtractive
058     */
059    public boolean isSubtractive() {
060        return subtractive;
061    }
062
063    /**
064     * Returns the characters in this set, if it is additive.
065     * If the set is subtractive the method returns the characters <b>not</b> in the set.
066     *
067     * @return the characters
068     */
069    public char[] getChars() {
070        return chars;
071    }
072
073    /**
074     * Adds the given character to the set.
075     *
076     * @param c the character to add
077     * @return a new Characters object
078     */
079    public Characters add(char c) {
080        return subtractive ? removeFromChars(c) : addToChars(c);
081    }
082
083    /**
084     * Removes the given character from the set.
085     *
086     * @param c the character to remove
087     * @return a new Characters object
088     */
089    public Characters remove(char c) {
090        return subtractive ? addToChars(c) : removeFromChars(c);
091    }
092
093    /**
094     * Determines whether this instance contains the given character.
095     *
096     * @param c the character to check for
097     * @return true if this instance contains c
098     */
099    public boolean contains(char c) {
100        // return indexOf(chars, c) == -1 ? subtractive : !subtractive;
101        return (Arrays.binarySearch(chars, c) < 0)  ? subtractive : !subtractive;  // wilbur: faster lookup
102    }
103
104    /**
105     * Returns a new Characters object containing all the characters of this instance plus all characters of the
106     * given instance.
107     *
108     * @param other the other Characters to add
109     * @return a new Characters object
110     */
111    public Characters add(Characters other) {
112        Preconditions.checkArgNotNull(other, "other");
113        if (!subtractive && !other.subtractive) {
114            return addToChars(other.chars);
115        }
116        if (subtractive && other.subtractive) {
117            return retainAllChars(other.chars);
118        }
119        return subtractive ? removeFromChars(other.chars) : other.removeFromChars(chars);
120    }
121
122    /**
123     * Returns a new Characters object containing all the characters of this instance minus all characters of the
124     * given instance.
125     *
126     * @param other the other Characters to remove
127     * @return a new Characters object
128     */
129    public Characters remove(Characters other) {
130        Preconditions.checkArgNotNull(other, "other");
131        if (!subtractive && !other.subtractive) {
132            return removeFromChars(other.chars);
133        }
134        if (subtractive && other.subtractive) {
135            return new Characters(false, other.removeFromChars(chars).chars);
136        }
137        return subtractive ? addToChars(other.chars) : retainAllChars(other.chars);
138    }
139
140    @Override
141    public String toString() {
142        StringBuilder sb = new StringBuilder();
143        sb.append(subtractive ? "![" : "[");
144        for (char c : chars) {
145            sb.append(StringUtils.escape(c));
146        }
147        sb.append(']');
148        return sb.toString();
149    }
150
151    @Override
152    public boolean equals(Object o) {
153        if (this == o) return true;
154        if (!(o instanceof Characters)) return false;
155        Characters that = (Characters) o;
156        return subtractive == that.subtractive && equivalent(chars, that.chars);
157    }
158
159    @Override
160    public int hashCode() {
161        int result = (subtractive ? 1 : 0);
162        result = 31 * result + Arrays.hashCode(chars);
163        return result;
164    }
165
166    private Characters addToChars(char[] chs) {
167        Characters characters = this;
168        for (char c : chs) {
169            characters = characters.addToChars(c);
170        }
171        return characters;
172    }
173
174    private Characters addToChars(char c) {
175        if (indexOf(chars, c) != -1) return this;
176        char[] newChars = new char[chars.length + 1];
177        System.arraycopy(chars, 0, newChars, 0, chars.length);
178        newChars[chars.length] = c;
179        return new Characters(subtractive, newChars);
180    }
181
182    private Characters removeFromChars(char[] chs) {
183        Characters characters = this;
184        for (char c : chs) {
185            characters = characters.removeFromChars(c);
186        }
187        return characters;
188    }
189
190    private Characters removeFromChars(char c) {
191        int ix = indexOf(chars, c);
192        if (ix == -1) return this;
193        if (chars.length == 1) return subtractive ? Characters.ALL : Characters.NONE;
194        char[] newChars = new char[chars.length - 1];
195        System.arraycopy(chars, 0, newChars, 0, ix);
196        System.arraycopy(chars, ix + 1, newChars, ix, chars.length - ix - 1);
197        return new Characters(subtractive, newChars);
198    }
199
200    private Characters retainAllChars(char[] chs) {
201        Characters characters = this;
202        for (char c : chars) {
203            if (indexOf(chs, c) == -1) {
204                characters = characters.removeFromChars(c);
205            }
206        }
207        return characters;
208    }
209
210    private static int indexOf(char[] chars, char c) {
211        for (int i = 0; i < chars.length; i++) {
212            if (chars[i] == c) return i;
213        }
214        return -1;
215    }
216
217    // order independent Array.equals()
218    private static boolean equivalent(char[] a, char[] b) {
219        Preconditions.checkArgNotNull(a, "a");
220        Preconditions.checkArgNotNull(b, "b");
221        if (a == b) return true;
222        int length = a.length;
223        if (b.length != length) return false;
224
225        outer:
226        for (int i = 0; i < length; i++) {
227            char ac = a[i];
228            for (int j = 0; j < length; j++) {
229                if (ac == b[j]) continue outer;
230            }
231            return false;
232        }
233        return true;
234    }
235
236    /**
237     * Creates a new Characters instance containing only the given char.
238     *
239     * @param c the char
240     * @return a new Characters object
241     */
242    public static Characters of(char c) {
243        return new Characters(false, new char[] {c});
244    }
245
246    /**
247     * Creates a new Characters instance containing only the given chars.
248     *
249     * @param chars the chars
250     * @return a new Characters object
251     */
252    public static Characters of(char... chars) {
253        return chars.length == 0 ? Characters.NONE : new Characters(false, chars.clone());
254    }
255
256    /**
257     * Creates a new Characters instance containing only the given chars.
258     *
259     * @param chars the chars
260     * @return a new Characters object
261     */
262    public static Characters of(String chars) {
263        return StringUtils.isEmpty(chars) ? Characters.NONE : new Characters(false, chars.toCharArray());
264    }
265
266    /**
267     * Creates a new Characters instance containing all characters minus the given one.
268     *
269     * @param c the char to NOT include
270     * @return a new Characters object
271     */
272    public static Characters allBut(char c) {
273        return new Characters(true, new char[] {c});
274    }
275
276    /**
277     * Creates a new Characters instance containing all characters minus the given ones.
278     *
279     * @param chars the chars to NOT include
280     * @return a new Characters object
281     */
282    public static Characters allBut(char... chars) {
283        return chars.length == 0 ? Characters.ALL : new Characters(true, chars.clone());
284    }
285
286    /**
287     * Creates a new Characters instance containing all characters minus the given ones.
288     *
289     * @param chars the chars to NOT include
290     * @return a new Characters object
291     */
292    public static Characters allBut(String chars) {
293        return StringUtils.isEmpty(chars) ? Characters.ALL : new Characters(true, chars.toCharArray());
294    }
295
296}