From cde5668128f7ccf63f72ce2db52549f0d95d7878 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 8 May 2023 19:18:04 +0100 Subject: [PATCH] Add unit test and improving some code. --- Terminal.Gui/Rune/RuneExtensions.cs | 65 +- Terminal.Gui/Rune/RuneUtilities.cs | 176 ++++ Terminal.Gui/StringExtensions.cs | 34 + Terminal.Gui/Views/DateField.cs | 2 +- Terminal.Gui/Views/TextField.cs | 2 +- Terminal.Gui/Views/TimeField.cs | 2 +- UICatalog/Scenarios/ListViewWithSelection.cs | 2 +- UnitTests/Text/RuneTests.cs | 910 +++++++++++++++++++ 8 files changed, 1182 insertions(+), 11 deletions(-) create mode 100644 Terminal.Gui/Rune/RuneUtilities.cs create mode 100644 UnitTests/Text/RuneTests.cs diff --git a/Terminal.Gui/Rune/RuneExtensions.cs b/Terminal.Gui/Rune/RuneExtensions.cs index 78aca4461..6a0986343 100644 --- a/Terminal.Gui/Rune/RuneExtensions.cs +++ b/Terminal.Gui/Rune/RuneExtensions.cs @@ -6,9 +6,21 @@ using System.Threading.Tasks; namespace Terminal.Gui { public static class RuneExtensions { + public static Rune MaxRune = new Rune (0x10FFFF); + public static int ColumnWidth (this Rune rune) { - return 0; + return RuneUtilities.ColumnWidth(rune); + } + + public static bool IsNonSpacingChar (this Rune rune) + { + return RuneUtilities.IsNonSpacingChar (rune.Value); + } + + public static bool IsWideChar (this Rune rune) + { + return RuneUtilities.IsWideChar (rune.Value); } public static int RuneLen (this Rune rune) @@ -16,16 +28,11 @@ namespace Terminal.Gui { return 0; } - public static int EncodeRune (this Rune rune, byte [] bytes, int offset) + public static int EncodeRune (this Rune rune, byte [] dest, int offset = 0) { return 0; } - public static (Rune, int) DecodeRune (string str, int index, int Length) - { - return new (new Rune(), 0); - } - public static bool DecodeSurrogatePair (this Rune rune, out char [] spair) { spair = null; @@ -33,6 +40,50 @@ namespace Terminal.Gui { return true; } + public static bool EncodeSurrogatePair (char highsurrogate, char lowSurrogate, out Rune result) + { + try { + result = (Rune)char.ConvertToUtf32 (highsurrogate, lowSurrogate); + } catch (Exception) { + result = default; + return false; + } + + return true; + } + + public static bool IsSurrogatePair (this Rune rune) + { + return char.IsSurrogatePair (rune.ToString (), 0); + } + + public static bool IsSurrogate (this Rune rune) + { + return char.IsSurrogate (rune.ToString (), 0); + } + + public static bool IsValid (byte [] buffer) + { + var str = Encoding.Unicode.GetString (buffer); + + return Rune.IsValid(str.EnumerateRunes ().Current.Value); + } + + public static bool IsValid (this Rune rune) + { + return Rune.IsValid (rune.Value); + } + + internal static bool IsHighSurrogate (this Rune rune) + { + return char.IsHighSurrogate (rune.ToString (), 0); + } + + internal static bool IsLowSurrogate (this Rune rune) + { + return char.IsLowSurrogate (rune.ToString (), 0); + } + //public static bool operator==(this Rune a, Rune b) { return a.Equals(b); } //public static bool operator!=(this Rune a, Rune b) { return a.Equals(b); } diff --git a/Terminal.Gui/Rune/RuneUtilities.cs b/Terminal.Gui/Rune/RuneUtilities.cs new file mode 100644 index 000000000..f15696e89 --- /dev/null +++ b/Terminal.Gui/Rune/RuneUtilities.cs @@ -0,0 +1,176 @@ +// +// Contains a port of this code: +// https://www.cl.cam.ac.uk/%7Emgk25/ucs/wcwidth.c +// + +using System.Text; + +namespace Terminal.Gui { + internal static class RuneUtilities { + static int [,] combining = new int [,] { + { 0x0300, 0x036F }, { 0x0483, 0x0486 }, { 0x0488, 0x0489 }, + { 0x0591, 0x05BD }, { 0x05BF, 0x05BF }, { 0x05C1, 0x05C2 }, + { 0x05C4, 0x05C5 }, { 0x05C7, 0x05C7 }, { 0x0600, 0x0603 }, + { 0x0610, 0x0615 }, { 0x064B, 0x065E }, { 0x0670, 0x0670 }, + { 0x06D6, 0x06E4 }, { 0x06E7, 0x06E8 }, { 0x06EA, 0x06ED }, + { 0x070F, 0x070F }, { 0x0711, 0x0711 }, { 0x0730, 0x074A }, + { 0x07A6, 0x07B0 }, { 0x07EB, 0x07F3 }, { 0x0901, 0x0902 }, + { 0x093C, 0x093C }, { 0x0941, 0x0948 }, { 0x094D, 0x094D }, + { 0x0951, 0x0954 }, { 0x0962, 0x0963 }, { 0x0981, 0x0981 }, + { 0x09BC, 0x09BC }, { 0x09C1, 0x09C4 }, { 0x09CD, 0x09CD }, + { 0x09E2, 0x09E3 }, { 0x0A01, 0x0A02 }, { 0x0A3C, 0x0A3C }, + { 0x0A41, 0x0A42 }, { 0x0A47, 0x0A48 }, { 0x0A4B, 0x0A4D }, + { 0x0A70, 0x0A71 }, { 0x0A81, 0x0A82 }, { 0x0ABC, 0x0ABC }, + { 0x0AC1, 0x0AC5 }, { 0x0AC7, 0x0AC8 }, { 0x0ACD, 0x0ACD }, + { 0x0AE2, 0x0AE3 }, { 0x0B01, 0x0B01 }, { 0x0B3C, 0x0B3C }, + { 0x0B3F, 0x0B3F }, { 0x0B41, 0x0B43 }, { 0x0B4D, 0x0B4D }, + { 0x0B56, 0x0B56 }, { 0x0B82, 0x0B82 }, { 0x0BC0, 0x0BC0 }, + { 0x0BCD, 0x0BCD }, { 0x0C3E, 0x0C40 }, { 0x0C46, 0x0C48 }, + { 0x0C4A, 0x0C4D }, { 0x0C55, 0x0C56 }, { 0x0CBC, 0x0CBC }, + { 0x0CBF, 0x0CBF }, { 0x0CC6, 0x0CC6 }, { 0x0CCC, 0x0CCD }, + { 0x0CE2, 0x0CE3 }, { 0x0D41, 0x0D43 }, { 0x0D4D, 0x0D4D }, + { 0x0DCA, 0x0DCA }, { 0x0DD2, 0x0DD4 }, { 0x0DD6, 0x0DD6 }, + { 0x0E31, 0x0E31 }, { 0x0E34, 0x0E3A }, { 0x0E47, 0x0E4E }, + { 0x0EB1, 0x0EB1 }, { 0x0EB4, 0x0EB9 }, { 0x0EBB, 0x0EBC }, + { 0x0EC8, 0x0ECD }, { 0x0F18, 0x0F19 }, { 0x0F35, 0x0F35 }, + { 0x0F37, 0x0F37 }, { 0x0F39, 0x0F39 }, { 0x0F71, 0x0F7E }, + { 0x0F80, 0x0F84 }, { 0x0F86, 0x0F87 }, { 0x0F90, 0x0F97 }, + { 0x0F99, 0x0FBC }, { 0x0FC6, 0x0FC6 }, { 0x102D, 0x1030 }, + { 0x1032, 0x1032 }, { 0x1036, 0x1037 }, { 0x1039, 0x1039 }, + { 0x1058, 0x1059 }, { 0x1160, 0x11FF }, { 0x135F, 0x135F }, + { 0x1712, 0x1714 }, { 0x1732, 0x1734 }, { 0x1752, 0x1753 }, + { 0x1772, 0x1773 }, { 0x17B4, 0x17B5 }, { 0x17B7, 0x17BD }, + { 0x17C6, 0x17C6 }, { 0x17C9, 0x17D3 }, { 0x17DD, 0x17DD }, + { 0x180B, 0x180D }, { 0x18A9, 0x18A9 }, { 0x1920, 0x1922 }, + { 0x1927, 0x1928 }, { 0x1932, 0x1932 }, { 0x1939, 0x193B }, + { 0x1A17, 0x1A18 }, { 0x1B00, 0x1B03 }, { 0x1B34, 0x1B34 }, + { 0x1B36, 0x1B3A }, { 0x1B3C, 0x1B3C }, { 0x1B42, 0x1B42 }, + { 0x1B6B, 0x1B73 }, { 0x1DC0, 0x1DCA }, { 0x1DFE, 0x1DFF }, + { 0x200B, 0x200F }, { 0x202A, 0x202E }, { 0x2060, 0x2063 }, + { 0x206A, 0x206F }, { 0x20D0, 0x20EF }, { 0x2E9A, 0x2E9A }, + { 0x2EF4, 0x2EFF }, { 0x2FD6, 0x2FEF }, { 0x2FFC, 0x2FFF }, + { 0x31E4, 0x31EF }, { 0x321F, 0x321F }, { 0xA48D, 0xA48F }, + { 0xA806, 0xA806 }, { 0xA80B, 0xA80B }, { 0xA825, 0xA826 }, + { 0xFB1E, 0xFB1E }, { 0xFE00, 0xFE0F }, { 0xFE1A, 0xFE1F }, + { 0xFE20, 0xFE23 }, { 0xFE53, 0xFE53 }, { 0xFE67, 0xFE67 }, + { 0xFEFF, 0xFEFF }, { 0xFFF9, 0xFFFB }, + { 0x10A01, 0x10A03 }, { 0x10A05, 0x10A06 }, { 0x10A0C, 0x10A0F }, + { 0x10A38, 0x10A3A }, { 0x10A3F, 0x10A3F }, { 0x1D167, 0x1D169 }, + { 0x1D173, 0x1D182 }, { 0x1D185, 0x1D18B }, { 0x1D1AA, 0x1D1AD }, + { 0x1D242, 0x1D244 }, { 0xE0001, 0xE0001 }, { 0xE0020, 0xE007F }, + { 0xE0100, 0xE01EF } + }; + + static int [,] combiningWideChars = new int [,] { + /* Hangul Jamo init. consonants - 0x1100, 0x11ff */ + /* Miscellaneous Technical - 0x2300, 0x23ff */ + /* Hangul Syllables - 0x11a8, 0x11c2 */ + /* CJK Compatibility Ideographs - f900, fad9 */ + /* Vertical forms - fe10, fe19 */ + /* CJK Compatibility Forms - fe30, fe4f */ + /* Fullwidth Forms - ff01, ffee */ + /* Alphabetic Presentation Forms - 0xFB00, 0xFb4f */ + /* Chess Symbols - 0x1FA00, 0x1FA0f */ + + { 0x1100, 0x115f }, { 0x231a, 0x231b }, { 0x2329, 0x232a }, + { 0x23e9, 0x23ec }, { 0x23f0, 0x23f0 }, { 0x23f3, 0x23f3 }, + { 0x25fd, 0x25fe }, { 0x2614, 0x2615 }, { 0x2648, 0x2653 }, + { 0x267f, 0x267f }, { 0x2693, 0x2693 }, { 0x26a1, 0x26a1 }, + { 0x26aa, 0x26ab }, { 0x26bd, 0x26be }, { 0x26c4, 0x26c5 }, + { 0x26ce, 0x26ce }, { 0x26d4, 0x26d4 }, { 0x26ea, 0x26ea }, + { 0x26f2, 0x26f3 }, { 0x26f5, 0x26f5 }, { 0x26fa, 0x26fa }, + { 0x26fd, 0x26fd }, { 0x2705, 0x2705 }, { 0x270a, 0x270b }, + { 0x2728, 0x2728 }, { 0x274c, 0x274c }, { 0x274e, 0x274e }, + { 0x2753, 0x2755 }, { 0x2757, 0x2757 }, { 0x2795, 0x2797 }, + { 0x27b0, 0x27b0 }, { 0x27bf, 0x27bf }, { 0x2b1b, 0x2b1c }, + { 0x2b50, 0x2b50 }, { 0x2b55, 0x2b55 }, { 0x2e80, 0x303e }, + { 0x3041, 0x3096 }, { 0x3099, 0x30ff }, { 0x3105, 0x312f }, + { 0x3131, 0x318e }, { 0x3190, 0x3247 }, { 0x3250, 0x4dbf }, + { 0x4e00, 0xa4c6 }, { 0xa960, 0xa97c }, { 0xac00 ,0xd7a3 }, + { 0xf900, 0xfaff }, { 0xfe10, 0xfe1f }, { 0xfe30 ,0xfe6b }, + { 0xff01, 0xff60 }, { 0xffe0, 0xffe6 }, + { 0x16fe0, 0x16fe4 }, { 0x16ff0, 0x16ff1 }, { 0x17000, 0x187f7 }, + { 0x18800, 0x18cd5 }, { 0x18d00, 0x18d08 }, { 0x1aff0, 0x1affc }, + { 0x1b000, 0x1b122 }, { 0x1b150, 0x1b152 }, { 0x1b164, 0x1b167 }, { 0x1b170, 0x1b2fb }, + { 0x1f004, 0x1f004 }, { 0x1f0cf, 0x1f0cf }, /*{ 0x1f100, 0x1f10a },*/ + //{ 0x1f110, 0x1f12d }, { 0x1f130, 0x1f169 }, { 0x1f170, 0x1f1ac }, + { 0x1f18f, 0x1f199 }, + { 0x1f1e6, 0x1f1ff }, { 0x1f200, 0x1f202 }, { 0x1f210, 0x1f23b }, + { 0x1f240, 0x1f248 }, { 0x1f250, 0x1f251 }, { 0x1f260, 0x1f265 }, + { 0x1f300, 0x1f320 }, { 0x1f32d, 0x1f33e }, { 0x1f340, 0x1f37e }, + { 0x1f380, 0x1f393 }, { 0x1f3a0, 0x1f3ca }, { 0x1f3cf, 0x1f3d3 }, + { 0x1f3e0, 0x1f3f0 }, { 0x1f3f4, 0x1f3f4 }, { 0x1f3f8, 0x1f43e }, + { 0x1f440, 0x1f44e }, { 0x1f450, 0x1f4fc }, { 0x1f4ff, 0x1f53d }, + { 0x1f54b, 0x1f54e }, { 0x1f550, 0x1f567 }, { 0x1f57a, 0x1f57a }, + { 0x1f595, 0x1f596 }, { 0x1f5a4, 0x1f5a4 }, { 0x1f5fb, 0x1f606 }, + { 0x1f607, 0x1f64f }, { 0x1f680, 0x1f6c5 }, { 0x1f6cc, 0x1f6cc }, + { 0x1f6d0, 0x1f6d2 }, { 0x1f6d5, 0x1f6d7 }, { 0x1f6dd, 0x1f6df }, { 0x1f6eb, 0x1f6ec }, + { 0x1f6f4, 0x1f6fc }, { 0x1f7e0, 0x1f7eb }, { 0x1f7f0, 0x1f7f0 }, { 0x1f90c, 0x1f93a }, + { 0x1f93c, 0x1f945 }, { 0x1f947, 0x1f97f }, { 0x1f980, 0x1f9cc }, + { 0x1f9cd, 0x1f9ff }, { 0x1fa70, 0x1fa74 }, { 0x1fa78, 0x1fa7c }, { 0x1fa80, 0x1fa86 }, + { 0x1fa90, 0x1faac }, { 0x1fab0, 0x1faba }, { 0x1fac0, 0x1fac5 }, + { 0x1fad0, 0x1fad9 }, { 0x1fae0, 0x1fae7 }, { 0x1faf0, 0x1faf6 }, { 0x20000, 0x2fffd }, { 0x30000, 0x3fffd }, + //{ 0xe0100, 0xe01ef }, { 0xf0000, 0xffffd }, { 0x100000, 0x10fffd } + }; + + static int bisearch (int rune, int [,] table, int max) + { + int min = 0; + int mid; + + if (rune < table [0, 0] || rune > table [max, 1]) + return 0; + while (max >= min) { + mid = (min + max) / 2; + if (rune > table [mid, 1]) + min = mid + 1; + else if (rune < table [mid, 0]) + max = mid - 1; + else + return 1; + } + + return 0; + } + + /// + /// Check if the rune is a non-spacing character. + /// + /// The rune. + /// True if is a non-spacing character, false otherwise. + public static bool IsNonSpacingChar (int rune) + { + return bisearch (rune, combining, combining.GetLength (0) - 1) != 0; + } + + /// + /// Check if the rune is a wide character. + /// + /// The rune. + /// True if is a wide character, false otherwise. + public static bool IsWideChar (int rune) + { + return bisearch (rune, combiningWideChars, combiningWideChars.GetLength (0) - 1) != 0; + } + + /// + /// Number of column positions of a wide-character code. This is used to measure runes as displayed by text-based terminals. + /// + /// The width in columns, 0 if the argument is the null character, -1 if the value is not printable, otherwise the number of columns that the rune occupies. + /// The rune. + public static int ColumnWidth (Rune rune) + { + int irune = rune.Value; + if (irune < 0x20 || (irune >= 0x7f && irune < 0xa0)) + return -1; + if (irune < 0x7f) + return 1; + /* binary search in table of non-spacing characters */ + if (bisearch (irune, combining, combining.GetLength (0) - 1) != 0) + return 0; + /* if we arrive here, ucs is not a combining or C0/C1 control character */ + return 1 + + (bisearch (irune, combiningWideChars, combiningWideChars.GetLength (0) - 1) != 0 ? 1 : 0); + } + } +} diff --git a/Terminal.Gui/StringExtensions.cs b/Terminal.Gui/StringExtensions.cs index 5b9a35c27..13baef883 100644 --- a/Terminal.Gui/StringExtensions.cs +++ b/Terminal.Gui/StringExtensions.cs @@ -55,6 +55,34 @@ namespace Terminal.Gui { return new List(); } + public static (Rune rune, int size) DecodeRune (this string instr, int start = 0, int nbytes = -1) + { + return new (new Rune (), 0); + } + + public static (Rune rune, int size) DecodeLastRune (this string instr, int end = -1) + { + return new (new Rune (), 0); + } + + public static bool DecodeSurrogatePair (this string instr, out char [] chars) + { + chars = null; + if (instr.Length == 2) { + chars = instr.ToCharArray (); + if (RuneExtensions.EncodeSurrogatePair (chars [0], chars [1], out _)) { + chars = new char[] { chars [0], chars [1] }; + return true; + } + } + return false; + } + + public static byte[] ToByteArray (this string instr) + { + return Encoding.Unicode.GetBytes (instr.ToCharArray ()); + } + public static string Make (Rune [] runes) { return runes.ToString (); @@ -65,6 +93,12 @@ namespace Terminal.Gui { return runes.ToString (); } + public static string Make (Rune rune) + { + return rune.ToString (); + } + + public static string Make (uint rune) { return rune.ToString (); diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs index 53c088b20..1acb8cefa 100644 --- a/Terminal.Gui/Views/DateField.cs +++ b/Terminal.Gui/Views/DateField.cs @@ -341,7 +341,7 @@ namespace Terminal.Gui { if (ReadOnly) return true; - if (SetText (TextModel.ToRunes (StringExtensions.Make ((uint)kb.Key)).First ())) + if (SetText (TextModel.ToRunes (StringExtensions.Make ((Rune)(uint)kb.Key)).First ())) IncCursorPosition (); return true; diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 36cfcb0d4..58a752cd4 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -649,7 +649,7 @@ namespace Terminal.Gui { if (!useOldCursorPos) { oldCursorPos = point; } - var kbstr = TextModel.ToRunes (StringExtensions.Make ((uint)kb.Key)); + var kbstr = TextModel.ToRunes (StringExtensions.Make ((Rune)(uint)kb.Key)); if (Used) { point++; if (point == newText.Count + 1) { diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs index 089f011b9..a1d5934d3 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -260,7 +260,7 @@ namespace Terminal.Gui { if (ReadOnly) return true; - if (SetText (TextModel.ToRunes (StringExtensions.Make ((uint)kb.Key)).First ())) + if (SetText (TextModel.ToRunes (StringExtensions.Make ((Rune)(uint)kb.Key)).First ())) IncCursorPosition (); return true; diff --git a/UICatalog/Scenarios/ListViewWithSelection.cs b/UICatalog/Scenarios/ListViewWithSelection.cs index 7e562b4c1..8822372dd 100644 --- a/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/UICatalog/Scenarios/ListViewWithSelection.cs @@ -207,7 +207,7 @@ namespace UICatalog.Scenarios { int used = 0; int index = start; while (index < ustr.Length) { - (var rune, var size) = RuneExtensions.DecodeRune (ustr, index, index - ustr.Length); + (var rune, var size) = ustr.DecodeRune (index, index - ustr.Length); var count = rune.ColumnWidth (); if (used + count >= width) break; driver.AddRune (rune); diff --git a/UnitTests/Text/RuneTests.cs b/UnitTests/Text/RuneTests.cs new file mode 100644 index 000000000..95247d54e --- /dev/null +++ b/UnitTests/Text/RuneTests.cs @@ -0,0 +1,910 @@ +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Xunit; + +namespace Terminal.Gui.TextTests { + public class RuneTests { + [Fact] + public void TestColumnWidth () + { + Rune a = (Rune)'a'; + Rune b = (Rune)'b'; + Rune c = (Rune)123; + Rune d = (Rune)'\u1150'; // 0x1150 ᅐ Unicode Technical Report #11 + Rune e = (Rune)'\u1161'; // 0x1161 ᅡ Unicode Hangul Jamo for join with column width equal to 0 alone. + Rune f = (Rune)31; // non printable character + Rune g = (Rune)127; // non printable character + string h = "\U0001fa01"; + string i = "\U000e0fe1"; + Rune j = (Rune)'\u20D0'; + Rune k = (Rune)'\u25a0'; + Rune l = (Rune)'\u25a1'; + Rune m = (Rune)'\uf61e'; + byte [] n = new byte [4] { 0xf0, 0x9f, 0x8d, 0x95 }; // UTF-8 Encoding + Rune o = new Rune ('\ud83c', '\udf55'); // UTF-16 Encoding; + string p = "\U0001F355"; // UTF-32 Encoding + Rune q = (Rune)'\u2103'; + Rune r = (Rune)'\u1100'; + Rune s = (Rune)'\u2501'; + + Assert.Equal (1, a.ColumnWidth ()); + Assert.Equal ("a", a.ToString ()); + Assert.Equal (1, a.ToString ().Length); + Assert.Equal (1, a.RuneLen ()); + Assert.Equal (1, b.ColumnWidth ()); + Assert.Equal ("b", b.ToString ()); + Assert.Equal (1, b.ToString ().Length); + Assert.Equal (1, b.RuneLen ()); + var rl = a < b; + Assert.True (rl); + Assert.Equal (1, c.ColumnWidth ()); + Assert.Equal ("{", c.ToString ()); + Assert.Equal (1, c.ToString ().Length); + Assert.Equal (1, c.RuneLen ()); + Assert.Equal (2, d.ColumnWidth ()); + Assert.Equal ("ᅐ", d.ToString ()); + Assert.Equal (1, d.ToString ().Length); + Assert.Equal (3, d.RuneLen ()); + Assert.Equal (0, e.ColumnWidth ()); + string join = "\u1104\u1161"; + Assert.Equal ("따", join); + Assert.Equal (2, join.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (OperationStatus.Done, Rune.DecodeFromUtf16 (join.ToCharArray (), out Rune result, out int charsConsumed)); + Assert.False (join.ToRunes () [0].DecodeSurrogatePair (out char [] spair)); + Assert.Equal (2, join.RuneCount ()); + Assert.Equal (2, join.Length); + Assert.Equal ("ᅡ", e.ToString ()); + Assert.Equal (1, e.ToString ().Length); + Assert.Equal (3, e.RuneLen ()); + string joinNormalize = join.Normalize (); + Assert.Equal ("따", joinNormalize); + Assert.Equal (2, joinNormalize.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (OperationStatus.Done, Rune.DecodeFromUtf16 (joinNormalize.ToCharArray (), out result, out charsConsumed)); + Assert.False (joinNormalize.ToRunes () [0].DecodeSurrogatePair (out spair)); + Assert.Equal (1, joinNormalize.RuneCount ()); + Assert.Equal (1, joinNormalize.Length); + Assert.Equal (-1, f.ColumnWidth ()); + Assert.Equal (1, f.ToString ().Length); + Assert.Equal (1, f.RuneLen ()); + Assert.Equal (-1, g.ColumnWidth ()); + Assert.Equal (1, g.ToString ().Length); + Assert.Equal (1, g.RuneLen ()); + var uh = h; + (var runeh, var sizeh) = uh.DecodeRune (); + Assert.Equal (2, runeh.ColumnWidth ()); + Assert.Equal ("🨁", h); + Assert.Equal (2, runeh.ToString ().Length); + Assert.Equal (4, runeh.RuneLen ()); + Assert.Equal (sizeh, runeh.RuneLen ()); + for (int x = 0; x < uh.Length - 1; x++) { + Assert.Equal (0, char.ConvertToUtf32 (uh [x], uh [x + 1])); + Assert.False (RuneExtensions.EncodeSurrogatePair (uh [x], uh [x + 1], out _)); + } + Assert.True (Rune.IsValid (runeh.Value)); + Assert.True (RuneExtensions.IsValid (uh.ToByteArray ())); + //Assert.True (Rune.FullRune (uh.ToByteArray ())); + Assert.Equal (1, uh.RuneCount ()); + (var runelh, var sizelh) = uh.DecodeLastRune (); + + Assert.Equal (2, runelh.ColumnWidth ()); + Assert.Equal (2, runelh.ToString ().Length); + Assert.Equal (4, runelh.RuneLen ()); + Assert.Equal (sizelh, runelh.RuneLen ()); + Assert.True (Rune.IsValid (runelh.Value)); + + var ui = i; + (var runei, var sizei) = ui.DecodeRune (); + Assert.Equal (2, runei.ColumnWidth ()); + Assert.Equal ("󠿡", i); + Assert.Equal (2, runei.ToString ().Length); + Assert.Equal (4, runei.RuneLen ()); + Assert.Equal (sizei, runei.RuneLen ()); + for (int x = 0; x < ui.Length - 1; x++) { + Assert.Equal (0, char.ConvertToUtf32 (uh [x], uh [x + 1])); + Assert.False (RuneExtensions.EncodeSurrogatePair (ui [x], ui [x + 1], out _)); + } + Assert.True (Rune.IsValid (runei.Value)); + Assert.True (RuneExtensions.IsValid (ui.ToByteArray ())); + //Assert.True (Rune.FullRune (ui.ToByteArray ())); + (var runeli, var sizeli) = ui.DecodeLastRune (); + Assert.Equal (2, runeli.ColumnWidth ()); + Assert.Equal (2, runeli.ToString ().Length); + Assert.Equal (4, runeli.RuneLen ()); + Assert.Equal (sizeli, runeli.RuneLen ()); + Assert.True (Rune.IsValid (runeli.Value)); + + Assert.Equal (runeh.ColumnWidth (), runei.ColumnWidth ()); + Assert.NotEqual (h, i); + Assert.Equal (runeh.ToString ().Length, runei.ToString ().Length); + Assert.Equal (runeh.RuneLen (), runei.RuneLen ()); + var uj = j.ToString (); + (var runej, var sizej) = uj.DecodeRune (); + Assert.Equal (0, j.ColumnWidth ()); + Assert.Equal (0, ((Rune)uj [0]).ColumnWidth ()); + Assert.Equal (j, (Rune)uj [0]); + Assert.Equal ("⃐", j.ToString ()); + Assert.Equal ("⃐", uj); + Assert.Equal (1, j.ToString ().Length); + Assert.Equal (1, runej.ToString ().Length); + Assert.Equal (3, j.RuneLen ()); + Assert.Equal (sizej, runej.RuneLen ()); + Assert.Equal (1, k.ColumnWidth ()); + Assert.Equal ("■", k.ToString ()); + Assert.Equal (1, k.ToString ().Length); + Assert.Equal (3, k.RuneLen ()); + Assert.Equal (1, l.ColumnWidth ()); + Assert.Equal ("□", l.ToString ()); + Assert.Equal (1, l.ToString ().Length); + Assert.Equal (3, l.RuneLen ()); + Assert.Equal (1, m.ColumnWidth ()); + Assert.Equal ("", m.ToString ()); + Assert.Equal (1, m.ToString ().Length); + Assert.Equal (3, m.RuneLen ()); + var rn = StringExtensions.Make (n).DecodeRune ().rune; + Assert.Equal (2, rn.ColumnWidth ()); + Assert.Equal ("🍕", rn.ToString ()); + Assert.Equal (2, rn.ToString ().Length); + Assert.Equal (4, rn.RuneLen ()); + Assert.Equal (2, o.ColumnWidth ()); + Assert.Equal ("🍕", o.ToString ()); + Assert.Equal (2, o.ToString ().Length); + Assert.Equal (4, o.RuneLen ()); + var rp = p.DecodeRune ().rune; + Assert.Equal (2, rp.ColumnWidth ()); + Assert.Equal ("🍕", p); + Assert.Equal (2, p.Length); + Assert.Equal (4, rp.RuneLen ()); + Assert.Equal (1, q.ColumnWidth ()); + Assert.Equal ("℃", q.ToString ()); + Assert.Equal (1, q.ToString ().Length); + Assert.Equal (3, q.RuneLen ()); + var rq = q.ToString ().DecodeRune ().rune; + Assert.Equal (1, rq.ColumnWidth ()); + Assert.Equal ("℃", rq.ToString ()); + Assert.Equal (1, rq.ToString ().Length); + Assert.Equal (3, rq.RuneLen ()); + Assert.Equal (2, r.ColumnWidth ()); + Assert.Equal ("ᄀ", r.ToString ()); + Assert.Equal (1, r.ToString ().Length); + Assert.Equal (3, r.RuneLen ()); + Assert.Equal (1, s.ColumnWidth ()); + Assert.Equal ("━", s.ToString ()); + Assert.Equal (1, s.ToString ().Length); + Assert.Equal (3, s.RuneLen ()); + var buff = new byte [4]; + var sb = ((Rune)'\u2503').EncodeRune (buff); + Assert.Equal (1, ((Rune)'\u2503').ColumnWidth ()); + (var rune, var size) = StringExtensions.Make ('\u2503').DecodeRune (); + Assert.Equal (sb, size); + Assert.Equal ('\u2503', (uint)rune.Value); + var scb = char.ConvertToUtf32 ("℃", 0); + var scr = '℃'.ToString ().Length; + Assert.Equal (scr, ((Rune)(uint)scb).ColumnWidth ()); + buff = new byte [4]; + sb = ((Rune)'\u1100').EncodeRune (buff); + Assert.Equal (2, ((Rune)'\u1100').ColumnWidth ()); + Assert.Equal (2, StringExtensions.Make ((Rune)'\u1100').ConsoleWidth ()); + Assert.Equal (1, '\u1100'.ToString ().Length); // Length as string returns 1 but in reality it occupies 2 columns. + (rune, size) = StringExtensions.Make ((Rune)'\u1100').DecodeRune (); + Assert.Equal (sb, size); + Assert.Equal ('\u1100', (uint)rune.Value); + string str = "\u2615"; + Assert.Equal ("☕", str); + Assert.Equal (2, str.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (2, str.ConsoleWidth ()); + Assert.Equal (1, str.RuneCount ()); + Assert.Equal (1, str.Length); + str = "\u2615\ufe0f"; // Identical but \ufe0f forces it to be rendered as a colorful image as compared to a monochrome text variant. + Assert.Equal ("☕️", str); + Assert.Equal (2, str.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (2, str.ConsoleWidth ()); + Assert.Equal (2, str.RuneCount ()); + Assert.Equal (2, str.Length); + str = "\u231a"; + Assert.Equal ("⌚", str); + Assert.Equal (2, str.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (2, str.ConsoleWidth ()); + Assert.Equal (1, str.RuneCount ()); + Assert.Equal (1, str.Length); + str = "\u231b"; + Assert.Equal ("⌛", str); + Assert.Equal (2, str.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (2, str.ConsoleWidth ()); + Assert.Equal (1, str.RuneCount ()); + Assert.Equal (1, str.Length); + str = "\u231c"; + Assert.Equal ("⌜", str); + Assert.Equal (1, str.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (1, str.ConsoleWidth ()); + Assert.Equal (1, str.RuneCount ()); + Assert.Equal (1, str.Length); + str = "\u1dc0"; + Assert.Equal ("᷀", str); + Assert.Equal (0, str.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (0, str.ConsoleWidth ()); + Assert.Equal (1, str.RuneCount ()); + Assert.Equal (1, str.Length); + str = "\ud83e\udd16"; + Assert.Equal ("🤖", str); + Assert.Equal (2, str.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (2, str.ConsoleWidth ()); + Assert.Equal (1, str.RuneCount ()); // Here returns 1 because is a valid surrogate pair resulting in only rune >=U+10000..U+10FFFF + Assert.Equal (2, str.Length); // String always preserves the originals values of each surrogate pair + str = "\U0001f9e0"; + Assert.Equal ("🧠", str); + Assert.Equal (2, str.Sum (x => ((Rune)x).ColumnWidth ())); + Assert.Equal (2, str.ConsoleWidth ()); + Assert.Equal (1, str.RuneCount ()); + Assert.Equal (2, str.Length); + } + + [Fact] + public void TestRune () + { + Rune a = new Rune ('a'); + Assert.Equal (1, a.ColumnWidth ()); + Assert.Equal (1, a.ToString ().Length); + Assert.Equal ("a", a.ToString ()); + Rune b = new Rune (0x0061); + Assert.Equal (1, b.ColumnWidth ()); + Assert.Equal (1, b.ToString ().Length); + Assert.Equal ("a", b.ToString ()); + Rune c = new Rune ('\u0061'); + Assert.Equal (1, c.ColumnWidth ()); + Assert.Equal (1, c.ToString ().Length); + Assert.Equal ("a", c.ToString ()); + Rune d = new Rune (0x10421); + Assert.Equal (2, d.ColumnWidth ()); + Assert.Equal (2, d.ToString ().Length); + Assert.Equal ("𐐡", d.ToString ()); + Assert.False (RuneExtensions.EncodeSurrogatePair ('\ud799', '\udc21', out Rune rune)); + Assert.Throws (() => new Rune ('\ud799', '\udc21')); + Rune e = new Rune ('\ud801', '\udc21'); + Assert.Equal (2, e.ColumnWidth ()); + Assert.Equal (2, e.ToString ().Length); + Assert.Equal ("𐐡", e.ToString ()); + Assert.False (Rune.IsValid (new Rune ('\ud801').Value)); + Rune f = new Rune ('\ud83c', '\udf39'); + Assert.Equal (2, f.ColumnWidth ()); + Assert.Equal (2, f.ToString ().Length); + Assert.Equal ("🌹", f.ToString ()); + var exception = Record.Exception (() => new Rune (0x10ffff)); + Assert.Null (exception); + Rune g = new Rune (0x10ffff); + string s = "\U0010ffff"; + Assert.Equal (2, g.ColumnWidth ()); + Assert.Equal (2, s.ConsoleWidth ()); + Assert.Equal (2, g.ToString ().Length); + Assert.Equal (2, s.Length); + Assert.Equal ("􏿿", g.ToString ()); + Assert.Equal ("􏿿", s); + Assert.Equal (g.ToString (), s); + Assert.Throws (() => new Rune (0x12345678)); + var h = new Rune ('\u1150'); + Assert.Equal (2, h.ColumnWidth ()); + Assert.Equal (1, h.ToString ().Length); + Assert.Equal ("ᅐ", h.ToString ()); + var i = new Rune ('\u4F60'); + Assert.Equal (2, i.ColumnWidth ()); + Assert.Equal (1, i.ToString ().Length); + Assert.Equal ("你", i.ToString ()); + var j = new Rune ('\u597D'); + Assert.Equal (2, j.ColumnWidth ()); + Assert.Equal (1, j.ToString ().Length); + Assert.Equal ("好", j.ToString ()); + var k = new Rune ('\ud83d', '\udc02'); + Assert.Equal (2, k.ColumnWidth ()); + Assert.Equal (2, k.ToString ().Length); + Assert.Equal ("🐂", k.ToString ()); + var l = new Rune ('\ud801', '\udcbb'); + Assert.Equal (2, l.ColumnWidth ()); + Assert.Equal (2, l.ToString ().Length); + Assert.Equal ("𐒻", l.ToString ()); + var m = new Rune ('\ud801', '\udccf'); + Assert.Equal (2, m.ColumnWidth ()); + Assert.Equal (2, m.ToString ().Length); + Assert.Equal ("𐓏", m.ToString ()); + var n = new Rune ('\u00e1'); + Assert.Equal (1, n.ColumnWidth ()); + Assert.Equal (1, n.ToString ().Length); + Assert.Equal ("á", n.ToString ()); + var o = new Rune ('\ud83d', '\udd2e'); + Assert.Equal (2, o.ColumnWidth ()); + Assert.Equal (2, o.ToString ().Length); + Assert.Equal ("🔮", o.ToString ()); + var p = new Rune ('\u2329'); + Assert.Equal (2, p.ColumnWidth ()); + Assert.Equal (1, p.ToString ().Length); + Assert.Equal ("〈", p.ToString ()); + var q = new Rune ('\u232a'); + Assert.Equal (2, q.ColumnWidth ()); + Assert.Equal (1, q.ToString ().Length); + Assert.Equal ("〉", q.ToString ()); + var r = "\U0000232a".DecodeRune ().rune; + Assert.Equal (2, r.ColumnWidth ()); + Assert.Equal (1, r.ToString ().Length); + Assert.Equal ("〉", r.ToString ()); + + PrintTextElementCount ('\u00e1'.ToString (), "á", 1, 1, 1, 1); + PrintTextElementCount (new string ('\u0061', '\u0301'), "á", 1, 2, 2, 1); + PrintTextElementCount (new string ('\u0065', '\u0301'), "é", 1, 2, 2, 1); + PrintTextElementCount (StringExtensions.Make (new Rune [] { new Rune (0x1f469), new Rune (0x1f3fd), new Rune ('\u200d'), new Rune (0x1f692) }), + "👩🏽‍🚒", 6, 4, 7, 1); + PrintTextElementCount (StringExtensions.Make (new Rune [] { new Rune (0x1f469), new Rune (0x1f3fd), new Rune ('\u200d'), new Rune (0x1f692) }), + "\U0001f469\U0001f3fd\u200d\U0001f692", 6, 4, 7, 1); + PrintTextElementCount (StringExtensions.Make (new Rune ('\ud801', '\udccf')), + "\ud801\udccf", 2, 1, 2, 1); + } + + void PrintTextElementCount (string us, string s, int consoleWidth, int runeCount, int stringCount, int txtElementCount) + { + Assert.NotEqual (us.Length, s.Length); + Assert.Equal (us.ToString (), s); + Assert.Equal (consoleWidth, us.ConsoleWidth ()); + Assert.Equal (runeCount, us.RuneCount ()); + Assert.Equal (stringCount, s.Length); + + TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator (s); + + int textElementCount = 0; + while (enumerator.MoveNext ()) { + textElementCount++; // For versions prior to Net5.0 the StringInfo class might handle some grapheme clusters incorrectly. + } + + Assert.Equal (txtElementCount, textElementCount); + } + + [Fact] + public void TestRuneIsLetter () + { + Assert.Equal (5, CountLettersInString ("Hello")); + Assert.Equal (8, CountLettersInString ("𐓏𐓘𐓻𐓘𐓻𐓟 𐒻𐓟")); + } + + int CountLettersInString (string s) + { + int letterCount = 0; + foreach (Rune rune in s) { + if (Rune.IsLetter (rune)) { letterCount++; } + } + + return letterCount; + } + + [Fact] + public void Test_SurrogatePair_From_String () + { + Assert.True (ProcessTestStringUseChar ("𐓏𐓘𐓻𐓘𐓻𐓟 𐒻𐓟")); + Assert.Throws (() => ProcessTestStringUseChar ("\ud801")); + + Assert.True (ProcessStringUseRune ("𐓏𐓘𐓻𐓘𐓻𐓟 𐒻𐓟")); + Assert.Throws (() => ProcessStringUseRune ("\ud801")); + } + + bool ProcessTestStringUseChar (string s) + { + for (int i = 0; i < s.Length; i++) { + Rune r = new Rune (s [i]); + if (!char.IsSurrogate (s [i])) { + var buff = new byte [4]; + ((Rune)s [i]).EncodeRune (buff); + Assert.Equal ((int)s [i], buff [0]); + Assert.Equal (s [i], r.Value); + Assert.True (Rune.IsValid (r.Value)); + Assert.False (r.IsSurrogatePair ()); + } else if (i + 1 < s.Length && char.IsSurrogatePair (s [i], s [i + 1])) { + int codePoint = char.ConvertToUtf32 (s [i], s [i + 1]); + RuneExtensions.EncodeSurrogatePair (s [i], s [i + 1], out Rune rune); + Assert.Equal (codePoint, rune.Value); + string sp = new string (new char [] { s [i], s [i + 1] }); + r = (Rune)codePoint; + Assert.Equal (sp, r.ToString ()); + Assert.True (r.IsSurrogatePair ()); + i++; // Increment the iterator by the number of surrogate pair + } else { + Assert.False (Rune.IsValid (r.Value)); + throw new Exception ("String was not well-formed UTF-16."); + } + } + return true; + } + + bool ProcessStringUseRune (string s) + { + var us = s; + string rs = ""; + Rune codePoint; + List runes = new List (); + int colWidth = 0; + + for (int i = 0; i < s.Length; i++) { + Rune rune = new Rune (s [i]); + if (Rune.IsValid (rune.Value)) { + Assert.True (Rune.IsValid (rune.Value)); + runes.Add (rune); + Assert.Equal (s [i], rune.Value); + Assert.False (rune.IsSurrogatePair ()); + } else if (i + 1 < s.Length && (RuneExtensions.EncodeSurrogatePair (s [i], s [i + 1], out codePoint))) { + Assert.False (Rune.IsValid (rune.Value)); + rune = codePoint; + runes.Add (rune); + string sp = new string (new char [] { s [i], s [i + 1] }); + Assert.Equal (sp, codePoint.ToString ()); + Assert.True (codePoint.IsSurrogatePair ()); + i++; // Increment the iterator by the number of surrogate pair + } else { + Assert.False (Rune.IsValid (rune.Value)); + throw new Exception ("String was not well-formed UTF-16."); + } + colWidth += rune.ColumnWidth (); // Increment the column width of this Rune + rs += rune.ToString (); + } + Assert.Equal (us.ConsoleWidth (), colWidth); + Assert.Equal (s, rs); + Assert.Equal (s, StringExtensions.Make (runes)); + return true; + } + + [Fact] + public void TestSplit () + { + string inputString = "🐂, 🐄, 🐆"; + string [] splitOnSpace = inputString.Split (' '); + string [] splitOnComma = inputString.Split (','); + Assert.Equal (3, splitOnSpace.Length); + Assert.Equal (3, splitOnComma.Length); + } + + [Fact] + public void TestValidRune () + { + Assert.True (Rune.IsValid (new Rune ('\u1100').Value)); + Assert.True (Rune.IsValid (new Rune ('\ud83c', '\udf39').Value)); + Assert.False (Rune.IsValid ('\ud801')); + Assert.False (Rune.IsValid ((uint)'\ud801')); + Assert.False (Rune.IsValid (((Rune)'\ud801').Value)); + } + + [Fact] + public void TestValid () + { + var rune1 = new Rune ('\ud83c', '\udf39'); + var buff1 = new byte [4]; + Assert.Equal (4, rune1.EncodeRune (buff1)); + Assert.True (RuneExtensions.IsValid (buff1)); + Assert.Equal (2, rune1.ToString ().Length); + Assert.Equal (4, rune1.RuneLen ()); + var rune2 = (Rune)'\ud801'; // To avoid throwing an exception set as uint instead a Rune instance. + var buff2 = new byte [4]; + Assert.Equal (3, rune2.EncodeRune (buff2)); + Assert.False (RuneExtensions.IsValid (buff2)); // To avoid throwing an exception pass as uint parameter instead Rune. + Assert.Equal (5, rune2.ToString ().Length); // Invalid string. It returns the decimal 55297 representation of the 0xd801 hexadecimal. + Assert.Equal (-1, rune2.RuneLen ()); + Assert.Equal (3, new Rune ('\ud801').EncodeRune (buff2)); // error + Assert.Equal (new byte [] { 0xef, 0x3f, 0x3d, 0 }, buff2); // error + } + + [Fact] + public void Test_IsNonSpacingChar () + { + Rune l = (Rune)'\u0370'; + Assert.False (l.IsNonSpacingChar ()); + Assert.Equal (1, l.ColumnWidth()); + Assert.Equal (1, StringExtensions.Make (l).ConsoleWidth()); + Rune ns = (Rune)'\u302a'; + Assert.False (ns.IsNonSpacingChar()); + Assert.Equal (2, ns.ColumnWidth()); + Assert.Equal (2, StringExtensions.Make(ns).ConsoleWidth()); + l = (Rune)'\u006f'; + ns = (Rune)'\u0302'; + var s = "\u006f\u0302"; + Assert.Equal (1, l.ColumnWidth()); + Assert.Equal (0, ns.ColumnWidth()); + var ul = StringExtensions.Make (l); + Assert.Equal ("o", ul); + var uns = StringExtensions.Make (ns); + Assert.Equal ("̂", uns); + var f = $"{l}{ns}"; + Assert.Equal ("ô", f); + Assert.Equal (f, s); + Assert.Equal (1, f.ConsoleWidth()); + Assert.Equal (1, s.Sum (c => ((Rune)c).ColumnWidth ())); + Assert.Equal (2, s.Length); + (var rune, var size) = f.DecodeRune (); + Assert.Equal (rune, l); + Assert.Equal (1, size); + l = (Rune)'\u0041'; + ns = (Rune)'\u0305'; + s = "\u0041\u0305"; + Assert.Equal (1, l.ColumnWidth()); + Assert.Equal (0, ns.ColumnWidth()); + ul = StringExtensions.Make (l); + Assert.Equal ("A", ul); + uns = StringExtensions.Make (ns); + Assert.Equal ("̅", uns); + f = $"{l}{ns}"; + Assert.Equal ("A̅", f); + Assert.Equal (f, s); + Assert.Equal (1, f.ConsoleWidth()); + Assert.Equal (1, s.Sum (c => ((Rune)c).ColumnWidth ())); + Assert.Equal (2, s.Length); + (rune, size) = f.DecodeRune (); + Assert.Equal (rune, l); + Assert.Equal (1, size); + l = (Rune)'\u0061'; + ns = (Rune)'\u0308'; + s = "\u0061\u0308"; + Assert.Equal (1, l.ColumnWidth ()); + Assert.Equal (0, ns.ColumnWidth()); + ul = StringExtensions.Make (l); + Assert.Equal ("a", ul); + uns = StringExtensions.Make (ns); + Assert.Equal ("̈", uns); + f = $"{l}{ns}"; + Assert.Equal ("ä", f); + Assert.Equal (f, s); + Assert.Equal (1, f.ConsoleWidth()); + Assert.Equal (1, s.Sum (c => ((Rune)c).ColumnWidth())); + Assert.Equal (2, s.Length); + (rune, size) = f.DecodeRune (); + Assert.Equal (rune, l); + Assert.Equal (1, size); + l = (Rune)'\u4f00'; + ns = (Rune)'\u302a'; + s = "\u4f00\u302a"; + Assert.Equal (2, l.ColumnWidth()); + Assert.Equal (2, ns.ColumnWidth()); + ul = StringExtensions.Make (l); + Assert.Equal ("伀", ul); + uns = StringExtensions.Make (ns); + Assert.Equal ("〪", uns); + f = $"{l}{ns}"; + Assert.Equal ("伀〪", f); // Occupies 4 columns. + Assert.Equal (f, s); + Assert.Equal (4, f.ConsoleWidth()); + Assert.Equal (4, s.Sum (c => ((Rune)c).ColumnWidth())); + Assert.Equal (2, s.Length); + (rune, size) = f.DecodeRune (); + Assert.Equal (rune, l); + Assert.Equal (3, size); + } + + [Fact] + public void Test_IsWideChar () + { + Assert.True (((Rune)0x115e).IsWideChar ()); + Assert.Equal (2, ((Rune)0x115e).ColumnWidth()); + Assert.False (((Rune)0x116f).IsWideChar()); + } + + [Fact] + public void Test_MaxRune () + { + Assert.Throws (() => new Rune (500000000)); + Assert.Throws (() => new Rune ((char)0xf801, (char)0xdfff)); + } + + [Fact] + public void Sum_Of_ColumnWidth_Is_Not_Always_Equal_To_ConsoleWidth () + { + const int start = 0x000000; + const int end = 0x10ffff; + + for (int i = start; i <= end; i++) { + Rune r = new Rune ((uint)i); + if (!r.IsValid()) { + continue; + } + string us = StringExtensions.Make (r); + string hex = i.ToString ("x6"); + int v = int.Parse (hex, System.Globalization.NumberStyles.HexNumber); + string s = char.ConvertFromUtf32 (v); + + if (!r.IsSurrogatePair()) { + Assert.Equal (r.ToString (), us); + Assert.Equal (us, s); + if (r.ColumnWidth () < 0) { + Assert.NotEqual (r.ColumnWidth (), us.ConsoleWidth()); + Assert.NotEqual (s.Sum (c => ((Rune)c).ColumnWidth ()), us.ConsoleWidth()); + } else { + Assert.Equal (r.ColumnWidth (), us.ConsoleWidth()); + Assert.Equal (s.Sum (c => ((Rune)c).ColumnWidth ()), us.ConsoleWidth()); + } + Assert.Equal (us.RuneCount(), s.Length); + } else { + Assert.Equal (r.ToString (), us.ToString ()); + Assert.Equal (us.ToString (), s); + Assert.Equal (r.ColumnWidth (), us.ConsoleWidth()); + Assert.Equal (s.Sum (c => ((Rune)c).ColumnWidth ()), us.ConsoleWidth()); + Assert.Equal (1, us.RuneCount()); // Here returns 1 because is a valid surrogate pair resulting in only rune >=U+10000..U+10FFFF + Assert.Equal (2, s.Length); // String always preserves the originals values of each surrogate pair + } + } + } + + [Fact] + public void Test_Right_To_Left_Runes () + { + Rune r0 = (Rune)0x020000; + Rune r7 = (Rune)0x020007; + Rune r1b = (Rune)0x02001b; + Rune r9b = (Rune)0x02009b; + + Assert.Equal (2, r0.ColumnWidth ()); + Assert.Equal (2, r7.ColumnWidth()); + Assert.Equal (2, r1b.ColumnWidth()); + Assert.Equal (2, r9b.ColumnWidth()); + + "𐨁".DecodeSurrogatePair (out char [] chars); + var rtl = new Rune (chars [0], chars [1]); + var rtlp = new Rune ('\ud802', '\ude01'); + var s = "\U00010a01"; + + Assert.Equal (2, rtl.ColumnWidth()); + Assert.Equal (2, rtlp.ColumnWidth()); + Assert.Equal (2, s.Length); + } + + [Theory] + [InlineData (0x20D0, 0x20EF)] + [InlineData (0x2310, 0x231F)] + [InlineData (0x1D800, 0x1D80F)] + public void Test_Range (int start, int end) + { + for (int i = start; i <= end; i++) { + Rune r = new Rune ((uint)i); + string us = StringExtensions.Make (r); + string hex = i.ToString ("x6"); + int v = int.Parse (hex, System.Globalization.NumberStyles.HexNumber); + string s = char.ConvertFromUtf32 (v); + + if (!r.IsSurrogatePair()) { + Assert.Equal (r.ToString (), us); + Assert.Equal (us, s); + Assert.Equal (r.ColumnWidth (), us.ConsoleWidth()); + Assert.Equal (us.RuneCount(), s.Length); // For not surrogate pairs string.RuneCount is always equal to String.Length + } else { + Assert.Equal (r.ToString (), us); + Assert.Equal (us, s); + Assert.Equal (r.ColumnWidth (), us.ConsoleWidth()); + Assert.Equal (1, us.RuneCount()); // Here returns 1 because is a valid surrogate pair resulting in only rune >=U+10000..U+10FFFF + Assert.Equal (2, s.Length); // String always preserves the originals values of each surrogate pair + } + Assert.Equal (s.Sum (c => ((Rune)c).ColumnWidth ()), us.ConsoleWidth()); + } + } + + [Fact] + public void Test_IsSurrogate () + { + Rune r = (Rune)'\ue0fd'; + Assert.False (r.IsSurrogate()); + Assert.False (char.IsSurrogate (r.ToString (), 0)); + r = (Rune)0x927C0; + Assert.False (r.IsSurrogate()); + Assert.False (char.IsSurrogate (r.ToString (), 0)); + + r = (Rune)'\ud800'; + Assert.True (r.IsSurrogate()); + Assert.True (char.IsSurrogate (r.ToString (), 0)); + r = (Rune)'\udfff'; + Assert.True (r.IsSurrogate()); + Assert.True (char.IsSurrogate(r.ToString(), 0)); + } + + [Fact] + public void Test_EncodeSurrogatePair () + { + Assert.False (RuneExtensions.EncodeSurrogatePair (unchecked((char)0x40D7C0), (char)0xDC20, out Rune rune)); + Assert.False (RuneExtensions.EncodeSurrogatePair ((char)0x0065, (char)0x0301, out _)); + Assert.True (RuneExtensions.EncodeSurrogatePair ('\ud83c', '\udf56', out rune)); + Assert.Equal (0x1F356, rune.Value); + Assert.Equal ("🍖", rune.ToString ()); + } + + [Fact] + public void Test_DecodeSurrogatePair () + { + Assert.False (((Rune)'\uea85').DecodeSurrogatePair (out char [] chars)); + Assert.Null (chars); + Assert.True (((Rune)0x1F356).DecodeSurrogatePair (out chars)); + Assert.Equal (2, chars.Length); + Assert.Equal ('\ud83c', chars [0]); + Assert.Equal ('\udf56', chars [1]); + Assert.Equal ("🍖", new Rune (chars [0], chars [1]).ToString ()); + } + + [Fact] + public void Test_Surrogate_Pairs_Range () + { + for (uint h = 0xd800; h <= 0xdbff; h++) { + for (uint l = 0xdc00; l <= 0xdfff; l++) { + Rune r = new Rune ((char)h, (char)l); + string us = StringExtensions.Make (r); + string hex = r.Value.ToString ("x6"); + int v = int.Parse (hex, System.Globalization.NumberStyles.HexNumber); + string s = char.ConvertFromUtf32 (v); + + Assert.True (v >= 0x10000 && v <= RuneExtensions.MaxRune.Value); + Assert.Equal (r.ToString (), us); + Assert.Equal (us, s); + Assert.Equal (r.ColumnWidth (), us.ConsoleWidth()); + Assert.Equal (s.Sum (c => ((Rune)c).ColumnWidth ()), us.ConsoleWidth()); + Assert.Equal (1, us.RuneCount()); // Here returns 1 because is a valid surrogate pair resulting in only rune >=U+10000..U+10FFFF + Assert.Equal (2, s.Length); // String always preserves the originals values of each surrogate pair + } + } + } + + //[Fact] + //public void Test_ExpectedSizeFromFirstByte () + //{ + // Assert.Equal (-1, Rune.ExpectedSizeFromFirstByte (255)); + // Assert.Equal (1, Rune.ExpectedSizeFromFirstByte (127)); + // Assert.Equal (4, Rune.ExpectedSizeFromFirstByte (240)); + //} + + [Fact] + public void Test_FullRune_Extension () + { + string us = "Hello, 世界"; + //Assert.True (us.FullRune ()); + us = $"Hello, {StringExtensions.Make (new byte [] { 228 })}界"; + //Assert.False (us.FullRune ()); + } + + [Fact] + public void Test_DecodeRune_Extension () + { + string us = "Hello, 世界"; + List runes = new List (); + int tSize = 0; + for (int i = 0; i < us.RuneCount(); i++) { + (Rune rune, int size) = us.Substring (i, 1).DecodeRune (); + runes.Add (rune); + tSize += size; + } + string result = StringExtensions.Make (runes); + Assert.Equal ("Hello, 世界", result); + Assert.Equal (13, tSize); + } + + [Fact] + public void Test_DecodeLastRune_Extension () + { + string us = "Hello, 世界"; + List runes = new List (); + int tSize = 0; + for (int i = us.RuneCount() - 1; i >= 0; i--) { + (Rune rune, int size) = us.Substring (i, 1).DecodeLastRune (); + runes.Add (rune); + tSize += size; + } + string result = StringExtensions.Make (runes); + Assert.Equal ("界世 ,olleH", result); + Assert.Equal (13, tSize); + } + + //[Fact] + //public void Test_InvalidIndex_Extension () + //{ + // string us = "Hello, 世界"; + // Assert.Equal (-1, us.InvalidIndex ()); + // us = string.Make (new byte [] { 0xff, 0xfe, 0xfd }); + // Assert.Equal (0, us.InvalidIndex ()); + //} + + [Fact] + public void Test_Valid_Extension () + { + string us = "Hello, 世界"; + Assert.True (RuneExtensions.IsValid (us.ToByteArray())); + us = StringExtensions.Make (new byte [] { 0xff, 0xfe, 0xfd }); + Assert.False (RuneExtensions.IsValid(us.ToByteArray())); + } + + //[Fact] + //public void Test_ExpectedSizeFromFirstByte_Extension () + //{ + // string us = StringExtensions.Make (255); + // Assert.Equal (-1, us.ExpectedSizeFromFirstByte ()); + // us = StringExtensions.Make (127); + // Assert.Equal (1, us.ExpectedSizeFromFirstByte ()); + // us = StringExtensions.Make (240); + // Assert.Equal (4, us.ExpectedSizeFromFirstByte ()); + //} + + [Fact] + public void Equals_Tests () + { + var a = new List> () { "First line.".ToRuneList () }; + var b = new List> () { "First line.".ToRuneList (), "Second line.".ToRuneList () }; + var c = new List (a [0]); + var d = a [0]; + + Assert.Equal (a [0], b [0]); + // Not the same reference + Assert.False (a [0] == b [0]); + Assert.NotEqual (a [0], b [1]); + Assert.False (a [0] == b [1]); + + Assert.Equal (c, a [0]); + Assert.False (c == a [0]); + Assert.Equal (c, b [0]); + Assert.False (c == b [0]); + Assert.NotEqual (c, b [1]); + Assert.False (c == b [1]); + + Assert.Equal (d, a [0]); + // Is the same reference + Assert.True (d == a [0]); + Assert.Equal (d, b [0]); + Assert.False (d == b [0]); + Assert.NotEqual (d, b [1]); + Assert.False (d == b [1]); + + Assert.True (a [0].SequenceEqual (b [0])); + Assert.False (a [0].SequenceEqual (b [1])); + + Assert.True (c.SequenceEqual (a [0])); + Assert.True (c.SequenceEqual (b [0])); + Assert.False (c.SequenceEqual (b [1])); + + Assert.True (d.SequenceEqual (a [0])); + Assert.True (d.SequenceEqual (b [0])); + Assert.False (d.SequenceEqual (b [1])); + } + + [Fact] + public void Rune_ColumnWidth_Versus_Ustring_ConsoleWidth_With_Non_Printable_Characters () + { + int sumRuneWidth = 0; + int sumConsoleWidth = 0; + for (uint i = 0; i < 32; i++) { + sumRuneWidth += ((Rune)i).ColumnWidth (); + sumConsoleWidth += StringExtensions.Make (i).ConsoleWidth(); + } + + Assert.Equal (-32, sumRuneWidth); + Assert.Equal (0, sumConsoleWidth); + } + + [Fact] + public void Rune_ColumnWidth_Versus_Ustring_ConsoleWidth () + { + string us = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"; + Assert.Equal (200, us.Length); + Assert.Equal (200, us.RuneCount()); + Assert.Equal (200, us.ConsoleWidth()); + int sumRuneWidth = us.Sum (x => ((Rune)x).ColumnWidth ()); + Assert.Equal (200, sumRuneWidth); + + us = "01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789\n"; + Assert.Equal (201, us.Length); + Assert.Equal (201, us.RuneCount()); + Assert.Equal (200, us.ConsoleWidth()); + sumRuneWidth = us.Sum (x => ((Rune)x).ColumnWidth ()); + Assert.Equal (199, sumRuneWidth); + } + + [Fact] + public void Rune_IsHighSurrogate_IsLowSurrogate () + { + Rune r = (Rune)'\ud800'; + Assert.True (r.IsHighSurrogate()); + + r = (Rune)'\udbff'; + Assert.True (r.IsHighSurrogate()); + + r = (Rune)'\udc00'; + Assert.True (r.IsLowSurrogate()); + + r = (Rune)'\udfff'; + Assert.True (r.IsLowSurrogate()); + } + } +}