1 /// Kudos to Juan Manuel Cabo
2 /// http://forum.dlang.org/post/cddkatcqmdtibcmfljff@forum.dlang.org
3 ///
4 /// This piece of code is in public domain.
5 module bio.core.utils.bylinefast;
6 
7 import std.stdio;
8 import std..string: indexOf;
9 import core.stdc..string: memmove;
10 
11 /**
12    Reads by line in an efficient way (10 times faster than File.byLine from std.stdio).
13    This is accomplished by reading entire buffers (fgetc() is not used),
14    and allocating as little as possible.
15 
16    The char \n is considered as separator, removing the previous \r if it exists.
17 
18    The \n is never returned. The \r is not returned if it was
19    part of a \r\n (but it is returned if it was by itself).
20 
21    The returned string is always a substring of a temporary
22    buffer, that must not be stored. If necessary, you must
23    use str[] or .dup or .idup to copy to another string.
24 
25    Example:
26 
27    File f = File("file.txt");
28    foreach (string line; ByLineFast(f)) {
29        ...process line...
30        //Make a copy:
31        string copy = line[];
32    }
33 
34    The file isn't closed when done iterating, unless it was the only reference to
35    the file (same as std.stdio.byLine). (example: ByLineFast(File("file.txt"))).
36 */
37 struct ByLineFast {
38     File file;
39     char[] line;
40     bool first_call = true;
41     char[] buffer;
42     char[] strBuffer;
43 
44     this(File f, int bufferSize=4096) {
45         assert(bufferSize > 0);
46         file = f;
47         buffer.length = bufferSize;
48     }
49 
50     @property bool empty() const {
51         //Its important to check "line !is null" instead of
52         //"line.length != 0", otherwise, no empty lines can
53         //be returned, the iteration would be closed.
54         if (line !is null) {
55             return false;
56         }
57         if (!file.isOpen) {
58             //Clean the buffer to avoid pointer false positives:
59             (cast(char[])buffer)[] = 0;
60             return true;
61         }
62 
63         //First read. Determine if it's empty and put the char back.
64             auto mutableFP = (cast(File*) &file).getFP();
65         auto c = fgetc(mutableFP);
66         if (c == -1) {
67             //Clean the buffer to avoid pointer false positives:
68             (cast(char[])buffer)[] = 0;
69             return true;
70         }
71         if (ungetc(c, mutableFP) != c) {
72             assert(false, "Bug in cstdlib implementation");
73         }
74         return false;
75     }
76 
77     @property char[] front() {
78         if (first_call) {
79             popFront();
80             first_call = false;
81         }
82         return line;
83     }
84 
85     void popFront() {
86         if (strBuffer.length == 0) {
87             strBuffer = file.rawRead(buffer);
88             if (strBuffer.length == 0) {
89                 file.detach();
90                 line = null;
91                 return;
92             }
93         }
94 
95         long pos = strBuffer.indexOf('\n');
96         if (pos != -1) {
97             if (pos != 0 && strBuffer[cast(size_t)pos-1] == '\r') {
98                 line = strBuffer[0 .. cast(size_t)(pos-1)];
99             } else {
100                 line = strBuffer[0 .. cast(size_t)pos];
101             }
102             //Pop the line, skipping the terminator:
103             strBuffer = strBuffer[cast(size_t)(pos+1) .. $];
104         } else {
105             //More needs to be read here. Copy the tail of the buffer
106             //to the beginning, and try to read with the empty part of
107             //the buffer.
108             //If no buffer was left, extend the size of the buffer before
109             //reading. If the file has ended, then the line is the entire
110             //buffer.
111 
112             if (strBuffer.ptr != buffer.ptr) {
113                 //Must use memmove because there might be overlap
114                 memmove(buffer.ptr, strBuffer.ptr,
115                         strBuffer.length * char.sizeof);
116             }
117             auto spaceBegin = strBuffer.length;
118             if (strBuffer.length == buffer.length) {
119                 //Must extend the buffer to keep reading.
120                 assumeSafeAppend(buffer);
121                 buffer.length = buffer.length * 2;
122             }
123             char[] readPart = file.rawRead(buffer[spaceBegin .. $]);
124             if (readPart.length == 0) {
125                 //End of the file. Return whats in the buffer.
126                 //The next popFront() will try to read again, and then
127                 //mark empty condition.
128                 if (spaceBegin != 0 && buffer[spaceBegin-1] == '\r') {
129                     line = buffer[0 .. spaceBegin-1];
130                 } else {
131                     line = buffer[0 .. spaceBegin];
132                 }
133                 strBuffer = null;
134                 return;
135             }
136             strBuffer = buffer[0 .. spaceBegin + readPart.length];
137             //Now that we have new data in strBuffer, we can go on.
138             //If a line isn't found, the buffer will be extended again to read more.
139             popFront();
140         }
141     }
142 }