You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by ee...@apache.org on 2020/08/14 23:52:46 UTC

[arrow] branch master updated: ARROW-8581: [C#] Accept and return DateTime from DateXXArray

This is an automated email from the ASF dual-hosted git repository.

eerhardt pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow.git


The following commit(s) were added to refs/heads/master by this push:
     new 5677f9e  ARROW-8581: [C#] Accept and return DateTime from DateXXArray
5677f9e is described below

commit 5677f9e7aeb5458d22ea799dc3d0bf23a3a388d6
Author: Adam Szmigin <ad...@jetstoneam.com>
AuthorDate: Fri Aug 14 18:51:56 2020 -0500

    ARROW-8581: [C#] Accept and return DateTime from DateXXArray
    
    This PR introduces additions to the public API for `Date32Array` and `Date64Array` by accepting and returning `System.DateTime` in additio to `System.DateTimeOffset`.
    
    The rationale for making this change is explained in detail on the [JIRA ticket](https://issues.apache.org/jira/browse/ARROW-8581), but briefly: making this change avoids a class of bugs that manifest based on the user's timezone, and where it is very easy to unknowingly fall into a trap.
    
    Note that the array builders no longer derive from `PrimitiveArrayBuilder<TFrom, TTo, ...>`: that base class is designed for conversion from exactly one outward-facing type to the underlying type, but these builders now have two outward-facing types.  As a replacement, I have introduced abstract types `DateArrayBuilder` (specific to dates) and `DelegatingArrayBuilder` (more general-purpose).   See inline comments for further details.
    
    Closes #7654 from mr-smidge/arrow-8581/date-array-builder-type-change
    
    Authored-by: Adam Szmigin <ad...@jetstoneam.com>
    Signed-off-by: Eric Erhardt <er...@microsoft.com>
---
 csharp/src/Apache.Arrow/Arrays/Date32Array.cs      |  66 +++++--
 csharp/src/Apache.Arrow/Arrays/Date64Array.cs      |  77 ++++++--
 csharp/src/Apache.Arrow/Arrays/DateArrayBuilder.cs | 209 +++++++++++++++++++++
 .../Apache.Arrow/Arrays/DelegatingArrayBuilder.cs  | 102 ++++++++++
 csharp/test/Apache.Arrow.Tests/ArrowArrayTests.cs  |   4 +-
 csharp/test/Apache.Arrow.Tests/Date32ArrayTests.cs | 115 ++++++++++--
 csharp/test/Apache.Arrow.Tests/Date64ArrayTests.cs | 133 +++++++++++++
 .../test/Apache.Arrow.Tests/TestDateAndTimeData.cs |  83 ++++++++
 8 files changed, 741 insertions(+), 48 deletions(-)

diff --git a/csharp/src/Apache.Arrow/Arrays/Date32Array.cs b/csharp/src/Apache.Arrow/Arrays/Date32Array.cs
index bb07c9d..35c0065 100644
--- a/csharp/src/Apache.Arrow/Arrays/Date32Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/Date32Array.cs
@@ -18,13 +18,20 @@ using System;
 
 namespace Apache.Arrow
 {
+    /// <summary>
+    /// The <see cref="Date32Array"/> class holds an array of dates in the <c>Date32</c> format, where each date is
+    /// stored as the number of days since the dawn of (UNIX) time.
+    /// </summary>
     public class Date32Array : PrimitiveArray<int>
     {
-        private const long MillisecondsPerDay = 86400000;
+        private static readonly DateTime _epochDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
 
-        public class Builder : PrimitiveArrayBuilder<DateTimeOffset, int, Date32Array, Builder>
+        /// <summary>
+        /// The <see cref="Builder"/> class can be used to fluently build <see cref="Date32Array"/> objects.
+        /// </summary>
+        public class Builder : DateArrayBuilder<int, Date32Array, Builder>
         {
-            internal class DateBuilder : PrimitiveArrayBuilder<int, Date32Array, DateBuilder>
+            private class DateBuilder : PrimitiveArrayBuilder<int, Date32Array, DateBuilder>
             {
                 protected override Date32Array Build(
                     ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
@@ -32,11 +39,23 @@ namespace Apache.Arrow
                     new Date32Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
             }
 
+            /// <summary>
+            /// Construct a new instance of the <see cref="Builder"/> class.
+            /// </summary>
             public Builder() : base(new DateBuilder()) { }
 
-            protected override int ConvertTo(DateTimeOffset value)
+            protected override int Convert(DateTime dateTime)
             {
-                return (int)(value.ToUnixTimeMilliseconds() / MillisecondsPerDay);
+                return (int)(dateTime.Date - _epochDate).TotalDays;
+            }
+
+            protected override int Convert(DateTimeOffset dateTimeOffset)
+            {
+                // The internal value stored for a DateTimeOffset can be thought of as the number of 24-hour "blocks"
+                // of time that have elapsed since the UNIX epoch.  This is the same as converting it to UTC first and
+                // then taking the date element from that.  It is not the same as what would result from looking at the
+                // DateTimeOffset.Date property.
+                return (int)(dateTimeOffset.UtcDateTime.Date - _epochDate).TotalDays;
             }
         }
 
@@ -55,16 +74,39 @@ namespace Apache.Arrow
 
         public override void Accept(IArrowArrayVisitor visitor) => Accept(this, visitor);
 
-        public DateTimeOffset? GetDate(int index)
+        [Obsolete("Use `GetDateTimeOffset()` instead")]
+        public DateTimeOffset? GetDate(int index) => GetDateTimeOffset(index);
+
+        /// <summary>
+        /// Get the date at the specified index in the form of a <see cref="DateTime"/> object.
+        /// </summary>
+        /// <remarks>
+        /// The <see cref="DateTime.Kind"/> property of the returned object is set to
+        /// <see cref="DateTimeKind.Unspecified"/>.
+        /// </remarks>
+        /// <param name="index">Index at which to get the date.</param>
+        /// <returns>Returns a <see cref="DateTime"/> object, or <c>null</c> if there is no object at that index.
+        /// </returns>
+        public DateTime? GetDateTime(int index)
         {
             int? value = GetValue(index);
+            return value.HasValue
+                ? _epochDate.AddDays(value.Value)
+                : default(DateTime?);
+        }
 
-            if (!value.HasValue)
-            {
-                return default;
-            }
-
-            return DateTimeOffset.FromUnixTimeMilliseconds(value.Value * MillisecondsPerDay);
+        /// <summary>
+        /// Get the date at the specified index in the form of a <see cref="DateTimeOffset"/> object.
+        /// </summary>
+        /// <param name="index">Index at which to get the date.</param>
+        /// <returns>Returns a <see cref="DateTimeOffset"/> object, or <c>null</c> if there is no object at that index.
+        /// </returns>
+        public DateTimeOffset? GetDateTimeOffset(int index)
+        {
+            int? value = GetValue(index);
+            return value.HasValue
+                ? new DateTimeOffset(_epochDate.AddDays(value.Value), TimeSpan.Zero)
+                : default(DateTimeOffset?);
         }
     }
 }
diff --git a/csharp/src/Apache.Arrow/Arrays/Date64Array.cs b/csharp/src/Apache.Arrow/Arrays/Date64Array.cs
index 763986d..cf977b2 100644
--- a/csharp/src/Apache.Arrow/Arrays/Date64Array.cs
+++ b/csharp/src/Apache.Arrow/Arrays/Date64Array.cs
@@ -15,13 +15,18 @@
 
 using Apache.Arrow.Types;
 using System;
-using System.Collections.Generic;
-using Apache.Arrow.Memory;
 
 namespace Apache.Arrow
 {
+    /// <summary>
+    /// The <see cref="Date64Array"/> class holds an array of dates in the <c>Date64</c> format, where each date is
+    /// stored as the number of milliseconds since the dawn of (UNIX) time, excluding leap seconds, in multiples of
+    /// 86400000.
+    /// </summary>
     public class Date64Array: PrimitiveArray<long>
     {
+        private const long MillisecondsPerDay = 86400000;
+
         public Date64Array(
             ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
             int length, int nullCount, int offset)
@@ -29,25 +34,44 @@ namespace Apache.Arrow
                 new[] { nullBitmapBuffer, valueBuffer }))
         { }
 
-        public class Builder : PrimitiveArrayBuilder<DateTimeOffset, long, Date64Array, Builder>
+        /// <summary>
+        /// The <see cref="Builder"/> class can be used to fluently build <see cref="Date64Array"/> objects.
+        /// </summary>
+        public class Builder : DateArrayBuilder<long, Date64Array, Builder>
         {
-            internal class DateBuilder: PrimitiveArrayBuilder<long, Date64Array, DateBuilder>
+            private class DateBuilder: PrimitiveArrayBuilder<long, Date64Array, DateBuilder>
             {
                 protected override Date64Array Build(
                     ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
                     int length, int nullCount, int offset) =>
                     new Date64Array(valueBuffer, nullBitmapBuffer, length, nullCount, offset);
-            } 
+            }
 
+            /// <summary>
+            /// Construct a new instance of the <see cref="Builder"/> class.
+            /// </summary>
             public Builder() : base(new DateBuilder()) { }
 
-            protected override long ConvertTo(DateTimeOffset value)
+            protected override long Convert(DateTime dateTime)
+            {
+                var dateTimeOffset = new DateTimeOffset(
+                    DateTime.SpecifyKind(dateTime.Date, DateTimeKind.Unspecified),
+                    TimeSpan.Zero);
+                return dateTimeOffset.ToUnixTimeMilliseconds();
+            }
+
+            protected override long Convert(DateTimeOffset dateTimeOffset)
             {
-                return value.ToUnixTimeMilliseconds();
+                // The internal value stored for a DateTimeOffset can be thought of as the number of milliseconds,
+                // in multiples of 86400000, that have passed since the UNIX epoch.  It is not the same as what would
+                // result from encoding the date from the DateTimeOffset.Date property.
+                long millis = dateTimeOffset.ToUnixTimeMilliseconds();
+                long days = millis / MillisecondsPerDay;
+                return (millis < 0 ? days - 1 : days) * MillisecondsPerDay;
             }
         }
 
-        public Date64Array(ArrayData data) 
+        public Date64Array(ArrayData data)
             : base(data)
         {
             data.EnsureDataType(ArrowTypeId.Date64);
@@ -55,16 +79,39 @@ namespace Apache.Arrow
 
         public override void Accept(IArrowArrayVisitor visitor) => Accept(this, visitor);
 
-        public DateTimeOffset? GetDate(int index)
+        [Obsolete("Use `GetDateTimeOffset()` instead")]
+        public DateTimeOffset? GetDate(int index) => GetDateTimeOffset(index);
+
+        /// <summary>
+        /// Get the date at the specified index in the form of a <see cref="DateTime"/> object.
+        /// </summary>
+        /// <remarks>
+        /// The <see cref="DateTime.Kind"/> property of the returned object is set to
+        /// <see cref="DateTimeKind.Unspecified"/>.
+        /// </remarks>
+        /// <param name="index">Index at which to get the date.</param>
+        /// <returns>Returns a <see cref="DateTime"/> object, or <c>null</c> if there is no object at that index.
+        /// </returns>
+        public DateTime? GetDateTime(int index)
         {
             long? value = GetValue(index);
+            return value.HasValue
+                ? DateTimeOffset.FromUnixTimeMilliseconds(value.Value).Date
+                : default(DateTime?);
+        }
 
-            if (!value.HasValue)
-            {
-                return default;
-            }
-
-            return DateTimeOffset.FromUnixTimeMilliseconds(value.Value);
+        /// <summary>
+        /// Get the date at the specified index in the form of a <see cref="DateTimeOffset"/> object.
+        /// </summary>
+        /// <param name="index">Index at which to get the date.</param>
+        /// <returns>Returns a <see cref="DateTimeOffset"/> object, or <c>null</c> if there is no object at that index.
+        /// </returns>
+        public DateTimeOffset? GetDateTimeOffset(int index)
+        {
+            long? value = GetValue(index);
+            return value.HasValue
+                ? DateTimeOffset.FromUnixTimeMilliseconds(value.Value)
+                : default(DateTimeOffset?);
         }
     }
 }
diff --git a/csharp/src/Apache.Arrow/Arrays/DateArrayBuilder.cs b/csharp/src/Apache.Arrow/Arrays/DateArrayBuilder.cs
new file mode 100644
index 0000000..4e69f6f
--- /dev/null
+++ b/csharp/src/Apache.Arrow/Arrays/DateArrayBuilder.cs
@@ -0,0 +1,209 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Apache.Arrow
+{
+    /// <summary>
+    /// The <see cref="DateArrayBuilder{TUnderlying,TArray,TBuilder}"/> class is an abstract array builder that can
+    /// accept dates in the form of <see cref="DateTime"/> or <see cref="DateTimeOffset"/> and convert to some
+    /// underlying date representation.
+    /// </summary>
+    public abstract class DateArrayBuilder<TUnderlying, TArray, TBuilder> :
+        DelegatingArrayBuilder<TUnderlying, TArray, TBuilder>,
+        IArrowArrayBuilder<DateTime, TArray, TBuilder>,
+        IArrowArrayBuilder<DateTimeOffset, TArray, TBuilder>
+        where TArray : IArrowArray
+        where TBuilder : class, IArrowArrayBuilder<TArray>
+    {
+        /// <summary>
+        /// Construct a new instance of the <see cref="DateArrayBuilder{TUnderlying,TArray,TBuilder}"/> class.
+        /// </summary>
+        /// <param name="innerBuilder">Inner builder that will produce arrays of type <typeparamref name="TArray"/>.
+        /// </param>
+        protected DateArrayBuilder(IArrowArrayBuilder<TUnderlying, TArray, IArrowArrayBuilder<TArray>> innerBuilder)
+            : base(innerBuilder)
+        { }
+
+        /// <summary>
+        /// Append a date in the form of a <see cref="DateTime"/> object to the array.
+        /// </summary>
+        /// <remarks>
+        /// The value of <see cref="DateTime.Kind"/> on the input does not have any effect on the behaviour of this
+        /// method.
+        /// </remarks>
+        /// <param name="value">Date to add.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Append(DateTime value)
+        {
+            InnerBuilder.Append(Convert(value));
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Append a date from a <see cref="DateTimeOffset"/> object to the array.
+        /// </summary>
+        /// <remarks>
+        /// Note that to convert the supplied <paramref name="value"/> parameter to a date, it is first converted to
+        /// UTC and the date then taken from the UTC date/time.  Depending on the value of its
+        /// <see cref="DateTimeOffset.Offset"/> property, this may not necessarily be the same as the date obtained by
+        /// calling its <see cref="DateTimeOffset.Date"/> property.
+        /// </remarks>
+        /// <param name="value">Date to add.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Append(DateTimeOffset value)
+        {
+            InnerBuilder.Append(Convert(value));
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Append a span of dates in the form of <see cref="DateTime"/> objects to the array.
+        /// </summary>
+        /// <remarks>
+        /// The value of <see cref="DateTime.Kind"/> on any of the inputs does not have any effect on the behaviour of
+        /// this method.
+        /// </remarks>
+        /// <param name="span">Span of dates to add.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Append(ReadOnlySpan<DateTime> span)
+        {
+            InnerBuilder.Reserve(span.Length);
+            foreach (var item in span)
+            {
+                InnerBuilder.Append(Convert(item));
+            }
+
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Append a span of dates in the form of <see cref="DateTimeOffset"/> objects to the array.
+        /// </summary>
+        /// <remarks>
+        /// Note that to convert the <see cref="DateTimeOffset"/> objects in the <paramref name="span"/> parameter to
+        /// dates, they are first converted to UTC and the date then taken from the UTC date/times.  Depending on the
+        /// value of each <see cref="DateTimeOffset.Offset"/> property, this may not necessarily be the same as the
+        /// date obtained by calling the <see cref="DateTimeOffset.Date"/> property.
+        /// </remarks>
+        /// <param name="span">Span of dates to add.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Append(ReadOnlySpan<DateTimeOffset> span)
+        {
+            InnerBuilder.Reserve(span.Length);
+            foreach (var item in span)
+            {
+                InnerBuilder.Append(Convert(item));
+            }
+
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Append a null date to the array.
+        /// </summary>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder AppendNull()
+        {
+            InnerBuilder.AppendNull();
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Append a collection of dates in the form of <see cref="DateTime"/> objects to the array.
+        /// </summary>
+        /// <remarks>
+        /// The value of <see cref="DateTime.Kind"/> on any of the inputs does not have any effect on the behaviour of
+        /// this method.
+        /// </remarks>
+        /// <param name="values">Collection of dates to add.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder AppendRange(IEnumerable<DateTime> values)
+        {
+            InnerBuilder.AppendRange(values.Select(Convert));
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Append a collection of dates in the form of <see cref="DateTimeOffset"/> objects to the array.
+        /// </summary>
+        /// <remarks>
+        /// Note that to convert the <see cref="DateTimeOffset"/> objects in the <paramref name="values"/> parameter to
+        /// dates, they are first converted to UTC and the date then taken from the UTC date/times.  Depending on the
+        /// value of each <see cref="DateTimeOffset.Offset"/> property, this may not necessarily be the same as the
+        /// date obtained by calling the <see cref="DateTimeOffset.Date"/> property.
+        /// </remarks>
+        /// <param name="values">Collection of dates to add.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder AppendRange(IEnumerable<DateTimeOffset> values)
+        {
+            InnerBuilder.AppendRange(values.Select(Convert));
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Set the value of a date in the form of a <see cref="DateTime"/> object at the specified index.
+        /// </summary>
+        /// <remarks>
+        /// The value of <see cref="DateTime.Kind"/> on the input does not have any effect on the behaviour of this
+        /// method.
+        /// </remarks>
+        /// <param name="index">Index at which to set value.</param>
+        /// <param name="value">Date to set.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Set(int index, DateTime value)
+        {
+            InnerBuilder.Set(index, Convert(value));
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Set the value of a date in the form of a <see cref="DateTimeOffset"/> object at the specified index.
+        /// </summary>
+        /// <remarks>
+        /// Note that to convert the supplied <paramref name="value"/> parameter to a date, it is first converted to
+        /// UTC and the date then taken from the UTC date/time.  Depending on the value of its
+        /// <see cref="DateTimeOffset.Offset"/> property, this may not necessarily be the same as the date obtained by
+        /// calling its <see cref="DateTimeOffset.Date"/> property.
+        /// </remarks>
+        /// <param name="index">Index at which to set value.</param>
+        /// <param name="value">Date to set.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Set(int index, DateTimeOffset value)
+        {
+            InnerBuilder.Set(index, Convert(value));
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Swap the values of the dates at the specified indices.
+        /// </summary>
+        /// <param name="i">First index.</param>
+        /// <param name="j">Second index.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Swap(int i, int j)
+        {
+            InnerBuilder.Swap(i, j);
+            return this as TBuilder;
+        }
+
+        protected abstract TUnderlying Convert(DateTime dateTime);
+
+        protected abstract TUnderlying Convert(DateTimeOffset dateTimeOffset);
+    }
+}
diff --git a/csharp/src/Apache.Arrow/Arrays/DelegatingArrayBuilder.cs b/csharp/src/Apache.Arrow/Arrays/DelegatingArrayBuilder.cs
new file mode 100644
index 0000000..f2ab3ee
--- /dev/null
+++ b/csharp/src/Apache.Arrow/Arrays/DelegatingArrayBuilder.cs
@@ -0,0 +1,102 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using Apache.Arrow.Memory;
+
+namespace Apache.Arrow
+{
+    /// <summary>
+    /// The <see cref="DelegatingArrayBuilder{T,TArray,TBuilder}"/> class can be used as the base for any array builder
+    /// that needs to delegate most of its functionality to an inner array builder.
+    /// </summary>
+    /// <remarks>
+    /// The typical use case is when an array builder may accept a number of different types as input, but which are
+    /// all internally converted to a single type for assembly into an array.
+    /// </remarks>
+    /// <typeparam name="T">Type of item accepted by inner array builder.</typeparam>
+    /// <typeparam name="TArray">Type of array produced by this (and the inner) builder.</typeparam>
+    /// <typeparam name="TBuilder">Type of builder (see Curiously-Recurring Template Pattern).</typeparam>
+    public abstract class DelegatingArrayBuilder<T, TArray, TBuilder> : IArrowArrayBuilder<TArray, TBuilder>
+        where TArray : IArrowArray
+        where TBuilder : class, IArrowArrayBuilder<TArray>
+    {
+        /// <summary>
+        /// Gets the inner array builder.
+        /// </summary>
+        protected IArrowArrayBuilder<T, TArray, IArrowArrayBuilder<TArray>> InnerBuilder { get; }
+
+        /// <summary>
+        /// Gets the number of items added to the array so far.
+        /// </summary>
+        public int Length => InnerBuilder.Length;
+
+        /// <summary>
+        /// Construct a new instance of the <see cref="DelegatingArrayBuilder{T,TArray,TBuilder}"/> class.
+        /// </summary>
+        /// <param name="innerBuilder">Inner array builder.</param>
+        protected DelegatingArrayBuilder(IArrowArrayBuilder<T, TArray, IArrowArrayBuilder<TArray>> innerBuilder)
+        {
+            InnerBuilder = innerBuilder ?? throw new ArgumentNullException(nameof(innerBuilder));
+        }
+
+        /// <summary>
+        /// Build an Arrow Array from the appended contents so far.
+        /// </summary>
+        /// <param name="allocator">Optional memory allocator.</param>
+        /// <returns>Returns the built array.</returns>
+        public TArray Build(MemoryAllocator allocator = default) => InnerBuilder.Build(allocator);
+
+        /// <summary>
+        /// Reserve a given number of items' additional capacity.
+        /// </summary>
+        /// <param name="additionalCapacity">Number of items of required additional capacity.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Reserve(int additionalCapacity)
+        {
+            InnerBuilder.Reserve(additionalCapacity);
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Resize the array to a given size.
+        /// </summary>
+        /// <remarks>
+        /// Note that if the required capacity is larger than the current length of the populated array so far,
+        /// the array's contents in the new, expanded region are undefined.
+        /// </remarks>
+        /// <remarks>
+        /// Note that if the required capacity is smaller than the current length of the populated array so far,
+        /// the array will be truncated and items at the end of the array will be lost.
+        /// </remarks>
+        /// <param name="capacity">Number of items of required capacity.</param>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Resize(int capacity)
+        {
+            InnerBuilder.Resize(capacity);
+            return this as TBuilder;
+        }
+
+        /// <summary>
+        /// Clear all contents appended so far.
+        /// </summary>
+        /// <returns>Returns the builder (for fluent-style composition).</returns>
+        public TBuilder Clear()
+        {
+            InnerBuilder.Clear();
+            return this as TBuilder;
+        }
+    }
+}
diff --git a/csharp/test/Apache.Arrow.Tests/ArrowArrayTests.cs b/csharp/test/Apache.Arrow.Tests/ArrowArrayTests.cs
index 32290fe..18d4056 100644
--- a/csharp/test/Apache.Arrow.Tests/ArrowArrayTests.cs
+++ b/csharp/test/Apache.Arrow.Tests/ArrowArrayTests.cs
@@ -204,7 +204,7 @@ namespace Apache.Arrow.Tests
                 Assert.IsAssignableFrom<Date32Array>(_baseArray);
                 var baseArray = (Date32Array)_baseArray;
 
-                Assert.Equal(baseArray.GetDate(array.Offset), array.GetDate(0));
+                Assert.Equal(baseArray.GetDateTimeOffset(array.Offset), array.GetDateTimeOffset(0));
             }
 
             public void Visit(Date64Array array)
@@ -213,7 +213,7 @@ namespace Apache.Arrow.Tests
                 Assert.IsAssignableFrom<Date64Array>(_baseArray);
                 var baseArray = (Date64Array)_baseArray;
 
-                Assert.Equal(baseArray.GetDate(array.Offset), array.GetDate(0));
+                Assert.Equal(baseArray.GetDateTimeOffset(array.Offset), array.GetDateTimeOffset(0));
             }
 
             public void Visit(FloatArray array) => ValidateArrays(array);
diff --git a/csharp/test/Apache.Arrow.Tests/Date32ArrayTests.cs b/csharp/test/Apache.Arrow.Tests/Date32ArrayTests.cs
index f056b11..0d6aad9 100644
--- a/csharp/test/Apache.Arrow.Tests/Date32ArrayTests.cs
+++ b/csharp/test/Apache.Arrow.Tests/Date32ArrayTests.cs
@@ -14,34 +14,111 @@
 // limitations under the License.
 
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using Xunit;
 
 namespace Apache.Arrow.Tests
 {
     public class Date32ArrayTests
     {
-        public class Set
+        public static IEnumerable<object[]> GetDatesData() =>
+            TestDateAndTimeData.ExampleDates.Select(d => new object[] { d });
+
+        public static IEnumerable<object[]> GetDateTimesData() =>
+            TestDateAndTimeData.ExampleDateTimes.Select(dt => new object[] { dt });
+
+        public static IEnumerable<object[]> GetDateTimeOffsetsData() =>
+            TestDateAndTimeData.ExampleDateTimeOffsets.Select(dto => new object[] { dto });
+
+        public class AppendNull
         {
             [Fact]
-            public void SetAndGet()
+            public void AppendThenGetGivesNull()
+            {
+                // Arrange
+                var builder = new Date32Array.Builder();
+
+                // Act
+                builder = builder.AppendNull();
+
+                // Assert
+                var array = builder.Build();
+                Assert.Equal(1, array.Length);
+                Assert.Null(array.GetDateTime(0));
+                Assert.Null(array.GetDateTimeOffset(0));
+                Assert.Null(array.GetValue(0));
+            }
+        }
+
+        public class AppendDateTime
+        {
+            [Theory]
+            [MemberData(nameof(GetDatesData), MemberType = typeof(Date32ArrayTests))]
+            public void AppendDateGivesSameDate(DateTime date)
+            {
+                // Arrange
+                var builder = new Date32Array.Builder();
+                var expectedDateTime = date;
+                var expectedDateTimeOffset =
+                    new DateTimeOffset(DateTime.SpecifyKind(date, DateTimeKind.Unspecified), TimeSpan.Zero);
+                int expectedValue = (int)date.Subtract(new DateTime(1970, 1, 1)).TotalDays;
+
+                // Act
+                builder = builder.Append(date);
+
+                // Assert
+                var array = builder.Build();
+                Assert.Equal(1, array.Length);
+                Assert.Equal(expectedDateTime, array.GetDateTime(0));
+                Assert.Equal(expectedDateTimeOffset, array.GetDateTimeOffset(0));
+                Assert.Equal(expectedValue, array.GetValue(0));
+            }
+
+            [Theory]
+            [MemberData(nameof(GetDateTimesData), MemberType = typeof(Date32ArrayTests))]
+            public void AppendWithTimeGivesSameWithTimeIgnored(DateTime dateTime)
+            {
+                // Arrange
+                var builder = new Date32Array.Builder();
+                var expectedDateTime = dateTime.Date;
+                var expectedDateTimeOffset =
+                    new DateTimeOffset(DateTime.SpecifyKind(dateTime.Date, DateTimeKind.Unspecified), TimeSpan.Zero);
+                int expectedValue = (int)dateTime.Date.Subtract(new DateTime(1970, 1, 1)).TotalDays;
+
+                // Act
+                builder = builder.Append(dateTime);
+
+                // Assert
+                var array = builder.Build();
+                Assert.Equal(1, array.Length);
+                Assert.Equal(expectedDateTime, array.GetDateTime(0));
+                Assert.Equal(expectedDateTimeOffset, array.GetDateTimeOffset(0));
+                Assert.Equal(expectedValue, array.GetValue(0));
+            }
+        }
+
+        public class AppendDateTimeOffset
+        {
+            [Theory]
+            [MemberData(nameof(GetDateTimeOffsetsData), MemberType = typeof(Date32ArrayTests))]
+            public void AppendGivesUtcDate(DateTimeOffset dateTimeOffset)
             {
-                var now = DateTimeOffset.UtcNow;
-
-                // throw away the time portion of the date time
-                var expected = new DateTime(now.Year,
-					    now.Month,
-					    now.Day,
-					    0,
-					    0,
-					    0,
-					    DateTimeKind.Utc);
-
-                var array = new Date32Array.Builder()
-                    .Resize(1)
-                    .Set(0, expected)
-                    .Build();
-
-                Assert.Equal(expected, array.GetDate(0).Value.Date);
+                // Arrange
+                var builder = new Date32Array.Builder();
+                var expectedDateTime = dateTimeOffset.UtcDateTime.Date;
+                var expectedDateTimeOffset = new DateTimeOffset(dateTimeOffset.UtcDateTime.Date, TimeSpan.Zero);
+                int expectedValue = (int)dateTimeOffset.UtcDateTime.Date.Subtract(new DateTime(1970, 1, 1)).TotalDays;
+
+                // Act
+                builder = builder.Append(dateTimeOffset);
+
+                // Assert
+                var array = builder.Build();
+                Assert.Equal(1, array.Length);
+                Assert.Equal(expectedDateTime, array.GetDateTime(0));
+                Assert.Equal(expectedDateTimeOffset, array.GetDateTimeOffset(0));
+                Assert.Equal(expectedValue, array.GetValue(0));
             }
         }
     }
diff --git a/csharp/test/Apache.Arrow.Tests/Date64ArrayTests.cs b/csharp/test/Apache.Arrow.Tests/Date64ArrayTests.cs
new file mode 100644
index 0000000..65cffc8
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Tests/Date64ArrayTests.cs
@@ -0,0 +1,133 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Xunit;
+
+namespace Apache.Arrow.Tests
+{
+    public class Date64ArrayTests
+    {
+        private const long MillisecondsPerDay = 86400000;
+
+        public static IEnumerable<object[]> GetDatesData() =>
+            TestDateAndTimeData.ExampleDates.Select(d => new object[] { d });
+
+        public static IEnumerable<object[]> GetDateTimesData() =>
+            TestDateAndTimeData.ExampleDateTimes.Select(dt => new object[] { dt });
+
+        public static IEnumerable<object[]> GetDateTimeOffsetsData() =>
+            TestDateAndTimeData.ExampleDateTimeOffsets.Select(dto => new object[] { dto });
+
+        public class AppendNull
+        {
+            [Fact]
+            public void AppendThenGetGivesNull()
+            {
+                // Arrange
+                var builder = new Date64Array.Builder();
+
+                // Act
+                builder = builder.AppendNull();
+
+                // Assert
+                var array = builder.Build();
+                Assert.Equal(1, array.Length);
+                Assert.Null(array.GetDateTime(0));
+                Assert.Null(array.GetDateTimeOffset(0));
+                Assert.Null(array.GetValue(0));
+            }
+        }
+
+        public class AppendDateTime
+        {
+            [Theory]
+            [MemberData(nameof(GetDatesData), MemberType = typeof(Date64ArrayTests))]
+            public void AppendDateGivesSameDate(DateTime date)
+            {
+                // Arrange
+                var builder = new Date64Array.Builder();
+                var expectedDateTime = date;
+                var expectedDateTimeOffset =
+                    new DateTimeOffset(DateTime.SpecifyKind(date, DateTimeKind.Unspecified), TimeSpan.Zero);
+                long expectedValue = (long)date.Subtract(new DateTime(1970, 1, 1)).TotalDays * MillisecondsPerDay;
+
+                // Act
+                builder = builder.Append(date);
+
+                // Assert
+                var array = builder.Build();
+                Assert.Equal(1, array.Length);
+                Assert.Equal(expectedDateTime, array.GetDateTime(0));
+                Assert.Equal(expectedDateTimeOffset, array.GetDateTimeOffset(0));
+                Assert.Equal(expectedValue, array.GetValue(0));
+                Assert.Equal(0, array.GetValue(0).Value % MillisecondsPerDay);
+            }
+
+            [Theory]
+            [MemberData(nameof(GetDateTimesData), MemberType = typeof(Date64ArrayTests))]
+            public void AppendWithTimeGivesSameWithTimeIgnored(DateTime dateTime)
+            {
+                // Arrange
+                var builder = new Date64Array.Builder();
+                var expectedDateTime = dateTime.Date;
+                var expectedDateTimeOffset =
+                    new DateTimeOffset(DateTime.SpecifyKind(dateTime.Date, DateTimeKind.Unspecified), TimeSpan.Zero);
+                long expectedValue =
+                    (long)dateTime.Date.Subtract(new DateTime(1970, 1, 1)).TotalDays * MillisecondsPerDay;
+
+                // Act
+                builder = builder.Append(dateTime);
+
+                // Assert
+                var array = builder.Build();
+                Assert.Equal(1, array.Length);
+                Assert.Equal(expectedDateTime, array.GetDateTime(0));
+                Assert.Equal(expectedDateTimeOffset, array.GetDateTimeOffset(0));
+                Assert.Equal(expectedValue, array.GetValue(0));
+                Assert.Equal(0, array.GetValue(0).Value % MillisecondsPerDay);
+            }
+        }
+
+        public class AppendDateTimeOffset
+        {
+            [Theory]
+            [MemberData(nameof(GetDateTimeOffsetsData), MemberType = typeof(Date64ArrayTests))]
+            public void AppendGivesUtcDate(DateTimeOffset dateTimeOffset)
+            {
+                // Arrange
+                var builder = new Date64Array.Builder();
+                var expectedDateTime = dateTimeOffset.UtcDateTime.Date;
+                var expectedDateTimeOffset = new DateTimeOffset(dateTimeOffset.UtcDateTime.Date, TimeSpan.Zero);
+                long expectedValue =
+                    (long)dateTimeOffset.UtcDateTime.Date.Subtract(new DateTime(1970, 1, 1)).TotalDays *
+                    MillisecondsPerDay;
+
+                // Act
+                builder = builder.Append(dateTimeOffset);
+
+                // Assert
+                var array = builder.Build();
+                Assert.Equal(1, array.Length);
+                Assert.Equal(expectedDateTime, array.GetDateTime(0));
+                Assert.Equal(expectedDateTimeOffset, array.GetDateTimeOffset(0));
+                Assert.Equal(expectedValue, array.GetValue(0));
+                Assert.Equal(0, array.GetValue(0).Value % MillisecondsPerDay);
+            }
+        }
+    }
+}
diff --git a/csharp/test/Apache.Arrow.Tests/TestDateAndTimeData.cs b/csharp/test/Apache.Arrow.Tests/TestDateAndTimeData.cs
new file mode 100644
index 0000000..1f2eae4
--- /dev/null
+++ b/csharp/test/Apache.Arrow.Tests/TestDateAndTimeData.cs
@@ -0,0 +1,83 @@
+// Licensed to the Apache Software Foundation (ASF) under one or more
+// contributor license agreements. See the NOTICE file distributed with
+// this work for additional information regarding copyright ownership.
+// The ASF licenses this file to You under the Apache License, Version 2.0
+// (the "License"); you may not use this file except in compliance with
+// the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Apache.Arrow.Tests
+{
+    /// <summary>
+    /// The <see cref="TestDateAndTimeData"/> class holds example dates and times useful for testing.
+    /// </summary>
+    internal static class TestDateAndTimeData
+    {
+        private static readonly DateTime _earliestDate = new DateTime(1, 1, 1);
+        private static readonly DateTime _latestDate = new DateTime(9999, 12, 31);
+
+        private static readonly DateTime[] _exampleDates =
+        {
+            _earliestDate, new DateTime(1969, 12, 31), new DateTime(1970, 1, 1), new DateTime(1970, 1, 2),
+            new DateTime(1972, 6, 30), new DateTime(2015, 6, 30), new DateTime(2016, 12, 31), new DateTime(2020, 2, 29),
+            new DateTime(2020, 7, 1), _latestDate,
+        };
+
+        private static readonly TimeSpan[] _exampleTimes =
+        {
+            new TimeSpan(0, 0, 1), new TimeSpan(12, 0, 0), new TimeSpan(23, 59, 59),
+        };
+
+        private static readonly DateTimeKind[] _exampleKinds =
+        {
+            DateTimeKind.Local, DateTimeKind.Unspecified, DateTimeKind.Utc,
+        };
+
+        private static readonly TimeSpan[] _exampleOffsets =
+        {
+            TimeSpan.FromHours(-2),
+            TimeSpan.Zero,
+            TimeSpan.FromHours(2),
+        };
+
+        /// <summary>
+        /// Gets a collection of example dates (i.e. with a zero time component), of all different kinds.
+        /// </summary>
+        public static IEnumerable<DateTime> ExampleDates =>
+            from date in _exampleDates
+            from kind in _exampleKinds
+            select DateTime.SpecifyKind(date, kind);
+
+        /// <summary>
+        /// Gets a collection of example date/times, of all different kinds.
+        /// </summary>
+        public static IEnumerable<DateTime> ExampleDateTimes =>
+            from date in _exampleDates
+            from time in _exampleTimes
+            from kind in _exampleKinds
+            select DateTime.SpecifyKind(date.Add(time), kind);
+
+        /// <summary>
+        /// Gets a collection of example date time offsets.
+        /// </summary>
+        /// <returns></returns>
+        public static IEnumerable<DateTimeOffset> ExampleDateTimeOffsets =>
+            from date in _exampleDates
+            from time in _exampleTimes
+            from offset in _exampleOffsets
+            where !(date == _earliestDate && offset.Ticks > 0)
+            where !(date == _latestDate && offset.Ticks < 0)
+            select new DateTimeOffset(date.Add(time), offset);
+    }
+}