Skip to content

Commit

Permalink
Decouple TimeOnlyConverter from TimeSpanConverter (#70035)
Browse files Browse the repository at this point in the history
* Decouple TimeOnlyConverter from TimeSpanConverter

* remove redundant using declarations
  • Loading branch information
eiriktsarpalis authored Jun 6, 2022
1 parent 7473ff2 commit 54f104a
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,72 @@

using System.Buffers.Text;
using System.Diagnostics;
using System.Globalization;

namespace System.Text.Json.Serialization.Converters
{
internal sealed class TimeOnlyConverter : JsonConverter<TimeOnly>
{
private static readonly TimeSpanConverter s_timeSpanConverter = new TimeSpanConverter();
private static readonly TimeSpan s_timeOnlyMaxValue = TimeOnly.MaxValue.ToTimeSpan();
private const int MinimumTimeOnlyFormatLength = 8; // hh:mm:ss
private const int MaximumTimeOnlyFormatLength = 16; // hh:mm:ss.fffffff
private const int MaximumEscapedTimeOnlyFormatLength = JsonConstants.MaxExpansionFactorWhileEscaping * MaximumTimeOnlyFormatLength;

public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
TimeSpan timespan = s_timeSpanConverter.Read(ref reader, typeToConvert, options);
if (reader.TokenType != JsonTokenType.String)
{
ThrowHelper.ThrowInvalidOperationException_ExpectedString(reader.TokenType);
}

if (timespan < TimeSpan.Zero || timespan > s_timeOnlyMaxValue)
if (!JsonHelpers.IsInRangeInclusive(reader.ValueLength, MinimumTimeOnlyFormatLength, MaximumEscapedTimeOnlyFormatLength))
{
ThrowHelper.ThrowJsonException();
ThrowHelper.ThrowFormatException(DataType.TimeOnly);
}

ReadOnlySpan<byte> source = stackalloc byte[0];
if (!reader.HasValueSequence && !reader.ValueIsEscaped)
{
source = reader.ValueSpan;
}
else
{
Span<byte> stackSpan = stackalloc byte[MaximumEscapedTimeOnlyFormatLength];
int bytesWritten = reader.CopyString(stackSpan);
source = stackSpan.Slice(0, bytesWritten);
}

byte firstChar = source[0];
int firstSeparator = source.IndexOfAny((byte)'.', (byte)':');
if (!JsonHelpers.IsDigit(firstChar) || firstSeparator < 0 || source[firstSeparator] == (byte)'.')
{
// Note: Utf8Parser.TryParse permits leading whitespace, negative values
// and numbers of days so we need to exclude these cases here.
ThrowHelper.ThrowFormatException(DataType.TimeOnly);
}

bool result = Utf8Parser.TryParse(source, out TimeSpan timespan, out int bytesConsumed, 'c');

// Note: Utf8Parser.TryParse will return true for invalid input so
// long as it starts with an integer. Example: "2021-06-18" or
// "1$$$$$$$$$$". We need to check bytesConsumed to know if the
// entire source was actually valid.

if (!result || source.Length != bytesConsumed)
{
ThrowHelper.ThrowFormatException(DataType.TimeOnly);
}

Debug.Assert(TimeOnly.MinValue.ToTimeSpan() <= timespan && timespan <= TimeOnly.MaxValue.ToTimeSpan());
return TimeOnly.FromTimeSpan(timespan);
}

public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options)
{
s_timeSpanConverter.Write(writer, value.ToTimeSpan(), options);
Span<byte> output = stackalloc byte[MaximumTimeOnlyFormatLength];

bool result = Utf8Formatter.TryFormat(value.ToTimeSpan(), output, out int bytesWritten, 'c');
Debug.Assert(result);

writer.WriteStringValue(output.Slice(0, bytesWritten));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -628,6 +628,7 @@ public static void ThrowFormatException(DataType dataType)
case DataType.DateOnly:
case DataType.DateTime:
case DataType.DateTimeOffset:
case DataType.TimeOnly:
case DataType.TimeSpan:
case DataType.Guid:
case DataType.Version:
Expand Down Expand Up @@ -723,6 +724,7 @@ internal enum DataType
DateOnly,
DateTime,
DateTimeOffset,
TimeOnly,
TimeSpan,
Base64String,
Guid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,8 +670,11 @@ public static void TimeOnly_Read_Nullable_Tests()
[InlineData("\\u0032\\u0034\\u003A\\u0030\\u0030\\u003A\\u0030\\u0030")]
[InlineData("00:60:00")]
[InlineData("00:00:60")]
[InlineData("-00:00:00")]
[InlineData("00:00:00.00000009")]
[InlineData("900000000.00:00:00")]
[InlineData("1.00:00:00")]
[InlineData("0.00:00:00")]
[InlineData("1:00:00")] // 'g' Format
[InlineData("1:2:00:00")] // 'g' Format
[InlineData("+00:00:00")]
Expand Down

0 comments on commit 54f104a

Please sign in to comment.