1 /******************************************************************************* 2 3 copyright: Copyright (c) 2004 Kris Bell. All rights reserved 4 5 license: BSD style: $(LICENSE) 6 7 version: Initial release: Oct 2004 8 9 authors: Kris 10 11 Fast Unicode transcoders. These are particularly sensitive to 12 minor changes on 32bit x86 devices, because the register set of 13 those devices is so small. Beware of subtle changes which might 14 extend the execution-period by as much as 200%. Because of this, 15 three of the six transcoders might read past the end of input by 16 one, two, or three bytes before arresting themselves. Note that 17 support for streaming adds a 15% overhead to the dchar => char 18 conversion, but has little effect on the others. 19 20 These routines were tuned on an Intel P4; other devices may work 21 more efficiently with a slightly different approach, though this 22 is likely to be reasonably optimal on AMD x86 CPUs also. These 23 algorithms would benefit significantly from those extra AMD64 24 registers. On a 3GHz P4, the dchar/char conversions take around 25 2500ns to process an array of 1000 ASCII elements. Invoking the 26 memory manager doubles that period, and quadruples the time for 27 arrays of 100 elements. Memory allocation can slow down notably 28 in a multi-threaded environment, so avoid that where possible. 29 30 Surrogate-pairs are dealt with in a non-optimal fashion when 31 transcoding between utf16 and utf8. Such cases are considered 32 to be boundary-conditions for this module. 33 34 There are three common cases where the input may be incomplete, 35 including each 'widening' case of utf8 => utf16, utf8 => utf32, 36 and utf16 => utf32. An edge-case is utf16 => utf8, if surrogate 37 pairs are present. Such cases will throw an exception, unless 38 streaming-mode is enabled ~ in the latter mode, an additional 39 integer is returned indicating how many elements of the input 40 have been consumed. In all cases, a correct slice of the output 41 is returned. 42 43 For details on Unicode processing see: 44 $(UL $(LINK http://www.utf-8.com/)) 45 $(UL $(LINK http://www.hackcraft.net/xmlUnicode/)) 46 $(UL $(LINK http://www.azillionmonkeys.com/qed/unicode.html/)) 47 $(UL $(LINK http://icu.sourceforge.net/docs/papers/forms_of_unicode/)) 48 49 *******************************************************************************/ 50 51 module tango.text.convert.Utf; 52 53 public extern (C) void onUnicodeError (const(char[]) msg, size_t idx = 0); 54 55 /******************************************************************************* 56 57 Symmetric calls for equivalent types; these return the provided 58 input with no conversion 59 60 *******************************************************************************/ 61 62 inout(char[]) toString (inout(char[]) src, char[] dst = null, size_t* ate=null) {return src;} 63 inout(wchar[]) toString16 (inout(wchar[]) src, wchar[] dst = null, size_t* ate=null) {return src;} 64 inout(dchar[]) toString32 (inout(dchar[]) src, dchar[] dst = null, size_t* ate=null) {return src;} 65 66 /******************************************************************************* 67 68 Encode Utf8 up to a maximum of 4 bytes long (five & six byte 69 variations are not supported). 70 71 If the output is provided off the stack, it should be large 72 enough to encompass the entire transcoding; failing to do 73 so will cause the output to be moved onto the heap instead. 74 75 Returns a slice of the output buffer, corresponding to the 76 converted characters. For optimum performance, the returned 77 buffer should be specified as 'output' on subsequent calls. 78 For example: 79 80 --- 81 char[] output; 82 83 char[] result = toString (input, output); 84 85 // reset output after a realloc 86 if (result.length > output.length) 87 output = result; 88 --- 89 90 Where 'ate' is provided, it will be set to the number of 91 elements consumed from the input, and the output buffer 92 will not be resized (or allocated). This represents a 93 streaming mode, where slices of the input are processed 94 in sequence rather than all at one time (should use 'ate' 95 as an index for slicing into unconsumed input). 96 97 *******************************************************************************/ 98 99 char[] toString (const(wchar[]) input, char[] output=null, size_t* ate=null) 100 { 101 if (ate) 102 *ate = input.length; 103 else 104 { 105 // potentially reallocate output 106 auto estimate = input.length * 2 + 3; 107 if (output.length < estimate) 108 output.length = estimate; 109 } 110 111 char* pOut = output.ptr; 112 char* pMax = pOut + output.length - 3; 113 114 foreach (eaten, wchar b; input) 115 { 116 // about to overflow the output? 117 if (pOut > pMax) 118 { 119 // if streaming, just return the unused input 120 if (ate) 121 { 122 *ate = eaten; 123 break; 124 } 125 126 // reallocate the output buffer 127 auto len = pOut - output.ptr; 128 output.length = len + len / 2; 129 pOut = output.ptr + len; 130 pMax = output.ptr + output.length - 3; 131 } 132 133 if (b < 0x80) 134 *pOut++ = cast(char)b; 135 else 136 if (b < 0x0800) 137 { 138 pOut[0] = cast(wchar)(0xc0 | ((b >> 6) & 0x3f)); 139 pOut[1] = cast(wchar)(0x80 | (b & 0x3f)); 140 pOut += 2; 141 } 142 else 143 if (b < 0xd800 || b > 0xdfff) 144 { 145 pOut[0] = cast(wchar)(0xe0 | ((b >> 12) & 0x3f)); 146 pOut[1] = cast(wchar)(0x80 | ((b >> 6) & 0x3f)); 147 pOut[2] = cast(wchar)(0x80 | (b & 0x3f)); 148 pOut += 3; 149 } 150 else 151 // deal with surrogate-pairs 152 return toString (toString32(input, null, ate), output); 153 } 154 155 // return the produced output 156 return output [0..(pOut - output.ptr)]; 157 } 158 159 /******************************************************************************* 160 161 Decode Utf8 produced by the above toString() method. 162 163 If the output is provided off the stack, it should be large 164 enough to encompass the entire transcoding; failing to do 165 so will cause the output to be moved onto the heap instead. 166 167 Returns a slice of the output buffer, corresponding to the 168 converted characters. For optimum performance, the returned 169 buffer should be specified as 'output' on subsequent calls. 170 171 Where 'ate' is provided, it will be set to the number of 172 elements consumed from the input, and the output buffer 173 will not be resized (or allocated). This represents a 174 streaming mode, where slices of the input are processed 175 in sequence rather than all at one time (should use 'ate' 176 as an index for slicing into unconsumed input). 177 178 *******************************************************************************/ 179 180 wchar[] toString16 (const(char[]) input, wchar[] output=null, size_t* ate=null) 181 { 182 int produced; 183 const(char)* pIn = input.ptr; 184 const(char)* pMax = pIn + input.length; 185 const(char)* pValid; 186 187 if (ate is null) 188 if (input.length > output.length) 189 output.length = input.length; 190 191 if (input.length) 192 foreach (ref wchar d; output) 193 { 194 pValid = pIn; 195 wchar b = cast(wchar) *pIn; 196 197 if (b & 0x80) 198 { 199 if (b < 0xe0) 200 { 201 b &= 0x1f; 202 b = cast(wchar)((b << 6) | (*++pIn & 0x3f)); 203 } 204 else 205 { 206 if (b < 0xf0) 207 { 208 b &= 0x0f; 209 b = cast(wchar)((b << 6) | (pIn[1] & 0x3f)); 210 b = cast(wchar)((b << 6) | (pIn[2] & 0x3f)); 211 pIn += 2; 212 } 213 else 214 // deal with surrogate-pairs 215 return toString16 (toString32(input, null, ate), output); 216 } 217 } 218 d = b; 219 ++produced; 220 221 // did we read past the end of the input? 222 if (++pIn >= pMax) 223 { 224 if (pIn > pMax) 225 { 226 // yep ~ return tail or throw error? 227 if (ate) 228 { 229 pIn = pValid; 230 --produced; 231 break; 232 } 233 onUnicodeError ("Unicode.toString16 : incomplete utf8 input", pIn - input.ptr); 234 } 235 else 236 break; 237 } 238 } 239 240 // do we still have some input left? 241 if (ate) 242 *ate = pIn - input.ptr; 243 else 244 if (pIn < pMax) 245 // this should never happen! 246 onUnicodeError ("Unicode.toString16 : utf8 overflow", pIn - input.ptr); 247 248 // return the produced output 249 return output [0..produced]; 250 } 251 252 253 /******************************************************************************* 254 255 Encode Utf8 up to a maximum of 4 bytes long (five & six 256 byte variations are not supported). Throws an exception 257 where the input dchar is greater than 0x10ffff. 258 259 If the output is provided off the stack, it should be large 260 enough to encompass the entire transcoding; failing to do 261 so will cause the output to be moved onto the heap instead. 262 263 Returns a slice of the output buffer, corresponding to the 264 converted characters. For optimum performance, the returned 265 buffer should be specified as 'output' on subsequent calls. 266 267 Where 'ate' is provided, it will be set to the number of 268 elements consumed from the input, and the output buffer 269 will not be resized (or allocated). This represents a 270 streaming mode, where slices of the input are processed 271 in sequence rather than all at one time (should use 'ate' 272 as an index for slicing into unconsumed input). 273 274 *******************************************************************************/ 275 276 char[] toString (const(dchar[]) input, char[] output=null, size_t* ate=null) 277 { 278 if (ate) 279 *ate = input.length; 280 else 281 { 282 // potentially reallocate output 283 auto estimate = input.length * 2 + 4; 284 if (output.length < estimate) 285 output.length = estimate; 286 } 287 288 char* pOut = output.ptr; 289 char* pMax = pOut + output.length - 4; 290 291 foreach (eaten, dchar b; input) 292 { 293 // about to overflow the output? 294 if (pOut > pMax) 295 { 296 // if streaming, just return the unused input 297 if (ate) 298 { 299 *ate = eaten; 300 break; 301 } 302 303 // reallocate the output buffer 304 auto len = pOut - output.ptr; 305 output.length = len + len / 2; 306 pOut = output.ptr + len; 307 pMax = output.ptr + output.length - 4; 308 } 309 310 if (b < 0x80) 311 *pOut++ = cast(char)b; 312 else 313 if (b < 0x0800) 314 { 315 pOut[0] = cast(wchar)(0xc0 | ((b >> 6) & 0x3f)); 316 pOut[1] = cast(wchar)(0x80 | (b & 0x3f)); 317 pOut += 2; 318 } 319 else 320 if (b < 0x10000) 321 { 322 pOut[0] = cast(wchar)(0xe0 | ((b >> 12) & 0x3f)); 323 pOut[1] = cast(wchar)(0x80 | ((b >> 6) & 0x3f)); 324 pOut[2] = cast(wchar)(0x80 | (b & 0x3f)); 325 pOut += 3; 326 } 327 else 328 if (b < 0x110000) 329 { 330 pOut[0] = cast(wchar)(0xf0 | ((b >> 18) & 0x3f)); 331 pOut[1] = cast(wchar)(0x80 | ((b >> 12) & 0x3f)); 332 pOut[2] = cast(wchar)(0x80 | ((b >> 6) & 0x3f)); 333 pOut[3] = cast(wchar)(0x80 | (b & 0x3f)); 334 pOut += 4; 335 } 336 else 337 onUnicodeError ("Unicode.toString : invalid dchar", eaten); 338 } 339 340 // return the produced output 341 return output [0..(pOut - output.ptr)]; 342 } 343 344 345 /******************************************************************************* 346 347 Decode Utf8 produced by the above toString() method. 348 349 If the output is provided off the stack, it should be large 350 enough to encompass the entire transcoding; failing to do 351 so will cause the output to be moved onto the heap instead. 352 353 Returns a slice of the output buffer, corresponding to the 354 converted characters. For optimum performance, the returned 355 buffer should be specified as 'output' on subsequent calls. 356 357 Where 'ate' is provided, it will be set to the number of 358 elements consumed from the input, and the output buffer 359 will not be resized (or allocated). This represents a 360 streaming mode, where slices of the input are processed 361 in sequence rather than all at one time (should use 'ate' 362 as an index for slicing into unconsumed input). 363 364 *******************************************************************************/ 365 366 dchar[] toString32 (const(char[]) input, dchar[] output=null, size_t* ate=null) 367 { 368 int produced; 369 const(char)* pIn = input.ptr; 370 const(char)* pMax = pIn + input.length; 371 const(char)* pValid; 372 373 if (ate is null) 374 if (input.length > output.length) 375 output.length = input.length; 376 377 if (input.length) 378 foreach (ref dchar d; output) 379 { 380 pValid = pIn; 381 dchar b = cast(dchar) *pIn; 382 383 if (b & 0x80) 384 { 385 if (b < 0xe0) 386 { 387 b &= 0x1f; 388 b = (b << 6) | (*++pIn & 0x3f); 389 } 390 else 391 { 392 if (b < 0xf0) 393 { 394 b &= 0x0f; 395 b = (b << 6) | (pIn[1] & 0x3f); 396 b = (b << 6) | (pIn[2] & 0x3f); 397 pIn += 2; 398 } 399 else 400 { 401 b &= 0x07; 402 b = (b << 6) | (pIn[1] & 0x3f); 403 b = (b << 6) | (pIn[2] & 0x3f); 404 b = (b << 6) | (pIn[3] & 0x3f); 405 406 if (b >= 0x110000) 407 onUnicodeError ("Unicode.toString32 : invalid utf8 input", pIn - input.ptr); 408 pIn += 3; 409 } 410 } 411 } 412 d = b; 413 ++produced; 414 415 // did we read past the end of the input? 416 if (++pIn >= pMax) 417 { 418 if (pIn > pMax) 419 { 420 // yep ~ return tail or throw error? 421 if (ate) 422 { 423 pIn = pValid; 424 --produced; 425 break; 426 } 427 onUnicodeError ("Unicode.toString32 : incomplete utf8 input", pIn - input.ptr); 428 } 429 else 430 break; 431 } 432 } 433 434 // do we still have some input left? 435 if (ate) 436 *ate = pIn - input.ptr; 437 else 438 if (pIn < pMax) 439 // this should never happen! 440 onUnicodeError ("Unicode.toString32 : utf8 overflow", pIn - input.ptr); 441 442 // return the produced output 443 return output [0..produced]; 444 } 445 446 /******************************************************************************* 447 448 Encode Utf16 up to a maximum of 2 bytes long. Throws an exception 449 where the input dchar is greater than 0x10ffff. 450 451 If the output is provided off the stack, it should be large 452 enough to encompass the entire transcoding; failing to do 453 so will cause the output to be moved onto the heap instead. 454 455 Returns a slice of the output buffer, corresponding to the 456 converted characters. For optimum performance, the returned 457 buffer should be specified as 'output' on subsequent calls. 458 459 Where 'ate' is provided, it will be set to the number of 460 elements consumed from the input, and the output buffer 461 will not be resized (or allocated). This represents a 462 streaming mode, where slices of the input are processed 463 in sequence rather than all at one time (should use 'ate' 464 as an index for slicing into unconsumed input). 465 466 *******************************************************************************/ 467 468 wchar[] toString16 (const(dchar[]) input, wchar[] output=null, size_t* ate=null) 469 { 470 if (ate) 471 *ate = input.length; 472 else 473 { 474 size_t estimate = input.length * 2 + 2; 475 if (output.length < estimate) 476 output.length = estimate; 477 } 478 479 wchar* pOut = output.ptr; 480 wchar* pMax = pOut + output.length - 2; 481 482 foreach (eaten, dchar b; input) 483 { 484 // about to overflow the output? 485 if (pOut > pMax) 486 { 487 // if streaming, just return the unused input 488 if (ate) 489 { 490 *ate = eaten; 491 break; 492 } 493 494 // reallocate the output buffer 495 size_t len = pOut - output.ptr; 496 output.length = len + len / 2; 497 pOut = output.ptr + len; 498 pMax = output.ptr + output.length - 2; 499 } 500 501 if (b < 0x10000) 502 *pOut++ = cast(wchar)b; 503 else 504 if (b < 0x110000) 505 { 506 pOut[0] = cast(wchar)(0xd800 | (((b - 0x10000) >> 10) & 0x3ff)); 507 pOut[1] = cast(wchar)(0xdc00 | ((b - 0x10000) & 0x3ff)); 508 pOut += 2; 509 } 510 else 511 onUnicodeError ("Unicode.toString16 : invalid dchar", eaten); 512 } 513 514 // return the produced output 515 return output [0..(pOut - output.ptr)]; 516 } 517 518 /******************************************************************************* 519 520 Decode Utf16 produced by the above toString16() method. 521 522 If the output is provided off the stack, it should be large 523 enough to encompass the entire transcoding; failing to do 524 so will cause the output to be moved onto the heap instead. 525 526 Returns a slice of the output buffer, corresponding to the 527 converted characters. For optimum performance, the returned 528 buffer should be specified as 'output' on subsequent calls. 529 530 Where 'ate' is provided, it will be set to the number of 531 elements consumed from the input, and the output buffer 532 will not be resized (or allocated). This represents a 533 streaming mode, where slices of the input are processed 534 in sequence rather than all at one time (should use 'ate' 535 as an index for slicing into unconsumed input). 536 537 *******************************************************************************/ 538 539 dchar[] toString32 (const(wchar[]) input, dchar[] output=null, size_t* ate=null) 540 { 541 int produced; 542 const(wchar)* pIn = input.ptr; 543 const(wchar)* pMax = pIn + input.length; 544 const(wchar)* pValid; 545 546 if (ate is null) 547 if (input.length > output.length) 548 output.length = input.length; 549 550 if (input.length) 551 foreach (ref dchar d; output) 552 { 553 pValid = pIn; 554 dchar b = cast(dchar) *pIn; 555 556 // simple conversion ~ see http://www.unicode.org/faq/utf_bom.html#35 557 if (b >= 0xd800 && b <= 0xdfff) 558 b = ((b - 0xd7c0) << 10) + (*++pIn - 0xdc00); 559 560 if (b >= 0x110000) 561 onUnicodeError ("Unicode.toString32 : invalid utf16 input", pIn - input.ptr); 562 563 d = b; 564 ++produced; 565 566 if (++pIn >= pMax) 567 { 568 if (pIn > pMax) 569 { 570 // yep ~ return tail or throw error? 571 if (ate) 572 { 573 pIn = pValid; 574 --produced; 575 break; 576 } 577 onUnicodeError ("Unicode.toString32 : incomplete utf16 input", pIn - input.ptr); 578 } 579 else 580 break; 581 } 582 } 583 584 // do we still have some input left? 585 if (ate) 586 *ate = pIn - input.ptr; 587 else 588 if (pIn < pMax) 589 // this should never happen! 590 onUnicodeError ("Unicode.toString32 : utf16 overflow", pIn - input.ptr); 591 592 // return the produced output 593 return output [0..produced]; 594 } 595 596 597 /******************************************************************************* 598 599 Decodes a single dchar from the given src text, and indicates how 600 many chars were consumed from src to do so. 601 602 *******************************************************************************/ 603 604 dchar decode (const(char[]) src, ref size_t ate) 605 { 606 dchar[1] ret; 607 return toString32 (src, ret, &ate)[0]; 608 } 609 610 /******************************************************************************* 611 612 Decodes a single dchar from the given src text, and indicates how 613 many wchars were consumed from src to do so. 614 615 *******************************************************************************/ 616 617 dchar decode (const(wchar[]) src, ref size_t ate) 618 { 619 dchar[1] ret; 620 return toString32 (src, ret, &ate)[0]; 621 } 622 623 /******************************************************************************* 624 625 Encode a dchar into the provided dst array, and return a slice of 626 it representing the encoding 627 628 *******************************************************************************/ 629 630 char[] encode (char[] dst, dchar c) 631 { 632 return toString ((&c)[0..1], dst); 633 } 634 635 /******************************************************************************* 636 637 Encode a dchar into the provided dst array, and return a slice of 638 it representing the encoding 639 640 *******************************************************************************/ 641 642 wchar[] encode (wchar[] dst, dchar c) 643 { 644 return toString16 ((&c)[0..1], dst); 645 } 646 647 /******************************************************************************* 648 649 Is the given character valid? 650 651 *******************************************************************************/ 652 653 bool isValid (dchar c) 654 { 655 return (c < 0xD800 || (c > 0xDFFF && c <= 0x10FFFF)); 656 } 657 658 /******************************************************************************* 659 660 Convert from a char[] into the type of the dst provided. 661 662 Returns a slice of the given dst, where it is sufficiently large 663 to house the result, or a heap-allocated array otherwise. Returns 664 the original input where no conversion is required. 665 666 *******************************************************************************/ 667 668 inout(T[]) fromString8(T) (inout(char[]) s, T[] dst) if (is (T == char)) 669 { 670 return s; 671 } 672 673 T[] fromString8(T) (const(char[]) s, T[] dst) if (!is (T == char)) 674 { 675 static if (is (T == wchar)) 676 return .toString16 (s, dst); 677 678 static if (is (T == dchar)) 679 return .toString32 (s, dst); 680 } 681 682 /******************************************************************************* 683 684 Convert from a wchar[] into the type of the dst provided. 685 686 Returns a slice of the given dst, where it is sufficiently large 687 to house the result, or a heap-allocated array otherwise. Returns 688 the original input where no conversion is required. 689 690 *******************************************************************************/ 691 692 inout(T[]) fromString16(T) (inout(wchar[]) s, T[] dst) if (is (T == wchar)) 693 { 694 return s; 695 } 696 697 T[] fromString16(T) (const(wchar[]) s, T[] dst) if (!is (T == wchar)) 698 { 699 static if (is (T == char)) 700 return .toString (s, dst); 701 702 static if (is (T == dchar)) 703 return .toString32 (s, dst); 704 } 705 706 /******************************************************************************* 707 708 Convert from a dchar[] into the type of the dst provided. 709 710 Returns a slice of the given dst, where it is sufficiently large 711 to house the result, or a heap-allocated array otherwise. Returns 712 the original input where no conversion is required. 713 714 *******************************************************************************/ 715 716 inout(T[]) fromString32(T) (inout(dchar[]) s, T[] dst) if (is (T == dchar)) 717 { 718 return s; 719 } 720 721 T[] fromString32(T) (const(dchar[]) s, T[] dst) if (!is (T == dchar)) 722 { 723 static if (is (T == char)) 724 return .toString (s, dst); 725 726 static if (is (T == wchar)) 727 return .toString16 (s, dst); 728 } 729 730 /******************************************************************************* 731 732 Adjust the content such that no partial encodings exist on the 733 left side of the provided text. 734 735 Returns a slice of the input 736 737 *******************************************************************************/ 738 739 T[] cropLeft(T) (T[] s) 740 { 741 static if (is (T == char)) 742 for (int i=0; i < s.length && (s[i] & 0x80); ++i) 743 if ((s[i] & 0xc0) is 0xc0) 744 return s [i..$]; 745 746 static if (is (T == wchar)) 747 // skip if first char is a trailing surrogate 748 if ((s[0] & 0xfffffc00) is 0xdc00) 749 return s [1..$]; 750 751 return s; 752 } 753 754 /******************************************************************************* 755 756 Adjust the content such that no partial encodings exist on the 757 right side of the provided text. 758 759 Returns a slice of the input 760 761 *******************************************************************************/ 762 763 T[] cropRight(T) (T[] s) 764 { 765 if (s.length) 766 { 767 size_t i = s.length - 1; 768 static if (is (T == char)) 769 while (i && (s[i] & 0x80)) 770 { 771 if ((s[i] & 0xc0) is 0xc0) 772 { 773 // located the first byte of a sequence 774 ubyte b = s[i]; 775 size_t d = s.length - i; 776 777 // is it a 3 byte sequence? 778 if (b & 0x20) 779 --d; 780 781 // or a four byte sequence? 782 if (b & 0x10) 783 --d; 784 785 // is the sequence complete? 786 if (d is 2) 787 i = s.length; 788 return s [0..i]; 789 } 790 else 791 --i; 792 } 793 794 static if (is (T == wchar)) 795 // skip if last char is a leading surrogate 796 if ((s[i] & 0xfffffc00) is 0xd800) 797 return s [0..$-1]; 798 } 799 return s; 800 } 801 802 803 804 /******************************************************************************* 805 806 *******************************************************************************/ 807 808 debug (Utf) 809 { 810 import tango.io.Console; 811 812 void main() 813 { 814 auto s = "[\xc2\xa2\xc2\xa2\xc2\xa2]"; 815 Cout (s).newline; 816 817 Cout (cropLeft(s[0..$])).newline; 818 Cout (cropLeft(s[1..$])).newline; 819 Cout (cropLeft(s[2..$])).newline; 820 Cout (cropLeft(s[3..$])).newline; 821 Cout (cropLeft(s[4..$])).newline; 822 Cout (cropLeft(s[5..$])).newline; 823 824 Cout (cropRight(s[0..$])).newline; 825 Cout (cropRight(s[0..$-1])).newline; 826 Cout (cropRight(s[0..$-2])).newline; 827 Cout (cropRight(s[0..$-3])).newline; 828 Cout (cropRight(s[0..$-4])).newline; 829 Cout (cropRight(s[0..$-5])).newline; 830 } 831 }