1 /**
2  * Simple (ASCII-only) text-processing functions,
3  * for speed and CTFE.
4  *
5  * License:
6  *   This Source Code Form is subject to the terms of
7  *   the Mozilla Public License, v. 2.0. If a copy of
8  *   the MPL was not distributed with this file, You
9  *   can obtain one at http://mozilla.org/MPL/2.0/.
10  *
11  * Authors:
12  *   Vladimir Panteleev <ae@cy.md>
13  */
14 
15 module ae.utils.text.ascii;
16 
17 import std.ascii;
18 import std.algorithm : max;
19 import std.traits : Unqual, isSigned;
20 
21 import ae.utils.array : contains;
22 
23 // ************************************************************************
24 
25 /// Semantic alias for an array of immutable bytes containing some
26 /// ASCII-based 8-bit character encoding. Might be UTF-8, but not
27 /// necessarily - thus, is a semantic superset of the D "string" alias.
28 alias string ascii;
29 
30 // ************************************************************************
31 
32 /// Maximum number of characters needed to fit the decimal
33 /// representation of any number of this basic integer type.
34 template DecimalSize(T : ulong)
35 {
36 	alias _U = Unqual!T;
37 	///
38 	static if (is(_U == ubyte))
39 		enum DecimalSize = 3;
40 	else
41 	static if (is(_U == byte))
42 		enum DecimalSize = 4;
43 	else
44 	static if (is(_U == ushort))
45 		enum DecimalSize = 5;
46 	else
47 	static if (is(_U == short))
48 		enum DecimalSize = 6;
49 	else
50 	static if (is(_U == uint))
51 		enum DecimalSize = 10;
52 	else
53 	static if (is(_U == int))
54 		enum DecimalSize = 11;
55 	else
56 	static if (is(_U == ulong))
57 		enum DecimalSize = 20;
58 	else
59 	static if (is(_U == long))
60 		enum DecimalSize = 20;
61 	else
62 		static assert(false, "Unknown type for DecimalSize");
63 }
64 
65 unittest
66 {
67 	template DecimalSize2(T : ulong)
68 	{
69 		import std.conv : text;
70 		enum DecimalSize2 = max(text(T.min).length, text(T.max).length);
71 	}
72 
73 	static assert(DecimalSize!ubyte == DecimalSize2!ubyte);
74 	static assert(DecimalSize!byte == DecimalSize2!byte);
75 	static assert(DecimalSize!ushort == DecimalSize2!ushort);
76 	static assert(DecimalSize!short == DecimalSize2!short);
77 	static assert(DecimalSize!uint == DecimalSize2!uint);
78 	static assert(DecimalSize!int == DecimalSize2!int);
79 	static assert(DecimalSize!ulong == DecimalSize2!ulong);
80 	static assert(DecimalSize!long == DecimalSize2!long);
81 
82 	static assert(DecimalSize!(const(long)) == DecimalSize!long);
83 }
84 
85 /// Writes n as decimal number to buf (right-aligned), returns slice of buf containing result.
86 char[] toDec(N : ulong, size_t U)(N o, ref char[U] buf) pure
87 {
88 	static assert(U >= DecimalSize!N, "Buffer too small to fit any " ~ N.stringof ~ " value");
89 
90 	Unqual!N n = o;
91 	char* p = buf.ptr+buf.length;
92 
93 	if (isSigned!N && n<0)
94 	{
95 		do
96 		{
97 			*--p = '0' - n%10;
98 			n = n/10;
99 		} while (n);
100 		*--p = '-';
101 	}
102 	else
103 		do
104 		{
105 			*--p = '0' + n%10;
106 			n = n/10;
107 		} while (n);
108 
109 	return p[0 .. buf.ptr + buf.length - p];
110 }
111 
112 /// CTFE-friendly variant.
113 char[] toDecCTFE(N : ulong, size_t U)(N o, ref char[U] buf)
114 {
115 	static assert(U >= DecimalSize!N, "Buffer too small to fit any " ~ N.stringof ~ " value");
116 
117 	Unqual!N n = o;
118 	size_t p = buf.length;
119 
120 	if (isSigned!N && n<0)
121 	{
122 		do
123 		{
124 			buf[--p] = '0' - n%10;
125 			n = n/10;
126 		} while (n);
127 		buf[--p] = '-';
128 	}
129 	else
130 		do
131 		{
132 			buf[--p] = '0' + n%10;
133 			n = n/10;
134 		} while (n);
135 
136 	return buf[p..$];
137 }
138 
139 /// Basic integer-to-string conversion.
140 string toDec(T : ulong)(T n)
141 {
142 	if (__ctfe)
143 	{
144 		char[DecimalSize!T] buf;
145 		return toDecCTFE(n, buf).idup;
146 	}
147 	else
148 	{
149 		static struct Buf { char[DecimalSize!T] buf; } // Can't put static array on heap, use struct
150 		return toDec(n, (new Buf).buf);
151 	}
152 }
153 
154 unittest
155 {
156 	import std.conv : to;
157 	assert(toDec(42) == "42");
158 	assert(toDec(int.min) == int.min.to!string());
159 	static assert(toDec(42) == "42", toDec(42));
160 }
161 
162 /// Print an unsigned integer as a zero-padded, right-aligned decimal number into a buffer
163 void toDecFixed(N : ulong, size_t U)(N n, ref char[U] buf)
164 	if (!isSigned!N)
165 {
166 	import std.meta : Reverse;
167 	import ae.utils.meta : RangeTuple;
168 
169 	enum limit = 10^^U;
170 	assert(n < limit, "Number too large");
171 
172 	foreach (i; Reverse!(RangeTuple!U))
173 	{
174 		buf[i] = cast(char)('0' + (n % 10));
175 		n /= 10;
176 	}
177 }
178 
179 /// ditto
180 char[U] toDecFixed(size_t U, N : ulong)(N n)
181 	if (!isSigned!N)
182 {
183 	char[U] buf;
184 	toDecFixed(n, buf);
185 	return buf;
186 }
187 
188 unittest
189 {
190 	assert(toDecFixed!6(12345u) == "012345");
191 }
192 
193 // ************************************************************************
194 
195 /// Basic string-to-integer conversion.
196 /// Doesn't check for overflows.
197 T fromDec(T)(string s)
198 {
199 	static if (isSigned!T)
200 	{
201 		bool neg;
202 		if (s.length && s[0] == '-')
203 		{
204 			neg = true;
205 			s = s[1..$];
206 		}
207 	}
208 
209 	T n;
210 	foreach (i, c; s)
211 	{
212 		if (c < '0' || c > '9')
213 			throw new Exception("Bad digit");
214 		n = n * 10 + cast(T)(c - '0');
215 	}
216 	static if (isSigned!T)
217 		if (neg)
218 			n = -n;
219 	return n;
220 }
221 
222 unittest
223 {
224 	assert(fromDec!int("456") == 456);
225 	assert(fromDec!int("-42") == -42);
226 }
227 
228 // ************************************************************************
229 
230 /// Returns `true` if `s` does not contain any characters which are not in `chars`.
231 bool containsOnlyChars(string s, string chars)
232 {
233 	foreach (c; s)
234 		if (!chars.contains(c))
235 			return false;
236 	return true;
237 }
238 
239 /// Returns `true` if `s` contains only digits and is non-empty.
240 bool isUnsignedInteger(string s)
241 {
242 	foreach (c; s)
243 		if (c < '0' || c > '9')
244 			return false;
245 	return s.length > 0;
246 }
247 
248 /// Returns `true` if `s` contains only digits
249 /// (excluding an optional leading '-') and is non-empty.
250 bool isSignedInteger(string s)
251 {
252 	return s.length && isUnsignedInteger(s[0] == '-' ? s[1..$] : s);
253 }
254 
255 // ************************************************************************
256 
257 private __gshared char[256] asciiLower, asciiUpper;
258 
259 shared static this()
260 {
261 	foreach (c; 0..256)
262 	{
263 		asciiLower[c] = cast(char)std.ascii.toLower(c);
264 		asciiUpper[c] = cast(char)std.ascii.toUpper(c);
265 	}
266 }
267 
268 void _xlat(alias TABLE, T)(T[] buf)
269 {
270 	foreach (ref c; buf)
271 		c = TABLE[c];
272 }
273 
274 /// Lowercases or uppercases a string in-place.
275 alias _xlat!(asciiLower, char) asciiToLower;
276 alias _xlat!(asciiUpper, char) asciiToUpper; /// ditto