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