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-2015, 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 : min, max;
38         import std.math : pow;
39 
40         // Override defaults for 's' spec.
41         if (fmt.spec == 's')
42         {
43             fmt.spec = 'f';
44             fmt.precision = 2;
45         }
46         
47         // List of prefixes (the first _ is for no prefix,
48         // the second _ if for kilo, which is special cased.
49         static immutable string PrefixList = "__MGTPEZY";
50         
51         static if (config.prefixUse == PrefixUse.decimal)
52             double base = 1000;
53         else
54             double base = 1024;
55         
56         int order = 0;
57         double tmp = value;
58         while (tmp > (base - 1))
59         {
60             ++order;
61             tmp /= base;
62         }
63         order = min(order, PrefixList.length);
64         
65         // Output the numeric value
66         if (order > 0)
67             sink.formatValue(value / pow(base, order), fmt);
68         else
69         {
70             auto ifmt = fmt;
71             ifmt.spec = 'd';
72             ifmt.precision = 1;
73             sink.formatValue(value, ifmt);
74         }
75 
76         sink(config.spacing);
77         
78         if (order > 0)
79         {
80             if (order == 1)
81                 sink(config.prefixUse == PrefixUse.decimal ? "k" : "K");
82             else
83                 sink(PrefixList[order .. order + 1]);
84             
85             static if (config.prefixUse == PrefixUse.IEC)
86                 sink("i");
87         }
88         
89         static if (config.useNameIfNoPrefix)
90         {
91             if (order == 0)
92                 sink(value <= 1 ? config.unitName : config.unitNamePlural);
93             else
94                 sink(config.symbol);
95         }
96         else
97             sink(config.symbol);
98     }
99 }
100 
101 /// Size struct with default options.
102 alias Size = SizeBase!(Config.init);
103 ///
104 unittest
105 {
106     assert("%s".format(Size(0)) == "0 B");
107     assert("%s".format(Size(1)) == "1 B");
108     assert("%s".format(Size(42)) == "42 B");
109     assert("%g".format(Size(1024)) == "1 KB");
110     assert("%.2f".format(Size(2_590_000)) == "2.47 MB");
111 }
112 
113 static assert(__traits(isPOD, Size));
114 
115 /++
116 Configuration of size format.
117 +/
118 struct Config
119 {
120     /// The symbol of the size unit.
121     string symbol = "B";
122     
123     /// The name of the size unit (singular).
124     string unitName = "byte"; 
125     
126     /// The name of the size unit (plural).
127     string unitNamePlural = "bytes"; 
128     
129     /// The type of prefix used along with the symbol.
130     PrefixUse prefixUse = PrefixUse.binary; 
131     
132     /// The spacing between the value and the unit.
133     string spacing = " "; 
134     
135     /// Whether to use the name of the symbol if there is no prefix.
136     bool useNameIfNoPrefix = false; 
137 
138     private size_t maxUnitLength()
139     {
140         import std.algorithm : max;
141         return max(
142             useNameIfNoPrefix ? max(unitName.length, unitNamePlural.length) : 0,
143             1 + (prefixUse == PrefixUse.IEC ? 1 : 0) + symbol.length
144         );
145     }
146 }
147 ///
148 unittest
149 {
150     enum Config config = {
151         symbol: "O",
152         unitName: "octet",
153         unitNamePlural: "octets",
154         prefixUse: PrefixUse.IEC,
155         useNameIfNoPrefix: true
156     };
157 
158 	alias MySize = SizeBase!config;
159     
160     assert("%4.1f".format(MySize(0))         == "   0 octet");
161     assert("%4.1f".format(MySize(1))         == "   1 octet");
162     assert("%4.1f".format(MySize(42))        == "  42 octets");
163     assert("%4.1f".format(MySize(1024))      == " 1.0 KiO");
164     assert("%4.1f".format(MySize(2_590_000)) == " 2.5 MiO");
165 }
166 
167 /++
168 The use of prefix when formatting long size values.
169 +/
170 enum PrefixUse
171 {
172     /++
173     Sizes will be formatted using the traditional _binary prefixes, e.g. 1024
174     bytes = 1 kilobyte = 1 KB.
175     +/
176     binary,
177     
178     /++
179     Sizes will be formatted using the _IEC recommandations for binary prefixes
180     equivalent of a multiple of 1024, e.g. 1024 bytes = kibibyte = 1 KiB.
181     +/
182     IEC,
183     
184     /++
185     Sizes will be formatted using _decimal prefixes, e.g. 1024 bytes = 1.024
186     kilobyte = 1.024 kB.
187     +/
188     decimal
189 }