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 }