diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs index 9f41125199f554..96668b91e1dc05 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DataContract.cs @@ -657,6 +657,8 @@ private static RuntimeTypeHandle GetDataContractAdapterTypeHandle(RuntimeTypeHan "Byte[]" => typeof(byte[]), "Object" => typeof(object), "TimeSpan" => typeof(TimeSpan), + "DateOnly" => typeof(DateOnly), + "TimeOnly" => typeof(TimeOnly), "Guid" => typeof(Guid), "Uri" => typeof(Uri), "Xml.XmlQualifiedName" => typeof(XmlQualifiedName), @@ -745,6 +747,10 @@ internal static bool TryCreateBuiltInDataContract(Type type, [NotNullWhen(true)] dataContract = new QNameDataContract(); else if (type == typeof(TimeSpan)) dataContract = new TimeSpanDataContract(); + else if (type == typeof(DateOnly)) + dataContract = new DateOnlyDataContract(); + else if (type == typeof(TimeOnly)) + dataContract = new TimeOnlyDataContract(); else if (type == typeof(Guid)) dataContract = new GuidDataContract(); else if (type == typeof(Enum) || type == typeof(ValueType)) @@ -864,6 +870,10 @@ internal static bool TryCreateBuiltInDataContract(string name, string ns, [NotNu dataContract = new GuidDataContract(); else if (DictionaryGlobals.CharLocalName.Value == name) dataContract = new CharDataContract(); + else if (DictionaryGlobals.DateOnlyLocalName.Value == name) + dataContract = new DateOnlyDataContract(); + else if (DictionaryGlobals.TimeOnlyLocalName.Value == name) + dataContract = new TimeOnlyDataContract(); else if ("ArrayOfanyType" == name) dataContract = new CollectionDataContract(typeof(Array)); } diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DictionaryGlobals.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DictionaryGlobals.cs index 2008f7689caa7d..1633d4b97f1804 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DictionaryGlobals.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/DictionaryGlobals.cs @@ -8,7 +8,7 @@ namespace System.Runtime.Serialization internal static class DictionaryGlobals { // Update array size when adding new strings or templates - private static readonly XmlDictionary s_dictionary = new XmlDictionary(61); + private static readonly XmlDictionary s_dictionary = new XmlDictionary(63); // 0 public static readonly XmlDictionaryString SchemaInstanceNamespace = s_dictionary.Add(Globals.SchemaInstanceNamespace); @@ -96,5 +96,7 @@ internal static class DictionaryGlobals // 60 public static readonly XmlDictionaryString AsmxTypesNamespace = s_dictionary.Add("http://microsoft.com/wsdl/types/"); + public static readonly XmlDictionaryString DateOnlyLocalName = s_dictionary.Add("DateOnly"); + public static readonly XmlDictionaryString TimeOnlyLocalName = s_dictionary.Add("TimeOnly"); } } diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Globals.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Globals.cs index 060e7f9964be83..acb1c40d56cc72 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Globals.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/Globals.cs @@ -34,6 +34,8 @@ internal static partial class Globals internal static Type TypeOfVoid => field ??= typeof(void); internal static Type TypeOfByteArray => field ??= typeof(byte[]); internal static Type TypeOfTimeSpan => field ??= typeof(TimeSpan); + internal static Type TypeOfDateOnly => field ??= typeof(DateOnly); + internal static Type TypeOfTimeOnly => field ??= typeof(TimeOnly); internal static Type TypeOfGuid => field ??= typeof(Guid); internal static Type TypeOfDateTimeOffset => field ??= typeof(DateTimeOffset); internal static Type TypeOfDateTimeOffsetAdapter => field ??= typeof(DateTimeOffsetAdapter); diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/PrimitiveDataContract.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/PrimitiveDataContract.cs index f0dcbc123013ac..c68a570165f925 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/PrimitiveDataContract.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/PrimitiveDataContract.cs @@ -1018,6 +1018,70 @@ internal sealed class AsmxGuidDataContract : GuidDataContract internal AsmxGuidDataContract() : base(DictionaryGlobals.GuidLocalName, DictionaryGlobals.AsmxTypesNamespace) { } } + internal sealed class DateOnlyDataContract : PrimitiveDataContract + { + public DateOnlyDataContract() : base(typeof(DateOnly), DictionaryGlobals.DateOnlyLocalName, DictionaryGlobals.SerializationNamespace) + { + } + + internal override string WriteMethodName => "WriteDateOnly"; + internal override string ReadMethodName => "ReadElementContentAsDateOnly"; + + [RequiresDynamicCode(DataContract.SerializerAOTWarning)] + [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)] + internal override void WriteXmlValue(XmlWriterDelegator writer, object obj, XmlObjectSerializerWriteContext? context) + { + writer.WriteDateOnly((DateOnly)obj); + } + + [RequiresDynamicCode(DataContract.SerializerAOTWarning)] + [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)] + internal override object? ReadXmlValue(XmlReaderDelegator reader, XmlObjectSerializerReadContext? context) + { + return (context == null) ? reader.ReadElementContentAsDateOnly() + : HandleReadValue(reader.ReadElementContentAsDateOnly(), context); + } + + [RequiresDynamicCode(DataContract.SerializerAOTWarning)] + [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)] + internal override void WriteXmlElement(XmlWriterDelegator xmlWriter, object? obj, XmlObjectSerializerWriteContext context, XmlDictionaryString name, XmlDictionaryString? ns) + { + xmlWriter.WriteDateOnly((DateOnly)obj!, name, ns); + } + } + + internal sealed class TimeOnlyDataContract : PrimitiveDataContract + { + public TimeOnlyDataContract() : base(typeof(TimeOnly), DictionaryGlobals.TimeOnlyLocalName, DictionaryGlobals.SerializationNamespace) + { + } + + internal override string WriteMethodName => "WriteTimeOnly"; + internal override string ReadMethodName => "ReadElementContentAsTimeOnly"; + + [RequiresDynamicCode(DataContract.SerializerAOTWarning)] + [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)] + internal override void WriteXmlValue(XmlWriterDelegator writer, object obj, XmlObjectSerializerWriteContext? context) + { + writer.WriteTimeOnly((TimeOnly)obj); + } + + [RequiresDynamicCode(DataContract.SerializerAOTWarning)] + [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)] + internal override object? ReadXmlValue(XmlReaderDelegator reader, XmlObjectSerializerReadContext? context) + { + return (context == null) ? reader.ReadElementContentAsTimeOnly() + : HandleReadValue(reader.ReadElementContentAsTimeOnly(), context); + } + + [RequiresDynamicCode(DataContract.SerializerAOTWarning)] + [RequiresUnreferencedCode(DataContract.SerializerTrimmerWarning)] + internal override void WriteXmlElement(XmlWriterDelegator xmlWriter, object? obj, XmlObjectSerializerWriteContext context, XmlDictionaryString name, XmlDictionaryString? ns) + { + xmlWriter.WriteTimeOnly((TimeOnly)obj!, name, ns); + } + } + internal sealed class UriDataContract : PrimitiveDataContract { public UriDataContract() : base(typeof(Uri), DictionaryGlobals.UriLocalName, DictionaryGlobals.SchemaNamespace) diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlReaderDelegator.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlReaderDelegator.cs index 210925beab240c..3317403b3d46ed 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlReaderDelegator.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlReaderDelegator.cs @@ -698,6 +698,54 @@ internal Guid ReadElementContentAsGuid() } } + internal DateOnly ReadElementContentAsDateOnly() + { + if (isEndOfEmptyElement) + ThrowNotAtElement(); + + string str = reader.ReadElementContentAsString(); + try + { + return XmlConvert.ToDateOnly(str); + } + catch (ArgumentException exception) + { + throw XmlExceptionHelper.CreateConversionException(str, "DateOnly", exception); + } + catch (FormatException exception) + { + throw XmlExceptionHelper.CreateConversionException(str, "DateOnly", exception); + } + catch (OverflowException exception) + { + throw XmlExceptionHelper.CreateConversionException(str, "DateOnly", exception); + } + } + + internal TimeOnly ReadElementContentAsTimeOnly() + { + if (isEndOfEmptyElement) + ThrowNotAtElement(); + + string str = reader.ReadElementContentAsString(); + try + { + return XmlConvert.ToTimeOnly(str); + } + catch (ArgumentException exception) + { + throw XmlExceptionHelper.CreateConversionException(str, "TimeOnly", exception); + } + catch (FormatException exception) + { + throw XmlExceptionHelper.CreateConversionException(str, "TimeOnly", exception); + } + catch (OverflowException exception) + { + throw XmlExceptionHelper.CreateConversionException(str, "TimeOnly", exception); + } + } + internal Guid ReadContentAsGuid() { string str = reader.ReadContentAsString(); diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlWriterDelegator.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlWriterDelegator.cs index 8ace7ae6187f02..8185b78d445059 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlWriterDelegator.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Runtime/Serialization/XmlWriterDelegator.cs @@ -341,6 +341,10 @@ internal void WriteAnyType(object value, Type valueType) } else if (valueType == Globals.TypeOfTimeSpan) WriteTimeSpan((TimeSpan)value); + else if (valueType == Globals.TypeOfDateOnly) + WriteDateOnly((DateOnly)value); + else if (valueType == Globals.TypeOfTimeOnly) + WriteTimeOnly((TimeOnly)value); else if (valueType == Globals.TypeOfGuid) WriteGuid((Guid)value); else if (valueType == Globals.TypeOfUri) @@ -420,6 +424,10 @@ internal void WriteExtensionData(IDataNode dataNode) } else if (valueType == Globals.TypeOfTimeSpan) WriteTimeSpan(((DataNode)dataNode).GetValue()); + else if (valueType == Globals.TypeOfDateOnly) + WriteDateOnly(((DataNode)dataNode).GetValue()); + else if (valueType == Globals.TypeOfTimeOnly) + WriteTimeOnly(((DataNode)dataNode).GetValue()); else if (valueType == Globals.TypeOfGuid) WriteGuid(((DataNode)dataNode).GetValue()); else if (valueType == Globals.TypeOfUri) @@ -639,6 +647,30 @@ internal void WriteGuid(Guid value, XmlDictionaryString name, XmlDictionaryStrin WriteEndElementPrimitive(); } + internal void WriteDateOnly(DateOnly value) + { + writer.WriteRaw(XmlConvert.ToString(value)); + } + + internal void WriteDateOnly(DateOnly value, XmlDictionaryString name, XmlDictionaryString? ns) + { + WriteStartElementPrimitive(name, ns); + WriteDateOnly(value); + WriteEndElementPrimitive(); + } + + internal void WriteTimeOnly(TimeOnly value) + { + writer.WriteRaw(XmlConvert.ToString(value)); + } + + internal void WriteTimeOnly(TimeOnly value, XmlDictionaryString name, XmlDictionaryString? ns) + { + WriteStartElementPrimitive(name, ns); + WriteTimeOnly(value); + WriteEndElementPrimitive(); + } + internal void WriteUri(Uri? value) { writer.WriteString(value?.GetComponents(UriComponents.SerializationInfoString, UriFormat.UriEscaped)); diff --git a/src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlConverter.cs b/src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlConverter.cs index 0f2c9162a850e1..48e9f2baa396e3 100644 --- a/src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlConverter.cs +++ b/src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlConverter.cs @@ -282,6 +282,56 @@ public static TimeSpan ToTimeSpan(byte[] buffer, int offset, int count) return ToTimeSpan(ToString(buffer, offset, count)); } + public static DateOnly ToDateOnly(string value) + { + try + { + return XmlConvert.ToDateOnly(value); + } + catch (ArgumentException exception) + { + throw XmlExceptionHelper.CreateConversionException(value, "DateOnly", exception); + } + catch (FormatException exception) + { + throw XmlExceptionHelper.CreateConversionException(value, "DateOnly", exception); + } + catch (OverflowException exception) + { + throw XmlExceptionHelper.CreateConversionException(value, "DateOnly", exception); + } + } + + public static DateOnly ToDateOnly(byte[] buffer, int offset, int count) + { + return ToDateOnly(ToString(buffer, offset, count)); + } + + public static TimeOnly ToTimeOnly(string value) + { + try + { + return XmlConvert.ToTimeOnly(value); + } + catch (ArgumentException exception) + { + throw XmlExceptionHelper.CreateConversionException(value, "TimeOnly", exception); + } + catch (FormatException exception) + { + throw XmlExceptionHelper.CreateConversionException(value, "TimeOnly", exception); + } + catch (OverflowException exception) + { + throw XmlExceptionHelper.CreateConversionException(value, "TimeOnly", exception); + } + } + + public static TimeOnly ToTimeOnly(byte[] buffer, int offset, int count) + { + return ToTimeOnly(ToString(buffer, offset, count)); + } + public static Guid ToGuid(string value) { try diff --git a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/PrimitiveXmlSerializers.cs b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/PrimitiveXmlSerializers.cs index fca690ef8095a3..2fdac41795bd4d 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/Serialization/PrimitiveXmlSerializers.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/Serialization/PrimitiveXmlSerializers.cs @@ -214,6 +214,30 @@ internal void Write_TimeSpan(object? o) WriteElementStringRaw(@"TimeSpan", @"", System.Xml.XmlConvert.ToString(timeSpan)); } + internal void Write_DateOnly(object? o) + { + WriteStartDocument(); + if (o == null) + { + WriteEmptyTag(@"DateOnly", @""); + return; + } + DateOnly dateOnly = (DateOnly)o; + WriteElementStringRaw(@"DateOnly", @"", System.Xml.XmlConvert.ToString(dateOnly)); + } + + internal void Write_TimeOnly(object? o) + { + WriteStartDocument(); + if (o == null) + { + WriteEmptyTag(@"TimeOnly", @""); + return; + } + TimeOnly timeOnly = (TimeOnly)o; + WriteElementStringRaw(@"TimeOnly", @"", System.Xml.XmlConvert.ToString(timeOnly)); + } + internal void Write_char(object? o) { WriteStartDocument(); @@ -699,6 +723,66 @@ internal sealed class XmlSerializationPrimitiveReader : System.Xml.Serialization return (object?)o; } + internal object? Read_DateOnly() + { + object? o = null; + Reader.MoveToContent(); + if (Reader.NodeType == System.Xml.XmlNodeType.Element) + { + if (((object)Reader.LocalName == (object)_id21_DateOnly && (object)Reader.NamespaceURI == (object)_id2_Item)) + { + if (Reader.IsEmptyElement) + { + Reader.Skip(); + o = default(DateOnly); + } + else + { + o = System.Xml.XmlConvert.ToDateOnly(Reader.ReadElementString()); + } + } + else + { + throw CreateUnknownNodeException(); + } + } + else + { + UnknownNode(null); + } + return (object?)o; + } + + internal object? Read_TimeOnly() + { + object? o = null; + Reader.MoveToContent(); + if (Reader.NodeType == System.Xml.XmlNodeType.Element) + { + if (((object)Reader.LocalName == (object)_id22_TimeOnly && (object)Reader.NamespaceURI == (object)_id2_Item)) + { + if (Reader.IsEmptyElement) + { + Reader.Skip(); + o = default(TimeOnly); + } + else + { + o = System.Xml.XmlConvert.ToTimeOnly(Reader.ReadElementString()); + } + } + else + { + throw CreateUnknownNodeException(); + } + } + else + { + UnknownNode(null); + } + return (object?)o; + } + internal object? Read_char() { object? o = null; @@ -769,6 +853,8 @@ protected override void InitCallbacks() private string _id8_double = null!; private string _id17_guid = null!; private string _id19_TimeSpan = null!; + private string _id21_DateOnly = null!; + private string _id22_TimeOnly = null!; private string _id2_Item = null!; private string _id13_unsignedShort = null!; private string _id18_char = null!; @@ -793,6 +879,8 @@ protected override void InitIDs() _id8_double = Reader.NameTable.Add(@"double"); _id17_guid = Reader.NameTable.Add(@"guid"); _id19_TimeSpan = Reader.NameTable.Add(@"TimeSpan"); + _id21_DateOnly = Reader.NameTable.Add(@"DateOnly"); + _id22_TimeOnly = Reader.NameTable.Add(@"TimeOnly"); _id2_Item = Reader.NameTable.Add(@""); _id13_unsignedShort = Reader.NameTable.Add(@"unsignedShort"); _id18_char = Reader.NameTable.Add(@"char"); diff --git a/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs b/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs index efe07812b079cf..5465ba20e5df2a 100644 --- a/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs +++ b/src/libraries/System.Private.Xml/src/System/Xml/XmlConvert.cs @@ -1285,6 +1285,30 @@ public static Guid ToGuid(string s) return exception; } + public static string ToString(DateOnly value) + { + return value.ToString("O", CultureInfo.InvariantCulture); + } + + public static DateOnly ToDateOnly(string s) + { + ArgumentNullException.ThrowIfNull(s); + + return DateOnly.ParseExact(s, "O", CultureInfo.InvariantCulture, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite); + } + + public static string ToString(TimeOnly value) + { + return value.ToString("O", CultureInfo.InvariantCulture); + } + + public static TimeOnly ToTimeOnly(string s) + { + ArgumentNullException.ThrowIfNull(s); + + return TimeOnly.ParseExact(s, "O", CultureInfo.InvariantCulture, DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite); + } + private static DateTime SwitchToLocalTime(DateTime value) => value.Kind switch { @@ -1803,5 +1827,15 @@ internal static bool TryFormat(Guid value, Span destination, out int chars { return value.TryFormat(destination, out charsWritten); } + + internal static bool TryFormat(DateOnly value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, "O", CultureInfo.InvariantCulture); + } + + internal static bool TryFormat(TimeOnly value, Span destination, out int charsWritten) + { + return value.TryFormat(destination, out charsWritten, "O", CultureInfo.InvariantCulture); + } } } diff --git a/src/libraries/System.Private.Xml/tests/XmlConvert/ToTypeTests.cs b/src/libraries/System.Private.Xml/tests/XmlConvert/ToTypeTests.cs index d8ff50e4aee396..cb568a1b9d1361 100644 --- a/src/libraries/System.Private.Xml/tests/XmlConvert/ToTypeTests.cs +++ b/src/libraries/System.Private.Xml/tests/XmlConvert/ToTypeTests.cs @@ -76,6 +76,14 @@ public override void AddChildren() AddChild(new CVariation(ToType59) { Attribute = new Variation("4. Roundtrip: String-DateTimeOffset-String") { Param = "9999-12-31T23:59:59+14:00" } }); AddChild(new CVariation(ToType59) { Attribute = new Variation("1. Roundtrip: String-DateTimeOffset-String") { Param = "0001-01-01T00:00:00Z" } }); AddChild(new CVariation(ToType62) { Attribute = new Variation("null params DateTime, DateTimeOffset - invalid cases") }); + AddChild(new CVariation(ToType63) { Attribute = new Variation("ToDateOnly - valid cases") }); + AddChild(new CVariation(ToType64) { Attribute = new Variation("ToDateOnly - invalid cases") }); + AddChild(new CVariation(ToType65) { Attribute = new Variation("ToTimeOnly - valid cases") }); + AddChild(new CVariation(ToType66) { Attribute = new Variation("ToTimeOnly - invalid cases") }); + AddChild(new CVariation(ToType67) { Attribute = new Variation("ToString(DateOnly) - valid cases") }); + AddChild(new CVariation(ToType68) { Attribute = new Variation("ToString(TimeOnly) - valid cases") }); + AddChild(new CVariation(ToType69) { Attribute = new Variation("Roundtrip: DateOnly-String-DateOnly") }); + AddChild(new CVariation(ToType70) { Attribute = new Variation("Roundtrip: TimeOnly-String-TimeOnly") }); } public int TestInvalid(string[] array, string type) @@ -126,6 +134,12 @@ public int TestInvalid(string[] array, string type, string[] format) case "timespan": XmlConvert.ToTimeSpan(array[i]); break; + case "dateonly": + XmlConvert.ToDateOnly(array[i]); + break; + case "timeonly": + XmlConvert.ToTimeOnly(array[i]); + break; case "uint16": XmlConvert.ToUInt16(array[i]); break; @@ -253,6 +267,14 @@ public int TestValid(object[] array0, object[] array1, string type, string[] for actual = XmlConvert.ToTimeSpan((string)array0[i]); expect = (TimeSpan)array1[i]; break; + case "dateonly": + actual = XmlConvert.ToDateOnly((string)array0[i]); + expect = (DateOnly)array1[i]; + break; + case "timeonly": + actual = XmlConvert.ToTimeOnly((string)array0[i]); + expect = (TimeOnly)array1[i]; + break; case "uint16": actual = XmlConvert.ToUInt16((string)array0[i]); expect = (ushort)array1[i]; @@ -1130,6 +1152,108 @@ private TimeSpan GetOffsetFromUtc(DateTime dt) TimeSpan offset = TimeZoneInfo.Local.GetUtcOffset(dt); return offset; } + + //[Variation("ToDateOnly - valid cases")] + public int ToType63() + { + object[] array0 = { "2024-01-15", "0001-01-01", "9999-12-31", "2000-02-29" }; + object[] array1 = { new DateOnly(2024, 1, 15), new DateOnly(1, 1, 1), new DateOnly(9999, 12, 31), new DateOnly(2000, 2, 29) }; + return TestValid(array0, array1, "dateonly"); + } + + //[Variation("ToDateOnly - invalid cases")] + public int ToType64() + { + string[] array = { "2024-13-01", "2024-01-32", "invalid", "2024/01/15", null, "2024-01-15T10:30:00", "" }; + return TestInvalid(array, "dateonly"); + } + + //[Variation("ToTimeOnly - valid cases")] + public int ToType65() + { + object[] array0 = { "14:30:45.1230000", "00:00:00", "23:59:59.9999999", "12:30:00" }; + object[] array1 = { new TimeOnly(14, 30, 45, 123), new TimeOnly(0, 0, 0), new TimeOnly(23, 59, 59, 999, 999), new TimeOnly(12, 30, 0) }; + return TestValid(array0, array1, "timeonly"); + } + + //[Variation("ToTimeOnly - invalid cases")] + public int ToType66() + { + string[] array = { "25:00:00", "12:60:00", "12:30:60", "invalid", null, "2024-01-15T12:30:00", "12:30", "" }; + return TestInvalid(array, "timeonly"); + } + + //[Variation("ToString(DateOnly) - valid cases")] + public int ToType67() + { + DateOnly[] input = { new DateOnly(2024, 1, 15), new DateOnly(1, 1, 1), new DateOnly(9999, 12, 31), new DateOnly(2000, 2, 29) }; + string[] expected = { "2024-01-15", "0001-01-01", "9999-12-31", "2000-02-29" }; + + for (int i = 0; i < input.Length; i++) + { + string result = XmlConvert.ToString(input[i]); + if (result != expected[i]) + { + CError.WriteLine($"Failed DateOnly ToString: {input[i]} -> {result}, expected {expected[i]}"); + return TEST_FAIL; + } + } + return TEST_PASS; + } + + //[Variation("ToString(TimeOnly) - valid cases")] + public int ToType68() + { + TimeOnly[] input = { new TimeOnly(14, 30, 45, 123), new TimeOnly(0, 0, 0), new TimeOnly(23, 59, 59, 999, 999), new TimeOnly(12, 30, 0) }; + string[] expected = { "14:30:45.1230000", "00:00:00", "23:59:59.9999999", "12:30:00" }; + + for (int i = 0; i < input.Length; i++) + { + string result = XmlConvert.ToString(input[i]); + if (result != expected[i]) + { + CError.WriteLine($"Failed TimeOnly ToString: {input[i]} -> {result}, expected {expected[i]}"); + return TEST_FAIL; + } + } + return TEST_PASS; + } + + //[Variation("Roundtrip: DateOnly-String-DateOnly")] + public int ToType69() + { + DateOnly[] testValues = { new DateOnly(2024, 1, 15), new DateOnly(1, 1, 1), new DateOnly(9999, 12, 31), new DateOnly(2000, 2, 29) }; + + for (int i = 0; i < testValues.Length; i++) + { + string str = XmlConvert.ToString(testValues[i]); + DateOnly roundTrip = XmlConvert.ToDateOnly(str); + if (roundTrip != testValues[i]) + { + CError.WriteLine($"Failed DateOnly roundtrip: {testValues[i]} -> {str} -> {roundTrip}"); + return TEST_FAIL; + } + } + return TEST_PASS; + } + + //[Variation("Roundtrip: TimeOnly-String-TimeOnly")] + public int ToType70() + { + TimeOnly[] testValues = { new TimeOnly(14, 30, 45, 123), new TimeOnly(0, 0, 0), new TimeOnly(23, 59, 59, 999, 999), new TimeOnly(12, 30, 0) }; + + for (int i = 0; i < testValues.Length; i++) + { + string str = XmlConvert.ToString(testValues[i]); + TimeOnly roundTrip = XmlConvert.ToTimeOnly(str); + if (roundTrip != testValues[i]) + { + CError.WriteLine($"Failed TimeOnly roundtrip: {testValues[i]} -> {str} -> {roundTrip}"); + return TEST_FAIL; + } + } + return TEST_PASS; + } #endregion } } diff --git a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs index 0d94c9fb861b25..b1ea8099253931 100644 --- a/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs +++ b/src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.cs @@ -967,6 +967,56 @@ public static void Xml_DeserializeEmptyTimeSpanType() } } + [Fact] + public static void Xml_TypeWithDateOnlyProperty() + { + var obj = new TypeWithDateOnlyProperty { DateOnlyProperty = new DateOnly(2024, 1, 15) }; + var deserializedObj = SerializeAndDeserialize(obj, WithXmlHeader(@" +2024-01-15 +")); + Assert.StrictEqual(obj.DateOnlyProperty, deserializedObj.DateOnlyProperty); + } + + [Fact] + public static void Xml_TypeWithTimeOnlyProperty() + { + var obj = new TypeWithTimeOnlyProperty { TimeOnlyProperty = new TimeOnly(14, 30, 45, 123) }; + var deserializedObj = SerializeAndDeserialize(obj, WithXmlHeader(@" +14:30:45.1230000 +")); + Assert.StrictEqual(obj.TimeOnlyProperty, deserializedObj.TimeOnlyProperty); + } + + [Fact] + public static void Xml_DeserializeEmptyDateOnlyType() + { + string xml = + @" + "; + XmlSerializer serializer = new XmlSerializer(typeof(DateOnly)); + + using (StringReader reader = new StringReader(xml)) + { + DateOnly deserializedObj = (DateOnly)serializer.Deserialize(reader); + Assert.Equal(default(DateOnly), deserializedObj); + } + } + + [Fact] + public static void Xml_DeserializeEmptyTimeOnlyType() + { + string xml = + @" + "; + XmlSerializer serializer = new XmlSerializer(typeof(TimeOnly)); + + using (StringReader reader = new StringReader(xml)) + { + TimeOnly deserializedObj = (TimeOnly)serializer.Deserialize(reader); + Assert.Equal(default(TimeOnly), deserializedObj); + } + } + [ConditionalFact(nameof(DefaultValueAttributeIsSupported))] public static void Xml_TypeWithDateTimeOffsetProperty() { diff --git a/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs b/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs index 7f5599020944f7..9c1a127361d446 100644 --- a/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs +++ b/src/libraries/System.Runtime.Serialization.Xml/tests/DataContractSerializer.cs @@ -168,6 +168,38 @@ public static void DCS_GuidAsRoot() } } + [Fact] + public static void DCS_DateOnlyAsRoot() + { + DateOnly[] testValues = { + new DateOnly(2024, 1, 15), + new DateOnly(1, 1, 1), + new DateOnly(9999, 12, 31), + new DateOnly(2000, 2, 29) // leap year + }; + + foreach (DateOnly value in testValues) + { + Assert.StrictEqual(DataContractSerializerHelper.SerializeAndDeserialize(value, string.Format(@"{0}", value.ToString("O"))), value); + } + } + + [Fact] + public static void DCS_TimeOnlyAsRoot() + { + TimeOnly[] testValues = { + new TimeOnly(14, 30, 45, 123), + new TimeOnly(0, 0, 0), + new TimeOnly(23, 59, 59, 999, 999), + new TimeOnly(12, 30, 0) + }; + + foreach (TimeOnly value in testValues) + { + Assert.StrictEqual(DataContractSerializerHelper.SerializeAndDeserialize(value, string.Format(@"{0}", value.ToString("O"))), value); + } + } + [Fact] public static void DCS_IntAsRoot() { diff --git a/src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.cs b/src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.cs index 74f2019d453034..129db3a9b20425 100644 --- a/src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.cs +++ b/src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.cs @@ -1158,6 +1158,16 @@ public TimeSpan GetDefaultValue(string propertyName) } } +public class TypeWithDateOnlyProperty +{ + public DateOnly DateOnlyProperty; +} + +public class TypeWithTimeOnlyProperty +{ + public TimeOnly TimeOnlyProperty; +} + public class TypeWithByteProperty { public byte ByteProperty; diff --git a/src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.cs b/src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.cs index b839aba44ee077..6a7feceb9ec435 100644 --- a/src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.cs +++ b/src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.cs @@ -226,6 +226,8 @@ public XmlConvert() { } public static decimal ToDecimal(string s) { throw null; } public static double ToDouble(string s) { throw null; } public static System.Guid ToGuid(string s) { throw null; } + public static System.DateOnly ToDateOnly(string s) { throw null; } + public static System.TimeOnly ToTimeOnly(string s) { throw null; } public static short ToInt16(string s) { throw null; } public static int ToInt32(string s) { throw null; } public static long ToInt64(string s) { throw null; } @@ -244,6 +246,8 @@ public XmlConvert() { } public static string ToString(decimal value) { throw null; } public static string ToString(double value) { throw null; } public static string ToString(System.Guid value) { throw null; } + public static string ToString(System.DateOnly value) { throw null; } + public static string ToString(System.TimeOnly value) { throw null; } public static string ToString(short value) { throw null; } public static string ToString(int value) { throw null; } public static string ToString(long value) { throw null; }