1 /******************************************************************************* 2 3 Functions for generating thousands separated string representations of a 4 number. 5 6 Usage: 7 8 --- 9 10 import ocean.text.util.DigitGrouping; 11 12 // Number to convert 13 const number = 12345678; 14 15 // Generating a thousands separated string. 16 char[] number_as_string; 17 DigitGrouping.format(number, number_as_string); 18 19 // Checking how many characters would be required for a thousands 20 // separated number. 21 cont max_len = 10; 22 assert(DigitGrouping.length(number) <= max_len); 23 24 --- 25 26 Copyright: 27 Copyright (c) 2009-2016 dunnhumby Germany GmbH. 28 All rights reserved. 29 30 License: 31 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 32 Alternatively, this file may be distributed under the terms of the Tango 33 3-Clause BSD License (see LICENSE_BSD.txt for details). 34 35 *******************************************************************************/ 36 37 module ocean.text.util.DigitGrouping; 38 39 40 41 42 import ocean.meta.types.Qualifiers; 43 44 import ocean.core.TypeConvert; 45 46 import ocean.core.Array; 47 48 import ocean.text.util.MetricPrefix; 49 50 import ocean.meta.traits.Basic; 51 52 import ocean.text.convert.Formatter; 53 54 55 56 /******************************************************************************* 57 58 Digit grouping class -- just a container for static functions. 59 60 *******************************************************************************/ 61 62 public class DigitGrouping 63 { 64 private alias typeof(this) This; 65 66 67 /*************************************************************************** 68 69 Calculates the number of characters in the string representation of a 70 thousands separated number. 71 72 Note: this method is faster than generating the string then checking its 73 .length property. 74 75 Params: 76 T = type of number 77 num = number to work out length of 78 79 Returns: 80 number of characters in the string representation of the thousands 81 separated number 82 83 ***************************************************************************/ 84 85 public static size_t length ( T ) ( T num ) 86 { 87 static assert(isIntegerType!(T), This.stringof ~ ".length - only works with integer types"); 88 89 bool negative = num < 0; 90 if ( negative ) num = -num; 91 92 // Calculate the number of digits in the number. 93 size_t len = 1; // every number has at least 1 digit 94 do 95 { 96 num /= 10; 97 if ( num > 0 ) 98 { 99 len++; 100 } 101 } 102 while ( num > 0); 103 104 // Extra characters for any thousands separating commas required. 105 if ( len > 3 ) 106 { 107 len += (len - 1) / 3; 108 } 109 110 // An extra character for a minus sign. 111 if ( negative ) len++; 112 113 return len; 114 } 115 116 117 /*************************************************************************** 118 119 Formats a number to a string, with comma separation every 3 digits 120 121 Params: 122 T = type of number 123 num = number to be formatted 124 output = string in which to store the formatted number 125 126 Returns: 127 formatted string 128 129 ***************************************************************************/ 130 131 public static mstring format ( T ) ( T num, ref mstring output ) 132 { 133 static assert(isIntegerType!(T), This.stringof ~ ".format - only works with integer types"); 134 135 output.length = 0; 136 137 char[20] string_buf; // 20 characters is enough to store ulong.max 138 size_t layout_pos; 139 140 void layoutSink ( cstring s ) 141 { 142 string_buf[layout_pos .. layout_pos + s.length] = s[]; 143 layout_pos += s.length; 144 } 145 146 // Format number into a string 147 sformat(&layoutSink, "{}", num); 148 mstring num_as_string = string_buf[0.. layout_pos]; 149 150 bool comma; 151 size_t left = 0; 152 size_t right = left + 3; 153 size_t first_comma; 154 155 // Handle negative numbers 156 if ( num_as_string[0] == '-' ) 157 { 158 output.append("-"[]); 159 num_as_string = num_as_string[1..$]; 160 } 161 162 // Find position of first comma 163 if ( num_as_string.length > 3 ) 164 { 165 comma = true; 166 first_comma = num_as_string.length % 3; 167 168 if ( first_comma > 0 ) 169 { 170 right = first_comma; 171 } 172 } 173 174 // Copy chunks of the formatted number into the destination string, with commas 175 do 176 { 177 if ( right >= num_as_string.length ) 178 { 179 right = num_as_string.length; 180 comma = false; 181 } 182 183 mstring digits = num_as_string[left..right]; 184 if ( comma ) 185 { 186 output.append(digits, ","[]); 187 } 188 else 189 { 190 output.append(digits); 191 } 192 193 left = right; 194 right = left + 3; 195 } 196 while( left < num_as_string.length ); 197 198 return output; 199 } 200 } 201 202 version (unittest) 203 { 204 import ocean.core.Test : test; 205 } 206 207 unittest 208 { 209 test!("==")(DigitGrouping.length(-100000), "-100,000".length); 210 test!("==")(DigitGrouping.length( -10000), "-10,000".length); 211 test!("==")(DigitGrouping.length( -1000), "-1,000".length); 212 test!("==")(DigitGrouping.length( -100), "-100".length); 213 test!("==")(DigitGrouping.length( -10), "-10".length); 214 test!("==")(DigitGrouping.length( -0), "0".length); 215 test!("==")(DigitGrouping.length( 0), "0".length); 216 test!("==")(DigitGrouping.length( 10), "10".length); 217 test!("==")(DigitGrouping.length( 100), "100".length); 218 test!("==")(DigitGrouping.length( 1000), "1,000".length); 219 test!("==")(DigitGrouping.length( 10000), "10,000".length); 220 test!("==")(DigitGrouping.length( 100000), "100,000".length); 221 test!("==")(DigitGrouping.length(1000000), "1,000,000".length); 222 223 mstring buf; 224 225 test!("==")(DigitGrouping.format(-100000, buf), "-100,000"[]); 226 test!("==")(DigitGrouping.format( -10000, buf), "-10,000"[]); 227 test!("==")(DigitGrouping.format( -1000, buf), "-1,000"[]); 228 test!("==")(DigitGrouping.format( -100, buf), "-100"[]); 229 test!("==")(DigitGrouping.format( -10, buf), "-10"[]); 230 test!("==")(DigitGrouping.format( -0, buf), "0"[]); 231 test!("==")(DigitGrouping.format( 0, buf), "0"[]); 232 test!("==")(DigitGrouping.format( 10, buf), "10"[]); 233 test!("==")(DigitGrouping.format( 100, buf), "100"[]); 234 test!("==")(DigitGrouping.format( 1000, buf), "1,000"[]); 235 test!("==")(DigitGrouping.format( 10000, buf), "10,000"[]); 236 test!("==")(DigitGrouping.format( 100000, buf), "100,000"[]); 237 test!("==")(DigitGrouping.format(1000000, buf), "1,000,000"[]); 238 } 239 240 241 242 /******************************************************************************* 243 244 Binary digit grouping class -- just a container for static functions. 245 246 *******************************************************************************/ 247 248 public class BitGrouping 249 { 250 /*************************************************************************** 251 252 Formats a number to a string, with binary prefix (K, M, T, etc) every 253 10 bits. 254 255 Params: 256 num = number to be formatted 257 output = string in which to store the formatted number 258 unit = string, describing the type of unit represented by the 259 number, to be appended after each binary prefix 260 261 Returns: 262 formatted string 263 264 ***************************************************************************/ 265 266 public static mstring format ( ulong num, ref mstring output, cstring unit = null ) 267 { 268 output.length = 0; 269 assumeSafeAppend(output); 270 271 if ( num == 0 ) 272 { 273 sformat(output, "0{}", unit); 274 } 275 else 276 { 277 void format ( char prefix, uint order, ulong order_val ) 278 { 279 if ( order_val > 0 ) 280 { 281 if ( order == 0 ) 282 { 283 sformat(output, "{}{}", order_val, unit); 284 } 285 else 286 { 287 sformat(output, "{}{}{} ", order_val, prefix, unit); 288 } 289 } 290 else if ( order_val == 0 && order == 0 ) 291 { 292 // Get rid of the additional space that was appended. 293 294 output = output[0 .. $ - 1]; 295 } 296 } 297 298 splitBinaryPrefix(num, &format); 299 } 300 301 return output; 302 } 303 } 304 305 unittest 306 { 307 mstring buf; 308 309 test!("==")(BitGrouping.format(0, buf), "0"[]); 310 test!("==")(BitGrouping.format(0, buf, "X"), "0X"[]); 311 312 test!("==")(BitGrouping.format(1000, buf), "1000"[]); 313 test!("==")(BitGrouping.format(1000, buf, "X"), "1000X"[]); 314 315 test!("==")(BitGrouping.format(1024, buf), "1K"[]); 316 test!("==")(BitGrouping.format(1024, buf, "M"), "1KM"[]); 317 318 test!("==")(BitGrouping.format(1025, buf), "1K 1"[]); 319 test!("==")(BitGrouping.format(1025, buf, "TEST"), "1KTEST 1TEST"[]); 320 321 test!("==")(BitGrouping.format(10000, buf), "9K 784"[]); 322 test!("==")(BitGrouping.format(10000, buf, "X"), "9KX 784X"[]); 323 324 test!("==")(BitGrouping.format(1000000, buf), "976K 576"[]); 325 test!("==")(BitGrouping.format(1000000, buf, "X"), "976KX 576X"[]); 326 327 test!("==")(BitGrouping.format(10000000, buf), "9M 549K 640"[]); 328 test!("==")(BitGrouping.format(10000000, buf, "X"), "9MX 549KX 640X"[]); 329 330 test!("==")(BitGrouping.format(10000000000, buf), "9G 320M 761K"[]); 331 test!("==")(BitGrouping.format(10000000000, buf, "X"), "9GX 320MX 761KX"[]); 332 } 333