blob: 07208a53df0167636a398ef4b5a07828659ff3a1 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
using System.Numerics;
namespace Apache.Fory;
public readonly record struct ForyDecimal(BigInteger UnscaledValue, int Scale);
public sealed class DecimalSerializer : Serializer<decimal>
{
private static readonly BigInteger UInt32Mask = uint.MaxValue;
public override decimal DefaultValue => 0m;
public override void WriteData(WriteContext context, in decimal value, bool hasGenerics)
{
_ = hasGenerics;
(int scale, BigInteger unscaled) = ToParts(value);
DecimalCodec.Write(context.Writer, scale, unscaled);
}
public override decimal ReadData(ReadContext context)
{
(int scale, BigInteger unscaled) = DecimalCodec.Read(context.Reader);
return FromParts(scale, unscaled);
}
private static (int Scale, BigInteger Unscaled) ToParts(decimal value)
{
int[] bits = decimal.GetBits(value);
BigInteger unscaled =
((BigInteger)(uint)bits[2] << 64) |
((BigInteger)(uint)bits[1] << 32) |
(uint)bits[0];
if ((bits[3] & unchecked((int)0x8000_0000)) != 0)
{
unscaled = BigInteger.Negate(unscaled);
}
return ((bits[3] >> 16) & 0xFF, unscaled);
}
private static decimal FromParts(int scale, BigInteger unscaled)
{
if (scale is < 0 or > 28)
{
throw new InvalidDataException($"decimal scale {scale} is outside System.Decimal range");
}
bool negative = unscaled.Sign < 0;
BigInteger magnitude = BigInteger.Abs(unscaled);
if ((magnitude >> 96) != BigInteger.Zero)
{
throw new InvalidDataException("decimal magnitude exceeds System.Decimal range");
}
int lo = unchecked((int)(uint)(magnitude & UInt32Mask));
int mid = unchecked((int)(uint)((magnitude >> 32) & UInt32Mask));
int hi = unchecked((int)(uint)((magnitude >> 64) & UInt32Mask));
return new decimal(lo, mid, hi, negative, (byte)scale);
}
}
internal sealed class ForyDecimalSerializer : Serializer<ForyDecimal>
{
public override ForyDecimal DefaultValue => default;
public override void WriteData(WriteContext context, in ForyDecimal value, bool hasGenerics)
{
_ = hasGenerics;
DecimalCodec.Write(context.Writer, value.Scale, value.UnscaledValue);
}
public override ForyDecimal ReadData(ReadContext context)
{
(int scale, BigInteger unscaled) = DecimalCodec.Read(context.Reader);
return new ForyDecimal(unscaled, scale);
}
}
internal static class DecimalCodec
{
private static readonly BigInteger LongMin = long.MinValue;
private static readonly BigInteger LongMax = long.MaxValue;
public static void Write(ByteWriter buffer, int scale, BigInteger unscaled)
{
buffer.WriteVarInt32(scale);
if (CanUseSmallEncoding(unscaled))
{
long smallValue = (long)unscaled;
ulong zigzag = EncodeZigZag64(smallValue);
buffer.WriteVarUInt64(zigzag << 1);
return;
}
BigInteger magnitude = BigInteger.Abs(unscaled);
if (magnitude.IsZero)
{
throw new InvalidDataException("zero must use the small decimal encoding");
}
byte[] payload = magnitude.ToByteArray(isUnsigned: true, isBigEndian: false);
ulong meta = ((ulong)payload.Length << 1) | (unscaled.Sign < 0 ? 1UL : 0UL);
ulong header = (meta << 1) | 1UL;
buffer.WriteVarUInt64(header);
buffer.WriteBytes(payload);
}
public static (int Scale, BigInteger Unscaled) Read(ByteReader buffer)
{
int scale = buffer.ReadVarInt32();
ulong header = buffer.ReadVarUInt64();
if ((header & 1UL) == 0UL)
{
return (scale, new BigInteger(DecodeZigZag64(header >> 1)));
}
ulong meta = header >> 1;
ulong lenLong = meta >> 1;
if (lenLong == 0 || lenLong > int.MaxValue)
{
throw new InvalidDataException($"invalid decimal magnitude length {lenLong}");
}
int length = checked((int)lenLong);
byte[] payload = buffer.ReadBytes(length);
if (payload[^1] == 0)
{
throw new InvalidDataException("non-canonical decimal payload: trailing zero byte");
}
BigInteger magnitude = new(payload, isUnsigned: true, isBigEndian: false);
if (magnitude.IsZero)
{
throw new InvalidDataException("big decimal encoding must not represent zero");
}
return (scale, (meta & 1UL) == 0UL ? magnitude : BigInteger.Negate(magnitude));
}
private static bool CanUseSmallEncoding(BigInteger value)
{
if (value < LongMin || value > LongMax)
{
return false;
}
ulong zigzag = EncodeZigZag64((long)value);
return (zigzag & (1UL << 63)) == 0;
}
private static ulong EncodeZigZag64(long value)
{
return unchecked((ulong)((value << 1) ^ (value >> 63)));
}
private static long DecodeZigZag64(ulong value)
{
return unchecked((long)((value >> 1) ^ (ulong)-(long)(value & 1UL)));
}
}