001    package edu.nrao.sss.util;
002    
003    import java.util.Comparator;
004    import java.util.Map;
005    import java.util.SortedMap;
006    import java.util.SortedSet;
007    import java.util.TreeMap;
008    import java.util.TreeSet;
009    
010    /**
011     * A lookup table.
012     * <p>
013     * A lookup table is very similar to a map.  The main difference is that to
014     * get a value from a map, you need to specify exactly a key that the map
015     * holds.  To get a value from a lookup table, though, all you need to have
016     * is a key that is at least as great as the key for the first entry in the
017     * table.  The value returned for any key, K, is the value found in the entry
018     * whose key is less than or equal to K.  The entries in a lookup table are
019     * sorted in ascending order of the entries' keys.  The definition of
020     * "ascending" depends on the comparator used to construct the table.</p>
021     * <p>
022     * A classic use of a lookup table is a table of marginal income tax rates.
023     * Your marginal tax rate is given by the entry whose income is less than
024     * or equal to your own.</p>
025     * <p>
026     * The keys of this table may be any object that implements the
027     * {@link java.lang.Comparable} interface.  Examples of common key types
028     * are numbers, strings, and dates.  Via the use of
029     * <a href="http://java.sun.com/j2se/1.5.0/docs/guide/language/generics.html">
030     * generics</a>, this
031     * table is type safe.</p>
032     * <p>
033     * <b>CVS Info:</b>
034     * <table style="margin-left:2em">
035     *   <tr><td>$Revision: 161 $</td></tr>
036     *   <tr><td>$Date: 2006-12-15 11:48:34 -0700 (Fri, 15 Dec 2006) $</td></tr>
037     *   <tr><td>$Author: btruitt $</td></tr>
038     * </table></p>
039     *  
040     * @author David M. Harland
041     * @since 2006-07-31
042     */
043    public class LookupTable<K extends Comparable<K>, V>
044      implements Cloneable
045    {
046      private SortedMap<K, V> table;
047    
048      /**
049       * Creates a new, empty table, sorted according to the keys' natural order.
050       */
051      public LookupTable()
052      {
053        table = new TreeMap<K, V>();
054      }
055      
056      /**
057       * Constructs a new, empty table, sorted according to the given comparator.
058       * 
059       * @param c the comparator that will be used to sort this map. A <i>null</i>
060       *          value indicates that the keys' natural ordering should be used.
061       */
062      public LookupTable(Comparator<? super K> c)
063      {
064        table = new TreeMap<K, V>(c);
065      }
066    
067      /**
068       * Removes all entries from this table. 
069       */
070      public void clear()
071      {
072        table.clear();
073      }
074      
075      /**
076       * Puts the given key / value pair into this table.
077       * @param key key with which the specified value is to be associated.
078       * @param value value to be associated with the specified key.
079       * @return the value that had been previously mapped to {@code key}.
080       */
081      public V put(K key, V value)
082      {
083        return table.put(key, value);
084      }
085      
086      /**
087       * Copies all the mappings from {@code map} into this table.
088       * @param map a source of key / value pairs.
089       */
090      public void putAll(Map<K, V> map)
091      {
092        table.putAll(map);
093      }
094      
095      /**
096       * Removes the entry whose key is {@code key} from this table, if present.
097       * 
098       * @param key the key whose entry should be removed from this table.
099       * 
100       * @return the value that had been associated with {@code key}, or 
101       *         <i>null</i> if this table had no entry for {@code key}.
102       */
103      public V remove(K key)
104      {
105        return table.remove(key);
106      }
107      
108      /**
109       * Removes all entries in this table whose value is {@code value}.
110       * @param value the value to be removed entirely from this table.
111       * @return the number of entries that were removed from this table.
112       */
113      public int removeValue(V value)
114      {
115        int removals = 0;
116        
117        for (K key : getKeySetFor(value))
118        {
119          if (remove(key).equals(value))
120            removals++;
121        }
122          
123        return removals;
124      }
125      
126      /**
127       * Returns <i>true</i> if this table contains an entry whose key is
128       * {@code key}.
129       * @param key a key that may be present in this table.
130       * @return <i>true</i> if this table contains an entry whose key is
131       *         {@code key}.
132       */
133      public boolean containsKey(K key)
134      {
135        return table.containsKey(key);
136      }
137      
138      /**
139       * Returns <i>true</i> if this table contains one or more entries whose value
140       * is {@code value}.
141       * @param value a value that may be present in this table.
142       * @return <i>true</i> if this table contains one or more entries whose value
143       *         is {@code value}.
144       */
145      public boolean containsValue(V value)
146      {
147        return table.containsValue(value);
148      }
149      
150      /**
151       * Returns the value associated with the first key that is less than or
152       * equal to {@code key}.  The keys of this table are ordered according
153       * to the compartor specified during the construction of this table.
154       * If no comparator was so specified, then the keys are in their
155       * natural order.
156       * <p>
157       * A return value of <i>null</i> means either that {@code key} is less
158       * than the first key in this table, or that the greatest key that is
159       * not greater than {@code key} maps to a value of <i>null</i>.</p>
160       *  
161       * @param key the key for which a value is sought.
162       * 
163       * @return the value associated with the first key that is less than or
164       *         equal to {@code key}.
165       */
166      public V get(K key)
167      {
168        //Quick exit if key is below bottom of table
169        if (key.compareTo(table.firstKey()) < 0)
170          return null;
171    
172        //Initialize key to last key in case we run off top of table
173        K correctKey = table.lastKey();
174    
175        K prevKey = null;
176    
177        //Loop through table's keys, looking for first one that is greater than
178        //or equal to our key.  If equal, correctKey is the current key.
179        //If greater than, correctKey was the previous one.
180        for (K tableKey : table.keySet())
181        {
182          int keyCompare = key.compareTo(tableKey);
183          
184          if (keyCompare < 1)
185          {
186            correctKey = (keyCompare == 0) ? tableKey : prevKey;
187            break;
188          }
189          
190          prevKey = tableKey;
191        }
192        
193        return table.get(correctKey);
194      }
195      
196      /**
197       * Returns this table's set of keys.
198       * @return this table's set of keys.
199       */
200      public SortedSet<K> getKeySet()
201      {
202        TreeSet<K> result = new TreeSet<K>();
203        
204        result.addAll(table.keySet());
205        
206        return result;
207      }
208      
209      /**
210       * Returns the set of keys under which {@code value} is stored.
211       * @param value a value that might be stored in this table.
212       * @return the set of keys under which {@code value} is stored.
213       */
214      public SortedSet<K> getKeySetFor(V value)
215      {
216        TreeSet<K> result = new TreeSet<K>();
217    
218        for (Map.Entry<K, V> tableEntry : table.entrySet())
219        {
220          if (tableEntry.getValue().equals(value))
221            result.add(tableEntry.getKey());
222        }
223        
224        return result;
225      }
226      
227      /**
228       * Returns the number of entries in this table.
229       * @return the number of entries in this table.
230       */
231      public int size()
232      {
233        return table.size();
234      }
235      
236      /**
237       * Returns this table in the form of a sorted map.
238       * The keys and values of the map are references to the keys and
239       * values of this table.
240       * 
241       * @return a sorted map expression of this lookup table.
242       */
243      public SortedMap<K,V> toMap()
244      {
245        return new TreeMap<K,V>(table);
246      }
247    
248      /**
249       * Returns a text representation of this table.
250       * The form of the returned text is<pre>
251       * 
252       *   (key1.toString, value1.toString)
253       *   ...
254       *   (keyN.toString, valueN.toString)</pre>
255       *   
256       * @return a text representation of this table.
257       */
258      public String toString()
259      {
260        final String EOL = System.getProperty("line.separator");
261        
262        StringBuilder buff = new StringBuilder(EOL);
263    
264        for (K key : getKeySet())
265          buff.append('(').append(key.toString())
266              .append(", ").append(get(key).toString())
267              .append(')').append(EOL);
268        
269        return buff.toString();
270      }
271      
272      /**
273       * Returns a shallow copy of this lookup table.
274       * The returned table is distinct from this one, but shares the actual
275       * instances of the keys and values with this table.
276       * <p>
277       * If anything goes wrong during the cloning procedure,
278       * a {@code RuntimeException} will be thrown.</p>
279       */
280      @SuppressWarnings("unchecked")
281      public LookupTable<K,V> clone()
282      {
283        LookupTable<K,V> clone;
284        
285        try
286        {
287          clone = (LookupTable<K,V>)super.clone();
288          
289          clone.table = this.toMap();
290        }
291        catch (Exception ex)
292        {
293          throw new RuntimeException(ex);
294        }
295      
296        return clone;
297      }
298      
299      /**
300       * Returns <i>true</i> if {@code o} is equal to this table.
301       * See {@link java.util.AbstractMap#equals(Object)} for a
302       * description on how the comparison is performed.
303       */
304      @SuppressWarnings("unchecked")
305      public boolean equals(Object o)
306      {
307        //Quick exit if o is null
308        if (o == null)
309          return false;
310        
311        //Quick exit if o is this
312        if (o == this)
313          return true;
314        
315        //Quick exit if classes are different
316        if (!o.getClass().equals(this.getClass()))
317          return false;
318        
319        LookupTable<K,V> other = (LookupTable<K,V>)o;
320        
321        //Compare the tables
322        return this.table.equals(other.table);
323      }
324    
325      /**
326       * Returns a hash code value for this table.
327       * See {@link java.util.AbstractMap#hashCode()} for a
328       * description on how the comparison is performed.
329       */
330      public int hashCode()
331      {
332        return table.hashCode();
333      }
334    
335      //Here for quick manual test
336      /*
337      public static void main(String[] args)
338      {
339        LookupTable<Integer, String> table = new LookupTable<Integer, String>();
340        
341        table.put( 0, "F");
342        table.put(60, "D");
343        table.put(70, "C");
344        table.put(80, "B");
345        table.put(90, "A");
346        
347        System.out.println();
348        System.out.println("Key Set:  " + table.getKeySet());
349        System.out.println("ValueSet: " + table.toMap().values());
350        
351        System.out.println();
352        System.out.println("A score of < 0 gives you a grade of " + table.get(-10));
353        for (int i=0; i < 20; i++)
354        {
355          //Random score w/ bias towards score >= 60
356          int score = (int)(Math.random() * 100.0);
357          if (score < 60)
358            score = (int)(Math.random() * 100.0);
359          
360          String grade = table.get(score);
361          System.out.println("A score of "+score+" gives you a grade of "+grade);
362        }
363        System.out.println();
364      }
365      */
366    }