1 module ggplotd.aes; 2 3 import std.range : front, popFront, empty; 4 5 version (unittest) 6 { 7 import dunit.toolkit; 8 } 9 10 import std.typecons : Tuple; 11 12 template Aes(Specs...) 13 { 14 import std.traits : Identity; 15 import std.typecons : isTuple; 16 import std.typetuple : staticMap, TypeTuple; 17 import std.range : isInputRange; 18 19 // Parse (type,name) pairs (FieldSpecs) out of the specified 20 // arguments. Some fields would have name, others not. 21 template parseSpecs(Specs...) 22 { 23 static if (Specs.length == 0) 24 { 25 alias parseSpecs = TypeTuple!(); 26 } 27 else static if (is(Specs[0]) && isInputRange!(Specs[0])) 28 { 29 static if (is(typeof(Specs[1]) : string)) 30 { 31 alias parseSpecs = TypeTuple!(FieldSpec!(Specs[0 .. 2]), parseSpecs!(Specs[2 .. $])); 32 } 33 else 34 { 35 alias parseSpecs = TypeTuple!(FieldSpec!(Specs[0]), parseSpecs!(Specs[1 .. $])); 36 } 37 } 38 else 39 { 40 static assert(0, 41 "Attempted to instantiate Tuple with an " ~ "invalid argument: " ~ Specs[0].stringof); 42 } 43 } 44 45 template FieldSpec(T, string s = "") 46 { 47 alias Type = T; 48 alias name = s; 49 } 50 51 alias fieldSpecs = parseSpecs!Specs; 52 53 // Used with staticMap. 54 alias extractType(alias spec) = spec.Type; 55 alias extractName(alias spec) = spec.name; 56 57 // Generates named fields as follows: 58 // alias name_0 = Identity!(field[0]); 59 // alias name_1 = Identity!(field[1]); 60 // : 61 // NOTE: field[k] is an expression (which yields a symbol of a 62 // variable) and can't be aliased directly. 63 string injectNamedFields() 64 { 65 string decl = ""; 66 67 foreach (i, name; staticMap!(extractName, fieldSpecs)) 68 { 69 import std.format : format; 70 71 decl ~= format("alias _%s = Identity!(field[%s]);", i, i); 72 if (name.length != 0) 73 { 74 decl ~= format("alias %s = _%s;", name, i); 75 } 76 } 77 return decl; 78 } 79 80 alias fieldNames = staticMap!(extractName, fieldSpecs); 81 82 // TODO Also update default grouping if appropiate 83 alias defaultNames = TypeTuple!("label","colour", "size", "angle", "alpha", "mask", "fill"); 84 alias defaultTypes = TypeTuple!(string,string, double, double, double, bool, double); 85 alias defaultValues = TypeTuple!(`""`, `"black"`, 10, 0, 1, true, 0.0); 86 string injectFront() 87 { 88 import std.format : format; 89 90 string decl = "auto front() { import std.range : ElementType;"; 91 decl ~= "import std.typecons : Tuple; import std.range : front;"; 92 93 string tupleType = "Tuple!("; 94 string values = "("; 95 96 foreach (i, name; fieldNames) 97 { 98 99 tupleType ~= format(q{typeof(%s.front),}, name); 100 tupleType ~= "q{" ~ name ~ "},"; 101 values ~= format("this.%s.front,", name); 102 } 103 104 foreach (i, name; defaultNames) 105 { 106 auto defined = false; 107 foreach (j, fName; fieldNames) 108 { 109 if (name == fName) 110 defined = true; 111 } 112 if (!defined) 113 { 114 //tupleType ~= format( q{typeof(%s),}, defaultValues[i] ); 115 tupleType ~= format(q{%s,}, defaultTypes[i].stringof); 116 tupleType ~= "q{" ~ name ~ "},"; 117 if (is(defaultTypes[i]==string)) 118 values ~= format("%s,", defaultValues[i]); 119 else 120 values ~= format("%s,", defaultValues[i].stringof); 121 } 122 } 123 124 decl ~= "return " ~ tupleType[0 .. $ - 1] ~ ")" ~ values[0 .. $ - 1] ~ "); }"; 125 //string decl2 = format("auto front() { import std.stdio; \"%s\".writeln; return 0.0; }", decl); 126 return decl; 127 } 128 129 // Returns Specs for a subtuple this[from .. to] preserving field 130 // names if any. 131 alias sliceSpecs(size_t from, size_t to) = staticMap!(expandSpec, fieldSpecs[from .. to]); 132 133 template expandSpec(alias spec) 134 { 135 static if (spec.name.length == 0) 136 { 137 alias expandSpec = TypeTuple!(spec.Type); 138 } 139 else 140 { 141 alias expandSpec = TypeTuple!(spec.Type, spec.name); 142 } 143 } 144 145 enum areCompatibleTuples(Tup1, Tup2, string op) = isTuple!Tup2 && is(typeof({ 146 Tup1 tup1 = void; 147 Tup2 tup2 = void; 148 static assert(tup1.field.length == tup2.field.length); 149 foreach (i, _; 150 Tup1.Types) 151 { 152 auto lhs = typeof(tup1.field[i]).init; 153 auto rhs = typeof(tup2.field[i]).init; 154 static if (op == "=") 155 lhs = rhs; 156 else 157 auto result = mixin("lhs " ~ op ~ " rhs"); 158 } 159 })); 160 161 enum areBuildCompatibleTuples(Tup1, Tup2) = isTuple!Tup2 && is(typeof({ 162 static assert(Tup1.Types.length == Tup2.Types.length); 163 foreach (i, _; 164 Tup1.Types) 165 static assert(isBuildable!(Tup1.Types[i], Tup2.Types[i])); 166 })); 167 168 /+ Returns $(D true) iff a $(D T) can be initialized from a $(D U). +/ 169 enum isBuildable(T, U) = is(typeof({ U u = U.init; T t = u; })); 170 /+ Helper for partial instanciation +/ 171 template isBuildableFrom(U) 172 { 173 enum isBuildableFrom(T) = isBuildable!(T, U); 174 } 175 176 /// 177 struct Aes 178 { 179 /** 180 * The type of the tuple's components. 181 */ 182 alias Types = staticMap!(extractType, fieldSpecs); 183 184 /** 185 * The names of the tuple's components. Unnamed fields have empty names. 186 * 187 * Examples: 188 * ---- 189 * alias Fields = Tuple!(int, "id", string, float); 190 * static assert(Fields.fieldNames == TypeTuple!("id", "", "")); 191 * ---- 192 */ 193 alias fieldNames = staticMap!(extractName, fieldSpecs); 194 195 /** 196 * Use $(D t.expand) for a tuple $(D t) to expand it into its 197 * components. The result of $(D expand) acts as if the tuple components 198 * were listed as a list of values. (Ordinarily, a $(D Tuple) acts as a 199 * single value.) 200 * 201 * Examples: 202 * ---- 203 * auto t = tuple(1, " hello ", 2.3); 204 * writeln(t); // Tuple!(int, string, double)(1, " hello ", 2.3) 205 * writeln(t.expand); // 1 hello 2.3 206 * ---- 207 */ 208 Types expand; 209 mixin(injectNamedFields()); 210 211 // backwards compatibility 212 alias field = expand; 213 214 /** 215 * Constructor taking one value for each field. 216 */ 217 static if (Types.length > 0) 218 { 219 this(Types values) 220 { 221 field[] = values[]; 222 } 223 } 224 225 /** 226 * Constructor taking a compatible array. 227 * 228 * Examples: 229 * ---- 230 * int[2] ints; 231 * Tuple!(int, int) t = ints; 232 * ---- 233 */ 234 this(U, size_t n)(U[n] values) if (n == Types.length 235 && allSatisfy!(isBuildableFrom!U, Types)) 236 { 237 foreach (i, _; Types) 238 { 239 field[i] = values[i]; 240 } 241 } 242 243 /** 244 * Constructor taking a compatible tuple. 245 */ 246 this(U)(U another) if (areBuildCompatibleTuples!(typeof(this), U)) 247 { 248 field[] = another.field[]; 249 } 250 251 mixin(injectFront()); 252 253 /// 254 void popFront() 255 { 256 import std.range : popFront; 257 258 foreach (i, _; Types[0 .. $]) 259 { 260 field[i].popFront(); 261 } 262 } 263 264 /// 265 @property bool empty() 266 { 267 if (length == 0) 268 return true; 269 return false; 270 } 271 272 /// 273 size_t length() 274 { 275 import std.algorithm : min; 276 import std.range : walkLength, isInfinite; 277 278 size_t l = size_t.max; 279 foreach (i, type; Types[0 .. $]) 280 { 281 static if (!isInfinite!type) 282 { 283 if (field[i].walkLength < l) 284 l = field[i].walkLength; 285 } 286 } 287 return l; 288 } 289 290 /** 291 * Comparison for equality. 292 */ 293 bool opEquals(R)(R rhs) if (areCompatibleTuples!(typeof(this), R, "==")) 294 { 295 return field[] == rhs.field[]; 296 } 297 298 /// ditto 299 bool opEquals(R)(R rhs) const if (areCompatibleTuples!(typeof(this), R, "==")) 300 { 301 return field[] == rhs.field[]; 302 } 303 304 /** 305 * Comparison for ordering. 306 */ 307 int opCmp(R)(R rhs) if (areCompatibleTuples!(typeof(this), R, "<")) 308 { 309 foreach (i, Unused; Types) 310 { 311 if (field[i] != rhs.field[i]) 312 { 313 return field[i] < rhs.field[i] ? -1 : 1; 314 } 315 } 316 return 0; 317 } 318 319 /// ditto 320 int opCmp(R)(R rhs) const if (areCompatibleTuples!(typeof(this), R, "<")) 321 { 322 foreach (i, Unused; Types) 323 { 324 if (field[i] != rhs.field[i]) 325 { 326 return field[i] < rhs.field[i] ? -1 : 1; 327 } 328 } 329 return 0; 330 } 331 332 /** 333 * Assignment from another tuple. Each element of the source must be 334 * implicitly assignable to the respective element of the target. 335 */ 336 void opAssign(R)(auto ref R rhs) if (areCompatibleTuples!(typeof(this), R, 337 "=")) 338 { 339 import std.algorithm : swap; 340 341 static if (is(R : Tuple!Types) && !__traits(isRef, rhs)) 342 { 343 if (__ctfe) 344 { 345 // Cannot use swap at compile time 346 field[] = rhs.field[]; 347 } 348 else 349 { 350 // Use swap-and-destroy to optimize rvalue assignment 351 swap!(Tuple!Types)(this, rhs); 352 } 353 } 354 else 355 { 356 // Do not swap; opAssign should be called on the fields. 357 field[] = rhs.field[]; 358 } 359 } 360 361 /** 362 * Takes a slice of the tuple. 363 * 364 * Examples: 365 * ---- 366 * Tuple!(int, string, float, double) a; 367 * a[1] = "abc"; 368 * a[2] = 4.5; 369 * auto s = a.slice!(1, 3); 370 * static assert(is(typeof(s) == Tuple!(string, float))); 371 * assert(s[0] == "abc" && s[1] == 4.5); 372 * ---- 373 */ 374 @property ref Tuple!(sliceSpecs!(from, to)) slice(size_t from, size_t to)() @trusted if ( 375 from <= to && to <= Types.length) 376 { 377 return *cast(typeof(return)*)&(field[from]); 378 } 379 380 /// 381 size_t toHash() const nothrow @trusted 382 { 383 size_t h = 0; 384 foreach (i, T; Types) 385 h += typeid(T).getHash(cast(const void*)&field[i]); 386 return h; 387 } 388 389 /** 390 * Converts to string. 391 */ 392 void toString(DG)(scope DG sink) 393 { 394 enum header = typeof(this).stringof ~ "(", footer = ")", separator = ", "; 395 sink(header); 396 foreach (i, Type; Types) 397 { 398 static if (i > 0) 399 { 400 sink(separator); 401 } 402 // TODO: Change this once toString() works for shared objects. 403 static if (is(Type == class) && is(typeof(Type.init) == shared)) 404 { 405 sink(Type.stringof); 406 } 407 else 408 { 409 import std.format : FormatSpec, formatElement; 410 411 FormatSpec!char f; 412 formatElement(sink, field[i], f); 413 } 414 } 415 sink(footer); 416 } 417 418 /// 419 string toString()() 420 { 421 import std.conv : to; 422 423 return this.to!string; 424 } 425 } 426 } 427 428 unittest 429 { 430 auto tup = Aes!(double[], "x", double[], "y", string[], "colour")([0, 1], 431 [2, 1], ["white", "white2"]); 432 auto tup2 = Aes!(double[], "x", double[], "y")([0, 1], [2, 1]); 433 assertEqual(tup.colour, ["white", "white2"]); 434 assertEqual(tup.length, 2); 435 assertEqual(tup2.length, 2); 436 437 tup2.x ~= 0.0; 438 tup2.x ~= 0.0; 439 assertEqual(tup2.length, 2); 440 tup2.y ~= 0.0; 441 assertEqual(tup2.length, 3); 442 } 443 444 unittest 445 { 446 auto tup = Aes!(double[], "x", double[], "y", string[], "colour")([0, 1], 447 [2, 1], ["white", "white2"]); 448 449 tup.popFront; 450 assertEqual(tup.front.y, 1); 451 assertEqual(tup.front.colour, "white2"); 452 assertEqual(tup.front.label, ""); 453 454 auto tup2 = Aes!(double[], "x", double[], "y")([0, 1], [2, 1]); 455 assertEqual(tup2.front.y, 2); 456 assertEqual(tup2.front.colour, "black"); 457 458 import std.range : repeat; 459 460 auto xs = repeat(0); 461 auto tup3 = Aes!(typeof(xs), "x", double[], "y")(xs, [2, 1]); 462 463 assertEqual(tup3.front.x, 0); 464 tup3.popFront; 465 tup3.popFront; 466 assertEqual(tup3.empty, true); 467 468 } 469 470 /// 471 template group(Specs...) 472 { 473 string buildExtractKey() 474 { 475 static if (Specs.length == 0) 476 { 477 import std.typecons : TypeTuple; 478 alias Specs = TypeTuple!("alpha","colour","label"); 479 } 480 string types = ""; 481 string values = ""; 482 foreach( spec; Specs ) 483 { 484 types ~= "typeof(a." ~ spec ~"),"; 485 values ~= "a." ~ spec ~","; 486 } 487 string str = "auto extractKey(T)(T a) 488 { return Tuple!(" ~ types[0..$-1] ~ ")(" ~ values[0..$-1] ~ "); }"; 489 return str; 490 } 491 492 auto group(AES)(AES aes) 493 { 494 import std.stdio; 495 mixin(buildExtractKey()); 496 extractKey( aes.front ); 497 498 typeof(aes.front())[][typeof(extractKey(aes.front))] grouped; 499 500 // Extract keys for aa and store in aa. Return the values of the aa 501 foreach( tup; aes ) 502 { 503 auto key = extractKey(tup); 504 if (key in grouped) 505 grouped[key] ~= tup; 506 else 507 grouped[key] = [tup]; 508 } 509 return grouped.values; 510 } 511 } 512 513 unittest 514 { 515 import std.range : walkLength; 516 auto aes = Aes!(double[], "x", string[], "colour", double[], "alpha") 517 ([0,1,2,3], ["a","a","b","b"], [0,1,0,1]); 518 519 assertEqual(group!("colour","alpha")(aes).walkLength,4); 520 assertEqual(group!("alpha")(aes).walkLength,2); 521 522 assertEqual(group(aes).walkLength,4); 523 } 524 525 unittest 526 { 527 auto aes = Aes!(double[], "x", double[], "y", string[], "colour")([1.0, 528 2.0, 1.1], [3.0, 1.5, 1.1], ["a", "b", "a"]); 529 530 import std.range : walkLength, front, popFront; 531 532 auto grouped = aes.group; 533 assertEqual(grouped.walkLength, 2); 534 assertEqual(grouped.front.walkLength, 2); 535 grouped.popFront; 536 assertEqual(grouped.front.walkLength, 1); 537 } 538 539 /// 540 import std.range : isInputRange; 541 542 /// 543 struct NumericLabel(T) if (isInputRange!T) 544 { 545 import std.range : ElementType; 546 import std.traits : isNumeric; 547 548 alias E = ElementType!T; 549 550 /// 551 this(T range) 552 { 553 original = range; 554 } 555 556 /// 557 @property auto front() 558 { 559 import std.typecons : Tuple; 560 import std.range : front; 561 import std.conv : to; 562 563 static if (isNumeric!E) 564 return Tuple!(double, string)(original.front.to!double, original.front.to!string); 565 else 566 { 567 if (original.front !in fromLabelMap) 568 { 569 fromLabelMap[original.front] = fromLabelMap.length.to!double; 570 //toLabelMap[fromLabelMap[original.front]] 571 // = original.front; 572 } 573 return Tuple!(double, string)(fromLabelMap[original.front], original.front.to!string, 574 ); 575 } 576 } 577 578 /// 579 void popFront() 580 { 581 import std.range : popFront; 582 583 original.popFront; 584 } 585 586 /// 587 @property bool empty() 588 { 589 import std.range : empty; 590 591 return original.empty; 592 } 593 594 /// 595 @property bool numeric() 596 { 597 static if (isNumeric!E) 598 return true; 599 else 600 return false; 601 } 602 603 private: 604 T original; 605 //E[double] toLabelMap; 606 double[E] fromLabelMap; 607 } 608 609 unittest 610 { 611 import std.stdio : writeln; 612 import std.array : array; 613 import std.algorithm : map; 614 615 auto num = NumericLabel!(double[])([0.0, 0.1, 1.0, 0.0]); 616 assertEqual(num.map!((a) => a[0]).array, [0.0, 0.1, 1.0, 0.0]); 617 assertEqual(num.map!((a) => a[1]).array, ["0", "0.1", "1", "0"]); 618 auto strs = NumericLabel!(string[])(["a", "c", "b", "a"]); 619 assertEqual(strs.map!((a) => a[0]).array, [0, 1, 2.0, 0.0]); 620 assertEqual(strs.map!((a) => a[1]).array, ["a", "c", "b", "a"]); 621 } 622 623 /++ 624 Merge two Aes structs 625 626 If it has similar named types, then it uses the second one. 627 628 Returns a new struct, with combined types. 629 +/ 630 template merge(T, U) 631 { 632 auto generateCode() 633 { 634 import std.string : split; 635 string typing = T.stringof.split("!")[0] ~ "!("; 636 string variables = "("; 637 foreach (i, t; U.fieldNames) 638 { 639 typing ~= U.Types[i].stringof ~ ",\"" ~ t ~ "\","; 640 variables ~= "other." ~ t ~ ","; 641 } 642 643 foreach (i, t; T.fieldNames) 644 { 645 bool contains = false; 646 foreach (_, t2; U.fieldNames) 647 { 648 if (t == t2) 649 contains = true; 650 } 651 if (!contains) 652 { 653 typing ~= T.Types[i].stringof ~ ",\"" ~ t ~ "\","; 654 variables ~= "base." ~ t ~ ","; 655 } 656 } 657 return "return " ~ typing[0 .. $ - 1] ~ ")" ~ variables[0 .. $ - 1] ~ ");"; 658 } 659 660 auto merge(T base, U other) 661 { 662 mixin(generateCode()); 663 } 664 } 665 666 /// 667 unittest 668 { 669 import std.range : front; 670 671 auto xs = ["a", "b"]; 672 auto ys = ["c", "d"]; 673 auto labels = ["e", "f"]; 674 auto aes = Aes!(string[], "x", string[], "y", string[], "label")(xs, ys, labels); 675 676 auto nlAes = merge(aes, Aes!(NumericLabel!(string[]), "x", 677 NumericLabel!(string[]), "y")(NumericLabel!(string[])(aes.x), 678 NumericLabel!(string[])(aes.y))); 679 680 assertEqual(nlAes.x.front[0], 0); 681 assertEqual(nlAes.label.front, "e"); 682 }