Cache Invariant CompareInfo (#15902)
[platform/upstream/coreclr.git] / src / mscorlib / shared / System / Globalization / TextInfo.cs
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4
5 ////////////////////////////////////////////////////////////////////////////
6 //
7 //
8 //  Purpose:  This Class defines behaviors specific to a writing system.
9 //            A writing system is the collection of scripts and
10 //            orthographic rules required to represent a language as text.
11 //
12 //
13 ////////////////////////////////////////////////////////////////////////////
14
15 using System.Diagnostics;
16 using System.Runtime.Serialization;
17 using System.Text;
18
19 namespace System.Globalization
20 {
21     public partial class TextInfo : ICloneable, IDeserializationCallback
22     {
23         private enum Tristate : byte
24         {
25             NotInitialized,
26             True,
27             False,
28         }
29
30         private string _listSeparator;
31         private bool _isReadOnly = false;
32
33         /*    _cultureName is the name of the creating culture.
34               _cultureData is the data that backs this class.
35               _textInfoName is the actual name of the textInfo (from cultureData.STEXTINFO)
36                       In the desktop, when we call the sorting dll, it doesn't
37                       know how to resolve custom locle names to sort ids so we have to have already resolved this.
38         */
39
40         private readonly string _cultureName;      // Name of the culture that created this text info
41         private readonly CultureData _cultureData; // Data record for the culture that made us, not for this textinfo
42         private readonly string _textInfoName;     // Name of the text info we're using (ie: _cultureData.STEXTINFO)
43
44         private Tristate _isAsciiCasingSameAsInvariant = Tristate.NotInitialized;
45
46         // _invariantMode is defined for the perf reason as accessing the instance field is faster than access the static property GlobalizationMode.Invariant
47         private readonly bool _invariantMode = GlobalizationMode.Invariant;
48
49         // Invariant text info
50         internal static TextInfo Invariant
51         {
52             get
53             {
54                 if (s_Invariant == null)
55                     s_Invariant = new TextInfo(CultureData.Invariant);
56                 return s_Invariant;
57             }
58         }
59         internal volatile static TextInfo s_Invariant;
60
61         //////////////////////////////////////////////////////////////////////////
62         ////
63         ////  TextInfo Constructors
64         ////
65         ////  Implements CultureInfo.TextInfo.
66         ////
67         //////////////////////////////////////////////////////////////////////////
68         internal TextInfo(CultureData cultureData)
69         {
70             // This is our primary data source, we don't need most of the rest of this
71             _cultureData = cultureData;
72             _cultureName = _cultureData.CultureName;
73             _textInfoName = _cultureData.STEXTINFO;
74
75             FinishInitialization();
76         }
77
78         void IDeserializationCallback.OnDeserialization(Object sender)
79         {
80             throw new PlatformNotSupportedException();
81         }
82
83         //
84         // Internal ordinal comparison functions
85         //
86
87         internal static int GetHashCodeOrdinalIgnoreCase(string s)
88         {
89             // This is the same as an case insensitive hash for Invariant
90             // (not necessarily true for sorting, but OK for casing & then we apply normal hash code rules)
91             return Invariant.GetCaseInsensitiveHashCode(s);
92         }
93
94         public virtual int ANSICodePage => _cultureData.IDEFAULTANSICODEPAGE;
95
96         public virtual int OEMCodePage => _cultureData.IDEFAULTOEMCODEPAGE;
97
98         public virtual int MacCodePage => _cultureData.IDEFAULTMACCODEPAGE;
99
100         public virtual int EBCDICCodePage => _cultureData.IDEFAULTEBCDICCODEPAGE;
101
102         // Just use the LCID from our text info name
103         public int LCID => CultureInfo.GetCultureInfo(_textInfoName).LCID;
104
105         public string CultureName => _textInfoName;
106
107         public bool IsReadOnly => _isReadOnly;
108
109         //////////////////////////////////////////////////////////////////////////
110         ////
111         ////  Clone
112         ////
113         ////  Is the implementation of ICloneable.
114         ////
115         //////////////////////////////////////////////////////////////////////////
116         public virtual object Clone()
117         {
118             object o = MemberwiseClone();
119             ((TextInfo)o).SetReadOnlyState(false);
120             return o;
121         }
122
123         ////////////////////////////////////////////////////////////////////////
124         //
125         //  ReadOnly
126         //
127         //  Create a cloned readonly instance or return the input one if it is 
128         //  readonly.
129         //
130         ////////////////////////////////////////////////////////////////////////
131         public static TextInfo ReadOnly(TextInfo textInfo)
132         {
133             if (textInfo == null) { throw new ArgumentNullException(nameof(textInfo)); }
134             if (textInfo.IsReadOnly) { return textInfo; }
135
136             TextInfo clonedTextInfo = (TextInfo)(textInfo.MemberwiseClone());
137             clonedTextInfo.SetReadOnlyState(true);
138
139             return clonedTextInfo;
140         }
141
142         private void VerifyWritable()
143         {
144             if (_isReadOnly)
145             {
146                 throw new InvalidOperationException(SR.InvalidOperation_ReadOnly);
147             }
148         }
149
150         internal void SetReadOnlyState(bool readOnly)
151         {
152             _isReadOnly = readOnly;
153         }
154
155
156         ////////////////////////////////////////////////////////////////////////
157         //
158         //  ListSeparator
159         //
160         //  Returns the string used to separate items in a list.
161         //
162         ////////////////////////////////////////////////////////////////////////
163         public virtual string ListSeparator
164         {
165             get
166             {
167                 if (_listSeparator == null)
168                 {
169                     _listSeparator = _cultureData.SLIST;
170                 }
171                 return _listSeparator;
172             }
173
174             set
175             {
176                 if (value == null)
177                 {
178                     throw new ArgumentNullException(nameof(value), SR.ArgumentNull_String);
179                 }
180                 VerifyWritable();
181                 _listSeparator = value;
182             }
183         }
184
185         ////////////////////////////////////////////////////////////////////////
186         //
187         //  ToLower
188         //
189         //  Converts the character or string to lower case.  Certain locales
190         //  have different casing semantics from the file systems in Win32.
191         //
192         ////////////////////////////////////////////////////////////////////////
193         public unsafe virtual char ToLower(char c)
194         {
195             if (_invariantMode || (IsAscii(c) && IsAsciiCasingSameAsInvariant))
196             {
197                 return ToLowerAsciiInvariant(c);
198             }
199
200             return ChangeCase(c, toUpper: false);
201         }
202
203         public unsafe virtual string ToLower(string str)
204         {
205             if (str == null) { throw new ArgumentNullException(nameof(str)); }
206
207             if (_invariantMode)
208             {
209                 return ToLowerAsciiInvariant(str);
210             }
211
212             return ChangeCase(str, toUpper: false);
213         }
214
215         private unsafe string ToLowerAsciiInvariant(string s)
216         {
217             if (s.Length == 0)
218             {
219                 return string.Empty;
220             }
221             
222             fixed (char* pSource = s)
223             {
224                 int i = 0;
225                 while (i < s.Length)
226                 {
227                     if ((uint)(pSource[i] - 'A') <= (uint)('Z' - 'A'))
228                     {
229                         break;
230                     }
231                     i++;
232                 }
233                 
234                 if (i >= s.Length)
235                 {
236                     return s;
237                 }
238
239                 string result = string.FastAllocateString(s.Length);
240                 fixed (char* pResult = result)
241                 {
242                     for (int j = 0; j < i; j++)
243                     {
244                         pResult[j] = pSource[j];
245                     }
246                     
247                     pResult[i] = (char)(pSource[i] | 0x20);
248                     i++;
249
250                     while (i < s.Length)
251                     {
252                         pResult[i] = ToLowerAsciiInvariant(pSource[i]);
253                         i++;
254                     }
255                 }
256
257                 return result;
258             }
259         }
260
261         private unsafe string ToUpperAsciiInvariant(string s)
262         {
263             if (s.Length == 0)
264             {
265                 return string.Empty;
266             }
267             
268             fixed (char* pSource = s)
269             {
270                 int i = 0;
271                 while (i < s.Length)
272                 {
273                     if ((uint)(pSource[i] - 'a') <= (uint)('z' - 'a'))
274                     {
275                         break;
276                     }
277                     i++;
278                 }
279                 
280                 if (i >= s.Length)
281                 {
282                     return s;
283                 }
284
285                 string result = string.FastAllocateString(s.Length);
286                 fixed (char* pResult = result)
287                 {
288                     for (int j = 0; j < i; j++)
289                     {
290                         pResult[j] = pSource[j];
291                     }
292                     
293                     pResult[i] = (char)(pSource[i] & ~0x20);
294                     i++;
295
296                     while (i < s.Length)
297                     {
298                         pResult[i] = ToUpperAsciiInvariant(pSource[i]);
299                         i++;
300                     }
301                 }
302
303                 return result;
304             }
305         }
306
307         private static char ToLowerAsciiInvariant(char c)
308         {
309             if ((uint)(c - 'A') <= (uint)('Z' - 'A'))
310             {
311                 c = (char)(c | 0x20);
312             }
313             return c;
314         }
315
316         ////////////////////////////////////////////////////////////////////////
317         //
318         //  ToUpper
319         //
320         //  Converts the character or string to upper case.  Certain locales
321         //  have different casing semantics from the file systems in Win32.
322         //
323         ////////////////////////////////////////////////////////////////////////
324         public unsafe virtual char ToUpper(char c)
325         {
326             if (_invariantMode || (IsAscii(c) && IsAsciiCasingSameAsInvariant))
327             {
328                 return ToUpperAsciiInvariant(c);
329             }
330             
331             return ChangeCase(c, toUpper: true);
332         }
333
334         public unsafe virtual string ToUpper(string str)
335         {
336             if (str == null) { throw new ArgumentNullException(nameof(str)); }
337
338             if (_invariantMode)
339             {
340                 return ToUpperAsciiInvariant(str);
341             }
342
343             return ChangeCase(str, toUpper: true);
344         }
345
346         internal static char ToUpperAsciiInvariant(char c)
347         {
348             if ((uint)(c - 'a') <= (uint)('z' - 'a'))
349             {
350                 c = (char)(c & ~0x20);
351             }
352             return c;
353         }
354
355         private static bool IsAscii(char c)
356         {
357             return c < 0x80;
358         }
359
360         private bool IsAsciiCasingSameAsInvariant
361         {
362             get
363             {
364                 if (_isAsciiCasingSameAsInvariant == Tristate.NotInitialized)
365                 {
366                     _isAsciiCasingSameAsInvariant = CultureInfo.GetCultureInfo(_textInfoName).CompareInfo.Compare("abcdefghijklmnopqrstuvwxyz",
367                                                                              "ABCDEFGHIJKLMNOPQRSTUVWXYZ",
368                                                                              CompareOptions.IgnoreCase) == 0 ? Tristate.True : Tristate.False;
369                 }
370                 return _isAsciiCasingSameAsInvariant == Tristate.True;
371             }
372         }
373
374         // IsRightToLeft
375         //
376         // Returns true if the dominant direction of text and UI such as the relative position of buttons and scroll bars
377         //
378         public bool IsRightToLeft => _cultureData.IsRightToLeft;
379
380         ////////////////////////////////////////////////////////////////////////
381         //
382         //  Equals
383         //
384         //  Implements Object.Equals().  Returns a boolean indicating whether
385         //  or not object refers to the same CultureInfo as the current instance.
386         //
387         ////////////////////////////////////////////////////////////////////////
388         public override bool Equals(Object obj)
389         {
390             TextInfo that = obj as TextInfo;
391
392             if (that != null)
393             {
394                 return CultureName.Equals(that.CultureName);
395             }
396
397             return false;
398         }
399
400         ////////////////////////////////////////////////////////////////////////
401         //
402         //  GetHashCode
403         //
404         //  Implements Object.GetHashCode().  Returns the hash code for the
405         //  CultureInfo.  The hash code is guaranteed to be the same for CultureInfo A
406         //  and B where A.Equals(B) is true.
407         //
408         ////////////////////////////////////////////////////////////////////////
409         public override int GetHashCode()
410         {
411             return CultureName.GetHashCode();
412         }
413
414         ////////////////////////////////////////////////////////////////////////
415         //
416         //  ToString
417         //
418         //  Implements Object.ToString().  Returns a string describing the
419         //  TextInfo.
420         //
421         ////////////////////////////////////////////////////////////////////////
422         public override string ToString()
423         {
424             return "TextInfo - " + _cultureData.CultureName;
425         }
426
427         //
428         // Titlecasing:
429         // -----------
430         // Titlecasing refers to a casing practice wherein the first letter of a word is an uppercase letter
431         // and the rest of the letters are lowercase.  The choice of which words to titlecase in headings
432         // and titles is dependent on language and local conventions.  For example, "The Merry Wives of Windor"
433         // is the appropriate titlecasing of that play's name in English, with the word "of" not titlecased.
434         // In German, however, the title is "Die lustigen Weiber von Windsor," and both "lustigen" and "von"
435         // are not titlecased.  In French even fewer words are titlecased: "Les joyeuses commeres de Windsor."
436         //
437         // Moreover, the determination of what actually constitutes a word is language dependent, and this can
438         // influence which letter or letters of a "word" are uppercased when titlecasing strings.  For example
439         // "l'arbre" is considered two words in French, whereas "can't" is considered one word in English.
440         //
441         public unsafe string ToTitleCase(string str)
442         {
443             if (str == null)
444             {
445                 throw new ArgumentNullException(nameof(str));
446             }
447             if (str.Length == 0)
448             {
449                 return str;
450             }
451
452             StringBuilder result = new StringBuilder();
453             string lowercaseData = null;
454             // Store if the current culture is Dutch (special case)
455             bool isDutchCulture = CultureName.StartsWith("nl-", StringComparison.OrdinalIgnoreCase);
456
457             for (int i = 0; i < str.Length; i++)
458             {
459                 UnicodeCategory charType;
460                 int charLen;
461
462                 charType = CharUnicodeInfo.InternalGetUnicodeCategory(str, i, out charLen);
463                 if (char.CheckLetter(charType))
464                 {
465                     // Special case to check for Dutch specific titlecasing with "IJ" characters 
466                     // at the beginning of a word
467                     if (isDutchCulture && i < str.Length - 1 && (str[i] == 'i' || str[i] == 'I') && (str[i+1] == 'j' || str[i+1] == 'J'))
468                     {
469                         result.Append("IJ");
470                         i += 2;
471                     }
472                     else
473                     {
474                         // Do the titlecasing for the first character of the word.
475                         i = AddTitlecaseLetter(ref result, ref str, i, charLen) + 1;
476                     }
477
478                     //
479                     // Convert the characters until the end of the this word
480                     // to lowercase.
481                     //
482                     int lowercaseStart = i;
483
484                     //
485                     // Use hasLowerCase flag to prevent from lowercasing acronyms (like "URT", "USA", etc)
486                     // This is in line with Word 2000 behavior of titlecasing.
487                     //
488                     bool hasLowerCase = (charType == UnicodeCategory.LowercaseLetter);
489                     // Use a loop to find all of the other letters following this letter.
490                     while (i < str.Length)
491                     {
492                         charType = CharUnicodeInfo.InternalGetUnicodeCategory(str, i, out charLen);
493                         if (IsLetterCategory(charType))
494                         {
495                             if (charType == UnicodeCategory.LowercaseLetter)
496                             {
497                                 hasLowerCase = true;
498                             }
499                             i += charLen;
500                         }
501                         else if (str[i] == '\'')
502                         {
503                             i++;
504                             if (hasLowerCase)
505                             {
506                                 if (lowercaseData == null)
507                                 {
508                                     lowercaseData = ToLower(str);
509                                 }
510                                 result.Append(lowercaseData, lowercaseStart, i - lowercaseStart);
511                             }
512                             else
513                             {
514                                 result.Append(str, lowercaseStart, i - lowercaseStart);
515                             }
516                             lowercaseStart = i;
517                             hasLowerCase = true;
518                         }
519                         else if (!IsWordSeparator(charType))
520                         {
521                             // This category is considered to be part of the word.
522                             // This is any category that is marked as false in wordSeprator array.
523                             i+= charLen;
524                         }
525                         else
526                         {
527                             // A word separator. Break out of the loop.
528                             break;
529                         }
530                     }
531
532                     int count = i - lowercaseStart;
533
534                     if (count > 0)
535                     {
536                         if (hasLowerCase)
537                         {
538                             if (lowercaseData == null)
539                             {
540                                 lowercaseData = ToLower(str);
541                             }
542                             result.Append(lowercaseData, lowercaseStart, count);
543                         }
544                         else
545                         {
546                             result.Append(str, lowercaseStart, count);
547                         }
548                     }
549
550                     if (i < str.Length)
551                     {
552                         // not a letter, just append it
553                         i = AddNonLetter(ref result, ref str, i, charLen);
554                     }
555                 }
556                 else
557                 {
558                     // not a letter, just append it
559                     i = AddNonLetter(ref result, ref str, i, charLen);
560                 }
561             }
562             return result.ToString();
563         }
564
565         private static int AddNonLetter(ref StringBuilder result, ref string input, int inputIndex, int charLen)
566         {
567             Debug.Assert(charLen == 1 || charLen == 2, "[TextInfo.AddNonLetter] CharUnicodeInfo.InternalGetUnicodeCategory returned an unexpected charLen!");
568             if (charLen == 2)
569             {
570                 // Surrogate pair
571                 result.Append(input[inputIndex++]);
572                 result.Append(input[inputIndex]);
573             }
574             else
575             {
576                 result.Append(input[inputIndex]);
577             }
578             return inputIndex;
579         }
580
581         private int AddTitlecaseLetter(ref StringBuilder result, ref string input, int inputIndex, int charLen)
582         {
583             Debug.Assert(charLen == 1 || charLen == 2, "[TextInfo.AddTitlecaseLetter] CharUnicodeInfo.InternalGetUnicodeCategory returned an unexpected charLen!");
584
585             // for surrogate pairs do a simple ToUpper operation on the substring
586             if (charLen == 2)
587             {
588                 // Surrogate pair
589                 result.Append(ToUpper(input.Substring(inputIndex, charLen)));
590                 inputIndex++;
591             }
592             else
593             {
594                 switch (input[inputIndex])
595                 {
596                     //
597                     // For AppCompat, the Titlecase Case Mapping data from NDP 2.0 is used below.
598                     case (char) 0x01C4:  // DZ with Caron -> Dz with Caron
599                     case (char) 0x01C5:  // Dz with Caron -> Dz with Caron
600                     case (char) 0x01C6:  // dz with Caron -> Dz with Caron
601                         result.Append((char) 0x01C5);
602                         break;
603                     case (char) 0x01C7:  // LJ -> Lj
604                     case (char) 0x01C8:  // Lj -> Lj
605                     case (char) 0x01C9:  // lj -> Lj
606                         result.Append((char) 0x01C8);
607                         break;
608                     case (char) 0x01CA:  // NJ -> Nj
609                     case (char) 0x01CB:  // Nj -> Nj
610                     case (char) 0x01CC:  // nj -> Nj
611                         result.Append((char) 0x01CB);
612                         break;
613                     case (char) 0x01F1:  // DZ -> Dz
614                     case (char) 0x01F2:  // Dz -> Dz
615                     case (char) 0x01F3:  // dz -> Dz
616                         result.Append((char) 0x01F2);
617                         break;
618                     default:
619                         result.Append(ToUpper(input[inputIndex]));
620                         break;
621                 }
622             }
623             return inputIndex;
624         }
625
626         //
627         // Used in ToTitleCase():
628         // When we find a starting letter, the following array decides if a category should be
629         // considered as word seprator or not.
630         //
631         private const int c_wordSeparatorMask = 
632             /* false */ (0 <<  0) | // UppercaseLetter = 0,
633             /* false */ (0 <<  1) | // LowercaseLetter = 1,
634             /* false */ (0 <<  2) | // TitlecaseLetter = 2,
635             /* false */ (0 <<  3) | // ModifierLetter = 3,
636             /* false */ (0 <<  4) | // OtherLetter = 4,
637             /* false */ (0 <<  5) | // NonSpacingMark = 5,
638             /* false */ (0 <<  6) | // SpacingCombiningMark = 6,
639             /* false */ (0 <<  7) | // EnclosingMark = 7,
640             /* false */ (0 <<  8) | // DecimalDigitNumber = 8,
641             /* false */ (0 <<  9) | // LetterNumber = 9,
642             /* false */ (0 << 10) | // OtherNumber = 10,
643             /* true  */ (1 << 11) | // SpaceSeparator = 11,
644             /* true  */ (1 << 12) | // LineSeparator = 12,
645             /* true  */ (1 << 13) | // ParagraphSeparator = 13,
646             /* true  */ (1 << 14) | // Control = 14,
647             /* true  */ (1 << 15) | // Format = 15,
648             /* false */ (0 << 16) | // Surrogate = 16,
649             /* false */ (0 << 17) | // PrivateUse = 17,
650             /* true  */ (1 << 18) | // ConnectorPunctuation = 18,
651             /* true  */ (1 << 19) | // DashPunctuation = 19,
652             /* true  */ (1 << 20) | // OpenPunctuation = 20,
653             /* true  */ (1 << 21) | // ClosePunctuation = 21,
654             /* true  */ (1 << 22) | // InitialQuotePunctuation = 22,
655             /* true  */ (1 << 23) | // FinalQuotePunctuation = 23,
656             /* true  */ (1 << 24) | // OtherPunctuation = 24,
657             /* true  */ (1 << 25) | // MathSymbol = 25,
658             /* true  */ (1 << 26) | // CurrencySymbol = 26,
659             /* true  */ (1 << 27) | // ModifierSymbol = 27,
660             /* true  */ (1 << 28) | // OtherSymbol = 28,
661             /* false */ (0 << 29);  // OtherNotAssigned = 29;
662         
663         private static bool IsWordSeparator(UnicodeCategory category) 
664         {
665             return (c_wordSeparatorMask & (1 << (int) category)) != 0;
666         }
667
668         private static bool IsLetterCategory(UnicodeCategory uc)
669         {
670             return (uc == UnicodeCategory.UppercaseLetter
671                  || uc == UnicodeCategory.LowercaseLetter
672                  || uc == UnicodeCategory.TitlecaseLetter
673                  || uc == UnicodeCategory.ModifierLetter
674                  || uc == UnicodeCategory.OtherLetter);
675         }
676
677         //
678         // Get case-insensitive hash code for the specified string.
679         //
680         internal unsafe int GetCaseInsensitiveHashCode(string str)
681         {
682             // Validate inputs
683             if (str == null)
684             {
685                 throw new ArgumentNullException(nameof(str));
686             }
687
688             // This code assumes that ASCII casing is safe for whatever context is passed in.
689             // this is true today, because we only ever call these methods on Invariant.  It would be ideal to refactor
690             // these methods so they were correct by construction and we could only ever use Invariant.
691
692             uint hash = 5381;
693             uint c;
694
695             // Note: We assume that str contains only ASCII characters until
696             // we hit a non-ASCII character to optimize the common case.
697             for (int i = 0; i < str.Length; i++)
698             {
699                 c = str[i];
700                 if (c >= 0x80)
701                 {
702                     return GetCaseInsensitiveHashCodeSlow(str);
703                 }
704
705                 // If we have a lowercase character, ANDing off 0x20
706                 // will make it an uppercase character.
707                 if ((c - 'a') <= ('z' - 'a'))
708                 {
709                     c = (uint)((int)c & ~0x20);
710                 }
711
712                 hash = ((hash << 5) + hash) ^ c;
713             }
714
715             return (int)hash;
716         }
717
718         private unsafe int GetCaseInsensitiveHashCodeSlow(string str)
719         {
720             Debug.Assert(str != null);
721
722             string upper = ToUpper(str);
723
724             uint hash = 5381;
725             uint c;
726
727             for (int i = 0; i < upper.Length; i++)
728             {
729                 c = upper[i];
730                 hash = ((hash << 5) + hash) ^ c;
731             }
732
733             return (int)hash;
734         }
735     }
736 }