|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Services
Chapters
Feature Zones
|
String Handling and Regular Expressions, Part IThis two part series on string handling and regular expressions is based on Chapter 10 of Inside C#, Second Edition. As the chapter was split into two sections, I've done the same here for ease of readability. Note that while each article presents a single primary class ( Having said that, this article will start by examining the String Handling with C# and .NETThe .NET FrameworkSystem.String class (or its alias, string) represents an immutable string of characters—immutable because its value can’t be modified once it’s been created. Methods that appear to modify a string actually return a new string containing the modification. Besides the string class, the .NET Framework classes offer StringBuilder, String.Format, StringCollection, and so on. Together these offer comparison, appending, inserting, conversion, copying, formatting, indexing, joining, splitting, padding, trimming, removing, replacing, and searching methods.
Consider this example, which uses public class TestStringsApp
{
public static void Main(string[] args)
{
string a = "strong";
// Replace all 'o' with 'i'
string b = a.Replace('o', 'i');
Console.WriteLine(b);
string c = b.Insert(3, "engthen");
string d = c.ToUpper();
Console.WriteLine(d);
}
}
The output from this application will be: string
STRENGTHENING
The if (d == c) // Different
{
Console.WriteLine("same");
}
else
{
Console.WriteLine("different");
}
The output from this additional block of code is: different
Note that the string variable a in the second to last example isn’t changed by the string q = "Foo";
q = q.Replace('o', 'i');
Console.WriteLine(q);
The output is: Fii
You can combine string objects with conventional char arrays and even index into a string in the conventional manner: string e = "dog" + "bee";
e += "cat";
string f = e.Substring(1,7);
Console.WriteLine(f);
for (int i = 0; i < f.Length; i++)
{
Console.Write("{0,-3}", f[i]);
}
Here’s the output: ogbeeca
o g b e e c a
If you want a null string, declare one and assign null to it. Subsequently, you can reassign it with another string, as shown in the following example. Because the assignment to string g = null;
if (f.StartsWith("og"))
{
g = f.Remove(2,3);
}
Console.WriteLine(g);
This is the output: ogca
If you’re familiar with the Microsoft Foundation Classes (MFC) int x = 16;
decimal y = 3.57m;
string h = String.Format(
"item {0} sells at {1:C}", x, y);
Console.WriteLine(h);
Here’s the output: item 16 sells at £3.57
If you have experience with Microsoft Visual Basic, you won’t be surprised to find that you can concatenate a string with any other data type using the plus sign (+). This is because all types have at least inherited string t =
"item " + 12 + " sells at " + '\xA3' + 3.45;
Console.WriteLine(t);
And here’s the output: item 12 sells at £3.45
// This works because last param is a params object[].
Console.WriteLine(
"Hello {0} {1} {2} {3} {4} {5} {6} {7} {8}",
123, 45.67, true, 'Q', 4, 5, 6, 7, '8');
// This also works.
string u = String.Format(
"Hello {0} {1} {2} {3} {4} {5} {6} {7} {8}",
123, 45.67, true, 'Q', 4, 5, 6, 7, '8');
Console.WriteLine(u);
The output follows: Hello 123 45.67 True Q 4 5 6 7 8
Hello 123 45.67 True Q 4 5 6 7 8
String FormattingBothString.Format and WriteLine formatting are governed by the same formatting rules: the format parameter is embedded with zero or more format specifications of the form "{ N [, M ][: formatString ]}", arg1, ... argN, where:
If public class TestConsoleApp
{
public static void Main(string[] args)
{
Console.WriteLine(123);
Console.WriteLine("{0}", 123);
Console.WriteLine("{0:D3}", 123);
}
}
Here’s the output: 123
123
123
We’d get exactly the same results using string s = string.Format("123");
string t = string.Format("{0}", 123);
string u = string.Format("{0:D3}", 123);
Console.WriteLine(s);
Console.WriteLine(t);
Console.WriteLine(u);
Therefore:
Of course, you can combine them—putting the comma first, then the colon: Console.WriteLine("{0,-10:D6} {1,-10:D6}", 123, 456);
Here’s the output: 000123 000456
We could use these formatting features to output data in columns with appropriate alignment—for example: Console.WriteLine("\n{0,-10}{1,-3}", "Name","Salary");
Console.WriteLine("----------------");
Console.WriteLine("{0,-10}{1,6}", "Bill", 123456);
Console.WriteLine("{0,-10}{1,6}", "Polly", 7890);
This is the output: Name Salary
----------------
Bill 123456
Polly 7890
Format SpecifiersStandard numeric format strings are used to return strings in commonly used formats. They take the formX0, in which X is the format specifier and 0 is the precision specifier. The format specifier can be one of the nine built-in format characters that define the most commonly used numeric format types, as shown in Table 10-1.
Table 10-1 - String and WriteLine Format Specifiers
Let’s see what happens if we have a string format for an integer value using each of the format specifiers in turn. The comments in the following code show the output. public class FormatSpecApp
{
public static void Main(string[] args)
{
int i = 123456;
Console.WriteLine("{0:C}", i); // £123,456.00
Console.WriteLine("{0:D}", i); // 123456
Console.WriteLine("{0:E}", i); // 1.234560E+005
Console.WriteLine("{0:F}", i); // 123456.00
Console.WriteLine("{0:G}", i); // 123456
Console.WriteLine("{0:N}", i); // 123,456.00
Console.WriteLine("{0:P}", i); // 12,345,600.00 %
Console.WriteLine("{0:X}", i); // 1E240
}
}
The precision specifier controls the number of significant digits or zeros to the right of a decimal: Console.WriteLine("{0:C5}", i); // £123,456.00000
Console.WriteLine("{0:D5}", i); // 123456
Console.WriteLine("{0:E5}", i); // 1.23456E+005
Console.WriteLine("{0:F5}", i); // 123456.00000
Console.WriteLine("{0:G5}", i); // 1.23456E5
Console.WriteLine("{0:N5}", i); // 123,456.00000
Console.WriteLine("{0:P5}", i); // 12,345,600.00000 %
Console.WriteLine("{0:X5}", i); // 1E240
The R (round-trip) format works only with floating-point values: the value is first tested using the general format, with 15 spaces of precision for a Double and seven spaces of precision for a Single. If the value is successfully parsed back to the same numeric value, it’s formatted using the general format specifier. On the other hand, if the value isn’t successfully parsed back to the same numeric value, the value is formatted using 17 digits of precision for a Double and nine digits of precision for a Single. Although a precision specifier can be appended to the round-trip format specifier, it’s ignored. double d = 1.2345678901234567890;
Console.WriteLine("Floating-Point:\t{0:F16}", d); // 1.2345678901234600
Console.WriteLine("Roundtrip:\t{0:R16}", d); // 1.2345678901234567
If the standard formatting specifiers aren’t enough for you, you can use picture format strings to create custom string output. Picture format definitions are described using placeholder strings that identify the minimum and maximum number of digits used, the placement or appearance of the negative sign, and the appearance of any other text within the number, as shown in Table 10-2. Table 10-2 - Custom Format Specifiers
Let’s see the strings that result from a set of customized formats, using first a positive integer, then using the negative value of that same integer, and finally using zero: int i = 123456;
Console.WriteLine();
Console.WriteLine("{0:#0}", i); // 123456
Console.WriteLine("{0:#0;(#0)}", i); // 123456
Console.WriteLine("{0:#0;(#0);<zero>}", i); // 123456
Console.WriteLine("{0:#%}", i); // 12345600%
i = -123456;
Console.WriteLine();
Console.WriteLine("{0:#0}", i); // -123456
Console.WriteLine("{0:#0;(#0)}", i); // (123456)
Console.WriteLine("{0:#0;(#0);<zero>}", i); // (123456)
Console.WriteLine("{0:#%}", i); // -12345600%
i = 0;
Console.WriteLine();
Console.WriteLine("{0:#0}", i); // 0
Console.WriteLine("{0:#0;(#0)}", i); // 0
Console.WriteLine("{0:#0;(#0);<zero>}", i); // <zero>
Console.WriteLine("{0:#%}", i); // %
Objects and ToStringRecall that all data types—both predefined and user-defined—inherit from theSystem.Object class in the .NET Framework, which is aliased as object: public class Thing
{
public int i = 2;
public int j = 3;
}
public class objectTypeApp
{
public static void Main()
{
object a;
a = 1;
Console.WriteLine(a);
Console.WriteLine(a.ToString());
Console.WriteLine(a.GetType());
Console.WriteLine();
Thing b = new Thing();
Console.WriteLine(b);
Console.WriteLine(b.ToString());
Console.WriteLine(b.GetType());
}
}
Here’s the output: 1
1
System.Int32
objectType.Thing
objectType.Thing
objectType.Thing
From the foregoing code, you can see that the statement Console.WriteLine(a);is the same as Console.WriteLine(a.ToString());
The reason for this equivalence is that the public class Thing
{
public int i = 2;
public int j = 3;
override public string ToString()
{
return String.Format("i = {0}, j = {1}", i, j);
}
}
The relevant output from this revised code is: i = 2, j = 3
i = 2, j = 3
objectType.Thing
Numeric String ParsingAll the basic types have aToString method, which is inherited from the Object type, and all the numeric types have a Parse method, which takes the string representation of a number and returns you its equivalent numeric value. For example: public class NumParsingApp
{
public static void Main(string[] args)
{
int i = int.Parse("12345");
Console.WriteLine("i = {0}", i);
int j = Int32.Parse("12345");
Console.WriteLine("j = {0}", j);
double d = Double.Parse("1.2345E+6");
Console.WriteLine("d = {0:F}", d);
string s = i.ToString();
Console.WriteLine("s = {0}", s);
}
}
The output from this application is shown here: i = 12345
j = 12345
d = 1234500.00
s = 12345
Certain non-digit characters in an input string are allowed by default, including leading and trailing spaces, commas and decimal points, and plus and minus signs. Therefore, the following string t = " -1,234,567.890 ";
//double g = double.Parse(t); // Same thing
double g = double.Parse(t,
NumberStyles.AllowLeadingSign ¦
NumberStyles.AllowDecimalPoint ¦
NumberStyles.AllowThousands ¦
NumberStyles.AllowLeadingWhite ¦
NumberStyles.AllowTrailingWhite);
Console.WriteLine("g = {0:F}", g);
The output from this additional code block is shown next: g = -1234567.89
Note that to use string u = "£ -1,234,567.890 ";
NumberFormatInfo ni = new NumberFormatInfo();
ni.CurrencySymbol = "£";
double h = Double.Parse(u, NumberStyles.Any, ni);
Console.WriteLine("h = {0:F}", h);
The output from this additional code block is shown here: h = -1234567.89
In addition to int k = 12345;
CultureInfo us = new CultureInfo("en-US");
string v = k.ToString("c", us);
Console.WriteLine(v);
This example would produce a string like this: $12,345.00
Note that we’re using a CultureInfo dk = new CultureInfo("da-DK");
string w = k.ToString("c", dk);
Console.WriteLine(w);
The output is: kr 12.345,00
Strings and DateTimeADateTime object has a property named Ticks that stores the date and time as the number of 100-nanosecond intervals since 12:00 AM January 1, 1 A.D. in the Gregorian calendar. For example, a ticks value of 31241376000000000L has the string representation "Friday, January 01, 0100 12:00:00 AM". Each additional tick increases the time interval by 100 nanoseconds.
using System.Globalization;
public class DatesApp
{
public static void Main(string[] args)
{
DateTime dt = DateTime.Now;
Console.WriteLine(dt);
Console.WriteLine("date = {0}, time = {1}\n",
dt.Date, dt.TimeOfDay);
}
}
This code will produce the following output: 23/06/2001 17:55:10
date = 23/06/2001 00:00:00, time = 17:55:10.3839296
Table 10-3 lists the standard format characters for each standard pattern and the associated Table 10-3 - DateTime Formatting
The Console.WriteLine(dt.ToString("d", dtfi));
Console.WriteLine(dt.ToString("d", null));
Console.WriteLine();
Here’s the output: 06/23/2001
23/06/2001
Compare the results of choosing DateTimeFormatInfo dtfi;
Console.Write("[I]nvariant or [C]urrent Info?: ");
if (Console.Read() == 'I')
dtfi = DateTimeFormatInfo.InvariantInfo;
else
dtfi = DateTimeFormatInfo.CurrentInfo;
DateTimeFormatInfo dtfi = DateTimeFormatInfo.InvariantInfo;
Console.WriteLine(dt.ToString("D", dtfi));
Console.WriteLine(dt.ToString("f", dtfi));
Console.WriteLine(dt.ToString("F", dtfi));
Console.WriteLine(dt.ToString("g", dtfi));
Console.WriteLine(dt.ToString("G", dtfi));
Console.WriteLine(dt.ToString("m", dtfi));
Console.WriteLine(dt.ToString("r", dtfi));
Console.WriteLine(dt.ToString("s", dtfi));
Console.WriteLine(dt.ToString("t", dtfi));
Console.WriteLine(dt.ToString("T", dtfi));
Console.WriteLine(dt.ToString("u", dtfi));
Console.WriteLine(dt.ToString("U", dtfi));
Console.WriteLine(dt.ToString("d", dtfi));
Console.WriteLine(dt.ToString("y", dtfi));
Console.WriteLine(dt.ToString("dd-MMM-yy", dtfi));
Here’s the output: [I]nvariant or [C]urrent Info?: I
01/03/2002
03/01/2002
Thursday, 03 January 2002
Thursday, 03 January 2002 12:55
Thursday, 03 January 2002 12:55:03
01/03/2002 12:55
01/03/2002 12:55:03
January 03
Thu, 03 Jan 2002 12:55:03 GMT
2002-01-03T12:55:03
12:55
12:55:03
2002-01-03 12:55:03Z
Thursday, 03 January 2002 12:55:03
01/03/2002
2002 January
03-Jan-02
[I]nvariant or [C]urrent Info?: C
03/01/2002
03/01/2002
03 January 2002
03 January 2002 12:55
03 January 2002 12:55:47
03/01/2002 12:55
03/01/2002 12:55:47
03 January
Thu, 03 Jan 2002 12:55:47 GMT
2002-01-03T12:55:47
12:55
12:55:47
2002-01-03 12:55:47Z
03 January 2002 12:55:47
03/01/2002
January 2002
03-Jan-02
Encoding StringsTheSystem.Text namespace offers an Encoding class. Encoding is an abstract class, so you can’t instantiate it directly. However, it does provide a range of methods and properties for converting arrays and strings of Unicode characters to and from arrays of bytes encoded for a target code page. These properties actually resolve to returning an implementation of the Encoding class. Table 10-4 shows some of these properties.
Table 10-4 - String Encoding Classes
For example, you can convert a simple sequence of bytes into a conventional ASCII string, as shown here: class StringEncodingApp
{
static void Main(string[] args)
{
byte[] ba = new byte[]
{72, 101, 108, 108, 111};
string s = Encoding.ASCII.GetString(ba);
Console.WriteLine(s);
}
}
This is the output: Hello
If you want to convert to something other than ASCII, simply use one of the other byte[] bb = new byte[]
{0,72, 0,101, 0,108, 0,108, 0,111};
string t = Encoding.BigEndianUnicode.GetString(bb);
Console.WriteLine(t);
The
You could achieve the same results as those from the previous example with the following code: ASCIIEncoding ae = new ASCIIEncoding();
Console.WriteLine(ae.GetString(ba));
UnicodeEncoding bu =
new UnicodeEncoding(true, false);
Console.WriteLine(bu.GetString(bb));
The StringBuilder ClassRecall that with theString class, methods that appear to modify a string actually return a new string containing the modification. This behavior is sometimes a nuisance because if you make several modifications to a string, you end up working with several generations of copies of the original. For this reason, the people at Redmond have provided the StringBuilder class in the System.Text namespace.
Consider this example, using the class UseSBApp
{
static void Main(string[] args)
{
StringBuilder sb = new StringBuilder("Pineapple");
sb.Replace('e', 'X');
sb.Insert(4, "Banana");
sb.Append("Kiwi");
sb.AppendFormat(", {0}:{1}", 123, 45.6789);
sb.Remove(sb.Length - 3, 3);
Console.WriteLine(sb);
}
}
This is the output: PinXBananaapplXKiwi, 123:45.6
Note that—as with most other types—you can easily convert from a string s = sb.ToString().ToUpper();
Console.WriteLine(s);
Here’s the output: PINXBANANAAPPLXKIWI, 123:45.6
Splitting StringsTheString class does offer a Split method for splitting a string into substrings, with the splits determined by arbitrary separator characters that you supply to the method. For example: class SplitStringApp
{
static void Main(string[] args)
{
string s = "Once Upon A Time In America";
char[] seps = new char[]{' '};
foreach (string ss in s.Split(seps))
Console.WriteLine(ss);
}
}
The output follows: Once
Upon
A
Time
In
America
The separators parameter to string t = "Once,Upon:A/Time\\In\'America";
char[] sep2 = new char[]{ ' ', ',', ':', '/', '\\', '\''};
foreach (string ss in t.Split(sep2))
Console.WriteLine(ss);
Note that the string u = "Once Upon A Time In America";
char[] sep3 = new char[]{' '};
foreach (string ss in u.Split(sep3))
Console.WriteLine(ss);
Here’s the output: Once
Upon
A
Time
In
America
In the second article of this two-part series, we’ll consider the regular expression classes in the .NET Framework, and we’ll see how to solve this particular problem and many others. Extending StringsIn libraries before the .NET era, it became common practice to extend theString class found in the library with enhanced features. Unfortunately, the String class in the .NET Framework is sealed; therefore, you can’t derive from it. On the other hand, it’s entirely possible to provide a series of encapsulated static methods that process strings. For example, the String class does offer the ToUpper and ToLower methods for converting to uppercase or lowercase, respectively, but this class doesn’t offer a method to convert to proper case (initial capitals on each word). Providing such functionality is simple, as shown here: public class StringEx
{
public static string ProperCase(string s)
{
s = s.ToLower();
string sProper = "";
char[] seps = new char[]{' '};
foreach (string ss in s.Split(seps))
{
sProper += char.ToUpper(ss[0]);
sProper +=
(ss.Substring(1, ss.Length - 1) + ' ');
}
return sProper;
}
}
class StringExApp
{
static void Main(string[] args)
{
string s = "the qUEEn wAs in HER parLOr";
Console.WriteLine("Initial String:\t{0}", s);
string t = StringEx.ProperCase(s);
Console.WriteLine("ProperCase:\t{0}", t);
}
}
This will produce the output shown here. (In the second part of this two-part series, we’ll see how to achieve the same results with regular expressions.) Initial String: the qUEEn wAs in HER parLOr
ProperCase: The Queen Was In Her Parlor
Another classic operation that doubtless will appear again is a test for a palindromic string—a string that reads the same backwards and forwards: public static bool IsPalindrome(string s)
{
int iLength, iHalfLen;
iLength = s.Length - 1;
iHalfLen = iLength / 2;
for (int i = 0; i <= iHalfLen; i++)
{
if (s.Substring(i, 1) !=
s.Substring(iLength - i, 1))
{
return false;
}
}
return true;
}
static void Main(string[] args)
{
Console.WriteLine("\nPalindromes?");
string[] sa = new string[]{
"level", "minim", "radar",
"foobar", "rotor", "banana"};
foreach (string v in sa)
Console.WriteLine("{0}\t{1}",
v, StringEx.IsPalindrome(v));
}
Here’s the output: Palindromes?
level True
minim True
radar True
foobar False
rotor True
banana False
For more complex operations—such as conditional splitting or joining, extended parsing or tokenizing, and sophisticated trimming in which the String InterningOne of the reasons strings were designed to be immutable is that this arrangement allows the system to intern them. During the process of string interning, all the constant strings in an application are stored in a common place in memory, thus eliminating unnecessary duplicates. This practice clearly saves space at run time but can confuse the unwary. For example, recall that the equivalence operator (==) will test for value equivalence for value types and for address (or reference) equivalence for reference types. Therefore, in the following application, when we compare two reference type objects of the same class with the same contents, the result isFalse. However, when we compare two string objects with the same contents, the result is True: class StringInterningApp
{
public class Thing
{
private int i;
public Thing(int i) { this.i = i; }
}
static void Main(string[] args)
{
Thing t1 = new Thing(123);
Thing t2 = new Thing(123);
Console.WriteLine(t1 == t2); // False
string a = "Hello";
string b = "Hello";
Console.WriteLine(a == b); // True
}
}
OK, but both strings are actually constants or literals. Suppose we have another string that’s a variable? Again, given the same contents, the string equivalence operator will return string c = String.Copy(a);
Console.WriteLine(a == c); // True
Now suppose we force the run-time system to treat the two strings as objects, not strings, and therefore use the most basic reference type equivalence operator. This time we get Console.WriteLine((object)a == (object)c);
Time to look at the underlying Microsoft intermediate language (MSIL), as shown in Figure 10-1.
The crucial differences are as follows: For the first comparison Note that Chapter 13 of Inside C# illustrates exactly how the What happens if we compare the two original string constants and force the use of the most primitive equivalence operator? Take a look: Console.WriteLine((object)a == (object)b);
You’ll find that the output from this is SummaryIn this article, we examined theString class and a range of ancillary classes that modify and support string operations. We explored the use of the String class methods for searching, sorting, splitting, joining, and otherwise returning modified strings. We also saw how many other classes in the .NET Framework support string processing—including Console, the basic numeric types, and DateTime—and how culture information and character encoding can affect string formatting. Finally, we saw how the system performs sneaky string interning to improve runtime efficiency. In the next article, you'll discover the Regex class and its supporting classes — Match, Group, and Capture — for encapsulating regular expressions.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||