Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

string

c#

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

like image 705
Astronavigator Avatar asked Aug 31 '11 10:08

Astronavigator


2 Answers

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));
}
like image 119
ghord Avatar answered Nov 15 '22 17:11

ghord


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...

like image 25
Jon Skeet Avatar answered Nov 15 '22 17:11

Jon Skeet