Chapter 6 - Framework Fundamentals
String and Text Handling
Char
// char literals: char c = 'A'; char newLine = '\n'; // System.Char defines a range of static methods for working with characters: Console.WriteLine (char.ToUpper ('c')); // C Console.WriteLine (char.IsWhiteSpace ('\t')); // True Console.WriteLine (char.IsLetter ('x')); // True Console.WriteLine (char.GetUnicodeCategory ('x')); // LowercaseLetter
ToUpper & ToLower - and the Turkey bug
// ToUpper and ToLower honor the end-user’s locale, which can lead to subtle bugs. // This applies to both char and string. // To illustrate, let's pretend we live in Turkey: Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo ("tr-TR"); // The following expression evaluates to false: Console.WriteLine (char.ToUpper ('i') == 'I'); // Let's see why: Console.WriteLine (char.ToUpper ('i')); // ? // In contrast, the *Invariant methods always apply the same culture: Console.WriteLine (char.ToUpperInvariant ('i')); // I Console.WriteLine (char.ToUpperInvariant ('i') == 'I'); // True
Constructing strings
// String literals: string s1 = "Hello"; string s2 = "First Line\r\nSecond Line"; string s3 = @"\\server\fileshare\helloworld.cs"; // To create a repeating sequence of characters you can use string’s constructor: Console.Write (new string ('*', 10)); // ********** // You can also construct a string from a char array. ToCharArray does the reverse: char[] ca = "Hello".ToCharArray(); string s = new string (ca); // s = "Hello" s.Dump();
Null and Empty Strings
// An empty string has a length of zero: string empty = ""; Console.WriteLine (empty == ""); // True Console.WriteLine (empty == string.Empty); // True Console.WriteLine (empty.Length == 0); // True //Because strings are reference types, they can also be null: string nullString = null; Console.WriteLine (nullString == null); // True Console.WriteLine (nullString == ""); // False Console.WriteLine (string.IsNullOrEmpty (nullString)); // True Console.WriteLine (nullString.Length == 0); // NullReferenceException
Accessing Characaters within a string
string str = "abcde"; char letter = str[1]; // letter == 'b' // string also implements IEnumerable<char>, so you can foreach over its characters: foreach (char c in "123") Console.Write (c + ","); // 1,2,3,
Searching within strings
// The simplest search methods are Contains, StartsWith, and EndsWith: Console.WriteLine ("quick brown fox".Contains ("brown")); // True Console.WriteLine ("quick brown fox".EndsWith ("fox")); // True // IndexOf returns the first position of a given character or substring: Console.WriteLine ("abcde".IndexOf ("cd")); // 2 Console.WriteLine ("abcde".IndexOf ("xx")); // -1 // IndexOf is overloaded to accept a startPosition StringComparison enum, which enables case-insensitive searches: Console.WriteLine ("abcde".IndexOf ("CD", StringComparison.CurrentCultureIgnoreCase)); // 2 // LastIndexOf is like IndexOf, but works backward through the string. // IndexOfAny returns the first matching position of any one of a set of characters: Console.WriteLine ("ab,cd ef".IndexOfAny (new char[] {' ', ','} )); // 2 Console.WriteLine ("pas5w0rd".IndexOfAny ("0123456789".ToCharArray() )); // 3 // LastIndexOfAny does the same in the reverse direction.
Manipulating strings
// Because String is immutable, all the methods below return a new string, leaving the original untouched. // Substring extracts a portion of a string: string left3 = "12345".Substring (0, 3); // left3 = "123"; string mid3 = "12345".Substring (1, 3); // mid3 = "234"; // If you omit the length, you get the remainder of the string: string end3 = "12345".Substring (2); // end3 = "345"; // Insert and Remove insert or remove characters at a specified position: string s1 = "helloworld".Insert (5, ", "); // s1 = "hello, world" string s2 = s1.Remove (5, 2); // s2 = "helloworld"; // PadLeft and PadRight pad a string to a given length with a specified character (or a space if unspecified): Console.WriteLine ("12345".PadLeft (9, '*')); // ****12345 Console.WriteLine ("12345".PadLeft (9)); // 12345 // TrimStart, TrimEnd and Trim remove specified characters (whitespace, by default) from the string: Console.WriteLine (" abc \t\r\n ".Trim().Length); // 3 // Replace replaces all occurrences of a particular character or substring: Console.WriteLine ("to be done".Replace (" ", " | ") ); // to | be | done Console.WriteLine ("to be done".Replace (" ", "") ); // tobedone
Splitting & Joining strings
// Split takes a sentence and returns an array of words (default delimiters = whitespace): string[] words = "The quick brown fox".Split(); words.Dump(); // The static Join method does the reverse of Split: string together = string.Join (" ", words); together.Dump(); // The quick brown fox // The static Concat method accepts only a params string array and applies no separator. // This is exactly equivalent to the + operator: string sentence = string.Concat ("The", " quick", " brown", " fox"); string sameSentence = "The" + " quick" + " brown" + " fox"; sameSentence.Dump(); // The quick brown fox
string.Format and Compostite Format Strings
// When calling String.Format, provide a composite format string followed by each of the embedded variables string composite = "It's {0} degrees in {1} on this {2} morning"; string s = string.Format (composite, 35, "Perth", DateTime.Now.DayOfWeek); s.Dump(); // The minimum width in a format string is useful for aligning columns. // If the value is negative, the data is left-aligned; otherwise, it’s right-aligned: composite = "Name={0,-20} Credit Limit={1,15:C}"; Console.WriteLine (string.Format (composite, "Mary", 500)); Console.WriteLine (string.Format (composite, "Elizabeth", 20000)); // The equivalent without using string.Format: s = "Name=" + "Mary".PadRight (20) + " Credit Limit=" + 500.ToString ("C").PadLeft (15); s.Dump();
Comparing strings
// String comparisons can be ordinal vs culture-sensitive; case-sensitive vs case-insensitive. Console.WriteLine (string.Equals ("foo", "FOO", StringComparison.OrdinalIgnoreCase)); // True // (The following symbols may not be displayed correctly, depending on your font): Console.WriteLine ("?" == "ǖ"); // False // The order comparison methods return a positive number, a negative number, or zero, depending // on whether the first value comes after, before, or alongside the second value: Console.WriteLine ("Boston".CompareTo ("Austin")); // 1 Console.WriteLine ("Boston".CompareTo ("Boston")); // 0 Console.WriteLine ("Boston".CompareTo ("Chicago")); // -1 Console.WriteLine ("?".CompareTo ("ǖ")); // 0 Console.WriteLine ("foo".CompareTo ("FOO")); // -1 // The following performs a case-insensitive comparison using the current culture: Console.WriteLine (string.Compare ("foo", "FOO", true)); // 0 // By supplying a CultureInfo object, you can plug in any alphabet: CultureInfo german = CultureInfo.GetCultureInfo ("de-DE"); int i = string.Compare ("Müller", "Muller", false, german); i.Dump(); // 1
StringBuilder
// Unlike string, StringBuilder is mutable. // The following is more efficient than repeatedly concatenating ordinary string types: StringBuilder sb = new StringBuilder(); for (int i = 0; i < 50; i++) sb.Append (i + ","); // To get the final result, call ToString(): Console.WriteLine (sb.ToString()); sb.Remove (0, 60); // Remove first 50 characters sb.Length = 10; // Truncate to 10 characters sb.Replace (",", "+"); // Replace comma with + sb.ToString().Dump(); sb.Length = 0; // Clear StringBuilder
Text Encodings and Unicode
// The easiest way to instantiate a correctly configured encoding class is to // call Encoding.GetEncoding with a standard IANA name: Encoding utf8 = Encoding.GetEncoding ("utf-8"); Encoding chinese = Encoding.GetEncoding ("GB18030"); utf8.Dump(); chinese.Dump(); // The static GetEncodings method returns a list of all supported encodings: foreach (EncodingInfo info in Encoding.GetEncodings()) Console.WriteLine (info.Name);
Encoding to byte Arrays
byte[] utf8Bytes = System.Text.Encoding.UTF8.GetBytes ("0123456789"); byte[] utf16Bytes = System.Text.Encoding.Unicode.GetBytes ("0123456789"); byte[] utf32Bytes = System.Text.Encoding.UTF32.GetBytes ("0123456789"); Console.WriteLine (utf8Bytes.Length); // 10 Console.WriteLine (utf16Bytes.Length); // 20 Console.WriteLine (utf32Bytes.Length); // 40 string original1 = System.Text.Encoding.UTF8.GetString (utf8Bytes); string original2 = System.Text.Encoding.Unicode.GetString (utf16Bytes); string original3 = System.Text.Encoding.UTF32.GetString (utf32Bytes); Console.WriteLine (original1); // 0123456789 Console.WriteLine (original2); // 0123456789 Console.WriteLine (original3); // 0123456789
UTF-16 and SurrogatePairs
int musicalNote = 0x1D161; string s = char.ConvertFromUtf32 (musicalNote); s.Length.Dump(); // 2 (surrogate pair) char.ConvertToUtf32 (s, 0).ToString ("X").Dump(); // Consumes two chars char.ConvertToUtf32 (s[0], s[1]).ToString ("X").Dump(); // Explicitly specify two chars
Dates and Times
TimeSpan
// There are three ways to construct a TimeSpan: // ? Through one of the constructors // ? By calling one of the static From . . . methods // ? By subtracting one DateTime from another Console.WriteLine (new TimeSpan (2, 30, 0)); // 02:30:00 Console.WriteLine (TimeSpan.FromHours (2.5)); // 02:30:00 Console.WriteLine (TimeSpan.FromHours (-2.5)); // -02:30:00 Console.WriteLine (DateTime.MaxValue - DateTime.MinValue); // TimeSpan overloads the < and > operators, as well as the + and - operators: (TimeSpan.FromHours(2) + TimeSpan.FromMinutes(30)).Dump ("2.5 hours"); (TimeSpan.FromDays(10) - TimeSpan.FromSeconds(1)).Dump ("One second short of 10 days");
TimeSpan - Properties
TimeSpan nearlyTenDays = TimeSpan.FromDays(10) - TimeSpan.FromSeconds(1); // The following properties are all of type int: Console.WriteLine (nearlyTenDays.Days); // 9 Console.WriteLine (nearlyTenDays.Hours); // 23 Console.WriteLine (nearlyTenDays.Minutes); // 59 Console.WriteLine (nearlyTenDays.Seconds); // 59 Console.WriteLine (nearlyTenDays.Milliseconds); // 0 // In contrast, the Total... properties return values of type double describing the entire time span: Console.WriteLine(); Console.WriteLine (nearlyTenDays.TotalDays); // 9.99998842592593 Console.WriteLine (nearlyTenDays.TotalHours); // 239.999722222222 Console.WriteLine (nearlyTenDays.TotalMinutes); // 14399.9833333333 Console.WriteLine (nearlyTenDays.TotalSeconds); // 863999 Console.WriteLine (nearlyTenDays.TotalMilliseconds); // 863999000
Constructing a DateTime or DateTimeOffset
DateTime d1 = new DateTime (2010, 1, 30); // Midnight, January 30 2010 d1.Dump ("d1"); DateTime d2 = new DateTime (2010, 1, 30, 12, 0, 0); // Midday, January 30 2010 d2.Dump ("d2"); d2.Kind.Dump(); DateTime d3 = new DateTime (2010, 1, 30, 12, 0, 0, DateTimeKind.Utc); d3.Dump ("d3"); d3.Kind.Dump(); DateTimeOffset d4 = d1; // Implicit conversion d4.Dump ("d4"); DateTimeOffset d5 = new DateTimeOffset (d1, TimeSpan.FromHours (-8)); // -8 hours UTC d5.Dump ("d5"); // See "Formatting & Parsing" for constructing a DateTime from a string
DateTime - Specifying a Calendar
DateTime d = new DateTime (5767, 1, 1, new System.Globalization.HebrewCalendar()); Console.WriteLine (d); // 12/12/2006 12:00:00 AM
Choosing between DateTime & DateTimeOffset
// (See book)
The Current DateTime or DateTimeOffset
Console.WriteLine (DateTime.Now); Console.WriteLine (DateTimeOffset.Now); Console.WriteLine (DateTime.Today); // No time portion Console.WriteLine (DateTime.UtcNow); Console.WriteLine (DateTimeOffset.UtcNow);
Working with Dates & Times
DateTime dt = new DateTime (2000, 2, 3, 10, 20, 30); Console.WriteLine (dt.Year); // 2000 Console.WriteLine (dt.Month); // 2 Console.WriteLine (dt.Day); // 3 Console.WriteLine (dt.DayOfWeek); // Thursday Console.WriteLine (dt.DayOfYear); // 34 Console.WriteLine (dt.Hour); // 10 Console.WriteLine (dt.Minute); // 20 Console.WriteLine (dt.Second); // 30 Console.WriteLine (dt.Millisecond); // 0 Console.WriteLine (dt.Ticks); // 630851700300000000 Console.WriteLine (dt.TimeOfDay); // 10:20:30 (returns a TimeSpan) TimeSpan ts = TimeSpan.FromMinutes (90); Console.WriteLine (dt.Add (ts)); // 3/02/2000 11:50:30 AM Console.WriteLine (dt + ts); // 3/02/2000 11:50:30 AM DateTime thisYear = new DateTime (2007, 1, 1); DateTime nextYear = thisYear.AddYears (1); TimeSpan oneYear = nextYear - thisYear;
Formatting & Parsing
// The following all honor local culture settings: DateTime.Now.ToString().Dump ("Short date followed by long time"); DateTimeOffset.Now.ToString().Dump ("Short date followed by long time (+ timezone)"); DateTime.Now.ToShortDateString().Dump ("ToShortDateString"); DateTime.Now.ToShortTimeString().Dump ("ToShortTimeString"); DateTime.Now.ToLongDateString().Dump ("ToLongDateString"); DateTime.Now.ToLongTimeString().Dump ("ToLongTimeString"); // Culture-agnostic methods make for reliable formatting & parsing: DateTime dt1 = DateTime.Now; string cannotBeMisparsed = dt1.ToString ("o"); DateTime dt2 = DateTime.Parse (cannotBeMisparsed); dt2.Dump();
Dates and Time Zones
DateTime and Time Zones
// When you compare two DateTime instances, only their ticks values are compared; their DateTimeKinds are ignored: DateTime dt1 = new DateTime (2000, 1, 1, 10, 20, 30, DateTimeKind.Local); DateTime dt2 = new DateTime (2000, 1, 1, 10, 20, 30, DateTimeKind.Utc); Console.WriteLine (dt1 == dt2); // True DateTime local = DateTime.Now; DateTime utc = local.ToUniversalTime(); Console.WriteLine (local == utc); // False // You can construct a DateTime that differs from another only in Kind with the static DateTime.SpecifyKind method: DateTime d = new DateTime (2000, 12, 12); // Unspecified DateTime utc2 = DateTime.SpecifyKind (d, DateTimeKind.Utc); Console.WriteLine (utc2); // 12/12/2000 12:00:00 AM
DateTimeOffset and Time Zones
// Comparisons look only at the (UTC) DateTime; the Offset is used primarily for formatting. DateTimeOffset local = DateTimeOffset.Now; DateTimeOffset utc = local.ToUniversalTime(); Console.WriteLine (local.Offset); // -06:00:00 (in Central America) Console.WriteLine (utc.Offset); // 00:00:00 Console.WriteLine (local == utc); // True //To include the Offset in the comparison, you must use the EqualsExact method: Console.WriteLine (local.EqualsExact (utc)); // False
TimeZone
// The static TimeZone.CurrentTimeZone method returns a TimeZone object based on the current local settings. TimeZone zone = TimeZone.CurrentTimeZone; zone.StandardName.Dump ("StandardName"); zone.DaylightName.Dump ("DaylightName"); // The IsDaylightSavingTime and GetUtcOffset methods work as follows: DateTime dt1 = new DateTime (2019, 1, 1); DateTime dt2 = new DateTime (2019, 6, 1); zone.IsDaylightSavingTime (dt1).Dump ("IsDaylightSavingTime (January)"); zone.IsDaylightSavingTime (dt2).Dump ("IsDaylightSavingTime (June)"); zone.GetUtcOffset (dt1).Dump ("UTC Offset (January)"); zone.GetUtcOffset (dt2).Dump ("UTC Offset (June)"); // The GetDaylightChanges method returns specific daylight saving information for a given year: DaylightTime day = zone.GetDaylightChanges (2019); if (day == null) return; day.Start.Dump ("day.Start"); day.End.Dump ("day.End"); day.Delta.Dump ("day.Delta");
TimeZoneInfo
// TimeZoneInfo.Local returns the current local time zone: TimeZoneInfo zone = TimeZoneInfo.Local; zone.StandardName.Dump ("StandardName (local)"); zone.DaylightName.Dump ("DaylightName (local)"); // You can obtain a TimeZoneInfo for any of the world’s time zones by calling FindSystemTimeZoneById with the zone ID: TimeZoneInfo wa = TimeZoneInfo.FindSystemTimeZoneById ("W. Australia Standard Time"); Console.WriteLine (wa.Id); // W. Australia Standard Time Console.WriteLine (wa.DisplayName); // (GMT+08:00) Perth Console.WriteLine (wa.BaseUtcOffset); // 08:00:00 Console.WriteLine (wa.SupportsDaylightSavingTime); // True Console.WriteLine(); // The following returns all world timezones: foreach (TimeZoneInfo z in TimeZoneInfo.GetSystemTimeZones()) Console.WriteLine (z.Id);
TimeZoneInfo - Adjustment Rules
void Main() { // Western Australia's daylight saving rules are interesting, having introduced daylight // saving midseason in 2006 (and then subsequently rescinding it): TimeZoneInfo wa = TimeZoneInfo.FindSystemTimeZoneById ("W. Australia Standard Time"); foreach (TimeZoneInfo.AdjustmentRule rule in wa.GetAdjustmentRules()) Console.WriteLine ("Rule: applies from " + rule.DateStart + " to " + rule.DateEnd); foreach (TimeZoneInfo.AdjustmentRule rule in wa.GetAdjustmentRules()) { Console.WriteLine(); Console.WriteLine ("Rule: applies from " + rule.DateStart + " to " + rule.DateEnd); Console.WriteLine (" Delta: " + rule.DaylightDelta); Console.WriteLine (" Start: " + FormatTransitionTime (rule.DaylightTransitionStart, false)); Console.WriteLine (" End: " + FormatTransitionTime (rule.DaylightTransitionEnd, true)); } } static string FormatTransitionTime (TimeZoneInfo.TransitionTime tt, bool endTime) { if (endTime && tt.IsFixedDateRule && tt.Day == 1 && tt.Month == 1 && tt.TimeOfDay == DateTime.MinValue) return "-"; string s; if (tt.IsFixedDateRule) s = tt.Day.ToString(); else s = "The " + "first second third fourth last".Split() [tt.Week - 1] + " " + tt.DayOfWeek + " in"; return s + " " + DateTimeFormatInfo.CurrentInfo.MonthNames [tt.Month-1] + " at " + tt.TimeOfDay.TimeOfDay; }
Daylight Saving and DateTime
// The IsDaylightSavingTime tells you whether a given local DateTime is subject to daylight saving. Console.WriteLine (DateTime.Now.IsDaylightSavingTime()); // True or False // UTC times always return false: Console.WriteLine (DateTime.UtcNow.IsDaylightSavingTime()); // Always False // The end of daylight saving presents a particular complication for algorithms that use local time. // The comments on the right show the results of running this in a daylight-saving-enabled zone: DaylightTime changes = TimeZone.CurrentTimeZone.GetDaylightChanges (2010); TimeSpan halfDelta = new TimeSpan (changes.Delta.Ticks / 2); DateTime utc1 = changes.End.ToUniversalTime() - halfDelta; DateTime utc2 = utc1 - changes.Delta; // Converting these variables to local times demonstrates why you should use UTC and not local time // if your code relies on time moving forward: DateTime loc1 = utc1.ToLocalTime(); // (Pacific Standard Time) DateTime loc2 = utc2.ToLocalTime(); Console.WriteLine (loc1); // 2/11/2010 1:30:00 AM Console.WriteLine (loc2); // 2/11/2010 1:30:00 AM Console.WriteLine (loc1 == loc2); // True // Despite loc1 and loc2 reporting as equal, they are different inside: Console.WriteLine (loc1.ToString ("o")); // 2010-11-02T02:30:00.0000000-08:00 Console.WriteLine (loc2.ToString ("o")); // 2010-11-02T02:30:00.0000000-07:00 // The extra bit ensures correct round-tripping between local and UTC times: Console.WriteLine (loc1.ToUniversalTime() == utc1); // True Console.WriteLine (loc2.ToUniversalTime() == utc2); // True
Formatting and Parsing
ToString and Parse
// The simplest formatting mechanism is the ToString method. string s = true.ToString(); s.Dump(); // Parse does the reverse: bool b = bool.Parse (s); b.Dump(); // TryParse avoids a FormatException in case of error: int i; int.TryParse ("qwerty", out i).Dump ("Successful"); int.TryParse ("123", out i).Dump ("Successful"); if (int.TryParse("123", out int j)) { j.Dump("Use j"); } bool validInt = int.TryParse("123", out int _); validInt.Dump("We don't care about the actual value so use discard."); // Culture trap: Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo ("de-DE"); // Germany double.Parse ("1.234").Dump ("Parsing 1.234"); // 1234 // Specifying invariant culture fixes this: double.Parse ("1.234", CultureInfo.InvariantCulture).Dump ("Parsing 1.234 Invariantly"); (1.234).ToString ().Dump ("1.234.ToString()"); (1.234).ToString (CultureInfo.InvariantCulture).Dump ("1.234.ToString Invariant");
Format Providers
// The format string provides instructions; the format provider determines how the instructions are translated: NumberFormatInfo f = new NumberFormatInfo(); f.CurrencySymbol = "$$"; Console.WriteLine (3.ToString ("C", f)); // $$ 3.00 // The default format provider is CultureInfo.CurrentCulture: Console.WriteLine (10.3.ToString ("C", null)); // For convenience, most types overload ToString such that you can omit a null provider: Console.WriteLine (10.3.ToString ("C")); Console.WriteLine (10.3.ToString ("F4")); // (Fix to 4 D.P.)
Format Providers and CultureInfo
// Requesting a specific culture (english language in Great Britain): CultureInfo uk = CultureInfo.GetCultureInfo ("en-GB"); Console.WriteLine (3.ToString ("C", uk)); // £3.00 // Invariant culture: DateTime dt = new DateTime (2000, 1, 2); CultureInfo iv = CultureInfo.InvariantCulture; Console.WriteLine (dt.ToString (iv)); // 01/02/2000 00:00:00 Console.WriteLine (dt.ToString ("d", iv)); // 01/02/2000
Using NumberFormatInfo or DateTimeFormatInfo
// Creating a custom NumberFormatInfo: NumberFormatInfo f = new NumberFormatInfo (); f.NumberGroupSeparator = " "; Console.WriteLine (12345.6789.ToString ("N3", f)); // 12 345.679 // Cloning: NumberFormatInfo f2 = (NumberFormatInfo) CultureInfo.CurrentCulture.NumberFormat.Clone(); // Now we can edit f2: f2.NumberGroupSeparator = "*"; Console.WriteLine (12345.6789.ToString ("N3", f2)); // 12 345.679
Composite Formatting
string composite = "Credit={0:C}"; Console.WriteLine (string.Format (composite, 500)); // Credit=$500.00 Console.WriteLine ("Credit={0:C}", 500); // Credit=$500.00 { object someObject = DateTime.Now; string s = string.Format (CultureInfo.InvariantCulture, "{0}", someObject); s.Dump(); } // Equivalent to: { object someObject = DateTime.Now; string s; if (someObject is IFormattable) s = ((IFormattable)someObject).ToString (null, CultureInfo.InvariantCulture); else if (someObject == null) s = ""; else s = someObject.ToString(); s.Dump(); }
Parsing with Format Providers
// There’s no standard interface for parsing through a format provider; instead use Parse/TryParse methods // on the target types: try { int error = int.Parse ("(2)"); // Exception thrown } catch (FormatException ex) { ex.Dump(); } int minusTwo = int.Parse ("(2)", NumberStyles.Integer | NumberStyles.AllowParentheses); // OK minusTwo.Dump(); decimal fivePointTwo = decimal.Parse ("£5.20", NumberStyles.Currency, CultureInfo.GetCultureInfo ("en-GB")); fivePointTwo.Dump();
IFormatProvider and ICustomFormatter
static void Main() { double n = -123.45; IFormatProvider fp = new WordyFormatProvider(); Console.WriteLine (string.Format (fp, "{0:C} in words is {0:W}", n)); } public class WordyFormatProvider : IFormatProvider, ICustomFormatter { static readonly string[] _numberWords = "zero one two three four five six seven eight nine minus point".Split(); IFormatProvider _parent; // Allows consumers to chain format providers public WordyFormatProvider () : this (CultureInfo.CurrentCulture) { } public WordyFormatProvider (IFormatProvider parent) { _parent = parent; } public object GetFormat (Type formatType) { if (formatType == typeof (ICustomFormatter)) return this; return null; } public string Format (string format, object arg, IFormatProvider prov) { // If it's not our format string, defer to the parent provider: if (arg == null || format != "W") return string.Format (_parent, "{0:" + format + "}", arg); StringBuilder result = new StringBuilder(); string digitList = string.Format (CultureInfo.InvariantCulture, "{0}", arg); foreach (char digit in digitList) { int i = "0123456789-.".IndexOf (digit); if (i == -1) continue; if (result.Length > 0) result.Append (' '); result.Append (_numberWords[i]); } return result.ToString(); } }
Standard Format Strings and Parsing Flags
// (See book)
NumberStyles
int thousand = int.Parse ("3E8", NumberStyles.HexNumber); int minusTwo = int.Parse ("(2)", NumberStyles.Integer | NumberStyles.AllowParentheses); double.Parse ("1,000,000", NumberStyles.Any).Dump ("million"); decimal.Parse ("3e6", NumberStyles.Any).Dump ("3 million"); decimal.Parse ("$5.20", NumberStyles.Currency).Dump ("5.2"); NumberFormatInfo ni = new NumberFormatInfo(); ni.CurrencySymbol = "€"; ni.CurrencyGroupSeparator = " "; double.Parse ("€1 000 000", NumberStyles.Currency, ni).Dump ("million");
Parsing and misparsing DateTimes
// Culture-agnostic: string s = DateTime.Now.ToString ("o"); // ParseExact demands strict compliance with the specified format string: DateTime dt1 = DateTime.ParseExact (s, "o", null); // Parse implicitly accepts both the "o" format and the CurrentCulture format: DateTime dt2 = DateTime.Parse (s); dt1.Dump(); dt2.Dump();
Enum Format Strings
void Main() { foreach (char c in "gfdx") Format (c.ToString()); } void Format (string formatString) { System.ConsoleColor.Red.ToString (formatString).Dump ("ToString (\"" + formatString + "\")"); }
Other Conversion Mechanisms
Convert
double d = 3.9; int i = Convert.ToInt32 (d); i.Dump(); int thirty = Convert.ToInt32 ("1E", 16); // Parse in hexadecimal uint five = Convert.ToUInt32 ("101", 2); // Parse in binary thirty.Dump(); five.Dump(); // Dynamic conversions: Type targetType = typeof (int); object source = "42"; object result = Convert.ChangeType (source, targetType); Console.WriteLine (result); // 42 Console.WriteLine (result.GetType()); // System.Int32 // Base-64 conversions: Convert.ToBase64String (new byte[] { 123, 5, 33, 210 }).Dump(); Convert.FromBase64String ("ewUh0g==").Dump();
XmlConvert
// XmlConvert honors XML formatting rules: string s = XmlConvert.ToString (true); s.Dump(); // true (rather than True) XmlConvert.ToBoolean (s).Dump(); DateTime dt = DateTime.Now; XmlConvert.ToString (dt, XmlDateTimeSerializationMode.Local).Dump ("local"); XmlConvert.ToString (dt, XmlDateTimeSerializationMode.Utc).Dump ("Utc"); XmlConvert.ToString (dt, XmlDateTimeSerializationMode.RoundtripKind).Dump ("RoundtripKind"); XmlConvert.ToString (DateTimeOffset.Now).Dump ("DateTimeOffset");
BitConverter
foreach (byte b in BitConverter.GetBytes (3.5)) Console.Write (b + " "); // 0 0 0 0 0 0 12 64
Type Converters
// Type converters are designed to format and parse in design-time environments. TypeConverter cc = TypeDescriptor.GetConverter (typeof (Color)); Color beige = (Color) cc.ConvertFromString ("Beige"); Color purple = (Color) cc.ConvertFromString ("#800080"); Color window = (Color) cc.ConvertFromString ("Window"); beige.Dump(); purple.Dump(); window.Dump();
Working with Numbers
BigInteger
// BigInteger supports arbitrary precision. BigInteger twentyFive = 25; // implicit cast from integer BigInteger googol = BigInteger.Pow (10, 100); // Alternatively, you can Parse a string: BigInteger googolFromString = BigInteger.Parse ("1".PadRight (101, '0')); Console.WriteLine (googol.ToString()); double g1 = 1e100; // implicit cast BigInteger g2 = (BigInteger) g1; // explicit cast g2.Dump ("Note loss of precision"); // This uses the System.Security.Cryptography namespace: RandomNumberGenerator rand = RandomNumberGenerator.Create(); byte[] bytes = new byte [32]; rand.GetBytes (bytes); var bigRandomNumber = new BigInteger (bytes); // Convert to BigInteger bigRandomNumber.Dump ("Big random number");
Complex Numbers
var c1 = new Complex (2, 3.5); var c2 = new Complex (3, 0); c1.Dump ("c1"); c2.Dump ("c2"); Console.WriteLine (c1.Real); // 2 Console.WriteLine (c1.Imaginary); // 3.5 Console.WriteLine (c1.Phase); // 1.05165021254837 Console.WriteLine (c1.Magnitude); // 4.03112887414927 Complex c3 = Complex.FromPolarCoordinates (1.3, 5); // The standard arithmetic operators are overloaded to work on Complex numbers: Console.WriteLine (c1 + c2); // (5, 3.5) Console.WriteLine (c1 * c2); // (6, 10.5) Complex.Atan (c1).Dump ("Atan"); Complex.Log10 (c1).Dump ("Log10"); Complex.Conjugate (c1).Dump ("Conjugate");
Random
// If given the same seed, the random number series will be the same: Random r1 = new Random (1); Random r2 = new Random (1); Console.WriteLine (r1.Next (100) + ", " + r1.Next (100)); // 24, 11 Console.WriteLine (r2.Next (100) + ", " + r2.Next (100)); // 24, 11 // Using system clock for seed: Random r3 = new Random(); Random r4 = new Random(); Console.WriteLine (r3.Next (100) + ", " + r3.Next (100)); // ?, ? Console.WriteLine (r4.Next (100) + ", " + r4.Next (100)); // ", " // Notice we still get same sequences, because of limitations in system clock resolution. // Here's a workaround: Random r5 = new Random (Guid.NewGuid().GetHashCode()); Random r6 = new Random (Guid.NewGuid().GetHashCode()); Console.WriteLine (r5.Next (100) + ", " + r5.Next (100)); // ?, ? Console.WriteLine (r6.Next (100) + ", " + r6.Next (100)); // ?, ? // Random is not crytographically strong (the following, however, is): var rand = System.Security.Cryptography.RandomNumberGenerator.Create(); byte[] bytes = new byte [4]; rand.GetBytes (bytes); // Fill the byte array with random numbers. BitConverter.ToInt32 (bytes, 0).Dump ("A cryptographically strong random integer");
Enums
Type Unification
// See also Enums in Chapter 3 enum Nut { Walnut, Hazelnut, Macadamia } enum Size { Small, Medium, Large } static void Main() { Display (Nut.Macadamia); // Nut.Macadamia Display (Size.Large); // Size.Large } static void Display (Enum value) // The Enum type unifies all enums { Console.WriteLine (value.GetType().Name + "." + value.ToString()); }
Enum to Integral Conversions
[Flags] public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 } static void Main() { int i = (int) BorderSides.Top; // i == 4 BorderSides side = (BorderSides) i; // side == BorderSides.Top GetIntegralValue (BorderSides.Top).Dump(); GetAnyIntegralValue (BorderSides.Top).Dump(); object result = GetBoxedIntegralValue (BorderSides.Top); Console.WriteLine (result); // 4 Console.WriteLine (result.GetType()); // System.Int32 GetIntegralValueAsString (BorderSides.Top).Dump(); } static int GetIntegralValue (Enum anyEnum) => (int) (object) anyEnum; static decimal GetAnyIntegralValue (Enum anyEnum) => Convert.ToDecimal (anyEnum); static object GetBoxedIntegralValue (Enum anyEnum) { Type integralType = Enum.GetUnderlyingType (anyEnum.GetType()); return Convert.ChangeType (anyEnum, integralType); } static string GetIntegralValueAsString (Enum anyEnum) => anyEnum.ToString ("D"); // returns something like "4"
Integral to enum Conversions
[Flags] public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 } static void Main() { object bs = Enum.ToObject (typeof (BorderSides), 3); Console.WriteLine (bs); // Left, Right //This is the dynamic equivalent of this: BorderSides bs2 = (BorderSides) 3; }
String Conversions
[Flags] public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 } static void Main() { // To string: BorderSides.Right.ToString().Dump(); Enum.Format (typeof (BorderSides), BorderSides.Right, "g").Dump(); // From string: BorderSides leftRight = (BorderSides) Enum.Parse (typeof (BorderSides), "Left, Right"); leftRight.Dump(); BorderSides leftRightCaseInsensitive = (BorderSides) Enum.Parse (typeof (BorderSides), "left, right", true); leftRightCaseInsensitive.Dump(); }
Enumerating enum Values
[Flags] public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 } static void Main() { foreach (Enum value in Enum.GetValues (typeof (BorderSides))) Console.WriteLine (value); }
Tuples
Tuples
// Three ways to create a Tuple: var t1 = new Tuple<int,string> (123, "Hello"); Tuple<int,string> t2 = Tuple.Create (123, "Hello"); var t3 = Tuple.Create (123, "Hello"); t1.Dump(); t2.Dump(); t3.Dump(); Console.WriteLine (t1.Item1 * 2); // 246 Console.WriteLine (t1.Item2.ToUpper()); // HELLO // The alternative sacrafices static type safety and causes boxing with value types: object[] items = { 123, "Hello" }; Console.WriteLine ( ((int) items[0]) * 2 ); // 246 Console.WriteLine ( ((string) items[1]).ToUpper() ); // HELLO
Comparing Tuples
var t1 = Tuple.Create (123, "Hello"); var t2 = Tuple.Create (123, "Hello"); Console.WriteLine (t1 == t2); // False Console.WriteLine (t1.Equals (t2)); // True
The Guid Struct
Guid
Guid g = Guid.NewGuid (); g.ToString().Dump ("Guid.NewGuid.ToString()"); Guid g1 = new Guid ("{0d57629c-7d6e-4847-97cb-9e2fc25083fe}"); Guid g2 = new Guid ("0d57629c7d6e484797cb9e2fc25083fe"); Console.WriteLine (g1 == g2); // True byte[] bytes = g.ToByteArray(); Guid g3 = new Guid (bytes); g3.Dump(); Guid.Empty.Dump ("Guid.Empty"); default(Guid).Dump ("default(Guid)"); Guid.Empty.ToByteArray().Dump ("Guid.Empty - bytes");
Equality Comparison
Value vs Referential Equality
static void Main() { // Simple value equality: int x = 5, y = 5; Console.WriteLine (x == y); // True (by virtue of value equality) // A more elaborate demonstration of value equality: var dt1 = new DateTimeOffset (2010, 1, 1, 1, 1, 1, TimeSpan.FromHours(8)); var dt2 = new DateTimeOffset (2010, 1, 1, 2, 1, 1, TimeSpan.FromHours(9)); Console.WriteLine (dt1 == dt2); // True (same point in time) // Referential equality: Foo f1 = new Foo { X = 5 }; Foo f2 = new Foo { X = 5 }; Console.WriteLine (f1 == f2); // False (different objects) Foo f3 = f1; Console.WriteLine (f1 == f3); // True (same objects) // Customizing classes to exhibit value equality: Uri uri1 = new Uri ("http://www.linqpad.net"); Uri uri2 = new Uri ("http://www.linqpad.net"); Console.WriteLine (uri1 == uri2); // True } class Foo { public int X; }
== and !=
{ int x = 5; int y = 5; Console.WriteLine (x == y); // True } { object x = 5; object y = 5; Console.WriteLine (x == y); // False }
Virtual Equals Method
void Main() { object x = 5; object y = 5; Console.WriteLine (x.Equals (y)); // True Console.WriteLine (AreEqual (x, y)); // True Console.WriteLine (AreEqual (null, null)); // True } // Here's an example of how we can leverage the virtual Equals mehtod: public static bool AreEqual (object obj1, object obj2) { if (obj1 == null) return obj2 == null; return obj1.Equals (obj2); // What we've written is in fact equivalent to the static object.Equals method! }
Static Equals Method
static void Main() { object x = 3, y = 3; Console.WriteLine (object.Equals (x, y)); // True x = null; Console.WriteLine (object.Equals (x, y)); // False y = null; Console.WriteLine (object.Equals (x, y)); // True } // Here's how we can use object.Equals: class Test <T> { T _value; public void SetValue (T newValue) { if (!object.Equals (newValue, _value)) { _value = newValue; OnValueChanged(); } } protected virtual void OnValueChanged() { /*...*/ } }
EqualityComparer
static void Main() { } // A more efficient version of the previous method, when you're dealing with generics: class Test <T> { T _value; public void SetValue (T newValue) { if (!EqualityComparer<T>.Default.Equals (newValue, _value)) { _value = newValue; OnValueChanged(); } } protected virtual void OnValueChanged() { /*...*/ } }
The static ReferenceEquals method
class Widget { // Let's suppose Widget overrides its Equals method and overloads its == operator such // that w1.Equals (w2) would return true if w1 and w2 were different objects. /*...*/ } static void Main() { Widget w1 = new Widget(); Widget w2 = new Widget(); Console.WriteLine (object.ReferenceEquals (w1, w2)); // False }
The IEquatable Interface
class Test<T> where T : IEquatable<T> { public bool IsEqual (T a, T b) => a.Equals (b); // No boxing with generic T } static void Main() { new Test<int>().IsEqual (3, 3).Dump(); }
When Equals and == are not Equal
// With value types, it's quite rare: double x = double.NaN; Console.WriteLine (x == x); // False Console.WriteLine (x.Equals (x)); // True // With reference types, it's more common: var sb1 = new StringBuilder ("foo"); var sb2 = new StringBuilder ("foo"); Console.WriteLine (sb1 == sb2); // False (referential equality) Console.WriteLine (sb1.Equals (sb2)); // True (value equality)
Customizing Equality - Full Example
public struct Area : IEquatable<Area> { public readonly int Measure1; public readonly int Measure2; public Area (int m1, int m2) { Measure1 = Math.Min (m1, m2); Measure2 = Math.Max (m1, m2); } public override bool Equals (object other) { if (!(other is Area)) return false; return Equals ((Area)other); // Calls method below } public bool Equals (Area other) // Implements IEquatable<Area> => Measure1 == other.Measure1 && Measure2 == other.Measure2; public override int GetHashCode() => HashCode.Combine (Measure1, Measure2); public static bool operator == (Area a1, Area a2) => a1.Equals (a2); public static bool operator != (Area a1, Area a2) => !a1.Equals (a2); } static void Main() { Area a1 = new Area (5, 10); Area a2 = new Area (10, 5); Console.WriteLine (a1.Equals (a2)); // True Console.WriteLine (a1 == a2); // True }
Order Comparison
Order Comparison
// The static Array.Sort method works because System.String implements the IComparable interfaces: string[] colors = { "Green", "Red", "Blue" }; Array.Sort (colors); foreach (string c in colors) Console.Write (c + " "); // Blue Green Red
IComparable
// The IComparable interfaces are defined as follows: // public interface IComparable { int CompareTo (object other); } // public interface IComparable<in T> { int CompareTo (T other); } Console.WriteLine ("Beck".CompareTo ("Anne")); // 1 Console.WriteLine ("Beck".CompareTo ("Beck")); // 0 Console.WriteLine ("Beck".CompareTo ("Chris")); // -1
LessThan & GreaterThan operators
// Some types define < and > operators: bool after2010 = DateTime.Now > new DateTime (2010, 1, 1); // The string type doesn't overload these operators (for good reason): bool error = "Beck" > "Anne"; // Compile-time error
Customizing Order Comparision - Full Example
public struct Note : IComparable<Note>, IEquatable<Note>, IComparable { int _semitonesFromA; public int SemitonesFromA => _semitonesFromA; public Note (int semitonesFromA) { _semitonesFromA = semitonesFromA; } public int CompareTo (Note other) // Generic IComparable<T> { if (Equals (other)) return 0; // Fail-safe check return _semitonesFromA.CompareTo (other._semitonesFromA); } int IComparable.CompareTo (object other) // Nongeneric IComparable { if (!(other is Note)) throw new InvalidOperationException ("CompareTo: Not a note"); return CompareTo ((Note) other); } public static bool operator < (Note n1, Note n2) => n1.CompareTo (n2) < 0; public static bool operator > (Note n1, Note n2) => n1.CompareTo (n2) > 0; public bool Equals (Note other) // for IEquatable<Note> => _semitonesFromA == other._semitonesFromA; public override bool Equals (object other) { if (!(other is Note)) return false; return Equals ((Note) other); } public override int GetHashCode() => _semitonesFromA.GetHashCode(); public static bool operator == (Note n1, Note n2) => n1.Equals (n2); public static bool operator != (Note n1, Note n2) => !(n1 == n2); } static void Main() { Note n1 = new Note (1); Note n2 = new Note (2); (n2 > n1).Dump(); }
Utility Classes
Process - Start
Process.Start ("notepad.exe");
ProcessStartInfo
ProcessStartInfo psi = new ProcessStartInfo { FileName = "cmd.exe", Arguments = "/c ipconfig /all", RedirectStandardOutput = true, UseShellExecute = false }; Process p = Process.Start (psi); string result = p.StandardOutput.ReadToEnd(); Console.WriteLine (result);
Process - Capturing output and error streams
void Main() { var test1 = Run ("ipconfig.exe"); test1.output.Dump ("Output"); test1.errors.Dump ("Errors"); } (string output, string errors) Run (string exePath, string args = "") { using var p = Process.Start (new ProcessStartInfo (exePath, args) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, }); var errors = new StringBuilder (); // Read from the error stream asynchronously... p.ErrorDataReceived += (sender, errorArgs) => { if (errorArgs.Data != null) errors.AppendLine (errorArgs.Data); }; p.BeginErrorReadLine (); // ...while we read from the output stream synchronously: string output = p.StandardOutput.ReadToEnd(); p.WaitForExit(); return (output, errors.ToString()); }
Opening a file or URL in Windows and Linux
void Main() { LaunchFileOrUrl ("http://www.albahari.com/nutshell"); } void LaunchFileOrUrl (string url) { if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) Process.Start ("xdg-open", url); else if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) Process.Start (new ProcessStartInfo (url) { UseShellExecute = true }); else throw new NotSupportedException ("Platform unsupported."); }