You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@avro.apache.org by fo...@apache.org on 2020/02/05 09:53:18 UTC
[avro] branch branch-1.9 updated: AVRO-2359: Support Logical Types
in C# (#492)
This is an automated email from the ASF dual-hosted git repository.
fokko pushed a commit to branch branch-1.9
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/branch-1.9 by this push:
new d43c0a4 AVRO-2359: Support Logical Types in C# (#492)
d43c0a4 is described below
commit d43c0a4d7ca2f8c22e9292c003ca91261aa577d2
Author: Tim Roberts <ti...@timjroberts.com>
AuthorDate: Wed Feb 5 09:51:28 2020 +0000
AVRO-2359: Support Logical Types in C# (#492)
* AVRO-2359: Support Logical Types in CSharp
* fix more warnings
* remove timezone change in TestTimestamp tests
* update logical Date and Time tests to be local time zone aware
* ensure TimeZone is set relative to a source TimeZone
* reparse Dates assuming local if they're not UTC
* try setting specific TimeZone offset
* actions for code review comments
* seperate the Timestamp* tests
* add missing license to AvroDecimalTests
* modify LogicalType to return .NET type rather than string
* add Register method to LogicalTypeFactory
* Fixup code-review comments
* Make Sign internal and add xmldocs
Co-authored-by: Fokko Driesprong <fo...@driesprong.frl>
---
.gitignore | 3 +-
lang/csharp/.gitignore | 3 +-
lang/csharp/src/apache/main/AvroDecimal.cs | 786 +++++++++++++++++++++
lang/csharp/src/apache/main/CodeGen/CodeGen.cs | 8 +
.../src/apache/main/Generic/GenericDatumWriter.cs | 2 +
.../src/apache/main/Generic/GenericReader.cs | 21 +
.../src/apache/main/Generic/GenericWriter.cs | 17 +
.../apache/main/Generic/PreresolvingDatumReader.cs | 11 +
.../apache/main/Generic/PreresolvingDatumWriter.cs | 13 +
.../csharp/src/apache/main/Schema/LogicalSchema.cs | 118 ++++
lang/csharp/src/apache/main/Schema/Property.cs | 2 +-
lang/csharp/src/apache/main/Schema/Schema.cs | 11 +-
.../apache/main/Specific/SpecificDatumWriter.cs | 2 +
.../src/apache/main/Specific/SpecificWriter.cs | 2 +
lang/csharp/src/apache/main/Util/Date.cs | 61 ++
lang/csharp/src/apache/main/Util/Decimal.cs | 131 ++++
lang/csharp/src/apache/main/Util/LogicalType.cs | 77 ++
.../src/apache/main/Util/LogicalTypeFactory.cs | 85 +++
.../src/apache/main/Util/LogicalUnixEpochType.cs | 54 ++
.../csharp/src/apache/main/Util/TimeMicrosecond.cs | 66 ++
.../csharp/src/apache/main/Util/TimeMillisecond.cs | 66 ++
.../src/apache/main/Util/TimestampMicrosecond.cs | 60 ++
.../src/apache/main/Util/TimestampMillisecond.cs | 59 ++
.../csharp/src/apache/test/Generic/GenericTests.cs | 44 ++
lang/csharp/src/apache/test/Schema/SchemaTests.cs | 21 +
.../src/apache/test/Util/LogicalTypeTests.cs | 182 +++++
26 files changed, 1901 insertions(+), 4 deletions(-)
diff --git a/.gitignore b/.gitignore
index 4bb7412..916e51a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,4 +18,5 @@ test-output
/lang/java/compiler/nbproject/
**/.vscode/**/*
.factorypath
-
+.vs/
+.DS_Store
diff --git a/lang/csharp/.gitignore b/lang/csharp/.gitignore
index eed99c6..8030457 100644
--- a/lang/csharp/.gitignore
+++ b/lang/csharp/.gitignore
@@ -17,7 +17,6 @@
/*.user
/*.suo
/_ReSharper.Avro
-/.vs
obj/
## Ignore Visual Studio temporary files, build results, and
@@ -53,3 +52,5 @@ obj/
#Test results
TestResult.xml
+
+.vs/
diff --git a/lang/csharp/src/apache/main/AvroDecimal.cs b/lang/csharp/src/apache/main/AvroDecimal.cs
new file mode 100644
index 0000000..0ab0f05
--- /dev/null
+++ b/lang/csharp/src/apache/main/AvroDecimal.cs
@@ -0,0 +1,786 @@
+/*
+ * 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
+ *
+ * https://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.Globalization;
+using System.Numerics;
+
+namespace Avro
+{
+ /// <summary>
+ /// Represents a big decimal.
+ /// </summary>
+ #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+ #pragma warning disable CA2225 // Operator overloads have named alternates
+ public struct AvroDecimal : IConvertible, IFormattable, IComparable, IComparable<AvroDecimal>, IEquatable<AvroDecimal>
+ {
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given double.
+ /// </summary>
+ /// <param name="value">The double value.</param>
+ public AvroDecimal(double value)
+ : this((decimal)value)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given float.
+ /// </summary>
+ /// <param name="value">The float value.</param>
+ public AvroDecimal(float value)
+ : this((decimal)value)
+ {
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given decimal.
+ /// </summary>
+ /// <param name="value">The decimal value.</param>
+ public AvroDecimal(decimal value)
+ {
+ var bytes = GetBytesFromDecimal(value);
+
+ var unscaledValueBytes = new byte[12];
+ Array.Copy(bytes, unscaledValueBytes, unscaledValueBytes.Length);
+
+ var unscaledValue = new BigInteger(unscaledValueBytes);
+ var scale = bytes[14];
+
+ if (bytes[15] == 128)
+ unscaledValue *= BigInteger.MinusOne;
+
+ UnscaledValue = unscaledValue;
+ Scale = scale;
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given int.
+ /// </summary>
+ /// <param name="value">The int value.</param>
+ public AvroDecimal(int value)
+ : this(new BigInteger(value), 0) { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given long.
+ /// </summary>
+ /// <param name="value">The long value.</param>
+ public AvroDecimal(long value)
+ : this(new BigInteger(value), 0) { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given unsigned int.
+ /// </summary>
+ /// <param name="value">The unsigned int value.</param>
+ public AvroDecimal(uint value)
+ : this(new BigInteger(value), 0) { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given unsigned long.
+ /// </summary>
+ /// <param name="value">The unsigned long value.</param>
+ public AvroDecimal(ulong value)
+ : this(new BigInteger(value), 0) { }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AvroDecimal"/> class from a given <see cref="BigInteger"/>
+ /// and a scale.
+ /// </summary>
+ /// <param name="unscaledValue">The double value.</param>
+ /// <param name="scale">The scale.</param>
+ public AvroDecimal(BigInteger unscaledValue, int scale)
+ {
+ UnscaledValue = unscaledValue;
+ Scale = scale;
+ }
+
+ /// <summary>
+ /// Gets the unscaled integer value represented by the current <see cref="AvroDecimal"/>.
+ /// </summary>
+ public BigInteger UnscaledValue { get; }
+
+ /// <summary>
+ /// Gets the scale of the current <see cref="AvroDecimal"/>.
+ /// </summary>
+ public int Scale { get; }
+
+ /// <summary>
+ /// Gets the sign of the current <see cref="AvroDecimal"/>.
+ /// </summary>
+ internal int Sign
+ {
+ get { return UnscaledValue.Sign; }
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a string.
+ /// </summary>
+ /// <returns>A string representation of the numeric value.</returns>
+ public override string ToString()
+ {
+ var number = UnscaledValue.ToString(CultureInfo.CurrentCulture);
+
+ if (Scale > 0)
+ return number.Insert(number.Length - Scale, CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator);
+
+ return number;
+ }
+
+ public static bool operator ==(AvroDecimal left, AvroDecimal right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(AvroDecimal left, AvroDecimal right)
+ {
+ return !left.Equals(right);
+ }
+
+ public static bool operator >(AvroDecimal left, AvroDecimal right)
+ {
+ return left.CompareTo(right) > 0;
+ }
+
+ public static bool operator >=(AvroDecimal left, AvroDecimal right)
+ {
+ return left.CompareTo(right) >= 0;
+ }
+
+ public static bool operator <(AvroDecimal left, AvroDecimal right)
+ {
+ return left.CompareTo(right) < 0;
+ }
+
+ public static bool operator <=(AvroDecimal left, AvroDecimal right)
+ {
+ return left.CompareTo(right) <= 0;
+ }
+
+ public static bool operator ==(AvroDecimal left, decimal right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(AvroDecimal left, decimal right)
+ {
+ return !left.Equals(right);
+ }
+
+ public static bool operator >(AvroDecimal left, decimal right)
+ {
+ return left.CompareTo(right) > 0;
+ }
+
+ public static bool operator >=(AvroDecimal left, decimal right)
+ {
+ return left.CompareTo(right) >= 0;
+ }
+
+ public static bool operator <(AvroDecimal left, decimal right)
+ {
+ return left.CompareTo(right) < 0;
+ }
+
+ public static bool operator <=(AvroDecimal left, decimal right)
+ {
+ return left.CompareTo(right) <= 0;
+ }
+
+ public static bool operator ==(decimal left, AvroDecimal right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(decimal left, AvroDecimal right)
+ {
+ return !left.Equals(right);
+ }
+
+ public static bool operator >(decimal left, AvroDecimal right)
+ {
+ return left.CompareTo(right) > 0;
+ }
+
+ public static bool operator >=(decimal left, AvroDecimal right)
+ {
+ return left.CompareTo(right) >= 0;
+ }
+
+ public static bool operator <(decimal left, AvroDecimal right)
+ {
+ return left.CompareTo(right) < 0;
+ }
+
+ public static bool operator <=(decimal left, AvroDecimal right)
+ {
+ return left.CompareTo(right) <= 0;
+ }
+
+ public static explicit operator byte(AvroDecimal value)
+ {
+ return ToByte(value);
+ }
+
+ /// <summary>
+ /// Creates a byte from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>A byte.</returns>
+ public static byte ToByte(AvroDecimal value)
+ {
+ return value.ToType<byte>();
+ }
+
+ public static explicit operator sbyte(AvroDecimal value)
+ {
+ return ToSByte(value);
+ }
+
+ /// <summary>
+ /// Creates a signed byte from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>A signed byte.</returns>
+ public static sbyte ToSByte(AvroDecimal value)
+ {
+ return value.ToType<sbyte>();
+ }
+
+ public static explicit operator short(AvroDecimal value)
+ {
+ return ToInt16(value);
+ }
+
+ /// <summary>
+ /// Creates a short from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>A short.</returns>
+ public static short ToInt16(AvroDecimal value)
+ {
+ return value.ToType<short>();
+ }
+
+ public static explicit operator int(AvroDecimal value)
+ {
+ return ToInt32(value);
+ }
+
+ /// <summary>
+ /// Creates an int from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>An int.</returns>
+ public static int ToInt32(AvroDecimal value)
+ {
+ return value.ToType<int>();
+ }
+
+ public static explicit operator long(AvroDecimal value)
+ {
+ return ToInt64(value);
+ }
+
+ /// <summary>
+ /// Creates a long from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>A long.</returns>
+ public static long ToInt64(AvroDecimal value)
+ {
+ return value.ToType<long>();
+ }
+
+ public static explicit operator ushort(AvroDecimal value)
+ {
+ return ToUInt16(value);
+ }
+
+ /// <summary>
+ /// Creates an unsigned short from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>An unsigned short.</returns>
+ public static ushort ToUInt16(AvroDecimal value)
+ {
+ return value.ToType<ushort>();
+ }
+
+ public static explicit operator uint(AvroDecimal value)
+ {
+ return ToUInt32(value);
+ }
+
+ /// <summary>
+ /// Creates an unsigned int from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>An unsigned int.</returns>
+ public static uint ToUInt32(AvroDecimal value)
+ {
+ return value.ToType<uint>();
+ }
+
+ public static explicit operator ulong(AvroDecimal value)
+ {
+ return ToUInt64(value);
+ }
+
+ /// <summary>
+ /// Creates an unsigned long from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>An unsigned long.</returns>
+ public static ulong ToUInt64(AvroDecimal value)
+ {
+ return value.ToType<ulong>();
+ }
+
+ public static explicit operator float(AvroDecimal value)
+ {
+ return ToSingle(value);
+ }
+
+ /// <summary>
+ /// Creates a double from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>A double.</returns>
+ public static float ToSingle(AvroDecimal value)
+ {
+ return value.ToType<float>();
+ }
+
+ public static explicit operator double(AvroDecimal value)
+ {
+ return ToDouble(value);
+ }
+
+ /// <summary>
+ /// Creates a double from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>A double.</returns>
+ public static double ToDouble(AvroDecimal value)
+ {
+ return value.ToType<double>();
+ }
+
+ public static explicit operator decimal(AvroDecimal value)
+ {
+ return ToDecimal(value);
+ }
+
+ /// <summary>
+ /// Creates a decimal from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>A decimal.</returns>
+ public static decimal ToDecimal(AvroDecimal value)
+ {
+ return value.ToType<decimal>();
+ }
+
+ public static explicit operator BigInteger(AvroDecimal value)
+ {
+ return ToBigInteger(value);
+ }
+
+ /// <summary>
+ /// Creates a <see cref="BigInteger"/> from a given <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="value">The <see cref="AvroDecimal"/>.</param>
+ /// <returns>A <see cref="BigInteger"/>.</returns>
+ public static BigInteger ToBigInteger(AvroDecimal value)
+ {
+ var scaleDivisor = BigInteger.Pow(new BigInteger(10), value.Scale);
+ var scaledValue = BigInteger.Divide(value.UnscaledValue, scaleDivisor);
+ return scaledValue;
+ }
+
+ public static implicit operator AvroDecimal(byte value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(sbyte value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(short value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(int value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(long value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(ushort value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(uint value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(ulong value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(float value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(double value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(decimal value)
+ {
+ return new AvroDecimal(value);
+ }
+
+ public static implicit operator AvroDecimal(BigInteger value)
+ {
+ return new AvroDecimal(value, 0);
+ }
+
+ /// <summary>
+ /// Converts the numeric value of the current <see cref="AvroDecimal"/> to a given type.
+ /// </summary>
+ /// <typeparam name="T">The type to which the value of the current <see cref="AvroDecimal"/> should be converted.</typeparam>
+ /// <returns>A value of type <typeparamref name="T"/> converted from the current <see cref="AvroDecimal"/>.</returns>
+ public T ToType<T>()
+ where T : struct
+ {
+ return (T)((IConvertible)this).ToType(typeof(T), null);
+ }
+
+ /// <summary>
+ /// Converts the numeric value of the current <see cref="AvroDecimal"/> to a given type.
+ /// </summary>
+ /// <param name="conversionType">The type to which the value of the current <see cref="AvroDecimal"/> should be converted.</param>
+ /// <param name="provider">An System.IFormatProvider interface implementation that supplies culture-specific formatting information.</param>
+ /// <returns></returns>
+ object IConvertible.ToType(Type conversionType, IFormatProvider provider)
+ {
+ var scaleDivisor = BigInteger.Pow(new BigInteger(10), Scale);
+ var remainder = BigInteger.Remainder(UnscaledValue, scaleDivisor);
+ var scaledValue = BigInteger.Divide(UnscaledValue, scaleDivisor);
+
+ if (scaledValue > new BigInteger(Decimal.MaxValue))
+ throw new ArgumentOutOfRangeException("value", "The value " + UnscaledValue + " cannot fit into " + conversionType.Name + ".");
+
+ var leftOfDecimal = (decimal)scaledValue;
+ var rightOfDecimal = ((decimal)remainder) / ((decimal)scaleDivisor);
+
+ var value = leftOfDecimal + rightOfDecimal;
+ return Convert.ChangeType(value, conversionType, provider);
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the current <see cref="AvroDecimal"/> and a specified object
+ /// have the same value.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns>true if the obj argument is an <see cref="AvroDecimal"/> object, and its value
+ /// is equal to the value of the current <see cref="AvroDecimal"/> instance; otherwise false.
+ /// </returns>
+ public override bool Equals(object obj)
+ {
+ return (obj is AvroDecimal) && Equals((AvroDecimal)obj);
+ }
+
+ /// <summary>
+ /// Returns the hash code for the current <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <returns>The hash code.</returns>
+ public override int GetHashCode()
+ {
+ return UnscaledValue.GetHashCode() ^ Scale.GetHashCode();
+ }
+
+ /// <summary>
+ /// Returns the <see cref="TypeCode"/> for the current <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <returns><see cref="TypeCode.Object"/>.</returns>
+ TypeCode IConvertible.GetTypeCode()
+ {
+ return TypeCode.Object;
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a boolean.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>true or false, which reflects the value of the current <see cref="AvroDecimal"/>.</returns>
+ bool IConvertible.ToBoolean(IFormatProvider provider)
+ {
+ return Convert.ToBoolean(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a byte.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>A byte.</returns>
+ byte IConvertible.ToByte(IFormatProvider provider)
+ {
+ return Convert.ToByte(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a char.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>This method always throws an <see cref="InvalidCastException"/>.</returns>
+ char IConvertible.ToChar(IFormatProvider provider)
+ {
+ throw new InvalidCastException("Cannot cast BigDecimal to Char");
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a <see cref="DateTime"/>.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>This method always throws an <see cref="InvalidCastException"/>.</returns>
+ DateTime IConvertible.ToDateTime(IFormatProvider provider)
+ {
+ throw new InvalidCastException("Cannot cast BigDecimal to DateTime");
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a decimal.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>A decimal.</returns>
+ decimal IConvertible.ToDecimal(IFormatProvider provider)
+ {
+ return Convert.ToDecimal(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a double.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>A double.</returns>
+ double IConvertible.ToDouble(IFormatProvider provider)
+ {
+ return Convert.ToDouble(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a short.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>A short.</returns>
+ short IConvertible.ToInt16(IFormatProvider provider)
+ {
+ return Convert.ToInt16(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to an int.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>An int.</returns>
+ int IConvertible.ToInt32(IFormatProvider provider)
+ {
+ return Convert.ToInt32(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a long.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>A long.</returns>
+ long IConvertible.ToInt64(IFormatProvider provider)
+ {
+ return Convert.ToInt64(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a signed byte.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>A signed byte.</returns>
+ sbyte IConvertible.ToSByte(IFormatProvider provider)
+ {
+ return Convert.ToSByte(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a float.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>A float.</returns>
+ float IConvertible.ToSingle(IFormatProvider provider)
+ {
+ return Convert.ToSingle(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a string.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>A string.</returns>
+ string IConvertible.ToString(IFormatProvider provider)
+ {
+ return Convert.ToString(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to an unsigned short.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>An unsigned short.</returns>
+ ushort IConvertible.ToUInt16(IFormatProvider provider)
+ {
+ return Convert.ToUInt16(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to an unsigned int.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>An unsigned int.</returns>
+ uint IConvertible.ToUInt32(IFormatProvider provider)
+ {
+ return Convert.ToUInt32(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to an unsigned long.
+ /// </summary>
+ /// <param name="provider">The format provider.</param>
+ /// <returns>An unsigned long.</returns>
+ ulong IConvertible.ToUInt64(IFormatProvider provider)
+ {
+ return Convert.ToUInt64(this, provider);
+ }
+
+ /// <summary>
+ /// Converts the current <see cref="AvroDecimal"/> to a string.
+ /// </summary>
+ /// <param name="format"></param>
+ /// <param name="formatProvider">The format provider.</param>
+ /// <returns>A string representation of the numeric value.</returns>
+ public string ToString(string format, IFormatProvider formatProvider)
+ {
+ return ToString();
+ }
+
+ /// <summary>
+ /// Compares the value of the current <see cref="AvroDecimal"/> to the value of another object.
+ /// </summary>
+ /// <param name="obj">The object to compare.</param>
+ /// <returns>A value that indicates the relative order of the objects being compared.</returns>
+ public int CompareTo(object obj)
+ {
+ if (obj == null)
+ return 1;
+
+ if (!(obj is AvroDecimal))
+ throw new ArgumentException("Compare to object must be a BigDecimal", nameof(obj));
+
+ return CompareTo((AvroDecimal)obj);
+ }
+
+ /// <summary>
+ /// Compares the value of the current <see cref="AvroDecimal"/> to the value of another
+ /// <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="other">The <see cref="AvroDecimal"/> to compare.</param>
+ /// <returns>A value that indicates the relative order of the <see cref="AvroDecimal"/>
+ /// instances being compared.</returns>
+ public int CompareTo(AvroDecimal other)
+ {
+ var unscaledValueCompare = UnscaledValue.CompareTo(other.UnscaledValue);
+ var scaleCompare = Scale.CompareTo(other.Scale);
+
+ // if both are the same value, return the value
+ if (unscaledValueCompare == scaleCompare)
+ return unscaledValueCompare;
+
+ // if the scales are both the same return unscaled value
+ if (scaleCompare == 0)
+ return unscaledValueCompare;
+
+ var scaledValue = BigInteger.Divide(UnscaledValue, BigInteger.Pow(new BigInteger(10), Scale));
+ var otherScaledValue = BigInteger.Divide(other.UnscaledValue, BigInteger.Pow(new BigInteger(10), other.Scale));
+
+ return scaledValue.CompareTo(otherScaledValue);
+ }
+
+ /// <summary>
+ /// Returns a value that indicates whether the current <see cref="AvroDecimal"/> has the same
+ /// value as another <see cref="AvroDecimal"/>.
+ /// </summary>
+ /// <param name="other">The <see cref="AvroDecimal"/> to compare.</param>
+ /// <returns>true if the current <see cref="AvroDecimal"/> has the same value as <paramref name="other"/>;
+ /// otherwise false.</returns>
+ public bool Equals(AvroDecimal other)
+ {
+ return Scale == other.Scale && UnscaledValue == other.UnscaledValue;
+ }
+
+ private static byte[] GetBytesFromDecimal(decimal d)
+ {
+ byte[] bytes = new byte[16];
+
+ int[] bits = decimal.GetBits(d);
+ int lo = bits[0];
+ int mid = bits[1];
+ int hi = bits[2];
+ int flags = bits[3];
+
+ bytes[0] = (byte)lo;
+ bytes[1] = (byte)(lo >> 8);
+ bytes[2] = (byte)(lo >> 0x10);
+ bytes[3] = (byte)(lo >> 0x18);
+ bytes[4] = (byte)mid;
+ bytes[5] = (byte)(mid >> 8);
+ bytes[6] = (byte)(mid >> 0x10);
+ bytes[7] = (byte)(mid >> 0x18);
+ bytes[8] = (byte)hi;
+ bytes[9] = (byte)(hi >> 8);
+ bytes[10] = (byte)(hi >> 0x10);
+ bytes[11] = (byte)(hi >> 0x18);
+ bytes[12] = (byte)flags;
+ bytes[13] = (byte)(flags >> 8);
+ bytes[14] = (byte)(flags >> 0x10);
+ bytes[15] = (byte)(flags >> 0x18);
+
+ return bytes;
+ }
+ }
+ #pragma warning restore CA2225 // Operator overloads have named alternates
+ #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member
+}
diff --git a/lang/csharp/src/apache/main/CodeGen/CodeGen.cs b/lang/csharp/src/apache/main/CodeGen/CodeGen.cs
index fe7791e..a38ddcc 100644
--- a/lang/csharp/src/apache/main/CodeGen/CodeGen.cs
+++ b/lang/csharp/src/apache/main/CodeGen/CodeGen.cs
@@ -232,6 +232,7 @@ namespace Avro
case Schema.Type.Double:
case Schema.Type.Bytes:
case Schema.Type.String:
+ case Schema.Type.Logical:
break;
case Schema.Type.Enumeration:
@@ -771,6 +772,13 @@ namespace Avro
return CodeGenUtil.Object;
else
return getType(nullibleType, true, ref nullibleEnum);
+
+ case Schema.Type.Logical:
+ var logicalSchema = schema as LogicalSchema;
+ if (null == logicalSchema)
+ throw new CodeGenException("Unable to cast schema into a logical schema");
+ return logicalSchema.LogicalType.GetCSharpType(nullible).ToString();
+
}
throw new CodeGenException("Unable to generate CodeTypeReference for " + schema.Name + " type " + schema.Tag);
}
diff --git a/lang/csharp/src/apache/main/Generic/GenericDatumWriter.cs b/lang/csharp/src/apache/main/Generic/GenericDatumWriter.cs
index e7d1138..eb8c81f 100644
--- a/lang/csharp/src/apache/main/Generic/GenericDatumWriter.cs
+++ b/lang/csharp/src/apache/main/Generic/GenericDatumWriter.cs
@@ -132,6 +132,8 @@ namespace Avro.Generic
case Schema.Type.Fixed:
//return obj is GenericFixed && (obj as GenericFixed).Schema.Equals(s);
return obj is GenericFixed && (obj as GenericFixed).Schema.SchemaName.Equals((sc as FixedSchema).SchemaName);
+ case Schema.Type.Logical:
+ return (sc as LogicalSchema).LogicalType.IsInstanceOfLogicalType(obj);
default:
throw new AvroException("Unknown schema type: " + sc.Tag);
}
diff --git a/lang/csharp/src/apache/main/Generic/GenericReader.cs b/lang/csharp/src/apache/main/Generic/GenericReader.cs
index b6813fa..74a1b67 100644
--- a/lang/csharp/src/apache/main/Generic/GenericReader.cs
+++ b/lang/csharp/src/apache/main/Generic/GenericReader.cs
@@ -226,6 +226,8 @@ namespace Avro.Generic
return ReadMap(reuse, (MapSchema)writerSchema, readerSchema, d);
case Schema.Type.Union:
return ReadUnion(reuse, (UnionSchema)writerSchema, readerSchema, d);
+ case Schema.Type.Logical:
+ return ReadLogical(reuse, (LogicalSchema)writerSchema, readerSchema, d);
default:
throw new AvroException("Unknown schema type: " + writerSchema);
}
@@ -551,6 +553,22 @@ namespace Avro.Generic
}
/// <summary>
+ /// Deserializes an object based on the writer's logical schema. Uses the underlying logical type to convert
+ /// the value to the logical type.
+ /// </summary>
+ /// <param name="reuse">If appropriate, uses this object instead of creating a new one.</param>
+ /// <param name="writerSchema">The UnionSchema that the writer used.</param>
+ /// <param name="readerSchema">The schema the reader uses.</param>
+ /// <param name="d">The decoder for serialization.</param>
+ /// <returns>The deserialized object.</returns>
+ protected virtual object ReadLogical(object reuse, LogicalSchema writerSchema, Schema readerSchema, Decoder d)
+ {
+ LogicalSchema ls = (LogicalSchema)readerSchema;
+
+ return writerSchema.LogicalType.ConvertToLogicalValue(Read(reuse, writerSchema.BaseSchema, ls.BaseSchema, d), ls);
+ }
+
+ /// <summary>
/// Deserializes a fixed object and returns the object. The default implementation uses CreateFixed()
/// and GetFixedBuffer() and returns what CreateFixed() returned.
/// </summary>
@@ -661,6 +679,9 @@ namespace Avro.Generic
case Schema.Type.Union:
Skip((writerSchema as UnionSchema)[d.ReadUnionIndex()], d);
break;
+ case Schema.Type.Logical:
+ Skip((writerSchema as LogicalSchema).BaseSchema, d);
+ break;
default:
throw new AvroException("Unknown schema type: " + writerSchema);
}
diff --git a/lang/csharp/src/apache/main/Generic/GenericWriter.cs b/lang/csharp/src/apache/main/Generic/GenericWriter.cs
index 5f439bb..3c0d5a9 100644
--- a/lang/csharp/src/apache/main/Generic/GenericWriter.cs
+++ b/lang/csharp/src/apache/main/Generic/GenericWriter.cs
@@ -160,6 +160,9 @@ namespace Avro.Generic
case Schema.Type.Union:
WriteUnion(schema as UnionSchema, value, encoder);
break;
+ case Schema.Type.Logical:
+ WriteLogical(schema as LogicalSchema, value, encoder);
+ break;
default:
Error(schema, value);
break;
@@ -404,6 +407,18 @@ namespace Avro.Generic
}
/// <summary>
+ /// Serializes a logical value object by using the underlying logical type to convert the value
+ /// to its base value.
+ /// </summary>
+ /// <param name="ls">The schema for serialization</param>
+ /// <param name="value">The value to be serialized</param>
+ /// <param name="encoder">The encoder for serialization</param>
+ protected virtual void WriteLogical(LogicalSchema ls, object value, Encoder encoder)
+ {
+ Write(ls.BaseSchema, ls.LogicalType.ConvertToBaseValue(value, ls), encoder);
+ }
+
+ /// <summary>
/// Serialized a fixed object. The default implementation requires that the value is
/// a GenericFixed object with an identical schema as es.
/// </summary>
@@ -486,6 +501,8 @@ namespace Avro.Generic
case Schema.Type.Fixed:
//return obj is GenericFixed && (obj as GenericFixed).Schema.Equals(s);
return obj is GenericFixed && (obj as GenericFixed).Schema.SchemaName.Equals((sc as FixedSchema).SchemaName);
+ case Schema.Type.Logical:
+ return (sc as LogicalSchema).LogicalType.IsInstanceOfLogicalType(obj);
default:
throw new AvroException("Unknown schema type: " + sc.Tag);
}
diff --git a/lang/csharp/src/apache/main/Generic/PreresolvingDatumReader.cs b/lang/csharp/src/apache/main/Generic/PreresolvingDatumReader.cs
index 8b0cffa..c55957a 100644
--- a/lang/csharp/src/apache/main/Generic/PreresolvingDatumReader.cs
+++ b/lang/csharp/src/apache/main/Generic/PreresolvingDatumReader.cs
@@ -177,6 +177,8 @@ namespace Avro.Generic
return ResolveMap((MapSchema)writerSchema, (MapSchema)readerSchema);
case Schema.Type.Union:
return ResolveUnion((UnionSchema)writerSchema, readerSchema);
+ case Schema.Type.Logical:
+ return ResolveLogical((LogicalSchema)writerSchema, (LogicalSchema)readerSchema);
default:
throw new AvroException("Unknown schema type: " + writerSchema);
}
@@ -390,6 +392,12 @@ namespace Avro.Generic
return array;
}
+ private ReadItem ResolveLogical(LogicalSchema writerSchema, LogicalSchema readerSchema)
+ {
+ var baseReader = ResolveReader(writerSchema.BaseSchema, readerSchema.BaseSchema);
+ return (r, d) => readerSchema.LogicalType.ConvertToLogicalValue(baseReader(r, d), readerSchema);
+ }
+
private ReadItem ResolveFixed(FixedSchema writerSchema, FixedSchema readerSchema)
{
if (readerSchema.Size != writerSchema.Size)
@@ -496,6 +504,9 @@ namespace Avro.Generic
lookup[i] = GetSkip( unionSchema[i] );
}
return d => lookup[d.ReadUnionIndex()](d);
+ case Schema.Type.Logical:
+ var logicalSchema = (LogicalSchema)writerSchema;
+ return GetSkip(logicalSchema.BaseSchema);
default:
throw new AvroException("Unknown schema type: " + writerSchema);
}
diff --git a/lang/csharp/src/apache/main/Generic/PreresolvingDatumWriter.cs b/lang/csharp/src/apache/main/Generic/PreresolvingDatumWriter.cs
index e4e96d3..a90ac34 100644
--- a/lang/csharp/src/apache/main/Generic/PreresolvingDatumWriter.cs
+++ b/lang/csharp/src/apache/main/Generic/PreresolvingDatumWriter.cs
@@ -99,6 +99,8 @@ namespace Avro.Generic
return ResolveMap((MapSchema)schema);
case Schema.Type.Union:
return ResolveUnion((UnionSchema)schema);
+ case Schema.Type.Logical:
+ return ResolveLogical((LogicalSchema)schema);
default:
return (v, e) => Error(schema, v);
}
@@ -232,6 +234,17 @@ namespace Avro.Generic
encoder.WriteArrayEnd();
}
+ /// <summary>
+ /// Serializes a logical value object by using the underlying logical type to convert the value
+ /// to its base value.
+ /// </summary>
+ /// <param name="schema">The logical schema.</param>
+ protected WriteItem ResolveLogical(LogicalSchema schema)
+ {
+ var baseWriter = ResolveWriter(schema.BaseSchema);
+ return (d, e) => baseWriter(schema.LogicalType.ConvertToBaseValue(d, schema), e);
+ }
+
private WriteItem ResolveMap(MapSchema mapSchema)
{
var itemWriter = ResolveWriter(mapSchema.ValueSchema);
diff --git a/lang/csharp/src/apache/main/Schema/LogicalSchema.cs b/lang/csharp/src/apache/main/Schema/LogicalSchema.cs
new file mode 100644
index 0000000..3c1928e
--- /dev/null
+++ b/lang/csharp/src/apache/main/Schema/LogicalSchema.cs
@@ -0,0 +1,118 @@
+/*
+ * 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
+ *
+ * https://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 Avro.Util;
+using Newtonsoft.Json.Linq;
+
+namespace Avro
+{
+ /// <summary>
+ /// Class for logical type schemas.
+ /// </summary>
+ public class LogicalSchema : UnnamedSchema
+ {
+ /// <summary>
+ /// Schema for the underlying type that the logical type is based on.
+ /// </summary>
+ public Schema BaseSchema { get; private set; }
+
+ /// <summary>
+ /// The logical type name.
+ /// </summary>
+ public string LogicalTypeName { get; private set; }
+
+ /// <summary>
+ /// The logical type implementation that supports this logical type.
+ /// </summary>
+ public LogicalType LogicalType { get; private set; }
+
+ internal static LogicalSchema NewInstance(JToken jtok, PropertyMap props, SchemaNames names, string encspace)
+ {
+ JToken jtype = jtok["type"];
+ if (null == jtype) throw new AvroTypeException("Logical Type does not have 'type'");
+
+ return new LogicalSchema(Schema.ParseJson(jtype, names, encspace), JsonHelper.GetRequiredString(jtok, "logicalType"), props);
+ }
+
+ private LogicalSchema(Schema baseSchema, string logicalTypeName, PropertyMap props) : base(Type.Logical, props)
+ {
+ if (null == baseSchema) throw new ArgumentNullException(nameof(baseSchema));
+ BaseSchema = baseSchema;
+ LogicalTypeName = logicalTypeName;
+ LogicalType = LogicalTypeFactory.Instance.GetFromLogicalSchema(this);
+ }
+
+ /// <summary>
+ /// Writes logical schema in JSON format
+ /// </summary>
+ /// <param name="writer">JSON writer</param>
+ /// <param name="names">list of named schemas already written</param>
+ /// <param name="encspace">enclosing namespace of the schema</param>
+ protected internal override void WriteJson(Newtonsoft.Json.JsonTextWriter writer, SchemaNames names, string encspace)
+ {
+ writer.WriteStartObject();
+ writer.WritePropertyName("type");
+ BaseSchema.WriteJson(writer, names, encspace);
+ writer.WritePropertyName("logicalType");
+ writer.WriteValue(LogicalTypeName);
+ if (null != Props)
+ Props.WriteJson(writer);
+ writer.WriteEndObject();
+ }
+
+ /// <summary>
+ /// Checks if this schema can read data written by the given schema. Used for decoding data.
+ /// </summary>
+ /// <param name="writerSchema">writer schema</param>
+ /// <returns>true if this and writer schema are compatible based on the AVRO specification, false otherwise</returns>
+ public override bool CanRead(Schema writerSchema)
+ {
+ if (writerSchema.Tag != Tag) return false;
+
+ LogicalSchema that = writerSchema as LogicalSchema;
+ return BaseSchema.CanRead(that.BaseSchema);
+ }
+
+ /// <summary>
+ /// Function to compare equality of two logical schemas
+ /// </summary>
+ /// <param name="obj">other logical schema</param>
+ /// <returns>true if two schemas are equal, false otherwise</returns>
+ public override bool Equals(object obj)
+ {
+ if (this == obj) return true;
+
+ if (obj != null && obj is LogicalSchema that)
+ {
+ if (BaseSchema.Equals(that.BaseSchema))
+ return areEqual(that.Props, Props);
+ }
+ return false;
+ }
+
+ /// <summary>
+ /// Hashcode function
+ /// </summary>
+ /// <returns></returns>
+ public override int GetHashCode()
+ {
+ return 29 * BaseSchema.GetHashCode() + getHashCode(Props);
+ }
+ }
+}
diff --git a/lang/csharp/src/apache/main/Schema/Property.cs b/lang/csharp/src/apache/main/Schema/Property.cs
index 8ffd6bf..5fdb9ef 100644
--- a/lang/csharp/src/apache/main/Schema/Property.cs
+++ b/lang/csharp/src/apache/main/Schema/Property.cs
@@ -30,7 +30,7 @@ namespace Avro
/// <summary>
/// Set of reserved schema property names, any other properties not defined in this set are custom properties and can be added to this map
/// </summary>
- private static readonly HashSet<string> ReservedProps = new HashSet<string>() { "type", "name", "namespace", "fields", "items", "size", "symbols", "values", "aliases", "order", "doc", "default" };
+ private static readonly HashSet<string> ReservedProps = new HashSet<string>() { "type", "name", "namespace", "fields", "items", "size", "symbols", "values", "aliases", "order", "doc", "default", "logicalType" };
/// <summary>
/// Parses the custom properties from the given JSON object and stores them
diff --git a/lang/csharp/src/apache/main/Schema/Schema.cs b/lang/csharp/src/apache/main/Schema/Schema.cs
index aba4e72..284b574 100644
--- a/lang/csharp/src/apache/main/Schema/Schema.cs
+++ b/lang/csharp/src/apache/main/Schema/Schema.cs
@@ -104,7 +104,12 @@ namespace Avro
/// <summary>
/// A protocol error.
/// </summary>
- Error
+ Error,
+
+ /// <summary>
+ /// A logical type.
+ /// </summary>
+ Logical
}
/// <summary>
@@ -187,6 +192,8 @@ namespace Avro
return ArraySchema.NewInstance(jtok, props, names, encspace);
if (type.Equals("map", StringComparison.Ordinal))
return MapSchema.NewInstance(jtok, props, names, encspace);
+ if (null != jo["logicalType"]) // logical type based on a primitive
+ return LogicalSchema.NewInstance(jtok, props, names, encspace);
Schema schema = PrimitiveSchema.NewInstance((string)type, props);
if (null != schema) return schema;
@@ -195,6 +202,8 @@ namespace Avro
}
else if (jtype.Type == JTokenType.Array)
return UnionSchema.NewInstance(jtype as JArray, props, names, encspace);
+ else if (jtype.Type == JTokenType.Object && null != jo["logicalType"]) // logical type based on a complex type
+ return LogicalSchema.NewInstance(jtok, props, names, encspace);
}
throw new AvroTypeException($"Invalid JSON for schema: {jtok} at '{jtok.Path}'");
}
diff --git a/lang/csharp/src/apache/main/Specific/SpecificDatumWriter.cs b/lang/csharp/src/apache/main/Specific/SpecificDatumWriter.cs
index ebf1121..bfc8884 100644
--- a/lang/csharp/src/apache/main/Specific/SpecificDatumWriter.cs
+++ b/lang/csharp/src/apache/main/Specific/SpecificDatumWriter.cs
@@ -149,6 +149,8 @@ namespace Avro.Specific
case Schema.Type.Fixed:
return obj is SpecificFixed &&
((obj as SpecificFixed).Schema as FixedSchema).SchemaName.Equals((sc as FixedSchema).SchemaName);
+ case Schema.Type.Logical:
+ return (sc as LogicalSchema).LogicalType.IsInstanceOfLogicalType(obj);
default:
throw new AvroException("Unknown schema type: " + sc.Tag);
}
diff --git a/lang/csharp/src/apache/main/Specific/SpecificWriter.cs b/lang/csharp/src/apache/main/Specific/SpecificWriter.cs
index 89f256b..b595241 100644
--- a/lang/csharp/src/apache/main/Specific/SpecificWriter.cs
+++ b/lang/csharp/src/apache/main/Specific/SpecificWriter.cs
@@ -219,6 +219,8 @@ namespace Avro.Specific
case Schema.Type.Fixed:
return obj is SpecificFixed &&
((obj as SpecificFixed).Schema as FixedSchema).SchemaName.Equals((sc as FixedSchema).SchemaName);
+ case Schema.Type.Logical:
+ return (sc as LogicalSchema).LogicalType.IsInstanceOfLogicalType(obj);
default:
throw new AvroException("Unknown schema type: " + sc.Tag);
}
diff --git a/lang/csharp/src/apache/main/Util/Date.cs b/lang/csharp/src/apache/main/Util/Date.cs
new file mode 100644
index 0000000..0f52b58
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/Date.cs
@@ -0,0 +1,61 @@
+/*
+ * 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
+ *
+ * https://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;
+
+namespace Avro.Util
+{
+ /// <summary>
+ /// The 'date' logical type.
+ /// </summary>
+ public class Date : LogicalUnixEpochType<DateTime>
+ {
+ /// <summary>
+ /// The logical type name for Date.
+ /// </summary>
+ public static readonly string LogicalTypeName = "date";
+
+ /// <summary>
+ /// Initializes a new Date logical type.
+ /// </summary>
+ public Date() : base(LogicalTypeName)
+ { }
+
+
+ /// <inheritdoc/>
+ public override void ValidateSchema(LogicalSchema schema)
+ {
+ if (Schema.Type.Int != schema.BaseSchema.Tag)
+ throw new AvroTypeException("'date' can only be used with an underlying int type");
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+ {
+ var date = ((DateTime)logicalValue).Date;
+ return (date - UnixEpochDateTime).Days;
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+ {
+ var noDays = (int)baseValue;
+ return UnixEpochDateTime.AddDays(noDays);
+ }
+ }
+}
diff --git a/lang/csharp/src/apache/main/Util/Decimal.cs b/lang/csharp/src/apache/main/Util/Decimal.cs
new file mode 100644
index 0000000..3714bd1
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/Decimal.cs
@@ -0,0 +1,131 @@
+/*
+ * 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
+ *
+ * https://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.Globalization;
+using System.Numerics;
+using Avro.Generic;
+
+namespace Avro.Util
+{
+ /// <summary>
+ /// The 'decimal' logical type.
+ /// </summary>
+ public class Decimal : LogicalType
+ {
+ /// <summary>
+ /// The logical type name for Decimal.
+ /// </summary>
+ public static readonly string LogicalTypeName = "decimal";
+
+ /// <summary>
+ /// Initializes a new Decimal logical type.
+ /// </summary>
+ public Decimal() : base(LogicalTypeName)
+ { }
+
+ /// <inheritdoc/>
+ public override void ValidateSchema(LogicalSchema schema)
+ {
+ if (Schema.Type.Bytes != schema.BaseSchema.Tag && Schema.Type.Fixed != schema.BaseSchema.Tag)
+ throw new AvroTypeException("'decimal' can only be used with an underlying bytes or fixed type");
+
+ var precisionVal = schema.GetProperty("precision");
+
+ if (string.IsNullOrEmpty(precisionVal))
+ throw new AvroTypeException("'decimal' requires a 'precision' property");
+
+ var precision = int.Parse(precisionVal, CultureInfo.CurrentCulture);
+
+ if (precision <= 0)
+ throw new AvroTypeException("'decimal' requires a 'precision' property that is greater than zero");
+
+ var scale = GetScalePropertyValueFromSchema(schema);
+
+ if (scale < 0 || scale > precision)
+ throw new AvroTypeException("'decimal' requires a 'scale' property that is zero or less than or equal to 'precision'");
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+ {
+ var decimalValue = (AvroDecimal)logicalValue;
+ var logicalScale = GetScalePropertyValueFromSchema(schema);
+ var scale = decimalValue.Scale;
+
+ if (scale != logicalScale)
+ throw new ArgumentOutOfRangeException(nameof(logicalValue), $"The decimal value has a scale of {scale} which cannot be encoded against a logical 'decimal' with a scale of {logicalScale}");
+
+ var buffer = decimalValue.UnscaledValue.ToByteArray();
+
+ Array.Reverse(buffer);
+
+ return Schema.Type.Bytes == schema.BaseSchema.Tag
+ ? (object)buffer
+ : (object)new GenericFixed(
+ (FixedSchema)schema.BaseSchema,
+ GetDecimalFixedByteArray(buffer, ((FixedSchema)schema.BaseSchema).Size,
+ decimalValue.Sign < 0 ? (byte)0xFF : (byte)0x00));
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+ {
+ var buffer = Schema.Type.Bytes == schema.BaseSchema.Tag
+ ? (byte[])baseValue
+ : ((GenericFixed)baseValue).Value;
+
+ Array.Reverse(buffer);
+
+ return new AvroDecimal(new BigInteger(buffer), GetScalePropertyValueFromSchema(schema));
+ }
+
+ /// <inheritdoc/>
+ public override Type GetCSharpType(bool nullible)
+ {
+ return nullible ? typeof(AvroDecimal?) : typeof(AvroDecimal);
+ }
+
+ /// <inheritdoc/>
+ public override bool IsInstanceOfLogicalType(object logicalValue)
+ {
+ return logicalValue is AvroDecimal;
+ }
+
+ private static int GetScalePropertyValueFromSchema(Schema schema, int defaultVal = 0)
+ {
+ var scaleVal = schema.GetProperty("scale");
+
+ return string.IsNullOrEmpty(scaleVal) ? defaultVal : int.Parse(scaleVal, CultureInfo.CurrentCulture);
+ }
+
+ private static byte[] GetDecimalFixedByteArray(byte[] sourceBuffer, int size, byte fillValue)
+ {
+ var paddedBuffer = new byte[size];
+
+ var offset = size - sourceBuffer.Length;
+
+ for (var idx = 0; idx < size; idx++)
+ {
+ paddedBuffer[idx] = idx < offset ? fillValue : sourceBuffer[idx - offset];
+ }
+
+ return paddedBuffer;
+ }
+ }
+}
diff --git a/lang/csharp/src/apache/main/Util/LogicalType.cs b/lang/csharp/src/apache/main/Util/LogicalType.cs
new file mode 100644
index 0000000..fec6bda
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/LogicalType.cs
@@ -0,0 +1,77 @@
+/*
+ * 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
+ *
+ * https://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;
+
+namespace Avro.Util
+{
+ /// <summary>
+ /// Base for all logical type implementations.
+ /// </summary>
+ public abstract class LogicalType
+ {
+ /// <summary>
+ /// The logical type name.
+ /// </summary>
+ public string Name { get; }
+
+ /// <summary>
+ /// Initializes the base logical type.
+ /// </summary>
+ /// <param name="name">The logical type name.</param>
+ protected LogicalType(string name)
+ {
+ Name = name;
+ }
+
+ /// <summary>
+ /// Applies logical type validation for a given logical schema.
+ /// </summary>
+ /// <param name="schema">The schema to be validated.</param>
+ public virtual void ValidateSchema(LogicalSchema schema)
+ { }
+
+ /// <summary>
+ /// Converts a logical value to an instance of its base type.
+ /// </summary>
+ /// <param name="logicalValue">The logical value to convert.</param>
+ /// <param name="schema">The schema that represents the target of the conversion.</param>
+ /// <returns>An object representing the encoded value of the base type.</returns>
+ public abstract object ConvertToBaseValue(object logicalValue, LogicalSchema schema);
+
+ /// <summary>
+ /// Converts a base value to an instance of the logical type.
+ /// </summary>
+ /// <param name="baseValue">The base value to convert.</param>
+ /// <param name="schema">The schema that represents the target of the conversion.</param>
+ /// <returns>An object representing the encoded value of the logical type.</returns>
+ public abstract object ConvertToLogicalValue(object baseValue, LogicalSchema schema);
+
+ /// <summary>
+ /// Retrieve the .NET type that is represented by the logical type implementation.
+ /// </summary>
+ /// <param name="nullible">A flag indicating whether it should be nullible.</param>
+ public abstract Type GetCSharpType(bool nullible);
+
+ /// <summary>
+ /// Determines if a given object is an instance of the logical type.
+ /// </summary>
+ /// <param name="logicalValue">The logical value to test.</param>
+ public abstract bool IsInstanceOfLogicalType(object logicalValue);
+ }
+}
diff --git a/lang/csharp/src/apache/main/Util/LogicalTypeFactory.cs b/lang/csharp/src/apache/main/Util/LogicalTypeFactory.cs
new file mode 100644
index 0000000..3dcae79
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/LogicalTypeFactory.cs
@@ -0,0 +1,85 @@
+/*
+ * 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
+ *
+ * https://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.Collections.Generic;
+
+namespace Avro.Util
+{
+ /// <summary>
+ /// A factory for logical type implementations.
+ /// </summary>
+ public class LogicalTypeFactory
+ {
+ private readonly IDictionary<string, LogicalType> _logicalTypes;
+
+ /// <summary>
+ /// Returns the <see cref="LogicalTypeFactory" /> singleton.
+ /// </summary>
+ /// <returns>The <see cref="LogicalTypeFactory" /> singleton. </returns>
+ public static LogicalTypeFactory Instance { get; } = new LogicalTypeFactory();
+
+ private LogicalTypeFactory()
+ {
+ _logicalTypes = new Dictionary<string, LogicalType>()
+ {
+ { Decimal.LogicalTypeName, new Decimal() },
+ { Date.LogicalTypeName, new Date() },
+ { TimeMillisecond.LogicalTypeName, new TimeMillisecond() },
+ { TimeMicrosecond.LogicalTypeName, new TimeMicrosecond() },
+ { TimestampMillisecond.LogicalTypeName, new TimestampMillisecond() },
+ { TimestampMicrosecond.LogicalTypeName, new TimestampMicrosecond() }
+ };
+ }
+
+ /// <summary>
+ /// Registers or replaces a logical type implementation.
+ /// </summary>
+ /// <param name="logicalType">The <see cref="LogicalType"/> implementation that should be registered.</param>
+ public void Register(LogicalType logicalType)
+ {
+ _logicalTypes[logicalType.Name] = logicalType;
+ }
+
+ /// <summary>
+ /// Retrieves a logical type implementation for a given logical schema.
+ /// </summary>
+ /// <param name="schema">The schema.</param>
+ /// <param name="ignoreInvalidOrUnknown">A flag to indicate if an exception should be thrown for invalid
+ /// or unknown logical types.</param>
+ /// <returns>A <see cref="LogicalType" />.</returns>
+ public LogicalType GetFromLogicalSchema(LogicalSchema schema, bool ignoreInvalidOrUnknown = false)
+ {
+ try
+ {
+ if (!_logicalTypes.TryGetValue(schema.LogicalTypeName, out LogicalType logicalType))
+ throw new AvroTypeException("Logical type '" + schema.LogicalTypeName + "' is not supported.");
+
+ logicalType.ValidateSchema(schema);
+
+ return logicalType;
+ }
+ catch (AvroTypeException)
+ {
+ if (!ignoreInvalidOrUnknown)
+ throw;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/lang/csharp/src/apache/main/Util/LogicalUnixEpochType.cs b/lang/csharp/src/apache/main/Util/LogicalUnixEpochType.cs
new file mode 100644
index 0000000..f4187d0
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/LogicalUnixEpochType.cs
@@ -0,0 +1,54 @@
+/*
+ * 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
+ *
+ * https://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;
+
+namespace Avro.Util
+{
+ /// <summary>
+ /// Base for all logical type implementations that are based on the Unix Epoch date/time.
+ /// </summary>
+ public abstract class LogicalUnixEpochType<T> : LogicalType
+ where T : struct
+ {
+ /// <summary>
+ /// The date and time of the Unix Epoch.
+ /// </summary>
+ protected static readonly DateTime UnixEpochDateTime = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+ /// <summary>
+ /// Initializes the base logical type.
+ /// </summary>
+ /// <param name="name">The logical type name.</param>
+ protected LogicalUnixEpochType(string name)
+ : base(name)
+ { }
+
+ /// <inheritdoc/>
+ public override Type GetCSharpType(bool nullible)
+ {
+ return nullible ? typeof(T?) : typeof(T);
+ }
+
+ /// <inheritdoc/>
+ public override bool IsInstanceOfLogicalType(object logicalValue)
+ {
+ return logicalValue is T;
+ }
+ }
+}
diff --git a/lang/csharp/src/apache/main/Util/TimeMicrosecond.cs b/lang/csharp/src/apache/main/Util/TimeMicrosecond.cs
new file mode 100644
index 0000000..f561d6f
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/TimeMicrosecond.cs
@@ -0,0 +1,66 @@
+/*
+ * 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
+ *
+ * https://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;
+
+namespace Avro.Util
+{
+ /// <summary>
+ /// The 'time-micros' logical type.
+ /// </summary>
+ public class TimeMicrosecond : LogicalUnixEpochType<TimeSpan>
+ {
+ private static readonly TimeSpan _maxTime = new TimeSpan(23, 59, 59);
+
+ /// <summary>
+ /// The logical type name for TimeMicrosecond.
+ /// </summary>
+ public static readonly string LogicalTypeName = "time-micros";
+
+ /// <summary>
+ /// Initializes a new TimeMicrosecond logical type.
+ /// </summary>
+ public TimeMicrosecond() : base(LogicalTypeName)
+ { }
+
+ /// <inheritdoc/>
+ public override void ValidateSchema(LogicalSchema schema)
+ {
+ if (Schema.Type.Long != schema.BaseSchema.Tag)
+ throw new AvroTypeException("'time-micros' can only be used with an underlying long type");
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+ {
+ var time = (TimeSpan)logicalValue;
+
+ if (time > _maxTime)
+ throw new ArgumentOutOfRangeException(nameof(logicalValue), "A 'time-micros' value can only have the range '00:00:00' to '23:59:59'.");
+
+ return (long)(time - UnixEpochDateTime.TimeOfDay).TotalMilliseconds * 1000;
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+ {
+ var noMs = (long)baseValue / 1000;
+ return UnixEpochDateTime.TimeOfDay.Add(TimeSpan.FromMilliseconds(noMs));
+ }
+ }
+}
diff --git a/lang/csharp/src/apache/main/Util/TimeMillisecond.cs b/lang/csharp/src/apache/main/Util/TimeMillisecond.cs
new file mode 100644
index 0000000..9008fa3
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/TimeMillisecond.cs
@@ -0,0 +1,66 @@
+/*
+ * 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
+ *
+ * https://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;
+
+namespace Avro.Util
+{
+ /// <summary>
+ /// The 'time-millis' logical type.
+ /// </summary>
+ public class TimeMillisecond : LogicalUnixEpochType<TimeSpan>
+ {
+ private static readonly TimeSpan _maxTime = new TimeSpan(23, 59, 59);
+
+ /// <summary>
+ /// The logical type name for TimeMillisecond.
+ /// </summary>
+ public static readonly string LogicalTypeName = "time-millis";
+
+ /// <summary>
+ /// Initializes a new TimeMillisecond logical type.
+ /// </summary>
+ public TimeMillisecond() : base(LogicalTypeName)
+ { }
+
+ /// <inheritdoc/>
+ public override void ValidateSchema(LogicalSchema schema)
+ {
+ if (Schema.Type.Int != schema.BaseSchema.Tag)
+ throw new AvroTypeException("'time-millis' can only be used with an underlying int type");
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+ {
+ var time = (TimeSpan)logicalValue;
+
+ if (time > _maxTime)
+ throw new ArgumentOutOfRangeException(nameof(logicalValue), "A 'time-millis' value can only have the range '00:00:00' to '23:59:59'.");
+
+ return (int)(time - UnixEpochDateTime.TimeOfDay).TotalMilliseconds;
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+ {
+ var noMs = (int)baseValue;
+ return UnixEpochDateTime.TimeOfDay.Add(TimeSpan.FromMilliseconds(noMs));
+ }
+ }
+}
diff --git a/lang/csharp/src/apache/main/Util/TimestampMicrosecond.cs b/lang/csharp/src/apache/main/Util/TimestampMicrosecond.cs
new file mode 100644
index 0000000..54a421a
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/TimestampMicrosecond.cs
@@ -0,0 +1,60 @@
+/*
+ * 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
+ *
+ * https://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;
+
+namespace Avro.Util
+{
+ /// <summary>
+ /// The 'timestamp-micros' logical type.
+ /// </summary>
+ public class TimestampMicrosecond : LogicalUnixEpochType<DateTime>
+ {
+ /// <summary>
+ /// The logical type name for TimestampMicrosecond.
+ /// </summary>
+ public static readonly string LogicalTypeName = "timestamp-micros";
+
+ /// <summary>
+ /// Initializes a new TimestampMicrosecond logical type.
+ /// </summary>
+ public TimestampMicrosecond() : base(LogicalTypeName)
+ { }
+
+ /// <inheritdoc/>
+ public override void ValidateSchema(LogicalSchema schema)
+ {
+ if (Schema.Type.Long != schema.BaseSchema.Tag)
+ throw new AvroTypeException("'timestamp-micros' can only be used with an underlying long type");
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+ {
+ var date = ((DateTime)logicalValue).ToUniversalTime();
+ return (long)((date - UnixEpochDateTime).TotalMilliseconds * 1000);
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+ {
+ var noMs = (long)baseValue / 1000;
+ return UnixEpochDateTime.AddMilliseconds(noMs);
+ }
+ }
+}
diff --git a/lang/csharp/src/apache/main/Util/TimestampMillisecond.cs b/lang/csharp/src/apache/main/Util/TimestampMillisecond.cs
new file mode 100644
index 0000000..481d474
--- /dev/null
+++ b/lang/csharp/src/apache/main/Util/TimestampMillisecond.cs
@@ -0,0 +1,59 @@
+/*
+ * 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
+ *
+ * https://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;
+
+namespace Avro.Util
+{
+ /// <summary>
+ /// The 'timestamp-millis' logical type.
+ /// </summary>
+ public class TimestampMillisecond : LogicalUnixEpochType<DateTime>
+ {
+ /// <summary>
+ /// The logical type name for TimestampMillisecond.
+ /// </summary>
+ public static readonly string LogicalTypeName = "timestamp-millis";
+
+ /// <summary>
+ /// Initializes a new TimestampMillisecond logical type.
+ /// </summary>
+ public TimestampMillisecond() : base(LogicalTypeName)
+ { }
+
+ /// <inheritdoc/>
+ public override void ValidateSchema(LogicalSchema schema)
+ {
+ if (Schema.Type.Long != schema.BaseSchema.Tag)
+ throw new AvroTypeException("'timestamp-millis' can only be used with an underlying long type");
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToBaseValue(object logicalValue, LogicalSchema schema)
+ {
+ var date = ((DateTime)logicalValue).ToUniversalTime();
+ return (long)(date - UnixEpochDateTime).TotalMilliseconds;
+ }
+
+ /// <inheritdoc/>
+ public override object ConvertToLogicalValue(object baseValue, LogicalSchema schema)
+ {
+ var noMs = (long)baseValue;
+ return UnixEpochDateTime.AddMilliseconds(noMs);
+ }
+ }
+}
diff --git a/lang/csharp/src/apache/test/Generic/GenericTests.cs b/lang/csharp/src/apache/test/Generic/GenericTests.cs
index 2f6e45d..e17215d 100644
--- a/lang/csharp/src/apache/test/Generic/GenericTests.cs
+++ b/lang/csharp/src/apache/test/Generic/GenericTests.cs
@@ -108,6 +108,50 @@ namespace Avro.Test.Generic
test(schema, mkMap(values));
}
+ [TestCase()]
+ public void TestLogical_Date()
+ {
+ test("{\"type\": \"int\", \"logicalType\": \"date\"}", DateTime.UtcNow.Date);
+ }
+
+ [TestCase()]
+ public void TestLogical_TimeMillisecond()
+ {
+ test("{\"type\": \"int\", \"logicalType\": \"time-millis\"}", new TimeSpan(3, 0, 0));
+ }
+
+ [TestCase()]
+ public void TestLogical_TimeMicrosecond()
+ {
+ test("{\"type\": \"long\", \"logicalType\": \"time-micros\"}", new TimeSpan(3, 0, 0));
+ }
+
+ [TestCase()]
+ public void TestLogical_TimestampMillisecond()
+ {
+ test("{\"type\": \"long\", \"logicalType\": \"timestamp-millis\"}", new DateTime(1990, 1, 1, 14, 15, 30));
+ }
+
+ [TestCase()]
+ public void TestLogical_TimestampMicrosecond()
+ {
+ test("{\"type\": \"long\", \"logicalType\": \"timestamp-micros\"}", new DateTime(1990, 1, 1, 14, 15, 30));
+ }
+
+ [TestCase()]
+ public void TestLogical_Decimal_Bytes()
+ {
+ test("{\"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 30, \"scale\": 2}",
+ (AvroDecimal)12345678912345.55M);
+ }
+
+ [TestCase()]
+ public void TestLogical_Decimal_Fixed()
+ {
+ test("{\"type\": {\"type\": \"fixed\", \"size\": 16, \"name\": \"n\"}, \"logicalType\": \"decimal\", \"precision\": 30, \"scale\": 2}",
+ (AvroDecimal)12345678912345.55M);
+ }
+
[TestCase("[{\"type\":\"record\", \"name\":\"n\", \"fields\":[{\"name\":\"f1\", \"type\":\"string\"}]}, \"string\"]",
"{\"type\":\"record\", \"name\":\"n\", \"fields\":[{\"name\":\"f1\", \"type\":\"string\"}]}",
new object[] { "f1", "v1" })]
diff --git a/lang/csharp/src/apache/test/Schema/SchemaTests.cs b/lang/csharp/src/apache/test/Schema/SchemaTests.cs
index ba07f5a..50d54b7 100644
--- a/lang/csharp/src/apache/test/Schema/SchemaTests.cs
+++ b/lang/csharp/src/apache/test/Schema/SchemaTests.cs
@@ -261,6 +261,27 @@ namespace Avro.Test
testToString(sc);
}
+ [TestCase("{\"type\": \"int\", \"logicalType\": \"date\"}", "int", "date")]
+ public void TestLogicalPrimitive(string s, string baseType, string logicalType)
+ {
+ Schema sc = Schema.Parse(s);
+ Assert.AreEqual(Schema.Type.Logical, sc.Tag);
+ LogicalSchema logsc = sc as LogicalSchema;
+ Assert.AreEqual(baseType, logsc.BaseSchema.Name);
+ Assert.AreEqual(logicalType, logsc.LogicalType.Name);
+
+ testEquality(s, sc);
+ testToString(sc);
+ }
+
+ [TestCase("{\"type\": \"int\", \"logicalType\": \"unknown\"}", "unknown")]
+ public void TestUnknownLogical(string s, string unknownType)
+ {
+ var err = Assert.Throws<AvroTypeException>(() => Schema.Parse(s));
+
+ Assert.AreEqual("Logical type '" + unknownType + "' is not supported.", err.Message);
+ }
+
[TestCase("{\"type\": \"map\", \"values\": \"long\"}", "long")]
public void TestMap(string s, string value)
{
diff --git a/lang/csharp/src/apache/test/Util/LogicalTypeTests.cs b/lang/csharp/src/apache/test/Util/LogicalTypeTests.cs
new file mode 100644
index 0000000..358177d
--- /dev/null
+++ b/lang/csharp/src/apache/test/Util/LogicalTypeTests.cs
@@ -0,0 +1,182 @@
+/*
+ * 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
+ *
+ * https://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.Globalization;
+using Avro.Util;
+using NUnit.Framework;
+
+namespace Avro.Test
+{
+ [TestFixture]
+ class LogicalTypeTests
+ {
+ [TestCase("1234.56")]
+ [TestCase("-1234.56")]
+ [TestCase("123456789123456789.56")]
+ [TestCase("-123456789123456789.56")]
+ [TestCase("000000000000000001.01")]
+ [TestCase("-000000000000000001.01")]
+ public void TestDecimal(string s)
+ {
+ var schema = (LogicalSchema)Schema.Parse("{\"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 4, \"scale\": 2 }");
+
+ var avroDecimal = new Avro.Util.Decimal();
+ var decimalVal = (AvroDecimal)decimal.Parse(s);
+
+ var convertedDecimalVal = (AvroDecimal)avroDecimal.ConvertToLogicalValue(avroDecimal.ConvertToBaseValue(decimalVal, schema), schema);
+
+ Assert.AreEqual(decimalVal, convertedDecimalVal);
+ }
+
+ [TestCase]
+ public void TestDecimalMinMax()
+ {
+ var schema = (LogicalSchema)Schema.Parse("{\"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 4, \"scale\": 0 }");
+
+ var avroDecimal = new Avro.Util.Decimal();
+
+ foreach (var decimalVal in new AvroDecimal[] { decimal.MinValue, decimal.MaxValue })
+ {
+ var convertedDecimalVal = (AvroDecimal)avroDecimal.ConvertToLogicalValue(avroDecimal.ConvertToBaseValue(decimalVal, schema), schema);
+
+ Assert.AreEqual(decimalVal, convertedDecimalVal);
+ }
+ }
+
+ [TestCase]
+ public void TestDecimalOutOfRangeException()
+ {
+ var schema = (LogicalSchema)Schema.Parse("{\"type\": \"bytes\", \"logicalType\": \"decimal\", \"precision\": 4, \"scale\": 2 }");
+
+ var avroDecimal = new Avro.Util.Decimal();
+ var decimalVal = (AvroDecimal)1234.567M; // scale of 3 should throw ArgumentOutOfRangeException
+
+ Assert.Throws<ArgumentOutOfRangeException>(() => avroDecimal.ConvertToBaseValue(decimalVal, schema));
+ }
+
+ [TestCase("01/01/2019")]
+ [TestCase("05/05/2019")]
+ [TestCase("05/05/2019 00:00:00Z")]
+ [TestCase("05/05/2019 01:00:00Z")]
+ [TestCase("05/05/2019 01:00:00+01:00")]
+ public void TestDate(string s)
+ {
+ var schema = (LogicalSchema)Schema.Parse("{\"type\": \"int\", \"logicalType\": \"date\"}");
+
+ var date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+ if (date.Kind != DateTimeKind.Utc)
+ {
+ date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.AssumeLocal);
+ }
+
+ var avroDate = new Date();
+
+ var convertedDate = (DateTime)avroDate.ConvertToLogicalValue(avroDate.ConvertToBaseValue(date, schema), schema);
+
+ Assert.AreEqual(new TimeSpan(0, 0, 0), convertedDate.TimeOfDay); // the time should always be 00:00:00
+ Assert.AreEqual(date.Date, convertedDate.Date);
+ }
+
+ [TestCase("01/01/2019 14:20:00Z", "01/01/2019 14:20:00Z")]
+ [TestCase("01/01/2019 14:20:00", "01/01/2019 14:20:00Z")]
+ [TestCase("05/05/2019 14:20:00Z", "05/05/2019 14:20:00Z")]
+ [TestCase("05/05/2019 14:20:00+01:00", "05/05/2019 13:20:00Z")]
+ [TestCase("05/05/2019 00:00:00Z", "05/05/2019 00:00:00Z")]
+ [TestCase("05/05/2019 00:00:00+01:00", "05/04/2019 23:00:00Z")] // adjusted to UTC
+ public void TestTimestampMillisecond(string s, string e)
+ {
+ var schema = (LogicalSchema)Schema.Parse("{\"type\": \"long\", \"logicalType\": \"timestamp-millis\"}");
+
+ var date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+ if (date.Kind != DateTimeKind.Utc)
+ {
+ date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.AssumeLocal);
+ }
+
+ var expectedDate = DateTime.Parse(e, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+ var avroTimestampMilli = new TimestampMillisecond();
+ var convertedDate = (DateTime)avroTimestampMilli.ConvertToLogicalValue(avroTimestampMilli.ConvertToBaseValue(date, schema), schema);
+ Assert.AreEqual(expectedDate, convertedDate);
+ }
+
+ [TestCase("01/01/2019 14:20:00Z", "01/01/2019 14:20:00Z")]
+ [TestCase("01/01/2019 14:20:00", "01/01/2019 14:20:00Z")]
+ [TestCase("05/05/2019 14:20:00Z", "05/05/2019 14:20:00Z")]
+ [TestCase("05/05/2019 14:20:00+01:00", "05/05/2019 13:20:00Z")]
+ [TestCase("05/05/2019 00:00:00Z", "05/05/2019 00:00:00Z")]
+ [TestCase("05/05/2019 00:00:00+01:00", "05/04/2019 23:00:00Z")] // adjusted to UTC
+ public void TestTimestampMicrosecond(string s, string e)
+ {
+ var schema = (LogicalSchema)Schema.Parse("{\"type\": \"long\", \"logicalType\": \"timestamp-micros\"}");
+
+ var date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+ if (date.Kind != DateTimeKind.Utc)
+ {
+ date = DateTime.Parse(s, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.AssumeLocal);
+ }
+
+ var expectedDate = DateTime.Parse(e, CultureInfo.GetCultureInfo("en-US").DateTimeFormat, DateTimeStyles.RoundtripKind);
+
+ var avroTimestampMicro = new TimestampMicrosecond();
+ var convertedDate = (DateTime)avroTimestampMicro.ConvertToLogicalValue(avroTimestampMicro.ConvertToBaseValue(date, schema), schema);
+ Assert.AreEqual(expectedDate, convertedDate);
+ }
+
+ [TestCase("01:20:10", "01:20:10", false)]
+ [TestCase("23:00:00", "23:00:00", false)]
+ [TestCase("01:00:00:00", null, true)]
+ public void TestTime(string s, string e, bool expectRangeError)
+ {
+ var timeMilliSchema = (LogicalSchema)Schema.Parse("{\"type\": \"int\", \"logicalType\": \"time-millis\"}");
+ var timeMicroSchema = (LogicalSchema)Schema.Parse("{\"type\": \"long\", \"logicalType\": \"time-micros\"}");
+
+ var time = TimeSpan.Parse(s);
+
+ var avroTimeMilli = new TimeMillisecond();
+ var avroTimeMicro = new TimeMicrosecond();
+
+ if (expectRangeError)
+ {
+ Assert.Throws<ArgumentOutOfRangeException>(() =>
+ {
+ avroTimeMilli.ConvertToLogicalValue(avroTimeMilli.ConvertToBaseValue(time, timeMilliSchema), timeMilliSchema);
+ });
+ Assert.Throws<ArgumentOutOfRangeException>(() =>
+ {
+ avroTimeMicro.ConvertToLogicalValue(avroTimeMilli.ConvertToBaseValue(time, timeMicroSchema), timeMicroSchema);
+ });
+ }
+ else
+ {
+ var expectedTime = TimeSpan.Parse(e);
+
+ var convertedTime = (TimeSpan)avroTimeMilli.ConvertToLogicalValue(avroTimeMilli.ConvertToBaseValue(time, timeMilliSchema), timeMilliSchema);
+ Assert.AreEqual(expectedTime, convertedTime);
+
+ convertedTime = (TimeSpan)avroTimeMicro.ConvertToLogicalValue(avroTimeMicro.ConvertToBaseValue(time, timeMicroSchema), timeMicroSchema);
+ Assert.AreEqual(expectedTime, convertedTime);
+
+ }
+ }
+ }
+}