From 9c5579b2338d4e2dab08b596334fa60949938bdd Mon Sep 17 00:00:00 2001 From: Ben Fry Date: Mon, 11 Apr 2016 17:03:59 -0400 Subject: [PATCH] several fixes, cleanups, and speed improvements to dictionary classes --- core/src/processing/data/FloatDict.java | 300 ++++++++--------------- core/src/processing/data/IntDict.java | 180 +++++++------- core/src/processing/data/StringDict.java | 100 ++++---- core/todo.txt | 9 + 4 files changed, 244 insertions(+), 345 deletions(-) diff --git a/core/src/processing/data/FloatDict.java b/core/src/processing/data/FloatDict.java index cb06c2b15..5fcc50737 100644 --- a/core/src/processing/data/FloatDict.java +++ b/core/src/processing/data/FloatDict.java @@ -69,6 +69,22 @@ public class FloatDict { } + /** + * @nowebref + */ + public FloatDict(String[] keys, float[] values) { + if (keys.length != values.length) { + throw new IllegalArgumentException("key and value arrays must be the same length"); + } + this.keys = keys; + this.values = values; + count = keys.length; + for (int i = 0; i < count; i++) { + indices.put(keys[i], i); + } + } + + /** * Constructor to allow (more intuitive) inline initialization, e.g.: *
@@ -90,21 +106,6 @@ public class FloatDict {
   }
 
 
-  /**
-   * @nowebref
-   */
-  public FloatDict(String[] keys, float[] values) {
-    if (keys.length != values.length) {
-      throw new IllegalArgumentException("key and value arrays must be the same length");
-    }
-    this.keys = keys;
-    this.values = values;
-    count = keys.length;
-    for (int i = 0; i < count; i++) {
-      indices.put(keys[i], i);
-    }
-  }
-
   /**
    * @webref floatdict:method
    * @brief Returns the number of key/value pairs
@@ -139,71 +140,35 @@ public class FloatDict {
   }
 
 
-//  /**
-//   * Return the internal array being used to store the keys. Allocated but
-//   * unused entries will be removed. This array should not be modified.
-//   */
-//  public String[] keys() {
-//    crop();
-//    return keys;
-//  }
-
-  /**
-   * @webref floatdict:method
-   * @brief Return the internal array being used to store the keys
-   */
   public Iterable keys() {
     return new Iterable() {
 
       @Override
       public Iterator iterator() {
-        return new Iterator() {
-          int index = -1;
-
-          public void remove() {
-            removeIndex(index);
-          }
-
-          public String next() {
-            return key(++index);
-          }
-
-          public boolean hasNext() {
-            return index+1 < size();
-          }
-        };
+        return keyIterator();
       }
     };
   }
+  
+  
+  // Use this to iterate when you want to be able to remove elements along the way
+  public Iterator keyIterator() {
+    return new Iterator() {
+      int index = -1;
 
+      public void remove() {
+        removeIndex(index);
+      }
 
-  /*
-  static class KeyIterator implements Iterator {
-    FloatHash parent;
-    int index;
+      public String next() {
+        return key(++index);
+      }
 
-    public KeyIterator(FloatHash parent) {
-      this.parent = parent;
-      index = -1;
-    }
-
-    public void remove() {
-      parent.removeIndex(index);
-    }
-
-    public String next() {
-      return parent.key(++index);
-    }
-
-    public boolean hasNext() {
-      return index+1 < parent.size();
-    }
-
-    public void reset() {
-      index = -1;
-    }
+      public boolean hasNext() {
+        return index+1 < size();
+      }
+    };
   }
-  */
 
 
   /**
@@ -213,6 +178,7 @@ public class FloatDict {
    * @brief Return a copy of the internal keys array
    */
   public String[] keyArray() {
+    crop();
     return keyArray(null);
   }
 
@@ -231,11 +197,6 @@ public class FloatDict {
   }
 
 
-//  public float[] values() {
-//    crop();
-//    return values;
-//  }
-
   /**
    * @webref floatdict:method
    * @brief Return the internal array being used to store the values
@@ -245,21 +206,26 @@ public class FloatDict {
 
       @Override
       public Iterator iterator() {
-        return new Iterator() {
-          int index = -1;
+        return valueIterator();
+      }
+    };
+  }
+  
+  
+  public Iterator valueIterator() {
+    return new Iterator() {
+      int index = -1;
 
-          public void remove() {
-            removeIndex(index);
-          }
+      public void remove() {
+        removeIndex(index);
+      }
 
-          public Float next() {
-            return value(++index);
-          }
+      public Float next() {
+        return value(++index);
+      }
 
-          public boolean hasNext() {
-            return index+1 < size();
-          }
-        };
+      public boolean hasNext() {
+        return index+1 < size();
       }
     };
   }
@@ -272,6 +238,7 @@ public class FloatDict {
    * @brief Create a new array and copy each of the values into it
    */
   public float[] valueArray() {
+    crop();
     return valueArray(null);
   }
 
@@ -337,18 +304,6 @@ public class FloatDict {
   }
 
 
-//  /** Increase the value of a specific key by 1. */
-//  public void inc(String key) {
-//    inc(key, 1);
-////    int index = index(key);
-////    if (index == -1) {
-////      create(key, 1);
-////    } else {
-////      values[index]++;
-////    }
-//  }
-
-
   /**
    * @webref floatdict:method
    * @brief Add to a value
@@ -363,12 +318,6 @@ public class FloatDict {
   }
 
 
-//  /** Decrease the value of a key by 1. */
-//  public void dec(String key) {
-//    inc(key, -1);
-//  }
-
-
   /**
    * @webref floatdict:method
    * @brief Subtract from a value
@@ -417,8 +366,10 @@ public class FloatDict {
    * @brief Return the smallest value
    */
   public int minIndex() {
-    checkMinMax("minIndex");
-    // Will still return NaN if there is 1 or more entries, and they're all NaN
+    //checkMinMax("minIndex");
+    if (count == 0) return -1;
+    
+    // Will still return NaN if there are 1 or more entries, and they're all NaN
     float m = Float.NaN;
     int mi = -1;
     for (int i = 0; i < count; i++) {
@@ -430,7 +381,7 @@ public class FloatDict {
         // calculate the rest
         for (int j = i+1; j < count; j++) {
           float d = values[j];
-          if (!Float.isNaN(d) && (d < m)) {
+          if ((d == d) && (d < m)) {
             m = values[j];
             mi = j;
           }
@@ -442,6 +393,7 @@ public class FloatDict {
   }
 
 
+  // return the key for the minimum value
   public String minKey() {
     checkMinMax("minKey");
     int index = minIndex();
@@ -452,6 +404,7 @@ public class FloatDict {
   }
 
 
+  // return the minimum value, or throw an error if there are no values
   public float minValue() {
     checkMinMax("minValue");
     int index = minIndex();
@@ -468,7 +421,10 @@ public class FloatDict {
    */
   // The index of the entry that has the max value. Reference above is incorrect.
   public int maxIndex() {
-    checkMinMax("maxIndex");
+    //checkMinMax("maxIndex");
+    if (count == 0) {
+      return -1;
+    }
     // Will still return NaN if there is 1 or more entries, and they're all NaN
     float m = Float.NaN;
     int mi = -1;
@@ -493,9 +449,9 @@ public class FloatDict {
   }
 
 
-  /** The key for a max value, or null if everything is NaN (no max). */
+  /** The key for a max value; null if empty or everything is NaN (no max). */
   public String maxKey() {
-    checkMinMax("maxKey");
+    //checkMinMax("maxKey");
     int index = maxIndex();
     if (index == -1) {
       return null;
@@ -504,9 +460,9 @@ public class FloatDict {
   }
 
 
-  /** The max value. (Or NaN if they're all NaN.) */
+  /** The max value. (Or NaN if no entries or they're all NaN.) */
   public float maxValue() {
-    checkMinMax("maxValue");
+    //checkMinMax("maxValue");
     int index = maxIndex();
     if (index == -1) {
       return Float.NaN;
@@ -573,24 +529,11 @@ public class FloatDict {
     keys[b] = tkey;
     values[b] = tvalue;
 
-    indices.put(keys[a], Integer.valueOf(a));
-    indices.put(keys[b], Integer.valueOf(b));
+//    indices.put(keys[a], Integer.valueOf(a));
+//    indices.put(keys[b], Integer.valueOf(b));
   }
 
 
-//  abstract class InternalSort extends Sort {
-//    @Override
-//    public int size() {
-//      return count;
-//    }
-//
-//    @Override
-//    public void swap(int a, int b) {
-//      FloatHash.this.swap(a, b);
-//    }
-//  }
-
-
   /**
    * Sort the keys alphabetically (ignoring case). Uses the value as a
    * tie-breaker (only really possible with a key that has a case change).
@@ -599,36 +542,16 @@ public class FloatDict {
    * @brief Sort the keys alphabetically
    */
   public void sortKeys() {
-    sortImpl(true, false);
-//    new InternalSort() {
-//      @Override
-//      public float compare(int a, int b) {
-//        int result = keys[a].compareToIgnoreCase(keys[b]);
-//        if (result != 0) {
-//          return result;
-//        }
-//        return values[b] - values[a];
-//      }
-//    }.run();
+    sortImpl(true, false, true);
   }
 
 
   /**
    * @webref floatdict:method
-   * @brief Sort the keys alphabetially in reverse
+   * @brief Sort the keys alphabetically in reverse
    */
   public void sortKeysReverse() {
-    sortImpl(true, true);
-//    new InternalSort() {
-//      @Override
-//      public float compare(int a, int b) {
-//        int result = keys[b].compareToIgnoreCase(keys[a]);
-//        if (result != 0) {
-//          return result;
-//        }
-//        return values[a] - values[b];
-//      }
-//    }.run();
+    sortImpl(true, true, true);
   }
 
 
@@ -639,13 +562,17 @@ public class FloatDict {
    * @brief Sort by values in ascending order
    */
   public void sortValues() {
-    sortImpl(false, false);
-//    new InternalSort() {
-//      @Override
-//      public float compare(int a, int b) {
-//
-//      }
-//    }.run();
+    sortValues(true);
+  }
+
+
+  /**
+   * Set true to ensure that the order returned is identical. Slightly
+   * slower because the tie-breaker for identical values compares the keys.
+   * @param stable
+   */
+  public void sortValues(boolean stable) {
+    sortImpl(false, false, stable);
   }
 
 
@@ -654,50 +581,17 @@ public class FloatDict {
    * @brief Sort by values in descending order
    */
   public void sortValuesReverse() {
-    sortImpl(false, true);
-//    new InternalSort() {
-//      @Override
-//      public float compare(int a, int b) {
-//        float diff = values[b] - values[a];
-//        if (diff == 0 && keys[a] != null && keys[b] != null) {
-//          diff = keys[a].compareToIgnoreCase(keys[b]);
-//        }
-//        return descending ? diff : -diff;
-//      }
-//    }.run();
+    sortValuesReverse(true);
   }
 
 
-//  // ascending puts the largest value at the end
-//  // descending puts the largest value at 0
-//  public void sortValues(final boolean descending, final boolean tiebreaker) {
-//    Sort s = new Sort() {
-//      @Override
-//      public int size() {
-//        return count;
-//      }
-//
-//      @Override
-//      public float compare(int a, int b) {
-//        float diff = values[b] - values[a];
-//        if (tiebreaker) {
-//          if (diff == 0) {
-//            diff = keys[a].compareToIgnoreCase(keys[b]);
-//          }
-//        }
-//        return descending ? diff : -diff;
-//      }
-//
-//      @Override
-//      public void swap(int a, int b) {
-//        FloatHash.this.swap(a, b);
-//      }
-//    };
-//    s.run();
-//  }
+  public void sortValuesReverse(boolean stable) {
+    sortImpl(false, true, stable);
+  }
 
 
-  protected void sortImpl(final boolean useKeys, final boolean reverse) {
+  protected void sortImpl(final boolean useKeys, final boolean reverse, 
+                          final boolean stable) {
     Sort s = new Sort() {
       @Override
       public int size() {
@@ -731,11 +625,11 @@ public class FloatDict {
         if (useKeys) {
           diff = keys[a].compareToIgnoreCase(keys[b]);
           if (diff == 0) {
-            return values[a] - values[b];
+            diff = values[a] - values[b];
           }
         } else {  // sort values
           diff = values[a] - values[b];
-          if (diff == 0) {
+          if (diff == 0 && stable) {
             diff = keys[a].compareToIgnoreCase(keys[b]);
           }
         }
@@ -748,18 +642,24 @@ public class FloatDict {
       }
     };
     s.run();
+
+    // Set the indices after sort/swaps (performance fix 160411)
+    indices = new HashMap();
+    for (int i = 0; i < count; i++) {
+      indices.put(keys[i], i);
+    }
   }
 
 
   /**
    * Sum all of the values in this dictionary, then return a new FloatDict of
    * each key, divided by the total sum. The total for all values will be ~1.0.
-   * @return a Dict with the original keys, mapped to their pct of the total
+   * @return a FloatDict with the original keys, mapped to their pct of the total
    */
   public FloatDict getPercent() {
     double sum = 0;
-    for (float value : valueArray()) {
-      sum += value;
+    for (int i = 0; i < count; i++) {
+      sum += values[i];
     }
     FloatDict outgoing = new FloatDict();
     for (int i = 0; i < size(); i++) {
diff --git a/core/src/processing/data/IntDict.java b/core/src/processing/data/IntDict.java
index d33f22130..316f4a73d 100644
--- a/core/src/processing/data/IntDict.java
+++ b/core/src/processing/data/IntDict.java
@@ -133,74 +133,20 @@ public class IntDict {
   }
 
 
-//  private void crop() {
-//    if (count != keys.length) {
-//      keys = PApplet.subset(keys, 0, count);
-//      values = PApplet.subset(values, 0, count);
-//    }
-//  }
+  protected void crop() {
+    if (count != keys.length) {
+      keys = PApplet.subset(keys, 0, count);
+      values = PApplet.subset(values, 0, count);
+    }
+  }
 
 
-  /**
-   * Return the internal array being used to store the keys. Allocated but
-   * unused entries will be removed. This array should not be modified.
-   *
-   * @webref intdict:method
-   * @brief Return the internal array being used to store the keys
-   */
-//  public String[] keys() {
-//    crop();
-//    return keys;
-//  }
-
-
-//  public Iterable keys() {
-//    return new Iterable() {
-//
-//      @Override
-//      public Iterator iterator() {
-//        return new Iterator() {
-//          int index = -1;
-//
-//          public void remove() {
-//            removeIndex(index);
-//          }
-//
-//          public String next() {
-//            return key(++index);
-//          }
-//
-//          public boolean hasNext() {
-//            return index+1 < size();
-//          }
-//        };
-//      }
-//    };
-//  }
-
-
-  // Use this with 'for' loops
   public Iterable keys() {
     return new Iterable() {
 
       @Override
       public Iterator iterator() {
         return keyIterator();
-//        return new Iterator() {
-//          int index = -1;
-//
-//          public void remove() {
-//            removeIndex(index);
-//          }
-//
-//          public String next() {
-//            return key(++index);
-//          }
-//
-//          public boolean hasNext() {
-//            return index+1 < size();
-//          }
-//        };
       }
     };
   }
@@ -233,6 +179,7 @@ public class IntDict {
    * @brief Return a copy of the internal keys array
    */
   public String[] keyArray() {
+    crop();
     return keyArray(null);
   }
 
@@ -292,6 +239,7 @@ public class IntDict {
    * @brief Create a new array and copy each of the values into it
    */
   public int[] valueArray() {
+    crop();
     return valueArray(null);
   }
 
@@ -428,7 +376,9 @@ public class IntDict {
 
   // return the index of the minimum value
   public int minIndex() {
-    checkMinMax("minIndex");
+    //checkMinMax("minIndex");
+    if (count == 0) return -1;
+
     int index = 0;
     int value = values[0];
     for (int i = 1; i < count; i++) {
@@ -441,23 +391,30 @@ public class IntDict {
   }
 
 
-  // return the minimum value
+  // return the key for the minimum value
+  public String minKey() {
+    checkMinMax("minKey");
+    int index = minIndex();
+    if (index == -1) {
+      return null;
+    }
+    return keys[index];
+  }
+
+
+  // return the minimum value, or throw an error if there are no values
   public int minValue() {
     checkMinMax("minValue");
     return values[minIndex()];
   }
 
 
-  // return the key for the minimum value
-  public String minKey() {
-    checkMinMax("minKey");
-    return keys[minIndex()];
-  }
-
-
   // return the index of the max value
   public int maxIndex() {
-    checkMinMax("maxIndex");
+    //checkMinMax("maxIndex");
+    if (count == 0) {
+      return -1;
+    }
     int index = 0;
     int value = values[0];
     for (int i = 1; i < count; i++) {
@@ -470,17 +427,21 @@ public class IntDict {
   }
 
 
-  // return the maximum value
-  public int maxValue() {
-    checkMinMax("maxValue");
-    return values[maxIndex()];
+  /** return the key corresponding to the maximum value or null if no entries */
+  public String maxKey() {
+    //checkMinMax("maxKey");
+    int index = maxIndex();
+    if (index == -1) {
+      return null;
+    }
+    return keys[index];
   }
 
 
-  // return the key corresponding to the maximum value
-  public String maxKey() {
-    checkMinMax("maxKey");
-    return keys[maxIndex()];
+  // return the maximum value or throw an error if zero length
+  public int maxValue() {
+    checkMinMax("maxIndex");
+    return values[maxIndex()];
   }
 
 
@@ -541,8 +502,8 @@ public class IntDict {
     keys[b] = tkey;
     values[b] = tvalue;
 
-    indices.put(keys[a], Integer.valueOf(a));
-    indices.put(keys[b], Integer.valueOf(b));
+//    indices.put(keys[a], Integer.valueOf(a));
+//    indices.put(keys[b], Integer.valueOf(b));
   }
 
 
@@ -554,7 +515,7 @@ public class IntDict {
    * @brief Sort the keys alphabetically
    */
   public void sortKeys() {
-    sortImpl(true, false);
+    sortImpl(true, false, true);
   }
 
   /**
@@ -562,10 +523,10 @@ public class IntDict {
    * tie-breaker (only really possible with a key that has a case change).
    *
    * @webref intdict:method
-   * @brief Sort the keys alphabetially in reverse
+   * @brief Sort the keys alphabetically in reverse
    */
   public void sortKeysReverse() {
-    sortImpl(true, true);
+    sortImpl(true, true, true);
   }
 
 
@@ -576,9 +537,20 @@ public class IntDict {
    * @brief Sort by values in ascending order
    */
   public void sortValues() {
-    sortImpl(false, false);
+    sortValues(true);
   }
 
+
+  /**
+   * Set true to ensure that the order returned is identical. Slightly
+   * slower because the tie-breaker for identical values compares the keys.
+   * @param stable
+   */
+  public void sortValues(boolean stable) {
+    sortImpl(false, false, stable);
+  }
+
+
   /**
    * Sort by values in descending order. The largest value will be at [0].
    *
@@ -586,11 +558,17 @@ public class IntDict {
    * @brief Sort by values in descending order
    */
   public void sortValuesReverse() {
-    sortImpl(false, true);
+    sortValuesReverse(true);
   }
 
 
-  protected void sortImpl(final boolean useKeys, final boolean reverse) {
+  public void sortValuesReverse(boolean stable) {
+    sortImpl(false, true, stable);
+  }
+
+
+  protected void sortImpl(final boolean useKeys, final boolean reverse,
+                          final boolean stable) {
     Sort s = new Sort() {
       @Override
       public int size() {
@@ -603,11 +581,11 @@ public class IntDict {
         if (useKeys) {
           diff = keys[a].compareToIgnoreCase(keys[b]);
           if (diff == 0) {
-            return values[a] - values[b];
+            diff = values[a] - values[b];
           }
         } else {  // sort values
           diff = values[a] - values[b];
-          if (diff == 0) {
+          if (diff == 0 && stable) {
             diff = keys[a].compareToIgnoreCase(keys[b]);
           }
         }
@@ -620,18 +598,24 @@ public class IntDict {
       }
     };
     s.run();
+
+    // Set the indices after sort/swaps (performance fix 160411)
+    indices = new HashMap();
+    for (int i = 0; i < count; i++) {
+      indices.put(keys[i], i);
+    }
   }
 
 
   /**
    * Sum all of the values in this dictionary, then return a new FloatDict of
    * each key, divided by the total sum. The total for all values will be ~1.0.
-   * @return a Dict with the original keys, mapped to their pct of the total
+   * @return an IntDict with the original keys, mapped to their pct of the total
    */
   public FloatDict getPercent() {
     double sum = 0;
-    for (int value : valueArray()) {
-      sum += value;
+    for (int i = 0; i < count; i++) {
+      sum += values[i];
     }
     FloatDict outgoing = new FloatDict();
     for (int i = 0; i < size(); i++) {
@@ -655,6 +639,13 @@ public class IntDict {
   }
 
 
+  public void print() {
+    for (int i = 0; i < size(); i++) {
+      System.out.println(keys[i] + " = " + values[i]);
+    }
+  }
+
+
   /**
    * Write tab-delimited entries out to
    * @param writer
@@ -667,13 +658,6 @@ public class IntDict {
   }
 
 
-  public void print() {
-    for (int i = 0; i < size(); i++) {
-      System.out.println(keys[i] + " = " + values[i]);
-    }
-  }
-
-
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder();
diff --git a/core/src/processing/data/StringDict.java b/core/src/processing/data/StringDict.java
index 69fb4b716..330d362c9 100644
--- a/core/src/processing/data/StringDict.java
+++ b/core/src/processing/data/StringDict.java
@@ -142,39 +142,32 @@ public class StringDict {
   }
 
 
-//  /**
-//   * Return the internal array being used to store the keys. Allocated but
-//   * unused entries will be removed. This array should not be modified.
-//   */
-//  public String[] keys() {
-//    crop();
-//    return keys;
-//  }
-
-  /**
-   * @webref stringdict:method
-   * @brief Return the internal array being used to store the keys
-   */
   public Iterable keys() {
     return new Iterable() {
 
       @Override
       public Iterator iterator() {
-        return new Iterator() {
-          int index = -1;
+        return keyIterator();
+      }
+    };
+  }
 
-          public void remove() {
-            removeIndex(index);
-          }
 
-          public String next() {
-            return key(++index);
-          }
+  // Use this to iterate when you want to be able to remove elements along the way
+  public Iterator keyIterator() {
+    return new Iterator() {
+      int index = -1;
 
-          public boolean hasNext() {
-            return index+1 < size();
-          }
-        };
+      public void remove() {
+        removeIndex(index);
+      }
+
+      public String next() {
+        return key(++index);
+      }
+
+      public boolean hasNext() {
+        return index+1 < size();
       }
     };
   }
@@ -187,6 +180,7 @@ public class StringDict {
    * @brief Return a copy of the internal keys array
    */
   public String[] keyArray() {
+    crop();
     return keyArray(null);
   }
 
@@ -213,21 +207,26 @@ public class StringDict {
 
       @Override
       public Iterator iterator() {
-        return new Iterator() {
-          int index = -1;
+        return valueIterator();
+      }
+    };
+  }
 
-          public void remove() {
-            removeIndex(index);
-          }
 
-          public String next() {
-            return value(++index);
-          }
+  public Iterator valueIterator() {
+    return new Iterator() {
+      int index = -1;
 
-          public boolean hasNext() {
-            return index+1 < size();
-          }
-        };
+      public void remove() {
+        removeIndex(index);
+      }
+
+      public String next() {
+        return value(++index);
+      }
+
+      public boolean hasNext() {
+        return index+1 < size();
       }
     };
   }
@@ -240,6 +239,7 @@ public class StringDict {
    * @brief Create a new array and copy each of the values into it
    */
   public String[] valueArray() {
+    crop();
     return valueArray(null);
   }
 
@@ -357,8 +357,8 @@ public class StringDict {
     keys[b] = tkey;
     values[b] = tvalue;
 
-    indices.put(keys[a], Integer.valueOf(a));
-    indices.put(keys[b], Integer.valueOf(b));
+//    indices.put(keys[a], Integer.valueOf(a));
+//    indices.put(keys[b], Integer.valueOf(b));
   }
 
 
@@ -375,7 +375,7 @@ public class StringDict {
 
   /**
    * @webref stringdict:method
-   * @brief Sort the keys alphabetially in reverse
+   * @brief Sort the keys alphabetically in reverse
    */
   public void sortKeysReverse() {
     sortImpl(true, true);
@@ -432,6 +432,12 @@ public class StringDict {
       }
     };
     s.run();
+
+    // Set the indices after sort/swaps (performance fix 160411)
+    indices = new HashMap();
+    for (int i = 0; i < count; i++) {
+      indices.put(keys[i], i);
+    }
   }
 
 
@@ -448,6 +454,13 @@ public class StringDict {
   }
 
 
+  public void print() {
+    for (int i = 0; i < size(); i++) {
+      System.out.println(keys[i] + " = " + values[i]);
+    }
+  }
+
+
   /**
    * Write tab-delimited entries out to
    * @param writer
@@ -460,13 +473,6 @@ public class StringDict {
   }
 
 
-  public void print() {
-    for (int i = 0; i < size(); i++) {
-      System.out.println(keys[i] + " = " + values[i]);
-    }
-  }
-
-
   @Override
   public String toString() {
     StringBuilder sb = new StringBuilder();
diff --git a/core/todo.txt b/core/todo.txt
index 87ac0fbba..744241f2c 100644
--- a/core/todo.txt
+++ b/core/todo.txt
@@ -1,4 +1,13 @@
 0249 (3.0.3)
+X Float/IntDict changes
+X   minIndex() and maxIndex() return -1 when count is zero (rather than ex)
+X   bug fix to reverse sorts
+X   2x performance increase for sorting
+X   added optional "stable" parameter for sorting by values
+X     set to false for higher performance
+X   call crop() in keyArray() and valueArray() functions
+X     they're duplicates, their length is an implementation detail
+X   normalize features and error handling between all of them
 
 _ add push() and pop() methods to mirror js?