Mega Code Archive

 
Categories / Java / Collections Data Structure
 

A space-optimized map for associating char keys with values

/*  * Copyright 2009 David Jurgens  *  * This file is part of the S-Space package and is covered under the terms and  * conditions therein.  *  * The S-Space package is free software: you can redistribute it and/or modify  * it under the terms of the GNU General Public License version 2 as published  * by the Free Software Foundation and distributed hereunder to you.  *  * THIS SOFTWARE IS PROVIDED "AS IS" AND NO REPRESENTATIONS OR WARRANTIES,  * EXPRESS OR IMPLIED ARE MADE.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, WE MAKE  * NO REPRESENTATIONS OR WARRANTIES OF MERCHANT- ABILITY OR FITNESS FOR ANY  * PARTICULAR PURPOSE OR THAT THE USE OF THE LICENSED SOFTWARE OR DOCUMENTATION  * WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER  * RIGHTS.  *  * You should have received a copy of the GNU General Public License  * along with this program. If not, see <http://www.gnu.org/licenses/>.  */ //package edu.ucla.sspace.util; import java.util.AbstractCollection; import java.util.AbstractSet; import java.util.AbstractMap; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; /**  * A space-optimized map for associating {@code char} keys with values.<p>  *  * This class makes a trade off for reduced space usage at the cost of decreased  * performace.  The {@code put}, {@code get}, {@code containsKey} and {@code  * get} operations are all logarithmic when the map is unmodified.  If a new  * mapping is added, or one is removed, the operation is linear in the number of  * mappings.  Both {@code size} and {@code isEmpty} are still constant time. <p>  *  * The {@code get} operation runs in logarithmic time.  The {@code set}  * operation runs in consant time if setting an existing non-zero value to a  * non-zero value.  However, if the {@code set} invocation sets a zero value to  * non-zero, the operation is linear with the size of the array.<p>  *  * This map allows {@code null values}.<p>  *  * Iteration ordering follows the natural ordering of {@code char} values, with  * the lowest {@code char} key value being returned first.  *  * <i>Implementation Note:</i> the {@code Iterator.remove()} method is currently  * unsupported and will throw an exception when called.  However, a future  * implementation will fix this.<p>  *  * @see TrieMap  * @see IntegerMap  * @see Map  *   * @author David Jurgens  */ public class CharMap<V> extends AbstractMap<Character,V>          implements java.io.Serializable {          private static final long serialVersionUID = 1L;     /**      * The keys stored in this map, in sorted order.  The index at which a key      * is found corresponds to the index at which its value is found.      */     char[] keyIndices;     /**      * The values stored in this map.      */     Object[] values;     /**      * Creates a new map.      */     public CharMap() {   keyIndices = new char[0];   values = new Object[0];     }     /**      * Creates a new map with the mappings contained in {@code m}.      */     public CharMap(Map<Character,? extends V> m) {   // Pre-allocate the arrays for all the mappings needed instead of just   // one at time   keyIndices = new char[m.size()];   values = new Object[m.size()];   // Get all of the integer keys and sort them to their correct position   // in the key array   Iterator<Character> it = m.keySet().iterator();   for (int i = 0; i < m.size(); ++i) {       keyIndices[i] = it.next();   }   Arrays.sort(keyIndices);   // Then map the values to their respective positions   for (int i = 0; i < keyIndices.length; ++i) {       values[i] = m.get(keyIndices[i]);   }     }          /**      * Checks that the key is non-{@code null} and is an {@code Integer} object,      * and then returns its {@code int} value.      */     private char checkKey(Object key) {   if (key == null) {       throw new NullPointerException("key cannot be null");   } else if (!(key instanceof Character)) {       throw new IllegalArgumentException("key must be an Character");   }   else {       return ((Character)key).charValue();   }     }     /**      * Removes all of the mappings from this map.      */     public void clear() {   keyIndices = new char[0];   values = new Object[0];     }     /**      * Returns {@code true} if this map contains a mapping for the specified key.      */     public boolean containsKey(Object key) {   char k = checkKey(key);   int index = Arrays.binarySearch(keyIndices, k);   return index >= 0;     }     public boolean containsValue(Object value) {   for (Object o : values) {       if (o == value || (o != null && o.equals(value))) {     return true;       }   }   return false;     }     /**      * Returns a {@link Set} view of the mappings contained in this map.      */     public Set<Entry<Character,V>> entrySet() {   return new EntrySet();     }     /**      * Returns the value to which the specified key is mapped, or {@code null}      * if this map contains no mapping for the key.      */       public V get(Object key) {   char k = checkKey(key);   return get(k);     }     /**      * Returns the value to which the specified key is mapped, or {@code null}      * if this map contains no mapping for the key.      */     @SuppressWarnings("unchecked")       public V get(char key) {   int pos = Arrays.binarySearch(keyIndices, key);   return (pos >= 0) ? (V)(values[pos]) : null;       }     /**      * Returns a {@link Set} view of the keys contained in this map.      */     public Set<Character> keySet() {   return new KeySet();     }     /**      * Adds the mapping from the provided key to the value.      *      * @param key      * @param value      *      * @throws NullPointerException if the key is {@code null}      * @throws IllegalArgumentException if the key is not an instance of {@link      *         Integer}      */     public V put(Character key, V value) {   char k = checkKey(key);         return put(k, value);     }     /**      * Adds the mapping from the provided key to the value.      *      * @param key      * @param value      *      * @throws NullPointerException if the key is {@code null}      * @throws IllegalArgumentException if the key is not an instance of {@link      *         Integer}      */     @SuppressWarnings("unchecked")     public V put(char key, V value) {         char k = key;   int index = Arrays.binarySearch(keyIndices, k);   if (index >= 0) {       V old = (V)(values[index]);       values[index] = value;       return old;   }   else {       int newIndex = 0 - (index + 1);             Object[] newValues = Arrays.copyOf(values, values.length + 1);       char[] newIndices = Arrays.copyOf(keyIndices, values.length + 1);       // shift the elements down to make room for the new value       for (int i = newIndex; i < values.length; ++i) {     newValues[i+1] = values[i];     newIndices[i+1] = keyIndices[i];       }       // insert the new value       newValues[newIndex] = value;       newIndices[newIndex] = k;       // switch the arrays with the lengthed versions       values = newValues;       keyIndices = newIndices;              return null;   }     }     /**      * Removes the mapping for a key from this map if it is present and returns      * the value to which this map previously associated the key, or {@code      * null} if the map contained no mapping for the key.      *      * @param key key whose mapping is to be removed from the map       *      * @return the previous value associated with key, or {@code null} if there      * was no mapping for key.      */     @SuppressWarnings("unchecked")         public V remove(Object key) {   char k = checkKey(key);         return remove(k);     }     /**      * Removes the mapping for a key from this map if it is present and returns      * the value to which this map previously associated the key, or {@code      * null} if the map contained no mapping for the key.      *      * @param key key whose mapping is to be removed from the map       *      * @return the previous value associated with key, or {@code null} if there      * was no mapping for key.      */     @SuppressWarnings("unchecked")         public V remove(char key) {         char k = key;   int index = Arrays.binarySearch(keyIndices, k);   if (index >= 0) {       V old = (V)(values[index]);       Object[] newValues = Arrays.copyOf(values, values.length - 1);       char[] newIndices = Arrays.copyOf(keyIndices, keyIndices.length - 1);       // shift the elements up to remove the values       for (int i = index; i < values.length - 1; ++i) {     newValues[i] = values[i+1];     newIndices[i] = keyIndices[i+1];       }       // update the arrays with the shorted versions       values = newValues;       keyIndices = newIndices;       return old;   }   return null;     }     /**      * Returns the number of key-value mappings in this trie.      */     public int size() {   return keyIndices.length;     }     /**      * Returns a {@link Collection} view of the values contained in this map.      */     public Collection<V> values() {   return new Values();     }     private class EntryIterator extends CharMapIterator<Map.Entry<Character,V>> {   public Map.Entry<Character,V> next() {       return nextEntry();   }        }     private class KeyIterator extends CharMapIterator<Character> {   public Character next() {       return nextEntry().getKey();   }        }     private class ValueIterator extends CharMapIterator<V> {   public V next() {       return nextEntry().getValue();   }        }     abstract class CharMapIterator<E> implements Iterator<E> {   private int next;   public CharMapIterator() {       next = 0;   }   public boolean hasNext() {       return next < size();   }   @SuppressWarnings("unchecked")   public Entry<Character,V> nextEntry() {       if (next >= size()) {     throw new NoSuchElementException("no further elements");       }       char key = keyIndices[next];       V value = (V)(values[next]);       next++;       return new CharEntry(key, value);   }      // REMINDER: this class needs to work with the actual indices for the   // key and value to avoid the logrithmic lookup   public void remove() {       throw new UnsupportedOperationException();   }     }     // REMINDER: this class needs to work with the actual indices for the key     // and value to avoid the logrithmic lookup     class CharEntry extends SimpleEntry<Character,V> {   private static final long serialVersionUID = 1L;      public CharEntry(char key, V value) {       super(key, value);   }   public V setValue(V newValue) {       return CharMap.this.put(getKey(), newValue);   }     }     class EntrySet extends AbstractSet<Entry<Character,V>> {   private static final long serialVersionUID = 1L;   public void clear() {       CharMap.this.clear();   }   public boolean contains(Object o) {       if (o instanceof Map.Entry) {     Map.Entry e = (Map.Entry)o;     Object key = e.getKey();     Object val = e.getValue();     Object mapVal = CharMap.this.get(key);     return mapVal == val || (val != null && val.equals(mapVal));       }       return false;   }   public Iterator<Map.Entry<Character,V>> iterator() {       return new EntryIterator();   }      public int size() {       return CharMap.this.size();   }     }     class KeySet extends AbstractSet<Character> {   private static final long serialVersionUID = 1L;      public KeySet() { }      public void clear() {       CharMap.this.clear();   }   public boolean contains(Object o) {       return containsKey(o);   }   public Iterator<Character> iterator() {       return new KeyIterator();   }      public boolean remove(Object o) {       return CharMap.this.remove(o) != null;   }   public int size() {       return CharMap.this.size();   }     }     /**      * A {@link Collection} view of the values contained in this trie.      */     private class Values extends AbstractCollection<V> {   private static final long serialVersionUID = 1L;      public void clear() {       CharMap.this.clear();   }   public boolean contains(Object o) {       return containsValue(o);   }      public Iterator<V> iterator() {       return new ValueIterator();   }      public int size() {       return CharMap.this.size();   }     }      } /*  * Copyright 2009 David Jurgens  *  * This file is part of the S-Space package and is covered under the terms and  * conditions therein.  *  * The S-Space package is free software: you can redistribute it and/or modify  * it under the terms of the GNU General Public License version 2 as published  * by the Free Software Foundation and distributed hereunder to you.  *  * THIS SOFTWARE IS PROVIDED "AS IS" AND NO REPRESENTATIONS OR WARRANTIES,  * EXPRESS OR IMPLIED ARE MADE.  BY WAY OF EXAMPLE, BUT NOT LIMITATION, WE MAKE  * NO REPRESENTATIONS OR WARRANTIES OF MERCHANT- ABILITY OR FITNESS FOR ANY  * PARTICULAR PURPOSE OR THAT THE USE OF THE LICENSED SOFTWARE OR DOCUMENTATION  * WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS, TRADEMARKS OR OTHER  * RIGHTS.  *  * You should have received a copy of the GNU General Public License  * along with this program. If not, see <http://www.gnu.org/licenses/>.  */ /* package edu.ucla.sspace.util; import java.util.Arrays; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import org.junit.Ignore; import org.junit.Test; import static org.junit.Assert.*; // * A collection of unit tests for {@link CharMap}  public class CharMapTests {          @Test public void testConstructor() {    CharMap<String> m = new CharMap<String>();     }     @Test public void testPut() {   CharMap<String> m = new CharMap<String>();   m.put('1', "1");   String s = m.get('1');   assertEquals("1", s);     }     @Test(expected=NullPointerException.class)      public void testPutNullKey() {   CharMap<String> m = new CharMap<String>();   m.put(null, "value");     }     public void testPutNullValue() {   CharMap<String> m = new CharMap<String>();   m.put('0', null);   String s = m.get('0');   assertEquals(null, s);     }     @Test public void testPutConflict() {   CharMap<String> m = new CharMap<String>();   m.put('1', "1");   m.put('1', "2");   String s = m.get('1');   assertEquals("2", s);     }     @Test public void testProgressive() {   CharMap<Integer> m = new CharMap<Integer>();   assertEquals(0, m.size());   int size = 1024;   for (int i = 0; i < size; ++i) {                    m.put((char)i, Integer.valueOf(i));              // check mapping       for (Map.Entry<Character,Integer> e : m.entrySet()) {     char p = e.getKey();     Integer q = e.getValue();     assertEquals(Integer.valueOf(p), q);       }   }     }     @Test public void testContainsKey() {   CharMap<String> m = new CharMap<String>();   m.put('1', "1");   m.put('2', "2");   assertTrue(m.containsKey('1'));   assertTrue(m.containsKey('2'));     }     @Test public void testContainsKeyFalse() {   CharMap<String> m = new CharMap<String>();   m.put('1', "1");   m.put('2', "2");   assertFalse(m.containsKey('3'));   assertFalse(m.containsKey('4'));   assertFalse(m.containsKey('0'));     }     @Test public void testContainsValue() {   CharMap<String> m = new CharMap<String>();   m.put('1', "1");   m.put('2', "2");   assertTrue(m.containsValue("1"));   assertTrue(m.containsValue("2"));     }     @Test public void testContainsValueFalse() {   CharMap<String> m = new CharMap<String>();   m.put('1', "1");   m.put('2', "2");   assertFalse(m.containsValue("3"));   assertFalse(m.containsValue("4"));   assertFalse(m.containsValue("5"));     }     @Test public void testKeySet() {   CharMap<String> m = new CharMap<String>();   m.put('1', "0");   m.put('2', "1");   m.put('3', "2");   m.put('4', "3");   m.put('5', "4");   Set<Character> test = m.keySet();      assertTrue(test.contains('1'));   assertTrue(test.contains('2'));   assertTrue(test.contains('3'));   assertTrue(test.contains('4'));   assertTrue(test.contains('5'));   assertFalse(test.contains('0'));   assertFalse(test.contains('6'));     }     @Test public void testKeyIterator() {   CharMap<String> m = new CharMap<String>();   m.put('0', "0");   m.put('1', "1");   m.put('2', "2");   Set<Character> control = new HashSet<Character>();   control.add('0');   control.add('1');   control.add('2');   Set<Character> test = m.keySet();   assertTrue(test.containsAll(control));   assertTrue(control.containsAll(test));     }     @Test public void testValueIterator() {   CharMap<String> m = new CharMap<String>();   m.put('0', "0");   m.put('1', "1");   m.put('2', "2");   Set<String> control = new HashSet<String>();   control.add("0");   control.add("1");   control.add("2");   Collection<String> test = m.values();      assertEquals(control.size(), test.size());   for (String s : test) {             assertTrue(control.contains(s));   }     }     @Test public void testIteratorHasNext() {      CharMap<String> m = new CharMap<String>();   Iterator<Character> it = m.keySet().iterator();   assertFalse(it.hasNext());   m.put('0', "0");   m.put('1', "1");   m.put('2', "2");   it = m.keySet().iterator();   Set<Character> control = new HashSet<Character>();   while (it.hasNext()) {       control.add(it.next());   }   Set<Character> test = m.keySet();   assertTrue(test.containsAll(control));   assertTrue(control.containsAll(test));     }     @Test(expected=NoSuchElementException.class)     public void testIteratorNextError() {      CharMap<String> m = new CharMap<String>();   m.put('0', "0");   Iterator<Character> it = m.keySet().iterator();   it.next();   it.next(); // error     }     @Test(expected=NoSuchElementException.class)     public void testEmptyIntIteratorNextError() {      CharMap<String> m = new CharMap<String>();   Iterator<Character> it = m.keySet().iterator();   it.next(); // error     }     @Test public void testRemove() {   CharMap<String> m = new CharMap<String>();   m.put('0', "0");   assertTrue(m.containsKey('0'));   m.remove('0');   assertFalse(m.containsKey('0'));   assertEquals(0, m.size());     }     @Test public void testMultipleRemoves() {   CharMap<String> m = new CharMap<String>();   Set<Character> control = new HashSet<Character>();      LinkedList<Character> list = new LinkedList<Character>();   for (int i = 0; i < 512; ++i) {       list.add((char)i);   }   for (Character c : list) {       m.put(c, String.valueOf(c));       control.add(c);   }   assertEquals(control.size(), m.size());   assertTrue(control.containsAll(m.keySet()));   // remove half   int half = control.size();   for (int i = 0; i < half; ++i) {       Character j = list.poll();       control.remove(j);       m.remove(j);   }   assertEquals(control.size(), m.size());   assertTrue(control.containsAll(m.keySet()));       }         @Test public void testSize() {   CharMap<String> m = new CharMap<String>();   assertEquals(0, m.size());   m.put('0', "0");   assertEquals(1, m.size());   m.put('1', "1");   assertEquals(2, m.size());   m.put('2', "2");   assertEquals(3, m.size());     }     @Test public void testIsEmpty() {   CharMap<String> m = new CharMap<String>();   assertTrue(m.isEmpty());   m.put('0', "0");   assertFalse(m.isEmpty());      m.clear();   assertTrue(m.isEmpty());     }     @Test public void testClear() {   CharMap<String> m = new CharMap<String>();   m.put('0', "0");   m.put('1', "1");   m.put('2', "2");   m.clear();   assertEquals(0, m.size());   assertFalse(m.containsKey('0'));   assertFalse(m.containsKey('1'));   assertFalse(m.containsKey('2'));     }     public static void main(String args[]) {   org.junit.runner.JUnitCore.main(CharMapTests.class.getName());     } } */