1 // Written in the D programming language
2 /++
3 This small library allows to easily format file sizes in a human-readable
4 format.
5 
6 Copyright: Copyright 2014, Nicolas Sicard
7 Authors: Nicolas Sicard
8 License: $(LINK www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
9 Source: $(LINK https://github.com/biozic/sizefmt)
10 +/
11 module sizefmt;
12 
13 import std.format;
14 import std.traits;
15 version (unittest) import std.string;
16 //debug import std.stdio;
17 
18 /++
19 Wraps a size value of type $(D ulong) which is formatted appropriately
20 when output as text.
21 +/
22 struct SizeBase(Config config)
23 {
24     /// The size that should be formatted.
25     ulong value;
26     
27     /++
28     Formats the size according to the format fmt, automatically choosing the
29     prefix and performing the unit conversion.
30 
31     The size is formatted as a floating point value, so fmt has to be a
32     floating-point-value format specification (s, f, F, e, E, g, G, a or A).
33     But when the unit is just bytes, it is formatted as an integer.
34     +/
35     void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const
36     {
37         import std.algorithm, std.array;
38         
39         // Override defaults for 's' spec.
40         if (fmt.spec == 's')
41         {
42             fmt.spec = 'f';
43             fmt.precision = 2;
44             if (config.spacing == Spacing.tabular)
45                 fmt.width = max(7, fmt.precision);
46         }
47         
48         // List of prefixes (the first _ is for no prefix,
49         // the second _ if for kilo, which is special cased.
50         static immutable string PrefixList = "__MGTPEZY";
51         
52         static if (config.prefixUse == PrefixUse.decimal)
53             double base = 1000;
54         else
55             double base = 1024;
56         
57         int order = 0;
58         double tmp = value;
59         while (tmp > (base - 1))
60         {
61             order++;
62             tmp /= base;
63         }
64         order = min(order, PrefixList.length);
65         
66         // Output the numeric value
67         if (order > 0)
68             sink.formatValue(value / base^^order, fmt);
69         else
70             sink.formattedWrite("%*d", fmt.width, value);
71         
72         // Prepare the unit part
73         static app = appender!(char[]);
74         
75         static if (config.spacing != Spacing.none)
76             app.put(" ");
77         
78         if (order > 0)
79         {
80             if (order == 1)
81                 app.put(config.prefixUse == PrefixUse.decimal ? "k" : "K");
82             else
83                 app.put(PrefixList[order .. order + 1]);
84             
85             static if (config.prefixUse == PrefixUse.IEC)
86                 app.put("i");
87         }
88         
89         static if (config.useNameIfNoPrefix)
90         {
91             if (order == 0)
92                 app.put(value == 1 ? config.unitName : config.unitNamePlural);
93             else
94                 app.put(config.symbol);
95         }
96         else
97             app.put(config.symbol);
98         
99         // Output the unit part
100         static if (config.spacing == Spacing.tabular)
101             sink.formattedWrite("%-*s", 1 + config.maxUnitLength, app.data);
102         else
103             sink(app.data);
104         
105         app.clear();
106     }
107 }
108 
109 /// Size struct with default options.
110 alias Size = SizeBase!(Config.init);
111 ///
112 unittest
113 {
114     assert("%s".format(Size(1)) == "1 B");
115     assert("%s".format(Size(42)) == "42 B");
116     assert("%g".format(Size(1024)) == "1 KB");
117     assert("%.2f".format(Size(2_590_000)) == "2.47 MB");
118 }
119 
120 static assert(__traits(isPOD, Size));
121 
122 /++
123 Configuration of size format.
124 +/
125 struct Config
126 {
127     /// The symbol of the size unit.
128     string symbol = "B";
129     
130     /// The name of the size unit (singular).
131     string unitName = "byte"; 
132     
133     /// The name of the size unit (plural).
134     string unitNamePlural = "bytes"; 
135     
136     /// The type of prefix used along with the symbol.
137     PrefixUse prefixUse = PrefixUse.binary; 
138     
139     /// The spacing between the value and the unit.
140     Spacing spacing = Spacing.singleSpace; 
141     
142     /// Whether to use the name of the symbol if there is no prefix.
143     bool useNameIfNoPrefix = false; 
144 
145     private size_t maxUnitLength()
146     {
147         import std.algorithm : max;
148         return max(
149             useNameIfNoPrefix ? max(unitName.length, unitNamePlural.length) : 0,
150             1 + (prefixUse == PrefixUse.IEC ? 1 : 0) + symbol.length
151         );
152     }
153 }
154 
155 /++
156 Builds a configuration from a JSON-like string.
157 +/
158 template config(string configString)
159 {
160     enum config = {
161         mixin("Config ret = " ~ configString ~ ";");
162         return ret;
163     }();
164 }
165 ///
166 unittest
167 {
168     alias MySize = SizeBase!(config!`{
169         symbol: "O",
170         unitName: "octet",
171         unitNamePlural: "octets",
172         prefixUse: PrefixUse.IEC,
173         spacing: Spacing.tabular,
174         useNameIfNoPrefix: true
175     }`);
176     
177     assert("|%4.1f|".format(MySize(1))         == "|   1 octet |");
178     assert("|%4.1f|".format(MySize(42))        == "|  42 octets|");
179     assert("|%4.1f|".format(MySize(1024))      == "| 1.0 KiO   |");
180     assert("|%4.1f|".format(MySize(2_590_000)) == "| 2.5 MiO   |");
181 }
182 
183 /++
184 The use of prefix when formatting long size values.
185 +/
186 enum PrefixUse
187 {
188     /++
189     Sizes will be formatted using the traditional _binary prefixes, e.g. 1024
190     bytes = 1 kilobyte = 1 KB.
191     +/
192     binary,
193     
194     /++
195     Sizes will be formatted using the _IEC recommandations for binary prefixes
196     equivalent of a multiple of 1024, e.g. 1024 bytes = kibibyte = 1 KiB.
197     +/
198     IEC,
199     
200     /++
201     Sizes will be formatted using _decimal prefixes, e.g. 1024 bytes = 1.024
202     kilobyte = 1.024 kB.
203     +/
204     decimal
205 }
206 
207 /++
208 The type of spacing around the symbol of the size unit.
209 +/
210 enum Spacing
211 {
212     /++
213     No space between the value and the unit.
214     +/
215     none,
216     
217     /++
218     A single space between the value and the unit.
219     +/
220     singleSpace,
221     
222     /++
223     The right amount of space so that sizes can be vertically aligned in a
224     table. In order to achieve this vertical alignement, the value itself must
225     be formatted as a fixed-size string.
226     +/
227     tabular
228 }