Chapter 6 - .NET 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

Encoding.UTF8.Dump();

// Call Encoding.GetEncoding with a standard IANA name to obtain an encoding:
Encoding.RegisterProvider (CodePagesEncodingProvider.Instance);
Encoding chinese = Encoding.GetEncoding ("GB18030");
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

TimeZoneInfo

// TimeZoneInfo.Local returns the current local time zone:
TimeZoneInfo zone = TimeZoneInfo.Local;
zone.StandardName.Dump ("StandardName (local)");
zone.DaylightName.Dump ("DaylightName (local)");

DateTime dt1 = new DateTime (2019, 1, 1);
DateTime dt2 = new DateTime (2019, 6, 1);
Console.WriteLine (zone.IsDaylightSavingTime (dt1));
Console.WriteLine (zone.IsDaylightSavingTime (dt2));
Console.WriteLine (zone.GetUtcOffset (dt1));
Console.WriteLine (zone.GetUtcOffset (dt2));

// 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

// 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));
}

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
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

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

foreach (char c in "gfdx")
  Format (c.ToString());

void Format (string formatString)
{
  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");

Half

Half h = (Half) 123.456;
Console.WriteLine (h);     // 123.44

Console.WriteLine (Half.MinValue);   // -65500
Console.WriteLine (Half.MaxValue);   // 65500

Console.WriteLine ((Half)65500);     // 65500
Console.WriteLine ((Half)65490);     // 65500
Console.WriteLine ((Half)65480);     // 65470

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

object bs = Enum.ToObject (typeof (BorderSides), 3);
Console.WriteLine (bs);                              // Left, Right

//This is the dynamic equivalent of this:
BorderSides bs2 = (BorderSides)3;

[Flags] public enum BorderSides { Left = 1, Right = 2, Top = 4, Bottom = 8 }

String Conversions

// 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();

[Flags] public enum BorderSides { Left = 1, Right = 2, Top = 4, Bottom = 8 }

Enumerating enum Values

foreach (Enum value in Enum.GetValues (typeof (BorderSides)))
  Console.WriteLine (value);

[Flags] public enum BorderSides { Left = 1, Right = 2, Top = 4, Bottom = 8 }
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

// 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

// Here's an example of how we can leverage the virtual Equals mehtod:
object x = 5;
object y = 5;
Console.WriteLine (x.Equals (y));      // True

Console.WriteLine (AreEqual (x, y));    // True
Console.WriteLine (AreEqual (null, null));  // True

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

// Here's how we can use object.Equals:
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

class Test<T>
{
  T _value;
  public void SetValue (T newValue)
  {
    if (!object.Equals (newValue, _value))
    {
      _value = newValue;
      OnValueChanged();
    }
  }

  protected virtual void OnValueChanged() { /*...*/ }
}

EqualityComparer

// 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

Widget w1 = new Widget();
Widget w2 = new Widget();
Console.WriteLine (object.ReferenceEquals (w1, w2));     // False

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.
  /*...*/
}

The IEquatable Interface

new Test<int>().IsEqual (3, 3).Dump();

class Test<T> where T : IEquatable<T>
{
  public bool IsEqual (T a, T b) =>  a.Equals (b);     // No boxing with generic T
}

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

Area a1 = new Area (5, 10);
Area a2 = new Area (10, 5);
Console.WriteLine (a1.Equals (a2));    // True
Console.WriteLine (a1 == a2);          // True

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);
}
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

Note n1 = new Note (1);
Note n2 = new Note (2);
(n2 > n1).Dump();

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);
}
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

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

LaunchFileOrUrl ("http://www.albahari.com/nutshell");

void LaunchFileOrUrl (string url)
{
    Process.Start (new ProcessStartInfo (url) { UseShellExecute = true });
}
C# 12 in a Nutshell
Buy from amazon.com Buy print or Kindle edition
Buy from ebooks.com Buy PDF edition
Buy from O'Reilly Read via O'Reilly subscription