1 /** 2 Command line tool that prints TSV data aligned for easier reading on consoles 3 and traditional command-line environments. 4 5 Copyright (c) 2017-2019, eBay Software Foundation 6 Initially written by Jon Degenhardt 7 8 License: Boost License 1.0 (http://boost.org/LICENSE_1_0.txt) 9 */ 10 module tsv_utils.tsv_pretty; 11 12 import std.range; 13 import std.stdio; 14 import std.typecons : Flag, Yes, No, tuple; 15 16 static if (__VERSION__ >= 2085) extern(C) __gshared string[] rt_options = [ "gcopt=cleanup:none" ]; 17 18 version(unittest) 19 { 20 // When running unit tests, use main from -main compiler switch. 21 } 22 else 23 { 24 /** Main program. Invokes command line arg processing and tsv-pretty to perform 25 * the real work. Any errors are caught and reported. 26 */ 27 int main(string[] cmdArgs) 28 { 29 /* When running in DMD code coverage mode, turn on report merging. */ 30 version(D_Coverage) version(DigitalMars) 31 { 32 import core.runtime : dmd_coverSetMerge; 33 dmd_coverSetMerge(true); 34 } 35 36 TsvPrettyOptions options; 37 auto r = options.processArgs(cmdArgs); 38 if (!r[0]) return r[1]; 39 try tsvPretty(options, cmdArgs[1 .. $]); 40 catch (Exception exc) 41 { 42 stderr.writefln("Error [%s]: %s", options.programName, exc.msg); 43 return 1; 44 } 45 return 0; 46 } 47 } 48 49 auto helpTextVerbose = q"EOS 50 Synopsis: tsv-pretty [options] [file...] 51 52 tsv-pretty outputs TSV data in a format intended to be more human readable when 53 working on the command line. This is done primarily by lining up data into 54 fixed-width columns. Text is left aligned, numbers are right aligned. Floating 55 points numbers are aligned on the decimal point when feasible. 56 57 Processing begins by reading the initial set of lines into memory to determine 58 the field widths and data types of each column. This look-ahead buffer is used 59 for header detection as well. Output begins after this processing is complete. 60 61 By default, only the alignment is changed, the actual values are not modified. 62 Several of the formatting options do modify the values. 63 64 Features: 65 66 * Floating point numbers: Floats can be printed in fixed-width precision, using 67 the same precision for all floats in a column. This makes then line up nicely. 68 Precision is determined by values seen during look-ahead processing. The max 69 precision defaults to 9, this can be changed when smaller or larger values are 70 desired. See the '--f|format-floats' and '--p|precision' options. 71 72 * Header lines: Headers are detected automatically when possible. This can be 73 overridden when automatic detection doesn't work as desired. Headers can be 74 underlined and repeated at regular intervals. 75 76 * Missing values: A substitute value can be used for empty fields. This is often 77 less confusing than spaces. See '--e|replace-empty' and '--E|empty-replacement'. 78 79 * Exponential notion: As part float formatting, '--f|format-floats' re-formats 80 columns where exponential notation is found so all the values in the column 81 are displayed using exponential notation with the same precision. 82 83 * Preamble: A number of initial lines can be designated as a preamble and output 84 unchanged. The preamble is before the header, if a header is present. 85 86 * Fonts: Fixed-width fonts are assumed. CJK characters are assumed to be double 87 width. This is not always correct, but works well in most cases. 88 89 Options: 90 EOS"; 91 92 auto helpText = q"EOS 93 Synopsis: tsv-pretty [options] [file...] 94 95 tsv-pretty outputs TSV data in a more human readable format. This is done by lining 96 up data into fixed-width columns. Text is left aligned, numbers are right aligned. 97 Floating points numbers are aligned on the decimal point when feasible. 98 99 Options: 100 EOS"; 101 102 /** TsvPrettyOptions is used to process and store command line options. */ 103 struct TsvPrettyOptions 104 { 105 string programName; 106 bool helpVerbose = false; // --help-verbose 107 bool hasHeader = false; // --H|header (Note: Default false assumed by validation code) 108 bool autoDetectHeader = true; // Derived (Note: Default true assumed by validation code) 109 bool noHeader = false; // --x|no-header (Note: Default false assumed by validation code) 110 size_t lookahead = 1000; // --l|lookahead 111 size_t repeatHeader = 0; // --r|repeat-header num (zero means no repeat) 112 bool underlineHeader = false; // --u|underline-header 113 bool formatFloats = false; // --f|format-floats 114 size_t floatPrecision = 9; // --p|precision num (max precision when formatting floats.) 115 bool replaceEmpty = false; // --e|replace-empty 116 string emptyReplacement = ""; // --E|empty-replacement 117 size_t emptyReplacementPrintWidth = 0; // Derived 118 char delim = '\t'; // --d|delimiter 119 size_t spaceBetweenFields = 2; // --s|space-between-fields num 120 size_t maxFieldPrintWidth = 40; // --m|max-text-width num; Max width for variable width text fields. 121 size_t preambleLines = 0; // --a|preamble; Number of preamble lines. 122 bool versionWanted = false; // --V|version 123 124 /* Returns a tuple. First value is true if command line arguments were successfully 125 * processed and execution should continue, or false if an error occurred or the user 126 * asked for help. If false, the second value is the appropriate exit code (0 or 1). 127 * 128 * Returning true (execution continues) means args have been validated and derived 129 * values calculated. In addition, field indices have been converted to zero-based. 130 * If the whole line is the key, the individual fields list will be cleared. 131 */ 132 auto processArgs (ref string[] cmdArgs) 133 { 134 import std.algorithm : any, each; 135 import std.getopt; 136 import std.path : baseName, stripExtension; 137 138 programName = (cmdArgs.length > 0) ? cmdArgs[0].stripExtension.baseName : "Unknown_program_name"; 139 140 try 141 { 142 arraySep = ","; // Use comma to separate values in command line options 143 auto r = getopt( 144 cmdArgs, 145 "help-verbose", " Print full help.", &helpVerbose, 146 std.getopt.config.caseSensitive, 147 "H|header", " Treat the first line of each file as a header.", &hasHeader, 148 std.getopt.config.caseInsensitive, 149 "x|no-header", " Assume no header. Turns off automatic header detection.", &noHeader, 150 "l|lookahead", "NUM Lines to read to interpret data before generating output. Default: 1000", &lookahead, 151 152 "r|repeat-header", "NUM Lines to print before repeating the header. Default: No repeating header", &repeatHeader, 153 154 "u|underline-header", " Underline the header.", &underlineHeader, 155 "f|format-floats", " Format floats for better readability. Default: No", &formatFloats, 156 "p|precision", "NUM Max floating point precision. Implies --format-floats. Default: 9", &floatPrecisionOptionHandler, 157 std.getopt.config.caseSensitive, 158 "e|replace-empty", " Replace empty fields with '--'.", &replaceEmpty, 159 "E|empty-replacement", "STR Replace empty fields with a string.", &emptyReplacement, 160 std.getopt.config.caseInsensitive, 161 "d|delimiter", "CHR Field delimiter. Default: TAB. (Single byte UTF-8 characters only.)", &delim, 162 "s|space-between-fields", "NUM Spaces between each field (Default: 2)", &spaceBetweenFields, 163 "m|max-text-width", "NUM Max reserved field width for variable width text fields. Default: 40", &maxFieldPrintWidth, 164 "a|preamble", "NUM Treat the first NUM lines as a preamble and output them unchanged.", &preambleLines, 165 std.getopt.config.caseSensitive, 166 "V|version", " Print version information and exit.", &versionWanted, 167 std.getopt.config.caseInsensitive, 168 ); 169 170 if (r.helpWanted) 171 { 172 defaultGetoptPrinter(helpText, r.options); 173 return tuple(false, 0); 174 } 175 else if (helpVerbose) 176 { 177 defaultGetoptPrinter(helpTextVerbose, r.options); 178 return tuple(false, 0); 179 } 180 else if (versionWanted) 181 { 182 import tsv_utils.common.tsvutils_version; 183 writeln(tsvutilsVersionNotice("tsv-pretty")); 184 return tuple(false, 0); 185 } 186 187 /* Validation and derivations. */ 188 if (noHeader && hasHeader) throw new Exception("Cannot specify both --H|header and --x|no-header."); 189 190 if (noHeader || hasHeader) autoDetectHeader = false; 191 192 /* Zero look-ahead has limited utility unless the first line is known to 193 * be a header. Good chance the user will get an unintended behavior. 194 */ 195 if (lookahead == 0 && autoDetectHeader) 196 { 197 assert (!noHeader && !hasHeader); 198 throw new Exception("Cannot auto-detect header with zero look-ahead. Specify either '--H|header' or '--x|no-header' when using '--l|lookahead 0'."); 199 } 200 201 if (emptyReplacement.length != 0) replaceEmpty = true; 202 else if (replaceEmpty) emptyReplacement = "--"; 203 204 if (emptyReplacement.length != 0) 205 { 206 emptyReplacementPrintWidth = emptyReplacement.monospacePrintWidth; 207 } 208 } 209 catch (Exception exc) 210 { 211 stderr.writefln("[%s] Error processing command line arguments: %s", programName, exc.msg); 212 return tuple(false, 1); 213 } 214 return tuple(true, 0); 215 } 216 217 /* Option handler for --p|precision. It also sets --f|format-floats. */ 218 private void floatPrecisionOptionHandler(string option, string optionVal) @safe pure 219 { 220 import std.conv : to; 221 floatPrecision = optionVal.to!size_t; 222 formatFloats = true; 223 } 224 } 225 226 /** tsvPretty is the main loop, operating on input files and passing control to a 227 * TSVPrettyProccessor instance. 228 * 229 * This separates physical I/O sources and sinks from the underlying processing 230 * algorithm, which operates on generic ranges. A lockingTextWriter is created and 231 * released on every input line. This has effect flushing standard output every line, 232 * desirable in command line tools. 233 */ 234 void tsvPretty(in ref TsvPrettyOptions options, string[] files) 235 { 236 auto firstNonPreambleLine = options.preambleLines + 1; 237 auto tpp = TsvPrettyProcessor(options); 238 foreach (filename; (files.length > 0) ? files : ["-"]) 239 { 240 auto inputStream = (filename == "-") ? stdin : filename.File(); 241 foreach (lineNum, line; inputStream.byLine.enumerate(1)) 242 { 243 if (lineNum < firstNonPreambleLine) 244 { 245 tpp.processPreambleLine(outputRangeObject!(char, char[])(stdout.lockingTextWriter), line); 246 } 247 else if (lineNum == firstNonPreambleLine) 248 { 249 tpp.processFileFirstLine(outputRangeObject!(char, char[])(stdout.lockingTextWriter), line); 250 } 251 else 252 { 253 tpp.processLine(outputRangeObject!(char, char[])(stdout.lockingTextWriter), line); 254 } 255 } 256 } 257 tpp.finish(outputRangeObject!(char, char[])(stdout.lockingTextWriter)); 258 } 259 260 /** TsvPrettyProcessor maintains state of processing and exposes operations for 261 * processing individual input lines. 262 * 263 * TsvPrettyProcessor knows that input is file-based, but doesn't deal with actual 264 * files or reading lines from input. That is the job of the caller. Output is 265 * written to an output range. The caller is expected to pass each line to in the 266 * order received, that is an assumption built-into the its processing. 267 * 268 * In addition to the constructor, there are four API methods: 269 * - processPreambleLine - Called to process a preamble line occurring before 270 * the header line or first line of data. 271 * - processFileFirstLine - Called to process the first line of each file. This 272 * enables header processing. 273 * - processLine - Called to process all lines except for the first line a file. 274 * - finish - Called at the end of all processing. This is needed in case the 275 * look-ahead cache is still being filled when input terminates. 276 */ 277 278 struct TsvPrettyProcessor 279 { 280 import std.array : appender; 281 282 private: 283 private enum AutoDetectHeaderResult { none, hasHeader, noHeader }; 284 285 private TsvPrettyOptions _options; 286 private size_t _fileCount = 0; 287 private size_t _dataLineOutputCount = 0; 288 private bool _stillCaching = true; 289 private string _candidateHeaderLine; 290 private auto _lookaheadCache = appender!(string[])(); 291 private FieldFormat[] _fieldVector; 292 private AutoDetectHeaderResult _autoDetectHeaderResult = AutoDetectHeaderResult.none; 293 294 /** Constructor. */ 295 this(const TsvPrettyOptions options) @safe pure nothrow @nogc 296 { 297 _options = options; 298 if (options.noHeader && options.lookahead == 0) _stillCaching = false; 299 } 300 301 invariant 302 { 303 assert(_options.hasHeader || _options.noHeader || _options.autoDetectHeader); 304 assert((_options.lookahead == 0 && _lookaheadCache.data.length == 0) || 305 _lookaheadCache.data.length < _options.lookahead); 306 } 307 308 /** Called to process a preamble line occurring before the header line or first 309 * line of data. 310 */ 311 void processPreambleLine(OutputRange!char outputStream, const char[] line) 312 { 313 if (_fileCount == 0) 314 { 315 put(outputStream, line); 316 put(outputStream, '\n'); 317 } 318 } 319 320 /** Called to process the first line of each file. This enables header processing. */ 321 void processFileFirstLine(OutputRange!char outputStream, const char[] line) 322 { 323 import std.conv : to; 324 325 _fileCount++; 326 327 if (_options.noHeader) 328 { 329 processLine(outputStream, line); 330 } 331 else if (_options.hasHeader) 332 { 333 if (_fileCount == 1) 334 { 335 setHeaderLine(line); 336 if (_options.lookahead == 0) outputLookaheadCache(outputStream); 337 } 338 } 339 else 340 { 341 assert(_options.autoDetectHeader); 342 343 final switch (_autoDetectHeaderResult) 344 { 345 case AutoDetectHeaderResult.noHeader: 346 assert(_fileCount > 1); 347 processLine(outputStream, line); 348 break; 349 350 case AutoDetectHeaderResult.hasHeader: 351 assert(_fileCount > 1); 352 break; 353 354 case AutoDetectHeaderResult.none: 355 if (_fileCount == 1) 356 { 357 assert(_candidateHeaderLine.length == 0); 358 _candidateHeaderLine = line.to!string; 359 } 360 else if (_fileCount == 2) 361 { 362 if (_candidateHeaderLine == line) 363 { 364 _autoDetectHeaderResult = AutoDetectHeaderResult.hasHeader; 365 setHeaderLine(_candidateHeaderLine); 366 367 /* Edge case: First file has only a header line and look-ahead set to zero. */ 368 if (_stillCaching && _options.lookahead == 0) outputLookaheadCache(outputStream); 369 } 370 else 371 { 372 _autoDetectHeaderResult = AutoDetectHeaderResult.noHeader; 373 updateFieldFormatsForLine(_candidateHeaderLine); 374 processLine(outputStream, line); 375 } 376 } 377 break; 378 } 379 } 380 } 381 382 /** Called to process all lines except for the first line a file. */ 383 void processLine(OutputRange!char outputStream, const char[] line) 384 { 385 if (_stillCaching) cacheDataLine(outputStream, line); 386 else outputDataLine(outputStream, line); 387 } 388 389 /** Called at the end of all processing. This is needed in case the look-ahead cache 390 * is still being filled when input terminates. 391 */ 392 void finish(OutputRange!char outputStream) 393 { 394 if (_stillCaching) outputLookaheadCache(outputStream); 395 } 396 397 private: 398 /* outputLookaheadCache finalizes processing of the lookahead cache. This includes 399 * Setting the type and width of each field, finalizing the auto-detect header 400 * decision, and outputing all lines in the cache. 401 */ 402 void outputLookaheadCache(OutputRange!char outputStream) 403 { 404 import std.algorithm : splitter; 405 406 assert(_stillCaching); 407 408 if (_options.autoDetectHeader && 409 _autoDetectHeaderResult == AutoDetectHeaderResult.none && 410 _candidateHeaderLine.length != 0) 411 { 412 if (candidateHeaderLooksLikeHeader()) 413 { 414 _autoDetectHeaderResult = AutoDetectHeaderResult.hasHeader; 415 setHeaderLine(_candidateHeaderLine); 416 } 417 else 418 { 419 _autoDetectHeaderResult = AutoDetectHeaderResult.noHeader; 420 } 421 } 422 423 424 if (_options.hasHeader || 425 (_options.autoDetectHeader && _autoDetectHeaderResult == AutoDetectHeaderResult.hasHeader)) 426 { 427 finalizeFieldFormatting(); 428 outputHeader(outputStream); 429 } 430 else if (_options.autoDetectHeader && _autoDetectHeaderResult == AutoDetectHeaderResult.noHeader && 431 _candidateHeaderLine.length != 0) 432 { 433 updateFieldFormatsForLine(_candidateHeaderLine); 434 finalizeFieldFormatting(); 435 outputDataLine(outputStream, _candidateHeaderLine); 436 } 437 else 438 { 439 finalizeFieldFormatting(); 440 } 441 442 foreach(line; _lookaheadCache.data) outputDataLine(outputStream, line); 443 _lookaheadCache.clear; 444 _stillCaching = false; 445 } 446 447 bool candidateHeaderLooksLikeHeader() @safe 448 { 449 import std.algorithm : splitter; 450 451 /* The candidate header is declared as the header if the look-ahead cache has at least 452 * one numeric field that is text in the candidate header. 453 */ 454 foreach(fieldIndex, fieldValue; _candidateHeaderLine.splitter(_options.delim).enumerate) 455 { 456 auto candidateFieldFormat = FieldFormat(fieldIndex); 457 candidateFieldFormat.updateForFieldValue(fieldValue, _options); 458 if (_fieldVector.length > fieldIndex && 459 candidateFieldFormat.fieldType == FieldType.text && 460 (_fieldVector[fieldIndex].fieldType == FieldType.integer || 461 _fieldVector[fieldIndex].fieldType == FieldType.floatingPoint || 462 _fieldVector[fieldIndex].fieldType == FieldType.exponent)) 463 { 464 return true; 465 } 466 } 467 468 return false; 469 } 470 471 void setHeaderLine(const char[] line) @safe 472 { 473 import std.algorithm : splitter; 474 475 foreach(fieldIndex, header; line.splitter(_options.delim).enumerate) 476 { 477 if (_fieldVector.length == fieldIndex) _fieldVector ~= FieldFormat(fieldIndex); 478 assert(_fieldVector.length > fieldIndex); 479 _fieldVector[fieldIndex].setHeader(header); 480 } 481 } 482 483 void cacheDataLine(OutputRange!char outputStream, const char[] line) 484 { 485 import std.conv : to; 486 487 assert(_lookaheadCache.data.length < _options.lookahead); 488 489 _lookaheadCache ~= line.to!string; 490 updateFieldFormatsForLine(line); 491 if (_lookaheadCache.data.length == _options.lookahead) outputLookaheadCache(outputStream); 492 } 493 494 void updateFieldFormatsForLine(const char[] line) @safe 495 { 496 import std.algorithm : splitter; 497 498 foreach(fieldIndex, fieldValue; line.splitter(_options.delim).enumerate) 499 { 500 if (_fieldVector.length == fieldIndex) _fieldVector ~= FieldFormat(fieldIndex); 501 assert(_fieldVector.length > fieldIndex); 502 _fieldVector[fieldIndex].updateForFieldValue(fieldValue, _options); 503 } 504 505 } 506 507 void finalizeFieldFormatting() @safe pure @nogc nothrow 508 { 509 size_t nextFieldStart = 0; 510 foreach(ref field; _fieldVector) 511 { 512 nextFieldStart = field.finalizeFormatting(nextFieldStart, _options) + _options.spaceBetweenFields; 513 } 514 } 515 516 void outputHeader(OutputRange!char outputStream) 517 { 518 size_t nextOutputPosition = 0; 519 foreach(fieldIndex, ref field; _fieldVector.enumerate) 520 { 521 size_t spacesNeeded = field.startPosition - nextOutputPosition; 522 put(outputStream, repeat(" ", spacesNeeded)); 523 nextOutputPosition += spacesNeeded; 524 nextOutputPosition += field.writeHeader(outputStream, _options); 525 } 526 put(outputStream, '\n'); 527 528 if (_options.underlineHeader) 529 { 530 nextOutputPosition = 0; 531 foreach(fieldIndex, ref field; _fieldVector.enumerate) 532 { 533 size_t spacesNeeded = field.startPosition - nextOutputPosition; 534 put(outputStream, repeat(" ", spacesNeeded)); 535 nextOutputPosition += spacesNeeded; 536 nextOutputPosition += field.writeHeader!(Yes.writeUnderline)(outputStream, _options); 537 } 538 put(outputStream, '\n'); 539 } 540 } 541 542 void outputDataLine(OutputRange!char outputStream, const char[] line) 543 { 544 import std.algorithm : splitter; 545 546 /* Repeating header option. */ 547 if (_options.repeatHeader != 0 && _dataLineOutputCount != 0 && 548 (_options.hasHeader || (_options.autoDetectHeader && 549 _autoDetectHeaderResult == AutoDetectHeaderResult.hasHeader)) && 550 _dataLineOutputCount % _options.repeatHeader == 0) 551 { 552 put(outputStream, '\n'); 553 outputHeader(outputStream); 554 } 555 556 _dataLineOutputCount++; 557 558 size_t nextOutputPosition = 0; 559 foreach(fieldIndex, fieldValue; line.splitter(_options.delim).enumerate) 560 { 561 if (fieldIndex == _fieldVector.length) 562 { 563 /* Line is longer than any seen while caching. Add a new FieldFormat entry 564 * and set the line formatting based on this field value. 565 */ 566 _fieldVector ~= FieldFormat(fieldIndex); 567 size_t startPosition = (fieldIndex == 0) ? 568 0 : 569 _fieldVector[fieldIndex - 1].endPosition + _options.spaceBetweenFields; 570 571 _fieldVector[fieldIndex].updateForFieldValue(fieldValue, _options); 572 _fieldVector[fieldIndex].finalizeFormatting(startPosition, _options); 573 } 574 575 assert(fieldIndex < _fieldVector.length); 576 577 FieldFormat fieldFormat = _fieldVector[fieldIndex]; 578 size_t nextFieldStart = fieldFormat.startPosition; 579 size_t spacesNeeded = (nextOutputPosition < nextFieldStart) ? 580 nextFieldStart - nextOutputPosition : 581 (fieldIndex == 0) ? 0 : 1; // Previous field went long. One space between fields 582 583 put(outputStream, repeat(" ", spacesNeeded)); 584 nextOutputPosition += spacesNeeded; 585 nextOutputPosition += fieldFormat.writeFieldValue(outputStream, nextOutputPosition, fieldValue, _options); 586 } 587 put(outputStream, '\n'); 588 } 589 } 590 591 /** Field types recognized and tracked by tsv-pretty processing. */ 592 enum FieldType { unknown, text, integer, floatingPoint, exponent }; 593 594 /** Field alignments used by tsv-pretty processing. */ 595 enum FieldAlignment { left, right }; 596 597 /** FieldFormat holds all the formatting info needed to format data values in a specific 598 * column. e.g. Field 1 may be text, field 2 may be a float, etc. This is calculated 599 * during the caching phase. Each FieldFormat instance is part of a vector representing 600 * the full row, so each includes the start position on the line and similar data. 601 * 602 * APIs used during the caching phase to gather field value samples 603 * - this - Initial construction. Takes the field index. 604 * - setHeader - Used to set the header text. 605 * - updateForFieldValue - Used to add the next field value sample. 606 * - finalizeFormatting - Used at the end of caching to finalize the format choices. 607 * 608 * APIs used after caching is finished (after finalizeFormatting): 609 * - startPosition - Returns the expected start position for the field. 610 * - endPosition - Returns the expected end position for the field. 611 * - writeHeader - Outputs the header, properly aligned. 612 * - writeFieldValue - Outputs the current field value, properly aligned. 613 */ 614 615 struct FieldFormat 616 { 617 private: 618 size_t _fieldIndex; // Zero-based index in the line 619 string _header = ""; // Original field header 620 size_t _headerPrintWidth = 0; 621 FieldType _type = FieldType.unknown; 622 FieldAlignment _alignment = FieldAlignment.left; 623 size_t _startPosition = 0; 624 size_t _printWidth = 0; 625 size_t _precision = 0; // Number of digits after the decimal point 626 627 /* These are used while doing initial type and print format detection. */ 628 size_t _minRawPrintWidth = 0; 629 size_t _maxRawPrintWidth = 0; 630 size_t _maxDigitsBeforeDecimal = 0; 631 size_t _maxDigitsAfterDecimal = 0; 632 size_t _maxSignificantDigits = 0; // Digits to include in exponential notation 633 634 public: 635 636 /** Initial construction. Takes a field index. */ 637 this(size_t fieldIndex) @safe pure nothrow @nogc 638 { 639 _fieldIndex = fieldIndex; 640 } 641 642 /** Sets the header text. */ 643 void setHeader(const char[] header) @safe 644 { 645 import std.conv : to; 646 647 _header = header.to!string; 648 _headerPrintWidth = _header.monospacePrintWidth; 649 } 650 651 /** Returns the expected start position for the field. */ 652 size_t startPosition() nothrow pure @safe @property 653 { 654 return _startPosition; 655 } 656 657 /** Returns the expected end position for the field. */ 658 size_t endPosition() nothrow pure @safe @property 659 { 660 return _startPosition + _printWidth; 661 } 662 663 /** Returns the type of field. */ 664 FieldType fieldType() nothrow pure @safe @property 665 { 666 return _type; 667 } 668 669 /** Writes the field header or underline characters to the output stream. 670 * 671 * The current output position should have been written up to the field's start position, 672 * including any spaces between fields. Unlike data fields, there is no need to correct 673 * for previous fields that have run long. This routine does not output trailing spaces. 674 * This makes it simpler for lines to avoid unnecessary trailing spaces. 675 * 676 * Underlines can either be written the full width of the field or the just under the 677 * text of the header. At present this is a template parameter (compile-time). 678 * 679 * The print width of the output is returned. 680 */ 681 size_t writeHeader (Flag!"writeUnderline" writeUnderline = No.writeUnderline, 682 Flag!"fullWidthUnderline" fullWidthUnderline = No.fullWidthUnderline) 683 (OutputRange!char outputStream, in ref TsvPrettyOptions options) 684 { 685 import std.range : repeat; 686 687 size_t positionsWritten = 0; 688 if (_headerPrintWidth > 0) 689 { 690 static if (writeUnderline) 691 { 692 static if (fullWidthUnderline) 693 { 694 put(outputStream, repeat("-", _printWidth)); 695 positionsWritten += _printWidth; 696 } 697 else // Underline beneath the header text only 698 { 699 if (_alignment == FieldAlignment.right) 700 { 701 put(outputStream, repeat(" ", _printWidth - _headerPrintWidth)); 702 positionsWritten += _printWidth - _headerPrintWidth; 703 } 704 put(outputStream, repeat("-", _headerPrintWidth)); 705 positionsWritten += _headerPrintWidth; 706 } 707 } 708 else 709 { 710 if (_alignment == FieldAlignment.right) 711 { 712 put(outputStream, repeat(" ", _printWidth - _headerPrintWidth)); 713 positionsWritten += _printWidth - _headerPrintWidth; 714 } 715 put(outputStream, _header); 716 positionsWritten += _headerPrintWidth; 717 } 718 } 719 return positionsWritten; 720 } 721 722 /** Writes the field value for the current column. 723 * 724 * The caller needs to generate output at least to the column's start position, but 725 * can go beyond if previous fields have run long. 726 * 727 * The field value is aligned properly in the field. Either left aligned (text) or 728 * right aligned (numeric). Floating point fields are both right aligned and 729 * decimal point aligned. The number of bytes written is returned. Trailing spaces 730 * are not added, the caller must add any necessary trailing spaces prior to 731 * printing the next field. 732 */ 733 size_t writeFieldValue(OutputRange!char outputStream, size_t currPosition, 734 const char[] fieldValue, in ref TsvPrettyOptions options) 735 in 736 { 737 assert(currPosition >= _startPosition); // Caller resposible for advancing to field start position. 738 assert(_type == FieldType.text || _type == FieldType.integer || 739 _type == FieldType.floatingPoint || _type == FieldType.exponent); 740 } 741 body 742 { 743 import std.algorithm : find, max, min; 744 import std.conv : to, ConvException; 745 import std.format : format; 746 747 /* Create the print version of the string. Either the raw value or a formatted 748 * version of a float. 749 */ 750 string printValue; 751 if (!options.formatFloats || _type == FieldType.text || _type == FieldType.integer) 752 { 753 printValue = fieldValue.to!string; 754 } 755 else 756 { 757 assert(options.formatFloats); 758 assert(_type == FieldType.exponent || _type == FieldType.floatingPoint); 759 760 if (_type == FieldType.exponent) 761 { 762 printValue = fieldValue.formatExponentValue(_precision); 763 } 764 else 765 { 766 printValue = fieldValue.formatFloatingPointValue(_precision); 767 } 768 } 769 770 if (printValue.length == 0 && options.replaceEmpty) printValue = options.emptyReplacement; 771 size_t printValuePrintWidth = printValue.monospacePrintWidth; 772 773 /* Calculate leading spaces needed for right alignment. */ 774 size_t leadingSpaces = 0; 775 if (_alignment == FieldAlignment.right) 776 { 777 /* Target width adjusts the column width to account for overrun by the previous field. */ 778 size_t targetWidth; 779 if (currPosition == _startPosition) 780 { 781 targetWidth = _printWidth; 782 } 783 else 784 { 785 size_t startGap = currPosition - _startPosition; 786 targetWidth = max(printValuePrintWidth, 787 startGap < _printWidth ? _printWidth - startGap : 0); 788 } 789 790 leadingSpaces = (printValuePrintWidth < targetWidth) ? 791 targetWidth - printValuePrintWidth : 0; 792 793 /* The above calculation assumes the print value is fully right aligned. 794 * This is not correct when raw value floats are being used rather than 795 * formatted floats, as different values will have different precision. 796 * The next adjustment accounts for this, dropping leading spaces as 797 * needed to align the decimal point. Note that text and exponential 798 * values get aligned strictly against right boundaries. 799 */ 800 if (leadingSpaces > 0 && _precision > 0 && 801 _type == FieldType.floatingPoint && !options.formatFloats) 802 { 803 import std.algorithm : canFind, findSplit; 804 import std..string : isNumeric; 805 806 if (printValue.isNumeric && !printValue.canFind!(x => x == 'e' || x == 'E')) 807 { 808 size_t decimalAndDigitsLength = printValue.find(".").length; 809 size_t trailingSpaces = 810 (decimalAndDigitsLength == 0) ? _precision + 1 : 811 (decimalAndDigitsLength > _precision) ? 0 : 812 _precision + 1 - decimalAndDigitsLength; 813 814 leadingSpaces = (leadingSpaces > trailingSpaces) ? 815 leadingSpaces - trailingSpaces : 0; 816 } 817 } 818 } 819 put(outputStream, repeat(' ', leadingSpaces)); 820 put(outputStream, printValue); 821 return printValuePrintWidth + leadingSpaces; 822 } 823 824 /** Updates type and format given a new field value. 825 * 826 * This is called during look-ahead caching to register a new sample value for the 827 * column. The key components updates are field type and print width. 828 */ 829 void updateForFieldValue(const char[] fieldValue, in ref TsvPrettyOptions options) @safe 830 { 831 import std.algorithm : findAmong, findSplit, max, min; 832 import std.conv : to, ConvException; 833 import std..string : isNumeric; 834 835 size_t fieldValuePrintWidth = fieldValue.monospacePrintWidth; 836 size_t fieldValuePrintWidthWithEmpty = 837 (fieldValuePrintWidth == 0 && options.replaceEmpty) ? 838 options.emptyReplacementPrintWidth : 839 fieldValuePrintWidth; 840 841 _maxRawPrintWidth = max(_maxRawPrintWidth, fieldValuePrintWidthWithEmpty); 842 _minRawPrintWidth = (_minRawPrintWidth == 0) ? 843 fieldValuePrintWidthWithEmpty : 844 min(_minRawPrintWidth, fieldValuePrintWidthWithEmpty); 845 846 if (_type == FieldType.text) 847 { 848 /* Already text, can't become anything else. */ 849 } 850 else if (fieldValuePrintWidth == 0) 851 { 852 /* Don't let an empty field override a numeric field type. */ 853 } 854 else if (!fieldValue.isNumeric) 855 { 856 /* Not parsable as a number. Switch from unknown or numeric type to text. */ 857 _type = FieldType.text; 858 } 859 else 860 { 861 /* Field type is currently unknown or numeric, and current field parses as numeric. 862 * See if it parses as integer or float. Integers will parse as floats, so try 863 * integer types first. 864 */ 865 FieldType parsesAs = FieldType.unknown; 866 long longValue; 867 ulong ulongValue; 868 double doubleValue; 869 try 870 { 871 longValue = fieldValue.to!long; 872 parsesAs = FieldType.integer; 873 } 874 catch (ConvException) 875 { 876 try 877 { 878 ulongValue = fieldValue.to!ulong; 879 parsesAs = FieldType.integer; 880 } 881 catch (ConvException) 882 { 883 try 884 { 885 doubleValue = fieldValue.to!double; 886 import std.algorithm : findAmong; 887 parsesAs = (fieldValue.findAmong("eE").length == 0) ? 888 FieldType.floatingPoint : FieldType.exponent; 889 } 890 catch (ConvException) 891 { 892 /* Note: This means isNumeric thinks it's a number, but conversions all failed. */ 893 parsesAs = FieldType.text; 894 } 895 } 896 } 897 898 if (parsesAs == FieldType.text) 899 { 900 /* Not parsable as a number (despite isNumeric result). Switch to text type. */ 901 _type = FieldType.text; 902 } 903 else if (parsesAs == FieldType.exponent) 904 { 905 /* Exponential notion supersedes both vanilla floats and integers. */ 906 _type = FieldType.exponent; 907 _maxSignificantDigits = max(_maxSignificantDigits, fieldValue.significantDigits); 908 909 if (auto decimalSplit = fieldValue.findSplit(".")) 910 { 911 auto fromExponent = decimalSplit[2].findAmong("eE"); 912 size_t numDigitsAfterDecimal = decimalSplit[2].length - fromExponent.length; 913 _maxDigitsBeforeDecimal = max(_maxDigitsBeforeDecimal, decimalSplit[0].length); 914 _maxDigitsAfterDecimal = max(_maxDigitsAfterDecimal, numDigitsAfterDecimal); 915 } 916 else 917 { 918 /* Exponent without a decimal point. */ 919 auto fromExponent = fieldValue.findAmong("eE"); 920 assert(fromExponent.length > 0); 921 size_t numDigits = fieldValue.length - fromExponent.length; 922 _maxDigitsBeforeDecimal = max(_maxDigitsBeforeDecimal, numDigits); 923 } 924 } 925 else if (parsesAs == FieldType.floatingPoint) 926 { 927 /* Floating point supercedes integer but not exponential. */ 928 if (_type != FieldType.exponent) _type = FieldType.floatingPoint; 929 _maxSignificantDigits = max(_maxSignificantDigits, fieldValue.significantDigits); 930 931 if (auto decimalSplit = fieldValue.findSplit(".")) 932 { 933 _maxDigitsBeforeDecimal = max(_maxDigitsBeforeDecimal, decimalSplit[0].length); 934 _maxDigitsAfterDecimal = max(_maxDigitsAfterDecimal, decimalSplit[2].length); 935 } 936 } 937 else 938 { 939 assert(parsesAs == FieldType.integer); 940 if (_type != FieldType.floatingPoint) _type = FieldType.integer; 941 _maxSignificantDigits = max(_maxSignificantDigits, fieldValue.significantDigits); 942 _maxDigitsBeforeDecimal = max(_maxDigitsBeforeDecimal, fieldValue.length); 943 } 944 } 945 } 946 947 /** Updates field formatting info based on the current state. It is expected to be 948 * called after adding field entries via updateForFieldValue(). It returns its new 949 * end position. 950 */ 951 size_t finalizeFormatting (size_t startPosition, in ref TsvPrettyOptions options) @safe pure @nogc nothrow 952 { 953 import std.algorithm : max, min; 954 _startPosition = startPosition; 955 if (_type == FieldType.unknown) _type = FieldType.text; 956 _alignment = (_type == FieldType.integer || _type == FieldType.floatingPoint 957 || _type == FieldType.exponent) ? 958 FieldAlignment.right : 959 FieldAlignment.left; 960 961 if (_type == FieldType.floatingPoint) 962 { 963 size_t precision = min(options.floatPrecision, _maxDigitsAfterDecimal); 964 size_t maxValueWidth = _maxDigitsBeforeDecimal + precision; 965 if (precision > 0) maxValueWidth++; // Account for the decimal point. 966 _printWidth = max(1, _headerPrintWidth, maxValueWidth); 967 _precision = precision; 968 } 969 else if (_type == FieldType.exponent) 970 { 971 size_t maxPrecision = (_maxSignificantDigits > 0) ? _maxSignificantDigits - 1 : 0; 972 _precision = min(options.floatPrecision, maxPrecision); 973 974 size_t maxValuePrintWidth = !options.formatFloats ? _maxRawPrintWidth : _precision + 7; 975 _printWidth = max(1, _headerPrintWidth, maxValuePrintWidth); 976 } 977 else if (_type == FieldType.integer) 978 { 979 _printWidth = max(1, _headerPrintWidth, _minRawPrintWidth, _maxRawPrintWidth); 980 _precision = 0; 981 } 982 else 983 { 984 _printWidth = max(1, _headerPrintWidth, _minRawPrintWidth, 985 min(options.maxFieldPrintWidth, _maxRawPrintWidth)); 986 _precision = 0; 987 } 988 989 return _startPosition + _printWidth; 990 } 991 } 992 993 /** formatFloatingPointValue returns the printed representation of a raw value 994 * formatted as a fixed precision floating number. This includes zero padding or 995 * truncation of trailing digits as necessary to meet the desired precision. 996 * 997 * If the value cannot be interpreted as a double then the raw value is returned. 998 * Similarly, values in exponential notion are returned without reformatting. 999 * 1000 * This routine is used to format values in columns identified as floating point. 1001 */ 1002 string formatFloatingPointValue(const char[] value, size_t precision) @safe 1003 { 1004 import std.algorithm : canFind, find; 1005 import std.array : join; 1006 import std.conv : to, ConvException; 1007 import std.format : format; 1008 import std.math : isFinite; 1009 import std.range : repeat; 1010 1011 string printValue; 1012 1013 if (value.canFind!(x => x == 'e' || x == 'E')) 1014 { 1015 /* Exponential notion. Use the raw value. */ 1016 printValue = value.to!string; 1017 } 1018 else 1019 { 1020 try 1021 { 1022 double doubleValue = value.to!double; 1023 if (doubleValue.isFinite) 1024 { 1025 size_t numPrecisionDigits = value.precisionDigits; 1026 if (numPrecisionDigits >= precision) 1027 { 1028 printValue = format("%.*f", precision, doubleValue); 1029 } 1030 else if (numPrecisionDigits == 0) 1031 { 1032 printValue = format("%.*f", numPrecisionDigits, doubleValue) ~ "." ~ repeat("0", precision).join; 1033 } 1034 else 1035 { 1036 printValue = format("%.*f", numPrecisionDigits, doubleValue) ~ repeat("0", precision - numPrecisionDigits).join; 1037 } 1038 } 1039 else printValue = value.to!string; // NaN or Infinity 1040 } 1041 catch (ConvException) printValue = value.to!string; 1042 } 1043 return printValue; 1044 } 1045 1046 @safe unittest 1047 { 1048 assert("".formatFloatingPointValue(3) == ""); 1049 assert(" ".formatFloatingPointValue(3) == " "); 1050 assert("abc".formatFloatingPointValue(3) == "abc"); 1051 assert("nan".formatFloatingPointValue(3) == "nan"); 1052 assert("0".formatFloatingPointValue(0) == "0"); 1053 assert("1".formatFloatingPointValue(0) == "1"); 1054 assert("1.".formatFloatingPointValue(0) == "1"); 1055 assert("1".formatFloatingPointValue(3) == "1.000"); 1056 assert("1000".formatFloatingPointValue(3) == "1000.000"); 1057 assert("1000.001".formatFloatingPointValue(5) == "1000.00100"); 1058 assert("1000.001".formatFloatingPointValue(3) == "1000.001"); 1059 assert("1000.001".formatFloatingPointValue(2) == "1000.00"); 1060 assert("1000.006".formatFloatingPointValue(2) == "1000.01"); 1061 assert("-0.1".formatFloatingPointValue(1) == "-0.1"); 1062 assert("-0.1".formatFloatingPointValue(3) == "-0.100"); 1063 assert("-0.001".formatFloatingPointValue(3) == "-0.001"); 1064 assert("-0.006".formatFloatingPointValue(2) == "-0.01"); 1065 assert("-0.001".formatFloatingPointValue(1) == "-0.0"); 1066 assert("-0.001".formatFloatingPointValue(0) == "-0"); 1067 assert("0e+00".formatFloatingPointValue(0) == "0e+00"); 1068 assert("0.00e+00".formatFloatingPointValue(0) == "0.00e+00"); 1069 assert("1e+06".formatFloatingPointValue(1) == "1e+06"); 1070 assert("1e+06".formatFloatingPointValue(2) == "1e+06"); 1071 assert("1E-06".formatFloatingPointValue(1) == "1E-06"); 1072 assert("1.1E+6".formatFloatingPointValue(2) == "1.1E+6"); 1073 assert("1.1E+100".formatFloatingPointValue(2) == "1.1E+100"); 1074 } 1075 1076 /** formatExponentValue returns the printed representation of a raw value formatted 1077 * using exponential notation and a specific precision. If the value cannot be interpreted 1078 * as a double then the a copy of the original value is returned. 1079 * 1080 * This routine is used to format values in columns identified as having exponent format. 1081 */ 1082 string formatExponentValue(const char[] value, size_t precision) @safe 1083 { 1084 import std.algorithm : canFind, find, findSplit; 1085 import std.array : join; 1086 import std.conv : to, ConvException; 1087 import std.format : format; 1088 import std.math : isFinite; 1089 import std.range : repeat; 1090 1091 string printValue; 1092 try 1093 { 1094 double doubleValue = value.to!double; 1095 if (doubleValue.isFinite) 1096 { 1097 size_t numSignificantDigits = value.significantDigits; 1098 size_t numPrecisionDigits = (numSignificantDigits == 0) ? 0 : numSignificantDigits - 1; 1099 if (numPrecisionDigits >= precision) 1100 { 1101 printValue = format("%.*e", precision, doubleValue); 1102 } 1103 else 1104 { 1105 string unpaddedPrintValue = format("%.*e", numPrecisionDigits, doubleValue); 1106 auto exponentSplit = unpaddedPrintValue.findSplit("e"); // Uses the same exponent case as format call. 1107 if (numPrecisionDigits == 0) 1108 { 1109 assert(precision != 0); 1110 assert(!exponentSplit[0].canFind(".")); 1111 printValue = exponentSplit[0] ~ "." ~ repeat("0", precision).join ~ exponentSplit[1] ~ exponentSplit[2]; 1112 } 1113 else 1114 { 1115 printValue = exponentSplit[0] ~ repeat("0", precision - numPrecisionDigits).join ~ exponentSplit[1] ~ exponentSplit[2]; 1116 } 1117 } 1118 } 1119 else printValue = value.to!string; // NaN or Infinity 1120 } 1121 catch (ConvException) printValue = value.to!string; 1122 1123 return printValue; 1124 } 1125 1126 @safe unittest 1127 { 1128 assert("".formatExponentValue(3) == ""); 1129 assert(" ".formatExponentValue(3) == " "); 1130 assert("abc".formatExponentValue(3) == "abc"); 1131 assert("nan".formatExponentValue(3) == "nan"); 1132 assert("0".formatExponentValue(0) == "0e+00"); 1133 assert("1".formatExponentValue(0) == "1e+00"); 1134 assert("1.".formatExponentValue(0) == "1e+00"); 1135 assert("1".formatExponentValue(3) == "1.000e+00"); 1136 assert("1000".formatExponentValue(3) == "1.000e+03"); 1137 assert("1000.001".formatExponentValue(5) == "1.00000e+03"); 1138 assert("1000.001".formatExponentValue(3) == "1.000e+03"); 1139 assert("1000.001".formatExponentValue(6) == "1.000001e+03"); 1140 assert("1000.006".formatExponentValue(5) == "1.00001e+03"); 1141 assert("-0.1".formatExponentValue(1) == "-1.0e-01"); 1142 assert("-0.1".formatExponentValue(3) == "-1.000e-01"); 1143 assert("-0.001".formatExponentValue(3) == "-1.000e-03"); 1144 assert("-0.001".formatExponentValue(1) == "-1.0e-03"); 1145 assert("-0.001".formatExponentValue(0) == "-1e-03"); 1146 assert("0e+00".formatExponentValue(0) == "0e+00"); 1147 assert("0.00e+00".formatExponentValue(0) == "0e+00"); 1148 assert("1e+06".formatExponentValue(1) == "1.0e+06"); 1149 assert("1e+06".formatExponentValue(2) == "1.00e+06"); 1150 assert("1.0001e+06".formatExponentValue(1) == "1.0e+06"); 1151 assert("1.0001e+06".formatExponentValue(5) == "1.00010e+06"); 1152 } 1153 1154 /** Returns the number of significant digits in a numeric string. 1155 * 1156 * Significant digits are those needed to represent a number in exponential notation. 1157 * Examples: 1158 * 22.345 - 5 digits 1159 * 10.010 - 4 digits 1160 * 0.0032 - 2 digits 1161 */ 1162 size_t significantDigits(const char[] numericString) @safe pure 1163 { 1164 import std.algorithm : canFind, find, findAmong, findSplit, stripRight; 1165 import std.ascii : isDigit; 1166 import std.math : isFinite; 1167 import std..string : isNumeric; 1168 import std.conv : to; 1169 1170 assert (numericString.isNumeric); 1171 1172 size_t significantDigits = 0; 1173 if (numericString.to!double.isFinite) 1174 { 1175 auto digitsPart = numericString.find!(x => x.isDigit && x != '0'); 1176 auto exponentPart = digitsPart.findAmong("eE"); 1177 digitsPart = digitsPart[0 .. $ - exponentPart.length]; 1178 1179 if (digitsPart.canFind('.')) 1180 { 1181 digitsPart = digitsPart.stripRight('0'); 1182 significantDigits = digitsPart.length - 1; 1183 } 1184 else 1185 { 1186 significantDigits = digitsPart.length; 1187 } 1188 1189 if (significantDigits == 0) significantDigits = 1; 1190 } 1191 1192 return significantDigits; 1193 } 1194 1195 @safe pure unittest 1196 { 1197 assert("0".significantDigits == 1); 1198 assert("10".significantDigits == 2); 1199 assert("0.0".significantDigits == 1); 1200 assert("-10.0".significantDigits == 2); 1201 assert("-.01".significantDigits == 1); 1202 assert("-.5401".significantDigits == 4); 1203 assert("1010.010".significantDigits == 6); 1204 assert("0.0003003".significantDigits == 4); 1205 assert("6e+06".significantDigits == 1); 1206 assert("6.0e+06".significantDigits == 1); 1207 assert("6.5e+06".significantDigits == 2); 1208 assert("6.005e+06".significantDigits == 4); 1209 } 1210 1211 /** Returns the number of digits to the right of the decimal point in a numeric string. 1212 * This routine includes trailing zeros in the count. 1213 */ 1214 size_t precisionDigits(const char[] numericString) @safe pure 1215 { 1216 import std.algorithm : canFind, find, findAmong, findSplit, stripRight; 1217 import std.ascii : isDigit; 1218 import std.math : isFinite; 1219 import std..string : isNumeric; 1220 import std.conv : to; 1221 1222 assert (numericString.isNumeric); 1223 1224 size_t precisionDigits = 0; 1225 if (numericString.to!double.isFinite) 1226 { 1227 if (auto decimalSplit = numericString.findSplit(".")) 1228 { 1229 auto exponentPart = decimalSplit[2].findAmong("eE"); 1230 precisionDigits = decimalSplit[2].length - exponentPart.length; 1231 } 1232 } 1233 1234 return precisionDigits; 1235 } 1236 1237 @safe pure unittest 1238 { 1239 assert("0".precisionDigits == 0); 1240 assert("10".precisionDigits == 0); 1241 assert("0.0".precisionDigits == 1); 1242 assert("-10.0".precisionDigits == 1); 1243 assert("-.01".precisionDigits == 2); 1244 assert("-.5401".precisionDigits == 4); 1245 } 1246 1247 /** Calculates the expected print width of a string in monospace (fixed-width) fonts. 1248 */ 1249 size_t monospacePrintWidth(const char[] str) @safe nothrow 1250 { 1251 bool isCJK(dchar c) 1252 { 1253 return c >= '\u3000' && c <= '\u9fff'; 1254 } 1255 1256 import std.uni : byGrapheme; 1257 1258 size_t width = 0; 1259 try foreach (g; str.byGrapheme) width += isCJK(g[0]) ? 2 : 1; 1260 catch (Exception) width = str.length; // Invalid utf-8 sequence. Catch avoids program failure. 1261 1262 return width; 1263 } 1264 1265 unittest 1266 { 1267 assert("".monospacePrintWidth == 0); 1268 assert(" ".monospacePrintWidth == 1); 1269 assert("abc".monospacePrintWidth == 3); 1270 assert("林檎".monospacePrintWidth == 4); 1271 assert("æble".monospacePrintWidth == 4); 1272 assert("ვაშლი".monospacePrintWidth == 5); 1273 assert("größten".monospacePrintWidth == 7); 1274 }