package de.skuzzle.polly.core.parser; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.TreeSet; import de.skuzzle.polly.tools.EqualsHelper; import de.skuzzle.polly.tools.Equatable; import de.skuzzle.polly.tools.Immutable; /** * Represents a span within a one-lined String. It consists of a inclusive * start position and an exclusive end position. * * For convenience, a Position has methods to retrieve substrings of a given String: * {@link #prefix(String)} which returns a String which consists of all characters * before this Position. * {@link #substring(String)} which returns exactly the String which Position this * object represents and {@link #postfix(String)} which returns a String consisting of * all characters which follow this span. * * @author Simon */ public final class Position implements Equatable, Immutable, Comparable<Position> { /** * Creates a position that spans from the lower of the given positions to the greater * of the given positions. * * @param pos Array of positions. * @return A new position that spans from the start of the lowest position to the end * of the greatest position. */ public final static Position correctSpan(Position...pos) { if (pos.length == 0) { throw new IllegalArgumentException("zero length array"); //$NON-NLS-1$ } Arrays.sort(pos); if (pos[0] == Position.NONE || pos[pos.length - 1] == Position.NONE) { return Position.NONE; } return new Position(pos[0], pos[pos.length - 1]); } /** * A Position instance that represents no valid position within a String. Can be used * for internally created AST Nodes, that have no matching representation within * the input String. */ public final static Position NONE = new Position(-1, 0); /** * Creates a list of indicator strings from the given collection of positions. * Disjunct position will be placed in one line. * * @param positions Collection of positions. * @param offset Offset to add to each position before creating the string. * @return A list of error indicator strings. */ public static List<String> indicatorStrings(Collection<Position> positions, int offset) { final List<String> result = new ArrayList<String>(positions.size()); final TreeSet<Position> posis = new TreeSet<Position>(positions); while (!posis.isEmpty()) { final Position next = posis.pollFirst(); final StringBuilder b = new StringBuilder(); b.append(next.offset(offset).errorIndicatorString()); final Iterator<Position> it = posis.iterator(); while (it.hasNext()) { final Position pos = it.next(); if (next.overlap(pos)) { break; } else { final Position off = pos.offset(offset); while (b.length() < off.start) { b.append(" "); //$NON-NLS-1$ } b.append("^"); //$NON-NLS-1$ if (off.getWidth() > 1) { while (b.length() < off.end - 1) { b.append("-"); //$NON-NLS-1$ } b.append("^"); //$NON-NLS-1$ } it.remove(); } } result.add(b.toString()); } return result; } private final int start; private final int end; /** * Creates a new Position Object with given start and end index. * * @param start The inclusive start index of this Position. * @param end The exclusive end index of this position. */ public Position(int start, int end) { if (start >= end) { throw new IllegalArgumentException( "start >= end (" + start + ">=" + end + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ } this.start = start; this.end = end; } /** * Creates a new Position using the positions of the given tokens. The new * position will span from the start of the left token until the end of the * right token. * * @param left The left token. * @param right The right token. */ public Position(Token left, Token right) { this(left.getPosition(), right.getPosition()); } /** * Creates a new Position using the start and end indizes of the given positions. * The new Position will span from the start of the left position until the end of * the right position. * * @param left The left position. * @param right The right position. */ public Position(Position left, Position right) { this(left.getStart(), right.getEnd()); } /** * Creates a new position by clipping this position to the given position if * necessary. That means the resulting position starts at * <code>max(this.start, other.start)</code> and ends at * <code>min(this.end, other.end)</code>. * * @param other Position to clip this to. * @return A new clipped position. */ public Position clip(Position other) { return this.clip(other.start, other.end); } /** * Creates a new position by clipping this position to the given start and end value * if necessary. That means the resulting position starts at * <code>max(this.start, start)</code> and ends at * <code>min(this.end, end)</code>. * * @param start Minimum starting position. * @param end Maximum ending position. * @return A new clipped position. */ public Position clip(int start, int end) { start = Math.max(start, this.start); end = Math.min(end, this.end); if (start == end) { ++end; } return new Position(start, end); } /** * Determines whether this position and the given represent overlapping string parts. * * @param position Position to compare. * @return If the positions overlap each other. */ public boolean overlap(Position position) { if (this.start < position.start) { return this.end > position.start; } else { return position.end > this.start; } } /** * Returns whether this position is completely inside the other position. * * @param other The other position. * @return Whether this position is inside the other one. */ public boolean isInside(Position other) { return this.start >= other.start && this.end <= other.end; } /** * Creates a new Position by adding the given offset to this position's start and * end. * * @param offset The offset to end. * @return A new position. */ public Position offset(int offset) { return new Position(this.start + offset, this.end + offset); } /** * Creates a new position spanning from the start of the given position to the end * of this position. * * @param start Start position. * @return A new position. */ public Position spanFrom(Position start) { return new Position(start, this); } /** * Creates a new position spanning from the start of this position to the end of the * given position. * * @param end End position. * @return A new position. */ public Position spanTo(Position end) { return new Position(this, end); } /** * Returns the starting index of this Position. * @return The inclusive start index. */ public int getStart() { return this.start; } /** * Returns the end index of this Position. * @return The inclusive end index. */ public int getEnd() { return this.end; } /** * Gets the count of characters that this position spans. * * @return Character count between end and start position. */ public int getWidth() { return this.end - this.start; } /** * Gives a String which indicates this position. That means, the returned String will * consist of spaces until the start position. There, a "^" will be printed to * indicate the beginning of this position. If {@link #getWidth()} is greater than * 1, a second "^" will be printed at the end position. So the returned String will * have the length <code>getWidth() > 1 ? this.getEnd() : this.getStart()</code> * * @return A String indicating this position. */ public String errorIndicatorString() { if (this == Position.NONE) { return ""; //$NON-NLS-1$ } final StringBuilder b = new StringBuilder(this.end); int i = 0; for (; i < this.start; ++i) { b.append(" "); //$NON-NLS-1$ } b.append("^"); //$NON-NLS-1$ if (this.getWidth() > 1) { for (; i < this.end - 2; ++i) { b.append("-"); //$NON-NLS-1$ } b.append("^"); //$NON-NLS-1$ } return b.toString(); } /** * Returns a prefix of the given String consisting of all characters with index * lower than {@link #getStart()}. * * @param original The String in which this object represents a Position. * @return A substring of the original String. * @throws IllegalArgumentException If the original String is too short. */ public String prefix(String original) { if (this.equals(Position.NONE)) { return original; } else if (this.start > original.length()) { throw new IllegalArgumentException("Original String is too short!"); //$NON-NLS-1$ } return original.substring(0, this.start); } /** * Returns the postfix of the given string which consists of all characters that * occur after this positions end index (including the character at the end index * itself, because it is exclusive). * * @param original The string to create the postfix from. * @return A postfix of that string. */ public String postfix(String original) { if (this.equals(Position.NONE)) { return original; } else if (this.start == this.end - 1 && this.start == original.length()) { return " "; //$NON-NLS-1$ } return original.substring(this.end); } /** * Returns the substring of the given string that this position represents. If this * is {@link Position#NONE}, the whole other string will be returned. * * @param original The string to create the substring from. * @return The substring. */ public String substring(String original) { if (this.equals(Position.NONE)) { return original; } else if (this.start == this.end - 1 && this.start == original.length()) { return " "; //$NON-NLS-1$ } return original.substring(this.start, this.end); } public String mark(String original) { return this.mark(original, "4\u001F", "\u001F"); //$NON-NLS-1$ //$NON-NLS-2$ } public String mark(String original, String begin, String end) { StringBuilder result = new StringBuilder(original.length() + 19); result.append(this.prefix(original)); result.append(begin); result.append(this.substring(original)); result.append(end); result.append(this.postfix(original)); return result.toString(); } @Override public String toString() { return (this.start + 1) + (this.getWidth() == 1 ? "" : "-" + (this.end + 1)); //$NON-NLS-1$ //$NON-NLS-2$ } @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + this.end; result = prime * result + this.start; return result; } @Override public Class<?> getEquivalenceClass() { return Position.class; } @Override public final boolean equals(Object obj) { return EqualsHelper.testEquality(this, obj); } @Override public boolean actualEquals(Equatable o) { final Position other = (Position) o; return this.start == other.start && this.end == other.end; } @Override public int compareTo(Position o) { int r = this.start - o.start; if (r == 0) { r = this.end - o.end; } return r; } }