Skip to content
This repository was archived by the owner on Dec 24, 2022. It is now read-only.

Commit 2dbc177

Browse files
author
Doug Schmidt
committed
Fixes two bugs in deserializing DateTime objects from strings.
1) Long-form dates "yyyy-MM-ddTHH:mm:ss.fffffffZ" are now correctedly parsed with Kind=Utc Previously, only short-form dates (no trailing ".ffff" milliseconds) were parsed as Kind=Utc. Long-form were parsed as Kind=Local 2) Added RepairXsdTimeSeparator() method to work around a bug in System.Data.SQLite v1.0.88, which sometimes uses a space instead of the 'T' separator between the time and date. Obligatory XKCD reference: http://xkcd.com/1179/
1 parent 9347b76 commit 2dbc177

2 files changed

Lines changed: 66 additions & 4 deletions

File tree

src/ServiceStack.Text/Common/DateTimeSerializer.cs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@ public static class DateTimeSerializer
3737
public const string UnspecifiedOffset = "-0000";
3838
public const string UtcOffset = "+0000";
3939

40+
private const char XsdTimeSeparator = 'T';
41+
private static readonly int XsdTimeSeparatorIndex = XsdDateTimeFormat.IndexOf(XsdTimeSeparator);
42+
private const string XsdUtcSuffix = "Z";
43+
4044
/// <summary>
4145
/// If AlwaysUseUtc is set to true then convert all DateTime to UTC.
4246
/// </summary>
@@ -73,12 +77,14 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
7377
return DateTime.Parse(dateTimeStr, CultureInfo.InvariantCulture).Prepare();
7478
}
7579

80+
dateTimeStr = RepairXsdTimeSeparator(dateTimeStr);
81+
7682
if (dateTimeStr.Length == XsdDateTimeFormatSeconds.Length)
7783
return DateTime.ParseExact(dateTimeStr, XsdDateTimeFormatSeconds, null, DateTimeStyles.AdjustToUniversal).Prepare(parsedAsUtc:true);
7884

7985
if (dateTimeStr.Length >= XsdDateTimeFormat3F.Length
8086
&& dateTimeStr.Length <= XsdDateTimeFormat.Length
81-
&& dateTimeStr.EndsWith("Z"))
87+
&& dateTimeStr.EndsWith(XsdUtcSuffix))
8288
{
8389
#if NETFX_CORE
8490
var dateTimeType = JsConfig.DateHandler != JsonDateHandler.ISO8601
@@ -91,7 +97,7 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
9197
if (dateTime != null)
9298
return dateTime.Value;
9399

94-
return XmlConvert.ToDateTime(dateTimeStr, XmlDateTimeSerializationMode.Utc).Prepare();
100+
return XmlConvert.ToDateTime(dateTimeStr, XmlDateTimeSerializationMode.Utc).Prepare(parsedAsUtc:true);
95101
#endif
96102
}
97103

@@ -109,13 +115,30 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
109115
}
110116
}
111117

118+
/// <summary>
119+
/// Repairs an out-of-spec XML date/time string which incorrectly uses a space instead of a 'T' to separate the date from the time.
120+
/// These string are occasionally generated by SQLite and can cause errors in OrmLite when reading these columns from the DB.
121+
/// </summary>
122+
/// <param name="dateTimeStr">The XML date/time string to repair</param>
123+
/// <returns>The repaired string. If no repairs were made, the original string is returned.</returns>
124+
private static string RepairXsdTimeSeparator(string dateTimeStr)
125+
{
126+
if( (dateTimeStr.Length > XsdTimeSeparatorIndex) && (dateTimeStr[XsdTimeSeparatorIndex] == ' ') && dateTimeStr.EndsWith(XsdUtcSuffix) )
127+
{
128+
dateTimeStr = dateTimeStr.Substring(0, XsdTimeSeparatorIndex) + XsdTimeSeparator +
129+
dateTimeStr.Substring(XsdTimeSeparatorIndex + 1);
130+
}
131+
132+
return dateTimeStr;
133+
}
134+
112135
public static DateTime? ParseManual(string dateTimeStr)
113136
{
114137
if (dateTimeStr == null || dateTimeStr.Length < "YYYY-MM-DD".Length)
115138
return null;
116139

117140
var dateKind = DateTimeKind.Utc;
118-
if (dateTimeStr.EndsWith("Z"))
141+
if (dateTimeStr.EndsWith(XsdUtcSuffix))
119142
{
120143
dateTimeStr = dateTimeStr.Substring(0, dateTimeStr.Length - 1);
121144
}
@@ -224,7 +247,7 @@ public static DateTimeOffset ParseDateTimeOffset(string dateTimeOffsetStr)
224247
// assume utc when no offset specified
225248
if (dateTimeOffsetStr.LastIndexOfAny(TimeZoneChars) < 10)
226249
{
227-
if (!dateTimeOffsetStr.EndsWith("Z")) dateTimeOffsetStr += "Z";
250+
if (!dateTimeOffsetStr.EndsWith(XsdUtcSuffix)) dateTimeOffsetStr += XsdUtcSuffix;
228251
#if __MonoCS__
229252
// Without that Mono uses a Local timezone))
230253
dateTimeOffsetStr = dateTimeOffsetStr.Substring(0, dateTimeOffsetStr.Length - 1) + "+00:00";
@@ -267,6 +290,8 @@ public static string ToXsdTimeSpanString(TimeSpan? timeSpan)
267290

268291
public static DateTime ParseXsdDateTime(string dateTimeStr)
269292
{
293+
dateTimeStr = RepairXsdTimeSeparator(dateTimeStr);
294+
270295
#if NETFX_CORE
271296
return XmlConvert.ToDateTimeOffset(dateTimeStr).DateTime;
272297
#else

tests/ServiceStack.Text.Tests/Utils/DateTimeSerializerTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,43 @@ public void UtcDateTime_Is_Deserialized_As_Kind_Utc()
147147
Assert.That(deserialized.Kind, Is.EqualTo(DateTimeKind.Utc)); //fails -> is DateTimeKind.Local
148148
}
149149

150+
/// <summary>
151+
/// These timestamp strings were pulled from SQLite columns written via OrmLite using SQlite.1.88
152+
/// Most of the time, timestamps correctly use the 'T' separator between the date and time,
153+
/// but under some (still unknown) scnearios, SQLite will write timestamps using a space instead of a 'T'.
154+
/// If that happens, OrmLite will fail to read the row, complaining that: The string '...' is not a valid Xsd value.
155+
/// </summary>
156+
private static string[] _problematicXsdStrings = new[] {
157+
"2013-10-10 20:04:04.8773249Z",
158+
"2013-10-10 20:04:04Z",
159+
};
160+
161+
[Test]
162+
[TestCase(0)]
163+
[TestCase(1)]
164+
public void CanParseProblematicXsdStrings(int whichString)
165+
{
166+
var xsdString = _problematicXsdStrings[whichString];
167+
168+
var dateTime = DateTimeSerializer.ParseShortestXsdDateTime(xsdString);
169+
170+
Assert.That(dateTime.Kind, Is.EqualTo(DateTimeKind.Local));
171+
}
172+
173+
[Test]
174+
public void CanParseLongAndShortXsdStrings()
175+
{
176+
var shortXsdString = "2013-10-10T13:40:50Z";
177+
var longXsdString = shortXsdString.Substring(0, shortXsdString.Length - 1) + ".0000000" +
178+
shortXsdString.Substring(shortXsdString.Length - 1);
179+
180+
var dateTimeShort = DateTimeSerializer.ParseShortestXsdDateTime(shortXsdString);
181+
var dateTimeLong = DateTimeSerializer.ParseShortestXsdDateTime(longXsdString);
182+
183+
Assert.That(dateTimeShort.Ticks, Is.EqualTo(dateTimeLong.Ticks));
184+
Assert.That(dateTimeShort.Kind, Is.EqualTo(dateTimeLong.Kind));
185+
}
186+
150187
private static DateTime[] _dateTimeTests = new[] {
151188
DateTime.Now,
152189
DateTime.UtcNow,

0 commit comments

Comments
 (0)