Skip to content

Commit

Permalink
Merge pull request #26 from XmasApple/addDecimal
Browse files Browse the repository at this point in the history
Add support of decimal type
  • Loading branch information
XmasApple authored Aug 31, 2023
2 parents ddffc45 + 23a9bfa commit 771b844
Show file tree
Hide file tree
Showing 7 changed files with 375 additions and 34 deletions.
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
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()
{
(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

0 comments on commit 771b844

Please sign in to comment.