DFortune

Unix fortune-cookie parser written in D
git clone git://xatko.vsos.ethz.ch/DFortune.git
Log | Files | Refs

commit d9724293af943b481c6411ec33cad55ae87a0f25
parent fdeabfa4ab946086d5ab8547fb2a7f38c200cfaa
Author: Dominik Schmidt <das1993@hotmail.com>
Date:   Mon, 22 Feb 2016 22:58:27 +0100

Mayor rewrite.

Don't implement the range-stuff myself, use a construct like
```D
iota(0,length).map!(a=>getCookie(a))
```
which leverages the random access range properties of iota.

Also, rewrite the Aggregate-type to do something similar.

Last but not least, Documentation!

Diffstat:
dfortune.d | 447++++++++++++++++++++++++++++++++++++++-----------------------------------------
1 file changed, 213 insertions(+), 234 deletions(-)

diff --git a/dfortune.d b/dfortune.d @@ -1,19 +1,23 @@ module dfortune; -import std.stdio; -import std.conv; -import std.bitmanip; -import std.encoding; +import std.stdio : File; +import std.bitmanip : bigEndianToNative; +import std.algorithm : map; +import core.exception : RangeError; import std.range; -import std.algorithm; -import core.exception; - -static struct Cookie{ - uint pos; - uint size; -} -class Fortune : RandomAccessFinite!(Cookie){ - static struct Header{ +/** + * A struct for retrieving single cookies from a cookiefile + * + * Examples: + * -------- + * Fortune f; + * f.open("file"); + * scope(exit) f.close(); + * writeln(f.strings[0]); + * -------- + */ +struct Fortune{ + private static struct Header{ uint version_; uint numstr; uint longlen; @@ -21,6 +25,7 @@ class Fortune : RandomAccessFinite!(Cookie){ uint flags; char delim; void read(File f){ + f.seek(0); ubyte[getOffset()] off; auto red=f.rawRead(off); version_=bigEndianToNative!(uint,uint.sizeof)(red[0*uint.sizeof..1*uint.sizeof]); @@ -34,46 +39,92 @@ class Fortune : RandomAccessFinite!(Cookie){ return 5*uint.sizeof+char.sizeof; } } - Header header; - string basename; - File table,content; - private EncodingScheme src; - private EncodingScheme dst; - uint pos,end; + invariant{ + assert(!(table.isOpen ^ content.isOpen)); + } - auto @property strings(){ - return this.map!(a=>read(a)); + /** + * A struct containing Information of a cookie inside a cookiefile + */ + static struct Cookie{ + ///The absolute position inside the textfile + size_t pos; + ///The length of the cookie in bytes + size_t size; } - auto strings(T)(T r) if(isInputRange!(T) && is(ElementType!T == Cookie)){ - return r.map!(a=>read(a)); + + private Header header; + private File table,content; + + /** + * Returns a random access range iterating over all cookies inside + * the cookiefile + * + * Returns: A random access Range iterating over all Cookie structures + */ + @property auto range(){ + return iota(0,header.numstr).map!(a=>getCookie(a)); + } + unittest{ + static assert(isRandomAccessRange!(typeof(Fortune.range))); } - this(string file){ - basename=file; - src=EncodingScheme.create("UTF-8"); - dst=EncodingScheme.create(encodingName!(char)); - initialize(); + /** + * Returns a random access range iterating over the text of all cookies inside + * the cookiefile + * + * Returns: A random access Range iterating over all cookies + */ + @property auto strings(){ + return range.map!(a=>read(a)); } - ~this(){ - if(table.isOpen || content.isOpen){ - close(); - } + unittest{ + static assert(isRandomAccessRange!(typeof(Fortune.strings))); } + /** + * Initializes the cookiefile pointing to the path file. + * + * file and file.dat have to exist! + * + * Params: + * file: The file to open. + */ + this(string file){ + open(file); + } + + /** + * Reads and caches the header of the opened dat-file + */ public void initialize(){ - open(); - table.seek(0); header.read(table); - end=header.numstr-1; } - public void open(){ + /** + * Opens "file" and "file.dat", and reads the header. + * + * Params: + * basename: The basename of the files to open + */ + public void open(string basename){ table.open(basename~".dat","r"); content.open(basename,"r"); + initialize(); } - public uint getOffset(uint i){ + /** + * Gets the start of the i-th fortunecookie, or the end(=the size) of + * the file, if i is one out of bound. + * This ensures, that we can get the end of the last cookie. + * + * Params: + * i: The number of the cookie + * Returns: The offset of the i-th fortunecookie + * Throws: RangeError, if i is out of range + */ + private size_t getOffset(uint i){ if(i>header.numstr){ throw new RangeError("Range violation"); } @@ -81,259 +132,187 @@ class Fortune : RandomAccessFinite!(Cookie){ return cast(uint)content.size; } table.seek(Header.getOffset()+(i+1)*4-1); + assert(table.isOpen); ubyte[uint.sizeof] b; table.rawRead(b); return bigEndianToNative!(uint,uint.sizeof)(b); } - public string sliceFile(uint pos, uint end) + /** + * Gets the content from the opened file (See open) from pos to end. + * + * Params: + * pos: The start position to read + * end: The position to read to. + * Returns: A GC-Allocated char-array containing the specified slice + * of the opened cookiefile + */ + public char[] sliceFile(size_t pos, size_t end) in{ - assert(table.isOpen && content.isOpen, "Fortunes must be opened before reading"); + assertOpen(); } body{ if(pos==end){ - return ""; + return null; } content.seek(pos); ubyte[] buf=new ubyte[end-pos]; content.rawRead(buf); - if(src==dst){ - return cast(string) buf; - } - - char[] str; - str.reserve(buf.length); - const(ubyte)[] derp=buf; - while(derp.length>0){ - ubyte[4] bufbuf; - ubyte written; - written=cast(ubyte)dst.encode(src.safeDecode(derp),bufbuf); - str~=bufbuf[0..written]; - } - - return cast(string)str; + return cast(char[])buf; } - public string read(uint i){ + /** + * Reads the i-th cookie + * Params: + * i: The cookie-index + * Returns: A GC-Allocated char[] buffer. + */ + public char[] read(uint i){ Cookie c=getCookie(i); return sliceFile(c.pos,c.pos+c.size); } - public Cookie getCookie(uint i){ + + /** + * Reads the boundaries of the i-th cookie. + * + * Params: + * i: THe cookie-index + * Returns: A Cookie struct denoting the position of the i-th cookie + * inside the cookiefile + */ + public Cookie getCookie(uint i) + in{ + assertOpen(); + } + body{ if(i>=header.numstr){ throw new RangeError("Range violation"); } - uint pos=getOffset(i); - uint end=getOffset(i+1)-1; //-1 to exclude \n + size_t pos=getOffset(i); + size_t end=getOffset(i+1)-1; //-1 to exclude \n if(end>pos+2){ end-=2; //to exclude the following %\n } return Cookie(pos, end-pos); } - public string read(in ref Cookie c){ + + /** + * Reads the boundaries of the i-th cookie. + * + * Params: + * i: THe cookie-index + * Returns: A Cookie struct denoting the position of the i-th cookie + * inside the cookiefile + */ + public char[] read(in ref Cookie c){ return sliceFile(c.pos,c.pos+c.size); } + /** + * Closes all opened files + * + * file and file.dat from the previous call to open, that is. + */ public void close(){ table.close(); content.close(); } - private Fortune clone(){ - Fortune c=new Fortune(this.basename); - - c.header=header; - c.table=table; - c.content=content; - c.src=src; - c.dst=dst; - c.pos=pos; - c.end=end; - return c; - } - @property size_t length() const{ - return end-pos+1; + /** + * Wether the files are opened + */ + @property private bool isOpen(){ + return table.isOpen && content.isOpen; } - Fortune opSlice(size_t a, size_t b){ - Fortune c=clone(); - c.pos=cast(uint)a; - c.end=cast(uint)b; - return c; + ///Ditto + private void assertOpen(){ + assert(isOpen, "Fortune must be opened before reading"); } - @property Cookie back(){ - return getCookie(end); - } - Cookie moveBack(){ - return back; - } - void popBack(){ - end--; - } - int opApply(int delegate(Cookie)f){ - int ret; - immutable uint pos=this.pos; - while(!empty()){ - ret=f(front()); - popFront(); - if(ret){ - break; - } - } - this.pos=pos; - return ret; - } - Cookie moveFront(){ - return getCookie(pos); - } - @property bool empty() const{ - return (length==0); - } - @property Cookie front(){ - return moveFront(); - } - void popFront(){ - pos++; - } - int opApply(int delegate(size_t,Cookie)f){ - int ret; - size_t cnt=0; - immutable uint pos=this.pos; - while(!empty()){ - ret=f(cnt++, front()); - popFront(); - if(ret){ - break; - } +} + +private auto rajoiner(Range)(Range r)if(isInputRange!(Range) && isRandomAccessRange!(ElementType!(Range))){ + static auto get(Range r, size_t index){ + while(index>=r.front.length){ + index-=r.front.length; + r.popFront(); } - this.pos=pos; - return ret; - } - Cookie moveAt(size_t i){ - pos=cast(uint)i; - return getCookie(pos); - } - Cookie opIndex(size_t i){ - return getCookie(cast(uint)i); - } - - @property Fortune save(){ - return clone(); - } - unittest{ - static assert(isRandomAccessRange!(Fortune)); + + return r.front[index]; } -} -struct CookieResult{ - Cookie c; - Fortune f; - alias c this; - string toString(){ - return f.read(c); + size_t length; + foreach(rr;r){ + length+=rr.length; } + return iota(0,length).map!(a=>get(r,a)); } + +/** + * An Fortune aggregate type + * + * Allows to load multiple fortunefiles and treat it as one. + * + * Examples: + * -------- + * Fortunes f; + * f~=Fortune("./fvl"); + * f~=Fortune("./bofh_excuses"); + * f.strings.take(10).joiner("\n"); + * f.close(); + * -------- + */ struct Fortunes{ Fortune[] fortunes; - size_t pos,end; + alias put=add; + /** + * Adds a new Fortunefile by struct + */ void add(Fortune f){ fortunes~=f; - end+=f.length-1; - } - auto @property strings(){ - return map!(a=>a.toString())(this); - } - auto static strings(T)(T r) if(isInputRange!T && is(ElementType!T==CookieResult)){ - return map!(a=>a.toString())(r); } - alias put=add; + + /** + * Adds a new Fortunefile by file-string + */ void add(string f){ - Fortune n=new Fortune(f); - n.initialize(); - add(n); + add(Fortune(f)); } + + /** + * Adds a range of new fortunes + */ void add(T)(T r) if(isInputRange!(T) && (is(ElementType!(T)==string) || is(ElementType!(T)==Fortune) )){ while(!r.empty){ - add(r.moveFront()); + add(r.front); r.popFront(); } } - CookieResult opIndex(size_t i){ - foreach(Fortune f; fortunes.save){ - if(i>=f.length){ - i-=f.length; - } - else{ - return CookieResult(f.opIndex(i),f); - } - } - throw new RangeError("Out of bounds"); - } - alias opDollar=length; - @property size_t length(){ - size_t size; - foreach(Fortune f; fortunes) - size+=f.length; - return size; - } - @property CookieResult front(){ - return opIndex(pos); - } - @property CookieResult back(){ - return opIndex(end); - } - CookieResult moveAt(size_t i){ - pos=i; - return opIndex(pos); - } - Fortunes opSlice(size_t a, size_t b){ - return Fortunes(fortunes, a, b); - } - @property Fortunes save(){ - return Fortunes(fortunes, pos, end); - } - CookieResult moveFront(){ - return front(); - } - CookieResult moveBack(){ - return back(); + ref Fortunes opOpAssign(string op)(Fortune f) if(op=="~"){ + add(f); + return this; } - void popBack(){ - end--; - } - int opApply(int delegate(CookieResult)f){ - int ret; - immutable size_t pos=this.pos; - while(!empty()){ - ret=f(moveFront()); - popFront(); - if(ret){ - break; - } - } - this.pos=pos; - return ret; + + + /** + * Returns the aggregate cookie-struct range + */ + @property auto range(){ + return fortunes.map!((ref a)=>a.range).rajoiner; + } + /** + * Returns the aggregate cookie-string range + */ + @property auto strings(){ + return fortunes.map!((ref a)=>a.strings).rajoiner; } - int opApply(int delegate(size_t,CookieResult)f){ - int ret; - size_t i; - immutable size_t pos=this.pos; - while(!empty()){ - ret=f(i++,moveFront()); - popFront(); - if(ret){ - break; - } + + /** + * Closes all the fortunefiles + */ + void close(){ + foreach(f;fortunes){ + f.close(); } - this.pos=pos; - return ret; - } - @property bool empty() const{ - return (pos>end); - } - void popFront(){ - pos++; - } - unittest{ - static assert(isRandomAccessRange!(Fortunes)); } }