1 /**
2 Copyright: Copyright (c) 2017-2019 Andrey Penechko.
3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 Authors: Andrey Penechko.
5 */
6 module vox.utils.numfmt;
7 
8 import core.time : Duration;
9 
10 /// Use 'i' format char to get binary prefixes (like Ki, instead of K), only for integers
11 /// Use '#' flag to get greek letter in the output (not compatible with 'i')
12 struct ScaledNumberFmt(T)
13 {
14 	import std.algorithm : min, max;
15 	import std.format : formattedWrite, FormatSpec;
16 	T value;
17 	void toString(scope void delegate(const(char)[]) sink, const ref FormatSpec!char fmt) const
18 	{
19 		if (fmt.spec == 'i') {
20 			// Use binary prefixes instead of decimal prefixes
21 			long intVal = cast(long)value;
22 			int scale = calcScale2(intVal);
23 			double scaledValue = scaled2(value, scale);
24 			int digits = numDigitsInNumber10(scaledValue);
25 			string prefix = scalePrefixesAscii[scaleToScaleIndex2(scale)]; // length is 1 or 0
26 			int width = max(fmt.width - (cast(int)prefix.length * 2), 0); // account for 'i' prefix
27 			int precision = max(min(3-digits, fmt.precision), 0); // gives 0 or 1
28 			string fmtString = (scale == 0) ? "%*.*f%s" : "%*.*f%si";
29 			sink.formattedWrite(fmtString, width, precision, scaledValue, prefix);
30 		} else {
31 			int scale = calcScale10(value);
32 			auto scaledValue = scaled10(value, -scale);
33 			int digits = numDigitsInNumber10(scaledValue);
34 			immutable string[] prefixes = (fmt.flHash) ? scalePrefixesGreek : scalePrefixesAscii;
35 			string prefix = prefixes[scaleToScaleIndex10(scale)]; // length is 1 or 0
36 			int width = max(fmt.width - cast(int)prefix.length, 0);
37 			int precision = max(min(3-digits, fmt.precision), 0); // gives 0 or 1
38 			sink.formattedWrite("%*.*f%s", width, precision, scaledValue, prefix);
39 		}
40 	}
41 }
42 
43 auto scaledNumberFmt(T)(T value)
44 {
45 	return ScaledNumberFmt!T(value);
46 }
47 
48 auto scaledNumberFmt(Duration value, double scale = 1)
49 {
50 	double seconds = value.total!"nsecs" / 1_000_000_000.0;
51 	return ScaledNumberFmt!double(seconds * scale);
52 }
53 
54 // -30 .. 30, with step of 3. Or -10 to 10 with step of 1
55 immutable string[] scalePrefixesAscii = ["q","r","y","z","a","f","p","n","u","m","","K","M","G","T","P","E","Z","Y","R","Q"];
56 immutable string[] scalePrefixesGreek = ["q","r","y","z","a","f","p","n","µ","m","","K","M","G","T","P","E","Z","Y","R","Q"];
57 enum NUM_SCALE_PREFIXES = 10;
58 enum MIN_SCALE_PREFIX = -30;
59 enum MAX_SCALE_PREFIX = 30;
60 
61 
62 int numDigitsInNumber10(Num)(const Num val)
63 {
64 	import std.math: abs, round;
65 	ulong absVal = cast(ulong)val.abs.round;
66 	int numDigits = 1;
67 
68 	while (absVal >= 10)
69 	{
70 		absVal /= 10;
71 		++numDigits;
72 	}
73 
74 	return numDigits;
75 }
76 
77 private int signum(T)(const T x) pure nothrow
78 {
79 	return (x > 0) - (x < 0);
80 }
81 
82 /// Returns number in range of [-30; 30]
83 int calcScale10(Num)(Num val)
84 {
85 	import std.math: abs, round, log10;
86 
87 	// cast to double is necessary in case of long.min, which overflows integral abs
88 	auto lg = log10(abs(cast(double)val));
89 
90 	// handle very small values and zero
91 	if (lg == -double.infinity) return 0;
92 
93 	double absLog = abs(lg);
94 	int scale = cast(int)(round(absLog/3.0))*3;
95 
96 	int logSign = signum(lg);
97 	int clampedScale = scale * logSign;
98 
99 	// we want
100 	//  0.9994 to be formatted as 999m
101 	//  0.9995 to be formatted as 1.0
102 	//  0.9996 to be formatted as 1.0
103 	if (abs(scaled10(val, -clampedScale)) < 0.9995) clampedScale -= 3;
104 
105 	if (clampedScale < MIN_SCALE_PREFIX)
106 		clampedScale = 0; // prevent zero, or values smaller that min scale to display with min scale
107 	else if (clampedScale > MAX_SCALE_PREFIX)
108 		clampedScale = MAX_SCALE_PREFIX;
109 
110 	return clampedScale;
111 }
112 
113 /// Returns number in range of [0; 100]
114 int calcScale2(Num)(Num val)
115 {
116 	import std.math: abs, round, log2;
117 
118 	auto lg = log2(abs(val));
119 	double absLog = abs(lg);
120 
121 	int scale = cast(int)(round(absLog/10.0))*10;
122 
123 	int logSign = signum(lg);
124 	int clampedScale = scale * logSign;
125 
126 	// we want
127 	//  0.9994 to be formatted as 999m
128 	//  0.9995 to be formatted as 1.0
129 	//  0.9996 to be formatted as 1.0
130 	if (abs(scaled2(val, clampedScale)) < 0.9995) clampedScale -= 10;
131 
132 	if (clampedScale < 0)
133 		clampedScale = 0; // negative scale should not happen for binary numbers
134 	else if (clampedScale > MAX_SCALE_PREFIX)
135 		clampedScale = MAX_SCALE_PREFIX;
136 
137 	return clampedScale;
138 }
139 
140 int scaleToScaleIndex10(int scale) {
141 	return scale / 3 + NUM_SCALE_PREFIXES; // -30...30 -> -10...10 -> 0...20
142 }
143 
144 int scaleToScaleIndex2(int scale) {
145 	return scale / 10 + NUM_SCALE_PREFIXES; // -100...100 -> -10...10 -> 0...20
146 }
147 
148 double scaled10(Num)(Num num, int scale)
149 {
150 	import std.math: pow;
151 	return num * pow(10.0, scale);
152 }
153 
154 double scaled2(Num)(Num num, int scale)
155 {
156 	double divisor = 1 << scale;
157 	return num / divisor;
158 }
159 
160 // Criteria:
161 // Should not produce `0.xx`
162 // must be `xxxm` instead
163 unittest
164 {
165 	void test(T)(T num, string expected, string file = __MODULE__, int line = __LINE__) {
166 		import std.format;
167 		string res = format("%s", scaledNumberFmt(num));
168 		assert(res == expected,
169 			format("%s:%s %s != %s", file, line, res, expected));
170 	}
171 
172 	test(-10_000_000_000_000_000_000_000_000_000_000_000.0, "-10000Q");
173 	test(-1_000_000_000_000_000_000_000_000_000_000_000.0, "-1000Q");
174 	test(-100_000_000_000_000_000_000_000_000_000_000.0, "-100Q");
175 	test(-10_000_000_000_000_000_000_000_000_000_000.0, "-10.0Q");
176 	test(-1_000_000_000_000_000_000_000_000_000_000.0, "-1.00Q");
177 	test(-100_000_000_000_000_000_000_000_000_000.0, "-100R");
178 	test(-10_000_000_000_000_000_000_000_000_000.0, "-10.0R");
179 	test(-1_000_000_000_000_000_000_000_000_000.0, "-1.00R");
180 	test(-100_000_000_000_000_000_000_000_000.0, "-100Y");
181 	test(-10_000_000_000_000_000_000_000_000.0, "-10.0Y");
182 	test(-1_000_000_000_000_000_000_000_000.0, "-1.00Y");
183 	test(-100_000_000_000_000_000_000_000.0, "-100Z");
184 	test(-10_000_000_000_000_000_000_000.0, "-10.0Z");
185 	test(-1_000_000_000_000_000_000_000.0, "-1.00Z");
186 	test(-100_000_000_000_000_000_000.0, "-100E");
187 	test(-10_000_000_000_000_000_000.0, "-10.0E");
188 
189 	test(long.min, "-9.22E");
190 	test(-9_223_372_036_854_775_807, "-9.22E");
191 	test(-1_000_000_000_000_000_000, "-1.00E");
192 	test(-100_000_000_000_000_000, "-100P");
193 	test(-10_000_000_000_000_000, "-10.0P");
194 	test(-1_000_000_000_000_000, "-1.00P");
195 	test(-100_000_000_000_000, "-100T");
196 	test(-10_000_000_000_000, "-10.0T");
197 	test(-1_000_000_000_000, "-1.00T");
198 	test(-100_000_000_000, "-100G");
199 	test(-10_000_000_000, "-10.0G");
200 	test(-1_000_000_000, "-1.00G");
201 	test(-100_000_000, "-100M");
202 	test(-10_000_000, "-10.0M");
203 	test(-1_000_000, "-1.00M");
204 	test(-100_000, "-100K");
205 	test(-10_000, "-10.0K");
206 	test(-1_000, "-1.00K");
207 	test(-100, "-100");
208 	test(-10, "-10.0");
209 	test(-1, "-1.00");
210 	test(0, "0.00");
211 	test(1, "1.00");
212 	test(10, "10.0");
213 	test(100, "100");
214 	test(1_000, "1.00K");
215 	test(10_000, "10.0K");
216 	test(100_000, "100K");
217 	test(1_000_000, "1.00M");
218 	test(10_000_000, "10.0M");
219 	test(100_000_000, "100M");
220 	test(1_000_000_000, "1.00G");
221 	test(10_000_000_000, "10.0G");
222 	test(100_000_000_000, "100G");
223 	test(1_000_000_000_000, "1.00T");
224 	test(10_000_000_000_000, "10.0T");
225 	test(100_000_000_000_000, "100T");
226 	test(1_000_000_000_000_000, "1.00P");
227 	test(10_000_000_000_000_000, "10.0P");
228 	test(100_000_000_000_000_000, "100P");
229 	test(1_000_000_000_000_000_000, "1.00E");
230 	test(ulong.max, "18.4E");
231 
232 	test(10_000_000_000_000_000_000.0, "10.0E");
233 	test(100_000_000_000_000_000_000.0, "100E");
234 	test(1_000_000_000_000_000_000_000.0, "1.00Z");
235 	test(10_000_000_000_000_000_000_000.0, "10.0Z");
236 	test(100_000_000_000_000_000_000_000.0, "100Z");
237 	test(1_000_000_000_000_000_000_000_000.0, "1.00Y");
238 	test(10_000_000_000_000_000_000_000_000.0, "10.0Y");
239 	test(100_000_000_000_000_000_000_000_000.0, "100Y");
240 	test(1_000_000_000_000_000_000_000_000_000.0, "1.00R");
241 	test(10_000_000_000_000_000_000_000_000_000.0, "10.0R");
242 	test(100_000_000_000_000_000_000_000_000_000.0, "100R");
243 	test(1_000_000_000_000_000_000_000_000_000_000.0, "1.00Q");
244 	test(10_000_000_000_000_000_000_000_000_000_000.0, "10.0Q");
245 	test(100_000_000_000_000_000_000_000_000_000_000.0, "100Q");
246 	test(1_000_000_000_000_000_000_000_000_000_000_000.0, "1000Q");
247 	test(10_000_000_000_000_000_000_000_000_000_000_000.0, "10000Q");
248 
249 	// numbers less than 1.0 or close to 1
250 	test(1.234, "1.23");
251 	test(1.000, "1.00");
252 	test(0.9994, "999m");
253 	test(0.9995, "1.00");
254 	test(0.9996, "1.00");
255 	test(0.1234, "123m");
256 	test(0.01234, "12.3m");
257 	test(0.001234, "1.23m");
258 	test(0.0001234, "123u");
259 
260 	test(-1.234, "-1.23");
261 	test(-1.000, "-1.00");
262 	test(-0.9994, "-999m");
263 	test(-0.9995, "-1.00");
264 	test(-0.9996, "-1.00");
265 	test(-0.1234, "-123m");
266 	test(-0.01234, "-12.3m");
267 	test(-0.001234, "-1.23m");
268 	test(-0.0001234, "-123u");
269 }