1 module plotcli.options;
2 
3 import docopt;
4 
5 version(unittest)
6 {
7     import dunit.toolkit;
8 }
9 
10 version(assert)
11 {
12     import std.stdio : writeln;
13 }
14 
15 import plotcli.parse : toRange;
16 
17 private string addDashes( string arg )
18 {
19     string dashes = "--";
20     if (arg.length == 1)
21         dashes = "-";
22     return dashes ~ arg;
23 }
24 
25 string helpText() // TODO cache result because will stay the same;
26 {
27     import std..string : toUpper, leftJustify;
28     import plotcli.data : aesDefaults;
29     auto header = "Usage: plotcli [-f] [--rolling ROLLING]";
30 
31     auto bodyText = "Plotcli is a plotting program that will plot data from provided data streams (files). It will ignore any lines it doesn't understand, making it possible to feed it \"dirty\" streams/files. All options can also be provided within the stream by using the prefix #plotcli (e.g. #plotcli -x 1 -y 2).
32 
33 Options:
34   -f                   Follow the stream, i.e. keep listening for new lines.
35   --rolling ROLLING    Keep the data rolling, i.e. limit the plot to the specified number of data points.";
36 
37     foreach( field; aesDefaults.fieldNames )
38     {
39         header ~= " [" ~ field.addDashes ~ " " ~ field.toUpper ~ "]";
40         bodyText ~= "\n  " ~ leftJustify(field.addDashes ~ " " ~ field.toUpper,20)~ " Specify " ~ field ~ " either by indices or labels/names";
41     }
42 
43     return header ~ "\n\n" ~ bodyText ~ "\n\nExamples:\n\tMost options allow you to specify indices or labels. If you provide integers (e.g. 0,2) they are interpreted as a column index (starting value 0), and that column is used as the values. For example passing `-x 0,1` will use the values from column 0 and 1 as x values. Any other value is used as a label. For example `--plotname name1,name2` will cause two plots to be created with the names name1 and name2. 
44     Specifying a single value for each option, except x and y, will result in that value to be used for all different lines/column. For example `-x 0,1 --type box` will use the first and second column for box plots. Finally one can also use `..` to indicate keep repeating/increasing. So `-x 0,2,..` will cause all even columns to be used. Similarly `-x 0,1,2 -y 3,..` will result in the first three columns being used for x values, but the 4 column being used for y values. In general it is also possible to keep values empty if they are not needed, e.g. `-x 0,1,2 -y ,2 --type box,line,box`.\n";
45 }
46 
47 private struct Options
48 {
49     bool follow = false;
50     int rolling = -1;
51 
52     OptionRange!string[string] values; 
53     Options dup() const
54     {
55         Options opts;
56         opts.rolling = rolling;
57         opts.follow = follow;
58         opts.explicitly_initialised = explicitly_initialised;
59         foreach(k, v; values)
60             opts.values[k] = v.dup;
61         return opts;
62     }
63 
64     bool explicitly_initialised = false;
65 }
66 
67 unittest {
68     Options opts1;
69     opts1.values["x"] = OptionRange!string("1,2");
70     assertEqual(opts1.values["x"].front, "1");
71     auto opts2 = opts1.dup;
72     opts2.values["x"].popFront;
73     assertEqual(opts1.values["x"].front, "1");
74 }
75 
76 auto defaultOptions()
77 {
78     import plotcli.data : aesDefaults;
79     Options options;
80     foreach( field; aesDefaults.fieldNames )
81     {
82         if (field != "x" && field != "y")
83             options.values[ field ] = OptionRange!string( 
84                 "", true);
85         else
86             options.values[ field ] = OptionRange!string( 
87                 "", true);
88     }
89 
90     options.values["y"] = OptionRange!string( "0", true );
91     options.values["y"].delta = 1;
92     return options;
93 }
94 
95 unittest
96 {
97     auto opts = defaultOptions();
98     assert(!opts.follow);
99 }
100 
101 import std.functional : memoize;
102 
103 alias cachedDocopt = memoize!(docopt.docopt);
104 
105 Options updateOptions(ref Options options, string[] args)
106 {
107     import std.algorithm : map;
108     import std.array : array;
109     import std.conv : to;
110 
111     auto arguments = cachedDocopt(helpText, args, true, "plotcli", false);
112 
113     debug writeln("Added arguments: ", arguments);
114     
115     if (arguments["-f"].to!string == "true")
116     {
117         options.follow = true;
118     }
119     if (!arguments["--rolling"].isNull)
120     {
121         options.rolling = arguments["--rolling"].to!string.to!int;
122     }
123 
124     import plotcli.data : aesDefaults;
125     foreach( field; aesDefaults.fieldNames )
126     {
127         if (!arguments[field.addDashes].isNull)
128         {
129             if (field != "x" && field != "y")
130                 options.values[ field ] = OptionRange!string( 
131                     arguments[field.addDashes].to!string, true);
132             else {
133                 if (!options.explicitly_initialised)
134                 {
135                     options.values["y"] = OptionRange!string("",false);
136                     options.explicitly_initialised = true;
137                 }
138 
139                 options.values[ field ] = OptionRange!string( 
140                     arguments[field.addDashes].to!string, false);
141             }
142         }
143     }
144 
145     return options;
146 }
147 
148 Options updateOptions(ref Options options, string message)
149 {
150     import std.regex : match;
151 
152     auto m = message.match(r"^#plotcli (.*)");
153     if (containOptions(message))
154     {
155         options = updateOptions(options, splitArgs(m.captures[1]));
156     }
157     return options;
158 }
159 
160 bool containOptions( string message )
161 {
162     import std.stdio;
163     if (message.length < 9)
164         return false;
165     return (message[0..9] == "#plotcli ");
166 }
167 
168 unittest
169 {
170     assert( "#plotcli bla".containOptions );
171     assert( !"#plotclibla".containOptions );
172     assert( !"#pl".containOptions );
173 }
174  
175 auto parseOptions(T)( T msg )
176 {
177     auto opts = defaultOptions();
178     return updateOptions( opts, msg );
179 }
180 
181 unittest
182 {
183     import std.array : array;
184     import std.range : empty;
185     Options options = defaultOptions;
186     assertEqual( 
187         updateOptions( options, "#plotcli -x 1,2,4" ).values["x"].array,
188         ["1","2","4"] );
189     assert( options.values["y"].empty ); 
190     assert( !options.follow ); 
191 
192     assertEqual( 
193         updateOptions( options, "#plotcli -y 3,2,4" ).values["y"].array,
194         ["3","2","4"] );
195     assertEqual( options.values["x"].array, ["1","2","4"] ); 
196 }
197 
198 unittest
199 {
200     // Test whether setting new value properly overrides defaults
201     auto opts = defaultOptions;
202     assertEqual( opts.values["y"].front, "0" );
203     assertEqual( opts.values["y"].minimumExpectedIndex, 0 );
204     assert( !opts.values["y"].empty );
205 
206     auto optsdup = opts.dup;
207     optsdup.values["y"].popFront;
208     assertEqual( optsdup.values["y"].front, "1" );
209 
210     // After change override it
211     assert( updateOptions( opts, "#plotcli -x 0" ).values["y"].empty );
212 
213     assertEqual( updateOptions( opts, "#plotcli -y 3" ).values["y"].front, "3" );
214     // If set explicitly (above) don't override it
215     assert( !updateOptions( opts, "#plotcli -x 0" ).values["y"].empty );
216 }
217 
218 /// Does the data fit with the given options?
219 bool validData(R1, R2)( R1 xColumns, R1 yColumns, in R2 columns )
220 {
221     import std.algorithm : max, reduce;
222     import std.conv;
223     import std.range : empty;
224     import plotcli.parse : areNumeric;
225 
226     if (xColumns.empty && yColumns.empty )
227     {
228         return ( columns.length > 0 && columns.areNumeric([0]));
229     }
230     auto maxCol = max(
231             xColumns.minimumExpectedIndex,
232             yColumns.minimumExpectedIndex );
233 
234     return (columns.length > maxCol 
235             && columns.areNumeric(xColumns.toColumnIDs(columns.length.to!int-1))
236             && columns.areNumeric(yColumns.toColumnIDs(columns.length.to!int-1)) 
237            );
238 }
239 
240 unittest
241 {
242     assert( validData( OptionRange!string(""), OptionRange!string(""), 
243                 ["1","a", "-2"] ) );
244     assert( validData( OptionRange!string("0,2"), OptionRange!string(""), 
245                 ["1","a", "-2"] ) );
246     assert( validData( OptionRange!string(""), OptionRange!string("0,2"), 
247                 ["1","a", "-2"] ) );
248     assert( !validData( OptionRange!string("1"), OptionRange!string("0,2"), 
249                 ["1","a", "-2"] ) );
250     assert( validData( OptionRange!string(""), OptionRange!string("0,2,.."), 
251                 ["1","a", "-2"] ) );
252 
253     auto opy = OptionRange!string("0");
254     opy.delta = 1;
255     assert( validData( OptionRange!string(""), opy, 
256                 ["1","a", "-2"] ) );
257 }
258 
259 /// Does the data fit with the given options?
260 bool validData(RANGE)( Options options, in RANGE columns )
261 {
262     return validData( options.values["x"], options.values["y"], columns );
263 }
264 
265 unittest
266 {
267     auto options = defaultOptions;
268     assert( !options.validData( ["1","a", "-2"] ) );
269     options.values["x"] = OptionRange!string("0");
270     options.values["y"] = OptionRange!string("0");
271     assert( options.validData( ["1","a", "-2"] ) );
272     options.values["y"] = OptionRange!string("0,2");
273     assert( options.validData( ["1","a", "-2"] ) );
274     assert( !options.validData( ["1","a"] ) );
275     assert( !options.validData( ["1","a", "b"] ) );
276 
277     options = defaultOptions();
278     options.values["x"] = OptionRange!string("0,0,1,0,1");
279     options.values["y"] = OptionRange!string(",2,,,");
280     assert( options.validData(["0.04", "3.22", "-0.27"]));
281 }
282 
283 string[] splitArgs(string args)
284 {
285     import std.conv : to;
286 
287     string[] splitted;
288     bool inner = false;
289     string curr = "";
290     foreach (s; args)
291     {
292         if (s == (" ").to!char && !inner)
293         {
294             splitted ~= curr;
295             curr = "";
296         }
297         else if (s == ("\"").to!char || s == ("\'").to!char)
298         {
299             if (inner)
300                 inner = false;
301             else
302                 inner = true;
303         }
304         else
305             curr ~= s;
306     }
307     splitted ~= curr;
308     return splitted;
309 }
310 
311 unittest
312 {
313     assert(("-b arg").splitArgs.length == 2);
314     assert(("-b \"arg b\"").splitArgs.length == 2);
315     assert(("-b \"arg b\" -f").splitArgs.length == 3);
316 }
317 
318 private string increaseString( string original, int delta )
319 {
320     import std.conv : to;
321     import std.range : back;
322     import plotcli.parse : isInteger;
323     if (original.length == 0)
324         return "";
325     else if (original.isInteger)
326         return (original.to!int + delta).to!string;
327     else if (original.length == 1)
328         return (original.back.to!char + delta)
329                         .to!char
330                         .to!string;
331     else
332         return original[0..$-1] ~ (original.back.to!char + delta)
333                         .to!char
334                         .to!string;
335 }
336 
337 unittest
338 {
339     assertEqual( increaseString( "a", 0 ), "a" );
340     assertEqual( increaseString( "a", 1 ), "b" );
341     assertEqual( increaseString( "c", 2 ), "e" );
342     assertEqual( increaseString( "cd", 1 ), "ce" );
343     assertEqual( increaseString( "19", 1 ), "20" );
344 }
345 
346 /// Range to correctly interpret 1,2,.. a,b,.. etc
347 struct OptionRange( T )
348 {
349     this( string opts, bool repeat = false )
350     {
351         import std.array : array;
352         import std.conv : to;
353         import std.algorithm : splitter;
354         import std.range : back, empty, popBack;
355 
356         splittedOpts = opts.splitter(',').array;
357 
358         if (splittedOpts.length == 1 && repeat)
359             splittedOpts ~= [".."];
360 
361         if (!splittedOpts.empty && splittedOpts.back == "..")
362         {
363             // Calculate the delta
364             static if (is(T==string))
365             {
366                 if (splittedOpts.length > 2)
367                     delta = splittedOpts[$-2].back.to!char - 
368                         splittedOpts[$-3].back.to!char;
369             } else {
370                 if (splittedOpts.length > 2)
371                     delta = splittedOpts[$-2].to!int - splittedOpts[$-3].to!int;
372             }
373         }
374     }
375 
376     @property bool empty()
377     {
378         import std.range : empty, front;
379         return splittedOpts.empty;
380     }
381 
382     /// Returns true if the range keeps repeating from now on
383     @property bool repeatForever()
384     {
385         import std.range : front;
386         if (this.empty)
387             return false;
388         return (splittedOpts.front == ".." && delta == 0);
389     }
390 
391     auto toColumnIDs(int max)
392     {
393         import plotcli.parse : isInteger;
394         import std.conv : to;
395         int[] ids;
396         auto saved = save();
397         while (!saved.empty && !saved.repeatForever )
398         {
399             if (saved.front.isInteger)
400             {
401                 auto id = saved.front.to!int;
402                 if (id > max)
403                     break;
404                 ids ~= id;
405             }
406             saved.popFront;
407         }
408         return ids;
409     }
410 
411     @property T front()
412     {
413         import std.conv : to;
414         import std.range : front;
415         if (splittedOpts.front == "..")
416             return extrapolatedValue;
417         return splittedOpts.front.to!T;
418     }
419 
420     void popFront()
421     {
422         import std.conv : to;
423         import std.range : back, empty, front, popFront;
424         auto tmpCache = splittedOpts.front;
425         if (splittedOpts.front != "..")
426         {
427             splittedOpts.popFront();
428         }
429         if (!splittedOpts.empty && splittedOpts.front == "..")
430         {
431             if (tmpCache != "..")
432             {
433                 extrapolatedValue = tmpCache.to!T;
434             }
435             static if (is(T==string))
436                 extrapolatedValue = increaseString(extrapolatedValue, delta);
437             else 
438                 extrapolatedValue += delta;
439         }
440     }
441 
442     @property auto save()
443     {
444         return this;
445     }
446 
447     OptionRange!T dup() const
448     {
449         auto nOptionRange = OptionRange!T();
450         nOptionRange.splittedOpts = splittedOpts.dup;
451         nOptionRange.delta = delta;
452         nOptionRange.extrapolatedValue = extrapolatedValue;
453         return nOptionRange;
454     }
455 
456  private:
457     import std.regex : ctRegex;
458     string[] splittedOpts;
459     //auto csvRegex = ctRegex!(`,\s*`);
460     int delta = 0;
461     T extrapolatedValue;
462 }
463 
464 unittest
465 {
466     import std.array;
467     import std.range : take;
468     assertEqual( OptionRange!int( "1,2,3" ).array, 
469         [1,2,3] );
470     assertEqual( OptionRange!int( "5,6,.." ).take(4).array, 
471         [5,6,7,8] );
472     assertEqual( OptionRange!int( "1,3,.." ).take(4).array, 
473         [1,3,5,7] );
474     assertEqual( OptionRange!int( "1,.." ).take(4).array, 
475         [1,1,1,1] );
476     assertEqual( OptionRange!string( "1,2,3" ).array, 
477         ["1","2","3"] );
478     assertEqual( OptionRange!string( "c,d,.." ).take(4).array, 
479         ["c","d","e","f"] );
480     assertEqual( OptionRange!string( "ac,ad,.." ).take(4).array, 
481         ["ac","ad","ae","af"] );
482     assertEqual( OptionRange!string( "bc,.." ).take(4).array, 
483         ["bc","bc","bc","bc"] );
484 
485     assertEqual( OptionRange!string( ",.." ).take(4).array, 
486         ["","","",""] );
487 
488     assertEqual( OptionRange!string( "a", true ).take(4).array, 
489         ["a","a","a","a"] );
490 
491     assert( OptionRange!int( "" ).empty );
492 }
493 
494 
495 auto minimumExpectedIndex( R : OptionRange!U, U )(R r)
496 {
497     // TODO This is not the best way of doing it.
498     import std.algorithm : filter, map, reduce;
499     import std.range : back;
500     import plotcli.parse : isInteger;
501 
502     if (r.empty)
503         return 0;
504 
505     auto rs = r.splittedOpts[0..$];
506     if (r.splittedOpts.back == "..")
507         rs = r.splittedOpts[0..$-1];
508 
509     return reduce!("max(a,b)")(0, 
510         rs
511         .filter!((a) => a.isInteger)
512         .map!("a.to!int"));
513 }
514 
515 auto minimumExpectedIndex( R )(R r)
516 {
517     // TODO This is not the best way of doing it.
518     import std.algorithm : map, reduce;
519     import std.range : back;
520     return reduce!("max(a,b)")(0, r);
521 }
522 
523 unittest
524 {
525     assertEqual( OptionRange!string( ",2,," ).minimumExpectedIndex, 2 );
526 }