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 deprecated alias DecimalSize = decimalSize;
66 
67 unittest
68 {
69 	template decimalSize2(T : ulong)
70 	{
71 		import std.conv : text;
72 		enum decimalSize2 = max(text(T.min).length, text(T.max).length);
73 	}
74 
75 	static assert(decimalSize!ubyte == decimalSize2!ubyte);
76 	static assert(decimalSize!byte == decimalSize2!byte);
77 	static assert(decimalSize!ushort == decimalSize2!ushort);
78 	static assert(decimalSize!short == decimalSize2!short);
79 	static assert(decimalSize!uint == decimalSize2!uint);
80 	static assert(decimalSize!int == decimalSize2!int);
81 	static assert(decimalSize!ulong == decimalSize2!ulong);
82 	static assert(decimalSize!long == decimalSize2!long);
83 
84 	static assert(decimalSize!(const(long)) == decimalSize!long);
85 }
86 
87 /// Writes n as decimal number to buf (right-aligned), returns slice of buf containing result.
88 char[] toDec(N : ulong, size_t U)(N o, ref char[U] buf) pure
89 {
90 	static assert(U >= decimalSize!N, "Buffer too small to fit any " ~ N.stringof ~ " value");
91 
92 	Unqual!N n = o;
93 	char* p = buf.ptr+buf.length;
94 
95 	if (isSigned!N && n<0)
96 	{
97 		do
98 		{
99 			*--p = '0' - n%10;
100 			n = n/10;
101 		} while (n);
102 		*--p = '-';
103 	}
104 	else
105 		do
106 		{
107 			*--p = '0' + n%10;
108 			n = n/10;
109 		} while (n);
110 
111 	return p[0 .. buf.ptr + buf.length - p];
112 }
113 
114 /// CTFE-friendly variant.
115 char[] toDecCTFE(N : ulong, size_t U)(N o, ref char[U] buf)
116 {
117 	static assert(U >= decimalSize!N, "Buffer too small to fit any " ~ N.stringof ~ " value");
118 
119 	Unqual!N n = o;
120 	size_t p = buf.length;
121 
122 	if (isSigned!N && n<0)
123 	{
124 		do
125 		{
126 			buf[--p] = '0' - n%10;
127 			n = n/10;
128 		} while (n);
129 		buf[--p] = '-';
130 	}
131 	else
132 		do
133 		{
134 			buf[--p] = '0' + n%10;
135 			n = n/10;
136 		} while (n);
137 
138 	return buf[p..$];
139 }
140 
141 /// Basic integer-to-string conversion.
142 string toDec(T : ulong)(T n)
143 {
144 	if (__ctfe)
145 	{
146 		char[decimalSize!T] buf;
147 		return toDecCTFE(n, buf).idup;
148 	}
149 	else
150 	{
151 		static struct Buf { char[decimalSize!T] buf; } // Can't put static array on heap, use struct
152 		return toDec(n, (new Buf).buf);
153 	}
154 }
155 
156 unittest
157 {
158 	import std.conv : to;
159 	assert(toDec(42) == "42");
160 	assert(toDec(int.min) == int.min.to!string());
161 	static assert(toDec(42) == "42", toDec(42));
162 }
163 
164 /// Print an unsigned integer as a zero-padded, right-aligned decimal number into a buffer
165 void toDecFixed(N : ulong, size_t U)(N n, ref char[U] buf)
166 	if (!isSigned!N)
167 {
168 	import std.meta : Reverse;
169 	import ae.utils.meta : rangeTuple;
170 
171 	enum limit = 10^^U;
172 	assert(n < limit, "Number too large");
173 
174 	foreach (i; Reverse!(rangeTuple!U))
175 	{
176 		buf[i] = cast(char)('0' + (n % 10));
177 		n /= 10;
178 	}
179 }
180 
181 /// ditto
182 char[U] toDecFixed(size_t U, N : ulong)(N n)
183 	if (!isSigned!N)
184 {
185 	char[U] buf;
186 	toDecFixed(n, buf);
187 	return buf;
188 }
189 
190 unittest
191 {
192 	assert(toDecFixed!6(12345u) == "012345");
193 }
194 
195 // ************************************************************************
196 
197 /// Basic string-to-integer conversion.
198 /// Doesn't check for overflows.
199 T fromDec(T)(string s)
200 {
201 	static if (isSigned!T)
202 	{
203 		bool neg;
204 		if (s.length && s[0] == '-')
205 		{
206 			neg = true;
207 			s = s[1..$];
208 		}
209 	}
210 
211 	T n;
212 	foreach (i, c; s)
213 	{
214 		if (c < '0' || c > '9')
215 			throw new Exception("Bad digit");
216 		n = n * 10 + cast(T)(c - '0');
217 	}
218 	static if (isSigned!T)
219 		if (neg)
220 			n = -n;
221 	return n;
222 }
223 
224 unittest
225 {
226 	assert(fromDec!int("456") == 456);
227 	assert(fromDec!int("-42") == -42);
228 }
229 
230 // ************************************************************************
231 
232 /// Returns `true` if `s` does not contain any characters which are not in `chars`.
233 bool containsOnlyChars(string s, string chars)
234 {
235 	foreach (c; s)
236 		if (!chars.contains(c))
237 			return false;
238 	return true;
239 }
240 
241 /// Returns `true` if `s` contains only digits and is non-empty.
242 bool isUnsignedInteger(string s)
243 {
244 	foreach (c; s)
245 		if (c < '0' || c > '9')
246 			return false;
247 	return s.length > 0;
248 }
249 
250 /// Returns `true` if `s` contains only digits
251 /// (excluding an optional leading '-') and is non-empty.
252 bool isSignedInteger(string s)
253 {
254 	return s.length && isUnsignedInteger(s[0] == '-' ? s[1..$] : s);
255 }
256 
257 // ************************************************************************
258 
259 private __gshared char[256] asciiLower, asciiUpper;
260 
261 shared static this()
262 {
263 	foreach (c; 0..256)
264 	{
265 		asciiLower[c] = cast(char)std.ascii.toLower(c);
266 		asciiUpper[c] = cast(char)std.ascii.toUpper(c);
267 	}
268 }
269 
270 void _xlat(alias TABLE, T)(T[] buf)
271 {
272 	foreach (ref c; buf)
273 		c = TABLE[c];
274 }
275 
276 /// Lowercases or uppercases a string in-place.
277 alias _xlat!(asciiLower, char) asciiToLower;
278 alias _xlat!(asciiUpper, char) asciiToUpper; /// ditto