package org.unicode.cldr.draft; /** * Class that permits quick edits; does insertions/deletions much faster, in general, than StringBuilder. This is based * on the fact * that such changes are generally not random; instead they tend to happen more often in either nearby or successive * locations. * The structure of the class keeps 2 buffers; one for the front and one for the end. * * @author markdavis */ // TODO investigate whether keeping the future in reverse order is the right approach in terms of performance. public class GapString implements CharSequence { private static final int GROWTH_INCREMENT = 15; private static final int GROWTH_FACTOR = 3; private char[] buffer = new char[10]; private int pastLength = 0; private int gapLength = 10; public GapString() { } public GapString(CharSequence s) { insert(0, s); } public char charAt(int index) { try { return buffer[index < pastLength ? index : index + gapLength]; } catch (ArrayIndexOutOfBoundsException e) { throw new StringIndexOutOfBoundsException(index); } } public int length() { return buffer.length - gapLength; } public CharSequence subSequence(int start, int end) { // TODO optimize this return toString().subSequence(start, end); } public String toString() { StringBuilder b = new StringBuilder(); // check to see whether second argument is length or count b.append(buffer, 0, pastLength); final int futureStart = pastLength + gapLength; b.append(buffer, futureStart, buffer.length - futureStart); return b.toString(); } public GapString insert(int index, CharSequence s, int start, int end) { if (s instanceof String) { return insert(index, (String) s, start, end); } final int gapNeeded = end - start; if (pastLength != index) { shiftTo(index, gapNeeded); } else { final int growthNeeded = gapNeeded - gapLength; if (growthNeeded > 0) { growToLength((buffer.length + growthNeeded) * GROWTH_FACTOR + GROWTH_INCREMENT); } } for (int i = start; i < end; ++i) { buffer[pastLength++] = s.charAt(i); } gapLength -= gapNeeded; return this; } public GapString insert(int index, CharSequence s) { return insert(index, s, 0, s.length()); } public GapString insert(int index, String s) { return insert(index, s, 0, s.length()); } public GapString insert(int index, String s, int start, int end) { final int gapNeeded = end - start; if (pastLength != index) { shiftTo(index, gapNeeded); } else { final int growthNeeded = gapNeeded - gapLength; if (growthNeeded > 0) { growToLength((buffer.length + growthNeeded) * GROWTH_FACTOR + GROWTH_INCREMENT); } } s.getChars(start, end, buffer, pastLength); pastLength += (end - start); gapLength -= gapNeeded; return this; } public GapString insert(int index, char[] s) { return insert(index, s, 0, s.length); } public GapString insert(int index, char[] s, int start, int end) { final int gapNeeded = end - start; if (pastLength != index) { shiftTo(index, gapNeeded); } else { final int growthNeeded = gapNeeded - gapLength; if (growthNeeded > 0) { growToLength((buffer.length + growthNeeded) * GROWTH_FACTOR + GROWTH_INCREMENT); } } System.arraycopy(s, start, buffer, pastLength, gapNeeded); pastLength += (end - start); gapLength -= gapNeeded; return this; } public GapString insert(int index, char s) { if (pastLength != index || pastLength < index + 1) { shiftTo(index, 1); } buffer[pastLength++] = s; gapLength -= 1; return this; } public GapString append(boolean x) { return insert(length(), String.valueOf(x)); } public GapString append(char x) { return insert(length(), x); } public GapString append(char[] x) { return insert(length(), x); } public GapString append(char[] x, int start, int end) { return insert(length(), x, start, end); } public GapString append(double x) { return insert(length(), String.valueOf(x)); } public GapString append(float x) { return insert(length(), String.valueOf(x)); } public GapString append(int x) { return insert(length(), String.valueOf(x)); } public GapString append(CharSequence x, int start, int end) { return insert(length(), x, start, end); } public GapString append(Object x) { return insert(length(), String.valueOf(x)); } public GapString append(long x) { return insert(length(), String.valueOf(x)); } public GapString appendCodePoint(int x) { return insertCodePoint(length(), x); } private GapString insertCodePoint(int index, int x) { if (x < 0x10000) { return insert(index, (char) x); } return insert(index, Character.toChars(x)); // rare, so inefficiency doesn't matter } public GapString append(String s) { return insert(buffer.length - gapLength, s, 0, s.length()); } public GapString append(CharSequence string) { return insert(buffer.length - gapLength, string); } public GapString delete(int start, int end) { // if our deletion includes the gap, we can shortcut this // we just reset the pastLength and gap if (pastLength >= start && pastLength < end) { pastLength = start; } else { // TODO There is a possible optimization, to only move enough to get to one end or the other. // However, I don't know whether it would be worth it or not: have to test. if (pastLength != start) { shiftTo(start, 0); } } gapLength += (end - start); return this; } public GapString replace(int start, int end, CharSequence other, int otherStart, int otherEnd) { delete(start, end); return insert(start, other, otherStart, otherEnd); } public GapString compact() { if (gapLength > 0) { growToLength(buffer.length - gapLength); // remove any gap } return this; } public boolean equals(Object other) { try { return equals((CharSequence) other); } catch (Exception e) { return false; } } public boolean equals(CharSequence other) { final int len = buffer.length - gapLength; if (other.length() != len) { return false; } int i = 0; for (; i < pastLength; ++i) { if (buffer[i] != other.charAt(i)) { return false; } } int j = i; i += gapLength; for (; i < buffer.length; ++i) { if (buffer[i] != other.charAt(j++)) { return false; } } return true; } public int hashCode() { int result = 0; int i = 0; for (; i < pastLength; ++i) { result = 37 * result + buffer[i]; } i += gapLength; for (; i < buffer.length; ++i) { result = 37 * result + buffer[i]; } return result; } // ======== PRIVATES =========== /** * This utility function just shifts the gap, so that it starts at newPastLength. The logical contents * of the string are unchanged. */ private void shiftTo(int newPastLength, int gapNeeded) { int growth = gapNeeded - gapLength; if (growth > 0) { growToLength((buffer.length + growth) * GROWTH_FACTOR + GROWTH_INCREMENT); } int newMinusOldPastLength = newPastLength - pastLength; if (newMinusOldPastLength == 0) { return; } if (newMinusOldPastLength > 0) { System.arraycopy(buffer, pastLength + gapLength, buffer, pastLength, newMinusOldPastLength); } else { System.arraycopy(buffer, newPastLength, buffer, pastLength + gapLength + newMinusOldPastLength, -newMinusOldPastLength); } pastLength = newPastLength; } /** * This utility function just grows the gap (and thus the storage). The logical contents * of the string are unchanged. */ private void growToLength(int neededLength) { char[] temp = new char[neededLength]; System.arraycopy(buffer, 0, temp, 0, pastLength); final int futureStart = pastLength + gapLength; final int futureLength = buffer.length - futureStart; System.arraycopy(buffer, futureStart, temp, neededLength - futureLength, futureLength); gapLength += neededLength - buffer.length; buffer = temp; } }