summaryrefslogtreecommitdiff
path: root/src/mscorlib/src/System/Globalization/DateTimeFormat.cs
diff options
context:
space:
mode:
Diffstat (limited to 'src/mscorlib/src/System/Globalization/DateTimeFormat.cs')
-rw-r--r--src/mscorlib/src/System/Globalization/DateTimeFormat.cs1054
1 files changed, 1054 insertions, 0 deletions
diff --git a/src/mscorlib/src/System/Globalization/DateTimeFormat.cs b/src/mscorlib/src/System/Globalization/DateTimeFormat.cs
new file mode 100644
index 0000000000..228e5f56a2
--- /dev/null
+++ b/src/mscorlib/src/System/Globalization/DateTimeFormat.cs
@@ -0,0 +1,1054 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+namespace System {
+ using System.Text;
+ using System.Threading;
+ using System.Globalization;
+ using System.Collections.Generic;
+ using System.Runtime.CompilerServices;
+ using System.Runtime.InteropServices;
+ using System.Runtime.Versioning;
+ using System.Security;
+ using System.Diagnostics.Contracts;
+
+ /*
+ Customized format patterns:
+ P.S. Format in the table below is the internal number format used to display the pattern.
+
+ Patterns Format Description Example
+ ========= ========== ===================================== ========
+ "h" "0" hour (12-hour clock)w/o leading zero 3
+ "hh" "00" hour (12-hour clock)with leading zero 03
+ "hh*" "00" hour (12-hour clock)with leading zero 03
+
+ "H" "0" hour (24-hour clock)w/o leading zero 8
+ "HH" "00" hour (24-hour clock)with leading zero 08
+ "HH*" "00" hour (24-hour clock) 08
+
+ "m" "0" minute w/o leading zero
+ "mm" "00" minute with leading zero
+ "mm*" "00" minute with leading zero
+
+ "s" "0" second w/o leading zero
+ "ss" "00" second with leading zero
+ "ss*" "00" second with leading zero
+
+ "f" "0" second fraction (1 digit)
+ "ff" "00" second fraction (2 digit)
+ "fff" "000" second fraction (3 digit)
+ "ffff" "0000" second fraction (4 digit)
+ "fffff" "00000" second fraction (5 digit)
+ "ffffff" "000000" second fraction (6 digit)
+ "fffffff" "0000000" second fraction (7 digit)
+
+ "F" "0" second fraction (up to 1 digit)
+ "FF" "00" second fraction (up to 2 digit)
+ "FFF" "000" second fraction (up to 3 digit)
+ "FFFF" "0000" second fraction (up to 4 digit)
+ "FFFFF" "00000" second fraction (up to 5 digit)
+ "FFFFFF" "000000" second fraction (up to 6 digit)
+ "FFFFFFF" "0000000" second fraction (up to 7 digit)
+
+ "t" first character of AM/PM designator A
+ "tt" AM/PM designator AM
+ "tt*" AM/PM designator PM
+
+ "d" "0" day w/o leading zero 1
+ "dd" "00" day with leading zero 01
+ "ddd" short weekday name (abbreviation) Mon
+ "dddd" full weekday name Monday
+ "dddd*" full weekday name Monday
+
+
+ "M" "0" month w/o leading zero 2
+ "MM" "00" month with leading zero 02
+ "MMM" short month name (abbreviation) Feb
+ "MMMM" full month name Febuary
+ "MMMM*" full month name Febuary
+
+ "y" "0" two digit year (year % 100) w/o leading zero 0
+ "yy" "00" two digit year (year % 100) with leading zero 00
+ "yyy" "D3" year 2000
+ "yyyy" "D4" year 2000
+ "yyyyy" "D5" year 2000
+ ...
+
+ "z" "+0;-0" timezone offset w/o leading zero -8
+ "zz" "+00;-00" timezone offset with leading zero -08
+ "zzz" "+00;-00" for hour offset, "00" for minute offset full timezone offset -07:30
+ "zzz*" "+00;-00" for hour offset, "00" for minute offset full timezone offset -08:00
+
+ "K" -Local "zzz", e.g. -08:00
+ -Utc "'Z'", representing UTC
+ -Unspecified ""
+ -DateTimeOffset "zzzzz" e.g -07:30:15
+
+ "g*" the current era name A.D.
+
+ ":" time separator : -- DEPRECATED - Insert separator directly into pattern (eg: "H.mm.ss")
+ "/" date separator /-- DEPRECATED - Insert separator directly into pattern (eg: "M-dd-yyyy")
+ "'" quoted string 'ABC' will insert ABC into the formatted string.
+ '"' quoted string "ABC" will insert ABC into the formatted string.
+ "%" used to quote a single pattern characters E.g.The format character "%y" is to print two digit year.
+ "\" escaped character E.g. '\d' insert the character 'd' into the format string.
+ other characters insert the character into the format string.
+
+ Pre-defined format characters:
+ (U) to indicate Universal time is used.
+ (G) to indicate Gregorian calendar is used.
+
+ Format Description Real format Example
+ ========= ================================= ====================== =======================
+ "d" short date culture-specific 10/31/1999
+ "D" long data culture-specific Sunday, October 31, 1999
+ "f" full date (long date + short time) culture-specific Sunday, October 31, 1999 2:00 AM
+ "F" full date (long date + long time) culture-specific Sunday, October 31, 1999 2:00:00 AM
+ "g" general date (short date + short time) culture-specific 10/31/1999 2:00 AM
+ "G" general date (short date + long time) culture-specific 10/31/1999 2:00:00 AM
+ "m"/"M" Month/Day date culture-specific October 31
+(G) "o"/"O" Round Trip XML "yyyy-MM-ddTHH:mm:ss.fffffffK" 1999-10-31 02:00:00.0000000Z
+(G) "r"/"R" RFC 1123 date, "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'" Sun, 31 Oct 1999 10:00:00 GMT
+(G) "s" Sortable format, based on ISO 8601. "yyyy-MM-dd'T'HH:mm:ss" 1999-10-31T02:00:00
+ ('T' for local time)
+ "t" short time culture-specific 2:00 AM
+ "T" long time culture-specific 2:00:00 AM
+(G) "u" Universal time with sortable format, "yyyy'-'MM'-'dd HH':'mm':'ss'Z'" 1999-10-31 10:00:00Z
+ based on ISO 8601.
+(U) "U" Universal time with full culture-specific Sunday, October 31, 1999 10:00:00 AM
+ (long date + long time) format
+ "y"/"Y" Year/Month day culture-specific October, 1999
+
+ */
+
+ //This class contains only static members and does not require the serializable attribute.
+ internal static
+ class DateTimeFormat {
+
+ internal const int MaxSecondsFractionDigits = 7;
+ internal static readonly TimeSpan NullOffset = TimeSpan.MinValue;
+
+ internal static char[] allStandardFormats =
+ {
+ 'd', 'D', 'f', 'F', 'g', 'G',
+ 'm', 'M', 'o', 'O', 'r', 'R',
+ 's', 't', 'T', 'u', 'U', 'y', 'Y',
+ };
+
+ internal const String RoundtripFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK";
+ internal const String RoundtripDateTimeUnfixed = "yyyy'-'MM'-'ddTHH':'mm':'ss zzz";
+
+ private const int DEFAULT_ALL_DATETIMES_SIZE = 132;
+
+ internal static String[] fixedNumberFormats = new String[] {
+ "0",
+ "00",
+ "000",
+ "0000",
+ "00000",
+ "000000",
+ "0000000",
+ };
+
+ ////////////////////////////////////////////////////////////////////////////
+ //
+ // Format the positive integer value to a string and perfix with assigned
+ // length of leading zero.
+ //
+ // Parameters:
+ // value: The value to format
+ // len: The maximum length for leading zero.
+ // If the digits of the value is greater than len, no leading zero is added.
+ //
+ // Notes:
+ // The function can format to Int32.MaxValue.
+ //
+ ////////////////////////////////////////////////////////////////////////////
+ internal static void FormatDigits(StringBuilder outputBuffer, int value, int len) {
+ Contract.Assert(value >= 0, "DateTimeFormat.FormatDigits(): value >= 0");
+ FormatDigits(outputBuffer, value, len, false);
+ }
+
+ [System.Security.SecuritySafeCritical] // auto-generated
+ internal unsafe static void FormatDigits(StringBuilder outputBuffer, int value, int len, bool overrideLengthLimit) {
+ Contract.Assert(value >= 0, "DateTimeFormat.FormatDigits(): value >= 0");
+
+ // Limit the use of this function to be two-digits, so that we have the same behavior
+ // as RTM bits.
+ if (!overrideLengthLimit && len > 2)
+ {
+ len = 2;
+ }
+
+ char* buffer = stackalloc char[16];
+ char* p = buffer + 16;
+ int n = value;
+ do {
+ *--p = (char)(n % 10 + '0');
+ n /= 10;
+ } while ((n != 0)&&(p > buffer));
+
+ int digits = (int) (buffer + 16 - p);
+
+ //If the repeat count is greater than 0, we're trying
+ //to emulate the "00" format, so we have to prepend
+ //a zero if the string only has one character.
+ while ((digits < len) && (p > buffer)) {
+ *--p='0';
+ digits++;
+ }
+ outputBuffer.Append(p, digits);
+ }
+
+ private static void HebrewFormatDigits(StringBuilder outputBuffer, int digits) {
+ outputBuffer.Append(HebrewNumber.ToString(digits));
+ }
+
+ internal static int ParseRepeatPattern(String format, int pos, char patternChar)
+ {
+ int len = format.Length;
+ int index = pos + 1;
+ while ((index < len) && (format[index] == patternChar))
+ {
+ index++;
+ }
+ return (index - pos);
+ }
+
+ private static String FormatDayOfWeek(int dayOfWeek, int repeat, DateTimeFormatInfo dtfi)
+ {
+ Contract.Assert(dayOfWeek >= 0 && dayOfWeek <= 6, "dayOfWeek >= 0 && dayOfWeek <= 6");
+ if (repeat == 3)
+ {
+ return (dtfi.GetAbbreviatedDayName((DayOfWeek)dayOfWeek));
+ }
+ // Call dtfi.GetDayName() here, instead of accessing DayNames property, because we don't
+ // want a clone of DayNames, which will hurt perf.
+ return (dtfi.GetDayName((DayOfWeek)dayOfWeek));
+ }
+
+ private static String FormatMonth(int month, int repeatCount, DateTimeFormatInfo dtfi)
+ {
+ Contract.Assert(month >=1 && month <= 12, "month >=1 && month <= 12");
+ if (repeatCount == 3)
+ {
+ return (dtfi.GetAbbreviatedMonthName(month));
+ }
+ // Call GetMonthName() here, instead of accessing MonthNames property, because we don't
+ // want a clone of MonthNames, which will hurt perf.
+ return (dtfi.GetMonthName(month));
+ }
+
+ //
+ // FormatHebrewMonthName
+ //
+ // Action: Return the Hebrew month name for the specified DateTime.
+ // Returns: The month name string for the specified DateTime.
+ // Arguments:
+ // time the time to format
+ // month The month is the value of HebrewCalendar.GetMonth(time).
+ // repeat Return abbreviated month name if repeat=3, or full month name if repeat=4
+ // dtfi The DateTimeFormatInfo which uses the Hebrew calendars as its calendar.
+ // Exceptions: None.
+ //
+
+ /* Note:
+ If DTFI is using Hebrew calendar, GetMonthName()/GetAbbreviatedMonthName() will return month names like this:
+ 1 Hebrew 1st Month
+ 2 Hebrew 2nd Month
+ .. ...
+ 6 Hebrew 6th Month
+ 7 Hebrew 6th Month II (used only in a leap year)
+ 8 Hebrew 7th Month
+ 9 Hebrew 8th Month
+ 10 Hebrew 9th Month
+ 11 Hebrew 10th Month
+ 12 Hebrew 11th Month
+ 13 Hebrew 12th Month
+
+ Therefore, if we are in a regular year, we have to increment the month name if moth is greater or eqaul to 7.
+ */
+ private static String FormatHebrewMonthName(DateTime time, int month, int repeatCount, DateTimeFormatInfo dtfi)
+ {
+ Contract.Assert(repeatCount != 3 || repeatCount != 4, "repeateCount should be 3 or 4");
+ if (dtfi.Calendar.IsLeapYear(dtfi.Calendar.GetYear(time))) {
+ // This month is in a leap year
+ return (dtfi.internalGetMonthName(month, MonthNameStyles.LeapYear, (repeatCount == 3)));
+ }
+ // This is in a regular year.
+ if (month >= 7) {
+ month++;
+ }
+ if (repeatCount == 3) {
+ return (dtfi.GetAbbreviatedMonthName(month));
+ }
+ return (dtfi.GetMonthName(month));
+ }
+
+ //
+ // The pos should point to a quote character. This method will
+ // append to the result StringBuilder the string encloed by the quote character.
+ //
+ internal static int ParseQuoteString(String format, int pos, StringBuilder result)
+ {
+ //
+ // NOTE : pos will be the index of the quote character in the 'format' string.
+ //
+ int formatLen = format.Length;
+ int beginPos = pos;
+ char quoteChar = format[pos++]; // Get the character used to quote the following string.
+
+ bool foundQuote = false;
+ while (pos < formatLen)
+ {
+ char ch = format[pos++];
+ if (ch == quoteChar)
+ {
+ foundQuote = true;
+ break;
+ }
+ else if (ch == '\\') {
+ // The following are used to support escaped character.
+ // Escaped character is also supported in the quoted string.
+ // Therefore, someone can use a format like "'minute:' mm\"" to display:
+ // minute: 45"
+ // because the second double quote is escaped.
+ if (pos < formatLen) {
+ result.Append(format[pos++]);
+ } else {
+ //
+ // This means that '\' is at the end of the formatting string.
+ //
+ throw new FormatException(Environment.GetResourceString("Format_InvalidString"));
+ }
+ } else {
+ result.Append(ch);
+ }
+ }
+
+ if (!foundQuote)
+ {
+ // Here we can't find the matching quote.
+ throw new FormatException(
+ String.Format(
+ CultureInfo.CurrentCulture,
+ Environment.GetResourceString("Format_BadQuote"), quoteChar));
+ }
+
+ //
+ // Return the character count including the begin/end quote characters and enclosed string.
+ //
+ return (pos - beginPos);
+ }
+
+ //
+ // Get the next character at the index of 'pos' in the 'format' string.
+ // Return value of -1 means 'pos' is already at the end of the 'format' string.
+ // Otherwise, return value is the int value of the next character.
+ //
+ internal static int ParseNextChar(String format, int pos)
+ {
+ if (pos >= format.Length - 1)
+ {
+ return (-1);
+ }
+ return ((int)format[pos+1]);
+ }
+
+ //
+ // IsUseGenitiveForm
+ //
+ // Actions: Check the format to see if we should use genitive month in the formatting.
+ // Starting at the position (index) in the (format) string, look back and look ahead to
+ // see if there is "d" or "dd". In the case like "d MMMM" or "MMMM dd", we can use
+ // genitive form. Genitive form is not used if there is more than two "d".
+ // Arguments:
+ // format The format string to be scanned.
+ // index Where we should start the scanning. This is generally where "M" starts.
+ // tokenLen The len of the current pattern character. This indicates how many "M" that we have.
+ // patternToMatch The pattern that we want to search. This generally uses "d"
+ //
+ private static bool IsUseGenitiveForm(String format, int index, int tokenLen, char patternToMatch) {
+ int i;
+ int repeat = 0;
+ //
+ // Look back to see if we can find "d" or "ddd"
+ //
+
+ // Find first "d".
+ for (i = index - 1; i >= 0 && format[i] != patternToMatch; i--) { /*Do nothing here */ };
+
+ if (i >= 0) {
+ // Find a "d", so look back to see how many "d" that we can find.
+ while (--i >= 0 && format[i] == patternToMatch) {
+ repeat++;
+ }
+ //
+ // repeat == 0 means that we have one (patternToMatch)
+ // repeat == 1 means that we have two (patternToMatch)
+ //
+ if (repeat <= 1) {
+ return (true);
+ }
+ // Note that we can't just stop here. We may find "ddd" while looking back, and we have to look
+ // ahead to see if there is "d" or "dd".
+ }
+
+ //
+ // If we can't find "d" or "dd" by looking back, try look ahead.
+ //
+
+ // Find first "d"
+ for (i = index + tokenLen; i < format.Length && format[i] != patternToMatch; i++) { /* Do nothing here */ };
+
+ if (i < format.Length) {
+ repeat = 0;
+ // Find a "d", so contine the walk to see how may "d" that we can find.
+ while (++i < format.Length && format[i] == patternToMatch) {
+ repeat++;
+ }
+ //
+ // repeat == 0 means that we have one (patternToMatch)
+ // repeat == 1 means that we have two (patternToMatch)
+ //
+ if (repeat <= 1) {
+ return (true);
+ }
+ }
+ return (false);
+ }
+
+
+ //
+ // FormatCustomized
+ //
+ // Actions: Format the DateTime instance using the specified format.
+ //
+ private static String FormatCustomized(DateTime dateTime, String format, DateTimeFormatInfo dtfi, TimeSpan offset) {
+
+
+ Calendar cal = dtfi.Calendar;
+ StringBuilder result = StringBuilderCache.Acquire();
+ // This is a flag to indicate if we are format the dates using Hebrew calendar.
+
+ bool isHebrewCalendar = (cal.ID == Calendar.CAL_HEBREW);
+ // This is a flag to indicate if we are formating hour/minute/second only.
+ bool bTimeOnly = true;
+
+ int i = 0;
+ int tokenLen, hour12;
+
+ while (i < format.Length) {
+ char ch = format[i];
+ int nextChar;
+ switch (ch) {
+ case 'g':
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ result.Append(dtfi.GetEraName(cal.GetEra(dateTime)));
+ break;
+ case 'h':
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ hour12 = dateTime.Hour % 12;
+ if (hour12 == 0)
+ {
+ hour12 = 12;
+ }
+ FormatDigits(result, hour12, tokenLen);
+ break;
+ case 'H':
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ FormatDigits(result, dateTime.Hour, tokenLen);
+ break;
+ case 'm':
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ FormatDigits(result, dateTime.Minute, tokenLen);
+ break;
+ case 's':
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ FormatDigits(result, dateTime.Second, tokenLen);
+ break;
+ case 'f':
+ case 'F':
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ if (tokenLen <= MaxSecondsFractionDigits) {
+ long fraction = (dateTime.Ticks % Calendar.TicksPerSecond);
+ fraction = fraction / (long)Math.Pow(10, 7 - tokenLen);
+ if (ch == 'f') {
+ result.Append(((int)fraction).ToString(fixedNumberFormats[tokenLen - 1], CultureInfo.InvariantCulture));
+ }
+ else {
+ int effectiveDigits = tokenLen;
+ while (effectiveDigits > 0) {
+ if (fraction % 10 == 0) {
+ fraction = fraction / 10;
+ effectiveDigits--;
+ }
+ else {
+ break;
+ }
+ }
+ if (effectiveDigits > 0) {
+ result.Append(((int)fraction).ToString(fixedNumberFormats[effectiveDigits - 1], CultureInfo.InvariantCulture));
+ }
+ else {
+ // No fraction to emit, so see if we should remove decimal also.
+ if (result.Length > 0 && result[result.Length - 1] == '.') {
+ result.Remove(result.Length - 1, 1);
+ }
+ }
+ }
+ } else {
+ throw new FormatException(Environment.GetResourceString("Format_InvalidString"));
+ }
+ break;
+ case 't':
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ if (tokenLen == 1)
+ {
+ if (dateTime.Hour < 12)
+ {
+ if (dtfi.AMDesignator.Length >= 1)
+ {
+ result.Append(dtfi.AMDesignator[0]);
+ }
+ }
+ else
+ {
+ if (dtfi.PMDesignator.Length >= 1)
+ {
+ result.Append(dtfi.PMDesignator[0]);
+ }
+ }
+
+ }
+ else
+ {
+ result.Append((dateTime.Hour < 12 ? dtfi.AMDesignator : dtfi.PMDesignator));
+ }
+ break;
+ case 'd':
+ //
+ // tokenLen == 1 : Day of month as digits with no leading zero.
+ // tokenLen == 2 : Day of month as digits with leading zero for single-digit months.
+ // tokenLen == 3 : Day of week as a three-leter abbreviation.
+ // tokenLen >= 4 : Day of week as its full name.
+ //
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ if (tokenLen <= 2)
+ {
+ int day = cal.GetDayOfMonth(dateTime);
+ if (isHebrewCalendar) {
+ // For Hebrew calendar, we need to convert numbers to Hebrew text for yyyy, MM, and dd values.
+ HebrewFormatDigits(result, day);
+ } else {
+ FormatDigits(result, day, tokenLen);
+ }
+ }
+ else
+ {
+ int dayOfWeek = (int)cal.GetDayOfWeek(dateTime);
+ result.Append(FormatDayOfWeek(dayOfWeek, tokenLen, dtfi));
+ }
+ bTimeOnly = false;
+ break;
+ case 'M':
+ //
+ // tokenLen == 1 : Month as digits with no leading zero.
+ // tokenLen == 2 : Month as digits with leading zero for single-digit months.
+ // tokenLen == 3 : Month as a three-letter abbreviation.
+ // tokenLen >= 4 : Month as its full name.
+ //
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ int month = cal.GetMonth(dateTime);
+ if (tokenLen <= 2)
+ {
+ if (isHebrewCalendar) {
+ // For Hebrew calendar, we need to convert numbers to Hebrew text for yyyy, MM, and dd values.
+ HebrewFormatDigits(result, month);
+ } else {
+ FormatDigits(result, month, tokenLen);
+ }
+ }
+ else {
+ if (isHebrewCalendar) {
+ result.Append(FormatHebrewMonthName(dateTime, month, tokenLen, dtfi));
+ } else {
+ if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0 && tokenLen >= 4) {
+ result.Append(
+ dtfi.internalGetMonthName(
+ month,
+ IsUseGenitiveForm(format, i, tokenLen, 'd')? MonthNameStyles.Genitive : MonthNameStyles.Regular,
+ false));
+ } else {
+ result.Append(FormatMonth(month, tokenLen, dtfi));
+ }
+ }
+ }
+ bTimeOnly = false;
+ break;
+ case 'y':
+ // Notes about OS behavior:
+ // y: Always print (year % 100). No leading zero.
+ // yy: Always print (year % 100) with leading zero.
+ // yyy/yyyy/yyyyy/... : Print year value. No leading zero.
+
+ int year = cal.GetYear(dateTime);
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ if (dtfi.HasForceTwoDigitYears) {
+ FormatDigits(result, year, tokenLen <= 2 ? tokenLen : 2);
+ }
+ else if (cal.ID == Calendar.CAL_HEBREW) {
+ HebrewFormatDigits(result, year);
+ }
+ else {
+ if (tokenLen <= 2) {
+ FormatDigits(result, year % 100, tokenLen);
+ }
+ else {
+ String fmtPattern = "D" + tokenLen.ToString();
+ result.Append(year.ToString(fmtPattern, CultureInfo.InvariantCulture));
+ }
+ }
+ bTimeOnly = false;
+ break;
+ case 'z':
+ tokenLen = ParseRepeatPattern(format, i, ch);
+ FormatCustomizedTimeZone(dateTime, offset, format, tokenLen, bTimeOnly, result);
+ break;
+ case 'K':
+ tokenLen = 1;
+ FormatCustomizedRoundripTimeZone(dateTime, offset, result);
+ break;
+ case ':':
+ result.Append(dtfi.TimeSeparator);
+ tokenLen = 1;
+ break;
+ case '/':
+ result.Append(dtfi.DateSeparator);
+ tokenLen = 1;
+ break;
+ case '\'':
+ case '\"':
+ tokenLen = ParseQuoteString(format, i, result);
+ break;
+ case '%':
+ // Optional format character.
+ // For example, format string "%d" will print day of month
+ // without leading zero. Most of the cases, "%" can be ignored.
+ nextChar = ParseNextChar(format, i);
+ // nextChar will be -1 if we already reach the end of the format string.
+ // Besides, we will not allow "%%" appear in the pattern.
+ if (nextChar >= 0 && nextChar != (int)'%') {
+ result.Append(FormatCustomized(dateTime, ((char)nextChar).ToString(), dtfi, offset));
+ tokenLen = 2;
+ }
+ else
+ {
+ //
+ // This means that '%' is at the end of the format string or
+ // "%%" appears in the format string.
+ //
+ throw new FormatException(Environment.GetResourceString("Format_InvalidString"));
+ }
+ break;
+ case '\\':
+ // Escaped character. Can be used to insert character into the format string.
+ // For exmple, "\d" will insert the character 'd' into the string.
+ //
+ // NOTENOTE : we can remove this format character if we enforce the enforced quote
+ // character rule.
+ // That is, we ask everyone to use single quote or double quote to insert characters,
+ // then we can remove this character.
+ //
+ nextChar = ParseNextChar(format, i);
+ if (nextChar >= 0)
+ {
+ result.Append(((char)nextChar));
+ tokenLen = 2;
+ }
+ else
+ {
+ //
+ // This means that '\' is at the end of the formatting string.
+ //
+ throw new FormatException(Environment.GetResourceString("Format_InvalidString"));
+ }
+ break;
+ default:
+ // NOTENOTE : we can remove this rule if we enforce the enforced quote
+ // character rule.
+ // That is, if we ask everyone to use single quote or double quote to insert characters,
+ // then we can remove this default block.
+ result.Append(ch);
+ tokenLen = 1;
+ break;
+ }
+ i += tokenLen;
+ }
+ return StringBuilderCache.GetStringAndRelease(result);
+
+ }
+
+
+ // output the 'z' famliy of formats, which output a the offset from UTC, e.g. "-07:30"
+ private static void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, String format, Int32 tokenLen, Boolean timeOnly, StringBuilder result) {
+ // See if the instance already has an offset
+ Boolean dateTimeFormat = (offset == NullOffset);
+ if (dateTimeFormat) {
+ // No offset. The instance is a DateTime and the output should be the local time zone
+
+ if (timeOnly && dateTime.Ticks < Calendar.TicksPerDay) {
+ // For time only format and a time only input, the time offset on 0001/01/01 is less
+ // accurate than the system's current offset because of daylight saving time.
+ offset = TimeZoneInfo.GetLocalUtcOffset(DateTime.Now, TimeZoneInfoOptions.NoThrowOnInvalidTime);
+ } else if (dateTime.Kind == DateTimeKind.Utc) {
+#if FEATURE_CORECLR
+ offset = TimeSpan.Zero;
+#else // FEATURE_CORECLR
+ // This code path points to a bug in user code. It would make sense to return a 0 offset in this case.
+ // However, because it was only possible to detect this in Whidbey, there is user code that takes a
+ // dependency on being serialize a UTC DateTime using the 'z' format, and it will work almost all the
+ // time if it is offset by an incorrect conversion to local time when parsed. Therefore, we need to
+ // explicitly emit the local time offset, which we can do by removing the UTC flag.
+ InvalidFormatForUtc(format, dateTime);
+ dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Local);
+ offset = TimeZoneInfo.GetLocalUtcOffset(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime);
+#endif // FEATURE_CORECLR
+ } else {
+ offset = TimeZoneInfo.GetLocalUtcOffset(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime);
+ }
+ }
+ if (offset >= TimeSpan.Zero) {
+ result.Append('+');
+ }
+ else {
+ result.Append('-');
+ // get a positive offset, so that you don't need a separate code path for the negative numbers.
+ offset = offset.Negate();
+ }
+
+ if (tokenLen <= 1) {
+ // 'z' format e.g "-7"
+ result.AppendFormat(CultureInfo.InvariantCulture, "{0:0}", offset.Hours);
+ }
+ else {
+ // 'zz' or longer format e.g "-07"
+ result.AppendFormat(CultureInfo.InvariantCulture, "{0:00}", offset.Hours);
+ if (tokenLen >= 3) {
+ // 'zzz*' or longer format e.g "-07:30"
+ result.AppendFormat(CultureInfo.InvariantCulture, ":{0:00}", offset.Minutes);
+ }
+ }
+ }
+
+ // output the 'K' format, which is for round-tripping the data
+ private static void FormatCustomizedRoundripTimeZone(DateTime dateTime, TimeSpan offset, StringBuilder result) {
+
+ // The objective of this format is to round trip the data in the type
+ // For DateTime it should round-trip the Kind value and preserve the time zone.
+ // DateTimeOffset instance, it should do so by using the internal time zone.
+
+ if (offset == NullOffset) {
+ // source is a date time, so behavior depends on the kind.
+ switch (dateTime.Kind) {
+ case DateTimeKind.Local:
+ // This should output the local offset, e.g. "-07:30"
+ offset = TimeZoneInfo.GetLocalUtcOffset(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime);
+ // fall through to shared time zone output code
+ break;
+ case DateTimeKind.Utc:
+ // The 'Z' constant is a marker for a UTC date
+ result.Append("Z");
+ return;
+ default:
+ // If the kind is unspecified, we output nothing here
+ return;
+ }
+ }
+ if (offset >= TimeSpan.Zero) {
+ result.Append('+');
+ }
+ else {
+ result.Append('-');
+ // get a positive offset, so that you don't need a separate code path for the negative numbers.
+ offset = offset.Negate();
+ }
+
+ result.AppendFormat(CultureInfo.InvariantCulture, "{0:00}:{1:00}", offset.Hours, offset.Minutes);
+ }
+
+
+ internal static String GetRealFormat(String format, DateTimeFormatInfo dtfi)
+ {
+ String realFormat = null;
+
+ switch (format[0])
+ {
+ case 'd': // Short Date
+ realFormat = dtfi.ShortDatePattern;
+ break;
+ case 'D': // Long Date
+ realFormat = dtfi.LongDatePattern;
+ break;
+ case 'f': // Full (long date + short time)
+ realFormat = dtfi.LongDatePattern + " " + dtfi.ShortTimePattern;
+ break;
+ case 'F': // Full (long date + long time)
+ realFormat = dtfi.FullDateTimePattern;
+ break;
+ case 'g': // General (short date + short time)
+ realFormat = dtfi.GeneralShortTimePattern;
+ break;
+ case 'G': // General (short date + long time)
+ realFormat = dtfi.GeneralLongTimePattern;
+ break;
+ case 'm':
+ case 'M': // Month/Day Date
+ realFormat = dtfi.MonthDayPattern;
+ break;
+ case 'o':
+ case 'O':
+ realFormat = RoundtripFormat;
+ break;
+ case 'r':
+ case 'R': // RFC 1123 Standard
+ realFormat = dtfi.RFC1123Pattern;
+ break;
+ case 's': // Sortable without Time Zone Info
+ realFormat = dtfi.SortableDateTimePattern;
+ break;
+ case 't': // Short Time
+ realFormat = dtfi.ShortTimePattern;
+ break;
+ case 'T': // Long Time
+ realFormat = dtfi.LongTimePattern;
+ break;
+ case 'u': // Universal with Sortable format
+ realFormat = dtfi.UniversalSortableDateTimePattern;
+ break;
+ case 'U': // Universal with Full (long date + long time) format
+ realFormat = dtfi.FullDateTimePattern;
+ break;
+ case 'y':
+ case 'Y': // Year/Month Date
+ realFormat = dtfi.YearMonthPattern;
+ break;
+ default:
+ throw new FormatException(Environment.GetResourceString("Format_InvalidString"));
+ }
+ return (realFormat);
+ }
+
+
+ // Expand a pre-defined format string (like "D" for long date) to the real format that
+ // we are going to use in the date time parsing.
+ // This method also convert the dateTime if necessary (e.g. when the format is in Universal time),
+ // and change dtfi if necessary (e.g. when the format should use invariant culture).
+ //
+ private static String ExpandPredefinedFormat(String format, ref DateTime dateTime, ref DateTimeFormatInfo dtfi, ref TimeSpan offset) {
+ switch (format[0]) {
+ case 'o':
+ case 'O': // Round trip format
+ dtfi = DateTimeFormatInfo.InvariantInfo;
+ break;
+ case 'r':
+ case 'R': // RFC 1123 Standard
+ if (offset != NullOffset) {
+ // Convert to UTC invariants mean this will be in range
+ dateTime = dateTime - offset;
+ }
+ else if (dateTime.Kind == DateTimeKind.Local) {
+ InvalidFormatForLocal(format, dateTime);
+ }
+ dtfi = DateTimeFormatInfo.InvariantInfo;
+ break;
+ case 's': // Sortable without Time Zone Info
+ dtfi = DateTimeFormatInfo.InvariantInfo;
+ break;
+ case 'u': // Universal time in sortable format.
+ if (offset != NullOffset) {
+ // Convert to UTC invariants mean this will be in range
+ dateTime = dateTime - offset;
+ }
+ else if (dateTime.Kind == DateTimeKind.Local) {
+
+ InvalidFormatForLocal(format, dateTime);
+ }
+ dtfi = DateTimeFormatInfo.InvariantInfo;
+ break;
+ case 'U': // Universal time in culture dependent format.
+ if (offset != NullOffset) {
+ // This format is not supported by DateTimeOffset
+ throw new FormatException(Environment.GetResourceString("Format_InvalidString"));
+ }
+ // Universal time is always in Greogrian calendar.
+ //
+ // Change the Calendar to be Gregorian Calendar.
+ //
+ dtfi = (DateTimeFormatInfo)dtfi.Clone();
+ if (dtfi.Calendar.GetType() != typeof(GregorianCalendar)) {
+ dtfi.Calendar = GregorianCalendar.GetDefaultInstance();
+ }
+ dateTime = dateTime.ToUniversalTime();
+ break;
+ }
+ format = GetRealFormat(format, dtfi);
+ return (format);
+ }
+
+ internal static String Format(DateTime dateTime, String format, DateTimeFormatInfo dtfi) {
+ return Format(dateTime, format, dtfi, NullOffset);
+ }
+
+
+ internal static String Format(DateTime dateTime, String format, DateTimeFormatInfo dtfi, TimeSpan offset)
+ {
+ Contract.Requires(dtfi != null);
+ if (format==null || format.Length==0) {
+ Boolean timeOnlySpecialCase = false;
+ if (dateTime.Ticks < Calendar.TicksPerDay) {
+ // If the time is less than 1 day, consider it as time of day.
+ // Just print out the short time format.
+ //
+ // This is a workaround for VB, since they use ticks less then one day to be
+ // time of day. In cultures which use calendar other than Gregorian calendar, these
+ // alternative calendar may not support ticks less than a day.
+ // For example, Japanese calendar only supports date after 1868/9/8.
+ // This will pose a problem when people in VB get the time of day, and use it
+ // to call ToString(), which will use the general format (short date + long time).
+ // Since Japanese calendar does not support Gregorian year 0001, an exception will be
+ // thrown when we try to get the Japanese year for Gregorian year 0001.
+ // Therefore, the workaround allows them to call ToString() for time of day from a DateTime by
+ // formatting as ISO 8601 format.
+ switch (dtfi.Calendar.ID) {
+ case Calendar.CAL_JAPAN:
+ case Calendar.CAL_TAIWAN:
+ case Calendar.CAL_HIJRI:
+ case Calendar.CAL_HEBREW:
+ case Calendar.CAL_JULIAN:
+ case Calendar.CAL_UMALQURA:
+ case Calendar.CAL_PERSIAN:
+ timeOnlySpecialCase = true;
+ dtfi = DateTimeFormatInfo.InvariantInfo;
+ break;
+ }
+ }
+ if (offset == NullOffset) {
+ // Default DateTime.ToString case.
+ if (timeOnlySpecialCase) {
+ format = "s";
+ }
+ else {
+ format = "G";
+ }
+ }
+ else {
+ // Default DateTimeOffset.ToString case.
+ if (timeOnlySpecialCase) {
+ format = RoundtripDateTimeUnfixed;
+ }
+ else {
+ format = dtfi.DateTimeOffsetPattern;
+ }
+
+ }
+
+ }
+
+ if (format.Length == 1) {
+ format = ExpandPredefinedFormat(format, ref dateTime, ref dtfi, ref offset);
+ }
+
+ return (FormatCustomized(dateTime, format, dtfi, offset));
+ }
+
+ internal static String[] GetAllDateTimes(DateTime dateTime, char format, DateTimeFormatInfo dtfi)
+ {
+ Contract.Requires(dtfi != null);
+ String [] allFormats = null;
+ String [] results = null;
+
+ switch (format)
+ {
+ case 'd':
+ case 'D':
+ case 'f':
+ case 'F':
+ case 'g':
+ case 'G':
+ case 'm':
+ case 'M':
+ case 't':
+ case 'T':
+ case 'y':
+ case 'Y':
+ allFormats = dtfi.GetAllDateTimePatterns(format);
+ results = new String[allFormats.Length];
+ for (int i = 0; i < allFormats.Length; i++)
+ {
+ results[i] = Format(dateTime, allFormats[i], dtfi);
+ }
+ break;
+ case 'U':
+ DateTime universalTime = dateTime.ToUniversalTime();
+ allFormats = dtfi.GetAllDateTimePatterns(format);
+ results = new String[allFormats.Length];
+ for (int i = 0; i < allFormats.Length; i++)
+ {
+ results[i] = Format(universalTime, allFormats[i], dtfi);
+ }
+ break;
+ //
+ // The following ones are special cases because these patterns are read-only in
+ // DateTimeFormatInfo.
+ //
+ case 'r':
+ case 'R':
+ case 'o':
+ case 'O':
+ case 's':
+ case 'u':
+ results = new String[] {Format(dateTime, new String(format, 1), dtfi)};
+ break;
+ default:
+ throw new FormatException(Environment.GetResourceString("Format_InvalidString"));
+
+ }
+ return (results);
+ }
+
+ internal static String[] GetAllDateTimes(DateTime dateTime, DateTimeFormatInfo dtfi)
+ {
+ List<String> results = new List<String>(DEFAULT_ALL_DATETIMES_SIZE);
+
+ for (int i = 0; i < allStandardFormats.Length; i++)
+ {
+ String[] strings = GetAllDateTimes(dateTime, allStandardFormats[i], dtfi);
+ for (int j = 0; j < strings.Length; j++)
+ {
+ results.Add(strings[j]);
+ }
+ }
+ String[] value = new String[results.Count];
+ results.CopyTo(0, value, 0, results.Count);
+ return (value);
+ }
+
+ // This is a placeholder for an MDA to detect when the user is using a
+ // local DateTime with a format that will be interpreted as UTC.
+ internal static void InvalidFormatForLocal(String format, DateTime dateTime) {
+ }
+
+ // This is an MDA for cases when the user is using a local format with
+ // a Utc DateTime.
+ [System.Security.SecuritySafeCritical] // auto-generated
+ internal static void InvalidFormatForUtc(String format, DateTime dateTime) {
+#if MDA_SUPPORTED
+ Mda.DateTimeInvalidLocalFormat();
+#endif
+ }
+
+
+ }
+}