Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize DateTime.Year/.DayOfYear properties; fix comments #73277

Merged
merged 16 commits into from
Aug 6, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
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
64 changes: 27 additions & 37 deletions src/libraries/System.Private.CoreLib/src/System/DateTime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,16 @@ public readonly partial struct DateTime
// All OA dates must be less than (not <=) OADateMaxAsDouble
private const double OADateMaxAsDouble = 2958466.0;

// Euclidean Affine Functions Algorithm constants
// Euclidean Affine Functions Algorithm (EAF) constants

// Constants used for fast calculation of following subexpressions
// x / DaysPer4Years
// x % DaysPer4Years / 4
private const uint EafMultiplier = (uint)(((1UL << 32) + DaysPer4Years - 1) / DaysPer4Years); // 2,939,745
private const uint EafDivider = EafMultiplier * 4; // 11,758,980

private const ulong TicksPer6Hours = TicksPerHour * 6;
private const int March1BasedDayOfNewYear = 306; // Days between March 1 and January 1
private const int March1BasedDayOfNewYear = 306; // Days between March 1 and January 1

private static readonly uint[] s_daysToMonth365 = {
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
Expand Down Expand Up @@ -1356,13 +1363,13 @@ public DateTime Date
// Cassio Neri, Lorenz Schneiderhttps - Euclidean Affine Functions and Applications to Calendar Algorithms - 2021
internal void GetDate(out int year, out int month, out int day)
{
// y400 = number of whole 400-year periods since 3/1/0000
// r1 = day number within 400-year period
(uint y400, uint r1) = Math.DivRem(((uint)(UTicks / TicksPer6Hours) | 3U) + 1224, DaysPer400Years);
ulong u2 = (ulong)Math.BigMul(2939745, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / 11758980);
// y100 = number of whole 100-year periods since 3/1/0000
danmoseley marked this conversation as resolved.
Show resolved Hide resolved
// r1 = (day number within 100-year period) * 4
(uint y100, uint r1) = Math.DivRem(((uint)(UTicks / TicksPer6Hours) | 3U) + 1224, DaysPer400Years);
ulong u2 = (ulong)Math.BigMul((int)EafMultiplier, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / EafDivider);
int n3 = 2141 * daySinceMarch1 + 197913;
year = (int)(100 * y400 + (uint)(u2 >> 32));
year = (int)(100 * y100 + (uint)(u2 >> 32));
// compute month and day
month = (ushort)(n3 >> 16);
day = (ushort)n3 / 2141 + 1;
Expand Down Expand Up @@ -1419,10 +1426,10 @@ public int Day
{
get
{
// r1 = day number within 400-year period
// r1 = (day number within 100-year period) * 4
uint r1 = (((uint)(UTicks / TicksPer6Hours) | 3U) + 1224) % DaysPer400Years;
ulong u2 = (ulong)Math.BigMul(2939745, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / 11758980);
ulong u2 = (ulong)Math.BigMul((int)EafMultiplier, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / EafDivider);
int n3 = 2141 * daySinceMarch1 + 197913;
// Return 1-based day-of-month
return (ushort)n3 / 2141 + 1;
Expand All @@ -1439,22 +1446,8 @@ public int Day
// Returns the day-of-year part of this DateTime. The returned value
// is an integer between 1 and 366.
//
public int DayOfYear
{
get
{
// y400 = number of whole 400-year periods since 3/1/0000
// r1 = day number within 400-year period
(uint y400, uint r1) = Math.DivRem(((uint)(UTicks / TicksPer6Hours) | 3U) + 1224, DaysPer400Years);
ulong u2 = (ulong)Math.BigMul(2939745, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / 11758980);

int year = (int)(100 * y400 + (uint)(u2 >> 32)) + (daySinceMarch1 >= March1BasedDayOfNewYear ? 1 : 0);
return daySinceMarch1 >= March1BasedDayOfNewYear // DatePartDayOfYear case
? daySinceMarch1 - March1BasedDayOfNewYear + 1 // rollover December 31
: daySinceMarch1 + (366 - March1BasedDayOfNewYear) + (IsLeapYear(year) ? 1 : 0);
}
}
public int DayOfYear =>
1 + (int)(((((uint)(UTicks / TicksPer6Hours) | 3U) % (uint)DaysPer400Years) | 3U) * EafMultiplier / EafDivider);

// Returns the hash code for this DateTime.
//
Expand Down Expand Up @@ -1510,10 +1503,10 @@ public int Month
{
get
{
// r1 = day number within 400-year period
// r1 = (day number within 100-year period) * 4
uint r1 = (((uint)(UTicks / TicksPer6Hours) | 3U) + 1224) % DaysPer400Years;
ulong u2 = (ulong)Math.BigMul(2939745, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / 11758980);
ulong u2 = (ulong)Math.BigMul((int)EafMultiplier, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / EafDivider);
int n3 = 2141 * daySinceMarch1 + 197913;
return (ushort)(n3 >> 16) - (daySinceMarch1 >= March1BasedDayOfNewYear ? 12 : 0);
}
Expand Down Expand Up @@ -1569,13 +1562,10 @@ public int Year
{
get
{
// y400 = number of whole 400-year periods since 3/1/0000
// r1 = day number within 400-year period
(uint y400, uint r1) = Math.DivRem(((uint)(UTicks / TicksPer6Hours) | 3U) + 1224, DaysPer400Years);
ulong u2 = (ulong)Math.BigMul(2939745, (int)r1 | 3);
ushort daySinceMarch1 = (ushort)((uint)u2 / 11758980);

return (int)(100 * y400 + (uint)(u2 >> 32)) + (daySinceMarch1 >= March1BasedDayOfNewYear ? 1 : 0);
// y100 = number of whole 100-year periods since 1/1/0001
// r1 = (day number within 100-year period) * 4
(uint y100, uint r1) = Math.DivRem(((uint)(UTicks / TicksPer6Hours) | 3U), DaysPer400Years);
return 1 + (int)(100 * y100 + (r1 | 3) / DaysPer4Years);
}
}

Expand Down
38 changes: 38 additions & 0 deletions src/libraries/System.Runtime/tests/System/DateTimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,44 @@ public void DayOfYear_Get_ReturnsExpected()
Assert.Equal(170, dateTime.DayOfYear);
}

[Fact]
public void DayOfYear_Random()
{
var random = new Random(2022);
var tries = 1000;
for (int i = 0; i < tries; ++i)
{
var dateTime = new DateTime(random.NextInt64(DateTime.MaxValue.Ticks));
var startOfYear = new DateTime(dateTime.Year, 1, 1);
var expectedDayOfYear = 1 + (dateTime - startOfYear).Days;
Assert.Equal(expectedDayOfYear, dateTime.DayOfYear);
}
}

[Fact]
public void DayOfYear_Exhaustive()
{
var ticksPer6hours = TimeSpan.TicksPerHour * 6;
for (int year = DateTime.MinValue.Year, lastYear = DateTime.MaxValue.Year; year <= lastYear; ++year)
{
var startOfYear = new DateTime(year, 1, 1);
for (int expectedDayOfYear = 1, lastDayOfYear = DateTime.IsLeapYear(expectedDayOfYear) ? 366 : 365; expectedDayOfYear <= lastDayOfYear; ++expectedDayOfYear)
{
var dateTime = startOfYear.AddDays(expectedDayOfYear - 1); // .DayOfYear implementation uses 6-hour granularity value
Assert.Equal(expectedDayOfYear, dateTime.DayOfYear); // and we check 4 time points per day

dateTime = dateTime.AddTicks(ticksPer6hours);
Assert.Equal(expectedDayOfYear, dateTime.DayOfYear);

dateTime = dateTime.AddTicks(ticksPer6hours);
Assert.Equal(expectedDayOfYear, dateTime.DayOfYear);

dateTime = dateTime.AddTicks(ticksPer6hours);
Assert.Equal(expectedDayOfYear, dateTime.DayOfYear);
}
}
}

[Fact]
public void TimeOfDay_Get_ReturnsExpected()
{
Expand Down