Astronavigator
Astronavigator

Reputation: 2051

What is simpliest way to get Line number from char position in String?

What is simpliest way to get Line number from char position in String in C#? (or get Position of line (first char in line) ) Is there any built-in function ? If there are no such function is it good solution to write extension like :

public static class StringExt {
    public static int LineFromPos(this String S, int Pos) { 
        int Res = 1;
        for (int i = 0; i <= Pos - 1; i++)
            if (S[i] == '\n') Res++;
        return Res;                
    }

    public static int PosFromLine(this String S, int Pos) { .... }

}

?

Edited: Added method PosFromLine

Upvotes: 11

Views: 10342

Answers (5)

Dorian
Dorian

Reputation: 9065

In ruby:

      def line_index
        source[0...position].count("\n")
      end

      def line_number
        line_index + 1
      end

      def lines
        source.lines
      end

      def line_source
        lines[line_index]
      end

      def line_position
        position - lines[0...line_index].map(&:size).sum
      end

Upvotes: -2

Rivenfall
Rivenfall

Reputation: 1263

For who is interested in javascript or a more iterative approach.

const {min} = Math

function lineAndColumnNumbersAt(str, pos) {
    let line = 1, col = 1
    const _pos = min(str.length, pos)
    for (let i = 0; i < _pos; i++)
        if (str[i] === '\n') {
            line++
            col = 1
        } else
            col++
    return {line, col}
}

lineAndColumnNumbersAt('test\ntest\ntest', 8)

Upvotes: -2

ghord
ghord

Reputation: 13797

If you are going to call the function many times on the same long string, this class can be usefull. It caches the new line positions, so that later it can perform O(log (line breaks in string)) lookup for GetLine and O(1) for GetOffset.

public class LineBreakCounter
{
    List<int> lineBreaks_ = new List<int>();
    int length_;

    public LineBreakCounter(string text)
    {
        if (text == null)
            throw new ArgumentNullException(nameof(text));

        length_ = text.Length;
        for (int i = 0; i < text.Length; i++)
        {
            if (text[i] == '\n')
                lineBreaks_.Add(i);

            else if (text[i] == '\r' && i < text.Length - 1 && text[i + 1] == '\n')
                lineBreaks_.Add(++i);
        }
    }

    public int GetLine(int offset)
    {
        if (offset < 0 || offset > length_)
            throw new ArgumentOutOfRangeException(nameof(offset));

        var result = lineBreaks_.BinarySearch(offset);
        if (result < 0)
            return ~result;
        else
            return result;
    }

    public int Lines => lineBreaks_.Count + 1;

    public int GetOffset(int line)
    {
        if (line < 0 || line >= Lines)
            throw new ArgumentOutOfRangeException(nameof(line));

        if (line == 0)
            return 0;

        return lineBreaks_[line - 1] + 1;
    }
}

Here is my test case:

[TestMethod]
public void LineBreakCounter_ShouldFindLineBreaks()
{
    var text = "Hello\nWorld!\r\n";
    var counter = new LineBreakCounter(text);

    Assert.AreEqual(0, counter.GetLine(0));
    Assert.AreEqual(0, counter.GetLine(3));
    Assert.AreEqual(0, counter.GetLine(5));
    Assert.AreEqual(1, counter.GetLine(6));
    Assert.AreEqual(1, counter.GetLine(8));
    Assert.AreEqual(1, counter.GetLine(12));
    Assert.AreEqual(1, counter.GetLine(13));
    Assert.AreEqual(2, counter.GetLine(14));

    Assert.AreEqual(3, counter.Lines);
    Assert.AreEqual(0, counter.GetOffset(0));
    Assert.AreEqual(6, counter.GetOffset(1));
    Assert.AreEqual(14, counter.GetOffset(2));
}

Upvotes: 5

Jon Skeet
Jon Skeet

Reputation: 1500145

A slight variation on Jan's suggestion, without creating a new string:

var lineNumber = input.Take(pos).Count(c => c == '\n') + 1;

Using Take limits the size of the input without having to copy the string data.

You should consider what you want the result to be if the given character is a line feed, by the way... as well as whether you want to handle "foo\rbar\rbaz" as three lines.

EDIT: To answer the new second part of the question, you could do something like:

var pos = input.Select((value, index) => new { value, index })
               .Where(pair => pair.value == '\n')
               .Select(pair => pair.index + 1)
               .Take(line - 1)
               .DefaultIfEmpty(1) // Handle line = 1
               .Last();

I think that will work... but I'm not sure I wouldn't just write out a non-LINQ approach...

Upvotes: 18

Jan Jongboom
Jan Jongboom

Reputation: 27323

Count the number of newlines in the substringed input string.

var lineNumber = input.Substring(0, pos).Count(c=>c == '\n') + 1;

edit: and do a +1 because line numbers begin at 1 :-)

Upvotes: 12

Related Questions