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