1 /++
2  + Print human readable units (e.g. time in days, hours, minutes or distance in km, m, cm, mm).
3  +/
4 module unit;
5 
6 /++
7  + A unit allows to easily print mixed resolution values.
8  + Typical examples include time (with hours, minutes, ...)
9  + and distances (with km, m, cm, mm).
10  + The unitclass simplifies definition of such things as well
11  + as transforming a high resolution value, to a supposedly
12  + more human readable representation.
13  + e.g. 3_610_123 would convert to 1h 0m 10s 123ms.
14  +/
15 public struct Unit
16 {
17     import std.algorithm.iteration;
18     import std.range;
19 
20     /++
21      + A scale is one resolution of a unit.
22      +/
23     public struct Scale
24     {
25         /// the name of the scale (e.g. h for hour)
26         string name;
27 
28         /++ factor to the next higher resolution (e.g. 60 from minutes to seconds) +/
29         long factor;
30 
31         /++ normal renderwidth for the application (e.g. 2 for minutes (00-59)) +/
32         int digits;
33     }
34 
35     /++ factory for Scale
36      + Params:
37      + name = unitname
38      + factor = factor to the next bigger unit
39      + digits = padding digits
40      +/
41     static auto scale(string name, long factor, int digits = 1)
42     {
43         return Scale(name, factor, digits);
44     }
45 
46     /++
47      + One part of the transformed Unit.
48      + a part of a unit is e.g. the minute resolution of a duration.
49      +/
50     public struct Part
51     {
52         /// name of the part
53         string name;
54         /// value of the part
55         long value;
56         /// number of digits
57         int digits;
58         /// convenient tostring function. e.g. 10min
59         auto toString()
60         {
61             import std.conv;
62 
63             return value.to!(string) ~ name;
64         }
65     }
66 
67     /// name of the unit
68     private string name;
69     /// resolutions of the unit
70     private Scale[] scales;
71 
72     public this(string name, Scale[] scales)
73     {
74         import std.exception;
75 
76         this.name = name;
77         this.scales = cumulativeFold!((result, x) => scale(x.name,
78                 result.factor * x.factor, x.digits))(scales).array.retro.array;
79         enforce(__ctfe);
80     }
81 
82     /++
83      + transforms the unit to its parts
84      +/
85     public auto transform(long v) immutable
86     {
87         import std.array;
88 
89         auto res = appender!(Part[]);
90         auto tmp = v;
91         foreach (Scale scale; scales)
92         {
93             auto h = tmp / scale.factor;
94             tmp = v % scale.factor;
95             res.put(Part(scale.name, h, scale.digits));
96         }
97         return res.data;
98     }
99 
100     private static auto parseNumberAndUnit(string s)
101     {
102         import std.ascii;
103 
104         string value;
105         while (!s.empty)
106         {
107             auto n = s.front;
108             if (n == ' ')
109             {
110                 s.popFront;
111                 continue;
112             }
113             if (isDigit(n))
114             {
115                 value ~= n;
116                 s.popFront;
117             }
118             else
119             {
120                 break;
121             }
122         }
123         string name;
124         while (!s.empty)
125         {
126             auto n = s.front;
127             if (n == ' ')
128             {
129                 s.popFront;
130                 continue;
131             }
132             if (isAlpha(n))
133             {
134                 name ~= n;
135                 s.popFront;
136             }
137             else
138             {
139                 break;
140             }
141         }
142 
143         auto rest = s;
144         import std.typecons;
145 
146         if ((name.length > 0) && (value.length > 0))
147         {
148             return tuple!("found", "name", "value", "rest")(true, name, value, rest);
149         }
150         else
151         {
152             return tuple!("found", "name", "value", "rest")(false, "", "", "");
153         }
154     }
155 
156     long parse(string s) immutable
157     {
158         import std.ascii;
159 
160         long res = 0;
161         auto next = parseNumberAndUnit(s);
162         while (next.found)
163         {
164             import std.algorithm;
165 
166             auto scale = scales.find!(i => i.name == next.name);
167             if (scale.empty)
168             {
169                 throw new Exception("unknown unit " ~ next.name);
170             }
171             import std.conv;
172 
173             res += std.conv.to!(long)(next.value) * scale.front.factor;
174             next = parseNumberAndUnit(next.rest);
175         }
176         return res;
177     }
178 }
179 
180 @("parse") unittest
181 {
182     import unit_threaded;
183 
184     TIME.parse("1s 2ms").shouldEqual(1002);
185     TIME.parse("1s2ms").shouldEqual(1002);
186     TIME.parse("1blub2ms").shouldThrow;
187 }
188 
189 @("creatingScales") unittest
190 {
191     import unit_threaded;
192 
193     auto s = Unit.scale("ttt", 1, 2);
194     s.digits.shouldEqual(2);
195 
196     s = Unit.scale("ttt2", 1);
197     s.digits.shouldEqual(1);
198 }
199 
200 /++
201  + get only relevant parts of an part array.
202  + relevant means all details starting from the first
203  + non 0 part.
204  +/
205 auto onlyRelevant(Unit.Part[] parts)
206 {
207     import std.array;
208 
209     auto res = appender!(Unit.Part[]);
210     bool needed = false;
211     foreach (part; parts)
212     {
213         if (needed || (part.value > 0))
214         {
215             needed = true;
216         }
217         if (needed)
218         {
219             res.put(part);
220         }
221     }
222     return res.data;
223 }
224 
225 /++
226  + get the first nr of parts (or less if not enough parts are available).
227  +/
228 auto mostSignificant(Unit.Part[] parts, long nr)
229 {
230     import std.algorithm.comparison;
231 
232     auto max = min(parts.length, nr);
233     return parts[0 .. max];
234 }
235 
236 /++
237  + example for a time unit definition
238  +/
239 @("basicUsage") unittest
240 {
241     import unit_threaded;
242 
243     auto res = TIME.transform(1 + 2 * 1000 + 3 * 1000 * 60 + 4 * 1000 * 60 * 60
244             + 5 * 1000 * 60 * 60 * 24);
245     res.length.shouldEqual(5);
246     res[0].name.shouldEqual("d");
247     res[0].value.shouldEqual(5);
248     res[1].name.shouldEqual("h");
249     res[1].value.shouldEqual(4);
250     res[2].name.shouldEqual("m");
251     res[2].value.shouldEqual(3);
252     res[3].name.shouldEqual("s");
253     res[3].value.shouldEqual(2);
254     res[4].name.shouldEqual("ms");
255     res[4].value.shouldEqual(1);
256 
257     res = TIME.transform(2001).onlyRelevant;
258     res.length.shouldEqual(2);
259     res[0].name.shouldEqual("s");
260     res[0].value.shouldEqual(2);
261     res[1].name.shouldEqual("ms");
262     res[1].value.shouldEqual(1);
263 
264     res = TIME.transform(2001).onlyRelevant.mostSignificant(1);
265     res.length.shouldEqual(1);
266     res[0].name.shouldEqual("s");
267     res[0].value.shouldEqual(2);
268 }
269 
270 // dfmt off
271 static immutable TIME =
272     Unit("time",
273          [Unit.Scale("ms", 1),
274           Unit.Scale("s", 1000),
275           Unit.Scale("m", 60),
276           Unit.Scale("h", 60),
277           Unit.Scale("d", 24)]);
278 // dfmt on