WarpPI/core/src/main/java/it/cavallium/warppi/math/rules/dsl/errorutils/LineMap.java

192 lines
6.6 KiB
Java

package it.cavallium.warppi.math.rules.dsl.errorutils;
import java.util.*;
import java.util.stream.Collectors;
/**
* Splits a string into lines and associates positions within the string to the lines they belong to.
* <p>
* For each line, the number (starting from 1), start position and content are stored.
* <p>
* A line can end at the end of the string, or with a line terminator ("\r", "\n" or "\r\n").
* The terminator defines the end of a line, but not necessarily the beginning of a new one: it's considered to be part
* of the line (however, for convenience, it's not included in the content), and a terminator at the end of the string
* doesn't start a new empty line.
* For example, the string <code>"abc\n\n"</code> contains two lines:
* <ul>
* <li> line 1 starts at position 0, and its content is <code>"abc"</code>;
* <li> line 2 starts at position 4 (the index of the second '\n'), and its content is <code>""</code> (the empty string).
* </ul>
* As a consequence of these criteria, an empty string has no lines.
*/
public class LineMap {
private final String text;
private final NavigableMap<Integer, LineInfo> lines;
/**
* Constructs a <code>LineMap</code> for the given string.
*
* @param text The string to split into lines.
*/
public LineMap(final String text) {
this.text = text;
this.lines = splitLines(text);
}
/**
* Gets all lines which the specified substring spans.
* <p>
* A substring spans a line if it contains at least one of the characters which belong to the line,
* including the terminator ("\r", "\n" or "\r\n"), within the original string.
* However, as a special case, an empty substring (<code>length == 0</code>) still spans the line corresponding to
* its <code>startPosition</code>, even though it doesn't contain any characters.
* Therefore, any substring spans at least one line, unless there are no lines at all (because the original string
* is empty).
*
* @param startPosition The index at which the substring starts within the original string.
* @param length The length of the substring within the original string.
* @return The (potentially empty) list of spanned lines (each one without the terminator characters).
* @throws StringIndexOutOfBoundsException If the specified substring isn't valid, because:
* <ul>
* <li> <code>startPosition</code> is negative, or
* <li> <code>startPosition</code> is larger than the length of the original string, or
* <li> <code>length</code> is negative, or
* <li> there are less than <code>length</code> characters from <code>startPosition</code>
* to the end of the original string.
* </ul>
*/
public List<Line> getSpannedLines(final int startPosition, final int length) {
if (startPosition < 0 || startPosition > text.length()) {
throw new StringIndexOutOfBoundsException("Substring start position out of range: " + startPosition);
}
int endPosition = startPosition + length;
if (endPosition < startPosition || endPosition > text.length()) {
throw new StringIndexOutOfBoundsException("Substring length out of range: " + length);
}
if (lines.isEmpty()) {
return Collections.emptyList();
}
final Map.Entry<Integer, LineInfo> firstSpannedLine = lines.floorEntry(startPosition);
if (length == 0) {
// For empty substrings, firstSpannedLine.getKey() may be equal to endPosition.
// In this case, the submap would be empty (because the upper bound is exclusive),
// so the single spanned line has to be returned manually.
return Collections.singletonList(lineFromMapEntry(firstSpannedLine));
}
final SortedMap<Integer, LineInfo> spannedLines = lines.subMap(firstSpannedLine.getKey(), endPosition);
return spannedLines.entrySet().stream()
.map(this::lineFromMapEntry)
.collect(Collectors.toList());
}
private static NavigableMap<Integer, LineInfo> splitLines(final String string) {
final TreeMap<Integer, LineInfo> lines = new TreeMap<>();
int lineNum = 1;
int lineStart = 0;
int pos = 0;
while (pos < string.length()) {
final char cur = string.charAt(pos);
int nextPos = pos + 1;
if (nextPos < string.length() && cur == '\r' && string.charAt(nextPos) == '\n') {
nextPos++; // Skip \n after \r because \r\n is a single line separator
}
if (cur == '\r' || cur == '\n') {
lines.put(lineStart, new LineInfo(lineNum, pos));
lineNum++;
lineStart = nextPos;
}
pos = nextPos;
}
// If the last line has no trailing separator, the loop won't add it to the map
if (lineStart < string.length()) {
lines.put(lineStart, new LineInfo(lineNum, string.length()));
}
return lines;
}
private Line lineFromMapEntry(final Map.Entry<Integer, LineInfo> entry) {
final int start = entry.getKey();
final LineInfo lineInfo = entry.getValue();
return new Line(
lineInfo.number,
start,
text.substring(start, lineInfo.end)
);
}
private static class LineInfo {
public final int number;
public final int end;
LineInfo(final int number, final int end) {
this.number = number;
this.end = end;
}
}
/**
* Represents a line of text within a string.
*/
public static class Line {
private final int number;
private final int startPosition;
private final String text;
Line(final int number, final int startPosition, final String text) {
this.number = number;
this.startPosition = startPosition;
this.text = text;
}
/**
* @return The line number (starting from 1).
*/
public int getNumber() {
return number;
}
/**
* @return The index at which this line starts within the original string.
* If the line is empty, this is the index of the terminator characters ("\r", "\n" or "\r\n").
*/
public int getStartPosition() {
return startPosition;
}
/**
* @return The contents of this line, <em>without</em> the terminator characters ("\r", "\n" or "\r\n").
*/
public String getText() {
return text;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Line line = (Line) o;
return number == line.number &&
startPosition == line.startPosition &&
Objects.equals(text, line.text);
}
@Override
public int hashCode() {
return Objects.hash(number, startPosition, text);
}
@Override
public String toString() {
return "Line{" +
"number=" + number +
", startPosition=" + startPosition +
", text='" + text + '\'' +
'}';
}
}
}