diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index ed8880c117feae..aff995d4cae217 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -63,6 +63,7 @@ public readonly partial struct JsonElement public sbyte GetSByte() { throw null; } public float GetSingle() { throw null; } public string? GetString() { throw null; } + public System.TimeSpan GetTimeSpan() { throw null; } [System.CLSCompliantAttribute(false)] public ushort GetUInt16() { throw null; } [System.CLSCompliantAttribute(false)] @@ -87,6 +88,7 @@ public readonly partial struct JsonElement [System.CLSCompliantAttribute(false)] public bool TryGetSByte(out sbyte value) { throw null; } public bool TryGetSingle(out float value) { throw null; } + public bool TryGetTimeSpan(out System.TimeSpan value) { throw null; } [System.CLSCompliantAttribute(false)] public bool TryGetUInt16(out ushort value) { throw null; } [System.CLSCompliantAttribute(false)] @@ -345,6 +347,7 @@ public ref partial struct Utf8JsonReader public sbyte GetSByte() { throw null; } public float GetSingle() { throw null; } public string? GetString() { throw null; } + public System.TimeSpan GetTimeSpan() { throw null; } [System.CLSCompliantAttribute(false)] public ushort GetUInt16() { throw null; } [System.CLSCompliantAttribute(false)] @@ -366,6 +369,7 @@ public void Skip() { } [System.CLSCompliantAttribute(false)] public bool TryGetSByte(out sbyte value) { throw null; } public bool TryGetSingle(out float value) { throw null; } + public bool TryGetTimeSpan(out System.TimeSpan value) { throw null; } [System.CLSCompliantAttribute(false)] public bool TryGetUInt16(out ushort value) { throw null; } [System.CLSCompliantAttribute(false)] @@ -477,6 +481,7 @@ public void WriteString(System.ReadOnlySpan utf8PropertyName, System.Guid public void WriteString(System.ReadOnlySpan utf8PropertyName, System.ReadOnlySpan utf8Value) { } public void WriteString(System.ReadOnlySpan utf8PropertyName, System.ReadOnlySpan value) { } public void WriteString(System.ReadOnlySpan utf8PropertyName, string? value) { } + public void WriteString(System.ReadOnlySpan utf8PropertyName, System.TimeSpan value) { } public void WriteString(System.ReadOnlySpan utf8PropertyName, System.Text.Json.JsonEncodedText value) { } public void WriteString(System.ReadOnlySpan propertyName, System.DateTime value) { } public void WriteString(System.ReadOnlySpan propertyName, System.DateTimeOffset value) { } @@ -484,6 +489,7 @@ public void WriteString(System.ReadOnlySpan propertyName, System.Guid valu public void WriteString(System.ReadOnlySpan propertyName, System.ReadOnlySpan utf8Value) { } public void WriteString(System.ReadOnlySpan propertyName, System.ReadOnlySpan value) { } public void WriteString(System.ReadOnlySpan propertyName, string? value) { } + public void WriteString(System.ReadOnlySpan propertyName, System.TimeSpan value) { } public void WriteString(System.ReadOnlySpan propertyName, System.Text.Json.JsonEncodedText value) { } public void WriteString(string propertyName, System.DateTime value) { } public void WriteString(string propertyName, System.DateTimeOffset value) { } @@ -491,6 +497,7 @@ public void WriteString(string propertyName, System.Guid value) { } public void WriteString(string propertyName, System.ReadOnlySpan utf8Value) { } public void WriteString(string propertyName, System.ReadOnlySpan value) { } public void WriteString(string propertyName, string? value) { } + public void WriteString(string propertyName, System.TimeSpan value) { } public void WriteString(string propertyName, System.Text.Json.JsonEncodedText value) { } public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.DateTime value) { } public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.DateTimeOffset value) { } @@ -498,6 +505,7 @@ public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.Gu public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.ReadOnlySpan utf8Value) { } public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.ReadOnlySpan value) { } public void WriteString(System.Text.Json.JsonEncodedText propertyName, string? value) { } + public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.TimeSpan value) { } public void WriteString(System.Text.Json.JsonEncodedText propertyName, System.Text.Json.JsonEncodedText value) { } public void WriteStringValue(System.DateTime value) { } public void WriteStringValue(System.DateTimeOffset value) { } @@ -505,6 +513,7 @@ public void WriteStringValue(System.Guid value) { } public void WriteStringValue(System.ReadOnlySpan utf8Value) { } public void WriteStringValue(System.ReadOnlySpan value) { } public void WriteStringValue(string? value) { } + public void WriteStringValue(System.TimeSpan value) { } public void WriteStringValue(System.Text.Json.JsonEncodedText value) { } } } @@ -550,6 +559,7 @@ internal JsonNode() { } public static explicit operator char (System.Text.Json.Nodes.JsonNode value) { throw null; } public static explicit operator System.DateTime (System.Text.Json.Nodes.JsonNode value) { throw null; } public static explicit operator System.DateTimeOffset (System.Text.Json.Nodes.JsonNode value) { throw null; } + public static explicit operator System.TimeSpan (System.Text.Json.Nodes.JsonNode value) { throw null; } public static explicit operator decimal (System.Text.Json.Nodes.JsonNode value) { throw null; } public static explicit operator double (System.Text.Json.Nodes.JsonNode value) { throw null; } public static explicit operator System.Guid (System.Text.Json.Nodes.JsonNode value) { throw null; } @@ -561,6 +571,7 @@ internal JsonNode() { } public static explicit operator char? (System.Text.Json.Nodes.JsonNode? value) { throw null; } public static explicit operator System.DateTimeOffset? (System.Text.Json.Nodes.JsonNode? value) { throw null; } public static explicit operator System.DateTime? (System.Text.Json.Nodes.JsonNode? value) { throw null; } + public static explicit operator System.TimeSpan? (System.Text.Json.Nodes.JsonNode? value) { throw null; } public static explicit operator decimal? (System.Text.Json.Nodes.JsonNode? value) { throw null; } public static explicit operator double? (System.Text.Json.Nodes.JsonNode? value) { throw null; } public static explicit operator System.Guid? (System.Text.Json.Nodes.JsonNode? value) { throw null; } @@ -591,6 +602,7 @@ internal JsonNode() { } public static implicit operator System.Text.Json.Nodes.JsonNode (char value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode (System.DateTime value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode (System.DateTimeOffset value) { throw null; } + public static implicit operator System.Text.Json.Nodes.JsonNode (System.TimeSpan value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode (decimal value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode (double value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode (System.Guid value) { throw null; } @@ -602,6 +614,7 @@ internal JsonNode() { } public static implicit operator System.Text.Json.Nodes.JsonNode? (char? value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode? (System.DateTimeOffset? value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode? (System.DateTime? value) { throw null; } + public static implicit operator System.Text.Json.Nodes.JsonNode? (System.TimeSpan? value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode? (decimal? value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode? (double? value) { throw null; } public static implicit operator System.Text.Json.Nodes.JsonNode? (System.Guid? value) { throw null; } @@ -672,6 +685,7 @@ public abstract partial class JsonValue : System.Text.Json.Nodes.JsonNode public static System.Text.Json.Nodes.JsonValue Create(char value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue Create(System.DateTime value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue Create(System.DateTimeOffset value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } + public static System.Text.Json.Nodes.JsonValue Create(System.TimeSpan value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue Create(decimal value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue Create(double value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue Create(System.Guid value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } @@ -683,6 +697,7 @@ public abstract partial class JsonValue : System.Text.Json.Nodes.JsonNode public static System.Text.Json.Nodes.JsonValue? Create(char? value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue? Create(System.DateTimeOffset? value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue? Create(System.DateTime? value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } + public static System.Text.Json.Nodes.JsonValue? Create(System.TimeSpan? value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue? Create(decimal? value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue? Create(double? value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } public static System.Text.Json.Nodes.JsonValue? Create(System.Guid? value, System.Text.Json.Nodes.JsonNodeOptions? options = default(System.Text.Json.Nodes.JsonNodeOptions?)) { throw null; } @@ -887,6 +902,7 @@ public static partial class JsonMetadataServices public static System.Text.Json.Serialization.JsonConverter SByteConverter { get { throw null; } } public static System.Text.Json.Serialization.JsonConverter SingleConverter { get { throw null; } } public static System.Text.Json.Serialization.JsonConverter StringConverter { get { throw null; } } + public static System.Text.Json.Serialization.JsonConverter TimeSpanConverter { get { throw null; } } [System.CLSCompliantAttribute(false)] public static System.Text.Json.Serialization.JsonConverter UInt16Converter { get { throw null; } } [System.CLSCompliantAttribute(false)] diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 9f73cb0ec2956f..c650f6ebad8bb5 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -318,6 +318,9 @@ The JSON value is not in a supported DateTimeOffset format. + + The JSON value is not in a supported TimeSpan format. + The JSON value is not in a supported Guid format. diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index 8a6315b1fe7c48..9c09f88006c2d2 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -50,6 +50,7 @@ + @@ -165,6 +166,7 @@ + @@ -245,6 +247,7 @@ + @@ -259,6 +262,7 @@ + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs index fb120a49ae72d9..6391373aefe952 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonDocument.cs @@ -709,6 +709,41 @@ internal bool TryGetValue(int index, out DateTimeOffset value) return false; } + internal bool TryGetValue(int index, out TimeSpan value) + { + CheckNotDisposed(); + + DbRow row = _parsedData.Get(index); + + CheckExpectedType(JsonTokenType.String, row.TokenType); + + ReadOnlySpan data = _utf8Json.Span; + ReadOnlySpan segment = data.Slice(row.Location, row.SizeOrLength); + + if (!JsonHelpers.IsValidTimeSpanParseLength(segment.Length)) + { + value = default; + return false; + } + + // Segment needs to be unescaped + if (row.HasComplexChildren) + { + return JsonReaderHelper.TryGetEscapedTimeSpan(segment, out value); + } + + Debug.Assert(segment.IndexOf(JsonConstants.BackSlash) == -1); + + if (JsonHelpers.TryParseAsConstantFormat(segment, out TimeSpan tmp)) + { + value = tmp; + return true; + } + + value = default; + return false; + } + internal bool TryGetValue(int index, out Guid value) { CheckNotDisposed(); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs index 18e69d8b0010fa..1a51b87a97237f 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Document/JsonElement.cs @@ -1095,6 +1095,57 @@ public DateTimeOffset GetDateTimeOffset() throw ThrowHelper.GetFormatException(); } + /// + /// Attempts to represent the current JSON string as a . + /// + /// Receives the value. + /// + /// This method does not create a TimeSpan representation of values other than JSON strings. + /// + /// + /// if the string can be represented as a , + /// otherwise. + /// + /// + /// This value's is not . + /// + /// + /// The parent has been disposed. + /// + public bool TryGetTimeSpan(out TimeSpan value) + { + CheckValidInstance(); + + return _parent.TryGetValue(_idx, out value); + } + + /// + /// Gets the value of the element as a . + /// + /// + /// This method does not create a TimeSpan representation of values other than JSON strings. + /// + /// The value of the element as a . + /// + /// This value's is not . + /// + /// + /// The value cannot be represented as a . + /// + /// + /// The parent has been disposed. + /// + /// + public TimeSpan GetTimeSpan() + { + if (TryGetTimeSpan(out TimeSpan value)) + { + return value; + } + + throw ThrowHelper.GetFormatException(); + } + /// /// Attempts to represent the current JSON string as a . /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs index e82811377a6820..aa25f8ab2d2b31 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonConstants.cs @@ -87,6 +87,9 @@ internal static class JsonConstants (DateTimeParseNumFractionDigits - DateTimeNumFractionDigits)); // Like StandardFormat 'O' for DateTimeOffset, but allowing 9 additional (up to 16) fraction digits. public const int MinimumDateTimeParseLength = 10; // YYYY-MM-DD public const int MaximumEscapedDateTimeOffsetParseLength = MaxExpansionFactorWhileEscaping * MaximumDateTimeOffsetParseLength; + public const int MinimumTimeSpanParseLength = 8; // hh:mm:ss + public const int MaximumTimeSpanParseLength = 28; // -dddddddddd.hh:mm:ss.fffffff + public const int MaximumEscapedTimeSpanParseLength = MaxExpansionFactorWhileEscaping * MaximumTimeSpanParseLength; // Encoding Helpers public const char HighSurrogateStart = '\ud800'; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.TimeSpan.cs b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.TimeSpan.cs new file mode 100644 index 00000000000000..562fc9a1600a36 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/JsonHelpers.TimeSpan.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers.Text; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace System.Text.Json +{ + internal static partial class JsonHelpers + { + public static bool TryParseAsConstantFormat(ReadOnlySpan source, out TimeSpan value) + { + if (!IsValidTimeSpanParseLength(source.Length)) + { + value = default; + return false; + } + + int maxLength = checked(source.Length * JsonConstants.MaxExpansionFactorWhileTranscoding); + + Span bytes = maxLength <= JsonConstants.StackallocThreshold + ? stackalloc byte[JsonConstants.StackallocThreshold] + : new byte[maxLength]; + + int length = JsonReaderHelper.GetUtf8FromText(source, bytes); + + bytes = bytes.Slice(0, length); + + if (bytes.IndexOf(JsonConstants.BackSlash) != -1) + { + return JsonReaderHelper.TryGetEscapedTimeSpan(bytes, out value); + } + + Debug.Assert(bytes.IndexOf(JsonConstants.BackSlash) == -1); + + if (TryParseAsConstantFormat(bytes, out TimeSpan tmp)) + { + value = tmp; + return true; + } + + value = default; + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidTimeSpanParseLength(int length) + { + return IsInRangeInclusive(length, JsonConstants.MinimumTimeSpanParseLength, JsonConstants.MaximumEscapedTimeSpanParseLength); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool IsValidTimeSpanParseLength(long length) + { + return IsInRangeInclusive(length, JsonConstants.MinimumTimeSpanParseLength, JsonConstants.MaximumEscapedTimeSpanParseLength); + } + + /// + /// Parse the given UTF-8 as TimeSpan constant ("c") format. + /// + /// UTF-8 source to parse. + /// The parsed if successful. + /// "true" if successfully parsed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool TryParseAsConstantFormat(ReadOnlySpan source, out TimeSpan value) + { + bool result = Utf8Parser.TryParse(source, out TimeSpan tmpValue, out int bytesConsumed, 'c'); + + // Note: Utf8Parser.TryParse will return true for invalid input so + // long as it starts with an integer. Example: "2021-06-08" or + // "1$$$$$$$$$$". We need to check bytesConsumed to know if the + // entire source was actually valid. + + if (result && source.Length == bytesConsumed) + { + value = tmpValue; + return true; + } + + value = default; + return false; + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.Operators.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.Operators.cs index 60474365e63035..e1b0acf65de5ce 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.Operators.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonNode.Operators.cs @@ -65,6 +65,18 @@ public partial class JsonNode /// A to implicitly convert. public static implicit operator JsonNode?(DateTimeOffset? value) => JsonValue.Create(value); + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode(TimeSpan value) => JsonValue.Create(value); + + /// + /// Defines an implicit conversion of a given to a . + /// + /// A to implicitly convert. + public static implicit operator JsonNode?(TimeSpan? value) => JsonValue.Create(value); + /// /// Defines an implicit conversion of a given to a . /// @@ -271,6 +283,18 @@ public partial class JsonNode /// A to implicitly convert. public static explicit operator DateTimeOffset?(JsonNode? value) => value?.GetValue(); + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator TimeSpan(JsonNode value) => value.GetValue(); + + /// + /// Defines an explicit conversion of a given to a . + /// + /// A to implicitly convert. + public static explicit operator TimeSpan?(JsonNode? value) => value?.GetValue(); + /// /// Defines an explicit conversion of a given to a . /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.CreateOverloads.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.CreateOverloads.cs index c708101f3ccf38..83124afce53f16 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.CreateOverloads.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValue.CreateOverloads.cs @@ -87,6 +87,22 @@ public partial class JsonValue /// The new instance of the class that contains the specified value. public static JsonValue? Create(DateTimeOffset? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValueTrimmable(value.Value, JsonMetadataServices.DateTimeOffsetConverter) : null; + /// + /// Initializes a new instance of the class that contains the specified value. + /// + /// The underlying value of the new instance. + /// Options to control the behavior. + /// The new instance of the class that contains the specified value. + public static JsonValue Create(TimeSpan value, JsonNodeOptions? options = null) => new JsonValueTrimmable(value, JsonMetadataServices.TimeSpanConverter); + + /// + /// Initializes a new instance of the class that contains the specified value. + /// + /// The underlying value of the new instance. + /// Options to control the behavior. + /// The new instance of the class that contains the specified value. + public static JsonValue? Create(TimeSpan? value, JsonNodeOptions? options = null) => value.HasValue ? new JsonValueTrimmable(value.Value, JsonMetadataServices.TimeSpanConverter) : null; + /// /// Initializes a new instance of the class that contains the specified value. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs index e7bc7b3f86d466..64c7252381ab76 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Nodes/JsonValueOfT.cs @@ -152,6 +152,11 @@ internal TypeToConvert ConvertJsonElement() return (TypeToConvert)(object)element.GetDateTimeOffset(); } + if (typeof(TypeToConvert) == typeof(TimeSpan) || typeof(TypeToConvert) == typeof(TimeSpan?)) + { + return (TypeToConvert)(object)element.GetTimeSpan(); + } + if (typeof(TypeToConvert) == typeof(Guid) || typeof(TypeToConvert) == typeof(Guid?)) { return (TypeToConvert)(object)element.GetGuid(); @@ -292,6 +297,13 @@ internal bool TryConvertJsonElement([NotNullWhen(true)] out TypeT return success; } + if (typeof(TypeToConvert) == typeof(TimeSpan) || typeof(TypeToConvert) == typeof(TimeSpan?)) + { + success = element.TryGetTimeSpan(out TimeSpan value); + result = (TypeToConvert)(object)value; + return success; + } + if (typeof(TypeToConvert) == typeof(Guid) || typeof(TypeToConvert) == typeof(Guid?)) { success = element.TryGetGuid(out Guid value); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs index 4ba1bff616b272..edf1467ece85e9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/JsonReaderHelper.cs @@ -296,6 +296,31 @@ public static bool TryGetEscapedDateTimeOffset(ReadOnlySpan source, out Da return false; } + public static bool TryGetEscapedTimeSpan(ReadOnlySpan source, out TimeSpan value) + { + int backslash = source.IndexOf(JsonConstants.BackSlash); + Debug.Assert(backslash != -1); + + Debug.Assert(source.Length <= JsonConstants.MaximumEscapedTimeSpanParseLength); + Span sourceUnescaped = stackalloc byte[source.Length]; + + Unescape(source, sourceUnescaped, backslash, out int written); + Debug.Assert(written > 0); + + sourceUnescaped = sourceUnescaped.Slice(0, written); + Debug.Assert(!sourceUnescaped.IsEmpty); + + if (sourceUnescaped.Length <= JsonConstants.MaximumTimeSpanParseLength + && JsonHelpers.TryParseAsConstantFormat(sourceUnescaped, out TimeSpan tmp)) + { + value = tmp; + return true; + } + + value = default; + return false; + } + public static bool TryGetEscapedGuid(ReadOnlySpan source, out Guid value) { Debug.Assert(source.Length <= JsonConstants.MaximumEscapedGuidLength); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs index 32947d9f090aeb..49cd42028cec37 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Reader/Utf8JsonReader.TryGet.cs @@ -607,6 +607,29 @@ internal DateTimeOffset GetDateTimeOffsetNoValidation() return value; } + /// + /// Parses the current JSON token value from the source as a . + /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a + /// value. + /// Throws exceptions otherwise. + /// + /// + /// Thrown if trying to get the value of a JSON token that is not a . + /// + /// + /// + /// Thrown if the JSON token value is of an unsupported format. The Constant ("c") TimeSpan Format Specifier is supported. + /// + public TimeSpan GetTimeSpan() + { + if (!TryGetTimeSpan(out TimeSpan value)) + { + throw ThrowHelper.GetFormatException(DataType.TimeSpan); + } + + return value; + } + /// /// Parses the current JSON token value from the source as a . /// Returns the value if the entire UTF-8 encoded token value can be successfully parsed to a @@ -1185,6 +1208,74 @@ internal bool TryGetDateTimeOffsetCore(out DateTimeOffset value) return false; } + /// + /// Parses the current JSON token value from the source as a . + /// Returns if the entire UTF-8 encoded token value can be successfully + /// parsed to a value. + /// Returns otherwise. + /// + /// + /// Thrown if trying to get the value of a JSON token that is not a . + /// + /// + public bool TryGetTimeSpan(out TimeSpan value) + { + if (TokenType != JsonTokenType.String) + { + throw ThrowHelper.GetInvalidOperationException_ExpectedString(TokenType); + } + + return TryGetTimeSpanCore(out value); + } + + internal bool TryGetTimeSpanCore(out TimeSpan value) + { + ReadOnlySpan span = stackalloc byte[0]; + + if (HasValueSequence) + { + long sequenceLength = ValueSequence.Length; + + if (!JsonHelpers.IsValidTimeSpanParseLength(sequenceLength)) + { + value = default; + return false; + } + + Debug.Assert(sequenceLength <= JsonConstants.MaximumEscapedTimeSpanParseLength); + Span stackSpan = stackalloc byte[(int)sequenceLength]; + + ValueSequence.CopyTo(stackSpan); + span = stackSpan; + } + else + { + if (!JsonHelpers.IsValidTimeSpanParseLength(ValueSpan.Length)) + { + value = default; + return false; + } + + span = ValueSpan; + } + + if (_stringHasEscaping) + { + return JsonReaderHelper.TryGetEscapedTimeSpan(span, out value); + } + + Debug.Assert(span.IndexOf(JsonConstants.BackSlash) == -1); + + if (JsonHelpers.TryParseAsConstantFormat(span, out TimeSpan tmp)) + { + value = tmp; + return true; + } + + value = default; + return false; + } + /// /// Parses the current JSON token value from the source as a . /// Returns if the entire UTF-8 encoded token value can be successfully diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs new file mode 100644 index 00000000000000..b249dde6a401cf --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Serialization.Converters +{ + internal sealed class TimeSpanConverter : JsonConverter + { + public override TimeSpan Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.GetTimeSpan(); + } + + public override void Write(Utf8JsonWriter writer, TimeSpan value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs index 95de3062e667f8..a5270028def60d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonSerializerOptions.Converters.cs @@ -49,7 +49,7 @@ private void RootBuiltInConverters() private static Dictionary GetDefaultSimpleConverters() { - const int NumberOfSimpleConverters = 23; + const int NumberOfSimpleConverters = 24; var converters = new Dictionary(NumberOfSimpleConverters); // Use a dictionary for simple converters. @@ -72,6 +72,7 @@ private static Dictionary GetDefaultSimpleConverters() Add(JsonMetadataServices.SByteConverter); Add(JsonMetadataServices.SingleConverter); Add(JsonMetadataServices.StringConverter); + Add(JsonMetadataServices.TimeSpanConverter); Add(JsonMetadataServices.UInt16Converter); Add(JsonMetadataServices.UInt32Converter); Add(JsonMetadataServices.UInt64Converter); diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs index 0be5273425cdb6..f51bc851a0a53b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonMetadataServices.Converters.cs @@ -110,6 +110,12 @@ public static partial class JsonMetadataServices public static JsonConverter StringConverter => s_stringConverter ??= new StringConverter(); private static JsonConverter? s_stringConverter; + /// + /// Returns a instance that converts values. + /// + public static JsonConverter TimeSpanConverter => s_timeSpanConverter ??= new TimeSpanConverter(); + private static JsonConverter? s_timeSpanConverter; + /// /// Returns a instance that converts values. /// diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs index 2b6b85878c9aef..96296fa7b2fedb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.cs @@ -636,6 +636,9 @@ public static FormatException GetFormatException(DataType dateType) case DataType.DateTimeOffset: message = SR.FormatDateTimeOffset; break; + case DataType.TimeSpan: + message = SR.FormatTimeSpan; + break; case DataType.Base64String: message = SR.CannotDecodeInvalidBase64; break; @@ -723,6 +726,7 @@ internal enum DataType Boolean, DateTime, DateTimeOffset, + TimeSpan, Base64String, Guid, } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.TimeSpan.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.TimeSpan.cs new file mode 100644 index 00000000000000..2f6ecc8bcf4fa8 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteProperties.TimeSpan.cs @@ -0,0 +1,388 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public sealed partial class Utf8JsonWriter + { + /// + /// Writes the pre-encoded property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The JSON-encoded name of the property to write. + /// The value to write. + /// + /// Thrown if this would result in invalid JSON being written (while validation is enabled). + /// + /// + /// Writes the using the constant ('c') , for example: 10:02:35. + /// The property name should already be escaped when the instance of was created. + /// + public void WriteString(JsonEncodedText propertyName, TimeSpan value) + { + ReadOnlySpan utf8PropertyName = propertyName.EncodedUtf8Bytes; + Debug.Assert(utf8PropertyName.Length <= JsonConstants.MaxUnescapedTokenSize); + + WriteStringByOptions(utf8PropertyName, value); + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The name of the property to write. + /// The value to write. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// The parameter is . + /// + /// + /// Thrown if this would result in invalid JSON being written (while validation is enabled). + /// + /// + /// Writes the using the constant ('c') , for example: 10:02:35. + /// The property name is escaped before writing. + /// + public void WriteString(string propertyName, TimeSpan value) + => WriteString((propertyName ?? throw new ArgumentNullException(nameof(propertyName))).AsSpan(), value); + + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The name of the property to write. + /// The value to write. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in invalid JSON being written (while validation is enabled). + /// + /// + /// Writes the using the constant ('c') , for example: 10:02:35. + /// The property name is escaped before writing. + /// + public void WriteString(ReadOnlySpan propertyName, TimeSpan value) + { + JsonWriterHelper.ValidateProperty(propertyName); + + WriteStringEscape(propertyName, value); + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + /// + /// Writes the property name and value (as a JSON string) as part of a name/value pair of a JSON object. + /// + /// The UTF-8 encoded name of the property to write. + /// The value to write. + /// + /// Thrown when the specified property name is too large. + /// + /// + /// Thrown if this would result in invalid JSON being written (while validation is enabled). + /// + /// + /// Writes the using the constant ('c') , for example: 10:02:35. + /// The property name is escaped before writing. + /// + public void WriteString(ReadOnlySpan utf8PropertyName, TimeSpan value) + { + JsonWriterHelper.ValidateProperty(utf8PropertyName); + + WriteStringEscape(utf8PropertyName, value); + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringEscape(ReadOnlySpan propertyName, TimeSpan value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(propertyName, _options.Encoder); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < propertyName.Length); + + if (propertyIdx != -1) + { + WriteStringEscapeProperty(propertyName, value, propertyIdx); + } + else + { + WriteStringByOptions(propertyName, value); + } + } + + private void WriteStringEscape(ReadOnlySpan utf8PropertyName, TimeSpan value) + { + int propertyIdx = JsonWriterHelper.NeedsEscaping(utf8PropertyName, _options.Encoder); + + Debug.Assert(propertyIdx >= -1 && propertyIdx < utf8PropertyName.Length); + + if (propertyIdx != -1) + { + WriteStringEscapeProperty(utf8PropertyName, value, propertyIdx); + } + else + { + WriteStringByOptions(utf8PropertyName, value); + } + } + + private void WriteStringEscapeProperty(ReadOnlySpan propertyName, TimeSpan value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= propertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < propertyName.Length); + + char[]? propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(propertyName.Length, firstEscapeIndexProp); + + Span escapedPropertyName = length <= JsonConstants.StackallocThreshold ? + stackalloc char[length] : + (propertyArray = ArrayPool.Shared.Rent(length)); + + JsonWriterHelper.EscapeString(propertyName, escapedPropertyName, firstEscapeIndexProp, _options.Encoder, out int written); + + WriteStringByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringEscapeProperty(ReadOnlySpan utf8PropertyName, TimeSpan value, int firstEscapeIndexProp) + { + Debug.Assert(int.MaxValue / JsonConstants.MaxExpansionFactorWhileEscaping >= utf8PropertyName.Length); + Debug.Assert(firstEscapeIndexProp >= 0 && firstEscapeIndexProp < utf8PropertyName.Length); + + byte[]? propertyArray = null; + + int length = JsonWriterHelper.GetMaxEscapedLength(utf8PropertyName.Length, firstEscapeIndexProp); + + Span escapedPropertyName = length <= JsonConstants.StackallocThreshold ? + stackalloc byte[length] : + (propertyArray = ArrayPool.Shared.Rent(length)); + + JsonWriterHelper.EscapeString(utf8PropertyName, escapedPropertyName, firstEscapeIndexProp, _options.Encoder, out int written); + + WriteStringByOptions(escapedPropertyName.Slice(0, written), value); + + if (propertyArray != null) + { + ArrayPool.Shared.Return(propertyArray); + } + } + + private void WriteStringByOptions(ReadOnlySpan propertyName, TimeSpan value) + { + ValidateWritingProperty(); + if (_options.Indented) + { + WriteStringIndented(propertyName, value); + } + else + { + WriteStringMinimized(propertyName, value); + } + } + + private void WriteStringByOptions(ReadOnlySpan utf8PropertyName, TimeSpan value) + { + ValidateWritingProperty(); + if (_options.Indented) + { + WriteStringIndented(utf8PropertyName, value); + } + else + { + WriteStringMinimized(utf8PropertyName, value); + } + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, TimeSpan value) + { + Debug.Assert(escapedPropertyName.Length < (int.MaxValue / JsonConstants.MaxExpansionFactorWhileTranscoding) - JsonConstants.MaximumTimeSpanParseLength - 6); + + // All ASCII, 2 quotes for property name, 2 quotes for timespan, and 1 colon => escapedPropertyName.Length + JsonConstants.MaximumTimeSpanParseLength + 5 + // Optionally, 1 list separator, and up to 3x growth when transcoding + int maxRequired = (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + JsonConstants.MaximumTimeSpanParseLength + 6; + + if (_memory.Length - BytesPending < maxRequired) + { + Grow(maxRequired); + } + + Span output = _memory.Span; + + if (_currentDepth < 0) + { + output[BytesPending++] = JsonConstants.ListSeparator; + } + output[BytesPending++] = JsonConstants.Quote; + + TranscodeAndWrite(escapedPropertyName, output); + + output[BytesPending++] = JsonConstants.Quote; + output[BytesPending++] = JsonConstants.KeyValueSeperator; + + output[BytesPending++] = JsonConstants.Quote; + + bool result = Utf8Formatter.TryFormat(value, output.Slice(BytesPending), out int bytesWritten, 'c'); + Debug.Assert(result); + BytesPending += bytesWritten; + + output[BytesPending++] = JsonConstants.Quote; + } + + private void WriteStringMinimized(ReadOnlySpan escapedPropertyName, TimeSpan value) + { + Debug.Assert(escapedPropertyName.Length < int.MaxValue - JsonConstants.MaximumTimeSpanParseLength - 6); + + int minRequired = escapedPropertyName.Length + JsonConstants.MaximumTimeSpanParseLength + 5; // 2 quotes for property name, 2 quotes for timespan, and 1 colon + int maxRequired = minRequired + 1; // Optionally, 1 list separator + + if (_memory.Length - BytesPending < maxRequired) + { + Grow(maxRequired); + } + + Span output = _memory.Span; + + if (_currentDepth < 0) + { + output[BytesPending++] = JsonConstants.ListSeparator; + } + output[BytesPending++] = JsonConstants.Quote; + + escapedPropertyName.CopyTo(output.Slice(BytesPending)); + BytesPending += escapedPropertyName.Length; + + output[BytesPending++] = JsonConstants.Quote; + output[BytesPending++] = JsonConstants.KeyValueSeperator; + + output[BytesPending++] = JsonConstants.Quote; + + bool result = Utf8Formatter.TryFormat(value, output.Slice(BytesPending), out int bytesWritten, 'c'); + Debug.Assert(result); + BytesPending += bytesWritten; + + output[BytesPending++] = JsonConstants.Quote; + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, TimeSpan value) + { + int indent = Indentation; + Debug.Assert(indent <= 2 * JsonConstants.MaxWriterDepth); + + Debug.Assert(escapedPropertyName.Length < (int.MaxValue / JsonConstants.MaxExpansionFactorWhileTranscoding) - indent - JsonConstants.MaximumTimeSpanParseLength - 7 - s_newLineLength); + + // All ASCII, 2 quotes for property name, 2 quotes for timespan, 1 colon, and 1 space => escapedPropertyName.Length + JsonConstants.MaximumTimeSpanParseLength + 6 + // Optionally, 1 list separator, 1-2 bytes for new line, and up to 3x growth when transcoding + int maxRequired = indent + (escapedPropertyName.Length * JsonConstants.MaxExpansionFactorWhileTranscoding) + JsonConstants.MaximumTimeSpanParseLength + 7 + s_newLineLength; + + if (_memory.Length - BytesPending < maxRequired) + { + Grow(maxRequired); + } + + Span output = _memory.Span; + + if (_currentDepth < 0) + { + output[BytesPending++] = JsonConstants.ListSeparator; + } + + Debug.Assert(_options.SkipValidation || _tokenType != JsonTokenType.PropertyName); + + if (_tokenType != JsonTokenType.None) + { + WriteNewLine(output); + } + + JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent); + BytesPending += indent; + + output[BytesPending++] = JsonConstants.Quote; + + TranscodeAndWrite(escapedPropertyName, output); + + output[BytesPending++] = JsonConstants.Quote; + output[BytesPending++] = JsonConstants.KeyValueSeperator; + output[BytesPending++] = JsonConstants.Space; + + output[BytesPending++] = JsonConstants.Quote; + + bool result = Utf8Formatter.TryFormat(value, output.Slice(BytesPending), out int bytesWritten, 'c'); + Debug.Assert(result); + BytesPending += bytesWritten; + + output[BytesPending++] = JsonConstants.Quote; + } + + private void WriteStringIndented(ReadOnlySpan escapedPropertyName, TimeSpan value) + { + int indent = Indentation; + Debug.Assert(indent <= 2 * JsonConstants.MaxWriterDepth); + + Debug.Assert(escapedPropertyName.Length < int.MaxValue - indent - JsonConstants.MaximumTimeSpanParseLength - 7 - s_newLineLength); + + int minRequired = indent + escapedPropertyName.Length + JsonConstants.MaximumTimeSpanParseLength + 6; // 2 quotes for property name, 2 quotes for timespan, 1 colon, and 1 space + int maxRequired = minRequired + 1 + s_newLineLength; // Optionally, 1 list separator and 1-2 bytes for new line + + if (_memory.Length - BytesPending < maxRequired) + { + Grow(maxRequired); + } + + Span output = _memory.Span; + + if (_currentDepth < 0) + { + output[BytesPending++] = JsonConstants.ListSeparator; + } + + Debug.Assert(_options.SkipValidation || _tokenType != JsonTokenType.PropertyName); + + if (_tokenType != JsonTokenType.None) + { + WriteNewLine(output); + } + + JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent); + BytesPending += indent; + + output[BytesPending++] = JsonConstants.Quote; + + escapedPropertyName.CopyTo(output.Slice(BytesPending)); + BytesPending += escapedPropertyName.Length; + + output[BytesPending++] = JsonConstants.Quote; + output[BytesPending++] = JsonConstants.KeyValueSeperator; + output[BytesPending++] = JsonConstants.Space; + + output[BytesPending++] = JsonConstants.Quote; + + bool result = Utf8Formatter.TryFormat(value, output.Slice(BytesPending), out int bytesWritten, 'c'); + Debug.Assert(result); + BytesPending += bytesWritten; + + output[BytesPending++] = JsonConstants.Quote; + } + + internal void WritePropertyName(TimeSpan value) + { + Span buffer = stackalloc byte[JsonConstants.MaximumTimeSpanParseLength]; + bool result = Utf8Formatter.TryFormat(value, buffer, out int bytesWritten, 'c'); + Debug.Assert(result); + WritePropertyNameUnescaped(buffer.Slice(0, bytesWritten)); + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.TimeSpan.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.TimeSpan.cs new file mode 100644 index 00000000000000..77f1e621977c1f --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Writer/Utf8JsonWriter.WriteValues.TimeSpan.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Text; +using System.Diagnostics; + +namespace System.Text.Json +{ + public sealed partial class Utf8JsonWriter + { + /// + /// Writes the value (as a JSON string) as an element of a JSON array. + /// + /// The value to write. + /// + /// Thrown if this would result in invalid JSON being written (while validation is enabled). + /// + /// + /// Writes the using the constant ('c') , for example: 10:02:35. + /// + public void WriteStringValue(TimeSpan value) + { + if (!_options.SkipValidation) + { + ValidateWritingValue(); + } + + if (_options.Indented) + { + WriteStringValueIndented(value); + } + else + { + WriteStringValueMinimized(value); + } + + SetFlagToAddListSeparatorBeforeNextItem(); + _tokenType = JsonTokenType.String; + } + + private void WriteStringValueMinimized(TimeSpan value) + { + int maxRequired = JsonConstants.MaximumTimeSpanParseLength + 3; // 2 quotes, and optionally, 1 list separator + + if (_memory.Length - BytesPending < maxRequired) + { + Grow(maxRequired); + } + + Span output = _memory.Span; + + if (_currentDepth < 0) + { + output[BytesPending++] = JsonConstants.ListSeparator; + } + + output[BytesPending++] = JsonConstants.Quote; + + bool result = Utf8Formatter.TryFormat(value, output.Slice(BytesPending), out int bytesWritten, 'c'); + Debug.Assert(result); + BytesPending += bytesWritten; + + output[BytesPending++] = JsonConstants.Quote; + } + + private void WriteStringValueIndented(TimeSpan value) + { + int indent = Indentation; + Debug.Assert(indent <= 2 * JsonConstants.MaxWriterDepth); + + // 2 quotes, and optionally, 1 list separator and 1-2 bytes for new line + int maxRequired = indent + JsonConstants.MaximumTimeSpanParseLength + 3 + s_newLineLength; + + if (_memory.Length - BytesPending < maxRequired) + { + Grow(maxRequired); + } + + Span output = _memory.Span; + + if (_currentDepth < 0) + { + output[BytesPending++] = JsonConstants.ListSeparator; + } + + if (_tokenType != JsonTokenType.PropertyName) + { + if (_tokenType != JsonTokenType.None) + { + WriteNewLine(output); + } + JsonWriterHelper.WriteIndentation(output.Slice(BytesPending), indent); + BytesPending += indent; + } + + output[BytesPending++] = JsonConstants.Quote; + + bool result = Utf8Formatter.TryFormat(value, output.Slice(BytesPending), out int bytesWritten, 'c'); + Debug.Assert(result); + BytesPending += bytesWritten; + + output[BytesPending++] = JsonConstants.Quote; + } + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs index 5facb8cc718216..bc2eeb038b4fc8 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs @@ -1432,6 +1432,36 @@ public static void ReadDateTimeAndDateTimeOffset_InvalidTests(string jsonString) } } + [Theory] + [InlineData("11:18:00", true)] + [InlineData("11:18", false)] + public static void ReadTimeSpan(string testString, bool expectedValid) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes($"\"{testString}\""); + + using (JsonDocument doc = JsonDocument.Parse(dataUtf8, default)) + { + JsonElement root = doc.RootElement; + + TimeSpan expectedTimeSpan = TimeSpan.Parse(testString); + + Assert.Equal(JsonValueKind.String, root.ValueKind); + + bool result = root.TryGetTimeSpan(out TimeSpan TimeSpanVal); + Assert.Equal(expectedValid, result); + Assert.Equal(expectedValid ? expectedTimeSpan : default, TimeSpanVal); + + if (expectedValid) + { + Assert.Equal(expectedTimeSpan, root.GetTimeSpan()); + } + else + { + Assert.Throws(() => root.GetTimeSpan()); + } + } + } + [Theory] [MemberData(nameof(JsonGuidTestData.ValidGuidTests), MemberType = typeof(JsonGuidTestData))] [MemberData(nameof(JsonGuidTestData.ValidHexGuidTests), MemberType = typeof(JsonGuidTestData))] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonNodeOperatorTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonNodeOperatorTests.cs index 0a3e130a317dc1..49e3c8cff40ac9 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonNodeOperatorTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonNodeOperatorTests.cs @@ -26,7 +26,8 @@ public static class OperatorTests @"""MyDecimal"":3.3," + @"""MyDateTime"":""2019-01-30T12:01:02Z""," + @"""MyDateTimeOffset"":""2019-01-30T12:01:02+01:00""," + - @"""MyGuid"":""1b33498a-7b7d-4dda-9c13-f6aa4ab449a6""" + // note lowercase + @"""MyGuid"":""1b33498a-7b7d-4dda-9c13-f6aa4ab449a6""," + // note lowercase + @"""MyTimeSpan"":""23:59:59.9999999""" + @"}"; [Fact] @@ -51,6 +52,7 @@ public static void ImplicitOperators_FromProperties() jObject["MyDateTime"] = new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc); jObject["MyDateTimeOffset"] = new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0)); jObject["MyGuid"] = new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6"); + jObject["MyTimeSpan"] = TimeSpan.Parse("23:59:59.9999999"); string expected = ExpectedPrimitiveJson; @@ -84,6 +86,7 @@ public static void ExplicitOperators_FromProperties() Assert.Equal(new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc), (DateTime)jObject["MyDateTime"]); Assert.Equal(new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0)), (DateTimeOffset)jObject["MyDateTimeOffset"]); Assert.Equal(new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6"), (Guid)jObject["MyGuid"]); + Assert.Equal(TimeSpan.Parse("23:59:59.9999999"), (TimeSpan)jObject["MyTimeSpan"]); } [Fact] @@ -110,6 +113,8 @@ public static void ExplicitOperators_FromValues() (DateTimeOffset)(JsonNode)new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0))); Assert.Equal(new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6"), (Guid)(JsonNode)new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6")); + Assert.Equal(TimeSpan.Parse("23:59:59.9999999"), + (TimeSpan)(JsonNode)TimeSpan.Parse("23:59:59.9999999")); } [Fact] @@ -132,6 +137,7 @@ public static void ExplicitOperators_FromNullValues() Assert.Null((DateTime?)(JsonValue)null); Assert.Null((DateTimeOffset?)(JsonValue)null); Assert.Null((Guid?)(JsonValue)null); + Assert.Null((TimeSpan?)(JsonValue)null); } [Fact] @@ -154,6 +160,7 @@ public static void ExplicitOperators_FromNullableValues() Assert.NotNull((DateTime?)(JsonValue)new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc)); Assert.NotNull((DateTimeOffset?)(JsonValue)new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0))); Assert.NotNull((Guid?)(JsonValue)new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6")); + Assert.NotNull((TimeSpan?)(JsonValue)TimeSpan.Parse("23:59:59.9999999")); } [Fact] @@ -176,6 +183,7 @@ public static void ImplicitOperators_FromNullValues() Assert.Null((JsonValue?)(DateTime?)null); Assert.Null((JsonValue?)(DateTimeOffset?)null); Assert.Null((JsonValue?)(Guid?)null); + Assert.Null((JsonValue?)(TimeSpan?)null); } [Fact] @@ -197,6 +205,7 @@ public static void ImplicitOperators_FromNullableValues() Assert.NotNull((JsonValue?)(DateTime?)new DateTime(2019, 1, 30, 12, 1, 2, DateTimeKind.Utc)); Assert.NotNull((JsonValue?)(DateTimeOffset?)new DateTimeOffset(2019, 1, 30, 12, 1, 2, new TimeSpan(1, 0, 0))); Assert.NotNull((JsonValue?)(Guid?)new Guid("1B33498A-7B7D-4DDA-9C13-F6AA4AB449A6")); + Assert.NotNull((JsonValue?)(TimeSpan?)TimeSpan.Parse("23:59:59.9999999")); } [Fact] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs index 61cd70b3a95ca3..016d3e7b4606aa 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/JsonValueTests.cs @@ -124,6 +124,7 @@ public static void TryGetValue_FromString() Assert.False(jValue.TryGetValue(out DateTime _)); Assert.False(jValue.TryGetValue(out DateTimeOffset _)); Assert.False(jValue.TryGetValue(out Guid _)); + Assert.False(jValue.TryGetValue(out TimeSpan _)); } [Fact] @@ -148,6 +149,7 @@ public static void TryGetValue_FromNumber() Assert.False(jValue.TryGetValue(out DateTime _)); Assert.False(jValue.TryGetValue(out DateTimeOffset _)); Assert.False(jValue.TryGetValue(out Guid _)); + Assert.False(jValue.TryGetValue(out TimeSpan _)); } [Fact] @@ -172,6 +174,7 @@ public static void TryGetValue_FromGuid() Assert.False(jValue.TryGetValue(out bool _)); Assert.False(jValue.TryGetValue(out DateTime _)); Assert.False(jValue.TryGetValue(out DateTimeOffset _)); + Assert.False(jValue.TryGetValue(out TimeSpan _)); } [Theory] @@ -198,6 +201,7 @@ public static void TryGetValue_FromDateTime(string json) Assert.False(jValue.TryGetValue(out decimal _)); Assert.False(jValue.TryGetValue(out bool _)); Assert.False(jValue.TryGetValue(out Guid _)); + Assert.False(jValue.TryGetValue(out TimeSpan _)); } [Fact] @@ -222,6 +226,32 @@ public static void TryGetValue_FromBoolean() Assert.False(jValue.TryGetValue(out DateTime _)); Assert.False(jValue.TryGetValue(out DateTimeOffset _)); Assert.False(jValue.TryGetValue(out Guid _)); + Assert.False(jValue.TryGetValue(out TimeSpan _)); + } + + [Fact] + public static void TryGetValue_FromTimeSpan() + { + JsonValue jValue = JsonNode.Parse("\"1:18:00.1\"").AsValue(); + + Assert.True(jValue.TryGetValue(out TimeSpan _)); + Assert.True(jValue.TryGetValue(out string _)); + Assert.False(jValue.TryGetValue(out char _)); + Assert.False(jValue.TryGetValue(out byte _)); + Assert.False(jValue.TryGetValue(out short _)); + Assert.False(jValue.TryGetValue(out int _)); + Assert.False(jValue.TryGetValue(out long _)); + Assert.False(jValue.TryGetValue(out sbyte _)); + Assert.False(jValue.TryGetValue(out ushort _)); + Assert.False(jValue.TryGetValue(out uint _)); + Assert.False(jValue.TryGetValue(out ulong _)); + Assert.False(jValue.TryGetValue(out float _)); + Assert.False(jValue.TryGetValue(out double _)); + Assert.False(jValue.TryGetValue(out decimal _)); + Assert.False(jValue.TryGetValue(out bool _)); + Assert.False(jValue.TryGetValue(out DateTime _)); + Assert.False(jValue.TryGetValue(out DateTimeOffset _)); + Assert.False(jValue.TryGetValue(out Guid _)); } [Theory] diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs index 92ce636e7c0982..ed9d8295c198d5 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonNode/ParseTests.cs @@ -49,6 +49,13 @@ public static void Parse() Assert.Equal(2, dtOffset.Minute); Assert.Equal(3, dtOffset.Second); Assert.Equal(new TimeSpan(1,15,0), dtOffset.Offset); + + TimeSpan ts = JsonNode.Parse("\"10.12:18:00\"").GetValue(); + Assert.Equal(10, ts.Days); + Assert.Equal(12, ts.Hours); + Assert.Equal(18, ts.Minutes); + Assert.Equal(0, ts.Seconds); + Assert.Equal(new TimeSpan(10, 12, 18, 0), ts); } [Fact] @@ -102,11 +109,13 @@ public static void Parse_TryGetValue() Assert.True(JsonNode.Parse("\"2020-07-08T00:00:00\"").AsValue().TryGetValue(out DateTime? _)); Assert.True(JsonNode.Parse("\"ed957609-cdfe-412f-88c1-02daca1b4f51\"").AsValue().TryGetValue(out Guid? _)); Assert.True(JsonNode.Parse("\"2020-07-08T01:02:03+01:15\"").AsValue().TryGetValue(out DateTimeOffset? _)); + Assert.True(JsonNode.Parse("\"-100.23:59:59\"").AsValue().TryGetValue(out TimeSpan? _)); JsonValue? jValue = JsonNode.Parse("\"Hello!\"").AsValue(); Assert.False(jValue.TryGetValue(out int _)); Assert.False(jValue.TryGetValue(out DateTime _)); Assert.False(jValue.TryGetValue(out DateTimeOffset _)); + Assert.False(jValue.TryGetValue(out TimeSpan _)); Assert.False(jValue.TryGetValue(out Guid _)); } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs index 27a53606963c51..6a63207838b560 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.cs @@ -1398,5 +1398,86 @@ static void test(string testString, bool isFinalBlock) test(testString, isFinalBlock: true); test(testString, isFinalBlock: false); } + + [Theory] + [InlineData("11:18:00", true)] + [InlineData("11:18", false)] + public static void TestingGetTimeSpan(string testString, bool expectedValid) + { + void ExecuteTest(bool hasValueSequence) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes($"\"{testString}\""); + Utf8JsonReader json; + if (hasValueSequence) + { + ReadOnlySequence sequence = JsonTestHelper.GetSequence(dataUtf8, 1); + json = new Utf8JsonReader(sequence, isFinalBlock: true, state: default); + } + else + { + json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + } + + Assert.True(json.Read()); + Assert.Equal(JsonTokenType.String, json.TokenType); + Assert.Equal(hasValueSequence, json.HasValueSequence); + + if (expectedValid) + { + Assert.Equal(TimeSpan.Parse(testString), json.GetTimeSpan()); + } + else + { + try + { + json.GetTimeSpan(); + Assert.True(false); + } + catch (Exception ex) + { + Assert.True(ex is FormatException); + } + } + } + + ExecuteTest(false); + ExecuteTest(true); + } + + [Theory] + [InlineData("11:18:00", true)] + [InlineData("-100.11:18:00.9999999", true)] + [InlineData("11:18:00.123", true)] + [InlineData("11:18", false)] // hh:mm:ss is the minimum you can specify + [InlineData("11:18:00.12345678", false)] // only 7 fractional digits supported + [InlineData("12345678901.11:18:00", false)] // only 10 day digits supported + public static void TestingTryGetTimeSpan(string testString, bool expectedValid) + { + void ExecuteTest(bool hasValueSequence) + { + byte[] dataUtf8 = Encoding.UTF8.GetBytes($"\"{testString}\""); + Utf8JsonReader json; + if (hasValueSequence) + { + ReadOnlySequence sequence = JsonTestHelper.GetSequence(dataUtf8, 1); + json = new Utf8JsonReader(sequence, isFinalBlock: true, state: default); + } + else + { + json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default); + } + + Assert.True(json.Read()); + Assert.Equal(JsonTokenType.String, json.TokenType); + Assert.Equal(hasValueSequence, json.HasValueSequence); + + bool result = json.TryGetTimeSpan(out TimeSpan value); + Assert.Equal(expectedValid, result); + Assert.Equal(expectedValid ? TimeSpan.Parse(testString) : default, value); + } + + ExecuteTest(false); + ExecuteTest(true); + } } } diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs index f4796dcf60e40e..b4fccb522a6e87 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonWriterTests.cs @@ -6104,6 +6104,72 @@ public void WriteDateTimeOffsetsValue(bool formatted, bool skipValidation, strin } } + [Theory] + [InlineData(true, true, "message")] + [InlineData(true, false, "message")] + [InlineData(false, true, "message")] + [InlineData(false, false, "message")] + [InlineData(true, true, "mess>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(true, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, true, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + [InlineData(false, false, ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")] + public void WriteTimeSpanValue(bool formatted, bool skipValidation, string keyString) + { + var random = new Random(42); + const int numberOfItems = 1_000; + + var timeSpans = new TimeSpan[numberOfItems]; + for (int i = 0; i < numberOfItems; i++) + timeSpans[i] = TimeSpan.FromTicks(random.Next(int.MaxValue)); + + string expectedStr = GetTimeSpansExpectedString(prettyPrint: formatted, keyString, timeSpans, escape: true); + + var options = new JsonWriterOptions { Indented = formatted, SkipValidation = skipValidation }; + + ReadOnlySpan keyUtf16 = keyString.AsSpan(); + ReadOnlySpan keyUtf8 = Encoding.UTF8.GetBytes(keyString); + + for (int i = 0; i < 3; i++) + { + var output = new ArrayBufferWriter(1024); + using var jsonUtf8 = new Utf8JsonWriter(output, options); + + jsonUtf8.WriteStartObject(); + + switch (i) + { + case 0: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyString, timeSpans[j]); + jsonUtf8.WriteStartArray(keyString); + break; + case 1: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf16, timeSpans[j]); + jsonUtf8.WriteStartArray(keyUtf16); + break; + case 2: + for (int j = 0; j < numberOfItems; j++) + jsonUtf8.WriteString(keyUtf8, timeSpans[j]); + jsonUtf8.WriteStartArray(keyUtf8); + break; + } + + jsonUtf8.WriteStringValue(timeSpans[0]); + jsonUtf8.WriteStringValue(timeSpans[1]); + jsonUtf8.WriteEndArray(); + + jsonUtf8.WriteEndObject(); + jsonUtf8.Flush(); + + JsonTestHelper.AssertContents(expectedStr, output); + } + } + // NOTE: WriteLargeKeyOrValue test is constrained to run on Windows and MacOSX because it causes // problems on Linux due to the way deferred memory allocation works. On Linux, the allocation can // succeed even if there is not enough memory but then the test may get killed by the OOM killer at the @@ -6583,6 +6649,17 @@ private static void WriteStringHelper(JsonEncodedText text, string expectedMessa JsonTestHelper.AssertContents($"{{{expectedMessage}:\"{value.ToString()}\"", output); } + + { + var output = new ArrayBufferWriter(); + using var jsonUtf8 = new Utf8JsonWriter(output); + jsonUtf8.WriteStartObject(); + TimeSpan value = new TimeSpan(2, 18, 30); + jsonUtf8.WriteString(text, value); + jsonUtf8.Flush(); + + JsonTestHelper.AssertContents($"{{{expectedMessage}:\"{value.ToString()}\"", output); + } } [ConditionalTheory(nameof(IsX64))] @@ -7691,6 +7768,38 @@ private static string GetDatesExpectedString(bool prettyPrint, string keyString, return Encoding.UTF8.GetString(ms.ToArray()); } + private static string GetTimeSpansExpectedString(bool prettyPrint, string keyString, TimeSpan[] timeSpans, bool escape = false) + { + var ms = new MemoryStream(); + TextWriter streamWriter = new StreamWriter(ms, new UTF8Encoding(false), 1024, true); + + var json = new JsonTextWriter(streamWriter) + { + Formatting = prettyPrint ? Formatting.Indented : Formatting.None, + StringEscapeHandling = StringEscapeHandling.EscapeHtml, + }; + + json.WriteStartObject(); + + for (int i = 0; i < timeSpans.Length; i++) + { + json.WritePropertyName(keyString, escape); + json.WriteValue(timeSpans[i]); + } + + json.WritePropertyName(keyString, escape); + json.WriteStartArray(); + json.WriteValue(timeSpans[0]); + json.WriteValue(timeSpans[1]); + json.WriteEnd(); + + json.WriteEnd(); + + json.Flush(); + + return Encoding.UTF8.GetString(ms.ToArray()); + } + public static IEnumerable JsonEncodedTextStrings { get