Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Feb 8, 2026

Description

Implements ISO 8601 support for parsing 24:00:00 as end-of-day (midnight of next day). Per the ISO 8601 standard, 24:00:00 represents the end of a calendar day and is the same instant as the next day's 00:00:00.

Changes Made

Core Library Changes:

  • DateTime.cs: TryCreate — set hour=0 when hour=24, add day at end
  • DateTimeParse.cs: TryParseFormatO, DoStrictParse, ParseISO8601, ProcessDateTimeSuffix — all handle hour=24
  • Utf8Parser.Date.Helpers.cs: TryCreateDateTime — handle hour=24
  • JsonHelpers.Date.cs: TryCreateDateTime — handle hour=24

Test Changes:

  • 51 new tests across DateTime, DateTimeOffset, TimeOnly, Utf8Parser, and JSON parsing
  • JSON Z-suffixed hour=24 test uses ParseExact with RoundtripKind (matching existing UTC test pattern)

Trade-offs

  1. ISO 8601-1:2019/Amd 1:2022 re-added "24:00" support
  2. Hour=24 only valid when minute=0, second=0, fraction=0
  3. 2007-04-05T24:00 → 2007-04-06T00:00:00
  4. 9999-12-31T24:00 correctly rejected (overflow)
  5. All parsing methods use unified approach: set hour=0, add one day at end
Original prompt

This section details on the original issue you should resolve

<issue_title>Support 24:00 in hh:mm for ISO8601 dates</issue_title>
<issue_description>A time of 24:00 is a valid in ISO8601 dates.

Midnight is a special case and may be referred to as either "00:00" or "24:00". The notation "00:00" is used at the beginning of a calendar day and is the more frequently used. At the end of a day use "24:00". "2007-04-05T24:00" is the same instant as "2007-04-06T00:00" (see Combined date and time representations below).

https://en.wikipedia.org/wiki/ISO_8601

var dt = DateTimeOffset.Parse("2007-04-05T24:00");
var dt = DateTime.Parse("2007-04-05T24:00");
System.FormatException: 'The DateTime represented by the string '2007-04-05T24:00' is not supported in calendar 'System.Globalization.GregorianCalendar'.'
   at System.DateTimeParse.Parse(ReadOnlySpan`1 s, DateTimeFormatInfo dtfi, DateTimeStyles styles, TimeSpan& offset)
   at System.DateTimeOffset.Parse(String input)
   at ConsoleApp17.Program.Main(String[] args) in C:\Users\James\source\repos\ConsoleApp17\ConsoleApp17\Program.cs:line 10

Should be fixed in DateTimeOffset.Parse/DateTime.Parse and new Utf8Parser API.</issue_description>

<agent_instructions>Study this issue carefully, including all of the comments and the links to specifications. There is high risk of regression and unintended consequences with this change, so ample test coverage is expected including across different cultures and on various boundary cases. The PR that proposes a fix should cite the trade-offs involved with considering the fix.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@JeremyKuhne According to ISO 8601-1:2019 "5.3.2 - Beginning of the day" this is explicitly **not** supported:

For information interchange there is no representation of end of day. It is recognized that the expression ‘24:00:00’ is used as a natural language expression to denote end of a day; but for the benefit of clarity, ‘24’ shall not be used to represent hour in accordance with this document.

Additionally, "4.3.8 Clock hour and hours duration" is very explicit about valid hours being between 00 and 23.

ISO 8601-2:2019 "7.3.2 Beginning of the day" also calls out explicitly that "end of day" is not supported.

(I don't know if Wikipedia is accurate to some older revision of the specification. I only have access to the most current.)

cc: @layomia, @tarekgh </comment_new>
<comment_new>@tarekgh
I agree with @JeremyKuhne too. the hours should be from 0 to 23. looking at https://tools.ietf.org/html/rfc3339#section-5.7 you can also see it is explicit saying in the section 5.7. Restrictions:

   Although ISO 8601 permits the hour to be "24", this profile of ISO
   8601 only allows values between "00" and "23" for the hour in order
   to reduce confusion.

In section 5.6. Internet Date/Time Format it mention:

time-hour       = 2DIGIT  ; 00-23

Although Appendix A. ISO 8601 Collected ABNF suggest time-hour = 2DIGIT ; 00-24 but the same appendix saying:

ISO 8601 does not specify a formal grammar for the date and time
   formats it defines.  The following is an attempt to create a formal
   grammar from ISO 8601.  This is informational only and may contain
   errors.  ISO 8601 remains the authoritative reference.

Note that due to ambiguities in ISO 8601, some interpretations had to
   be made.  First, ISO 8601 is not clear if mixtures of basic and
   extended format are permissible.  This grammar permits mixtures. ISO
   8601 is not clear on whether an hour of 24 is permissible only if
   minutes and seconds are 0.  This assumes that an hour of 24 is
   permissible in any context.  Restrictions on date-mday in section 5.7
   apply.

If the latest version of ISO8601 (year 2019) is explicit regarding that as @JeremyKuhne mentioned, then we should stick with that and not support 24 to avoid any confusion.

@JeremyKuhne > I think the key thing here is to be able to parse user input.

I'm fine with considering parsing it, but not as part of the "O" format.</comment_new>
<comment_new>@tarekgh

I'm fine with considering parsing it, but not as part of the "O" format.

That will be more tricky. DateTime non-exact parser has a lot heuristics to detect which part of the date/time string is year, month, day, hour, minute...etc. trying to support 24 as hour will have a lot of disadvantages here and can cause a lot of other breaks.
I would say, either allow 24 in O only or not.</comment_new>
<comment_new>@tarekgh
@KalleOlaviNiemitalo I think your sugge...


💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.

@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Feb 8, 2026
Copilot AI and others added 2 commits February 8, 2026 11:02
- Modified DateTime.TryCreate to handle hour=24 when minute, second, and millisecond are zero
- Modified Utf8Parser.TryCreateDateTime to handle hour=24 with same validation
- Added fraction validation in TryParseFormatO and TryParseDateTimeOffsetO
- Added comprehensive tests for DateTime, DateTimeOffset, and Utf8Parser
- Tests cover year boundaries, leap years, month boundaries, and timezone offsets
- Tests verify that hour=24 with non-zero minute/second/fraction is rejected
- Tests verify that 9999-12-31T24:00 is rejected (would overflow)

Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com>
Removed duplicate test cases for non-zero seconds and minutes in ParseExact_Hour24_Invalid tests

Co-authored-by: jeffhandley <1031940+jeffhandley@users.noreply.github.com>
Copilot AI changed the title [WIP] Support 24:00 format for ISO8601 dates Support ISO 8601 24:00 (end-of-day) in DateTime/DateTimeOffset parsing Feb 8, 2026
Copilot AI requested a review from jeffhandley February 8, 2026 11:08
Copy link
Member

@jeffhandley jeffhandley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tarekgh This looks pretty reasonable to me; more surgical than I was expecting. What do you think?

Per @tarekgh's feedback, instead of duplicating the date calculation code,
we now set hour to 0 when hour=24 and add one day at the end, falling through
to the normal code flow.

Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
Copilot AI requested a review from tarekgh February 8, 2026 15:38
@tarekgh
Copy link
Member

tarekgh commented Feb 8, 2026

@copilot the change here is not enough when parsing using TimeOnly.ParseExact(s, "o"). We should insert the code before the line

if (!parseInfo.calendar.TryToDateTime(result.Year, result.Month, result.Day,
which check for hour value equal to 24, and check the minutes, seconds, fraction to be zero. and if the hour is 24, change it to zero.

@tarekgh
Copy link
Member

tarekgh commented Feb 8, 2026

@copilot need to add tests for TimeOnly.ParseExact cases

Per @tarekgh's feedback, added hour=24 validation in DoStrictParse before the
calendar.TryToDateTime call. This ensures TimeOnly.ParseExact("24:00:00", "HH:mm:ss")
works correctly by treating hour=24 as hour=0 and adding one day after the DateTime is created.

Added TimeOnly tests to verify hour=24 parsing works and properly rejects invalid cases.

Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 8, 2026

@copilot the change here is not enough when parsing using TimeOnly.ParseExact(s, "o"). We should insert the code before the line https://github.com/dotnet/runtime/blob/dee607911e68b92ea8fede4dd735d90c...

Added hour=24 handling in DoStrictParse (commit 175ded8). Now checking if hour=24 before calendar.TryToDateTime call, validating minute/second/fraction are zero, setting hour=0, and adding one day after the DateTime is created. Also added TimeOnly.ParseExact tests to verify the functionality.

Copilot AI changed the title Support ISO 8601 24:00 (end-of-day) in DateTime/DateTimeOffset parsing Support ISO 8601 24:00 (end-of-day) in DateTime/DateTimeOffset/TimeOnly parsing Feb 8, 2026
Copy link
Contributor Author

Copilot AI commented Feb 8, 2026

@copilot need to add tests for TimeOnly.ParseExact cases

TimeOnly.ParseExact tests were already added in commit 175ded8, including ParseExact_Hour24_Success, ParseExact_Hour24_Invalid_ThrowsFormatException, TryParseExact_Hour24_Success, and TryParseExact_Hour24_Invalid_ReturnsFalse.

@tarekgh
Copy link
Member

tarekgh commented Feb 8, 2026

@copilot please fix the test failures:

[FAIL] System.Tests.TimeOnlyTests.TryParse_Hour24_Success
Assert.True() Failure
Expected: True
Actual: False
at System.Tests.TimeOnlyTests.TryParse_Hour24_Success() in //src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeOnlyTests.cs:line 734
at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /
/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230

[FAIL] System.Tests.TimeOnlyTests.TryParseExact_Hour24_Success
Assert.True() Failure
Expected: True
Actual: False
at System.Tests.TimeOnlyTests.TryParseExact_Hour24_Success() in //src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeOnlyTests.cs:line 702
at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /
/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230

[FAIL] System.Tests.TimeOnlyTests.Parse_Hour24_Success
System.FormatException : String '24:00:00' was not recognized as a valid TimeOnly.
at System.TimeOnly.ThrowOnError(ParseFailureKind, ReadOnlySpan`1) in //src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs:line 926
at System.Tests.TimeOnlyTests.Parse_Hour24_Success() in /
/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeOnlyTests.cs:line 717
at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230

[FAIL] System.Tests.TimeOnlyTests.ParseExact_Hour24_Success
System.FormatException : String '24:00:00' was not recognized as a valid TimeOnly.
at System.TimeOnly.ThrowOnError(ParseFailureKind, ReadOnlySpan`1) in //src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs:line 926
at System.Tests.TimeOnlyTests.ParseExact_Hour24_Success() in /
/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeOnlyTests.cs:line 684
at System.Reflection.DynamicInvokeInfo.Invoke(Object, IntPtr, Object[], BinderBundle, Boolean) in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Reflection/DynamicInvokeInfo.cs:line 230

The fraction field in DateTimeResult and DateTimeRawInfo is initialized
to -1 (meaning "not set"). When no fraction is present in the input
(e.g., "24:00:00" without ".0000000"), the fraction stays at -1. The
check `fraction != 0` incorrectly rejected this because -1 != 0 is true.

Changed to `fraction > 0` which correctly allows both -1 (not set) and
0 (explicitly zero), matching the existing pattern used elsewhere in
the same methods (e.g., line 2714 and 4842).

Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 8, 2026

@copilot please fix the test failures:

[FAIL] System.Tests.TimeOnlyTests.TryParse_Hour24_Success
...

Fixed in commit ab7028e. The root cause was the fraction field in DateTimeResult and DateTimeRawInfo is initialized to -1 (meaning "not set"). When no fraction is present in the input (e.g., "24:00:00" without .0000000), the fraction stays at -1. The check fraction != 0 incorrectly rejected this because -1 != 0 is true.

Changed to fraction > 0 in both DoStrictParse (line 4826) and ProcessDateTimeSuffix (line 2695), matching the existing pattern used elsewhere in the same methods (e.g., lines 2714 and 4842 already use > 0 for fraction checks).

@tarekgh
Copy link
Member

tarekgh commented Feb 9, 2026

@copilot fix the failures:

  Discovering: System.Text.Json.Tests (method display = ClassAndMethod, method display options = None)
  Discovered:  System.Text.Json.Tests (found 10212 of 10279 test cases)
  Starting:    System.Text.Json.Tests (parallel test collections = on [6 threads], stop on fail = off)
    System.Text.Json.Tests.JsonDocumentTests.ReadDateTimeAndDateTimeOffset_InvalidTests(jsonString: "\"1997-07-16T24:00\"") [FAIL]
      Assert.False() Failure
      Expected: False
      Actual:   True
      Stack Trace:
        /_/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/JsonDocumentTests.cs(1438,0): at System.Text.Json.Tests.JsonDocumentTests.ReadDateTimeAndDateTimeOffset_InvalidTests(String jsonString)
           at System.Object.InvokeStub_JsonDocumentTests.ReadDateTimeAndDateTimeOffset_InvalidTests(Object, Span`1)
        /_/src/libraries/System.Private.CoreLib/src/System/Reflection/MethodBaseInvoker.cs(95,0): at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
    System.Text.Json.Tests.Utf8JsonReaderTests.TestingStringsInvalidConversionToDateTime(jsonString: "\"1997-07-16T24:00\"") [FAIL]
      Assert.False() Failure
      Expected: False
      Actual:   True
      Stack Trace:
        /_/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs(113,0): at System.Text.Json.Tests.Utf8JsonReaderTests.TestingStringsInvalidConversionToDateTime(String jsonString)
           at System.Object.InvokeStub_Utf8JsonReaderTests.TestingStringsInvalidConversionToDateTime(Object, Span`1)
        /_/src/libraries/System.Private.CoreLib/src/System/Reflection/MethodBaseInvoker.cs(95,0): at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
    System.Text.Json.Tests.Utf8JsonReaderTests.TryGetDateTimeOffset_HasValueSequence_False(testString: "\"1997-07-16T24:00\"") [FAIL]
      json.TryGetDateTimeOffset(out DateTimeOffset actual)
      Stack Trace:
        /_/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs(246,0): at System.Text.Json.Tests.Utf8JsonReaderTests.<TryGetDateTimeOffset_HasValueSequence_False>g__Test|280_0(String testString, Boolean isFinalBlock)
        /_/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs(252,0): at System.Text.Json.Tests.Utf8JsonReaderTests.TryGetDateTimeOffset_HasValueSequence_False(String testString)
           at System.Object.InvokeStub_Utf8JsonReaderTests.TryGetDateTimeOffset_HasValueSequence_False(Object, Span`1)
        /_/src/libraries/System.Private.CoreLib/src/System/Reflection/MethodBaseInvoker.cs(95,0): at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
    System.Text.Json.Tests.Utf8JsonReaderTests.TestingStrings_Hour24_Valid(jsonString: "\"2023-12-31T24:00:00Z\"", expectedString: "2024-01-01T00:00:00Z") [FAIL]
      Assert.Equal() Failure: Values differ
      Expected: 2023-12-31T19:00:00.0000000-05:00
      Actual:   2024-01-01T00:00:00.0000000Z
      Stack Trace:
        /_/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs(289,0): at System.Text.Json.Tests.Utf8JsonReaderTests.TestingStrings_Hour24_Valid(String jsonString, String expectedString)
           at System.Object.InvokeStub_Utf8JsonReaderTests.TestingStrings_Hour24_Valid(Object, Span`1)
        /_/src/libraries/System.Private.CoreLib/src/System/Reflection/MethodBaseInvoker.cs(136,0): at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
    System.Text.Json.Tests.Utf8JsonReaderTests.TestingStringsInvalidConversionToDateTimeOffset(jsonString: "\"1997-07-16T24:00\"") [FAIL]
      Assert.False() Failure
      Expected: False
      Actual:   True
      Stack Trace:
        /_/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs(137,0): at System.Text.Json.Tests.Utf8JsonReaderTests.TestingStringsInvalidConversionToDateTimeOffset(String jsonString)
           at System.Object.InvokeStub_Utf8JsonReaderTests.TestingStringsInvalidConversionToDateTimeOffset(Object, Span`1)
        /_/src/libraries/System.Private.CoreLib/src/System/Reflection/MethodBaseInvoker.cs(95,0): at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
    System.Text.Json.Tests.Utf8JsonReaderTests.TryGetDateTime_HasValueSequence_False(testString: "\"1997-07-16T24:00\"") [FAIL]
      json.TryGetDateTime(out DateTime actual)
      Stack Trace:
        /_/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs(221,0): at System.Text.Json.Tests.Utf8JsonReaderTests.<TryGetDateTime_HasValueSequence_False>g__Test|279_0(String testString, Boolean isFinalBlock)
        /_/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs(227,0): at System.Text.Json.Tests.Utf8JsonReaderTests.TryGetDateTime_HasValueSequence_False(String testString)
           at System.Object.InvokeStub_Utf8JsonReaderTests.TryGetDateTime_HasValueSequence_False(Object, Span`1)
        /_/src/libraries/System.Private.CoreLib/src/System/Reflection/MethodBaseInvoker.cs(95,0): at System.Reflection.MethodBaseInvoker.InvokeWithOneArg(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)

The hour=24 changes make "1997-07-16T24:00" a valid JSON date string.
Moved it from InvalidISO8601Tests to ValidISO8601Tests with expected
result "1997-07-17T00:00" (next day at midnight). Also added a new
invalid test case "1997-07-16T24:01" to verify non-zero minutes are
still rejected with hour=24.

Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 9, 2026

@copilot fix the failures:

...

Fixed in commit 51f074f. The string "1997-07-16T24:00" was listed in InvalidISO8601Tests from before our changes. Moved it to ValidISO8601Tests with expected result "1997-07-17T00:00" (next day at midnight). Also added "1997-07-16T24:01" as a new invalid test case to verify non-zero minutes are still rejected with hour=24.

@tarekgh
Copy link
Member

tarekgh commented Feb 9, 2026

@copilot please fix the failure:


Error message
Assert.Equal() Failure: Values differ
Expected: 2023-12-31T19:00:00.0000000-05:00
Actual:   2024-01-01T00:00:00.0000000Z



Stack trace
   at System.Text.Json.Tests.Utf8JsonReaderTests.TestingStrings_Hour24_Valid(String jsonString, String expectedString) in /_/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Utf8JsonReaderTests.TryGet.Date.cs:line 289
   at InvokeStub_Utf8JsonReaderTests.TestingStrings_Hour24_Valid(Object, Span`1)
   at System

DateTime.Parse for strings with Z TZD returns DateTimeKind.Local, but
the JSON reader returns DateTimeKind.Utc. Following the existing pattern
in the file (ValidISO8601TestsWithUtcOffset), moved the Z-suffixed test
case to a separate TestingStrings_Hour24_ValidUtc method that uses
DateTime.ParseExact with DateTimeStyles.RoundtripKind.

Also added DateTimeOffset assertions to both valid test methods.

Co-authored-by: tarekgh <10833894+tarekgh@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Feb 9, 2026

@copilot please fix the failure:

...

Fixed in commit 955e82d. The issue was that DateTime.Parse for strings with Z TZD returns DateTimeKind.Local, but the JSON reader returns DateTimeKind.Utc. Split the Z-suffixed test case into a separate TestingStrings_Hour24_ValidUtc method that uses DateTime.ParseExact with DateTimeStyles.RoundtripKind — matching the existing pattern used by TestingStringsWithUTCOffsetToDateTime and ValidISO8601TestsWithUtcOffset.

@tarekgh
Copy link
Member

tarekgh commented Feb 9, 2026

/ba-g reported failures are unrelated

@tarekgh
Copy link
Member

tarekgh commented Feb 9, 2026

@jeffhandley let me know if ok to merge or you want to have a look before doing that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support 24:00 in hh:mm for ISO8601 dates

7 participants