Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make LocationMap return a line/col on EOF #286

Merged
merged 1 commit into from
Nov 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions core/shared/src/main/scala/cats/parse/LocationMap.scala
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ import java.util.Arrays
* numbers
*/
class LocationMap(val input: String) {

private[this] val lines: Array[String] =
input.split("\n", -1)

private[this] val endsWithNewLine: Boolean =
(input.length > 0) && (input.last == '\n')

// The position of the first element of the ith line
private[this] val firstPos: Array[Int] = {
val it = lines.iterator.map(_.length)
Expand All @@ -52,11 +56,27 @@ class LocationMap(val input: String) {
.scanLeft(0)(_ + _)
}

/** Given a string offset return the line and column
/** How many lines are there
*/
def lineCount: Int = lines.length

/** Given a string offset return the line and column If input.length is given (EOF) we return the
* same value as if the string were one character longer (i.e. if we have appended a non-newline
* character at the EOF)
*/
def toLineCol(offset: Int): Option[(Int, Int)] =
if (offset < 0 || offset >= input.length) None
else {
if (offset < 0 || offset > input.length) None
else if (offset == input.length) {
// this is end of line
if (offset == 0) Some((0, 0))
else {
toLineCol(offset - 1)
.map { case (line, col) =>
if (endsWithNewLine) (line + 1, 0)
else (line, col + 1)
}
}
} else {
val idx = Arrays.binarySearch(firstPos, offset)
if (idx == firstPos.length) {
// greater than all elements
Expand Down Expand Up @@ -85,6 +105,13 @@ class LocationMap(val input: String) {
if (i >= 0 && i < lines.length) Some(lines(i))
else None

/** Return the offset for a given row/col. if we return Some(input.length) this means EOF if we
* return Some(i) for 0 <= i < input.length it is a valid item else offset < 0 or offset >
* input.length we return None
*/
def toOffset(line: Int, col: Int): Option[Int] =
if ((line < 0) || (line > lines.length)) None
else Some(firstPos(line) + col)
}

object LocationMap {
Expand Down
71 changes: 58 additions & 13 deletions core/shared/src/test/scala/cats/parse/LocationMapTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,19 @@ class LocationMapTest extends munit.ScalaCheckSuite {
}
}

property("position of end-of-line is the same as adding a constant") {
forAll { (str: String) =>
val lm0 = LocationMap(str)
val lm1 = LocationMap(str + "a")

assert(lm0.toLineCol(str.length) == lm1.toLineCol(str.length))
}
}

test("some specific examples") {
val lm0 = LocationMap("\n")
assert(lm0.toLineCol(0) == Some((0, 0)))
assert(lm0.toLineCol(1) == None)
assertEquals(lm0.toLineCol(1), Some((1, 0)))

val lm1 = LocationMap("012\n345\n678")
assert(lm1.toLineCol(-1) == None)
Expand All @@ -62,7 +71,7 @@ class LocationMapTest extends munit.ScalaCheckSuite {
assert(lm1.toLineCol(8) == Some((2, 0)))
assert(lm1.toLineCol(9) == Some((2, 1)))
assert(lm1.toLineCol(10) == Some((2, 2)))
assert(lm1.toLineCol(11) == None)
assert(lm1.toLineCol(11) == Some((2, 3)))
}

property("we can reassemble input with getLine") {
Expand Down Expand Up @@ -94,7 +103,7 @@ class LocationMapTest extends munit.ScalaCheckSuite {
case None => fail(s"offset = $offset, s = $s")
case Some(line) =>
assert(line.length >= col)
if (line.length == col) assert(s(offset) == '\n')
if (line.length == col) assert((offset == s.length) || s(offset) == '\n')
else assert(line(col) == s(offset))
}
}
Expand All @@ -107,8 +116,8 @@ class LocationMapTest extends munit.ScalaCheckSuite {
property("if a string is not empty, 0 offset is (0, 0)") {
forAll { (s: String) =>
LocationMap(s).toLineCol(0) match {
case Some(r) => assert(r == ((0, 0)))
case None => assert(s.isEmpty)
case Some(r) => assertEquals(r, ((0, 0)))
case None => fail("could not get the first item")
}
}
}
Expand All @@ -117,28 +126,42 @@ class LocationMapTest extends munit.ScalaCheckSuite {

def slow(str: String, offset: Int): Option[(Int, Int)] = {
val split = str.split("\n", -1)
def lineCol(off: Int, row: Int): Option[(Int, Int)] =
if (row == split.length) None
def lineCol(off: Int, row: Int): (Int, Int) =
if (row == split.length) (row, 0)
else {
val r = split(row)
val extraNewLine =
if (row < (split.length - 1)) 1 else 0 // all but the last have an extra newline
val chars = r.length + extraNewLine

if (off >= chars) lineCol(off - chars, row + 1)
else Some((row, off))
else (row, off)
}

if (offset < 0 || offset >= str.length) None
else lineCol(offset, 0)
if (offset < 0 || offset > str.length) None
else if (offset == str.length) Some {
if (offset == 0) (0, 0)
else {
val (l, c) = lineCol(offset - 1, 0)
if (str.last == '\n') (l + 1, 0)
else (l, c + 1)
}
}
else Some(lineCol(offset, 0))
}

assert(slow("\n", 0) == Some((0, 0)))
assert(LocationMap("\n").toLineCol(0) == Some((0, 0)))

assertEquals(slow("\n", 1), Some((1, 0)))
assertEquals(LocationMap("\n").toLineCol(1), Some((1, 0)))

assert(slow(" \n", 1) == Some((0, 1)))
assert(LocationMap(" \n").toLineCol(1) == Some((0, 1)))

assertEquals(slow(" \n", 2), Some((1, 0)))
assertEquals(LocationMap(" \n").toLineCol(2), Some((1, 0)))

assert(slow(" \n ", 1) == Some((0, 1)))
assert(LocationMap(" \n ").toLineCol(1) == Some((0, 1)))

Expand All @@ -148,9 +171,8 @@ class LocationMapTest extends munit.ScalaCheckSuite {
forAll { (str: String, offset: Int) =>
val lm = LocationMap(str)
assertEquals(lm.toLineCol(offset), slow(str, offset))
if (str.length > 0) {
val validOffset = math.abs(offset % str.length)
assertEquals(lm.toLineCol(validOffset), slow(str, validOffset))
(0 to str.length).foreach { o =>
assertEquals(lm.toLineCol(o), slow(str, o))
}
}
}
Expand All @@ -171,4 +193,27 @@ class LocationMapTest extends munit.ScalaCheckSuite {
}
}
}

property("toLineCol toOffset round trips") {
forAll { (s: String, offset: Int) =>
val offsets = (-s.length to 2 * s.length).toSet + offset

val lm = LocationMap(s)
offsets.foreach { o =>
lm.toLineCol(o) match {
case Some((l, c)) =>
assertEquals(lm.toOffset(l, c), Some(o))
case None =>
assert(o < 0 || o > s.length)
}
}
}
}

property("lineCount and getLine are consistent") {
forAll { (s: String) =>
val lm = LocationMap(s)
assert(s.endsWith(lm.getLine(lm.lineCount - 1).get))
}
}
}