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 
101 @("creatingScales") unittest
102 {
103     import unit_threaded;
104 
105     auto s = Unit.scale("ttt", 1, 2);
106     s.digits.shouldEqual(2);
107 
108     s = Unit.scale("ttt2", 1);
109     s.digits.shouldEqual(1);
110 }
111 
112 /++
113  + get only relevant parts of an part array.
114  + relevant means all details starting from the first
115  + non 0 part.
116  +/
117 auto onlyRelevant(Unit.Part[] parts)
118 {
119     import std.array;
120 
121     auto res = appender!(Unit.Part[]);
122     bool needed = false;
123     foreach (part; parts)
124     {
125         if (needed || (part.value > 0))
126         {
127             needed = true;
128         }
129         if (needed)
130         {
131             res.put(part);
132         }
133     }
134     return res.data;
135 }
136 
137 /++
138  + get the first nr of parts (or less if not enough parts are available).
139  +/
140 auto mostSignificant(Unit.Part[] parts, long nr)
141 {
142     import std.algorithm.comparison;
143 
144     auto max = min(parts.length, nr);
145     return parts[0 .. max];
146 }
147 
148 /++
149  + example for a time unit definition
150  +/
151 @("basicUsage") unittest
152 {
153     import unit_threaded;
154 
155     auto res = TIME.transform(1 + 2 * 1000 + 3 * 1000 * 60 + 4 * 1000 * 60 * 60
156             + 5 * 1000 * 60 * 60 * 24);
157     res.length.shouldEqual(5);
158     res[0].name.shouldEqual("d");
159     res[0].value.shouldEqual(5);
160     res[1].name.shouldEqual("h");
161     res[1].value.shouldEqual(4);
162     res[2].name.shouldEqual("m");
163     res[2].value.shouldEqual(3);
164     res[3].name.shouldEqual("s");
165     res[3].value.shouldEqual(2);
166     res[4].name.shouldEqual("ms");
167     res[4].value.shouldEqual(1);
168 
169     res = TIME.transform(2001).onlyRelevant;
170     res.length.shouldEqual(2);
171     res[0].name.shouldEqual("s");
172     res[0].value.shouldEqual(2);
173     res[1].name.shouldEqual("ms");
174     res[1].value.shouldEqual(1);
175 
176     res = TIME.transform(2001).onlyRelevant.mostSignificant(1);
177     res.length.shouldEqual(1);
178     res[0].name.shouldEqual("s");
179     res[0].value.shouldEqual(2);
180 }
181 
182 // dfmt off
183 static immutable TIME =
184     Unit("time",
185          [Unit.Scale("ms", 1),
186           Unit.Scale("s", 1000),
187           Unit.Scale("m", 60),
188           Unit.Scale("h", 60),
189           Unit.Scale("d", 24)]);
190 // dfmt on