Skip to content

Commit b9a77ba

Browse files
authored
Add AlwaysOutputFractionalSeconds to DateTime scalar options (#9745)
1 parent 9cd9563 commit b9a77ba

13 files changed

Lines changed: 279 additions & 25 deletions

File tree

src/HotChocolate/Core/src/Types.NodaTime/DateTimeOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,12 @@ public byte OutputPrecision
6262
field = value;
6363
}
6464
} = DefaultOutputPrecision;
65+
66+
/// <summary>
67+
/// Gets a value indicating whether fractional seconds are always emitted in serialized output,
68+
/// padded with trailing zeros up to <see cref="OutputPrecision"/>. When <see langword="false"/>
69+
/// (the default), trailing zeros are stripped and the fractional component is omitted entirely
70+
/// when zero. Has no effect when <see cref="OutputPrecision"/> is <c>0</c>.
71+
/// </summary>
72+
public bool AlwaysOutputFractionalSeconds { get; init; }
6573
}

src/HotChocolate/Core/src/Types.NodaTime/DateTimeType.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ public DateTimeType(
3939
Description = description;
4040
Pattern = GetPattern();
4141
SpecifiedBy = new Uri(SpecifiedByUri);
42-
_inputPattern = OffsetDateTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
43-
_outputFormat = GetFormat(_options.OutputPrecision);
42+
_inputPattern = OffsetDateTimePattern.CreateWithInvariantCulture(
43+
GetFormat(_options.InputPrecision, padFractionalSeconds: false));
44+
_outputFormat = GetFormat(_options.OutputPrecision, _options.AlwaysOutputFractionalSeconds);
4445
}
4546

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

127-
private static string GetFormat(byte precision)
128+
private static string GetFormat(byte precision, bool padFractionalSeconds)
128129
=> precision == 0
129130
? "uuuu-MM-dd'T'HH:mm:sso<Z+HH:mm>"
130-
: $"uuuu-MM-dd'T'HH:mm:ss.{new string('F', precision)}o<Z+HH:mm>";
131+
: $"uuuu-MM-dd'T'HH:mm:ss.{new string(padFractionalSeconds ? 'f' : 'F', precision)}o<Z+HH:mm>";
131132
}

src/HotChocolate/Core/src/Types.NodaTime/LocalDateTimeType.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,9 @@ public LocalDateTimeType(
4040
Description = description;
4141
Pattern = GetPattern();
4242
SpecifiedBy = new Uri(SpecifiedByUri);
43-
_inputPattern = LocalDateTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
44-
_outputFormat = GetFormat(_options.OutputPrecision);
43+
_inputPattern = LocalDateTimePattern.CreateWithInvariantCulture(
44+
GetFormat(_options.InputPrecision, padFractionalSeconds: false));
45+
_outputFormat = GetFormat(_options.OutputPrecision, _options.AlwaysOutputFractionalSeconds);
4546
}
4647

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

120-
private static string GetFormat(byte precision)
121+
private static string GetFormat(byte precision, bool padFractionalSeconds)
121122
=> precision == 0
122123
? @"uuuu-MM-dd'T'HH\:mm\:ss"
123-
: @$"uuuu-MM-dd'T'HH\:mm\:ss.{new string('F', precision)}";
124+
: @$"uuuu-MM-dd'T'HH\:mm\:ss.{new string(padFractionalSeconds ? 'f' : 'F', precision)}";
124125
}

src/HotChocolate/Core/src/Types.NodaTime/LocalTimeType.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,9 @@ public LocalTimeType(
3939
Description = description;
4040
Pattern = GetPattern();
4141
SpecifiedBy = new Uri(SpecifiedByUri);
42-
_inputPattern = LocalTimePattern.CreateWithInvariantCulture(GetFormat(_options.InputPrecision));
43-
_outputFormat = GetFormat(_options.OutputPrecision);
42+
_inputPattern = LocalTimePattern.CreateWithInvariantCulture(
43+
GetFormat(_options.InputPrecision, padFractionalSeconds: false));
44+
_outputFormat = GetFormat(_options.OutputPrecision, _options.AlwaysOutputFractionalSeconds);
4445
}
4546

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

119-
private static string GetFormat(byte precision)
120+
private static string GetFormat(byte precision, bool padFractionalSeconds)
120121
=> precision == 0
121122
? "HH:mm:ss"
122-
: $"HH:mm:ss.{new string('F', precision)}";
123+
: $"HH:mm:ss.{new string(padFractionalSeconds ? 'f' : 'F', precision)}";
123124
}

src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeOptions.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,12 @@ public byte OutputPrecision
7373
/// against the expected format. Defaults to <c>true</c>.
7474
/// </summary>
7575
public bool ValidateInputFormat { get; init; } = true;
76+
77+
/// <summary>
78+
/// Gets a value indicating whether fractional seconds are always emitted in serialized output,
79+
/// padded with trailing zeros up to <see cref="OutputPrecision"/>. When <see langword="false"/>
80+
/// (the default), trailing zeros are stripped and the fractional component is omitted entirely
81+
/// when zero. Has no effect when <see cref="OutputPrecision"/> is <c>0</c>.
82+
/// </summary>
83+
public bool AlwaysOutputFractionalSeconds { get; init; }
7684
}

src/HotChocolate/Core/src/Types/Types/Scalars/DateTimeType.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -171,20 +171,26 @@ private string GetPattern()
171171
+ @"})?(?:[Zz]|[+-]\d{2}:\d{2})$";
172172

173173
private string GetUtcFormat()
174-
=> _options.OutputPrecision switch
174+
{
175+
var pad = _options.AlwaysOutputFractionalSeconds;
176+
return _options.OutputPrecision switch
175177
{
176-
DateTimeOptions.DefaultOutputPrecision => UtcFormat,
178+
DateTimeOptions.DefaultOutputPrecision when !pad => UtcFormat,
177179
0 => @"yyyy-MM-ddTHH\:mm\:ssZ",
178-
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}Z"
180+
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}Z"
179181
};
182+
}
180183

181184
private string GetLocalFormat()
182-
=> _options.OutputPrecision switch
185+
{
186+
var pad = _options.AlwaysOutputFractionalSeconds;
187+
return _options.OutputPrecision switch
183188
{
184-
DateTimeOptions.DefaultOutputPrecision => LocalFormat,
189+
DateTimeOptions.DefaultOutputPrecision when !pad => LocalFormat,
185190
0 => @"yyyy-MM-ddTHH\:mm\:sszzz",
186-
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}zzz"
191+
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}zzz"
187192
};
193+
}
188194

189195
private Regex GetDateTimeRegex()
190196
=> _options.InputPrecision switch

src/HotChocolate/Core/src/Types/Types/Scalars/LocalDateTimeType.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,12 +159,15 @@ private string GetPattern()
159159
: @"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$";
160160

161161
private string GetLocalFormat()
162-
=> _options.OutputPrecision switch
162+
{
163+
var pad = _options.AlwaysOutputFractionalSeconds;
164+
return _options.OutputPrecision switch
163165
{
164-
DateTimeOptions.DefaultOutputPrecision => LocalFormat,
166+
DateTimeOptions.DefaultOutputPrecision when !pad => LocalFormat,
165167
0 => @"yyyy-MM-ddTHH\:mm\:ss",
166-
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string('F', _options.OutputPrecision)}"
168+
_ => @$"yyyy-MM-ddTHH\:mm\:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}"
167169
};
170+
}
168171

169172
private Regex GetLocalDateTimeRegex()
170173
=> _options.InputPrecision switch

src/HotChocolate/Core/src/Types/Types/Scalars/LocalTimeType.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,15 @@ private string GetPattern()
157157
: @"^\d{2}:\d{2}:\d{2}(?:\.\d{1," + _options.InputPrecision + "})?$";
158158

159159
private string GetLocalFormat()
160-
=> _options.OutputPrecision switch
160+
{
161+
var pad = _options.AlwaysOutputFractionalSeconds;
162+
return _options.OutputPrecision switch
161163
{
162-
DateTimeOptions.DefaultOutputPrecision => LocalFormat,
164+
DateTimeOptions.DefaultOutputPrecision when !pad => LocalFormat,
163165
0 => "HH:mm:ss",
164-
_ => $"HH:mm:ss.{new string('F', _options.OutputPrecision)}"
166+
_ => $"HH:mm:ss.{new string(pad ? 'f' : 'F', _options.OutputPrecision)}"
165167
};
168+
}
166169

167170
private Regex GetLocalTimeRegex()
168171
=> _options.InputPrecision switch

src/HotChocolate/Core/test/Types.NodaTime.Tests/DateTimeTypeTests.cs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,80 @@ public void CoerceOutputValue_Valid(byte precision, OffsetDateTime dateTime, str
160160
resultValue.MatchInlineSnapshot($"\"{result}\"");
161161
}
162162

163+
[Theory]
164+
[InlineData(9, "2023-12-24T15:30:00.123456789Z")]
165+
[InlineData(3, "2023-12-24T15:30:00.123Z")]
166+
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_Pads(byte precision, string expected)
167+
{
168+
// arrange
169+
var type = new DateTimeType(
170+
new DateTimeOptions
171+
{
172+
OutputPrecision = precision,
173+
AlwaysOutputFractionalSeconds = true
174+
});
175+
var dateTime = new OffsetDateTime(
176+
new LocalDateTime(2023, 12, 24, 15, 30, 0, 123),
177+
Offset.Zero).PlusNanoseconds(456_789);
178+
179+
// act
180+
var operation = CommonTestExtensions.CreateOperation();
181+
var resultDocument = new ResultDocument(operation, 0);
182+
var resultValue = resultDocument.Data.GetProperty("first");
183+
type.CoerceOutputValue(dateTime, resultValue);
184+
185+
// assert
186+
resultValue.MatchInlineSnapshot($"\"{expected}\"");
187+
}
188+
189+
[Fact]
190+
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_EmitsZerosForWholeSecond()
191+
{
192+
// arrange
193+
var type = new DateTimeType(
194+
new DateTimeOptions
195+
{
196+
OutputPrecision = 3,
197+
AlwaysOutputFractionalSeconds = true
198+
});
199+
var dateTime = new OffsetDateTime(
200+
new LocalDateTime(2023, 12, 24, 15, 30, 0),
201+
Offset.Zero);
202+
203+
// act
204+
var operation = CommonTestExtensions.CreateOperation();
205+
var resultDocument = new ResultDocument(operation, 0);
206+
var resultValue = resultDocument.Data.GetProperty("first");
207+
type.CoerceOutputValue(dateTime, resultValue);
208+
209+
// assert
210+
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.000Z\"");
211+
}
212+
213+
[Fact]
214+
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_NoOpWhenPrecisionZero()
215+
{
216+
// arrange
217+
var type = new DateTimeType(
218+
new DateTimeOptions
219+
{
220+
OutputPrecision = 0,
221+
AlwaysOutputFractionalSeconds = true
222+
});
223+
var dateTime = new OffsetDateTime(
224+
new LocalDateTime(2023, 12, 24, 15, 30, 0, 123),
225+
Offset.Zero);
226+
227+
// act
228+
var operation = CommonTestExtensions.CreateOperation();
229+
var resultDocument = new ResultDocument(operation, 0);
230+
var resultValue = resultDocument.Data.GetProperty("first");
231+
type.CoerceOutputValue(dateTime, resultValue);
232+
233+
// assert
234+
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00Z\"");
235+
}
236+
163237
[Fact]
164238
public void CoerceOutputValue_OffsetDateTime()
165239
{

src/HotChocolate/Core/test/Types.Tests/Types/Scalars/DateTimeTypeTests.cs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,96 @@ public void CoerceOutputValue_Valid(byte precision, DateTimeOffset dateTime, str
159159
resultValue.MatchInlineSnapshot($"\"{result}\"");
160160
}
161161

162+
[Theory]
163+
[InlineData(DateTimeOptions.DefaultOutputPrecision, "2023-12-24T15:30:00.1234567Z")]
164+
[InlineData(3, "2023-12-24T15:30:00.123Z")]
165+
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_Pads(byte precision, string expected)
166+
{
167+
// arrange
168+
var type = new DateTimeType(
169+
new DateTimeOptions
170+
{
171+
OutputPrecision = precision,
172+
AlwaysOutputFractionalSeconds = true
173+
});
174+
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, 456, TimeSpan.Zero).AddTicks(7);
175+
176+
// act
177+
var operation = CommonTestExtensions.CreateOperation();
178+
var resultDocument = new ResultDocument(operation, 0);
179+
var resultValue = resultDocument.Data.GetProperty("first");
180+
type.CoerceOutputValue(dateTime, resultValue);
181+
182+
// assert
183+
resultValue.MatchInlineSnapshot($"\"{expected}\"");
184+
}
185+
186+
[Fact]
187+
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_EmitsZerosForWholeSecond()
188+
{
189+
// arrange
190+
var type = new DateTimeType(
191+
new DateTimeOptions
192+
{
193+
OutputPrecision = 3,
194+
AlwaysOutputFractionalSeconds = true
195+
});
196+
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, TimeSpan.Zero);
197+
198+
// act
199+
var operation = CommonTestExtensions.CreateOperation();
200+
var resultDocument = new ResultDocument(operation, 0);
201+
var resultValue = resultDocument.Data.GetProperty("first");
202+
type.CoerceOutputValue(dateTime, resultValue);
203+
204+
// assert
205+
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.000Z\"");
206+
}
207+
208+
[Fact]
209+
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_NoOpWhenPrecisionZero()
210+
{
211+
// arrange
212+
var type = new DateTimeType(
213+
new DateTimeOptions
214+
{
215+
OutputPrecision = 0,
216+
AlwaysOutputFractionalSeconds = true
217+
});
218+
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, 123, TimeSpan.Zero);
219+
220+
// act
221+
var operation = CommonTestExtensions.CreateOperation();
222+
var resultDocument = new ResultDocument(operation, 0);
223+
var resultValue = resultDocument.Data.GetProperty("first");
224+
type.CoerceOutputValue(dateTime, resultValue);
225+
226+
// assert
227+
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00Z\"");
228+
}
229+
230+
[Fact]
231+
public void CoerceOutputValue_AlwaysOutputFractionalSeconds_LocalOffset()
232+
{
233+
// arrange
234+
var type = new DateTimeType(
235+
new DateTimeOptions
236+
{
237+
OutputPrecision = 3,
238+
AlwaysOutputFractionalSeconds = true
239+
});
240+
var dateTime = new DateTimeOffset(2023, 12, 24, 15, 30, 0, TimeSpan.FromHours(4));
241+
242+
// act
243+
var operation = CommonTestExtensions.CreateOperation();
244+
var resultDocument = new ResultDocument(operation, 0);
245+
var resultValue = resultDocument.Data.GetProperty("first");
246+
type.CoerceOutputValue(dateTime, resultValue);
247+
248+
// assert
249+
resultValue.MatchInlineSnapshot("\"2023-12-24T15:30:00.000+04:00\"");
250+
}
251+
162252
[Fact]
163253
public void CoerceOutputValue_Utc_DateTimeOffset()
164254
{

0 commit comments

Comments
 (0)