1 module stringbuffer;
2 
3 /** A super simple string buffer that will free its string if its gets out of
4 scope. To use it as a OutputRange use the value returned from the writer
5 method. getData will return a string that points to the data stored in the
6 StringBuffer. If the StringBuffer gets cleared up the string returned by
7 getData can no longer be used.
8 */
9 struct StringBufferImpl(int stackLen) {
10 	import core.memory : GC;
11 	char[stackLen] stack;
12 	char* overflow = null;
13 	size_t capacity = stackLen;
14 	size_t length;
15 	bool copied;
16 
17 	~this() @safe {
18 		if(this.overflow !is null) {
19 			() @trusted { 
20 				GC.free(cast(void*)this.overflow); 
21 			}();
22 		}
23 	}
24 
25 	struct OutputRange {
26 		StringBufferImpl!(stackLen)* buf;
27 
28 		void put(const(char) c) @safe {
29 			this.buf.insertBack(c);
30 		}
31 
32 		void put(dchar c) @safe {
33 			this.buf.insertBack(c);
34 		}
35 
36 		void put(const(char)[] s) @safe {
37 			this.buf.insertBack(s);
38 		}
39 
40 		void put(string s) @safe {
41 			this.buf.insertBack(s);
42 		}
43 	}
44 
45 	OutputRange writer() {
46 		return OutputRange(&this);
47 	}
48 
49 	private void putImpl(const(char) c) @trusted {
50 		if(this.length < stackLen) {
51 			this.stack[this.length++] = c;
52 		} else {
53 			if(this.length + 1 >= this.capacity || this.overflow is null) {
54 				this.grow();
55 				assert(this.overflow !is null);
56 			}
57 			this.overflow[this.length++] = c;
58 		}
59 	}
60 
61 	private void grow() @trusted {
62 		this.capacity *= 2;
63 		this.overflow = cast(char*)GC.realloc(this.overflow, this.capacity);
64 		this.copy();
65 	}
66 
67 	private void copy() @trusted {
68 		if(!this.copied) {
69 			for(size_t i = 0; i < stackLen; ++i) {
70 				this.overflow[i] = this.stack[i];
71 			}
72 			this.copied = true;
73 		}
74 	}
75 
76 	void insertBack(const(char) c) @safe {
77 		this.putImpl(c);
78 	}
79 
80 	void insertBack(dchar c) @safe {
81 		import std.utf : encode;
82 		char[4] encoded;
83 		size_t len = encode(encoded, c);
84 		this.insertBack(encoded[0 .. len]);
85 	}
86 
87 	void insertBack(const(char)[] s) @trusted {
88 		if(s.length + this.length < stackLen) {
89 			for(size_t i = 0; i < s.length; ++i) {
90 				this.stack[this.length++] = s[i];
91 			}
92 		} else {
93 			while(s.length + this.length >= this.capacity 
94 					|| this.overflow is null) 
95 			{
96 				this.grow();
97 				assert(this.overflow !is null);
98 			}
99 			this.copy();
100 			for(size_t i = 0; i < s.length; ++i) {
101 				this.overflow[this.length++] = s[i];
102 			}
103 		}
104 	}
105 
106 	void insertBack(string s) @safe {
107 		this.insertBack(cast(const(char)[])s);
108 	}
109 
110 	void removeAll() @safe {
111 		this.length = 0;
112 		this.copied = false;
113 	}
114 
115 	T getData(T = string)() @system {
116 		if(this.length >= stackLen) {
117 			return cast(T)this.overflow[0 .. this.length];
118 		} else {
119 			return cast(T)this.stack[0 .. this.length];
120 		}
121 	}
122 }
123 
124 ///
125 alias StringBuffer = StringBufferImpl!512;
126 
127 unittest {
128 	import std.meta : AliasSeq;
129 
130 	void localTest(int Size)() {
131 		alias SmallStringBuf = StringBufferImpl!Size;
132 		SmallStringBuf buf;
133 		buf.insertBack('c');
134 		buf.insertBack("c");
135 
136 		assert(buf.getData() == "cc");
137 
138 		for(int i = 0; i < 2048; ++i) {
139 			buf.insertBack(cast(dchar)'c');
140 		}
141 
142 		for(int i = 0; i < 2050; ++i) {
143 			assert(buf.getData()[i] == 'c');
144 		}
145 	}
146 
147 	foreach(s; AliasSeq!(1,2,3,7,8,9,10,11,127,255,256,500,512)) {
148 		localTest!(s)();
149 	}
150 }
151 
152 unittest {
153 	StringBuffer buf;
154 	buf.insertBack('ö');
155 	assert(buf.getData() == "ö");
156 
157 	buf.writer().put('ö');
158 	assert(buf.getData() == "öö");
159 }
160 
161 unittest {
162 	StringBuffer buf;
163 	for(int i = 0; i < 1028; ++i) {
164 		buf.insertBack('a');
165 	}
166 
167 	auto s = buf.getData();
168 	for(int i = 0; i < s.length; ++i) {
169 		assert(s[i] == 'a');
170 	}
171 }
172 
173 unittest {
174 	import std.range.primitives : isOutputRange;
175 	static assert(isOutputRange!(typeof(StringBuffer.writer()), char));
176 }
177 
178 unittest {
179 	import std.format : formattedWrite;
180 
181 	StringBuffer buf;
182 	formattedWrite(buf.writer(), "%d", 42);
183 	assert(buf.getData() == "42");
184 }
185 
186 unittest {
187 	import std.format : formattedWrite;
188 
189 	StringBuffer buf;
190 	auto w = buf.writer();
191 	formattedWrite(w, "foobar %d", 10);
192 	assert(buf.getData() == "foobar 10");
193 }
194 
195 unittest {
196 	StringBuffer buf;
197 	auto w = buf.writer();
198 	w.put('a');
199 	assert(buf.getData() == "a");
200 	assert(buf.getData!(byte[])() == cast(byte[])"a");
201 
202 	w.put("fo");
203 	assert(buf.getData!(ubyte[])() == cast(ubyte[])"afo");
204 }
205 
206 unittest {
207 	string s = "0123456789";
208 	string l;
209 	for(int i = 0; i < 1026; ++i) {
210 		l ~= s;
211 	}
212 
213 	StringBuffer buf;
214 	buf.insertBack(l);
215 
216 	string ls = buf.getData();
217 	for(int i = 0; i < ls.length; ++i) {
218 		assert(ls[i] == ('0' + (i % 10)));
219 	}
220 }
221 
222 unittest {
223 	import std.meta : AliasSeq;
224 
225 	void localTest(int Size)() {
226 		alias SmallStringBuf = StringBufferImpl!Size;
227 
228 		SmallStringBuf buf;
229 		assert(buf.overflow is null);
230 		buf.insertBack("0123456789");
231 		buf.insertBack("0123456789");
232 		assert(buf.overflow !is null);
233 		buf.removeAll();
234 		assert(buf.overflow !is null);
235 		assert(buf.length == 0);
236 		assert(buf.copied == false);
237 
238 		buf.insertBack("543210");
239 		assert(buf.length == 6);
240 		assert(buf.overflow !is null);
241 		//assert(buf.overflow[0 .. 20] == "01234567890123456789", 
242 		//	cast(string)buf.overflow[0 .. 20]);
243 		buf.insertBack("98765432109876543210");
244 
245 		assert(buf.overflow !is null);
246 		assert(buf.length == 26);
247 		assert(buf.copied == true);
248 		assert(buf.getData() == "54321098765432109876543210", buf.getData());
249 	}
250 
251 	foreach(s; AliasSeq!(1,2,3,4,5,6,7,8,9,10,11,12)) {
252 		localTest!(s)();
253 	}
254 }