/*******************************************************************************
* Copyright (c) 2007, 2014 compeople AG and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* compeople AG - initial API and implementation
*******************************************************************************/
package org.eclipse.riena.internal.ui.ridgets.swt;
import org.eclipse.core.runtime.Assert;
import org.eclipse.riena.ui.ridgets.IDateTextRidget;
/**
* Deals with insertion, deletion and replacing in segmented strings. A
* segmented string has one or more segments of numbers separated by a single
* separator.
* <p>
* Implementation note regarding valid format patterns: this class has been
* tested with the format patterns in {@link IDateTextRidget}. For details
* regarding the pattern format see {@link IDateTextRidget#setFormat(String).
*/
public class SegmentedString {
/**
* An array holding the value of this segmented string
*/
private final char[] fields;
/**
* An encoding of the pattern of this segmented string. 'd's stand for
* digits. '|'s stand for separators.
*/
private final String pattern;
/**
* Returns true if fChar is a digit (0-9).
*/
public static boolean isDigit(final char fChar) {
return Character.isDigit(fChar);
}
/**
* Returns true if fChar is a valid separator character.
*
* Implementation note: be careful how you use this method. Because ' ' can
* be used both as a separator and as a placeholder, the results of this
* method must be carefully considered against the context in which this
* method is invoked. Example: if you know the user typed ' ' it is a
* separator. If you look at a ' ' value in {@code fields}, it is only a
* separator if the corresponding character in {@pattern} is '|'.
*/
public static boolean isSeparator(final char fChar) {
return ".:/- ".indexOf(fChar) != -1; //$NON-NLS-1$
}
/**
* Create a new segmented string with the given format.
*
* @param format
* a valid format String. See class javadoc for details.
* @throws RuntimeException
* if format is not valid
*/
public SegmentedString(final String format) {
pattern = createPattern(format);
fields = new char[pattern.length()];
for (int i = 0; i < pattern.length(); i++) {
final char pChar = pattern.charAt(i);
fields[i] = pChar != 'd' ? format.charAt(i) : ' ';
}
}
/**
* Create a new segmented string with the given format and initial value.
* The initial value is not checked against the format.
*
* @param format
* a valid format String. See class javadoc for details.
* @param value
* an initial value; non-null
* @throws RuntimeException
* if format is not valid, or value is too long
*/
public SegmentedString(final String format, final String value) {
pattern = createPattern(format);
fields = new char[pattern.length()];
final String msg = String.format("Value '%s' is longer than '%s'", value, format); //$NON-NLS-1$
Assert.isTrue(value.length() <= pattern.length(), msg);
for (int i = 0; i < value.length(); i++) {
fields[i] = value.charAt(i);
}
}
/**
* Delete between {@code from} and {@code to} (inclusive) preserving
* separators.
*
* @param from
* 0-based starting position
* @param to
* 0-based ending position (inclusive; {@code from <= to <
* pattern.length})
* @return the new cursor position
* @throws RuntimeException
* if {@code from} or {@code to} are not valid
*/
public int delete(final int from, final int to) {
return delete(from, to, true);
}
/**
* Find a new cursor position, starting at caretPosition and moving in the
* direction of delta. While moving in either direction, it will ignore
* placeholder spaces (i.e. ' ' on digit positions).
*
* @param caretPosition
* the current cursor position (0 <= caretPosition <=
* pattern.length)
* @param delta
* moving direction (1 or -1)
* @return a new cursor position
* @throws RuntimeException
* if delta has an illegal value.
*/
public int findNewCursorPosition(final int caretPosition, final int delta) {
Assert.isLegal(delta == 1 || delta == -1);
int pos = caretPosition;
if (caretPosition + delta <= pattern.length() && caretPosition + delta >= 0) {
pos = caretPosition + delta;
}
while (pos < pattern.length() && pos > -1) {
if (pattern.charAt(pos) == '|') {
break;
} else if (pattern.charAt(pos) == 'd' && fields[pos] == ' ') {
pos = pos + delta;
} else {
break;
}
}
return pos;
}
/**
* Returns the internal representation of pattern.
*/
public String getPattern() {
return pattern;
}
/**
* Insert the given value at the specified index position.
*
* @param index
* a valid index (zero-based; 0 <= index < pattern.length)
* @param value
* the String to insert. The string is expected to consist of
* valid digits and separators. Insertion will silently abort
* when the first invalid character is encountered.
* @return the cursor position after insertion
*/
public int insert(final int index, final String value) {
int idx = index;
boolean proceed = true;
for (int i = 0; proceed && i < value.length(); i++) {
final char sChar = value.charAt(i);
proceed = isDigit(sChar) || isSeparator(sChar);
if (proceed) {
idx = insert(idx, sChar);
}
}
idx = idx + shiftSpacesLeft(idx);
return idx;
}
/**
* Returns true if the given candidate partially matches this strings
* pattern.
* <p>
* A partial match is defined as: the candidate must not be longer than the
* pattern. Each character in the candidate must match the expected type of
* character in the pattern. If a digit is expected valid values are 0-9 or
* ' ' (placeholder). If a separator is expected the only valid value is the
* exact same separator.
*
* @param candidate
* a non-null String
*/
public boolean isValidPartialMatch(final String candidate) {
boolean result = candidate.length() <= pattern.length();
for (int i = 0; result && i < candidate.length(); i++) {
final char cChar = candidate.charAt(i);
final char pChar = pattern.charAt(i);
if (pChar == 'd') {
result = isDigit(cChar) || cChar == ' ';
} else if (pChar == '|') {
result = isSeparator(cChar) && cChar == fields[i];
} else {
result = false;
}
}
return result;
}
/**
* Replace the characters between {@code from} and {@code to} by the given
* {@code value}.
* <p>
* Implementation note: replace will first delete between {@code from} and
* {@code to}. Afterwards in will insert {@code value} starting at
* {@code from}.
*
* @param from
* a valid starting position (zero-based; 0 <= from <
* pattern.length)
* @param to
* a valid ending position (zero-based; 0 < to < pattern.lengthl
* from <= to)
* @param value
* the String to insert. The string is expected to consist of
* valid digits and separators. Insertion will silently abort
* when the first invalid character is encountered.
* @return the cursor position after replacing
*/
public int replace(final int from, final int to, final String value) {
delete(from, to, false);
final int idx = insert(from, value);
return idx;
}
/**
* Beautify the complete segmented string, by shifting placeholder ' ' to
* the leftmost position within their segment.
*
* @param index
* an index for the delta calculation (this is usually the cursor
* position; 0-based; 0 < index <= pattern.length). Each shift
* occuring at the right of the index will be counted. The count
* will be returned as a delta.
*
* @return a delta, indicating how many positions the index position has
* shifted to the right
*/
public int shiftSpacesLeft(final int index) {
int delta = 0;
for (int i = 0; i < fields.length - 1; i++) {
if (isDigit(fields[i]) && fields[i + 1] == ' ' && pattern.charAt(i + 1) == 'd') {
fields[i + 1] = fields[i];
fields[i] = ' ';
if (index <= i + 1) {
delta++;
}
if (i > 0) {
i = i - 2;
}
}
}
return delta;
}
@Override
public String toString() {
return String.valueOf(fields);
}
// helping methods
//////////////////
private int computeCursorPositionAfterDelete(final int from, final int to) {
int sepIndex = -1;
for (int i = to; i >= from; i--) {
if (pattern.charAt(i) == '|') {
sepIndex = i;
}
}
return sepIndex != -1 ? sepIndex : to + 1;
}
private String createPattern(final String format) {
final StringBuilder result = new StringBuilder(format.length());
for (int i = 0; i < format.length(); i++) {
final char fChar = format.charAt(i);
if (isDigitPattern(fChar)) {
result.append('d');
} else if (isSeparator(fChar)) {
result.append('|');
} else {
throw new IllegalStateException("unsupported format character: " + fChar); //$NON-NLS-1$
}
}
return result.toString();
}
private int delete(final int from, final int to, final boolean shift) {
Assert.isLegal(from > -1, "'from' out of bounds: " + from); //$NON-NLS-1$
Assert.isLegal(from <= to, String.format("'from' must be less-or-equal than 'to': %d, %d", from, to)); //$NON-NLS-1$
Assert.isLegal(to < fields.length, "'to' out of bounds: " + to); //$NON-NLS-1$
for (int i = from; i <= to; i++) {
if (pattern.charAt(i) != '|') {
fields[i] = ' ';
}
}
final int pos = computeCursorPositionAfterDelete(from, to);
if (shift) {
// for the delete operation we can ignore the value returned by
// shiftSpacesLeft(...). The method computeCursorPositionAfterDelete
// already returns the correct location
shiftSpacesLeft(to);
}
return pos;
}
private int findFreePosition(final int index) {
int result = -1;
final int pos = index < pattern.length() ? index : pattern.length() - 1;
if (fields[pos] == ' ' && pattern.charAt(pos) == 'd') {
result = pos;
} else if (groupHasSpaceOnLeft(pos)) {
shiftRight(index - 1);
Assert.isLegal(fields[index - 1] == ' ');
result = index - 1;
} else if (groupHasSpaceOnRight(pos)) {
result = pos + 1;
}
return result;
}
private void shiftRight(final int index) {
int spacePos = -1;
int pos = index;
while (pos > -1 && spacePos == -1) {
if (fields[pos] == ' ') {
spacePos = pos;
} else {
pos--;
}
}
final String msg = String.format("did not find space in '%s' starting at %d", String.valueOf(fields), index); //$NON-NLS-1$
Assert.isLegal(spacePos != -1, msg);
Assert.isLegal(fields[spacePos] == ' ');
for (int i = spacePos; i < index; i++) {
fields[i] = fields[i + 1];
fields[i + 1] = ' ';
}
}
private boolean groupHasSpaceOnLeft(final int index) {
boolean result = false;
int pos = index;
while (pos > 0 && !result) {
result = pattern.charAt(pos - 1) == 'd' && fields[pos - 1] == ' ';
if (pattern.charAt(pos - 1) == '|') {
pos = -1;
} else {
pos--;
}
}
return result;
}
private boolean groupHasSpaceOnRight(final int index) {
return index < pattern.length() - 1 && pattern.charAt(index) == '|' && pattern.charAt(index + 1) == 'd'
&& fields[index + 1] == ' ';
}
private int insert(final int index, final char ch) {
Assert.isLegal(index > -1, "index out of bounds: " + index); //$NON-NLS-1$
Assert.isLegal(index <= fields.length, "index out of bounds: " + index); //$NON-NLS-1$
int result = index;
if (index < fields.length) {
if (isSeparator(ch) && isSeparator(fields[index])) {
result = index + 1;
} else if (isDigit(ch)) {
final int freePosition = findFreePosition(index);
if (freePosition != -1) {
fields[freePosition] = ch;
result = freePosition + 1;
}
}
} else if (index == fields.length) {
if (isDigit(ch)) {
final int freePosition = findFreePosition(index);
if (freePosition != -1) {
fields[freePosition] = ch;
result = freePosition + 1;
}
}
}
return result;
}
private boolean isDigitPattern(final char fChar) {
return "dMyHms".indexOf(fChar) != -1; //$NON-NLS-1$
}
}