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 }