如何最简单地从字符串中的字符位置获取行号?

11

在C#中,从字符位置获取行号(或获取行中的第一个字符位置)的最简单方法是什么?是否有内置函数可用?如果没有这样的功能,编写扩展是否是一个好的解决方案?

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) { .... }

}

编辑:添加了PosFromLine方法


1
如果你需要频繁调用这个函数,比如在几百或几千行的代码中,有更好的方法可以使用。例如,如果你正在按顺序处理一个文件,你可以“记住”你所在的行号,并在每次遇到换行符时递增它。或者你可以使用字典缓存每1000个字符左右的行号,并使用查询前面的缓存条目作为起点。如果性能不是问题,直接选择Jan/Jon的清晰明了的方法即可。 - Kieren Johnstone
请参见"问题标题中是否应包含“标签”?",共识是“不应该”! - user57508
5个回答

18

在Jan的建议上稍作修改,但不需要创建一个新字符串:

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

使用Take可以限制输入的大小而不必复制字符串数据。

顺便提一下,你应该考虑如果给定字符是换行符时的结果,以及是否要将"foo\rbar\rbaz"视为三行。

编辑:为了回答问题的第二部分,你可以这样做:

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();

认为那会起作用...但我不确定我是否只应该编写一个非LINQ方法...


+1:不是因为"Skeet"因素,而是因为"不创建新字符串"的原因。 - Christian.K
@Jon:关于“foo\rbar\rbaz”:我认为为了获得最佳结果,它应该识别所有三种换行符(“\n”,“\r”,“\r\n”),但所有解决方案都很有趣。 - Astronavigator
@Astronavigator:这变得更加困难了,因为您希望将"\r\n"视为单个换行符,所以它变得有状态。除非您需要这种行为,否则我建议仅计算\n :) - Jon Skeet
1
在LINQ阶段之前,您也可以使用.Replace("\r\n", "\n").Replace("\r", "\n")进行预处理。 - Kieren Johnstone

12

计算子字符串中换行符的数量。

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

编辑:并且执行 +1,因为行号从1开始 :-)


非常清晰明了,虽然可能不如问题中的示例那样高效。 - Kieren Johnstone
如果你想编写高效的代码,你可能甚至不会问这个问题,并且始终像处理过程一样维护行号变量。 - Rivenfall

5

如果您需要在同一个长字符串上多次调用该函数,那么这个类非常有用。它会缓存换行位置,以便稍后可以执行O(log(字符串中的换行符数))查找GetLine,并且执行GetOffset时为O(1)。

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;
    }
}

这是我的测试用例:

[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));
}

-2

在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

2
这不是一个Ruby问题。该问题已标记并要求使用C#解决方案。 - Lance U. Matthews
@LanceU.Matthews 同意,只是当我在谷歌中寻找如何从字符串的字节位置找到行源和行位置时(没有指定任何语言),我最终来到了这里,我认为 Ruby 的解决方案更易读,这可能有助于其他人。 - Dorian
1
我看到Ruby在你的顶级标签中排名第一,而C#则不见踪影。当然,Ruby用户会发现Ruby比C#更易读。同样地,C#用户也会发现C#代码更易读且更有用,这就是为什么在这里发布Ruby答案并不实用的原因。即使这是正确的语言,它也只是一个纯代码答案,没有解释它在做什么,所以在这方面也不是很好。 - Lance U. Matthews

-2

对于那些对JavaScript或更迭代的方法感兴趣的人。

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)

1
该问题标记为并要求使用C#解决方案。 - Lance U. Matthews
我知道这个,但将其转换为C#有多难? - Rivenfall

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接