diff --git a/csharp/src/Apache.Arrow/Arrays/DurationArray.cs b/csharp/src/Apache.Arrow/Arrays/DurationArray.cs index 2a48768e5aaa3..3649dda50cd97 100644 --- a/csharp/src/Apache.Arrow/Arrays/DurationArray.cs +++ b/csharp/src/Apache.Arrow/Arrays/DurationArray.cs @@ -68,6 +68,17 @@ public DurationArray(ArrayData data) data.EnsureDataType(ArrowTypeId.Duration); } + public DurationType DataType => (DurationType)this.Data.DataType; + + public TimeSpan? GetTimeSpan(int index) + { + if (index < 0 || index >= Length) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + return IsValid(index) ? new TimeSpan(DataType.Unit.ConvertToTicks(Values[index])) : null; + } + public override void Accept(IArrowArrayVisitor visitor) => Accept(this, visitor); } } diff --git a/csharp/src/Apache.Arrow/Types/DurationType.cs b/csharp/src/Apache.Arrow/Types/DurationType.cs index 0e92639499a81..7e937a6e72e0b 100644 --- a/csharp/src/Apache.Arrow/Types/DurationType.cs +++ b/csharp/src/Apache.Arrow/Types/DurationType.cs @@ -15,7 +15,7 @@ namespace Apache.Arrow.Types { - public sealed class DurationType: TimeBasedType + public sealed class DurationType : TimeBasedType { public static readonly DurationType Second = new DurationType(TimeUnit.Second); public static readonly DurationType Millisecond = new DurationType(TimeUnit.Millisecond); diff --git a/csharp/test/Apache.Arrow.IntegrationTest/JsonFile.cs b/csharp/test/Apache.Arrow.IntegrationTest/JsonFile.cs index d06249bef2661..a6316662756a7 100644 --- a/csharp/test/Apache.Arrow.IntegrationTest/JsonFile.cs +++ b/csharp/test/Apache.Arrow.IntegrationTest/JsonFile.cs @@ -346,6 +346,7 @@ private class ArrayCreator : IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, + IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, @@ -396,6 +397,7 @@ public void Visit(BooleanType type) public void Visit(DoubleType type) => GenerateArray((v, n, c, nc, o) => new DoubleArray(v, n, c, nc, o)); public void Visit(Time32Type type) => GenerateArray((v, n, c, nc, o) => new Time32Array(type, v, n, c, nc, o)); public void Visit(Time64Type type) => GenerateLongArray((v, n, c, nc, o) => new Time64Array(type, v, n, c, nc, o), s => long.Parse(s)); + public void Visit(DurationType type) => GenerateLongArray((v, n, c, nc, o) => new DurationArray(type, v, n, c, nc, o), s => long.Parse(s)); public void Visit(Decimal128Type type) { diff --git a/csharp/test/Apache.Arrow.Tests/ArrowArrayConcatenatorTests.cs b/csharp/test/Apache.Arrow.Tests/ArrowArrayConcatenatorTests.cs index f1dcbb5d37b8f..a4ff898e17eac 100644 --- a/csharp/test/Apache.Arrow.Tests/ArrowArrayConcatenatorTests.cs +++ b/csharp/test/Apache.Arrow.Tests/ArrowArrayConcatenatorTests.cs @@ -136,6 +136,7 @@ private class TestDataGenerator : IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, + IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, @@ -263,6 +264,33 @@ public void Visit(TimestampType type) ExpectedArray = resultBuilder.Build(); } + public void Visit(DurationType type) + { + DurationArray.Builder resultBuilder = new DurationArray.Builder(type).Reserve(_baseDataTotalElementCount); + DateTimeOffset basis = DateTimeOffset.UtcNow; + + for (int i = 0; i < _baseDataListCount; i++) + { + List dataList = _baseData[i]; + DurationArray.Builder builder = new DurationArray.Builder(type).Reserve(dataList.Count); + foreach (int? value in dataList) + { + if (value.HasValue) + { + builder.Append(value.Value); + resultBuilder.Append(value.Value); + } + else + { + builder.AppendNull(); + resultBuilder.AppendNull(); + } + } + TestTargetArrayList.Add(builder.Build()); + } + + ExpectedArray = resultBuilder.Build(); + } public void Visit(BinaryType type) { diff --git a/csharp/test/Apache.Arrow.Tests/CDataInterfacePythonTests.cs b/csharp/test/Apache.Arrow.Tests/CDataInterfacePythonTests.cs index 7aee37b8212c3..4efa94e8c7363 100644 --- a/csharp/test/Apache.Arrow.Tests/CDataInterfacePythonTests.cs +++ b/csharp/test/Apache.Arrow.Tests/CDataInterfacePythonTests.cs @@ -117,6 +117,11 @@ private static Schema GetTestSchema() .Field(f => f.Name("map").DataType(new MapType(StringType.Default, Int32Type.Default)).Nullable(false)) + .Field(f => f.Name("duration_s").DataType(DurationType.Second).Nullable(false)) + .Field(f => f.Name("duration_ms").DataType(DurationType.Millisecond).Nullable(true)) + .Field(f => f.Name("duration_us").DataType(DurationType.Microsecond).Nullable(false)) + .Field(f => f.Name("duration_ns").DataType(DurationType.Nanosecond).Nullable(true)) + // Checking wider characters. .Field(f => f.Name("hello 你好 😄").DataType(BooleanType.Default).Nullable(true)) @@ -182,6 +187,11 @@ private static IEnumerable GetPythonFields() yield return pa.field("map", pa.map_(pa.@string(), pa.int32()), false); + yield return pa.field("duration_s", pa.duration("s"), false); + yield return pa.field("duration_ms", pa.duration("ms"), true); + yield return pa.field("duration_us", pa.duration("us"), false); + yield return pa.field("duration_ns", pa.duration("ns"), true); + yield return pa.field("hello 你好 😄", pa.bool_(), true); } } @@ -520,8 +530,9 @@ public unsafe void ImportRecordBatch() List(0, 0, 1, 2, 4, 10), pa.array(List("one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten")), pa.array(List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10))), + pa.array(List(1234, 2345, 3456, null, 6789), pa.duration("ms")), }), - new[] { "col1", "col2", "col3", "col4", "col5", "col6", "col7", "col8", "col9", "col10" }); + new[] { "col1", "col2", "col3", "col4", "col5", "col6", "col7", "col8", "col9", "col10", "col11" }); dynamic batch = table.to_batches()[0]; @@ -598,6 +609,9 @@ public unsafe void ImportRecordBatch() Assert.Equal(5, col10.Length); Assert.Equal(new int[] { 0, 0, 1, 2, 4, 10}, col10.ValueOffsets.ToArray()); Assert.Equal(new long?[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }, ((Int64Array)col10.Values).ToList().ToArray()); + + DurationArray col11 = (DurationArray)recordBatch.Column("col11"); + Assert.Equal(5, col11.Length); } [SkippableFact] diff --git a/csharp/test/Apache.Arrow.Tests/DurationArrayTests.cs b/csharp/test/Apache.Arrow.Tests/DurationArrayTests.cs new file mode 100644 index 0000000000000..0890d356b8e90 --- /dev/null +++ b/csharp/test/Apache.Arrow.Tests/DurationArrayTests.cs @@ -0,0 +1,134 @@ +// 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 Apache.Arrow.Types; +using Xunit; + +namespace Apache.Arrow.Tests +{ + public class DurationArrayTests + { + private const long TicksPerMicrosecond = 10; + + private static readonly TimeSpan?[] _exampleTimeSpans = + { + null, + TimeSpan.FromDays(10.5), + TimeSpan.FromHours(10.5), + TimeSpan.FromMinutes(10.5), + TimeSpan.FromSeconds(10.5), + TimeSpan.FromMilliseconds(10.5), + TimeSpan.FromTicks(11), + }; + + private static readonly long?[] _exampleDurations = + { + null, + 1, + 1000, + 1000000, + 1000000000, + 1000000000000, + }; + + private static readonly DurationType[] _durationTypes = + { + DurationType.Second, + DurationType.Millisecond, + DurationType.Microsecond, + DurationType.Nanosecond, + }; + + public static IEnumerable GetTimeSpansData() => + from timeSpan in _exampleTimeSpans + from type in _durationTypes + where type.Unit >= RequiredPrecision(timeSpan) + select new object[] { timeSpan, type }; + + public static IEnumerable GetDurationsData() => + from duration in _exampleDurations + from type in _durationTypes + select new object[] { duration, type }; + + static TimeUnit RequiredPrecision(TimeSpan? timeSpan) + { + if (timeSpan == null) { return TimeUnit.Second; } + if ((timeSpan.Value.Ticks % TicksPerMicrosecond) > 0) { return TimeUnit.Nanosecond; } + if (timeSpan.Value.Microseconds > 0) { return TimeUnit.Microsecond; } + if (timeSpan.Value.Milliseconds > 0) { return TimeUnit.Millisecond; } + return TimeUnit.Second; + } + + public class AppendNull + { + [Fact] + public void AppendThenGetGivesNull() + { + // Arrange + var builder = new DurationArray.Builder(DurationType.Millisecond); + + // Act + builder = builder.AppendNull(); + + // Assert + var array = builder.Build(); + Assert.Equal(1, array.Length); + Assert.Null(array.GetValue(0)); + Assert.Null(array.GetTimeSpan(0)); + } + } + + public class AppendTimeSpan + { + [Theory] + [MemberData(nameof(GetTimeSpansData), MemberType = typeof(DurationArrayTests))] + public void AppendTimeSpanGivesSameTimeSpan(TimeSpan? timeSpan, DurationType type) + { + // Arrange + var builder = new DurationArray.Builder(type); + + // Act + builder = builder.Append(timeSpan); + + // Assert + var array = builder.Build(); + Assert.Equal(1, array.Length); + Assert.Equal(timeSpan, array.GetTimeSpan(0)); + } + } + + public class AppendDuration + { + [Theory] + [MemberData(nameof(GetDurationsData), MemberType = typeof(DurationArrayTests))] + public void AppendDurationGivesSameDuration(long? duration, DurationType type) + { + // Arrange + var builder = new DurationArray.Builder(type); + + // Act + builder = builder.Append(duration); + + // Assert + var array = builder.Build(); + Assert.Equal(1, array.Length); + Assert.Equal(duration, array.GetValue(0)); + } + } + } +} diff --git a/csharp/test/Apache.Arrow.Tests/TestData.cs b/csharp/test/Apache.Arrow.Tests/TestData.cs index e3a40dbdafd61..3af6efb97b437 100644 --- a/csharp/test/Apache.Arrow.Tests/TestData.cs +++ b/csharp/test/Apache.Arrow.Tests/TestData.cs @@ -113,6 +113,7 @@ private class ArrayCreator : IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, + IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, IArrowTypeVisitor, @@ -233,6 +234,18 @@ public void Visit(Time64Type type) Array = builder.Build(); } + public void Visit(DurationType type) + { + var builder = new DurationArray.Builder(type).Reserve(Length); + + for (var i = 0; i < Length; i++) + { + builder.Append(i); + } + + Array = builder.Build(); + } + public void Visit(TimestampType type) { var builder = new TimestampArray.Builder().Reserve(Length); diff --git a/docs/source/status.rst b/docs/source/status.rst index 6024c1d3172bb..c8c0e6dfc1dfe 100644 --- a/docs/source/status.rst +++ b/docs/source/status.rst @@ -54,7 +54,7 @@ Data Types +-------------------+-------+-------+-------+------------+-------+-------+-------+-------+ | Timestamp | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | +-------------------+-------+-------+-------+------------+-------+-------+-------+-------+ -| Duration | ✓ | ✓ | ✓ | ✓ | | ✓ | ✓ | | +| Duration | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | | +-------------------+-------+-------+-------+------------+-------+-------+-------+-------+ | Interval | ✓ | ✓ | ✓ | | | ✓ | ✓ | | +-------------------+-------+-------+-------+------------+-------+-------+-------+-------+