1 package org.unicode.cldr.draft; 2 3 /** 4 * Class that permits quick edits; does insertions/deletions much faster, in general, than StringBuilder. This is based 5 * on the fact 6 * that such changes are generally not random; instead they tend to happen more often in either nearby or successive 7 * locations. 8 * The structure of the class keeps 2 buffers; one for the front and one for the end. 9 * 10 * @author markdavis 11 */ 12 // TODO investigate whether keeping the future in reverse order is the right approach in terms of performance. 13 public class GapString implements CharSequence { 14 private static final int GROWTH_INCREMENT = 15; 15 private static final int GROWTH_FACTOR = 3; 16 private char[] buffer = new char[10]; 17 private int pastLength = 0; 18 private int gapLength = 10; 19 GapString()20 public GapString() { 21 } 22 GapString(CharSequence s)23 public GapString(CharSequence s) { 24 insert(0, s); 25 } 26 27 @Override charAt(int index)28 public char charAt(int index) { 29 try { 30 return buffer[index < pastLength ? index : index + gapLength]; 31 } catch (ArrayIndexOutOfBoundsException e) { 32 throw new StringIndexOutOfBoundsException(index); 33 } 34 } 35 36 @Override length()37 public int length() { 38 return buffer.length - gapLength; 39 } 40 41 @Override subSequence(int start, int end)42 public CharSequence subSequence(int start, int end) { 43 // TODO optimize this 44 return toString().subSequence(start, end); 45 } 46 47 @Override toString()48 public String toString() { 49 StringBuilder b = new StringBuilder(); 50 // check to see whether second argument is length or count 51 b.append(buffer, 0, pastLength); 52 final int futureStart = pastLength + gapLength; 53 b.append(buffer, futureStart, buffer.length - futureStart); 54 return b.toString(); 55 } 56 insert(int index, CharSequence s, int start, int end)57 public GapString insert(int index, CharSequence s, int start, int end) { 58 if (s instanceof String) { 59 return insert(index, (String) s, start, end); 60 } 61 final int gapNeeded = end - start; 62 if (pastLength != index) { 63 shiftTo(index, gapNeeded); 64 } else { 65 final int growthNeeded = gapNeeded - gapLength; 66 if (growthNeeded > 0) { 67 growToLength((buffer.length + growthNeeded) * GROWTH_FACTOR + GROWTH_INCREMENT); 68 } 69 } 70 for (int i = start; i < end; ++i) { 71 buffer[pastLength++] = s.charAt(i); 72 } 73 gapLength -= gapNeeded; 74 return this; 75 } 76 insert(int index, CharSequence s)77 public GapString insert(int index, CharSequence s) { 78 return insert(index, s, 0, s.length()); 79 } 80 insert(int index, String s)81 public GapString insert(int index, String s) { 82 return insert(index, s, 0, s.length()); 83 } 84 insert(int index, String s, int start, int end)85 public GapString insert(int index, String s, int start, int end) { 86 final int gapNeeded = end - start; 87 if (pastLength != index) { 88 shiftTo(index, gapNeeded); 89 } else { 90 final int growthNeeded = gapNeeded - gapLength; 91 if (growthNeeded > 0) { 92 growToLength((buffer.length + growthNeeded) * GROWTH_FACTOR + GROWTH_INCREMENT); 93 } 94 } 95 s.getChars(start, end, buffer, pastLength); 96 pastLength += (end - start); 97 gapLength -= gapNeeded; 98 return this; 99 } 100 insert(int index, char[] s)101 public GapString insert(int index, char[] s) { 102 return insert(index, s, 0, s.length); 103 } 104 insert(int index, char[] s, int start, int end)105 public GapString insert(int index, char[] s, int start, int end) { 106 final int gapNeeded = end - start; 107 if (pastLength != index) { 108 shiftTo(index, gapNeeded); 109 } else { 110 final int growthNeeded = gapNeeded - gapLength; 111 if (growthNeeded > 0) { 112 growToLength((buffer.length + growthNeeded) * GROWTH_FACTOR + GROWTH_INCREMENT); 113 } 114 } 115 System.arraycopy(s, start, buffer, pastLength, gapNeeded); 116 pastLength += (end - start); 117 gapLength -= gapNeeded; 118 return this; 119 } 120 insert(int index, char s)121 public GapString insert(int index, char s) { 122 if (pastLength != index || pastLength < index + 1) { 123 shiftTo(index, 1); 124 } 125 buffer[pastLength++] = s; 126 gapLength -= 1; 127 return this; 128 } 129 append(boolean x)130 public GapString append(boolean x) { 131 return insert(length(), String.valueOf(x)); 132 } 133 append(char x)134 public GapString append(char x) { 135 return insert(length(), x); 136 } 137 append(char[] x)138 public GapString append(char[] x) { 139 return insert(length(), x); 140 } 141 append(char[] x, int start, int end)142 public GapString append(char[] x, int start, int end) { 143 return insert(length(), x, start, end); 144 } 145 append(double x)146 public GapString append(double x) { 147 return insert(length(), String.valueOf(x)); 148 } 149 append(float x)150 public GapString append(float x) { 151 return insert(length(), String.valueOf(x)); 152 } 153 append(int x)154 public GapString append(int x) { 155 return insert(length(), String.valueOf(x)); 156 } 157 append(CharSequence x, int start, int end)158 public GapString append(CharSequence x, int start, int end) { 159 return insert(length(), x, start, end); 160 } 161 append(Object x)162 public GapString append(Object x) { 163 return insert(length(), String.valueOf(x)); 164 } 165 append(long x)166 public GapString append(long x) { 167 return insert(length(), String.valueOf(x)); 168 } 169 appendCodePoint(int x)170 public GapString appendCodePoint(int x) { 171 return insertCodePoint(length(), x); 172 } 173 insertCodePoint(int index, int x)174 private GapString insertCodePoint(int index, int x) { 175 if (x < 0x10000) { 176 return insert(index, (char) x); 177 } 178 return insert(index, Character.toChars(x)); // rare, so inefficiency doesn't matter 179 } 180 append(String s)181 public GapString append(String s) { 182 return insert(buffer.length - gapLength, s, 0, s.length()); 183 } 184 append(CharSequence string)185 public GapString append(CharSequence string) { 186 return insert(buffer.length - gapLength, string); 187 } 188 delete(int start, int end)189 public GapString delete(int start, int end) { 190 // if our deletion includes the gap, we can shortcut this 191 // we just reset the pastLength and gap 192 if (pastLength >= start && pastLength < end) { 193 pastLength = start; 194 } else { 195 // TODO There is a possible optimization, to only move enough to get to one end or the other. 196 // However, I don't know whether it would be worth it or not: have to test. 197 if (pastLength != start) { 198 shiftTo(start, 0); 199 } 200 } 201 gapLength += (end - start); 202 return this; 203 } 204 replace(int start, int end, CharSequence other, int otherStart, int otherEnd)205 public GapString replace(int start, int end, CharSequence other, int otherStart, int otherEnd) { 206 delete(start, end); 207 return insert(start, other, otherStart, otherEnd); 208 } 209 compact()210 public GapString compact() { 211 if (gapLength > 0) { 212 growToLength(buffer.length - gapLength); // remove any gap 213 } 214 return this; 215 } 216 217 @Override equals(Object other)218 public boolean equals(Object other) { 219 try { 220 return equals((CharSequence) other); 221 } catch (Exception e) { 222 return false; 223 } 224 } 225 equals(CharSequence other)226 public boolean equals(CharSequence other) { 227 final int len = buffer.length - gapLength; 228 if (other.length() != len) { 229 return false; 230 } 231 int i = 0; 232 for (; i < pastLength; ++i) { 233 if (buffer[i] != other.charAt(i)) { 234 return false; 235 } 236 } 237 int j = i; 238 i += gapLength; 239 for (; i < buffer.length; ++i) { 240 if (buffer[i] != other.charAt(j++)) { 241 return false; 242 } 243 } 244 return true; 245 } 246 247 @Override hashCode()248 public int hashCode() { 249 int result = 0; 250 int i = 0; 251 for (; i < pastLength; ++i) { 252 result = 37 * result + buffer[i]; 253 } 254 i += gapLength; 255 for (; i < buffer.length; ++i) { 256 result = 37 * result + buffer[i]; 257 } 258 return result; 259 } 260 261 // ======== PRIVATES =========== 262 263 /** 264 * This utility function just shifts the gap, so that it starts at newPastLength. The logical contents 265 * of the string are unchanged. 266 */ shiftTo(int newPastLength, int gapNeeded)267 private void shiftTo(int newPastLength, int gapNeeded) { 268 int growth = gapNeeded - gapLength; 269 if (growth > 0) { 270 growToLength((buffer.length + growth) * GROWTH_FACTOR + GROWTH_INCREMENT); 271 } 272 273 int newMinusOldPastLength = newPastLength - pastLength; 274 if (newMinusOldPastLength == 0) { 275 return; 276 } 277 if (newMinusOldPastLength > 0) { 278 System.arraycopy(buffer, pastLength + gapLength, buffer, pastLength, newMinusOldPastLength); 279 } else { 280 System.arraycopy(buffer, newPastLength, buffer, pastLength + gapLength + newMinusOldPastLength, 281 -newMinusOldPastLength); 282 } 283 pastLength = newPastLength; 284 } 285 286 /** 287 * This utility function just grows the gap (and thus the storage). The logical contents 288 * of the string are unchanged. 289 */ growToLength(int neededLength)290 private void growToLength(int neededLength) { 291 char[] temp = new char[neededLength]; 292 System.arraycopy(buffer, 0, temp, 0, pastLength); 293 final int futureStart = pastLength + gapLength; 294 final int futureLength = buffer.length - futureStart; 295 System.arraycopy(buffer, futureStart, temp, neededLength - futureLength, futureLength); 296 gapLength += neededLength - buffer.length; 297 buffer = temp; 298 } 299 300 } 301