Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/HotChocolate/Core/src/Types.NodaTime/DateTimeOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,12 @@ public byte OutputPrecision
field = value;
}
} = DefaultOutputPrecision;

/// <summary>
/// Gets a value indicating whether fractional seconds are always emitted in serialized output,
/// padded with trailing zeros up to <see cref="OutputPrecision"/>. When <see langword="false"/>
/// (the default), trailing zeros are stripped and the fractional component is omitted entirely
/// when zero. Has no effect when <see cref="OutputPrecision"/> is <c>0</c>.
/// </summary>
public bool AlwaysOutputFractionalSeconds { get; init; }
}
9 changes: 5 additions & 4 deletions src/HotChocolate/Core/src/Types.NodaTime/DateTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ public DateTimeType(
Description = description;
Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
_inputPattern = OffsetDateTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
_outputFormat = GetFormat(_options.OutputPrecision);
_inputPattern = OffsetDateTimePattern.CreateWithInvariantCulture(
GetFormat(_options.InputPrecision, padFractionalSeconds: false));
_outputFormat = GetFormat(_options.OutputPrecision, _options.AlwaysOutputFractionalSeconds);
}

/// <summary>
Expand Down Expand Up @@ -124,8 +125,8 @@ private string GetPattern()
+ _options.InputPrecision
+ @"})?(?:[Zz]|[+-]\d{2}:\d{2})$";

private static string GetFormat(byte precision)
private static string GetFormat(byte precision, bool padFractionalSeconds)
=> precision == 0
? "uuuu-MM-dd'T'HH:mm:sso<Z+HH:mm>"
: $"uuuu-MM-dd'T'HH:mm:ss.{new string('F', precision)}o<Z+HH:mm>";
: $"uuuu-MM-dd'T'HH:mm:ss.{new string(padFractionalSeconds ? 'f' : 'F', precision)}o<Z+HH:mm>";
}
9 changes: 5 additions & 4 deletions src/HotChocolate/Core/src/Types.NodaTime/LocalDateTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ public LocalDateTimeType(
Description = description;
Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
_inputPattern = LocalDateTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
_outputFormat = GetFormat(_options.OutputPrecision);
_inputPattern = LocalDateTimePattern.CreateWithInvariantCulture(
GetFormat(_options.InputPrecision, padFractionalSeconds: false));
_outputFormat = GetFormat(_options.OutputPrecision, _options.AlwaysOutputFractionalSeconds);
}

/// <summary>
Expand Down Expand Up @@ -117,8 +118,8 @@ private string GetPattern()
? @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}$"
: @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$";

private static string GetFormat(byte precision)
private static string GetFormat(byte precision, bool padFractionalSeconds)
=> precision == 0
? @"uuuu-MM-dd'T'HH\:mm\:ss"
: @$"uuuu-MM-dd'T'HH\:mm\:ss.{new string('F', precision)}";
: @$"uuuu-MM-dd'T'HH\:mm\:ss.{new string(padFractionalSeconds ? 'f' : 'F', precision)}";
}
9 changes: 5 additions & 4 deletions src/HotChocolate/Core/src/Types.NodaTime/LocalTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ public LocalTimeType(
Description = description;
Pattern = GetPattern();
SpecifiedBy = new Uri(SpecifiedByUri);
_inputPattern = LocalTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
_outputFormat = GetFormat(_options.OutputPrecision);
_inputPattern = LocalTimePattern.CreateWithInvariantCulture(
GetFormat(_options.InputPrecision, padFractionalSeconds: false));
_outputFormat = GetFormat(_options.OutputPrecision, _options.AlwaysOutputFractionalSeconds);
}

/// <summary>
Expand Down Expand Up @@ -116,8 +117,8 @@ private string GetPattern()
? @"^\d{2}:\d{2}:\d{2}$"
: @"^\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$";

private static string GetFormat(byte precision)
private static string GetFormat(byte precision, bool padFractionalSeconds)
=> precision == 0
? "HH:mm:ss"
: $"HH:mm:ss.{new string('F', precision)}";
: $"HH:mm:ss.{new string(padFractionalSeconds ? 'f' : 'F', precision)}";
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,12 @@ public byte OutputPrecision
/// against the expected format. Defaults to <c>true</c>.
/// </summary>
public bool ValidateInputFormat { get; init; } = true;

/// <summary>
/// Gets a value indicating whether fractional seconds are always emitted in serialized output,
/// padded with trailing zeros up to <see cref="OutputPrecision"/>. When <see langword="false"/>
/// (the default), trailing zeros are stripped and the fractional component is omitted entirely
/// when zero. Has no effect when <see cref="OutputPrecision"/> is <c>0</c>.
/// </summary>
public bool AlwaysOutputFractionalSeconds { get; init; }
}
18 changes: 12 additions & 6 deletions src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,26 @@ private string GetPattern()
+ @"})?(?:[Zz]|[+-]\d{2}:\d{2})$";

private string GetUtcFormat()
=> _options.OutputPrecision switch
{
var pad = _options.AlwaysOutputFractionalSeconds;
return _options.OutputPrecision switch
{
DateTimeOptions.DefaultOutputPrecision => UtcFormat,
DateTimeOptions.DefaultOutputPrecision when !pad => UtcFormat,
0 => @"yyyy-MM-ddTHH\:mm\:ssZ",
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}Z"
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}Z"
};
}

private string GetLocalFormat()
=> _options.OutputPrecision switch
{
var pad = _options.AlwaysOutputFractionalSeconds;
return _options.OutputPrecision switch
{
DateTimeOptions.DefaultOutputPrecision => LocalFormat,
DateTimeOptions.DefaultOutputPrecision when !pad => LocalFormat,
0 => @"yyyy-MM-ddTHH\:mm\:sszzz",
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}zzz"
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}zzz"
};
}

private Regex GetDateTimeRegex()
=> _options.InputPrecision switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,15 @@ private string GetPattern()
: @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$";

private string GetLocalFormat()
=> _options.OutputPrecision switch
{
var pad = _options.AlwaysOutputFractionalSeconds;
return _options.OutputPrecision switch
{
DateTimeOptions.DefaultOutputPrecision => LocalFormat,
DateTimeOptions.DefaultOutputPrecision when !pad => LocalFormat,
0 => @"yyyy-MM-ddTHH\:mm\:ss",
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}"
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}"
};
}

private Regex GetLocalDateTimeRegex()
=> _options.InputPrecision switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,15 @@ private string GetPattern()
: @"^\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$";

private string GetLocalFormat()
=> _options.OutputPrecision switch
{
var pad = _options.AlwaysOutputFractionalSeconds;
return _options.OutputPrecision switch
{
DateTimeOptions.DefaultOutputPrecision => LocalFormat,
DateTimeOptions.DefaultOutputPrecision when !pad => LocalFormat,
0 => "HH:mm:ss",
_ => $"HH:mm:ss.{new string('F', _options.OutputPrecision)}"
_ => $"HH:mm:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}"
};
}

private Regex GetLocalTimeRegex()
=> _options.InputPrecision switch
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,80 @@ public void CoerceOutputValue_Valid(byte precision, OffsetDateTime dateTime, str
resultValue.MatchInlineSnapshot($"\"{result}\"");
}

[Theory]
[InlineData(9, "2023-12-24T15:30:00.123456789Z")]
[InlineData(3, "2023-12-24T15:30:00.123Z")]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_Pads(byte precision, string expected)
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = precision,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new OffsetDateTime(
new LocalDateTime(2023, 12, 24, 15, 30, 0, 123),
Offset.Zero).PlusNanoseconds(456_789);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot($"\"{expected}\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_EmitsZerosForWholeSecond()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 3,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new OffsetDateTime(
new LocalDateTime(2023, 12, 24, 15, 30, 0),
Offset.Zero);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.000Z\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_NoOpWhenPrecisionZero()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 0,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new OffsetDateTime(
new LocalDateTime(2023, 12, 24, 15, 30, 0, 123),
Offset.Zero);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00Z\"");
}

[Fact]
public void CoerceOutputValue_OffsetDateTime()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,96 @@ public void CoerceOutputValue_Valid(byte precision, DateTimeOffset dateTime, str
resultValue.MatchInlineSnapshot($"\"{result}\"");
}

[Theory]
[InlineData(DateTimeOptions.DefaultOutputPrecision, "2023-12-24T15:30:00.1234567Z")]
[InlineData(3, "2023-12-24T15:30:00.123Z")]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_Pads(byte precision, string expected)
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = precision,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot($"\"{expected}\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_EmitsZerosForWholeSecond()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 3,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, TimeSpan.Zero);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.000Z\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_NoOpWhenPrecisionZero()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 0,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, TimeSpan.Zero);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00Z\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_LocalOffset()
{
// arrange
var type = new DateTimeType(
new DateTimeOptions
{
OutputPrecision = 3,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, TimeSpan.FromHours(4));

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.000+04:00\"");
}

[Fact]
public void CoerceOutputValue_Utc_DateTimeOffset()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,29 @@ public void CoerceOutputValue_Valid(byte precision, DateTime dateTime, string re
resultValue.MatchInlineSnapshot($"\"{result}\"");
}

[Fact]
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_PadsAtDefaultPrecision()
{
// arrange
// exercises the bypass of the `LocalFormat` const-string shortcut at default precision
var type = new LocalDateTimeType(
new DateTimeOptions
{
OutputPrecision = DateTimeOptions.DefaultOutputPrecision,
AlwaysOutputFractionalSeconds = true
});
var dateTime = new DateTime(2023, 12, 24, 15, 30, 0);

// act
var operation = CommonTestExtensions.CreateOperation();
var resultDocument = new ResultDocument(operation, 0);
var resultValue = resultDocument.Data.GetProperty("first");
type.CoerceOutputValue(dateTime, resultValue);

// assert
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.0000000\"");
}

[Fact]
public void CoerceOutputValue()
{
Expand Down
Loading
Loading