Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support of decimal type #26

Merged
merged 11 commits into from
Aug 31, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
- Add support of decimal type
## v0.0.9
- Remove support for .NET Core 3.1
- Add support for .NET 7.0
Expand Down
127 changes: 121 additions & 6 deletions src/Ydb.Sdk/src/Value/YdbValueBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public static YdbValue MakeBool(bool value)
BoolValue = value
});
}

public static YdbValue MakeInt8(sbyte value)
{
return new YdbValue(
Expand Down Expand Up @@ -201,12 +202,115 @@ public static YdbValue MakeJsonDocument(string value)
});
}

private static byte GetDecimalScale(decimal value)
{
var bits = decimal.GetBits(value);
var flags = bits[3];
var scale = (byte)((flags >> 16) & 0x7F);
return scale;
}

private static uint GetDecimalPrecision(decimal value)
{
var bits = decimal.GetBits(value);
value = new decimal(lo: bits[0], mid: bits[1], hi: bits[2], isNegative: false, scale: 0);

var precision = 0u;
while (value != decimal.Zero)
{
value = Math.Round(value / 10);
precision++;
}

return precision;
}

private static Ydb.Value MakeDecimalValue(decimal value)
{
var bits = decimal.GetBits(value);

var low64 = ((ulong)(uint)bits[1] << 32) + (uint)bits[0];
var high64 = (ulong)(uint)bits[2];

unchecked
{
// make value negative
if (value < 0)
{
low64 = ~low64;
high64 = ~high64;

if (low64 == (ulong)-1L)
{
high64 += 1;
}

low64 += 1;
}
}

return new Ydb.Value
{
Low128 = low64,
High128 = high64
};
}

public static YdbValue MakeDecimalWithPrecision(decimal value, uint? precision = null, uint? scale = null)
{
var valueScale = GetDecimalScale(value);
var valuePrecision = GetDecimalPrecision(value);
scale ??= GetDecimalScale(value);
precision ??= valuePrecision;

if ((int)valuePrecision - valueScale > (int)precision - scale)
{
throw new InvalidCastException(
$"Decimal with precision ({valuePrecision}, {valueScale}) can't fit into ({precision}, {scale})");
}

// multiply for fill value with trailing zeros
// ex: 123.45 -> 123.4500...00
value *= 1.00000000000000000000000000000m; // 29 zeros, max supported by c# decimal
rekby marked this conversation as resolved.
Show resolved Hide resolved
value = Math.Round(value, (int)scale);

var type = new Type
{
DecimalType = new DecimalType { Scale = (uint)scale, Precision = (uint)precision }
};

var ydbValue = MakeDecimalValue(value);

return new YdbValue(type, ydbValue);
}

public static YdbValue MakeDecimal(decimal value)
{
return MakeDecimalWithPrecision(value, 22, 9);
}

// TODO: EmptyOptional with complex types
public static YdbValue MakeEmptyOptional(YdbTypeId typeId)
{
return new YdbValue(
new Ydb.Type { OptionalType = new OptionalType { Item = MakePrimitiveType(typeId) } },
new Ydb.Value { NullFlagValue = new Google.Protobuf.WellKnownTypes.NullValue() });
if (IsPrimitiveTypeId(typeId))
{
return new YdbValue(
new Type { OptionalType = new OptionalType { Item = MakePrimitiveType(typeId) } },
new Ydb.Value { NullFlagValue = new Google.Protobuf.WellKnownTypes.NullValue() });
}

if (typeId == YdbTypeId.DecimalType)
{
return new YdbValue(
new Type
{
OptionalType = new OptionalType { Item = new Type { DecimalType = new DecimalType() } }
},
new Ydb.Value { NullFlagValue = new Google.Protobuf.WellKnownTypes.NullValue() }
);
}

throw new ArgumentException($"This type is not supported: {typeId}", nameof(typeId));
}

public static YdbValue MakeOptional(YdbValue value)
Expand Down Expand Up @@ -266,7 +370,8 @@ public static YdbValue MakeStruct(IReadOnlyDictionary<string, YdbValue> members)
StructType = new StructType()
};

type.StructType.Members.Add(members.Select(m => new StructMember { Name = m.Key, Type = m.Value._protoType }));
type.StructType.Members.Add(
members.Select(m => new StructMember { Name = m.Key, Type = m.Value._protoType }));

var value = new Ydb.Value();
value.Items.Add(members.Select(m => m.Value._protoValue));
Expand All @@ -287,9 +392,14 @@ private static Ydb.Type MakePrimitiveType(YdbTypeId typeId)
return new Ydb.Type { TypeId = (Type.Types.PrimitiveTypeId)typeId };
}

private static bool IsPrimitiveTypeId(YdbTypeId typeId)
{
return (uint)typeId < YdbTypeIdRanges.ComplexTypesFirst;
}

private static void EnsurePrimitiveTypeId(YdbTypeId typeId)
{
if ((uint)typeId >= YdbTypeIdRanges.ComplexTypesFirst)
if (!IsPrimitiveTypeId(typeId))
{
throw new ArgumentException($"Complex types aren't supported in current method: {typeId}", "typeId");
}
Expand Down Expand Up @@ -442,5 +552,10 @@ public static YdbValue MakeOptionalJsonDocument(string? value)
return MakeOptional(MakeJsonDocument(value));
}
}

public static YdbValue MakeOptionalDecimal(decimal? value)
{
return MakeOptionalOf(value, YdbTypeId.DecimalType, MakeDecimal);
}
}
}
}
23 changes: 23 additions & 0 deletions src/Ydb.Sdk/src/Value/YdbValueCast.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ public static explicit operator bool(YdbValue value)
{
return (bool)GetObject(value, typeof(bool));
}

public static explicit operator bool?(YdbValue value)
{
return (bool?)GetOptionalObject(value, typeof(bool));
}

public static explicit operator sbyte(YdbValue value)
{
return (sbyte)GetObject(value, typeof(sbyte));
Expand Down Expand Up @@ -142,6 +144,16 @@ public static explicit operator TimeSpan(YdbValue value)
return (byte[]?)GetOptionalObject(value, typeof(byte[]));
}

public static explicit operator decimal(YdbValue value)
{
return (decimal)GetObject(value, typeof(decimal));
}

public static explicit operator decimal?(YdbValue value)
{
return (decimal?)GetOptionalObject(value, typeof(decimal));
}

private static object GetObject(YdbValue value, System.Type targetType)
{
return GetObjectInternal(value.TypeId, value, targetType);
Expand Down Expand Up @@ -179,6 +191,7 @@ private static object GetObject(YdbValue value, System.Type targetType)
case YdbTypeId.Yson: return value?.GetYson();
case YdbTypeId.Json: return value?.GetJson();
case YdbTypeId.JsonDocument: return value?.GetJsonDocument();
case YdbTypeId.DecimalType: return value?.GetDecimal();
default:
throw new InvalidCastException($"Cannot cast YDB type {typeId} to {targetType.Name}.");

Expand Down Expand Up @@ -304,5 +317,15 @@ public static explicit operator YdbValue(TimeSpan? value)
{
return MakeOptionalInterval(value);
}

public static explicit operator YdbValue(decimal value)
{
return MakeDecimal(value);
}

public static explicit operator YdbValue(decimal? value)
{
return MakeOptionalDecimal(value);
}
}
}
37 changes: 36 additions & 1 deletion src/Ydb.Sdk/src/Value/YdbValueParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,36 @@ public string GetJsonDocument()
return _protoValue.TextValue;
}

public decimal GetDecimal()
{
EnsureType(Type.TypeOneofCase.DecimalType);
var low64 = _protoValue.Low128;
var high64 = _protoValue.High128;

var scale = _protoType.DecimalType.Scale;

var isNegative = false;

unchecked
{
if (high64 >> 63 == 1) // if negative
{
isNegative = true;
if (low64 == 0)
{
high64 -= 1;
}

low64 -= 1;

low64 = ~low64;
high64 = ~high64;
}
}

return new decimal((int)low64, (int)(low64 >> 32), (int)high64, isNegative, (byte)scale);
}

public bool? GetOptionalBool()
{
return GetOptional()?.GetBool();
Expand Down Expand Up @@ -228,6 +258,11 @@ public string GetJsonDocument()
return GetOptional()?.GetJsonDocument();
}

public decimal? GetOptionalDecimal()
{
return GetOptional()?.GetDecimal();
}

public YdbValue? GetOptional()
{
EnsureType(Ydb.Type.TypeOneofCase.OptionalType);
Expand Down Expand Up @@ -288,4 +323,4 @@ internal InvalidTypeException(string expectedType, string actualType)
}
}
}
}
}
59 changes: 58 additions & 1 deletion src/Ydb.Sdk/tests/Value/TestBasicUnit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,63 @@ public void OptionalPrimitiveCast()
Assert.Null((TimeSpan?)(YdbValue)(TimeSpan?)null);
}

[Fact]
public void DecimalType()
XmasApple marked this conversation as resolved.
Show resolved Hide resolved
{
(decimal, decimal)[] values =
{
(-0.1m, -0.1m),
(0.0000000000000000000000000001m, 0m),
(0.0000000000000000000000000000m, 0m),
(-18446744073.709551616m,
-18446744073.709551616m), // covers situation when need to add/substract 1 to/from high64
(123.456m, 123.456m),
};
foreach (var (value, excepted) in values)
{
var ydbval = YdbValue.MakeDecimal(value);
var result = ydbval.GetDecimal();
Assert.Equal(excepted, result);

Assert.Equal(excepted, YdbValue.MakeDecimal(value).GetDecimal());
Assert.Equal(excepted, YdbValue.MakeOptionalDecimal(value).GetOptionalDecimal());

Assert.Equal(excepted, (decimal)(YdbValue)value);
Assert.Equal(excepted, (decimal?)(YdbValue)(decimal?)value);
}

Assert.Null(YdbValue.MakeOptionalDecimal(null).GetOptionalDecimal());
Assert.Null((decimal?)(YdbValue)(decimal?)null);

Assert.Equal("Decimal with precision (30, 0) can't fit into (22, 9)",
Assert.Throws<InvalidCastException>(() => YdbValue.MakeDecimal(decimal.MaxValue)).Message);
}

[Fact]
public void DecimalTypeWithPrecision()
{
Assert.Equal(12345m, YdbValue.MakeDecimalWithPrecision(12345m).GetDecimal());
Assert.Equal(12345m, YdbValue.MakeDecimalWithPrecision(12345m, precision: 5, scale: 0).GetDecimal());
Assert.Equal(12345m, YdbValue.MakeDecimalWithPrecision(12345m, precision: 7, scale: 2).GetDecimal());
Assert.Equal(123.46m, YdbValue.MakeDecimalWithPrecision(123.456m, precision: 5, scale: 2).GetDecimal());
Assert.Equal(-18446744073.709551616m,
YdbValue.MakeDecimalWithPrecision(-18446744073.709551616m).GetDecimal());
Assert.Equal(-18446744073.709551616m,
YdbValue.MakeDecimalWithPrecision(-18446744073.709551616m, precision: 21, scale: 9).GetDecimal());
Assert.Equal(-18446744074m,
YdbValue.MakeDecimalWithPrecision(-18446744073.709551616m, precision: 12, scale: 0).GetDecimal());
Assert.Equal(-184467440730709551616m,
YdbValue.MakeDecimalWithPrecision(-184467440730709551616m, precision: 21, scale: 0).GetDecimal());


Assert.Equal("Decimal with precision (5, 0) can't fit into (4, 0)",
Assert.Throws<InvalidCastException>(() => YdbValue.MakeDecimalWithPrecision(12345m, precision: 4))
.Message);
Assert.Equal("Decimal with precision (5, 0) can't fit into (5, 2)",
Assert.Throws<InvalidCastException>(() => YdbValue.MakeDecimalWithPrecision(12345m, precision: 5, 2))
.Message);
}

[Fact]
public void ListType()
{
Expand Down Expand Up @@ -306,4 +363,4 @@ public void StructType()
Assert.Equal("ten", (string)s["Member2"]!);
}
}
}
}
Loading