Skip to content

Commit

Permalink
GH-34620: [C#] Support DateOnly and TimeOnly on .NET 6.0+ (#36125)
Browse files Browse the repository at this point in the history
### What changes are included in this PR?

Date32Array and Date64Array now support DateOnly values for construction and reading on .NET 6.0 and later.
Time32Array and Time64Array now support TimeOnly values for construction and reading on .NET 6.0 and later.
A new TimeArrayBuilder type is used to share logic between Time32Array.Builder and Time64Array.Builder just as the DateArrayBuilder does for the date array types.

### Are these changes tested?

Yes
* Closes: #34620

Authored-by: Curt Hagenlocher <curt@hagenlocher.org>
Signed-off-by: Weston Pace <weston.pace@gmail.com>
  • Loading branch information
CurtHagenlocher authored Jul 19, 2023
1 parent be2014a commit bebd2bf
Show file tree
Hide file tree
Showing 13 changed files with 574 additions and 26 deletions.
26 changes: 26 additions & 0 deletions csharp/src/Apache.Arrow/Arrays/Date32Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ namespace Apache.Arrow
public class Date32Array : PrimitiveArray<int>
{
private static readonly DateTime _epochDate = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Unspecified);
#if NET6_0_OR_GREATER
private static readonly int _epochDayNumber = new DateOnly(1970, 1, 1).DayNumber;
#endif

/// <summary>
/// The <see cref="Builder"/> class can be used to fluently build <see cref="Date32Array"/> objects.
Expand Down Expand Up @@ -57,6 +60,13 @@ protected override int Convert(DateTimeOffset dateTimeOffset)
// DateTimeOffset.Date property.
return (int)(dateTimeOffset.UtcDateTime.Date - _epochDate).TotalDays;
}

#if NET6_0_OR_GREATER
protected override int Convert(DateOnly date)
{
return (int)(date.DayNumber - _epochDayNumber);
}
#endif
}

public Date32Array(
Expand Down Expand Up @@ -108,5 +118,21 @@ public Date32Array(ArrayData data)
? new DateTimeOffset(_epochDate.AddDays(value.Value), TimeSpan.Zero)
: default(DateTimeOffset?);
}

#if NET6_0_OR_GREATER
/// <summary>
/// Get the date at the specified index
/// </summary>
/// <param name="index">Index at which to get the date.</param>
/// <returns>Returns a <see cref="DateOnly" />, or <c>null</c> if there is no object at that index.
/// </returns>
public DateOnly? GetDateOnly(int index)
{
int? value = GetValue(index);
return value.HasValue
? DateOnly.FromDayNumber(_epochDayNumber + value.Value)
: default(DateOnly?);
}
#endif
}
}
23 changes: 23 additions & 0 deletions csharp/src/Apache.Arrow/Arrays/Date64Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ protected override long Convert(DateTimeOffset dateTimeOffset)
long days = millis / MillisecondsPerDay;
return (millis < 0 ? days - 1 : days) * MillisecondsPerDay;
}

#if NET6_0_OR_GREATER
protected override long Convert(DateOnly date)
{
return ((long)date.DayNumber - _epochDayNumber) * MillisecondsPerDay;
}
#endif
}

public Date64Array(ArrayData data)
Expand Down Expand Up @@ -113,5 +120,21 @@ public Date64Array(ArrayData data)
? DateTimeOffset.FromUnixTimeMilliseconds(value.Value)
: default(DateTimeOffset?);
}

#if NET6_0_OR_GREATER
/// <summary>
/// Get the date at the specified index
/// </summary>
/// <param name="index">Index at which to get the date.</param>
/// <returns>Returns a <see cref="DateOnly" />, or <c>null</c> if there is no object at that index.
/// </returns>
public DateOnly? GetDateOnly(int index)
{
long? value = GetValue(index);
return value.HasValue
? DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeMilliseconds(value.Value).UtcDateTime)
: default(DateOnly?);
}
#endif
}
}
70 changes: 70 additions & 0 deletions csharp/src/Apache.Arrow/Arrays/DateArrayBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,16 @@ public abstract class DateArrayBuilder<TUnderlying, TArray, TBuilder> :
DelegatingArrayBuilder<TUnderlying, TArray, TBuilder>,
IArrowArrayBuilder<DateTime, TArray, TBuilder>,
IArrowArrayBuilder<DateTimeOffset, TArray, TBuilder>
#if NET6_0_OR_GREATER
, IArrowArrayBuilder<DateOnly, TArray, TBuilder>
#endif
where TArray : IArrowArray
where TBuilder : class, IArrowArrayBuilder<TArray>
{
#if NET6_0_OR_GREATER
protected static readonly long _epochDayNumber = new DateOnly(1970, 1, 1).DayNumber;
#endif

/// <summary>
/// Construct a new instance of the <see cref="DateArrayBuilder{TUnderlying,TArray,TBuilder}"/> class.
/// </summary>
Expand Down Expand Up @@ -72,6 +79,20 @@ public TBuilder Append(DateTimeOffset value)
return this as TBuilder;
}

#if NET6_0_OR_GREATER
/// <summary>
/// Append a date from a <see cref="DateOnly"/> object to the array.
/// </summary>
/// </remarks>
/// <param name="value">Date to add.</param>
/// <returns>Returns the builder (for fluent-style composition).</returns>
public TBuilder Append(DateOnly value)
{
InnerBuilder.Append(Convert(value));
return this as TBuilder;
}
#endif

/// <summary>
/// Append a span of dates in the form of <see cref="DateTime"/> objects to the array.
/// </summary>
Expand Down Expand Up @@ -114,6 +135,24 @@ public TBuilder Append(ReadOnlySpan<DateTimeOffset> span)
return this as TBuilder;
}

#if NET6_0_OR_GREATER
/// <summary>
/// Append a span of dates in the form of <see cref="DateOnly"/> objects to the array.
/// </summary>
/// <param name="span">Span of dates to add.</param>
/// <returns>Returns the builder (for fluent-style composition).</returns>
public TBuilder Append(ReadOnlySpan<DateOnly> span)
{
InnerBuilder.Reserve(span.Length);
foreach (var item in span)
{
InnerBuilder.Append(Convert(item));
}

return this as TBuilder;
}
#endif

/// <summary>
/// Append a null date to the array.
/// </summary>
Expand Down Expand Up @@ -156,6 +195,19 @@ public TBuilder AppendRange(IEnumerable<DateTimeOffset> values)
return this as TBuilder;
}

#if NET6_0_OR_GREATER
/// <summary>
/// Append a collection of dates in the form of <see cref="DateOnly"/> objects to the array.
/// </summary>
/// <param name="values">Collection of dates to add.</param>
/// <returns>Returns the builder (for fluent-style composition).</returns>
public TBuilder AppendRange(IEnumerable<DateOnly> values)
{
InnerBuilder.AppendRange(values.Select(Convert));
return this as TBuilder;
}
#endif

/// <summary>
/// Set the value of a date in the form of a <see cref="DateTime"/> object at the specified index.
/// </summary>
Expand Down Expand Up @@ -190,6 +242,20 @@ public TBuilder Set(int index, DateTimeOffset value)
return this as TBuilder;
}

#if NET6_0_OR_GREATER
/// <summary>
/// Set the value of a date in the form of a <see cref="DateOnly"/> object at the specified index.
/// </summary>
/// <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, DateOnly value)
{
InnerBuilder.Set(index, Convert(value));
return this as TBuilder;
}
#endif

/// <summary>
/// Swap the values of the dates at the specified indices.
/// </summary>
Expand All @@ -205,5 +271,9 @@ public TBuilder Swap(int i, int j)
protected abstract TUnderlying Convert(DateTime dateTime);

protected abstract TUnderlying Convert(DateTimeOffset dateTimeOffset);

#if NET6_0_OR_GREATER
protected abstract TUnderlying Convert(DateOnly date);
#endif
}
}
61 changes: 52 additions & 9 deletions csharp/src/Apache.Arrow/Arrays/Time32Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.

using Apache.Arrow.Types;
using System;
using System.IO;

namespace Apache.Arrow
Expand All @@ -27,14 +28,19 @@ public class Time32Array : PrimitiveArray<int>
/// <summary>
/// The <see cref="Builder"/> class can be used to fluently build <see cref="Time32Array"/> objects.
/// </summary>
public class Builder : PrimitiveArrayBuilder<int, Time32Array, Builder>
public class Builder : TimeArrayBuilder<int, Time32Array, Builder>
{
protected override Time32Array Build(
ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
int length, int nullCount, int offset) =>
new Time32Array(DataType, valueBuffer, nullBitmapBuffer, length, nullCount, offset);

protected Time32Type DataType { get; }
private class TimeBuilder : PrimitiveArrayBuilder<int, Time32Array, TimeBuilder>
{
public Time32Type DataType { get; }

public TimeBuilder(Time32Type dataType) => DataType = dataType;

protected override Time32Array Build(
ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
int length, int nullCount, int offset) =>
new Time32Array(DataType, valueBuffer, nullBitmapBuffer, length, nullCount, offset);
}

public Builder()
: this(Time32Type.Default) { }
Expand All @@ -46,10 +52,22 @@ public Builder(TimeUnit unit)
/// Construct a new instance of the <see cref="Builder"/> class.
/// </summary>
public Builder(Time32Type type)
: base()
: base(new TimeBuilder(type))
{
}

#if NET6_0_OR_GREATER
protected override int Convert(TimeOnly time)
{
DataType = type;
var unit = ((TimeBuilder)InnerBuilder).DataType.Unit;
return unit switch
{
TimeUnit.Second => (int)(time.Ticks / TimeSpan.TicksPerSecond),
TimeUnit.Millisecond => (int)(time.Ticks / TimeSpan.TicksPerMillisecond),
_ => throw new InvalidDataException($"Unsupported time unit for Time32Type: {unit}")
};
}
#endif
}

public Time32Array(
Expand Down Expand Up @@ -113,5 +131,30 @@ public Time32Array(ArrayData data)
_ => throw new InvalidDataException($"Unsupported time unit for Time32Type: {unit}")
};
}

#if NET6_0_OR_GREATER
/// <summary>
/// Get the time at the specified index as <see cref="TimeOnly"/>
/// </summary>
/// <param name="index">Index at which to get the time.</param>
/// <returns>Returns a <see cref="TimeOnly" />, or <c>null</c> if there is no object at that index.
/// </returns>
public TimeOnly? GetTime(int index)
{
int? value = GetValue(index);
if (value == null)
{
return null;
}

var unit = ((Time32Type)Data.DataType).Unit;
return unit switch
{
TimeUnit.Second => new TimeOnly(value.Value * TimeSpan.TicksPerSecond),
TimeUnit.Millisecond => new TimeOnly(value.Value * TimeSpan.TicksPerMillisecond),
_ => throw new InvalidDataException($"Unsupported time unit for Time32Type: {unit}")
};
}
#endif
}
}
65 changes: 57 additions & 8 deletions csharp/src/Apache.Arrow/Arrays/Time64Array.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// limitations under the License.

using Apache.Arrow.Types;
using System;
using System.IO;

namespace Apache.Arrow
Expand All @@ -24,17 +25,25 @@ namespace Apache.Arrow
/// </summary>
public class Time64Array : PrimitiveArray<long>
{
private const long TicksPerMicrosecond = 10;
private const long NanosecondsPerTick = 100;

/// <summary>
/// The <see cref="Builder"/> class can be used to fluently build <see cref="Time64Array"/> objects.
/// </summary>
public class Builder : PrimitiveArrayBuilder<long, Time64Array, Builder>
public class Builder : TimeArrayBuilder<long, Time64Array, Builder>
{
protected override Time64Array Build(
ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
int length, int nullCount, int offset) =>
new Time64Array(DataType, valueBuffer, nullBitmapBuffer, length, nullCount, offset);
private class TimeBuilder : PrimitiveArrayBuilder<long, Time64Array, TimeBuilder>
{
public Time64Type DataType { get; }

protected Time64Type DataType { get; }
public TimeBuilder(Time64Type dataType) => DataType = dataType;

protected override Time64Array Build(
ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
int length, int nullCount, int offset) =>
new Time64Array(DataType, valueBuffer, nullBitmapBuffer, length, nullCount, offset);
}

public Builder()
: this(Time64Type.Default) { }
Expand All @@ -46,10 +55,22 @@ public Builder(TimeUnit unit)
/// Construct a new instance of the <see cref="Builder"/> class.
/// </summary>
public Builder(Time64Type type)
: base()
: base(new TimeBuilder(type))
{
}

#if NET6_0_OR_GREATER
protected override long Convert(TimeOnly time)
{
DataType = type;
var unit = ((TimeBuilder)InnerBuilder).DataType.Unit;
return unit switch
{
TimeUnit.Microsecond => (long)(time.Ticks / TicksPerMicrosecond),
TimeUnit.Nanosecond => (long)(time.Ticks * NanosecondsPerTick),
_ => throw new InvalidDataException($"Unsupported time unit for Time32Type: {unit}")
};
}
#endif
}

public Time64Array(
Expand Down Expand Up @@ -113,5 +134,33 @@ public Time64Array(ArrayData data)
_ => throw new InvalidDataException($"Unsupported time unit for Time64Type: {unit}")
};
}

#if NET6_0_OR_GREATER
/// <summary>
/// Get the time at the specified index as <see cref="TimeOnly"/>
/// </summary>
/// <remarks>
/// This may cause truncation of nanosecond values, as the resolution of TimeOnly is in 100-ns increments.
/// </remarks>
/// <param name="index">Index at which to get the time.</param>
/// <returns>Returns a <see cref="TimeOnly" />, or <c>null</c> if there is no object at that index.
/// </returns>
public TimeOnly? GetTime(int index)
{
long? value = GetValue(index);
if (value == null)
{
return null;
}

var unit = ((Time64Type)Data.DataType).Unit;
return unit switch
{
TimeUnit.Microsecond => new TimeOnly(value.Value * TicksPerMicrosecond),
TimeUnit.Nanosecond => new TimeOnly(value.Value / NanosecondsPerTick),
_ => throw new InvalidDataException($"Unsupported time unit for Time64Type: {unit}")
};
}
#endif
}
}
Loading

0 comments on commit bebd2bf

Please sign in to comment.