C# DateTime Extension Class (Floor, Ceiling and Rounding from TimeSpan)

Wed, 27 Jan 2021 09:49 UTC by garethbrown

So many projects I work on end up with a DateTime extesion class for common date operations in C#.

Here is what that class usually consists of:

using System;

namespace SomeProject.Extensions
{
    public static class DateTimeExtensions
    {
        /// <summary>
        /// Compare date equality to within a given interval (TimeSpan)
        /// </summary>
        /// <param name="dateTime"></param>
        /// <param name="timeSpan"></param>
        /// <param name="compareDateTime"></param>
        /// <returns></returns>
        public static bool IsEqualToWithin(this DateTime dateTime, TimeSpan timeSpan, DateTime compareDateTime)
        {
            var diffTicks = Math.Abs((dateTime - compareDateTime).Ticks);

            return timeSpan.Ticks >= diffTicks;
        }

        /// <summary>
        /// Floor date to interval (TimeSpan)
        /// </summary>
        /// <param name="dateTime"></param>
        /// <param name="interval"></param>
        /// <returns></returns>
        public static DateTime Floor(this DateTime dateTime, TimeSpan interval)
        {
            return dateTime.AddTicks(-(dateTime.Ticks % interval.Ticks));
        }

        /// <summary>
        /// Ceiling date to interval (TimeSpan)
        /// </summary>
        /// <param name="dateTime"></param>
        /// <param name="interval"></param>
        /// <returns></returns>
        public static DateTime Ceiling(this DateTime dateTime, TimeSpan interval)
        {
            var overflow = dateTime.Ticks % interval.Ticks;

            return overflow == 0 ? dateTime : dateTime.AddTicks(interval.Ticks - overflow);
        }

        /// <summary>
        /// Round date to interval (TimeSpan)
        /// </summary>
        /// <param name="dateTime"></param>
        /// <param name="interval"></param>
        /// <returns></returns>
        public static DateTime Round(this DateTime dateTime, TimeSpan interval)
        {
            var halfIntervalTicks = (interval.Ticks + 1) >> 1;

            return dateTime.AddTicks(halfIntervalTicks - ((dateTime.Ticks + halfIntervalTicks) % interval.Ticks));
        }
    }
}

And here are a few tests to excercise the above (these could do with some work / clarification)

using System;
using SomeProject.Extensions;
using Xunit;

namespace SomeProject.UnitTests
{
    public class DateTimeExtensionTests
    {
        [Fact]
        public void TestIsEqualToWithinSeconds()
        {
            // Seconds

            var isEqualToWithin1 = new DateTime(2021, 01, 02, 13, 01, 55).IsEqualToWithin(new TimeSpan(0, 0, 1), new DateTime(2021, 01, 02, 13, 01, 56));

            Assert.True(isEqualToWithin1);

            var isEqualToWithin2 = new DateTime(2021, 01, 02, 13, 01, 55).IsEqualToWithin(new TimeSpan(0, 0, 1), new DateTime(2021, 01, 02, 13, 01, 54));

            Assert.True(isEqualToWithin2);

            var isEqualToWithin3 = new DateTime(2021, 01, 02, 13, 01, 50).IsEqualToWithin(new TimeSpan(0, 0, 1), new DateTime(2021, 01, 02, 13, 01, 56));

            Assert.False(isEqualToWithin3);

            var isEqualToWithin4 = new DateTime(2021, 01, 02, 13, 02, 00).IsEqualToWithin(new TimeSpan(0, 0, 1), new DateTime(2021, 01, 02, 13, 01, 54));

            Assert.False(isEqualToWithin4);
        }

        [Fact]
        public void TestIsEqualToWithinYears()
        {
            var isEqualToWithin1 = new DateTime(2022, 01, 02, 13, 01, 55).IsEqualToWithin(new TimeSpan(365, 0, 0, 0), new DateTime(2021, 01, 02, 13, 01, 55));

            Assert.True(isEqualToWithin1);

            var isEqualToWithin2 = new DateTime(2023, 01, 02, 13, 01, 55).IsEqualToWithin(new TimeSpan(365, 0, 0, 0), new DateTime(2021, 01, 02, 13, 01, 55));

            Assert.False(isEqualToWithin2);
        }

        [Fact]
        public void TestHourInterval()
        {
            // Basic date

            var testDateTime1 = new DateTime(2021, 01, 02, 13, 01, 55);

            Assert.True(testDateTime1.Floor(new TimeSpan(0, 1, 0, 0)).Equals(new DateTime(2021, 01, 02, 13, 00, 00))); // Floor on hour

            // Test with timezones specified makes no difference

            var testDateTime2 = new DateTime(2021, 01, 02, 13, 01, 55, DateTimeKind.Utc);

            Assert.True(testDateTime2.Floor(new TimeSpan(0, 1, 0, 0)).Equals(new DateTime(2021, 01, 02, 13, 00, 00))); // Floor on hour

            var testDateTime3 = new DateTime(2021, 01, 02, 13, 01, 55, DateTimeKind.Local);

            Assert.True(testDateTime3.Floor(new TimeSpan(0, 1, 0, 0)).Equals(new DateTime(2021, 01, 02, 13, 00, 00))); // Floor on hour

            // Test with timezones in BST specified makes no difference

            var testDateTime4 = new DateTime(2021, 09, 02, 13, 01, 55, DateTimeKind.Utc);

            Assert.True(testDateTime4.Floor(new TimeSpan(0, 1, 0, 0)).Equals(new DateTime(2021, 09, 02, 13, 00, 00))); // Floor on hour

            var testDateTime5 = new DateTime(2021, 09, 02, 13, 01, 55, DateTimeKind.Local);

            Assert.True(testDateTime5.Floor(new TimeSpan(0, 1, 0, 0)).Equals(new DateTime(2021, 09, 02, 13, 00, 00))); // Floor on hour

            // Test with timezones in BST specified makes no difference

            var testDateTime6 = new DateTime(2021, 09, 02, 13, 01, 55, DateTimeKind.Utc);

            Assert.True(testDateTime6.Ceiling(new TimeSpan(0, 1, 0, 0)).Equals(new DateTime(2021, 09, 02, 14, 00, 00))); // Ceiling on hour

            var testDateTime7 = new DateTime(2021, 09, 02, 13, 01, 55, DateTimeKind.Local);

            Assert.True(testDateTime7.Ceiling(new TimeSpan(0, 1, 0, 0)).Equals(new DateTime(2021, 09, 02, 14, 00, 00))); // Ceiling on hour
        }

        [Fact]
        public void TestDayInterval()
        {
            // Basic date

            var testDateTime1 = new DateTime(2021, 01, 02, 13, 01, 55);

            Assert.True(testDateTime1.Floor(new TimeSpan(1, 0, 0, 0)).Equals(new DateTime(2021, 01, 02, 00, 00, 00))); // Floor on hour

            // Test with timezones specified makes no difference

            var testDateTime2 = new DateTime(2021, 01, 02, 13, 01, 55, DateTimeKind.Utc);

            Assert.True(testDateTime2.Floor(new TimeSpan(1, 0, 0, 0)).Equals(new DateTime(2021, 01, 02, 00, 00, 00))); // Floor on hour

            var testDateTime3 = new DateTime(2021, 01, 02, 13, 01, 55, DateTimeKind.Local);

            Assert.True(testDateTime3.Floor(new TimeSpan(1, 0, 0, 0)).Equals(new DateTime(2021, 01, 02, 00, 00, 00))); // Floor on hour

            // Test with timezones in BST specified makes no difference

            var testDateTime4 = new DateTime(2021, 09, 02, 13, 01, 55, DateTimeKind.Utc);

            Assert.True(testDateTime4.Floor(new TimeSpan(1, 0, 0, 0)).Equals(new DateTime(2021, 09, 02, 00, 00, 00))); // Floor on hour

            var testDateTime5 = new DateTime(2021, 09, 02, 13, 01, 55, DateTimeKind.Local);

            Assert.True(testDateTime5.Floor(new TimeSpan(1, 0, 0, 0)).Equals(new DateTime(2021, 09, 02, 00, 00, 00))); // Floor on hour

            // Test with timezones in BST specified makes no difference

            var testDateTime6 = new DateTime(2021, 09, 02, 13, 01, 55, DateTimeKind.Utc);

            Assert.True(testDateTime6.Ceiling(new TimeSpan(1, 0, 0, 0)).Equals(new DateTime(2021, 09, 03, 00, 00, 00))); // Ceiling on hour

            var testDateTime7 = new DateTime(2021, 09, 02, 13, 01, 55, DateTimeKind.Local);

            Assert.True(testDateTime7.Ceiling(new TimeSpan(1, 0, 0, 0)).Equals(new DateTime(2021, 09, 03, 00, 00, 00))); // Ceiling on hour
        }
    }
}

The information on this site is provided “AS IS” and without warranties of any kind either
express or implied. To the fullest extent permissible pursuant to applicable laws, the author disclaims all warranties, express or implied, including, but not limited to, implied warranties of merchantability, non-infringement and suitability for a particular purpose.

UI block loader
One moment please ...