1 module bytesize;
2 
3 import std.meta : AliasSeq;
4 
5 import std.math : pow;
6 import std.format : FormatSpec, formatValue;
7 
8 ///
9 struct ByteSize
10 {
11 	/// Converts this byte size to a string, use %f for base 1024 or %F for base 1000
12 	void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) const
13 	{
14 		int base = 1024;
15 		if (fmt.precision == FormatSpec!char.UNSPECIFIED)
16 			fmt.precision = 2;
17 
18 		if (fmt.spec == 's')
19 			fmt.spec = 'f';
20 		else if (fmt.spec == 'S')
21 		{
22 			base = 1000;
23 			fmt.spec = 'f';
24 		}
25 
26 		static immutable string PrefixList = "_KMGTPE";
27 
28 		long tmp = bytes;
29 		int order;
30 		while (tmp > (base - 1))
31 		{
32 			tmp /= base;
33 			order++;
34 		}
35 		// as a long can only hold up to 8EiB we don't need to index check here
36 		double val = bytes / pow(cast(double) base, order);
37 		if (order > 0)
38 			formatValue(sink, val, fmt);
39 		else
40 		{
41 			auto ifmt = fmt;
42 			ifmt.spec = 'd';
43 			ifmt.precision = 1;
44 			formatValue(sink, bytes, ifmt);
45 		}
46 		sink(" ");
47 		if (order > 0)
48 		{
49 			sink([PrefixList[order]]);
50 			if (base == 1024)
51 				sink("i");
52 		}
53 		sink("B");
54 	}
55 
56 	string toString() const
57 	{
58 		const(char)[] s;
59 		toString((d) { s ~= d; }, FormatSpec!char("%s"));
60 		return cast(string) s;
61 	}
62 
63 @safe pure:
64 	/++
65 		A $(D ByteSize) of $(D 0). It's shorter than doing something like
66 		$(D size!"bytes"(0)) and more explicit than $(D ByteSize.init).
67 	+/
68 	static @property nothrow @nogc ByteSize zero()
69 	{
70 		return ByteSize(0);
71 	}
72 
73 	/++
74 			Largest $(D ByteSize) possible.
75 		+/
76 	static @property nothrow @nogc ByteSize max()
77 	{
78 		return ByteSize(long.max);
79 	}
80 
81 	/++
82 			Most negative $(D ByteSize) possible.
83 		+/
84 	static @property nothrow @nogc ByteSize min()
85 	{
86 		return ByteSize(long.min);
87 	}
88 
89 	///
90 	long bytes;
91 
92 	int opCmp(ByteSize rhs) const nothrow @nogc
93 	{
94 		if (bytes < rhs.bytes)
95 			return -1;
96 		if (bytes > rhs.bytes)
97 			return 1;
98 		return 0;
99 	}
100 
101 	bool opEquals(ByteSize b) const nothrow @nogc
102 	{
103 		return bytes == b.bytes;
104 	}
105 
106 	hash_t toHash() const nothrow @nogc
107 	{
108 		return cast(hash_t) bytes;
109 	}
110 
111 	///
112 	bool isNegative() const nothrow @nogc
113 	{
114 		return bytes < 0;
115 	}
116 
117 	///
118 	T total(string unit, T = long)() const nothrow @nogc
119 	{
120 		return cast(T)(bytes / cast(T) bytesInUnit!unit);
121 	}
122 
123 	///
124 	auto opBinary(string op)(ByteSize rhs) const
125 	{
126 		return ByteSize(mixin("bytes " ~ op ~ " rhs.bytes"));
127 	}
128 
129 	///
130 	auto opUnary(string op)() const
131 	{
132 		return ByteSize(mixin(op ~ "bytes"));
133 	}
134 }
135 
136 static assert(__traits(isPOD, ByteSize));
137 
138 ///
139 unittest
140 {
141 	import std.format;
142 
143 	assert(1.bytes.toString == "1 B");
144 	assert(1.KiB.toString == "1.00 KiB");
145 	assert(1.MiB.toString == "1.00 MiB");
146 	assert(1.GiB.toString == "1.00 GiB");
147 	assert(1.TiB.toString == "1.00 TiB");
148 	assert(1.PiB.toString == "1.00 PiB");
149 	assert(1.EiB.toString == "1.00 EiB");
150 
151 	assert(1.bytes.format!"%S" == "1 B");
152 	assert(1.KB.format!"%S" == "1.00 KB");
153 	assert(1.MB.format!"%S" == "1.00 MB");
154 	assert(1.GB.format!"%S" == "1.00 GB");
155 	assert(1.TB.format!"%S" == "1.00 TB");
156 	assert(1.PB.format!"%S" == "1.00 PB");
157 	assert(1.EB.format!"%S" == "1.00 EB");
158 
159 	assert("%g".format(1024.bytes) == "1 KiB");
160 	assert("%.2f".format(2_590_000.bytes) == "2.47 MiB");
161 }
162 
163 /// Returns the number of bytes per one unit
164 enum bytesInUnit(string unit) = {
165 	switch (unit)
166 	{
167 	case "bytes":
168 		return 1;
169 	case "KB":
170 		return 1000L;
171 	case "KiB":
172 		return 1024L;
173 	case "MB":
174 		return 1000L * 1000L;
175 	case "MiB":
176 		return 1024L * 1024L;
177 	case "GB":
178 		return 1000L * 1000L * 1000L;
179 	case "GiB":
180 		return 1024L * 1024L * 1024L;
181 	case "TB":
182 		return 1000L * 1000L * 1000L * 1000L;
183 	case "TiB":
184 		return 1024L * 1024L * 1024L * 1024L;
185 	case "PB":
186 		return 1000L * 1000L * 1000L * 1000L * 1000L;
187 	case "PiB":
188 		return 1024L * 1024L * 1024L * 1024L * 1024L;
189 	case "EB":
190 		return 1000L * 1000L * 1000L * 1000L * 1000L * 1000L;
191 	case "EiB":
192 		return 1024L * 1024L * 1024L * 1024L * 1024L * 1024L;
193 	default:
194 		assert(false,
195 				"Can only use bytes, KB, KiB, MB, MiB, GB, GiB, TB, TiB, PB, PiB, EB, EiB as units");
196 	}
197 }();
198 
199 //dfmt off
200 // only doing this so auto completion helps you...
201 
202 /// Convenience method to convert to a ByteSize using `bytesInUnit`
203 ByteSize bytes(long l) { return ByteSize(l); }
204 /// ditto
205 ByteSize KB(long l) { return ByteSize(l * bytesInUnit!"KB"); }
206 /// ditto
207 ByteSize KiB(long l) { return ByteSize(l * bytesInUnit!"KiB"); }
208 /// ditto
209 ByteSize MB(long l) { return ByteSize(l * bytesInUnit!"MB"); }
210 /// ditto
211 ByteSize MiB(long l) { return ByteSize(l * bytesInUnit!"MiB"); }
212 /// ditto
213 ByteSize GB(long l) { return ByteSize(l * bytesInUnit!"GB"); }
214 /// ditto
215 ByteSize GiB(long l) { return ByteSize(l * bytesInUnit!"GiB"); }
216 /// ditto
217 ByteSize TB(long l) { return ByteSize(l * bytesInUnit!"TB"); }
218 /// ditto
219 ByteSize TiB(long l) { return ByteSize(l * bytesInUnit!"TiB"); }
220 /// ditto
221 ByteSize PB(long l) { return ByteSize(l * bytesInUnit!"PB"); }
222 /// ditto
223 ByteSize PiB(long l) { return ByteSize(l * bytesInUnit!"PiB"); }
224 /// ditto
225 ByteSize EB(long l) { return ByteSize(l * bytesInUnit!"EB"); }
226 /// ditto
227 ByteSize EiB(long l) { return ByteSize(l * bytesInUnit!"EiB"); }
228 
229 /// ditto
230 ByteSize size(string unit)(long l) { return ByteSize(l * bytesInUnit!unit); }
231 //dfmt on