1 module unit;
2 
3 /++
4  + a unit allows to easily print mixed resolution values.
5  + typical examples include time (with hours, minutes, ...)
6  + and distances (with km, m, cm, mm).
7  + The unitclass simplifies definition of such things as well
8  + as transforming a high resolution value, to a supposedly
9  + more human readable representation.
10  + e.g. 3_610_123 would convert to 1h 0m 10s 123ms.
11  +/
12 public struct Unit {
13   import std.algorithm.iteration;
14   import std.range;
15 
16   /++
17    + A scale is one resolution of a unit.
18    +/
19   public struct Scale {
20     /// the name of the scale (e.g. h for hour)
21     string name;
22 
23     /// factor to the next higher resolution (e.g. 60 from minutes to seconds)
24     long factor;
25 
26     /// normal renderwidth for the application (e.g. 2 for minutes (00-59))
27     int digits;
28   }
29 
30   /// factory for Scale
31   static Scale scale(string name, long factor, int digits=1) {
32     return Scale(name, factor, digits);
33   }
34 
35   /++
36    + One part of the transformed Unit.
37    + a part of a unit is e.g. the minute resolution of a duration.
38    +/
39   public struct Part {
40     /// name of the part
41     string name;
42     /// value of the part
43     long value;
44     /// number of digits
45     int digits;
46     /// convenient tostring function. e.g. 10min
47     string toString() {
48       import std.conv;
49       return value.to!(string) ~ name;
50     }
51   }
52 
53   /// name of the unit
54   private string name;
55   /// resolutions of the unit
56   private Scale[] scales;
57 
58   public this(string name, Scale[] scales) {
59     import std.exception;
60     this.name = name;
61     this.scales = cumulativeFold!((result,x) => scale(x.name, result.factor * x.factor, x.digits))(scales).array.retro.array;
62     enforce(__ctfe);
63   }
64 
65   /++
66    + transforms the unit to its parts
67    +/
68   public Part[] transform(long v) immutable {
69     import std.array;
70 
71     auto res = appender!(Part[]);
72     auto tmp = v;
73     foreach (Scale scale; scales) {
74       auto h = tmp / scale.factor;
75       tmp = v % scale.factor;
76       res.put(Part(scale.name, h, scale.digits));
77     }
78     return res.data;
79   }
80 }
81 
82 @("creatingScales") unittest {
83   import unit_threaded;
84   auto s = Unit.scale("ttt", 1, 2);
85   s.digits.shouldEqual(2);
86 
87   s = Unit.scale("ttt2", 1);
88   s.digits.shouldEqual(1);
89 }
90 
91 /++
92  + get only relevant parts of an part array.
93  + relevant means all details starting from the first
94  + non 0 part.
95  +/
96 Unit.Part[] onlyRelevant(Unit.Part[] parts) {
97   import std.array;
98   auto res = appender!(Unit.Part[]);
99   bool needed = false;
100   foreach (part; parts) {
101     if (needed || (part.value > 0)) {
102       needed = true;
103     }
104     if (needed) {
105       res.put(part);
106     }
107   }
108   return res.data;
109 }
110 
111 /++
112  + get the first nr of parts (or less if not enough parts are available.
113  +/
114 Unit.Part[] mostSignificant(Unit.Part[] parts, long nr) {
115   import std.algorithm.comparison;
116   auto max = min(parts.length, nr);
117   return parts[0..max];
118 }
119 
120 /++
121  + example for a time unit definition
122  +/
123 @("basicUsage") unittest {
124   import unit_threaded;
125 
126   static immutable time = Unit("time", [Unit.Scale("ms", 1), Unit.Scale("s", 1000), Unit.Scale("m", 60), Unit.Scale("h", 60), Unit.Scale("d", 24)]);
127 
128   auto res = time.transform(1 + 2*1000 + 3*1000*60 + 4*1000*60*60 + 5 * 1000*60*60*24);
129   res.length.shouldEqual(5);
130   res[0].name.shouldEqual("d");
131   res[0].value.shouldEqual(5);
132   res[1].name.shouldEqual("h");
133   res[1].value.shouldEqual(4);
134   res[2].name.shouldEqual("m");
135   res[2].value.shouldEqual(3);
136   res[3].name.shouldEqual("s");
137   res[3].value.shouldEqual(2);
138   res[4].name.shouldEqual("ms");
139   res[4].value.shouldEqual(1);
140 
141   res = time.transform(2001).onlyRelevant;
142   res.length.shouldEqual(2);
143   res[0].name.shouldEqual("s");
144   res[0].value.shouldEqual(2);
145   res[1].name.shouldEqual("ms");
146   res[1].value.shouldEqual(1);
147 
148   res = time.transform(2001).onlyRelevant.mostSignificant(1);
149   res.length.shouldEqual(1);
150   res[0].name.shouldEqual("s");
151   res[0].value.shouldEqual(2);
152 }