Skip to content

Commit

Permalink
Allow two-digit-max-year customization
Browse files Browse the repository at this point in the history
Fixes #1720, at least the main feature.

Additional potential features to be implemented later:

- WithTwoDigitYearMaxFromCalendar
- An AppContext switch to prohibit two-digit years in patterns
  • Loading branch information
jskeet committed Jun 3, 2023
1 parent b81a841 commit 838afe8
Show file tree
Hide file tree
Showing 23 changed files with 437 additions and 118 deletions.
24 changes: 24 additions & 0 deletions src/NodaTime.Test/Text/InstantPatternTest.cs
Expand Up @@ -2,6 +2,7 @@
// Use of this source code is governed by the Apache License 2.0,
// as found in the LICENSE.txt file.

using System;
using System.Collections.Generic;
using System.Linq;
using NodaTime.Text;
Expand Down Expand Up @@ -68,6 +69,29 @@ public void Create()
[Test]
public void ParseNull() => AssertParseNull(InstantPattern.General);

[Test]
[TestCase(0, "00-01-01T00:00:00", 2000)]
[TestCase(0, "01-01-01T00:00:00", 1901)]
[TestCase(50, "49-01-01T00:00:00", 2049)]
[TestCase(50, "50-01-01T00:00:00", 2050)]
[TestCase(50, "51-01-01T00:00:00", 1951)]
[TestCase(99, "00-01-01T00:00:00", 2000)]
[TestCase(99, "99-01-01T00:00:00", 2099)]
public void WithTwoDigitYearMax(int twoDigitYearMax, string text, int expectedYear)
{
var pattern = InstantPattern.CreateWithInvariantCulture("yy-MM-dd'T'HH:mm:ss").WithTwoDigitYearMax(twoDigitYearMax);
var value = pattern.Parse(text).Value;
Assert.AreEqual(expectedYear, value.InUtc().Year);
}

[Test]
[TestCase(-1)]
[TestCase(100)]
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
public void WithTwoDigitYearMax_Invalid(int twoDigitYearMax) =>
Assert.Throws<ArgumentOutOfRangeException>(() => InstantPattern.General.WithTwoDigitYearMax(twoDigitYearMax));

/// <summary>
/// Common test data for both formatting and parsing. A test should be placed here unless is truly
/// cannot be run both ways. This ensures that as many round-trip type tests are performed as possible.
Expand Down
23 changes: 23 additions & 0 deletions src/NodaTime.Test/Text/LocalDatePatternTest.cs
Expand Up @@ -261,6 +261,29 @@ public void CreateWithCurrentCulture()
[Test]
public void ParseNull() => AssertParseNull(LocalDatePattern.Iso);

[Test]
[TestCase(0, "00-01-01", 2000)]
[TestCase(0, "01-01-01", 1901)]
[TestCase(50, "49-01-01", 2049)]
[TestCase(50, "50-01-01", 2050)]
[TestCase(50, "51-01-01", 1951)]
[TestCase(99, "00-01-01", 2000)]
[TestCase(99, "99-01-01", 2099)]
public void WithTwoDigitYearMax(int twoDigitYearMax, string text, int expectedYear)
{
var pattern = LocalDatePattern.CreateWithInvariantCulture("yy-MM-dd").WithTwoDigitYearMax(twoDigitYearMax);
var value = pattern.Parse(text).Value;
Assert.AreEqual(expectedYear, value.Year);
}

[Test]
[TestCase(-1)]
[TestCase(100)]
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
public void WithTwoDigitYearMax_Invalid(int twoDigitYearMax) =>
Assert.Throws<ArgumentOutOfRangeException>(() => LocalDatePattern.Iso.WithTwoDigitYearMax(twoDigitYearMax));

private void AssertBclNodaEquality(CultureInfo culture, string patternText)
{
// The BCL never seems to use abbreviated month genitive names.
Expand Down
24 changes: 24 additions & 0 deletions src/NodaTime.Test/Text/LocalDateTimePatternTest.cs
Expand Up @@ -8,6 +8,7 @@
using NodaTime.Text;
using NUnit.Framework;
using NodaTime.Test.Calendars;
using System;

namespace NodaTime.Test.Text
{
Expand Down Expand Up @@ -187,6 +188,29 @@ public void CreateWithCurrentCulture()
[Test]
public void ParseNull() => AssertParseNull(LocalDateTimePattern.ExtendedIso);

[Test]
[TestCase(0, "00-01-01T00:00:00", 2000)]
[TestCase(0, "01-01-01T00:00:00", 1901)]
[TestCase(50, "49-01-01T00:00:00", 2049)]
[TestCase(50, "50-01-01T00:00:00", 2050)]
[TestCase(50, "51-01-01T00:00:00", 1951)]
[TestCase(99, "00-01-01T00:00:00", 2000)]
[TestCase(99, "99-01-01T00:00:00", 2099)]
public void WithTwoDigitYearMax(int twoDigitYearMax, string text, int expectedYear)
{
var pattern = LocalDateTimePattern.CreateWithInvariantCulture("yy-MM-dd'T'HH:mm:ss").WithTwoDigitYearMax(twoDigitYearMax);
var value = pattern.Parse(text).Value;
Assert.AreEqual(expectedYear, value.Year);
}

[Test]
[TestCase(-1)]
[TestCase(100)]
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
public void WithTwoDigitYearMax_Invalid(int twoDigitYearMax) =>
Assert.Throws<ArgumentOutOfRangeException>(() => LocalDateTimePattern.ExtendedIso.WithTwoDigitYearMax(twoDigitYearMax));

[Test]
[TestCaseSource(nameof(AllCulturesStandardPatterns))]
public void BclStandardPatternComparison(CultureInfo culture, string pattern)
Expand Down
24 changes: 24 additions & 0 deletions src/NodaTime.Test/Text/OffsetDatePatternTest.cs
Expand Up @@ -5,6 +5,7 @@
using NodaTime.Globalization;
using NodaTime.Text;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;

Expand Down Expand Up @@ -142,6 +143,29 @@ public void WithCalendar()
[Test]
public void ParseNull() => AssertParseNull(OffsetDatePattern.GeneralIso);

[Test]
[TestCase(0, "00-01-01 +01", 2000)]
[TestCase(0, "01-01-01 +01", 1901)]
[TestCase(50, "49-01-01 +01", 2049)]
[TestCase(50, "50-01-01 +01", 2050)]
[TestCase(50, "51-01-01 +01", 1951)]
[TestCase(99, "00-01-01 +01", 2000)]
[TestCase(99, "99-01-01 +01", 2099)]
public void WithTwoDigitYearMax(int twoDigitYearMax, string text, int expectedYear)
{
var pattern = OffsetDatePattern.CreateWithInvariantCulture("yy-MM-dd o<g>").WithTwoDigitYearMax(twoDigitYearMax);
var value = pattern.Parse(text).Value;
Assert.AreEqual(expectedYear, value.Year);
}

[Test]
[TestCase(-1)]
[TestCase(100)]
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
public void WithTwoDigitYearMax_Invalid(int twoDigitYearMax) =>
Assert.Throws<ArgumentOutOfRangeException>(() => OffsetDatePattern.GeneralIso.WithTwoDigitYearMax(twoDigitYearMax));

internal sealed class Data : PatternTestData<OffsetDate>
{
// Default to the start of the year 2000 UTC
Expand Down
24 changes: 24 additions & 0 deletions src/NodaTime.Test/Text/OffsetDateTimePatternTest.cs
Expand Up @@ -5,6 +5,7 @@
using NodaTime.Globalization;
using NodaTime.Text;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Linq;

Expand Down Expand Up @@ -190,6 +191,29 @@ public void WithCalendar()
[Test]
public void ParseNull() => AssertParseNull(OffsetDateTimePattern.ExtendedIso);

[Test]
[TestCase(0, "00-01-01T00:00:00 +01", 2000)]
[TestCase(0, "01-01-01T00:00:00 +01", 1901)]
[TestCase(50, "49-01-01T00:00:00 +01", 2049)]
[TestCase(50, "50-01-01T00:00:00 +01", 2050)]
[TestCase(50, "51-01-01T00:00:00 +01", 1951)]
[TestCase(99, "00-01-01T00:00:00 +01", 2000)]
[TestCase(99, "99-01-01T00:00:00 +01", 2099)]
public void WithTwoDigitYearMax(int twoDigitYearMax, string text, int expectedYear)
{
var pattern = OffsetDateTimePattern.CreateWithInvariantCulture("yy-MM-dd'T'HH:mm:ss o<g>").WithTwoDigitYearMax(twoDigitYearMax);
var value = pattern.Parse(text).Value;
Assert.AreEqual(expectedYear, value.Year);
}

[Test]
[TestCase(-1)]
[TestCase(100)]
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
public void WithTwoDigitYearMax_Invalid(int twoDigitYearMax) =>
Assert.Throws<ArgumentOutOfRangeException>(() => OffsetDateTimePattern.GeneralIso.WithTwoDigitYearMax(twoDigitYearMax));

internal sealed class Data : PatternTestData<OffsetDateTime>
{
// Default to the start of the year 2000 UTC
Expand Down
23 changes: 23 additions & 0 deletions src/NodaTime.Test/Text/YearMonthPatternTest.cs
Expand Up @@ -211,6 +211,29 @@ public void CreateWithTemplateValue()
[Test]
public void ParseNull() => AssertParseNull(YearMonthPattern.Iso);

[Test]
[TestCase(0, "00-01", 2000)]
[TestCase(0, "01-01", 1901)]
[TestCase(50, "49-01", 2049)]
[TestCase(50, "50-01", 2050)]
[TestCase(50, "51-01", 1951)]
[TestCase(99, "00-01", 2000)]
[TestCase(99, "99-01", 2099)]
public void WithTwoDigitYearMax(int twoDigitYearMax, string text, int expectedYear)
{
var pattern = YearMonthPattern.CreateWithInvariantCulture("yy-MM").WithTwoDigitYearMax(twoDigitYearMax);
var value = pattern.Parse(text).Value;
Assert.AreEqual(expectedYear, value.Year);
}

[Test]
[TestCase(-1)]
[TestCase(100)]
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
public void WithTwoDigitYearMax_Invalid(int twoDigitYearMax) =>
Assert.Throws<ArgumentOutOfRangeException>(() => YearMonthPattern.Iso.WithTwoDigitYearMax(twoDigitYearMax));

public sealed class Data : PatternTestData<YearMonth>
{
// Default to the start of the year 2000.
Expand Down
24 changes: 24 additions & 0 deletions src/NodaTime.Test/Text/ZonedDateTimePatternTest.cs
Expand Up @@ -6,6 +6,7 @@
using NodaTime.Text;
using NodaTime.TimeZones;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
Expand Down Expand Up @@ -321,6 +322,29 @@ public void FindLongestZoneId()
[Test]
public void ParseNull() => AssertParseNull(ZonedDateTimePattern.ExtendedFormatOnlyIso.WithZoneProvider(TestProvider));

[Test]
[TestCase(0, "00-01-01T00:00:00 abc", 2000)]
[TestCase(0, "01-01-01T00:00:00 abc", 1901)]
[TestCase(50, "49-01-01T00:00:00 abc", 2049)]
[TestCase(50, "50-01-01T00:00:00 abc", 2050)]
[TestCase(50, "51-01-01T00:00:00 abc", 1951)]
[TestCase(99, "00-01-01T00:00:00 abc", 2000)]
[TestCase(99, "99-01-01T00:00:00 abc", 2099)]
public void WithTwoDigitYearMax(int twoDigitYearMax, string text, int expectedYear)
{
var pattern = ZonedDateTimePattern.CreateWithInvariantCulture("yy-MM-dd'T'HH:mm:ss z", TestProvider).WithTwoDigitYearMax(twoDigitYearMax);
var value = pattern.Parse(text).Value;
Assert.AreEqual(expectedYear, value.Year);
}

[Test]
[TestCase(-1)]
[TestCase(100)]
[TestCase(int.MinValue)]
[TestCase(int.MaxValue)]
public void WithTwoDigitYearMax_Invalid(int twoDigitYearMax) =>
Assert.Throws<ArgumentOutOfRangeException>(() => ZonedDateTimePattern.GeneralFormatOnlyIso.WithTwoDigitYearMax(twoDigitYearMax));

public sealed class Data : PatternTestData<ZonedDateTime>
{
// Default to the start of the year 2000 UTC
Expand Down
14 changes: 7 additions & 7 deletions src/NodaTime/Globalization/NodaFormatInfo.cs
Expand Up @@ -203,16 +203,16 @@ private static IReadOnlyList<string> ConvertGenitiveMonthArray(IReadOnlyList<str

internal FixedFormatInfoPatternParser<Duration> DurationPatternParser => EnsureFixedFormatInitialized(ref durationPatternParser, () => new DurationPatternParser());
internal FixedFormatInfoPatternParser<Offset> OffsetPatternParser => EnsureFixedFormatInitialized(ref offsetPatternParser, () => new OffsetPatternParser());
internal FixedFormatInfoPatternParser<Instant> InstantPatternParser => EnsureFixedFormatInitialized(ref instantPatternParser, () => new InstantPatternParser(InstantPattern.DefaultTemplateValue));
internal FixedFormatInfoPatternParser<Instant> InstantPatternParser => EnsureFixedFormatInitialized(ref instantPatternParser, () => new InstantPatternParser(InstantPattern.DefaultTemplateValue, LocalDatePattern.DefaultTwoDigitYearMax));
internal FixedFormatInfoPatternParser<LocalTime> LocalTimePatternParser => EnsureFixedFormatInitialized(ref localTimePatternParser, () => new LocalTimePatternParser(LocalTime.Midnight));
internal FixedFormatInfoPatternParser<LocalDate> LocalDatePatternParser => EnsureFixedFormatInitialized(ref localDatePatternParser, () => new LocalDatePatternParser(LocalDatePattern.DefaultTemplateValue));
internal FixedFormatInfoPatternParser<LocalDateTime> LocalDateTimePatternParser => EnsureFixedFormatInitialized(ref localDateTimePatternParser, () => new LocalDateTimePatternParser(LocalDateTimePattern.DefaultTemplateValue));
internal FixedFormatInfoPatternParser<OffsetDateTime> OffsetDateTimePatternParser => EnsureFixedFormatInitialized(ref offsetDateTimePatternParser, () => new OffsetDateTimePatternParser(OffsetDateTimePattern.DefaultTemplateValue));
internal FixedFormatInfoPatternParser<OffsetDate> OffsetDatePatternParser => EnsureFixedFormatInitialized(ref offsetDatePatternParser, () => new OffsetDatePatternParser(OffsetDatePattern.DefaultTemplateValue));
internal FixedFormatInfoPatternParser<LocalDate> LocalDatePatternParser => EnsureFixedFormatInitialized(ref localDatePatternParser, () => new LocalDatePatternParser(LocalDatePattern.DefaultTemplateValue, LocalDatePattern.DefaultTwoDigitYearMax));
internal FixedFormatInfoPatternParser<LocalDateTime> LocalDateTimePatternParser => EnsureFixedFormatInitialized(ref localDateTimePatternParser, () => new LocalDateTimePatternParser(LocalDateTimePattern.DefaultTemplateValue, LocalDatePattern.DefaultTwoDigitYearMax));
internal FixedFormatInfoPatternParser<OffsetDateTime> OffsetDateTimePatternParser => EnsureFixedFormatInitialized(ref offsetDateTimePatternParser, () => new OffsetDateTimePatternParser(OffsetDateTimePattern.DefaultTemplateValue, LocalDatePattern.DefaultTwoDigitYearMax));
internal FixedFormatInfoPatternParser<OffsetDate> OffsetDatePatternParser => EnsureFixedFormatInitialized(ref offsetDatePatternParser, () => new OffsetDatePatternParser(OffsetDatePattern.DefaultTemplateValue, LocalDatePattern.DefaultTwoDigitYearMax));
internal FixedFormatInfoPatternParser<OffsetTime> OffsetTimePatternParser => EnsureFixedFormatInitialized(ref offsetTimePatternParser, () => new OffsetTimePatternParser(OffsetTimePattern.DefaultTemplateValue));
internal FixedFormatInfoPatternParser<ZonedDateTime> ZonedDateTimePatternParser => EnsureFixedFormatInitialized(ref zonedDateTimePatternParser, () => new ZonedDateTimePatternParser(ZonedDateTimePattern.DefaultTemplateValue, Resolvers.StrictResolver, null));
internal FixedFormatInfoPatternParser<ZonedDateTime> ZonedDateTimePatternParser => EnsureFixedFormatInitialized(ref zonedDateTimePatternParser, () => new ZonedDateTimePatternParser(ZonedDateTimePattern.DefaultTemplateValue, Resolvers.StrictResolver, null, LocalDatePattern.DefaultTwoDigitYearMax));
internal FixedFormatInfoPatternParser<AnnualDate> AnnualDatePatternParser => EnsureFixedFormatInitialized(ref annualDatePatternParser, () => new AnnualDatePatternParser(AnnualDatePattern.DefaultTemplateValue));
internal FixedFormatInfoPatternParser<YearMonth> YearMonthPatternParser => EnsureFixedFormatInitialized(ref yearMonthPatternParser, () => new YearMonthPatternParser(YearMonthPattern.DefaultTemplateValue));
internal FixedFormatInfoPatternParser<YearMonth> YearMonthPatternParser => EnsureFixedFormatInitialized(ref yearMonthPatternParser, () => new YearMonthPatternParser(YearMonthPattern.DefaultTemplateValue, LocalDatePattern.DefaultTwoDigitYearMax));

private FixedFormatInfoPatternParser<T> EnsureFixedFormatInitialized<T>(ref FixedFormatInfoPatternParser<T>? field,
Func<IPatternParser<T>> patternParserFactory)
Expand Down

0 comments on commit 838afe8

Please sign in to comment.