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