Skip to content

Commit 6aaf627

Browse files
authored
Merge pull request #96 from rsdn/feature-round-datetime
+ DateTime/DatetimeOffset .Round() methods
2 parents 180f87f + ae3b0e1 commit 6aaf627

File tree

4 files changed

+476
-25
lines changed

4 files changed

+476
-25
lines changed

CodeJam.Main.Tests/Dates/DateTimeExtensionsTests.cs

Lines changed: 247 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,58 @@
88

99
namespace CodeJam.Dates
1010
{
11-
// TODO: tests with lastDayOfMont / tests with DST
11+
// TODO: tests with lastDayOfMonth / tests with DST
1212
public static class DateTimeExtensionsTests
1313
{
14+
private static DateTime DateWithSameOddityDay(DateTime date, int day)
15+
{
16+
// Ensure that day is as even as value
17+
var dateDay = date.WithDay(day);
18+
var totalDays = (long)(dateDay - DateTime.MinValue).TotalDays;
19+
if (totalDays % 2 != day % 2)
20+
dateDay = date.WithDay(day + 1);
21+
22+
return dateDay;
23+
}
24+
25+
private static DateTimeOffset DateWithSameOddityDay(DateTimeOffset date, int day) =>
26+
DateWithSameOddityDay(date.Date, day);
27+
28+
[Test]
29+
public static void TestDivideRoundToEvenNaive()
30+
{
31+
AreEqual(DateTimeExtensions.DivideRoundToEvenNaive(6, 2), 3);
32+
AreEqual(DateTimeExtensions.DivideRoundToEvenNaive(7, 3), 2);
33+
AreEqual(DateTimeExtensions.DivideRoundToEvenNaive(11, 3), 4);
34+
AreEqual(DateTimeExtensions.DivideRoundToEvenNaive(14, 3), 5);
35+
36+
AreEqual(DateTimeExtensions.DivideRoundToEvenNaive(7, 2), 4);
37+
AreEqual(DateTimeExtensions.DivideRoundToEvenNaive(9, 2), 4);
38+
39+
AreEqual(DateTimeExtensions.DivideRoundToEvenNaive(14000, 4000), 4);
40+
AreEqual(DateTimeExtensions.DivideRoundToEvenNaive(18000, 4000), 4);
41+
}
42+
43+
[Test]
44+
public static void TestDivideRoundToEvenNaiveRandom()
45+
{
46+
var rnd = TestTools.GetTestRandom();
47+
for (int i = 0; i < 100; i++)
48+
{
49+
var a = (long)rnd.Next(1, int.MaxValue);
50+
var b = (long)rnd.Next(1, 10);
51+
var b2 = (long)rnd.Next((int)TimeSpan.TicksPerSecond, 200 * (int)TimeSpan.TicksPerSecond);
52+
53+
var div1 = DateTimeExtensions.DivideRoundToEvenNaive(a, b);
54+
var div2 = (long)Math.Round(1m * a / b);
55+
AreEqual(div1, div2);
56+
57+
div1 = DateTimeExtensions.DivideRoundToEvenNaive(a, b2);
58+
div2 = (long)Math.Round(1m * a / b2);
59+
AreEqual(div1, div2);
60+
}
61+
}
62+
1463
[Test]
1564
public static void TestDateTimeExtensions()
1665
{
@@ -36,36 +85,17 @@ public static void TestDateTimeExtensions()
3685
AreEqual(d.GetYearRange().CountOfDays(), d.DaysInYear());
3786
}
3887

39-
[Test]
40-
public static void TestIsUtc()
41-
{
42-
var now = DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(1));
43-
var utcNow = DateTimeOffset.UtcNow;
44-
IsFalse(now.IsUtc());
45-
IsTrue(utcNow.IsUtc());
46-
IsTrue(now.ToUniversalTime().IsUtc());
47-
}
48-
49-
[Test]
50-
public static void TestTruncateTime()
51-
{
52-
var now = DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(1));
53-
var utcNow = DateTimeOffset.UtcNow;
54-
AreEqual(now.TruncateTime(), now - now.TimeOfDay);
55-
AreEqual(utcNow.TruncateTime(), utcNow - utcNow.TimeOfDay);
56-
}
57-
5888
[Test]
5989
public static void TestDateTimeOffsetExtensions()
6090
{
61-
var d = DateTime.Today.FirstDayOfMonth().AddHours(12);
91+
var d = DateTime.Today.FirstDayOfMonth().AddHours(12).ToOffset();
6292

6393
AreEqual(d.NextDay().PrevDay(), d);
6494
AreEqual(d.NextMonth().PrevMonth(), d);
6595
AreEqual(d.NextYear().PrevYear(), d);
6696
AreEqual(d.LastDayOfMonth().Day, d.DaysInMonth());
6797
AreEqual(d.WithMonth(1).WithDay(1), d.FirstDayOfYear().AddHours(12));
68-
AreEqual(d.WithYearAndMonth(2000, 1).WithDay(1), new DateTime(2000, 1, 1).AddHours(12));
98+
AreEqual(d.WithYearAndMonth(2000, 1).WithDay(1), new DateTime(2000, 1, 1).AddHours(12).ToOffset());
6999
AreEqual(d.WithMonthAndDay(1, 1), d.FirstDayOfYear().AddHours(12));
70100

71101
AreEqual(d.GetMonthRange().CountOfDays(), d.DaysInMonth());
@@ -81,7 +111,26 @@ public static void TestDateTimeOffsetExtensions()
81111
}
82112

83113
[Test]
84-
public static void UseLastDayTests()
114+
public static void TestIsUtc()
115+
{
116+
var now = DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(1));
117+
var utcNow = DateTimeOffset.UtcNow;
118+
IsFalse(now.IsUtc());
119+
IsTrue(utcNow.IsUtc());
120+
IsTrue(now.ToUniversalTime().IsUtc());
121+
}
122+
123+
[Test]
124+
public static void TestTruncateTime()
125+
{
126+
var now = DateTimeOffset.Now.ToOffset(TimeSpan.FromHours(1));
127+
var utcNow = DateTimeOffset.UtcNow;
128+
AreEqual(now.TruncateTime(), now - now.TimeOfDay);
129+
AreEqual(utcNow.TruncateTime(), utcNow - utcNow.TimeOfDay);
130+
}
131+
132+
[Test]
133+
public static void TestUseLastDayMode()
85134
{
86135
var d = new DateTime(2017, 2, 28);
87136
AreEqual(31, d.AddMonths(1, true).Day, "AddMonths");
@@ -116,5 +165,180 @@ public static void PreserveOriginalOffset(double offset)
116165
AreEqual(timespan, d.FirstDayOfYear().Offset, "#3");
117166
AreEqual(timespan, d.LastDayOfYear().Offset, "#4");
118167
}
168+
169+
[Test]
170+
public static void TestTruncate()
171+
{
172+
var date = new DateTime(1, 2, 3, 4, 55, 6, DateTimeKind.Local);
173+
var dateOffset = date.ToOffset();
174+
175+
AreEqual(date.AddMilliseconds(500).TruncateMilliseconds(), date);
176+
AreEqual(date.AddMilliseconds(499).TruncateMilliseconds(), date);
177+
AreEqual(date.AddMilliseconds(1600).TruncateMilliseconds(), date.AddSeconds(1));
178+
179+
AreEqual(date.Truncate(TimeSpan.FromDays(2)), date.Date.AddDays(-1));
180+
AreEqual(date.Truncate(TimeSpan.FromHours(1)), date.Date.AddHours(4));
181+
AreEqual(date.Truncate(TimeSpan.FromMinutes(30)), date.Date.AddHours(4).AddMinutes(30));
182+
183+
AreEqual(dateOffset.AddMilliseconds(500).TruncateMilliseconds(), dateOffset);
184+
AreEqual(dateOffset.AddMilliseconds(499).TruncateMilliseconds(), dateOffset);
185+
AreEqual(dateOffset.AddMilliseconds(1600).TruncateMilliseconds(), dateOffset.AddSeconds(1));
186+
187+
AreEqual(dateOffset.Truncate(TimeSpan.FromDays(2)), dateOffset.TruncateTime().AddDays(-1));
188+
AreEqual(dateOffset.Truncate(TimeSpan.FromHours(1)), dateOffset.TruncateTime().AddHours(4));
189+
AreEqual(dateOffset.Truncate(TimeSpan.FromMinutes(30)), dateOffset.TruncateTime().AddHours(4).AddMinutes(30));
190+
}
191+
192+
[Test]
193+
public static void TestCeiling()
194+
{
195+
var date = new DateTime(1, 2, 3, 4, 55, 6, DateTimeKind.Local);
196+
var dateOffset = date.ToOffset();
197+
198+
AreEqual(date.AddMilliseconds(500).CeilingMilliseconds(), date.AddSeconds(1));
199+
AreEqual(date.AddMilliseconds(499).CeilingMilliseconds(), date.AddSeconds(1));
200+
AreEqual(date.AddMilliseconds(1600).CeilingMilliseconds(), date.AddSeconds(2));
201+
202+
AreEqual(date.Ceiling(TimeSpan.FromDays(2)), date.Date.AddDays(1));
203+
AreEqual(date.Ceiling(TimeSpan.FromHours(1)), date.Date.AddHours(5));
204+
AreEqual(date.Ceiling(TimeSpan.FromMinutes(30)), date.Date.AddHours(5));
205+
206+
AreEqual(dateOffset.AddMilliseconds(500).CeilingMilliseconds(), dateOffset.AddSeconds(1));
207+
AreEqual(dateOffset.AddMilliseconds(499).CeilingMilliseconds(), dateOffset.AddSeconds(1));
208+
AreEqual(dateOffset.AddMilliseconds(1600).CeilingMilliseconds(), dateOffset.AddSeconds(2));
209+
210+
AreEqual(dateOffset.Ceiling(TimeSpan.FromDays(2)), dateOffset.TruncateTime().AddDays(1));
211+
AreEqual(dateOffset.Ceiling(TimeSpan.FromHours(1)), dateOffset.TruncateTime().AddHours(5));
212+
AreEqual(dateOffset.Ceiling(TimeSpan.FromMinutes(30)), dateOffset.TruncateTime().AddHours(5));
213+
}
214+
215+
[Test]
216+
public static void TestRound()
217+
{
218+
var date = new DateTime(1, 2, 3, 4, 55, 6, DateTimeKind.Local);
219+
var dateOffset = date.ToOffset();
220+
221+
AreEqual(date.AddMilliseconds(500).RoundMilliseconds(), date);
222+
AreEqual(date.AddMilliseconds(499).RoundMilliseconds(), date);
223+
AreEqual(date.AddMilliseconds(1600).RoundMilliseconds(), date.AddSeconds(2));
224+
225+
AreEqual(date.Round(TimeSpan.FromDays(2)), date.Date.AddDays(1));
226+
AreEqual(date.Round(TimeSpan.FromHours(1)), date.Date.AddHours(5));
227+
AreEqual(date.Round(TimeSpan.FromMinutes(30)), date.Date.AddHours(5));
228+
229+
AreEqual(dateOffset.AddMilliseconds(500).RoundMilliseconds(), dateOffset);
230+
AreEqual(dateOffset.AddMilliseconds(499).RoundMilliseconds(), dateOffset);
231+
AreEqual(dateOffset.AddMilliseconds(1600).RoundMilliseconds(), dateOffset.AddSeconds(2));
232+
233+
AreEqual(dateOffset.Round(TimeSpan.FromDays(2)), dateOffset.TruncateTime().AddDays(1));
234+
AreEqual(dateOffset.Round(TimeSpan.FromHours(1)), dateOffset.TruncateTime().AddHours(5));
235+
AreEqual(dateOffset.Round(TimeSpan.FromMinutes(30)), dateOffset.TruncateTime().AddHours(5));
236+
}
237+
238+
[TestCase(MidpointRounding.ToEven, 1)]
239+
[TestCase(MidpointRounding.ToEven, 2)]
240+
[TestCase(MidpointRounding.ToEven, 10)]
241+
[TestCase(MidpointRounding.ToEven, 5)]
242+
[TestCase(MidpointRounding.AwayFromZero, 1)]
243+
[TestCase(MidpointRounding.AwayFromZero, 2)]
244+
[TestCase(MidpointRounding.AwayFromZero, 10)]
245+
[TestCase(MidpointRounding.AwayFromZero, 5)]
246+
#if TARGETS_NET || LESSTHAN_NETCOREAPP30
247+
// Some MidpointRounding values are missing if targeting to these frameworks
248+
#else
249+
[TestCase(MidpointRounding.ToZero, 1)]
250+
[TestCase(MidpointRounding.ToZero, 2)]
251+
[TestCase(MidpointRounding.ToZero, 10)]
252+
[TestCase(MidpointRounding.ToZero, 5)]
253+
[TestCase(MidpointRounding.ToNegativeInfinity, 1)]
254+
[TestCase(MidpointRounding.ToNegativeInfinity, 2)]
255+
[TestCase(MidpointRounding.ToNegativeInfinity, 10)]
256+
[TestCase(MidpointRounding.ToNegativeInfinity, 5)]
257+
[TestCase(MidpointRounding.ToPositiveInfinity, 1)]
258+
[TestCase(MidpointRounding.ToPositiveInfinity, 2)]
259+
[TestCase(MidpointRounding.ToPositiveInfinity, 10)]
260+
[TestCase(MidpointRounding.ToPositiveInfinity, 5)]
261+
#endif
262+
public static void TestRounding(MidpointRounding mode, int value)
263+
{
264+
var date = DateTime.Today.FirstDayOfMonth();
265+
var dateDay = DateWithSameOddityDay(date, value);
266+
var dateHours = date.AddHours(value);
267+
var dateMinutes = date.AddMinutes(value);
268+
var dateSeconds = date.AddSeconds(value);
269+
270+
var minRoundOffset = (int)Math.Round(value + 0.1, mode) - value;
271+
var roundOffset = (int)Math.Round(value + 0.5, mode) - value;
272+
var maxRoundOffset = (int)Math.Round(value + 0.9, mode) - value;
273+
274+
AreEqual(dateSeconds.AddMilliseconds(1).RoundMilliseconds(mode), dateSeconds.AddSeconds(minRoundOffset));
275+
AreEqual(dateSeconds.AddMilliseconds(500).RoundMilliseconds(mode), dateSeconds.AddSeconds(roundOffset));
276+
AreEqual(dateSeconds.AddMilliseconds(999).RoundMilliseconds(mode), dateSeconds.AddSeconds(maxRoundOffset));
277+
278+
AreEqual(dateMinutes.AddSeconds(1).Round(TimeSpan.FromMinutes(1), mode), dateMinutes.AddMinutes(minRoundOffset));
279+
AreEqual(dateMinutes.AddSeconds(30).Round(TimeSpan.FromMinutes(1), mode), dateMinutes.AddMinutes(roundOffset));
280+
AreEqual(dateMinutes.AddSeconds(59).Round(TimeSpan.FromMinutes(1), mode), dateMinutes.AddMinutes(maxRoundOffset));
281+
282+
AreEqual(dateHours.AddMinutes(1).Round(TimeSpan.FromHours(1), mode), dateHours.AddHours(minRoundOffset));
283+
AreEqual(dateHours.AddMinutes(30).Round(TimeSpan.FromHours(1), mode), dateHours.AddHours(roundOffset));
284+
AreEqual(dateHours.AddMinutes(59).Round(TimeSpan.FromHours(1), mode), dateHours.AddHours(maxRoundOffset));
285+
286+
AreEqual(dateDay.AddHours(1).Round(TimeSpan.FromDays(1), mode), dateDay.AddDays(minRoundOffset));
287+
AreEqual(dateDay.AddHours(12).Round(TimeSpan.FromDays(1), mode), dateDay.AddDays(roundOffset));
288+
AreEqual(dateDay.AddHours(23).Round(TimeSpan.FromDays(1), mode), dateDay.AddDays(maxRoundOffset));
289+
}
290+
291+
[TestCase(MidpointRounding.ToEven, 1)]
292+
[TestCase(MidpointRounding.ToEven, 2)]
293+
[TestCase(MidpointRounding.ToEven, 10)]
294+
[TestCase(MidpointRounding.ToEven, 5)]
295+
[TestCase(MidpointRounding.AwayFromZero, 1)]
296+
[TestCase(MidpointRounding.AwayFromZero, 2)]
297+
[TestCase(MidpointRounding.AwayFromZero, 10)]
298+
[TestCase(MidpointRounding.AwayFromZero, 5)]
299+
#if TARGETS_NET || LESSTHAN_NETCOREAPP30
300+
// Some MidpointRounding values are missing if targeting to these frameworks
301+
#else
302+
[TestCase(MidpointRounding.ToZero, 1)]
303+
[TestCase(MidpointRounding.ToZero, 2)]
304+
[TestCase(MidpointRounding.ToZero, 10)]
305+
[TestCase(MidpointRounding.ToZero, 5)]
306+
[TestCase(MidpointRounding.ToNegativeInfinity, 1)]
307+
[TestCase(MidpointRounding.ToNegativeInfinity, 2)]
308+
[TestCase(MidpointRounding.ToNegativeInfinity, 10)]
309+
[TestCase(MidpointRounding.ToNegativeInfinity, 5)]
310+
[TestCase(MidpointRounding.ToPositiveInfinity, 1)]
311+
[TestCase(MidpointRounding.ToPositiveInfinity, 2)]
312+
[TestCase(MidpointRounding.ToPositiveInfinity, 10)]
313+
[TestCase(MidpointRounding.ToPositiveInfinity, 5)]
314+
#endif
315+
public static void TestRoundingOffset(MidpointRounding mode, int value)
316+
{
317+
var date = DateTime.Today.FirstDayOfMonth().ToOffset();
318+
var dateDay = DateWithSameOddityDay(date, value);
319+
var dateHours = date.AddHours(value);
320+
var dateMinutes = date.AddMinutes(value);
321+
var dateSeconds = date.AddSeconds(value);
322+
323+
var minRoundOffset = (int)Math.Round(value + 0.1, mode) - value;
324+
var roundOffset = (int)Math.Round(value + 0.5, mode) - value;
325+
var maxRoundOffset = (int)Math.Round(value + 0.9, mode) - value;
326+
327+
AreEqual(dateSeconds.AddMilliseconds(1).RoundMilliseconds(mode), dateSeconds.AddSeconds(minRoundOffset));
328+
AreEqual(dateSeconds.AddMilliseconds(500).RoundMilliseconds(mode), dateSeconds.AddSeconds(roundOffset));
329+
AreEqual(dateSeconds.AddMilliseconds(999).RoundMilliseconds(mode), dateSeconds.AddSeconds(maxRoundOffset));
330+
331+
AreEqual(dateMinutes.AddSeconds(1).Round(TimeSpan.FromMinutes(1), mode), dateMinutes.AddMinutes(minRoundOffset));
332+
AreEqual(dateMinutes.AddSeconds(30).Round(TimeSpan.FromMinutes(1), mode), dateMinutes.AddMinutes(roundOffset));
333+
AreEqual(dateMinutes.AddSeconds(59).Round(TimeSpan.FromMinutes(1), mode), dateMinutes.AddMinutes(maxRoundOffset));
334+
335+
AreEqual(dateHours.AddMinutes(1).Round(TimeSpan.FromHours(1), mode), dateHours.AddHours(minRoundOffset));
336+
AreEqual(dateHours.AddMinutes(30).Round(TimeSpan.FromHours(1), mode), dateHours.AddHours(roundOffset));
337+
AreEqual(dateHours.AddMinutes(59).Round(TimeSpan.FromHours(1), mode), dateHours.AddHours(maxRoundOffset));
338+
339+
AreEqual(dateDay.AddHours(1).Round(TimeSpan.FromDays(1), mode), dateDay.AddDays(minRoundOffset));
340+
AreEqual(dateDay.AddHours(12).Round(TimeSpan.FromDays(1), mode), dateDay.AddDays(roundOffset));
341+
AreEqual(dateDay.AddHours(23).Round(TimeSpan.FromDays(1), mode), dateDay.AddDays(maxRoundOffset));
342+
}
119343
}
120344
}

CodeJam.Main/Dates/DateTimeExtensions.NonGenerated.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,49 @@ namespace CodeJam.Dates
1010
[PublicAPI]
1111
public static partial class DateTimeExtensions
1212
{
13+
// DONTTOUCH: benchmark first.
14+
// This implementation is fast enough, ~1.5x compared to long division.
15+
// internal as covered by test.
16+
internal static long DivideRoundToEvenNaive(long ticks, long ticksModule)
17+
{
18+
DebugCode.BugIf(ticks < 0, "value < 0");
19+
DebugCode.BugIf(ticksModule <= 0, "div <= 0");
20+
21+
#if LESSTHAN_NET20 || LESSTHAN_NETSTANDARD20 || LESSTHAN_NETCOREAPP20
22+
var truncate = ticks / ticksModule;
23+
var offset = ticks % ticksModule;
24+
#else
25+
var truncate = Math.DivRem(ticks, ticksModule, out var offset);
26+
#endif
27+
28+
if (offset == 0)
29+
return truncate;
30+
31+
var doubleOffset = offset << 1;
32+
if (doubleOffset < ticksModule) // below midpoint
33+
return truncate;
34+
if (doubleOffset > ticksModule) // above midpoint
35+
return truncate + 1;
36+
37+
// Tie breaker part, round to nearest even
38+
if (truncate % 2 == 0)
39+
return truncate;
40+
41+
return truncate + 1;
42+
}
43+
1344
private static DateTime Create(DateTime origin, int year, int month, int day) =>
1445
new DateTime(year, month, day, 0, 0, 0, origin.Kind);
1546

1647
private static DateTimeOffset Create(DateTimeOffset origin, int year, int month, int day) =>
1748
new DateTimeOffset(year, month, day, 0, 0, 0, origin.Offset);
1849

50+
private static DateTime Create(DateTime origin, long ticks) =>
51+
new DateTime(ticks, origin.Kind);
52+
53+
private static DateTimeOffset Create(DateTimeOffset origin, long ticks) =>
54+
new DateTimeOffset(ticks, origin.Offset);
55+
1956
/// <summary>Returns count of days in month.</summary>
2057
/// <param name="date">The date.</param>
2158
/// <returns>Count of days in month.</returns>

0 commit comments

Comments
 (0)