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 }