1 /** 2 Utilities used by tsv-utils applications. InputFieldReordering, BufferedOututRange, 3 and a several others. 4 5 Utilities in this file: 6 $(LIST 7 * [InputFieldReordering] - A class that creates a reordered subset of fields from 8 an input line. Fields in the subset are accessed by array indicies. This is 9 especially useful when processing the subset in a specific order, such as the 10 order listed on the command-line at run-time. 11 12 * [BufferedOutputRange] - An OutputRange with an internal buffer used to buffer 13 output. Intended for use with stdout, it is a significant performance benefit. 14 15 * [joinAppend] - A function that performs a join, but appending the join output to 16 an output stream. It is a performance improvement over using join or joiner with 17 writeln. 18 19 * [getTsvFieldValue] - A convenience function when only a single value is needed from 20 an input line. 21 22 * Field-lists: [parseFieldList], [makeFieldListOptionHandler] - Helper functions for 23 parsing field-lists entered on the command line. 24 25 * [throwIfWindowsNewlineOnUnix] - A utility for Unix platform builds to detecting 26 Windows newlines in input. 27 ) 28 29 Copyright (c) 2015-2018, eBay Software Foundation 30 Initially written by Jon Degenhardt 31 32 License: Boost Licence 1.0 (http://boost.org/LICENSE_1_0.txt) 33 */ 34 35 module tsv_utils.common.utils; 36 37 import std.range; 38 import std.traits : isIntegral, isSomeChar, isSomeString, isUnsigned; 39 import std.typecons : Flag, No, Yes; 40 41 // InputFieldReording class. 42 43 /** Flag used by the InputFieldReordering template. */ 44 alias EnablePartialLines = Flag!"enablePartialLines"; 45 46 /** 47 InputFieldReordering - Move select fields from an input line to an output array, 48 reordering along the way. 49 50 The InputFieldReordering class is used to reorder a subset of fields from an input line. 51 The caller instantiates an InputFieldReordering object at the start of input processing. 52 The instance contains a mapping from input index to output index, plus a buffer holding 53 the reordered fields. The caller processes each input line by calling initNewLine, 54 splitting the line into fields, and calling processNextField on each field. The output 55 buffer is ready when the allFieldsFilled method returns true. 56 57 Fields are not copied, instead the output buffer points to the fields passed by the caller. 58 The caller needs to use or copy the output buffer while the fields are still valid, which 59 is normally until reading the next input line. The program below illustrates the basic use 60 case. It reads stdin and outputs fields [3, 0, 2], in that order. (See also joinAppend, 61 below, which has a performance improvement over join used here.) 62 63 --- 64 int main(string[] args) 65 { 66 import tsv_utils.common.utils; 67 import std.algorithm, std.array, std.range, std.stdio; 68 size_t[] fieldIndicies = [3, 0, 2]; 69 auto fieldReordering = new InputFieldReordering!char(fieldIndicies); 70 foreach (line; stdin.byLine) 71 { 72 fieldReordering.initNewLine; 73 foreach(fieldIndex, fieldValue; line.splitter('\t').enumerate) 74 { 75 fieldReordering.processNextField(fieldIndex, fieldValue); 76 if (fieldReordering.allFieldsFilled) break; 77 } 78 if (fieldReordering.allFieldsFilled) 79 { 80 writeln(fieldReordering.outputFields.join('\t')); 81 } 82 else 83 { 84 writeln("Error: Insufficient number of field on the line."); 85 } 86 } 87 return 0; 88 } 89 --- 90 91 Field indicies are zero-based. An individual field can be listed multiple times. The 92 outputFields array is not valid until all the specified fields have been processed. The 93 allFieldsFilled method tests this. If a line does not have enough fields the outputFields 94 buffer cannot be used. For most TSV applications this is okay, as it means the line is 95 invalid and cannot be used. However, if partial lines are okay, the template can be 96 instantiated with EnablePartialLines.yes. This will ensure that any fields not filled-in 97 are empty strings in the outputFields return. 98 */ 99 class InputFieldReordering(C, EnablePartialLines partialLinesOk = EnablePartialLines.no) 100 if (isSomeChar!C) 101 { 102 /* Implementation: The class works by creating an array of tuples mapping the input 103 * field index to the location in the outputFields array. The 'fromToMap' array is 104 * sorted in input field order, enabling placement in the outputFields buffer during a 105 * pass over the input fields. The map is created by the constructor. An example: 106 * 107 * inputFieldIndicies: [3, 0, 7, 7, 1, 0, 9] 108 * fromToMap: [<0,1>, <0,5>, <1,4>, <3,0>, <7,2>, <7,3>, <9,6>] 109 * 110 * During processing of an a line, an array slice, mapStack, is used to track how 111 * much of the fromToMap remains to be processed. 112 */ 113 import std.range; 114 import std.typecons : Tuple; 115 116 alias TupleFromTo = Tuple!(size_t, "from", size_t, "to"); 117 118 private C[][] outputFieldsBuf; 119 private TupleFromTo[] fromToMap; 120 private TupleFromTo[] mapStack; 121 122 final this(const ref size_t[] inputFieldIndicies, size_t start = 0) pure nothrow @safe 123 { 124 import std.algorithm : sort; 125 126 outputFieldsBuf = new C[][](inputFieldIndicies.length); 127 fromToMap.reserve(inputFieldIndicies.length); 128 129 foreach (to, from; inputFieldIndicies.enumerate(start)) 130 { 131 fromToMap ~= TupleFromTo(from, to); 132 } 133 134 sort(fromToMap); 135 initNewLine; 136 } 137 138 /** initNewLine initializes the object for a new line. */ 139 final void initNewLine() pure nothrow @safe 140 { 141 mapStack = fromToMap; 142 static if (partialLinesOk) 143 { 144 import std.algorithm : each; 145 outputFieldsBuf.each!((ref s) => s.length = 0); 146 } 147 } 148 149 /** processNextField maps an input field to the correct locations in the outputFields 150 * array. It should be called once for each field on the line, in the order found. 151 */ 152 final size_t processNextField(size_t fieldIndex, C[] fieldValue) pure nothrow @safe @nogc 153 { 154 size_t numFilled = 0; 155 while (!mapStack.empty && fieldIndex == mapStack.front.from) 156 { 157 outputFieldsBuf[mapStack.front.to] = fieldValue; 158 mapStack.popFront; 159 numFilled++; 160 } 161 return numFilled; 162 } 163 164 /** allFieldsFilled returned true if all fields expected have been processed. */ 165 final bool allFieldsFilled() const pure nothrow @safe @nogc 166 { 167 return mapStack.empty; 168 } 169 170 /** outputFields is the assembled output fields. Unless partial lines are enabled, 171 * it is only valid after allFieldsFilled is true. 172 */ 173 final C[][] outputFields() pure nothrow @safe @nogc 174 { 175 return outputFieldsBuf[]; 176 } 177 } 178 179 /* Tests using different character types. */ 180 unittest 181 { 182 import std.conv : to; 183 184 auto inputLines = [["r1f0", "r1f1", "r1f2", "r1f3"], 185 ["r2f0", "abc", "ÀBCßßZ", "ghi"], 186 ["r3f0", "123", "456", "789"]]; 187 188 size_t[] fields_2_0 = [2, 0]; 189 190 auto expected_2_0 = [["r1f2", "r1f0"], 191 ["ÀBCßßZ", "r2f0"], 192 ["456", "r3f0"]]; 193 194 char[][][] charExpected_2_0 = to!(char[][][])(expected_2_0); 195 wchar[][][] wcharExpected_2_0 = to!(wchar[][][])(expected_2_0); 196 dchar[][][] dcharExpected_2_0 = to!(dchar[][][])(expected_2_0); 197 dstring[][] dstringExpected_2_0 = to!(dstring[][])(expected_2_0); 198 199 auto charIFR = new InputFieldReordering!char(fields_2_0); 200 auto wcharIFR = new InputFieldReordering!wchar(fields_2_0); 201 auto dcharIFR = new InputFieldReordering!dchar(fields_2_0); 202 203 foreach (lineIndex, line; inputLines) 204 { 205 charIFR.initNewLine; 206 wcharIFR.initNewLine; 207 dcharIFR.initNewLine; 208 209 foreach (fieldIndex, fieldValue; line) 210 { 211 charIFR.processNextField(fieldIndex, to!(char[])(fieldValue)); 212 wcharIFR.processNextField(fieldIndex, to!(wchar[])(fieldValue)); 213 dcharIFR.processNextField(fieldIndex, to!(dchar[])(fieldValue)); 214 215 assert ((fieldIndex >= 2) == charIFR.allFieldsFilled); 216 assert ((fieldIndex >= 2) == wcharIFR.allFieldsFilled); 217 assert ((fieldIndex >= 2) == dcharIFR.allFieldsFilled); 218 } 219 assert(charIFR.allFieldsFilled); 220 assert(wcharIFR.allFieldsFilled); 221 assert(dcharIFR.allFieldsFilled); 222 223 assert(charIFR.outputFields == charExpected_2_0[lineIndex]); 224 assert(wcharIFR.outputFields == wcharExpected_2_0[lineIndex]); 225 assert(dcharIFR.outputFields == dcharExpected_2_0[lineIndex]); 226 } 227 } 228 229 /* Test of partial line support. */ 230 unittest 231 { 232 import std.conv : to; 233 234 auto inputLines = [["r1f0", "r1f1", "r1f2", "r1f3"], 235 ["r2f0", "abc", "ÀBCßßZ", "ghi"], 236 ["r3f0", "123", "456", "789"]]; 237 238 size_t[] fields_2_0 = [2, 0]; 239 240 // The expected states of the output field while each line and field are processed. 241 auto expectedBylineByfield_2_0 = 242 [ 243 [["", "r1f0"], ["", "r1f0"], ["r1f2", "r1f0"], ["r1f2", "r1f0"]], 244 [["", "r2f0"], ["", "r2f0"], ["ÀBCßßZ", "r2f0"], ["ÀBCßßZ", "r2f0"]], 245 [["", "r3f0"], ["", "r3f0"], ["456", "r3f0"], ["456", "r3f0"]], 246 ]; 247 248 char[][][][] charExpectedBylineByfield_2_0 = to!(char[][][][])(expectedBylineByfield_2_0); 249 250 auto charIFR = new InputFieldReordering!(char, EnablePartialLines.yes)(fields_2_0); 251 252 foreach (lineIndex, line; inputLines) 253 { 254 charIFR.initNewLine; 255 foreach (fieldIndex, fieldValue; line) 256 { 257 charIFR.processNextField(fieldIndex, to!(char[])(fieldValue)); 258 assert(charIFR.outputFields == charExpectedBylineByfield_2_0[lineIndex][fieldIndex]); 259 } 260 } 261 } 262 263 /* Field combination tests. */ 264 unittest 265 { 266 import std.conv : to; 267 import std.stdio; 268 269 auto inputLines = [["00", "01", "02", "03"], 270 ["10", "11", "12", "13"], 271 ["20", "21", "22", "23"]]; 272 273 size_t[] fields_0 = [0]; 274 size_t[] fields_3 = [3]; 275 size_t[] fields_01 = [0, 1]; 276 size_t[] fields_10 = [1, 0]; 277 size_t[] fields_03 = [0, 3]; 278 size_t[] fields_30 = [3, 0]; 279 size_t[] fields_0123 = [0, 1, 2, 3]; 280 size_t[] fields_3210 = [3, 2, 1, 0]; 281 size_t[] fields_03001 = [0, 3, 0, 0, 1]; 282 283 auto expected_0 = to!(char[][][])([["00"], 284 ["10"], 285 ["20"]]); 286 287 auto expected_3 = to!(char[][][])([["03"], 288 ["13"], 289 ["23"]]); 290 291 auto expected_01 = to!(char[][][])([["00", "01"], 292 ["10", "11"], 293 ["20", "21"]]); 294 295 auto expected_10 = to!(char[][][])([["01", "00"], 296 ["11", "10"], 297 ["21", "20"]]); 298 299 auto expected_03 = to!(char[][][])([["00", "03"], 300 ["10", "13"], 301 ["20", "23"]]); 302 303 auto expected_30 = to!(char[][][])([["03", "00"], 304 ["13", "10"], 305 ["23", "20"]]); 306 307 auto expected_0123 = to!(char[][][])([["00", "01", "02", "03"], 308 ["10", "11", "12", "13"], 309 ["20", "21", "22", "23"]]); 310 311 auto expected_3210 = to!(char[][][])([["03", "02", "01", "00"], 312 ["13", "12", "11", "10"], 313 ["23", "22", "21", "20"]]); 314 315 auto expected_03001 = to!(char[][][])([["00", "03", "00", "00", "01"], 316 ["10", "13", "10", "10", "11"], 317 ["20", "23", "20", "20", "21"]]); 318 319 auto ifr_0 = new InputFieldReordering!char(fields_0); 320 auto ifr_3 = new InputFieldReordering!char(fields_3); 321 auto ifr_01 = new InputFieldReordering!char(fields_01); 322 auto ifr_10 = new InputFieldReordering!char(fields_10); 323 auto ifr_03 = new InputFieldReordering!char(fields_03); 324 auto ifr_30 = new InputFieldReordering!char(fields_30); 325 auto ifr_0123 = new InputFieldReordering!char(fields_0123); 326 auto ifr_3210 = new InputFieldReordering!char(fields_3210); 327 auto ifr_03001 = new InputFieldReordering!char(fields_03001); 328 329 foreach (lineIndex, line; inputLines) 330 { 331 ifr_0.initNewLine; 332 ifr_3.initNewLine; 333 ifr_01.initNewLine; 334 ifr_10.initNewLine; 335 ifr_03.initNewLine; 336 ifr_30.initNewLine; 337 ifr_0123.initNewLine; 338 ifr_3210.initNewLine; 339 ifr_03001.initNewLine; 340 341 foreach (fieldIndex, fieldValue; line) 342 { 343 ifr_0.processNextField(fieldIndex, to!(char[])(fieldValue)); 344 ifr_3.processNextField(fieldIndex, to!(char[])(fieldValue)); 345 ifr_01.processNextField(fieldIndex, to!(char[])(fieldValue)); 346 ifr_10.processNextField(fieldIndex, to!(char[])(fieldValue)); 347 ifr_03.processNextField(fieldIndex, to!(char[])(fieldValue)); 348 ifr_30.processNextField(fieldIndex, to!(char[])(fieldValue)); 349 ifr_0123.processNextField(fieldIndex, to!(char[])(fieldValue)); 350 ifr_3210.processNextField(fieldIndex, to!(char[])(fieldValue)); 351 ifr_03001.processNextField(fieldIndex, to!(char[])(fieldValue)); 352 } 353 354 assert(ifr_0.outputFields == expected_0[lineIndex]); 355 assert(ifr_3.outputFields == expected_3[lineIndex]); 356 assert(ifr_01.outputFields == expected_01[lineIndex]); 357 assert(ifr_10.outputFields == expected_10[lineIndex]); 358 assert(ifr_03.outputFields == expected_03[lineIndex]); 359 assert(ifr_30.outputFields == expected_30[lineIndex]); 360 assert(ifr_0123.outputFields == expected_0123[lineIndex]); 361 assert(ifr_3210.outputFields == expected_3210[lineIndex]); 362 assert(ifr_03001.outputFields == expected_03001[lineIndex]); 363 } 364 } 365 366 /** 367 BufferedOutputRange is a performance enhancement over writing directly to an output 368 stream. It holds a File open for write or an OutputRange. Ouput is accumulated in an 369 internal buffer and written to the output stream as a block. 370 371 Writing to stdout is a key use case. BufferedOutputRange is often dramatically faster 372 than writing to stdout directly. This is especially noticable for outputs with short 373 lines, as it blocks many writes together in a single write. 374 375 The internal buffer is written to the output stream after flushSize has been reached. 376 This is checked at newline boundaries, when appendln is called or when put is called 377 with a single newline character. Other writes check maxSize, which is used to avoid 378 runaway buffers. 379 380 381 BufferedOutputRange has a put method allowing it to be used a range. It has a number 382 of other methods providing additional control. 383 384 $(LIST 385 * `this(outputStream [, flushSize, reserveSize, maxSize])` - Constructor. Takes the 386 output stream, e.g. stdout. Other arguments are optional, defaults normally suffice. 387 388 * `append(stuff)` - Append to the internal buffer. 389 390 * `appendln(stuff)` - Append to the internal buffer, followed by a newline. The buffer 391 is flushed to the output stream if is has reached flushSize. 392 393 * `appendln()` - Append a newline to the internal buffer. The buffer is flushed to the 394 output stream if is has reached flushSize. 395 396 * `joinAppend(inputRange, delim)` - An optimization of `append(inputRange.joiner(delim))`. 397 For reasons that are not clear, joiner is quite slow. 398 399 * `flushIfFull()` - Flush the internal buffer to the output stream if flushSize has been 400 reached. 401 402 * `flush()` - Write the internal buffer to the output stream. 403 404 * `put(stuff)` - Appends to the internal buffer. Acts as `appendln()` if passed a single 405 newline character, '\n' or "\n". 406 ) 407 408 The internal buffer is automatically flushed when the BufferedOutputRange goes out of 409 scope. 410 */ 411 412 import std.stdio : isFileHandle; 413 import std.range : isOutputRange; 414 import std.traits : Unqual; 415 416 struct BufferedOutputRange(OutputTarget) 417 if (isFileHandle!(Unqual!OutputTarget) || isOutputRange!(Unqual!OutputTarget, char)) 418 { 419 import std.range : isOutputRange; 420 import std.array : appender; 421 import std.format : format; 422 423 /* Identify the output element type. Only supporting char and ubyte for now. */ 424 static if (isFileHandle!OutputTarget || isOutputRange!(OutputTarget, char)) 425 { 426 alias C = char; 427 } 428 else static if (isOutputRange!(OutputTarget, ubyte)) 429 { 430 alias C = ubyte; 431 } 432 else static assert(false); 433 434 private enum defaultReserveSize = 11264; 435 private enum defaultFlushSize = 10240; 436 private enum defaultMaxSize = 4194304; 437 438 private OutputTarget _outputTarget; 439 private auto _outputBuffer = appender!(C[]); 440 private immutable size_t _flushSize; 441 private immutable size_t _maxSize; 442 443 this(OutputTarget outputTarget, 444 size_t flushSize = defaultFlushSize, 445 size_t reserveSize = defaultReserveSize, 446 size_t maxSize = defaultMaxSize) 447 { 448 assert(flushSize <= maxSize); 449 450 _outputTarget = outputTarget; 451 _flushSize = flushSize; 452 _maxSize = (flushSize <= maxSize) ? maxSize : flushSize; 453 _outputBuffer.reserve(reserveSize); 454 } 455 456 ~this() 457 { 458 flush(); 459 } 460 461 void flush() 462 { 463 static if (isFileHandle!OutputTarget) _outputTarget.write(_outputBuffer.data); 464 else _outputTarget.put(_outputBuffer.data); 465 466 _outputBuffer.clear; 467 } 468 469 bool flushIfFull() 470 { 471 bool isFull = _outputBuffer.data.length >= _flushSize; 472 if (isFull) flush(); 473 return isFull; 474 } 475 476 /* flushIfMaxSize is a safety check to avoid runaway buffer growth. */ 477 void flushIfMaxSize() 478 { 479 if (_outputBuffer.data.length >= _maxSize) flush(); 480 } 481 482 private void appendRaw(T)(T stuff) 483 { 484 import std.range : rangePut = put; 485 rangePut(_outputBuffer, stuff); 486 } 487 488 void append(T)(T stuff) 489 { 490 appendRaw(stuff); 491 flushIfMaxSize(); 492 } 493 494 bool appendln() 495 { 496 appendRaw('\n'); 497 return flushIfFull(); 498 } 499 500 bool appendln(T)(T stuff) 501 { 502 appendRaw(stuff); 503 return appendln(); 504 } 505 506 /* joinAppend is an optimization of append(inputRange.joiner(delimiter). 507 * This form is quite a bit faster, 40%+ on some benchmarks. 508 */ 509 void joinAppend(InputRange, E)(InputRange inputRange, E delimiter) 510 if (isInputRange!InputRange && 511 is(ElementType!InputRange : const C[]) && 512 (is(E : const C[]) || is(E : const C))) 513 { 514 if (!inputRange.empty) 515 { 516 appendRaw(inputRange.front); 517 inputRange.popFront; 518 } 519 foreach (x; inputRange) 520 { 521 appendRaw(delimiter); 522 appendRaw(x); 523 } 524 flushIfMaxSize(); 525 } 526 527 /* Make this an output range. */ 528 void put(T)(T stuff) 529 { 530 import std.traits; 531 import std.stdio; 532 533 static if (isSomeChar!T) 534 { 535 if (stuff == '\n') appendln(); 536 else appendRaw(stuff); 537 } 538 else static if (isSomeString!T) 539 { 540 if (stuff == "\n") appendln(); 541 else append(stuff); 542 } 543 else append(stuff); 544 } 545 } 546 547 unittest 548 { 549 import tsv_utils.common.unittest_utils; 550 import std.file : rmdirRecurse, readText; 551 import std.path : buildPath; 552 553 auto testDir = makeUnittestTempDir("tsv_utils_buffered_output"); 554 scope(exit) testDir.rmdirRecurse; 555 556 import std.algorithm : map, joiner; 557 import std.range : iota; 558 import std.conv : to; 559 560 /* Basic test. Note that exiting the scope triggers flush. */ 561 string filepath1 = buildPath(testDir, "file1.txt"); 562 { 563 import std.stdio : File; 564 565 auto ostream = BufferedOutputRange!File(filepath1.File("w")); 566 ostream.append("file1: "); 567 ostream.append("abc"); 568 ostream.append(["def", "ghi", "jkl"]); 569 ostream.appendln(100.to!string); 570 ostream.append(iota(0, 10).map!(x => x.to!string).joiner(" ")); 571 ostream.appendln(); 572 } 573 assert(filepath1.readText == "file1: abcdefghijkl100\n0 1 2 3 4 5 6 7 8 9\n"); 574 575 /* Test with no reserve and no flush at every line. */ 576 string filepath2 = buildPath(testDir, "file2.txt"); 577 { 578 import std.stdio : File; 579 580 auto ostream = BufferedOutputRange!File(filepath2.File("w"), 0, 0); 581 ostream.append("file2: "); 582 ostream.append("abc"); 583 ostream.append(["def", "ghi", "jkl"]); 584 ostream.appendln("100"); 585 ostream.append(iota(0, 10).map!(x => x.to!string).joiner(" ")); 586 ostream.appendln(); 587 } 588 assert(filepath2.readText == "file2: abcdefghijkl100\n0 1 2 3 4 5 6 7 8 9\n"); 589 590 /* With a locking text writer. Requires version 2.078.0 591 See: https://issues.dlang.org/show_bug.cgi?id=9661 592 */ 593 static if (__VERSION__ >= 2078) 594 { 595 string filepath3 = buildPath(testDir, "file3.txt"); 596 { 597 import std.stdio : File; 598 599 auto ltw = filepath3.File("w").lockingTextWriter; 600 { 601 auto ostream = BufferedOutputRange!(typeof(ltw))(ltw); 602 ostream.append("file3: "); 603 ostream.append("abc"); 604 ostream.append(["def", "ghi", "jkl"]); 605 ostream.appendln("100"); 606 ostream.append(iota(0, 10).map!(x => x.to!string).joiner(" ")); 607 ostream.appendln(); 608 } 609 } 610 assert(filepath3.readText == "file3: abcdefghijkl100\n0 1 2 3 4 5 6 7 8 9\n"); 611 } 612 613 /* With an Appender. */ 614 import std.array : appender; 615 auto app1 = appender!(char[]); 616 { 617 auto ostream = BufferedOutputRange!(typeof(app1))(app1); 618 ostream.append("appender1: "); 619 ostream.append("abc"); 620 ostream.append(["def", "ghi", "jkl"]); 621 ostream.appendln("100"); 622 ostream.append(iota(0, 10).map!(x => x.to!string).joiner(" ")); 623 ostream.appendln(); 624 } 625 assert(app1.data == "appender1: abcdefghijkl100\n0 1 2 3 4 5 6 7 8 9\n"); 626 627 /* With an Appender, but checking flush boundaries. */ 628 auto app2 = appender!(char[]); 629 { 630 auto ostream = BufferedOutputRange!(typeof(app2))(app2, 10, 0); // Flush if 10+ 631 bool wasFlushed = false; 632 633 assert(app2.data == ""); 634 635 ostream.append("12345678"); // Not flushed yet. 636 assert(app2.data == ""); 637 638 wasFlushed = ostream.appendln; // Nineth char, not flushed yet. 639 assert(!wasFlushed); 640 assert(app2.data == ""); 641 642 wasFlushed = ostream.appendln; // Tenth char, now flushed. 643 assert(wasFlushed); 644 assert(app2.data == "12345678\n\n"); 645 646 app2.clear; 647 assert(app2.data == ""); 648 649 ostream.append("12345678"); 650 651 wasFlushed = ostream.flushIfFull; 652 assert(!wasFlushed); 653 assert(app2.data == ""); 654 655 ostream.flush; 656 assert(app2.data == "12345678"); 657 658 app2.clear; 659 assert(app2.data == ""); 660 661 ostream.append("123456789012345"); 662 assert(app2.data == ""); 663 } 664 assert(app2.data == "123456789012345"); 665 666 /* Using joinAppend. */ 667 auto app1b = appender!(char[]); 668 { 669 auto ostream = BufferedOutputRange!(typeof(app1b))(app1b); 670 ostream.append("appenderB: "); 671 ostream.joinAppend(["a", "bc", "def"], '-'); 672 ostream.append(':'); 673 ostream.joinAppend(["g", "hi", "jkl"], '-'); 674 ostream.appendln("*100*"); 675 ostream.joinAppend(iota(0, 6).map!(x => x.to!string), ' '); 676 ostream.append(' '); 677 ostream.joinAppend(iota(6, 10).map!(x => x.to!string), " "); 678 ostream.appendln(); 679 } 680 assert(app1b.data == "appenderB: a-bc-def:g-hi-jkl*100*\n0 1 2 3 4 5 6 7 8 9\n", 681 "app1b.data: |" ~app1b.data ~ "|"); 682 683 /* Operating as an output range. When passed to a function as a ref, exiting 684 * the function does not flush. When passed as a value, it get flushed when 685 * the function returns. Also test both UCFS and non-UFCS styles. 686 */ 687 688 void outputStuffAsRef(T)(ref T range) 689 if (isOutputRange!(T, char)) 690 { 691 range.put('1'); 692 put(range, "23"); 693 range.put('\n'); 694 range.put(["5", "67"]); 695 put(range, iota(8, 10).map!(x => x.to!string)); 696 put(range, "\n"); 697 } 698 699 void outputStuffAsVal(T)(T range) 700 if (isOutputRange!(T, char)) 701 { 702 put(range, '1'); 703 range.put("23"); 704 put(range, '\n'); 705 put(range, ["5", "67"]); 706 range.put(iota(8, 10).map!(x => x.to!string)); 707 range.put("\n"); 708 } 709 710 auto app3 = appender!(char[]); 711 { 712 auto ostream = BufferedOutputRange!(typeof(app3))(app3, 12, 0); 713 outputStuffAsRef(ostream); 714 assert(app3.data == "", "app3.data: |" ~app3.data ~ "|"); 715 outputStuffAsRef(ostream); 716 assert(app3.data == "123\n56789\n123\n", "app3.data: |" ~app3.data ~ "|"); 717 } 718 assert(app3.data == "123\n56789\n123\n56789\n", "app3.data: |" ~app3.data ~ "|"); 719 720 auto app4 = appender!(char[]); 721 { 722 auto ostream = BufferedOutputRange!(typeof(app4))(app4, 12, 0); 723 outputStuffAsVal(ostream); 724 assert(app4.data == "123\n56789\n", "app4.data: |" ~app4.data ~ "|"); 725 outputStuffAsVal(ostream); 726 assert(app4.data == "123\n56789\n123\n56789\n", "app4.data: |" ~app4.data ~ "|"); 727 } 728 assert(app4.data == "123\n56789\n123\n56789\n", "app4.data: |" ~app4.data ~ "|"); 729 730 /* Test maxSize. */ 731 auto app5 = appender!(char[]); 732 { 733 auto ostream = BufferedOutputRange!(typeof(app5))(app5, 5, 0, 10); // maxSize 10 734 assert(app5.data == ""); 735 736 ostream.append("1234567"); // Not flushed yet (no newline). 737 assert(app5.data == ""); 738 739 ostream.append("89012"); // Flushed by maxSize 740 assert(app5.data == "123456789012"); 741 742 ostream.put("1234567"); // Not flushed yet (no newline). 743 assert(app5.data == "123456789012"); 744 745 ostream.put("89012"); // Flushed by maxSize 746 assert(app5.data == "123456789012123456789012"); 747 748 ostream.joinAppend(["ab", "cd"], '-'); // Not flushed yet 749 ostream.joinAppend(["de", "gh", "ij"], '-'); // Flushed by maxSize 750 assert(app5.data == "123456789012123456789012ab-cdde-gh-ij"); 751 } 752 assert(app5.data == "123456789012123456789012ab-cdde-gh-ij"); 753 } 754 755 /** 756 joinAppend performs a join operation on an input range, appending the results to 757 an output range. 758 759 Note: The main uses of joinAppend have been replaced by BufferedOutputRange, which has 760 its own joinAppend method. 761 762 joinAppend was written as a performance enhancement over using std.algorithm.joiner 763 or std.array.join with writeln. Using joiner with writeln is quite slow, 3-4x slower 764 than std.array.join with writeln. The joiner performance may be due to interaction 765 with writeln, this was not investigated. Using joiner with stdout.lockingTextWriter 766 is better, but still substantially slower than join. Using join works reasonably well, 767 but is allocating memory unnecessarily. 768 769 Using joinAppend with Appender is a bit faster than join, and allocates less memory. 770 The Appender re-uses the underlying data buffer, saving memory. The example below 771 illustrates. It is a modification of the InputFieldReordering example. The role 772 Appender plus joinAppend are playing is to buffer the output. BufferedOutputRange 773 uses a similar technique to buffer multiple lines. 774 775 --- 776 int main(string[] args) 777 { 778 import tsvutil; 779 import std.algorithm, std.array, std.range, std.stdio; 780 size_t[] fieldIndicies = [3, 0, 2]; 781 auto fieldReordering = new InputFieldReordering!char(fieldIndicies); 782 auto outputBuffer = appender!(char[]); 783 foreach (line; stdin.byLine) 784 { 785 fieldReordering.initNewLine; 786 foreach(fieldIndex, fieldValue; line.splitter('\t').enumerate) 787 { 788 fieldReordering.processNextField(fieldIndex, fieldValue); 789 if (fieldReordering.allFieldsFilled) break; 790 } 791 if (fieldReordering.allFieldsFilled) 792 { 793 outputBuffer.clear; 794 writeln(fieldReordering.outputFields.joinAppend(outputBuffer, ('\t'))); 795 } 796 else 797 { 798 writeln("Error: Insufficient number of field on the line."); 799 } 800 } 801 return 0; 802 } 803 --- 804 */ 805 OutputRange joinAppend(InputRange, OutputRange, E) 806 (InputRange inputRange, ref OutputRange outputRange, E delimiter) 807 if (isInputRange!InputRange && 808 (is(ElementType!InputRange : const E[]) && 809 isOutputRange!(OutputRange, E[])) 810 || 811 (is(ElementType!InputRange : const E) && 812 isOutputRange!(OutputRange, E)) 813 ) 814 { 815 if (!inputRange.empty) 816 { 817 outputRange.put(inputRange.front); 818 inputRange.popFront; 819 } 820 foreach (x; inputRange) 821 { 822 outputRange.put(delimiter); 823 outputRange.put(x); 824 } 825 return outputRange; 826 } 827 828 @safe unittest 829 { 830 import std.array : appender; 831 import std.algorithm : equal; 832 833 char[] c1 = ['a', 'b', 'c']; 834 char[] c2 = ['d', 'e', 'f']; 835 char[] c3 = ['g', 'h', 'i']; 836 auto cvec = [c1, c2, c3]; 837 838 auto s1 = "abc"; 839 auto s2 = "def"; 840 auto s3 = "ghi"; 841 auto svec = [s1, s2, s3]; 842 843 auto charAppender = appender!(char[])(); 844 845 assert(cvec.joinAppend(charAppender, '_').data == "abc_def_ghi"); 846 assert(equal(cvec, [c1, c2, c3])); 847 848 charAppender.put('$'); 849 assert(svec.joinAppend(charAppender, '|').data == "abc_def_ghi$abc|def|ghi"); 850 assert(equal(cvec, [s1, s2, s3])); 851 852 charAppender.clear; 853 assert(svec.joinAppend(charAppender, '|').data == "abc|def|ghi"); 854 855 auto intAppender = appender!(int[])(); 856 857 auto i1 = [100, 101, 102]; 858 auto i2 = [200, 201, 202]; 859 auto i3 = [300, 301, 302]; 860 auto ivec = [i1, i2, i3]; 861 862 assert(ivec.joinAppend(intAppender, 0).data == 863 [100, 101, 102, 0, 200, 201, 202, 0, 300, 301, 302]); 864 865 intAppender.clear; 866 assert(i1.joinAppend(intAppender, 0).data == 867 [100, 0, 101, 0, 102]); 868 assert(i2.joinAppend(intAppender, 1).data == 869 [100, 0, 101, 0, 102, 870 200, 1, 201, 1, 202]); 871 assert(i3.joinAppend(intAppender, 2).data == 872 [100, 0, 101, 0, 102, 873 200, 1, 201, 1, 202, 874 300, 2, 301, 2, 302]); 875 } 876 877 /** 878 getTsvFieldValue extracts the value of a single field from a delimited text string. 879 880 This is a convenience function intended for cases when only a single field from an 881 input line is needed. If multiple values are needed, it will be more efficient to 882 work directly with std.algorithm.splitter or the InputFieldReordering class. 883 884 The input text is split by a delimiter character. The specified field is converted 885 to the desired type and the value returned. 886 887 An exception is thrown if there are not enough fields on the line or if conversion 888 fails. Conversion is done with std.conv.to, it throws a std.conv.ConvException on 889 failure. If not enough fields, the exception text is generated referencing 1-upped 890 field numbers as would be provided by command line users. 891 */ 892 T getTsvFieldValue(T, C)(const C[] line, size_t fieldIndex, C delim) pure @safe 893 if (isSomeChar!C) 894 { 895 import std.algorithm : splitter; 896 import std.conv : to; 897 import std.format : format; 898 import std.range; 899 900 auto splitLine = line.splitter(delim); 901 size_t atField = 0; 902 903 while (atField < fieldIndex && !splitLine.empty) 904 { 905 splitLine.popFront; 906 atField++; 907 } 908 909 T val; 910 if (splitLine.empty) 911 { 912 if (fieldIndex == 0) 913 { 914 /* This is a workaround to a splitter special case - If the input is empty, 915 * the returned split range is empty. This doesn't properly represent a single 916 * column file. More correct mathematically, and for this case, would be a 917 * single value representing an empty string. The input line is a convenient 918 * source of an empty line. Info: 919 * Bug: https://issues.dlang.org/show_bug.cgi?id=15735 920 * Pull Request: https://github.com/D-Programming-Language/phobos/pull/4030 921 */ 922 assert(line.empty); 923 val = line.to!T; 924 } 925 else 926 { 927 throw new Exception( 928 format("Not enough fields on line. Number required: %d; Number found: %d", 929 fieldIndex + 1, atField)); 930 } 931 } 932 else 933 { 934 val = splitLine.front.to!T; 935 } 936 937 return val; 938 } 939 940 unittest 941 { 942 import std.conv : ConvException, to; 943 import std.exception; 944 945 /* Common cases. */ 946 assert(getTsvFieldValue!double("123", 0, '\t') == 123.0); 947 assert(getTsvFieldValue!double("-10.5", 0, '\t') == -10.5); 948 assert(getTsvFieldValue!size_t("abc|123", 1, '|') == 123); 949 assert(getTsvFieldValue!int("紅\t红\t99", 2, '\t') == 99); 950 assert(getTsvFieldValue!int("紅\t红\t99", 2, '\t') == 99); 951 assert(getTsvFieldValue!string("紅\t红\t99", 2, '\t') == "99"); 952 assert(getTsvFieldValue!string("紅\t红\t99", 1, '\t') == "红"); 953 assert(getTsvFieldValue!string("紅\t红\t99", 0, '\t') == "紅"); 954 assert(getTsvFieldValue!string("红色和绿色\tred and green\t赤と緑\t10.5", 2, '\t') == "赤と緑"); 955 assert(getTsvFieldValue!double("红色和绿色\tred and green\t赤と緑\t10.5", 3, '\t') == 10.5); 956 957 /* The empty field cases. */ 958 assert(getTsvFieldValue!string("", 0, '\t') == ""); 959 assert(getTsvFieldValue!string("\t", 0, '\t') == ""); 960 assert(getTsvFieldValue!string("\t", 1, '\t') == ""); 961 assert(getTsvFieldValue!string("", 0, ':') == ""); 962 assert(getTsvFieldValue!string(":", 0, ':') == ""); 963 assert(getTsvFieldValue!string(":", 1, ':') == ""); 964 965 /* Tests with different data types. */ 966 string stringLine = "orange and black\tნარინჯისფერი და შავი\t88.5"; 967 char[] charLine = "orange and black\tნარინჯისფერი და შავი\t88.5".to!(char[]); 968 dchar[] dcharLine = stringLine.to!(dchar[]); 969 wchar[] wcharLine = stringLine.to!(wchar[]); 970 971 assert(getTsvFieldValue!string(stringLine, 0, '\t') == "orange and black"); 972 assert(getTsvFieldValue!string(stringLine, 1, '\t') == "ნარინჯისფერი და შავი"); 973 assert(getTsvFieldValue!wstring(stringLine, 1, '\t') == "ნარინჯისფერი და შავი".to!wstring); 974 assert(getTsvFieldValue!double(stringLine, 2, '\t') == 88.5); 975 976 assert(getTsvFieldValue!string(charLine, 0, '\t') == "orange and black"); 977 assert(getTsvFieldValue!string(charLine, 1, '\t') == "ნარინჯისფერი და შავი"); 978 assert(getTsvFieldValue!wstring(charLine, 1, '\t') == "ნარინჯისფერი და შავი".to!wstring); 979 assert(getTsvFieldValue!double(charLine, 2, '\t') == 88.5); 980 981 assert(getTsvFieldValue!string(dcharLine, 0, '\t') == "orange and black"); 982 assert(getTsvFieldValue!string(dcharLine, 1, '\t') == "ნარინჯისფერი და შავი"); 983 assert(getTsvFieldValue!wstring(dcharLine, 1, '\t') == "ნარინჯისფერი და შავი".to!wstring); 984 assert(getTsvFieldValue!double(dcharLine, 2, '\t') == 88.5); 985 986 assert(getTsvFieldValue!string(wcharLine, 0, '\t') == "orange and black"); 987 assert(getTsvFieldValue!string(wcharLine, 1, '\t') == "ნარინჯისფერი და შავი"); 988 assert(getTsvFieldValue!wstring(wcharLine, 1, '\t') == "ნარინჯისფერი და შავი".to!wstring); 989 assert(getTsvFieldValue!double(wcharLine, 2, '\t') == 88.5); 990 991 /* Conversion errors. */ 992 assertThrown!ConvException(getTsvFieldValue!double("", 0, '\t')); 993 assertThrown!ConvException(getTsvFieldValue!double("abc", 0, '|')); 994 assertThrown!ConvException(getTsvFieldValue!size_t("-1", 0, '|')); 995 assertThrown!ConvException(getTsvFieldValue!size_t("a23|23.4", 1, '|')); 996 assertThrown!ConvException(getTsvFieldValue!double("23.5|def", 1, '|')); 997 998 /* Not enough field errors. These should throw, but not a ConvException.*/ 999 assertThrown(assertNotThrown!ConvException(getTsvFieldValue!double("", 1, '\t'))); 1000 assertThrown(assertNotThrown!ConvException(getTsvFieldValue!double("abc", 1, '\t'))); 1001 assertThrown(assertNotThrown!ConvException(getTsvFieldValue!double("abc\tdef", 2, '\t'))); 1002 } 1003 1004 /** 1005 Field-lists - A field-list is a string entered on the command line identifying one or more 1006 field numbers. They are used by the majority of the tsv utility applications. There are 1007 two helper functions, makeFieldListOptionHandler and parseFieldList. Most applications 1008 will use makeFieldListOptionHandler, it creates a delegate that can be passed to 1009 std.getopt to process the command option. Actual processing of the option text is done by 1010 parseFieldList. It can be called directly when the text of the option value contains more 1011 than just the field number. 1012 1013 Syntax and behavior: 1014 1015 A 'field-list' is a list of numeric field numbers entered on the command line. Fields are 1016 1-upped integers representing locations in an input line, in the traditional meaning of 1017 Unix command line tools. Fields can be entered as single numbers or a range. Multiple 1018 entries are separated by commas. Some examples (with 'fields' as the command line option): 1019 1020 --fields 3 // Single field 1021 --fields 4,1 // Two fields 1022 --fields 3-9 // A range, fields 3 to 9 inclusive 1023 --fields 1,2,7-34,11 // A mix of ranges and fields 1024 --fields 15-5,3-1 // Two ranges in reverse order. 1025 1026 Incomplete ranges are not supported, for example, '6-'. Zero is disallowed as a field 1027 value by default, but can be enabled to support the notion of zero as representing the 1028 entire line. However, zero cannot be part of a range. Field numbers are one-based by 1029 default, but can be converted to zero-based. If conversion to zero-based is enabled, field 1030 number zero must be disallowed or a signed integer type specified for the returned range. 1031 1032 An error is thrown if an invalid field specification is encountered. Error text is 1033 intended for display. Error conditions include: 1034 - Empty fields list 1035 - Empty value, e.g. Two consequtive commas, a trailing comma, or a leading comma 1036 - String that does not parse as a valid integer 1037 - Negative integers, or zero if zero is disallowed. 1038 - An incomplete range 1039 - Zero used as part of a range. 1040 1041 No other behaviors are enforced. Repeated values are accepted. If zero is allowed, other 1042 field numbers can be entered as well. Additional restrictions need to be applied by the 1043 caller. 1044 1045 Notes: 1046 - The data type determines the max field number that can be entered. Enabling conversion 1047 to zero restricts to the signed version of the data type. 1048 - Use 'import std.typecons : Yes, No' to use the convertToZeroBasedIndex and 1049 allowFieldNumZero template parameters. 1050 */ 1051 1052 /** [Yes|No].convertToZeroBasedIndex parameter controls whether field numbers are 1053 * converted to zero-based indices by makeFieldListOptionHander and parseFieldList. 1054 */ 1055 alias ConvertToZeroBasedIndex = Flag!"convertToZeroBasedIndex"; 1056 1057 /** [Yes|No].allowFieldNumZero parameter controls whether zero is a valid field. This is 1058 * used by makeFieldListOptionHander and parseFieldList. 1059 */ 1060 alias AllowFieldNumZero = Flag!"allowFieldNumZero"; 1061 1062 alias OptionHandlerDelegate = void delegate(string option, string value); 1063 1064 /** 1065 makeFieldListOptionHandler creates a std.getopt option hander for processing field lists 1066 entered on the command line. A field list is as defined by parseFieldList. 1067 */ 1068 OptionHandlerDelegate makeFieldListOptionHandler( 1069 T, 1070 ConvertToZeroBasedIndex convertToZero = No.convertToZeroBasedIndex, 1071 AllowFieldNumZero allowZero = No.allowFieldNumZero) 1072 (ref T[] fieldsArray) 1073 if (isIntegral!T && (!allowZero || !convertToZero || !isUnsigned!T)) 1074 { 1075 void fieldListOptionHandler(ref T[] fieldArray, string option, string value) 1076 { 1077 import std.algorithm : each; 1078 try value.parseFieldList!(T, convertToZero, allowZero).each!(x => fieldArray ~= x); 1079 catch (Exception exc) 1080 { 1081 import std.format : format; 1082 exc.msg = format("[--%s] %s", option, exc.msg); 1083 throw exc; 1084 } 1085 } 1086 1087 return (option, value) => fieldListOptionHandler(fieldsArray, option, value); 1088 } 1089 1090 unittest 1091 { 1092 import std.exception : assertThrown, assertNotThrown; 1093 import std.getopt; 1094 1095 { 1096 size_t[] fields; 1097 auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"]; 1098 getopt(args, "f|fields", fields.makeFieldListOptionHandler); 1099 assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]); 1100 } 1101 { 1102 size_t[] fields; 1103 auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"]; 1104 getopt(args, 1105 "f|fields", fields.makeFieldListOptionHandler!(size_t, Yes.convertToZeroBasedIndex)); 1106 assert(fields == [0, 1, 3, 6, 7, 8, 22, 21, 20]); 1107 } 1108 { 1109 size_t[] fields; 1110 auto args = ["program", "-f", "0"]; 1111 getopt(args, 1112 "f|fields", fields.makeFieldListOptionHandler!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1113 assert(fields == [0]); 1114 } 1115 { 1116 size_t[] fields; 1117 auto args = ["program", "-f", "0", "-f", "1,0", "-f", "0,1"]; 1118 getopt(args, 1119 "f|fields", fields.makeFieldListOptionHandler!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1120 assert(fields == [0, 1, 0, 0, 1]); 1121 } 1122 { 1123 size_t[] ints; 1124 size_t[] fields; 1125 auto args = ["program", "--ints", "1,2,3", "--fields", "1", "--ints", "4,5,6", "--fields", "2,4,7-9,23-21"]; 1126 std.getopt.arraySep = ","; 1127 getopt(args, 1128 "i|ints", "Built-in list of integers.", &ints, 1129 "f|fields", "Field-list style integers.", fields.makeFieldListOptionHandler); 1130 assert(ints == [1, 2, 3, 4, 5, 6]); 1131 assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]); 1132 } 1133 1134 /* Basic cases involved unsinged types smaller than size_t. */ 1135 { 1136 uint[] fields; 1137 auto args = ["program", "-f", "0", "-f", "1,0", "-f", "0,1", "-f", "55-58"]; 1138 getopt(args, 1139 "f|fields", fields.makeFieldListOptionHandler!(uint, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1140 assert(fields == [0, 1, 0, 0, 1, 55, 56, 57, 58]); 1141 } 1142 { 1143 ushort[] fields; 1144 auto args = ["program", "-f", "0", "-f", "1,0", "-f", "0,1", "-f", "55-58"]; 1145 getopt(args, 1146 "f|fields", fields.makeFieldListOptionHandler!(ushort, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1147 assert(fields == [0, 1, 0, 0, 1, 55, 56, 57, 58]); 1148 } 1149 1150 /* Basic cases involving unsigned types. */ 1151 { 1152 long[] fields; 1153 auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"]; 1154 getopt(args, "f|fields", fields.makeFieldListOptionHandler); 1155 assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]); 1156 } 1157 { 1158 long[] fields; 1159 auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"]; 1160 getopt(args, 1161 "f|fields", fields.makeFieldListOptionHandler!(long, Yes.convertToZeroBasedIndex)); 1162 assert(fields == [0, 1, 3, 6, 7, 8, 22, 21, 20]); 1163 } 1164 { 1165 long[] fields; 1166 auto args = ["program", "-f", "0"]; 1167 getopt(args, 1168 "f|fields", fields.makeFieldListOptionHandler!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1169 assert(fields == [-1]); 1170 } 1171 { 1172 int[] fields; 1173 auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"]; 1174 getopt(args, "f|fields", fields.makeFieldListOptionHandler); 1175 assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]); 1176 } 1177 { 1178 int[] fields; 1179 auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"]; 1180 getopt(args, 1181 "f|fields", fields.makeFieldListOptionHandler!(int, Yes.convertToZeroBasedIndex)); 1182 assert(fields == [0, 1, 3, 6, 7, 8, 22, 21, 20]); 1183 } 1184 { 1185 int[] fields; 1186 auto args = ["program", "-f", "0"]; 1187 getopt(args, 1188 "f|fields", fields.makeFieldListOptionHandler!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1189 assert(fields == [-1]); 1190 } 1191 { 1192 short[] fields; 1193 auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"]; 1194 getopt(args, "f|fields", fields.makeFieldListOptionHandler); 1195 assert(fields == [1, 2, 4, 7, 8, 9, 23, 22, 21]); 1196 } 1197 { 1198 short[] fields; 1199 auto args = ["program", "--fields", "1", "--fields", "2,4,7-9,23-21"]; 1200 getopt(args, 1201 "f|fields", fields.makeFieldListOptionHandler!(short, Yes.convertToZeroBasedIndex)); 1202 assert(fields == [0, 1, 3, 6, 7, 8, 22, 21, 20]); 1203 } 1204 { 1205 short[] fields; 1206 auto args = ["program", "-f", "0"]; 1207 getopt(args, 1208 "f|fields", fields.makeFieldListOptionHandler!(short, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1209 assert(fields == [-1]); 1210 } 1211 1212 { 1213 /* Error cases. */ 1214 size_t[] fields; 1215 auto args = ["program", "-f", "0"]; 1216 assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler)); 1217 1218 args = ["program", "-f", "-1"]; 1219 assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler)); 1220 1221 args = ["program", "-f", "--fields", "1"]; 1222 assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler)); 1223 1224 args = ["program", "-f", "a"]; 1225 assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler)); 1226 1227 args = ["program", "-f", "1.5"]; 1228 assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler)); 1229 1230 args = ["program", "-f", "2-"]; 1231 assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler)); 1232 1233 args = ["program", "-f", "3,5,-7"]; 1234 assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler)); 1235 1236 args = ["program", "-f", "3,5,"]; 1237 assertThrown(getopt(args, "f|fields", fields.makeFieldListOptionHandler)); 1238 1239 args = ["program", "-f", "-1"]; 1240 assertThrown(getopt(args, 1241 "f|fields", fields.makeFieldListOptionHandler!( 1242 size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero))); 1243 } 1244 } 1245 1246 /** 1247 parseFieldList lazily generates a range of fields numbers from a 'field-list' string. 1248 */ 1249 auto parseFieldList(T = size_t, 1250 ConvertToZeroBasedIndex convertToZero = No.convertToZeroBasedIndex, 1251 AllowFieldNumZero allowZero = No.allowFieldNumZero) 1252 (string fieldList, char delim = ',') 1253 if (isIntegral!T && (!allowZero || !convertToZero || !isUnsigned!T)) 1254 { 1255 import std.algorithm : splitter; 1256 1257 auto _splitFieldList = fieldList.splitter(delim); 1258 auto _currFieldParse = 1259 (_splitFieldList.empty ? "" : _splitFieldList.front) 1260 .parseFieldRange!(T, convertToZero, allowZero); 1261 1262 if (!_splitFieldList.empty) _splitFieldList.popFront; 1263 1264 struct Result 1265 { 1266 @property bool empty() { return _currFieldParse.empty; } 1267 1268 @property T front() 1269 { 1270 import std.conv : to; 1271 1272 assert(!empty, "Attempting to fetch the front of an empty field-list."); 1273 assert(!_currFieldParse.empty, "Internal error. Call to front with an empty _currFieldParse."); 1274 1275 return _currFieldParse.front.to!T; 1276 } 1277 1278 void popFront() 1279 { 1280 assert(!empty, "Attempting to popFront an empty field-list."); 1281 1282 _currFieldParse.popFront; 1283 if (_currFieldParse.empty && !_splitFieldList.empty) 1284 { 1285 _currFieldParse = _splitFieldList.front.parseFieldRange!(T, convertToZero, allowZero); 1286 _splitFieldList.popFront; 1287 } 1288 } 1289 } 1290 1291 return Result(); 1292 } 1293 1294 unittest 1295 { 1296 import std.algorithm : each, equal; 1297 import std.exception : assertThrown, assertNotThrown; 1298 1299 /* Basic tests. */ 1300 assert("1".parseFieldList.equal([1])); 1301 assert("1,2".parseFieldList.equal([1, 2])); 1302 assert("1,2,3".parseFieldList.equal([1, 2, 3])); 1303 assert("1-2".parseFieldList.equal([1, 2])); 1304 assert("1-2,6-4".parseFieldList.equal([1, 2, 6, 5, 4])); 1305 assert("1-2,1,1-2,2,2-1".parseFieldList.equal([1, 2, 1, 1, 2, 2, 2, 1])); 1306 assert("1-2,5".parseFieldList!size_t.equal([1, 2, 5])); 1307 1308 /* Signed Int tests */ 1309 assert("1".parseFieldList!int.equal([1])); 1310 assert("1,2,3".parseFieldList!int.equal([1, 2, 3])); 1311 assert("1-2".parseFieldList!int.equal([1, 2])); 1312 assert("1-2,6-4".parseFieldList!int.equal([1, 2, 6, 5, 4])); 1313 assert("1-2,5".parseFieldList!int.equal([1, 2, 5])); 1314 1315 /* Convert to zero tests */ 1316 assert("1".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0])); 1317 assert("1,2,3".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0, 1, 2])); 1318 assert("1-2".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0, 1])); 1319 assert("1-2,6-4".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0, 1, 5, 4, 3])); 1320 assert("1-2,5".parseFieldList!(size_t, Yes.convertToZeroBasedIndex).equal([0, 1, 4])); 1321 1322 assert("1".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0])); 1323 assert("1,2,3".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0, 1, 2])); 1324 assert("1-2".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0, 1])); 1325 assert("1-2,6-4".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0, 1, 5, 4, 3])); 1326 assert("1-2,5".parseFieldList!(long, Yes.convertToZeroBasedIndex).equal([0, 1, 4])); 1327 1328 /* Allow zero tests. */ 1329 assert("0".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0])); 1330 assert("1,0,3".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([1, 0, 3])); 1331 assert("1-2,5".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([1, 2, 5])); 1332 assert("0".parseFieldList!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0])); 1333 assert("1,0,3".parseFieldList!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([1, 0, 3])); 1334 assert("1-2,5".parseFieldList!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([1, 2, 5])); 1335 assert("0".parseFieldList!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([-1])); 1336 assert("1,0,3".parseFieldList!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0, -1, 2])); 1337 assert("1-2,5".parseFieldList!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0, 1, 4])); 1338 1339 /* Error cases. */ 1340 assertThrown("".parseFieldList.each); 1341 assertThrown(" ".parseFieldList.each); 1342 assertThrown(",".parseFieldList.each); 1343 assertThrown("5 6".parseFieldList.each); 1344 assertThrown(",7".parseFieldList.each); 1345 assertThrown("8,".parseFieldList.each); 1346 assertThrown("8,9,".parseFieldList.each); 1347 assertThrown("10,,11".parseFieldList.each); 1348 assertThrown("".parseFieldList!(long, Yes.convertToZeroBasedIndex).each); 1349 assertThrown("1,2-3,".parseFieldList!(long, Yes.convertToZeroBasedIndex).each); 1350 assertThrown("2-,4".parseFieldList!(long, Yes.convertToZeroBasedIndex).each); 1351 assertThrown("1,2,3,,4".parseFieldList!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each); 1352 assertThrown(",7".parseFieldList!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each); 1353 assertThrown("8,".parseFieldList!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each); 1354 assertThrown("10,0,,11".parseFieldList!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each); 1355 assertThrown("8,9,".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).each); 1356 1357 assertThrown("0".parseFieldList.each); 1358 assertThrown("1,0,3".parseFieldList.each); 1359 assertThrown("0".parseFieldList!(int, Yes.convertToZeroBasedIndex, No.allowFieldNumZero).each); 1360 assertThrown("1,0,3".parseFieldList!(int, Yes.convertToZeroBasedIndex, No.allowFieldNumZero).each); 1361 assertThrown("0-2,6-0".parseFieldList!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).each); 1362 assertThrown("0-2,6-0".parseFieldList!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).each); 1363 assertThrown("0-2,6-0".parseFieldList!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).each); 1364 } 1365 1366 /* parseFieldRange parses a single number or number range. E.g. '5' or '5-8'. These are 1367 * the values in a field-list separated by a comma or other delimiter. It returns a range 1368 * that iterates over all the values in the range. 1369 */ 1370 private auto parseFieldRange(T = size_t, 1371 ConvertToZeroBasedIndex convertToZero = No.convertToZeroBasedIndex, 1372 AllowFieldNumZero allowZero = No.allowFieldNumZero) 1373 (string fieldRange) 1374 if (isIntegral!T && (!allowZero || !convertToZero || !isUnsigned!T)) 1375 { 1376 import std.algorithm : findSplit; 1377 import std.conv : to; 1378 import std.format : format; 1379 import std.range : iota; 1380 import std.traits : Signed; 1381 1382 /* Pick the largest compatible integral type for the IOTA range. This must be the 1383 * signed type if convertToZero is true, as a reverse order range may end at -1. 1384 */ 1385 static if (convertToZero) alias S = Signed!T; 1386 else alias S = T; 1387 1388 if (fieldRange.length == 0) throw new Exception("Empty field number."); 1389 1390 auto rangeSplit = findSplit(fieldRange, "-"); 1391 1392 if (!rangeSplit[1].empty && (rangeSplit[0].empty || rangeSplit[2].empty)) 1393 { 1394 // Range starts or ends with a dash. 1395 throw new Exception(format("Incomplete ranges are not supported: '%s'", fieldRange)); 1396 } 1397 1398 S start = rangeSplit[0].to!S; 1399 S last = rangeSplit[1].empty ? start : rangeSplit[2].to!S; 1400 Signed!T increment = (start <= last) ? 1 : -1; 1401 1402 static if (allowZero) 1403 { 1404 if (start == 0 && !rangeSplit[1].empty) 1405 { 1406 throw new Exception(format("Zero cannot be used as part of a range: '%s'", fieldRange)); 1407 } 1408 } 1409 1410 static if (allowZero) 1411 { 1412 if (start < 0 || last < 0) 1413 { 1414 throw new Exception(format("Field numbers must be non-negative integers: '%d'", 1415 (start < 0) ? start : last)); 1416 } 1417 } 1418 else 1419 { 1420 if (start < 1 || last < 1) 1421 { 1422 throw new Exception(format("Field numbers must be greater than zero: '%d'", 1423 (start < 1) ? start : last)); 1424 } 1425 } 1426 1427 static if (convertToZero) 1428 { 1429 start--; 1430 last--; 1431 } 1432 1433 return iota(start, last + increment, increment); 1434 } 1435 1436 unittest // parseFieldRange 1437 { 1438 import std.algorithm : equal; 1439 import std.exception : assertThrown, assertNotThrown; 1440 1441 /* Basic cases */ 1442 assert(parseFieldRange("1").equal([1])); 1443 assert("2".parseFieldRange.equal([2])); 1444 assert("3-4".parseFieldRange.equal([3, 4])); 1445 assert("3-5".parseFieldRange.equal([3, 4, 5])); 1446 assert("4-3".parseFieldRange.equal([4, 3])); 1447 assert("10-1".parseFieldRange.equal([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])); 1448 1449 /* Convert to zero-based indices */ 1450 assert(parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)("1").equal([0])); 1451 assert("2".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([1])); 1452 assert("3-4".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([2, 3])); 1453 assert("3-5".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([2, 3, 4])); 1454 assert("4-3".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([3, 2])); 1455 assert("10-1".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex).equal([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])); 1456 1457 /* Allow zero. */ 1458 assert("0".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0])); 1459 assert(parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)("1").equal([1])); 1460 assert("3-4".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([3, 4])); 1461 assert("10-1".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])); 1462 1463 /* Allow zero, convert to zero-based index. */ 1464 assert("0".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([-1])); 1465 assert(parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)("1").equal([0])); 1466 assert("3-4".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([2, 3])); 1467 assert("10-1".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([9, 8, 7, 6, 5, 4, 3, 2, 1, 0])); 1468 1469 /* Alternate integer types. */ 1470 assert("2".parseFieldRange!uint.equal([2])); 1471 assert("3-5".parseFieldRange!uint.equal([3, 4, 5])); 1472 assert("10-1".parseFieldRange!uint.equal([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])); 1473 assert("2".parseFieldRange!int.equal([2])); 1474 assert("3-5".parseFieldRange!int.equal([3, 4, 5])); 1475 assert("10-1".parseFieldRange!int.equal([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])); 1476 assert("2".parseFieldRange!ushort.equal([2])); 1477 assert("3-5".parseFieldRange!ushort.equal([3, 4, 5])); 1478 assert("10-1".parseFieldRange!ushort.equal([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])); 1479 assert("2".parseFieldRange!short.equal([2])); 1480 assert("3-5".parseFieldRange!short.equal([3, 4, 5])); 1481 assert("10-1".parseFieldRange!short.equal([10, 9, 8, 7, 6, 5, 4, 3, 2, 1])); 1482 1483 assert("0".parseFieldRange!(long, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0])); 1484 assert("0".parseFieldRange!(uint, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0])); 1485 assert("0".parseFieldRange!(int, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0])); 1486 assert("0".parseFieldRange!(ushort, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0])); 1487 assert("0".parseFieldRange!(short, No.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([0])); 1488 assert("0".parseFieldRange!(int, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([-1])); 1489 assert("0".parseFieldRange!(short, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero).equal([-1])); 1490 1491 /* Max field value cases. */ 1492 assert("65535".parseFieldRange!ushort.equal([65535])); // ushort max 1493 assert("65533-65535".parseFieldRange!ushort.equal([65533, 65534, 65535])); 1494 assert("32767".parseFieldRange!short.equal([32767])); // short max 1495 assert("32765-32767".parseFieldRange!short.equal([32765, 32766, 32767])); 1496 assert("32767".parseFieldRange!(short, Yes.convertToZeroBasedIndex).equal([32766])); 1497 1498 /* Error cases. */ 1499 assertThrown("".parseFieldRange); 1500 assertThrown(" ".parseFieldRange); 1501 assertThrown("-".parseFieldRange); 1502 assertThrown(" -".parseFieldRange); 1503 assertThrown("- ".parseFieldRange); 1504 assertThrown("1-".parseFieldRange); 1505 assertThrown("-2".parseFieldRange); 1506 assertThrown("-1".parseFieldRange); 1507 assertThrown("1.0".parseFieldRange); 1508 assertThrown("0".parseFieldRange); 1509 assertThrown("0-3".parseFieldRange); 1510 assertThrown("-2-4".parseFieldRange); 1511 assertThrown("2--4".parseFieldRange); 1512 assertThrown("2-".parseFieldRange); 1513 assertThrown("a".parseFieldRange); 1514 assertThrown("0x3".parseFieldRange); 1515 assertThrown("3U".parseFieldRange); 1516 assertThrown("1_000".parseFieldRange); 1517 assertThrown(".".parseFieldRange); 1518 1519 assertThrown("".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1520 assertThrown(" ".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1521 assertThrown("-".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1522 assertThrown("1-".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1523 assertThrown("-2".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1524 assertThrown("-1".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1525 assertThrown("0".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1526 assertThrown("0-3".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1527 assertThrown("-2-4".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1528 assertThrown("2--4".parseFieldRange!(size_t, Yes.convertToZeroBasedIndex)); 1529 1530 assertThrown("".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1531 assertThrown(" ".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1532 assertThrown("-".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1533 assertThrown("1-".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1534 assertThrown("-2".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1535 assertThrown("-1".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1536 assertThrown("0-3".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1537 assertThrown("-2-4".parseFieldRange!(size_t, No.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1538 1539 assertThrown("".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1540 assertThrown(" ".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1541 assertThrown("-".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1542 assertThrown("1-".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1543 assertThrown("-2".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1544 assertThrown("-1".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1545 assertThrown("0-3".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1546 assertThrown("-2-4".parseFieldRange!(long, Yes.convertToZeroBasedIndex, Yes.allowFieldNumZero)); 1547 1548 /* Value out of range cases. */ 1549 assertThrown("65536".parseFieldRange!ushort); // One more than ushort max. 1550 assertThrown("65535-65536".parseFieldRange!ushort); 1551 assertThrown("32768".parseFieldRange!short); // One more than short max. 1552 assertThrown("32765-32768".parseFieldRange!short); 1553 // Convert to zero limits signed range. 1554 assertThrown("32768".parseFieldRange!(ushort, Yes.convertToZeroBasedIndex)); 1555 assert("32767".parseFieldRange!(ushort, Yes.convertToZeroBasedIndex).equal([32766])); 1556 } 1557 1558 /** [Yes|No.newlineWasRemoved] is a template parameter to throwIfWindowsNewlineOnUnix. 1559 * A Yes value indicates the Unix newline was already removed, as might be done via 1560 * std.File.byLine or similar mechanism. 1561 */ 1562 alias NewlineWasRemoved = Flag!"newlineWasRemoved"; 1563 1564 /** 1565 throwIfWindowsLineNewlineOnUnix is used to throw an exception if a Windows/DOS 1566 line ending is found on a build compiled for a Unix platform. This is used by 1567 the TSV Utilities to detect Window/DOS line endings and terminate processing 1568 with an error message to the user. 1569 */ 1570 void throwIfWindowsNewlineOnUnix 1571 (NewlineWasRemoved nlWasRemoved = Yes.newlineWasRemoved) 1572 (const char[] line, const char[] filename, size_t lineNum) 1573 { 1574 version(Posix) 1575 { 1576 static if (nlWasRemoved) 1577 { 1578 bool hasWindowsLineEnding = line.length != 0 && line[$ - 1] == '\r'; 1579 } 1580 else 1581 { 1582 bool hasWindowsLineEnding = 1583 line.length > 1 && 1584 line[$ - 2] == '\r' && 1585 line[$ - 1] == '\n'; 1586 } 1587 1588 if (hasWindowsLineEnding) 1589 { 1590 import std.format; 1591 throw new Exception( 1592 format("Windows/DOS line ending found. Convert file to Unix newlines before processing (e.g. 'dos2unix').\n File: %s, Line: %s", 1593 (filename == "-") ? "Standard Input" : filename, lineNum)); 1594 } 1595 } 1596 } 1597 1598 unittest 1599 { 1600 /* Note: Currently only building on Posix. Need to add non-Posix test cases 1601 * if Windows builds are ever done. 1602 */ 1603 version(Posix) 1604 { 1605 import std.exception; 1606 1607 assertNotThrown(throwIfWindowsNewlineOnUnix("", "afile.tsv", 1)); 1608 assertNotThrown(throwIfWindowsNewlineOnUnix("a", "afile.tsv", 2)); 1609 assertNotThrown(throwIfWindowsNewlineOnUnix("ab", "afile.tsv", 3)); 1610 assertNotThrown(throwIfWindowsNewlineOnUnix("abc", "afile.tsv", 4)); 1611 1612 assertThrown(throwIfWindowsNewlineOnUnix("\r", "afile.tsv", 1)); 1613 assertThrown(throwIfWindowsNewlineOnUnix("a\r", "afile.tsv", 2)); 1614 assertThrown(throwIfWindowsNewlineOnUnix("ab\r", "afile.tsv", 3)); 1615 assertThrown(throwIfWindowsNewlineOnUnix("abc\r", "afile.tsv", 4)); 1616 1617 assertNotThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("\n", "afile.tsv", 1)); 1618 assertNotThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("a\n", "afile.tsv", 2)); 1619 assertNotThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("ab\n", "afile.tsv", 3)); 1620 assertNotThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("abc\n", "afile.tsv", 4)); 1621 1622 assertThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("\r\n", "afile.tsv", 5)); 1623 assertThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("a\r\n", "afile.tsv", 6)); 1624 assertThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("ab\r\n", "afile.tsv", 7)); 1625 assertThrown(throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("abc\r\n", "afile.tsv", 8)); 1626 1627 /* Standard Input formatting. */ 1628 import std.algorithm : endsWith; 1629 bool exceptionCaught = false; 1630 1631 try (throwIfWindowsNewlineOnUnix("\r", "-", 99)); 1632 catch (Exception e) 1633 { 1634 assert(e.msg.endsWith("File: Standard Input, Line: 99")); 1635 exceptionCaught = true; 1636 } 1637 finally 1638 { 1639 assert(exceptionCaught); 1640 exceptionCaught = false; 1641 } 1642 1643 try (throwIfWindowsNewlineOnUnix!(No.newlineWasRemoved)("\r\n", "-", 99)); 1644 catch (Exception e) 1645 { 1646 assert(e.msg.endsWith("File: Standard Input, Line: 99")); 1647 exceptionCaught = true; 1648 } 1649 finally 1650 { 1651 assert(exceptionCaught); 1652 exceptionCaught = false; 1653 } 1654 } 1655 }