/******************************************************************************* * 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 java.text.DecimalFormatSymbols; import org.eclipse.core.runtime.Assert; /** * Deals with manipulation of numeric strings. * <p> * A numeric string consists of several digits, thay may or may not be separated * by grouping separators (groups of 3 digits). The string may contain a decimal * separator, followed by a fractional part of 0 to <i>n</i> digits. Grouping * and decimal separator must conform to the current locale. Negative values may * be prefixed by a '-' character. * <p> * Examples: * <ul> * <li>DE - valid strings: "1.234,56" / "1234,56" / "1234" / "1.234" / "1234," / * "1.234,"</li> * <li>US - valid strings: "1,234.56" / "1234.56" / "1234" / "1,234" / "1234." / * "1,234."</li> * </ul> * * @see DecimalFormatSymbols#getDecimalSeparator() */ public class NumericString { private Digit start; private final boolean isGrouping; /** * Create a new instnace * * @param value * the non-null value of this string. See class javadoc for * accepted formats * @param isGrouping * true, if a grouping separator should be used, false otherwise. * Grouping separators will automatically be added to the value * argument, if necessary. */ public NumericString(final String value, final boolean isGrouping) { Assert.isNotNull(value); if (value.length() > 0) { Digit prev = null; for (int i = 0; i < value.length(); i++) { final char ch = value.charAt(i); prev = new Digit(ch, prev); if (i == 0) { start = prev; } } } this.isGrouping = isGrouping; if (isGrouping) { applyGrouping(); } } /** * Deletes between {@code from} and {@code to} (exclusive) preserving * separators. * * @param from * 0-based starting position * @param to * 0-based ending position (exclusive; {@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, final char ch) { Assert.isLegal(-1 < from); Assert.isLegal(from < to); final int delta = '\b' == ch ? -1 : 1; if (to - from == 1) { final Digit fromDigit = findDigit(from); fromDigit.delete(delta); } else { final Digit fromDigit = findDigit(from); final Digit toDigitExcl = findDigit(to); final boolean endsWithSep = toDigitExcl != null && toDigitExcl.prev != null && toDigitExcl.prev.isDecimalSeparator(); fromDigit.delete(toDigitExcl); if (endsWithSep && toDigitExcl.prev != null) { toDigitExcl.prev.setCursorBefore(); } else if (toDigitExcl != null) { toDigitExcl.setCursorBefore(); } else { final Digit lastDigit = getLastDigit(); if (lastDigit != null && lastDigit == start && lastDigit.isDecimalSeparator()) { lastDigit.setCursorBefore(); } else if (lastDigit != null) { lastDigit.setCursorAfter(); } } } applyGrouping(); return getCursorLocation(); } @Override public String toString() { final StringBuilder result = new StringBuilder(); if (start != null) { Digit current = start; while (current != null) { result.append(current.toString()); current = current.next; } } return result.toString(); } // helping methods ////////////////// private void applyGrouping() { if (!isGrouping) { return; } Digit here = start; while (here != null) { if (here.isGroupingSeparator()) { here.deleteThis(); } here = here.next; } Digit decSep = null; Digit lastDigit = null; here = start; while (decSep == null && here != null) { if (here.isDecimalSeparator()) { decSep = here; } if (here.next == null) { lastDigit = here; } here = here.next; } Digit firstDigit = decSep != null ? decSep.prev : lastDigit; int i = 2; while (firstDigit != null) { if (i == 0 && firstDigit.prev != null && Character.isDigit(firstDigit.prev.ch)) { final Digit sep = new Digit(NumericTextRidget.GROUPING_SEPARATOR, null); sep.next = firstDigit; sep.prev = firstDigit.prev; firstDigit.prev.next = sep; firstDigit.prev = sep; i = 4; } i--; firstDigit = firstDigit.prev; } } private int getCursorLocation() { int result = start == null ? 0 : -1; Digit here = start; int index = 0; while (result == -1 && here != null) { if (here.isCursorBeforeMe()) { result = index; } else if (here.isCursorAfterMe()) { result = index + 1; } index++; here = here.next; } return result; } private Digit getLastDigit() { Digit result = start; while (result != null && result.next != null) { result = result.next; } return result; } private Digit findDigit(final int index) { Digit result = null; Digit here = start; int count = index; while (here != null && count >= 0) { if (count == 0) { result = here; } here = here.next; count--; } return result; } // helping classes ////////////////// private final class Digit { private final char ch; private boolean cursorAfterMe; private boolean cursorBeforeMe; private Digit prev; private Digit next; public Digit(final char ch, final Digit prev) { this.ch = ch; this.prev = prev; if (prev != null) { prev.next = this; } } public void deleteThis() { if (prev != null) { prev.next = next; } if (next != null) { next.prev = prev; } if (prev == null) { start = next; } if (cursorAfterMe) { if (next != null) { next.setCursorBefore(); } else if (prev != null) { prev.setCursorAfter(); } } if (cursorBeforeMe) { if (prev != null) { prev.setCursorAfter(); } else if (next != null) { next.setCursorBefore(); } } } public void delete(final int delta) { Assert.isLegal(delta == -1 || delta == 1); if (isSeparator()) { if (delta == -1) { if (prev != null) { prev.delete(delta); } else { this.setCursorBefore(); } } else if (delta == 1) { if (next != null) { next.delete(delta); } else { this.setCursorAfter(); } } } else { deleteThis(); if (prev != null) { prev.setCursorAfter(); } else if (next != null) { next.setCursorBefore(); } } } public void delete(final Digit end) { if (this != end) { final boolean hasPrev = this.prev != null; final boolean hasNext = this.next != null; if (!isDecimalSeparator()) { if (hasPrev) { this.prev.next = this.next; } else { start = this.next; } if (hasNext) { this.next.prev = this.prev; } } if (hasNext) { this.next.delete(end); } } } public boolean isCursorAfterMe() { return cursorAfterMe; } public boolean isCursorBeforeMe() { return cursorBeforeMe; } public boolean isSeparator() { return isDecimalSeparator() || isGroupingSeparator(); } public boolean isDecimalSeparator() { return NumericTextRidget.DECIMAL_SEPARATOR == ch; } public boolean isGroupingSeparator() { return NumericTextRidget.GROUPING_SEPARATOR == ch; } public void setCursorAfter() { resetCursorLocation(); cursorAfterMe = true; if (next != null) { next.cursorBeforeMe = true; } } public void setCursorBefore() { resetCursorLocation(); cursorBeforeMe = true; if (prev != null) { prev.cursorAfterMe = true; } } @Override public String toString() { return String.valueOf(ch); } private void resetCursorLocation() { Digit p = this; while (p != null) { p.cursorAfterMe = false; p.cursorBeforeMe = false; p = p.prev; } Digit n = this.next; while (n != null) { n.cursorAfterMe = false; n.cursorBeforeMe = false; n = n.next; } } }; }