21

I need to track the position of the line that I am reading from the stream reader. When I say reader.ReadLine(), I need to know the position of that line in the file and I also want to be able to then read the file from the position I have previously tracked.

Is this possible?

peterh
  • 11,875
  • 18
  • 85
  • 108
johnnie
  • 1,837
  • 5
  • 23
  • 36

4 Answers4

30

You can do this one of three ways:

1) Write your own StreamReader. Here's a good place to start: How to know position(linenumber) of a streamreader in a textfile?

2) The StreamReader class has two very important, but private variables called charPos and charLen that are needed in locating the actual "read" position and not just the underlying position of the stream. You could use reflection to get the values as suggested here

Int32 charpos = (Int32) s.GetType().InvokeMember("charPos", 
BindingFlags.DeclaredOnly | 
BindingFlags.Public | BindingFlags.NonPublic | 
BindingFlags.Instance | BindingFlags.GetField
 ,null, s, null); 

Int32 charlen= (Int32) s.GetType().InvokeMember("charLen", 
BindingFlags.DeclaredOnly | 
BindingFlags.Public | BindingFlags.NonPublic | 
BindingFlags.Instance | BindingFlags.GetField
 ,null, s, null);

return (Int32)s.BaseStream.Position-charlen+charpos;

3) Simply read the entire file into a string array. Something like this:

char[] CRLF = new char[2] { '\n', '\r' };
TextReader tr = File.OpenText("some path to file");
string[] fileLines = tr.ReadToEnd().Split(CRLF);

Another possibility (along the sames lines as #3) is to read in the lines and store the line in an array. When you want to read the prior line, just use the array.

Community
  • 1
  • 1
Chris Gessler
  • 22,727
  • 7
  • 57
  • 83
  • Thanks for the response, but I have tried out solution #2 but I dont think that that gives me the current position/line I am currently reading with the streamreader.ReadLine() method. And Also is there a way to start reading the file from a specified line/position? Thanks in advance. – johnnie Apr 17 '12 at 12:14
  • @johnnie - possibly by setting the BaseStream.Position = N and using StreamReader.Read(). I think it would be better to keep your own collection of cached lines so you can re-read any line by line number without going back to the file itself. However, this may present memory issues with large files. – Chris Gessler Apr 17 '12 at 12:28
  • thats the problem I am lying with, the file that i need to read from a certain line will be a huge file and reading each line to find a certain position i need to read from will be to intensive. SO i am trying to find a way to get the current position and then save that and read it from there. – johnnie Apr 17 '12 at 12:33
  • @johnnie - go with option 1. There are several examples in the link provided. – Chris Gessler Apr 17 '12 at 12:41
  • 2
    Are you sure option 2) is safe when a 1024*N boundary slices an extended char in 2 pieces? – H H May 09 '12 at 17:50
  • 3
    Option #2 is what I went with, but the code is wrong. What I currently use is here: http://stackoverflow.com/a/17457085/530545 – Granger Jul 11 '13 at 16:01
  • @HenkHolterman - It's safe as long as you only set Position to a valid location and you use DiscardBufferedData() after changing it. As long as you only set Position according to the "actual" position in the Stream where StreamReader is effectively reading from, you'll be at a valid character boundary. – Granger Jul 11 '13 at 16:08
  • 2
    option 2 wont work properly when using an encoding that does not create 1 to 1 encoding for chars to bytes. see my solution here: http://stackoverflow.com/a/22975649/718033 (Edit: my solution seems very similar to Granger's) – Eamon Apr 09 '14 at 23:14
  • If you just want to show a progress bar to show the approximate position when processing a large text file or resource, this is good enough: int iProgress = (int)((sr.BaseStream.Position * 100L) / sr.BaseStream.Length); BaseStream.Position will be slightly ahead of the position of the actual line returned by the stream reader, but if the buffer size is small compared to the file size, the difference is negligable. If the file size is small, the progress will go to 100% too soon, but also processing will be fast, so most users won't care. – Berend Engelbrecht Jul 16 '15 at 11:39
9

Tracking the actual StreamReader position (in bytes):

readonly static FieldInfo charPosField = typeof(StreamReader).GetField("charPos", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
readonly static FieldInfo charLenField = typeof(StreamReader).GetField("charLen", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
readonly static FieldInfo charBufferField = typeof(StreamReader).GetField("charBuffer", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);

static long ActualPosition(StreamReader reader)
{
    var charBuffer = (char[])charBufferField.GetValue(reader);
    var charLen = (int)charLenField.GetValue(reader);
    var charPos = (int)charPosField.GetValue(reader);

    return reader.BaseStream.Position - reader.CurrentEncoding.GetByteCount(charBuffer, charPos, charLen-charPos);
}

You are welcome.

Serge Savel
  • 91
  • 1
  • 3
  • 2
    For .NET Core, all field names have an underscore prefix, see https://source.dot.net/#System.Private.CoreLib/StreamReader.cs,b5fe1efcec14de32 – ygoe Jan 08 '21 at 14:26
3

Maybe this can help you

  public class StreamLineReader : IDisposable
    {
        const int BufferLength = 1024;

        Stream _Base;
        int _Read = 0, _Index = 0;
        byte[] _Bff = new byte[BufferLength];

        long _CurrentPosition = 0;
        int _CurrentLine = 0;

        /// <summary>
        /// CurrentLine number
        /// </summary>
        public long CurrentPosition { get { return _CurrentPosition; } }
        /// <summary>
        /// CurrentLine number
        /// </summary>
        public int CurrentLine { get { return _CurrentLine; } }
        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="stream">Stream</param>
        public StreamLineReader(Stream stream) { _Base = stream; }
        /// <summary>
        /// Count lines and goto line number
        /// </summary>
        /// <param name="goToLine">Goto Line number</param>
        /// <returns>Return true if goTo sucessfully</returns>
        public bool GoToLine(int goToLine) { return IGetCount(goToLine, true) == goToLine; }
        /// <summary>
        /// Count lines and goto line number
        /// </summary>
        /// <param name="goToLine">Goto Line number</param>
        /// <returns>Return the Count of lines</returns>
        public int GetCount(int goToLine) { return IGetCount(goToLine, false); }
        /// <summary>
        /// Internal method for goto&Count
        /// </summary>
        /// <param name="goToLine">Goto Line number</param>
        /// <param name="stopWhenLine">Stop when found the selected line number</param>
        /// <returns>Return the Count of lines</returns>
        int IGetCount(int goToLine, bool stopWhenLine)
        {
            _Base.Seek(0, SeekOrigin.Begin);
            _CurrentPosition = 0;
            _CurrentLine = 0;
            _Index = 0;
            _Read = 0;

            long savePosition = _Base.Length;

            do
            {
                if (_CurrentLine == goToLine)
                {
                    savePosition = _CurrentPosition;
                    if (stopWhenLine) return _CurrentLine;
                }
            }
            while (ReadLine() != null);

            // GoToPosition

            int count = _CurrentLine;

            _CurrentLine = goToLine;
            _Base.Seek(savePosition, SeekOrigin.Begin);

            return count;
        }
        /// <summary>
        /// Read Line
        /// </summary>
        /// <returns></returns>
        public string ReadLine()
        {
            bool found = false;

            StringBuilder sb = new StringBuilder();
            while (!found)
            {
                if (_Read <= 0)
                {
                    // Read next block
                    _Index = 0;
                    _Read = _Base.Read(_Bff, 0, BufferLength);
                    if (_Read == 0)
                    {
                        if (sb.Length > 0) break;
                        return null;
                    }
                }

                for (int max = _Index + _Read; _Index < max; )
                {
                    char ch = (char)_Bff[_Index];
                    _Read--; _Index++;
                    _CurrentPosition++;

                    if (ch == '\0' || ch == '\n')
                    {
                        found = true;
                        break;
                    }
                    else if (ch == '\r') continue;
                    else sb.Append(ch);
                }
            }

            _CurrentLine++;
            return sb.ToString();
        }
        /// <summary>
        /// Free resources
        /// </summary>
        public void Dispose()
        {
            if (_Base != null)
            {
                _Base.Close();
                _Base.Dispose();
                _Base = null;
            }
        }
    }

Use:

 using (StreamLineReader st = new StreamLineReader(File.OpenRead("E:\\log.txt")))
        {
            bool ok = st.GoToLine(1);
            int count= st.GetCount(0);

            string w0 = st.ReadLine();
            string w1 = st.ReadLine();
            string w2 = st.ReadLine();
            string w3 = st.ReadLine();
        }
1

Another pattern you can use is to use a new StreamReader when you want to change positions. This should work with any version of .net and doesn't take too much code.

using var stream = File.Open("file.txt");

using (var reader = new StreamReader(stream, Encoding.UTF8, true, 4096, leaveOpen: true) {
    reader.ReadLine();
}

stream.Seek(0, SeekLocation.Begin);

using (var reader = new StreamReader(stream, Encoding.UTF8, true, 4096, leaveOpen: true) {
    // read the same line again
    reader.ReadLine();
}
Kelly Elton
  • 4,373
  • 10
  • 53
  • 97