diff --git a/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs b/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs index 94067e9..4b3b532 100644 --- a/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs +++ b/csharp/EPAM.Deltix.DFP.Test/Decimal64Test.cs @@ -1227,6 +1227,48 @@ private static void TestToDecimalAndBackCase(long mantissa, int exp) throw new Exception($"TestToDecimalAndBackCase({mantissa}L, {exp}); // d1(={d0}) != d2(={d2})"); } + private class ToStringData + { + public Decimal64 TestValue; + public string NormalOut; + public string FloatOut; + + public ToStringData(Decimal64 testValue, string normalOut, string floatOut) + { + this.TestValue = testValue; + this.NormalOut = normalOut; + this.FloatOut = floatOut; + } + } + + [Test] + public void Issue91ToFloatString() + { + var testCases = new ToStringData[] + { + new ToStringData(Decimal64.FromFixedPoint(14L, 0), "14", "14.0"), + new ToStringData(Decimal64.FromFixedPoint(140000000000L, 10), "14", "14.0"), + new ToStringData(Decimal64.Zero, "0", "0.0") + }; + + foreach(var testCase in testCases) + { + var testValue = testCase.TestValue; + Assert.AreEqual(testCase.NormalOut, testValue.ToString()); + Assert.AreEqual(testCase.FloatOut, testValue.ToFloatString()); + + { + var sb = new StringBuilder(); + Assert.AreEqual(testCase.NormalOut, testValue.AppendTo(sb).ToString()); + } + + { + var sb = new StringBuilder(); + Assert.AreEqual(testCase.FloatOut, testValue.FloatAppendTo(sb).ToString()); + } + } + } + readonly int N = 5000000; static void Main() diff --git a/csharp/EPAM.Deltix.DFP/Decimal64.cs b/csharp/EPAM.Deltix.DFP/Decimal64.cs index d3b2282..ceaef1e 100644 --- a/csharp/EPAM.Deltix.DFP/Decimal64.cs +++ b/csharp/EPAM.Deltix.DFP/Decimal64.cs @@ -58,12 +58,12 @@ internal Decimal64(UInt64 value) public override String ToString() { - return DotNetImpl.ToString(Bits, DecimalMarkDefault); + return DotNetImpl.ToString(Bits, DecimalMarkDefault, false); } public String ToString(char decimalMark) { - return DotNetImpl.ToString(Bits, decimalMark); + return DotNetImpl.ToString(Bits, decimalMark, false); //return ((Double)this).ToString(CultureInfo.InvariantCulture); } @@ -1036,14 +1036,24 @@ public String ToScientificString(char decimalMark) return DotNetImpl.ToScientificString(Bits, decimalMark); } + public String ToFloatString() + { + return DotNetImpl.ToString(Bits, DecimalMarkDefault, true); + } + + public String ToFloatString(char decimalMark) + { + return DotNetImpl.ToString(Bits, decimalMark, true); + } + public StringBuilder AppendTo(StringBuilder text) { - return DotNetImpl.AppendTo(Bits, DecimalMarkDefault, text); + return DotNetImpl.AppendTo(Bits, DecimalMarkDefault, false, text); } public StringBuilder AppendTo(char decimalMark, StringBuilder text) { - return DotNetImpl.AppendTo(Bits, decimalMark, text); + return DotNetImpl.AppendTo(Bits, decimalMark, false, text); } public StringBuilder ScientificAppendTo(StringBuilder text) @@ -1056,6 +1066,17 @@ public StringBuilder ScientificAppendTo(char decimalMark, StringBuilder text) return DotNetImpl.ScientificAppendTo(Bits, decimalMark, text); } + public StringBuilder FloatAppendTo(StringBuilder text) + { + return DotNetImpl.AppendTo(Bits, DecimalMarkDefault, true, text); + } + + public StringBuilder FloatAppendTo(char decimalMark, StringBuilder text) + { + return DotNetImpl.AppendTo(Bits, decimalMark, true, text); + } + + #endregion #region IComparable<> Interface implementation diff --git a/csharp/EPAM.Deltix.DFP/DotNetImpl.cs b/csharp/EPAM.Deltix.DFP/DotNetImpl.cs index 33bc5b3..2c01f62 100644 --- a/csharp/EPAM.Deltix.DFP/DotNetImpl.cs +++ b/csharp/EPAM.Deltix.DFP/DotNetImpl.cs @@ -13,8 +13,6 @@ namespace EPAM.Deltix.DFP { internal static class DotNetImpl { - private const bool ToStringRemoveTrailingZeroes = true; // Controls if ToString removes trailing zeroes or not - #region Constants public const UInt64 PositiveInfinity = 0x7800000000000000UL; public const UInt64 NegativeInfinity = 0xF800000000000000UL; @@ -1158,112 +1156,206 @@ static bool IsRoundedToReciprocalImpl(int addExponent, Pair96 coefficientMulR, F #endregion #region Formatting & Parsing - public static String ToString(UInt64 value, char decimalMark) + private const int BcdTableDigits = 3; + private const int BcdDivider = 1000000000; + private const int BcdDividerGroups = 3; // log10(BCD_DIVIDER) / BCD_TABLE_DIGITS must be natural value + + private static char[] BCD_TABLE = MakeBcdTable(BcdTableDigits); + + private static char[] MakeBcdTable(int tenPowerMaxIndex) + { + int n = 1; + for (int i = 0; i < tenPowerMaxIndex; ++i) + n *= 10; + + char[] table = new char[n * tenPowerMaxIndex]; + + char[] value = new char[tenPowerMaxIndex]; + for (int i = 0; i < value.Length; ++i) // Array.Fill is not available in .NET Standard 2.0, but 2.1 + value[i] = '0'; + + for (int i = 0, ib = 0; i < n; ++i) + { + for (int j = 0; j < tenPowerMaxIndex; ++j) + table[ib++] = value[j]; + value[0] = (char)(value[0] + 1); + for (int j = 0; j < tenPowerMaxIndex - 1; ++j) + { + if (value[j] <= '9') + break; + else + { + value[j] = (char)(value[j] - 10); + value[j + 1] = (char)(value[j + 1] + 1); + } + } + } + + return table; + } + + private static unsafe int FormatUIntFromBcdTable(int value, char* buffer, int bi) + { + for (int blockIndex = 0; blockIndex < BcdDividerGroups; ++blockIndex) + { + int newValue = (int)((ulong)(2199023256L * value) >> 41); + int remainder = value - newValue * 1000; + //final int remainder = value - ((newValue << 10) - (newValue << 4) - (newValue << 3)); + value = newValue; + + for (int j = 0, ti = remainder * BcdTableDigits /* (remainder << 1) + remainder */; j < BcdTableDigits; ++j, ++ti) + buffer[--bi] = BCD_TABLE[ti]; + } + + return bi; + } + + public static String ToString(UInt64 value, char decimalMark, bool floatStyle) { if (!IsFinite(value)) { - return - IsInfinity(value) ? - (SignBit(value) ? "-Infinity" : "Infinity") - : IsNaN(value) ? - (SignBit(value) ? "SNaN" : "NaN") - : "?"; + // Value is either Inf or NaN + // TODO: Do we need SNaN? + return IsNaN(value) ? "NaN" : SignBit(value) ? "-Infinity" : "Infinity"; } - Int32 exponent; - Boolean isNegative = (Int64)value < 0; - Int64 coefficient; // Unsigned div by constant in not optimized by .NET + Int32 partsExponent; + BID_UINT64 partsCoefficient; if ((~value & SpecialEncodingMask) == 0) //if ((x & SpecialEncodingMask) == SpecialEncodingMask) { - Int32 exp2; - coefficient = (Int64)UnpackSpecial(value, out exp2); - exponent = exp2; - + partsCoefficient = UnpackSpecial(value, out partsExponent); } else { // Extract the exponent. - exponent = (int)(value >> ExponentShiftSmall) & (int)ShiftedExponentMask; + partsExponent = (int)(value >> ExponentShiftSmall) & (int)ShiftedExponentMask; // Extract the coefficient. - coefficient = (Int64)(value & SmallCoefficientMask); + partsCoefficient = value & SmallCoefficientMask; } + if (partsCoefficient == 0) + { + return !floatStyle ? "0" : ("0" + decimalMark + "0"); + } + + int exponent = partsExponent - ExponentBias; + unsafe { - // TODO: Special case possible for mantissa = 0, otherwise will be printed according to common rules, w/o normalization -#pragma warning disable CS0162 // Unreachable code detected - if (ToStringRemoveTrailingZeroes) - if (0 == coefficient) - return "0"; + var bufferLength = 512; + var buffer = stackalloc char[bufferLength]; - if (exponent >= BaseExponent) + if (exponent >= 0) { - if (!ToStringRemoveTrailingZeroes) - if (0 == coefficient) - return "0"; + int bi = bufferLength; + if (floatStyle) + { + buffer[--bi] = '0'; + buffer[--bi] = decimalMark; + } + for (int i = 0; i < exponent; ++i) + buffer[--bi] = '0'; - int nZeros = exponent - BaseExponent; - int nAlloc = exponent + (20 - BaseExponent); - char* s = stackalloc char[nAlloc]; - char* e = s + nAlloc - 2, p = e - nZeros; + while (partsCoefficient > 0) + { + bi = FormatUIntFromBcdTable((int)(partsCoefficient % BcdDivider), buffer, bi); + partsCoefficient /= BcdDivider; + } - for (int i = nZeros; i != 0; --i) - p[i] = '0'; + while (buffer[bi] == '0') + ++bi; - do - { - // This is to make the code generator generate 1 DIV instead of 2 - Int64 old = coefficient + '0'; - coefficient /= 10; - *p-- = (char)(old - coefficient * 10); // = [old - new * 10] - } while (coefficient != 0); + if (SignBit(value)) + buffer[--bi] = '-'; - if ((Int64)value < 0) - *p-- = '-'; + return new string(buffer, bi, bufferLength - bi); - return new string(p + 1, 0, (int)(e - p)); } else - { - int dotPos = BaseExponent - exponent; - int nAlloc = (20 + BaseExponent) - exponent; - char* s = stackalloc char[nAlloc]; - char* p = s + nAlloc - 2, e = p; + { // exponent < 0 + int bi = bufferLength; - do - { - Int64 old = coefficient + '0'; - coefficient /= 10; - *p = decimalMark; - p += 0 == dotPos-- ? -1 : 0; // Hopefully branch-free method to insert decimal dot - *p-- = (char)(old - coefficient * 10); // = [old - new * 10] - } while (coefficient != 0); - // Haven't placed the dot yet? - if (dotPos >= 0) + int digits = NumberOfDigits(partsCoefficient); + + if (digits + exponent > 0) { - for (; dotPos > 0; --dotPos) - *p-- = '0'; - p[0] = decimalMark; - p[-1] = '0'; - p -= 2; - } + ulong integralPart = partsCoefficient / PowersOfTen[-exponent]; + ulong fractionalPart = partsCoefficient % PowersOfTen[-exponent]; - if ((Int64)value < 0) - *p-- = '-'; + while (fractionalPart > 0) + { + bi = FormatUIntFromBcdTable((int)(fractionalPart % BcdDivider), buffer, bi); + fractionalPart /= BcdDivider; + } - if (ToStringRemoveTrailingZeroes) - { - if ('0' == *e) + int written = bufferLength - bi /* already written */; + //if (written < -exponent /* must be written */) + for (int ei = 0, ee = -exponent - written; ei < ee; ++ei) + buffer[--bi] = '0'; + + bi = bufferLength + exponent; /* buffer.length - (-exponent) */ + + buffer[--bi] = decimalMark; + + while (integralPart > 0) { - while ('0' == *--e) { } - if (decimalMark == *e) - --e; + bi = FormatUIntFromBcdTable((int)(integralPart % BcdDivider), buffer, bi); + integralPart /= BcdDivider; } + + while (buffer[bi] == '0') + ++bi; + + if (SignBit(value)) + buffer[--bi] = '-'; + + int be = bufferLength; + while (buffer[be - 1] == '0') + --be; + + if (buffer[be - 1] == decimalMark) + { + if (!floatStyle) + { + --be; + } + else if (be < bufferLength) + { + buffer[be++] = '0'; + } + } + + return new string(buffer, bi, be - bi); + } + else + { + while (partsCoefficient > 0) + { + bi = FormatUIntFromBcdTable((int)(partsCoefficient % BcdDivider), buffer, bi); + partsCoefficient /= BcdDivider; + } + + int written = bufferLength - bi /* already written */; + //if (written < -exponent /* must be written */) + for (int ei = 0, ee = -exponent - written; ei < ee; ++ei) + buffer[--bi] = '0'; + + bi = bufferLength + exponent; /* buffer.length - (-exponent) */ + + buffer[--bi] = decimalMark; + buffer[--bi] = '0'; - return new string(p + 1, 0, (int)(e - p)); + if (SignBit(value)) + buffer[--bi] = '-'; + + int be = bufferLength; + while (buffer[be - 1] == '0') + --be; + + return new string(buffer, bi, be - bi); + } } -#pragma warning restore CS0162 } } @@ -1329,125 +1421,172 @@ public static string ToScientificString(UInt64 value, char decimalMark) } } - public static StringBuilder AppendTo(UInt64 value, char decimalMark, StringBuilder text) + public static StringBuilder AppendTo(UInt64 value, char decimalMark, bool floatStyle, StringBuilder stringBuilder) { if (!IsFinite(value)) { - return text.Append( - IsInfinity(value) ? - (SignBit(value) ? "-Infinity" : "Infinity") - : IsNaN(value) ? - (SignBit(value) ? "SNaN" : "NaN") - : "?"); + // Value is either Inf or NaN + // TODO: Do we need SNaN? + return stringBuilder.Append(IsNaN(value) ? "NaN" : SignBit(value) ? "-Infinity" : "Infinity"); } - Int32 exponent; - Boolean isNegative = (Int64)value < 0; - Int64 coefficient; // Unsigned div by constant in not optimized by .NET + Int32 partsExponent; + BID_UINT64 partsCoefficient; if ((~value & SpecialEncodingMask) == 0) //if ((x & SpecialEncodingMask) == SpecialEncodingMask) { - Int32 exp2; - coefficient = (Int64)UnpackSpecial(value, out exp2); - exponent = exp2; - + partsCoefficient = UnpackSpecial(value, out partsExponent); } else { // Extract the exponent. - exponent = (int)(value >> ExponentShiftSmall) & (int)ShiftedExponentMask; + partsExponent = (int)(value >> ExponentShiftSmall) & (int)ShiftedExponentMask; // Extract the coefficient. - coefficient = (Int64)(value & SmallCoefficientMask); + partsCoefficient = value & SmallCoefficientMask; } - unsafe + if (partsCoefficient == 0) { - // TODO: Special case possible for mantissa = 0, otherwise will be printed according to common rules, w/o normalization -#pragma warning disable CS0162 // Unreachable code detected - if (ToStringRemoveTrailingZeroes) - if (0 == coefficient) - return text.Append("0"); + return stringBuilder.Append(!floatStyle ? "0" : ("0" + decimalMark + "0")); + } - if (exponent >= BaseExponent) - { - if (!ToStringRemoveTrailingZeroes) - if (0 == coefficient) - return text.Append("0"); + int exponent = partsExponent - ExponentBias; - int nZeros = exponent - BaseExponent; - int nAlloc = exponent + (20 - BaseExponent); - char* s = stackalloc char[nAlloc]; - char* e = s + nAlloc - 2, p = e - nZeros; + unsafe + { + var bufferLength = 512; + var buffer = stackalloc char[bufferLength]; - for (int i = nZeros; i != 0; --i) - p[i] = '0'; + if (exponent >= 0) + { + int bi = bufferLength; + if (floatStyle) + { + buffer[--bi] = '0'; + buffer[--bi] = decimalMark; + } + for (int i = 0; i < exponent; ++i) + buffer[--bi] = '0'; - do + while (partsCoefficient > 0) { - // This is to make the code generator generate 1 DIV instead of 2 - Int64 old = coefficient + '0'; - coefficient /= 10; - *p-- = (char)(old - coefficient * 10); // = [old - new * 10] - } while (coefficient != 0); + bi = FormatUIntFromBcdTable((int)(partsCoefficient % BcdDivider), buffer, bi); + partsCoefficient /= BcdDivider; + } - if ((Int64)value < 0) - *p-- = '-'; + while (buffer[bi] == '0') + ++bi; + + if (SignBit(value)) + buffer[--bi] = '-'; #if NET40 - char[] heapBuffer = new char[(int)(e - p)]; - Marshal.Copy((IntPtr)(p + 1), heapBuffer, 0, heapBuffer.Length); - return text.Append(heapBuffer); + char[] heapBuffer = new char[bufferLength - bi]; + Marshal.Copy((IntPtr)(buffer + bi), heapBuffer, 0, heapBuffer.Length); + return stringBuilder.Append(heapBuffer); #else - return text.Append(p + 1, (int)(e - p)); + return stringBuilder.Append(buffer + bi, bufferLength - bi); #endif + } else - { - int dotPos = BaseExponent - exponent; - int nAlloc = (20 + BaseExponent) - exponent; - char* s = stackalloc char[nAlloc]; - char* p = s + nAlloc - 2, e = p; + { // exponent < 0 + int bi = bufferLength; - do - { - Int64 old = coefficient + '0'; - coefficient /= 10; - *p = decimalMark; - p += 0 == dotPos-- ? -1 : 0; // Hopefully branch-free method to insert decimal dot - *p-- = (char)(old - coefficient * 10); // = [old - new * 10] - } while (coefficient != 0); - // Haven't placed the dot yet? - if (dotPos >= 0) + int digits = NumberOfDigits(partsCoefficient); + + if (digits + exponent > 0) { - for (; dotPos > 0; --dotPos) - *p-- = '0'; - p[0] = decimalMark; - p[-1] = '0'; - p -= 2; - } + ulong integralPart = partsCoefficient / PowersOfTen[-exponent]; + ulong fractionalPart = partsCoefficient % PowersOfTen[-exponent]; - if ((Int64)value < 0) - *p-- = '-'; + while (fractionalPart > 0) + { + bi = FormatUIntFromBcdTable((int)(fractionalPart % BcdDivider), buffer, bi); + fractionalPart /= BcdDivider; + } - if (ToStringRemoveTrailingZeroes) - { - if ('0' == *e) + int written = bufferLength - bi /* already written */; + //if (written < -exponent /* must be written */) + for (int ei = 0, ee = -exponent - written; ei < ee; ++ei) + buffer[--bi] = '0'; + + bi = bufferLength + exponent; /* buffer.length - (-exponent) */ + + buffer[--bi] = decimalMark; + + while (integralPart > 0) { - while ('0' == *--e) { } - if (decimalMark == *e) - --e; + bi = FormatUIntFromBcdTable((int)(integralPart % BcdDivider), buffer, bi); + integralPart /= BcdDivider; } + + while (buffer[bi] == '0') + ++bi; + + if (SignBit(value)) + buffer[--bi] = '-'; + + int be = bufferLength; + while (buffer[be - 1] == '0') + --be; + + if (buffer[be - 1] == decimalMark) + { + if (!floatStyle) + { + --be; + } + else if (be < bufferLength) + { + buffer[be++] = '0'; + } + } + +#if NET40 + char[] heapBuffer = new char[be - bi]; + Marshal.Copy((IntPtr)(buffer + bi), heapBuffer, 0, heapBuffer.Length); + return stringBuilder.Append(heapBuffer); +#else + return stringBuilder.Append(buffer + bi, be - bi); +#endif + } + else + { + while (partsCoefficient > 0) + { + bi = FormatUIntFromBcdTable((int)(partsCoefficient % BcdDivider), buffer, bi); + partsCoefficient /= BcdDivider; + } + + int written = bufferLength - bi /* already written */; + //if (written < -exponent /* must be written */) + for (int ei = 0, ee = -exponent - written; ei < ee; ++ei) + buffer[--bi] = '0'; + + bi = bufferLength + exponent; /* buffer.length - (-exponent) */ + + buffer[--bi] = decimalMark; + buffer[--bi] = '0'; + + if (SignBit(value)) + buffer[--bi] = '-'; + + int be = bufferLength; + while (buffer[be - 1] == '0') + --be; #if NET40 - char[] heapBuffer = new char[(int)(e - p)]; - Marshal.Copy((IntPtr)(p + 1), heapBuffer, 0, heapBuffer.Length); - return text.Append(heapBuffer); + char[] heapBuffer = new char[be - bi]; + Marshal.Copy((IntPtr)(buffer + bi), heapBuffer, 0, heapBuffer.Length); + return stringBuilder.Append(heapBuffer); #else - return text.Append(p + 1, (int)(e - p)); + return stringBuilder.Append(buffer + bi, be - bi); #endif + } } -#pragma warning restore CS0162 } +#pragma warning restore CS0162 } public static StringBuilder ScientificAppendTo(UInt64 value, char decimalMark, StringBuilder text) @@ -1568,7 +1707,8 @@ internal static String ToDebugString(UInt64 value) public const Int32 MinExponent = -383; public const Int32 MaxExponent = 384; public const Int32 BiasedExponentMaxValue = 767; - public const Int32 BaseExponent = 0x18E; + public const Int32 ExponentBias = 398; + public const Int32 BaseExponent = ExponentBias; public const Int32 MaxFormatDigits = 16; public const Int32 BidRoundingToNearest = 0x00000; diff --git a/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java b/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java index 96c6c54..51f878e 100644 --- a/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java +++ b/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64.java @@ -875,6 +875,10 @@ public static String toScientificString(final Decimal64 decimal64) { return null == decimal64 ? "null" : Decimal64Utils.toScientificString(decimal64.value); } + public static String toFloatString(final Decimal64 decimal64) { + return null == decimal64 ? "null" : Decimal64Utils.toFloatString(decimal64.value); + } + public Appendable appendTo(final Appendable appendable) throws IOException { return Decimal64Utils.appendTo(value, appendable); } @@ -883,6 +887,10 @@ public Appendable scientificAppendTo(final Appendable appendable) throws IOExcep return Decimal64Utils.scientificAppendTo(value, appendable); } + public Appendable floatAppendTo(final Appendable appendable) throws IOException { + return Decimal64Utils.floatAppendTo(value, appendable); + } + public StringBuilder appendTo(final StringBuilder builder) { return Decimal64Utils.appendTo(value, builder); } @@ -891,6 +899,10 @@ public StringBuilder scientificAppendTo(final StringBuilder builder) { return Decimal64Utils.scientificAppendTo(value, builder); } + public StringBuilder floatAppendTo(final StringBuilder builder) { + return Decimal64Utils.floatAppendTo(value, builder); + } + public static Appendable appendTo(final Decimal64 decimal64, final Appendable appendable) throws IOException { return null == decimal64 ? appendable.append("null") : Decimal64Utils.appendTo(decimal64.value, appendable); } @@ -899,6 +911,10 @@ public static Appendable scientificAppendTo(final Decimal64 decimal64, final App return null == decimal64 ? appendable.append("null") : Decimal64Utils.scientificAppendTo(decimal64.value, appendable); } + public static Appendable floatAppendTo(final Decimal64 decimal64, final Appendable appendable) throws IOException { + return null == decimal64 ? appendable.append("null") : Decimal64Utils.floatAppendTo(decimal64.value, appendable); + } + public static StringBuilder appendTo(final Decimal64 decimal64, final StringBuilder builder) { return null == decimal64 ? builder.append("null") : Decimal64Utils.appendTo(decimal64.value, builder); } @@ -907,6 +923,10 @@ public static StringBuilder scientificAppendTo(final Decimal64 decimal64, final return null == decimal64 ? builder.append("null") : Decimal64Utils.scientificAppendTo(decimal64.value, builder); } + public static StringBuilder floatAppendTo(final Decimal64 decimal64, final StringBuilder builder) { + return null == decimal64 ? builder.append("null") : Decimal64Utils.floatAppendTo(decimal64.value, builder); + } + /** * Try parse a dfp floating-point value from the given textual representation. *
@@ -1137,6 +1157,10 @@ public String toScientificString() { return Decimal64Utils.toScientificString(value); } + public String toFloatString() { + return Decimal64Utils.toFloatString(value); + } + /// endregion /// region Number Interface Implementation diff --git a/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64Utils.java b/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64Utils.java index d8f7f17..0c6f657 100644 --- a/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64Utils.java +++ b/java/dfp/src/main/java/com/epam/deltix/dfp/Decimal64Utils.java @@ -196,11 +196,11 @@ public static int hashCode(@Decimal final long value) { public static String toString(@Decimal final long value) { - return JavaImpl.fastToString(value, DECIMAL_MARK_DEFAULT); + return JavaImpl.fastToString(value, DECIMAL_MARK_DEFAULT, false); } public static String toString(@Decimal final long value, final char decimalMark) { - return JavaImpl.fastToString(value, decimalMark); + return JavaImpl.fastToString(value, decimalMark, false); } public static String toScientificString(@Decimal final long value) { @@ -211,6 +211,14 @@ public static String toScientificString(@Decimal final long value, final char de return JavaImpl.fastToScientificString(value, decimalMark); } + public static String toFloatString(@Decimal final long value) { + return JavaImpl.fastToString(value, DECIMAL_MARK_DEFAULT, true); + } + + public static String toFloatString(@Decimal final long value, final char decimalMark) { + return JavaImpl.fastToString(value, decimalMark, true); + } + static String toDebugString(@Decimal final long value) { return JavaImpl.toDebugString(value); } @@ -1249,7 +1257,7 @@ public static long canonize(@Decimal final long value) { * @throws IOException from {@link Appendable#append(char)} */ public static Appendable appendTo(@Decimal final long value, final Appendable appendable) throws IOException { - return JavaImpl.fastAppendToAppendable(value, DECIMAL_MARK_DEFAULT, appendable); + return JavaImpl.fastAppendToAppendable(value, DECIMAL_MARK_DEFAULT, false, appendable); } /** @@ -1264,7 +1272,7 @@ public static Appendable appendTo(@Decimal final long value, final Appendable ap * @throws IOException from {@link Appendable#append(char)} */ public static Appendable appendTo(@Decimal final long value, final char decimalMark, final Appendable appendable) throws IOException { - return JavaImpl.fastAppendToAppendable(value, decimalMark, appendable); + return JavaImpl.fastAppendToAppendable(value, decimalMark, false, appendable); } /** @@ -1325,6 +1333,63 @@ public static Appendable scientificAppendToChecked(@Decimal final long value, fi return scientificAppendTo(value, decimalMark, appendable); } + /** + * Append string representation of {@code DFP} {@code value} to {@link Appendable} {@code appendable} + *
+ * Same as {@code appendable.append(value.toFloatString())}, but more efficient. + * + * @param value {@code DFP64} argument + * @param appendable {@link Appendable} instance to which the string representation of the {@code value} will be appended + * @return the 2nd argument ({@link Appendable} {@code appendable}) + * @throws IOException from {@link Appendable#append(char)} + */ + public static Appendable floatAppendTo(@Decimal final long value, final Appendable appendable) throws IOException { + return JavaImpl.fastAppendToAppendable(value, DECIMAL_MARK_DEFAULT, true, appendable); + } + + /** + * Append string representation of {@code DFP} {@code value} to {@link Appendable} {@code appendable} + *
+ * Same as {@code appendable.append(value.toFloatString())}, but more efficient. + * + * @param value {@code DFP64} argument + * @param decimalMark A decimal separator used to separate the integer part from the fractional part. + * @param appendable {@link Appendable} instance to which the string representation of the {@code value} will be appended + * @return the 2nd argument ({@link Appendable} {@code appendable}) + * @throws IOException from {@link Appendable#append(char)} + */ + public static Appendable floatAppendTo(@Decimal final long value, final char decimalMark, final Appendable appendable) throws IOException { + return JavaImpl.fastAppendToAppendable(value, decimalMark, true, appendable); + } + + /** + * Implements {@link Decimal64#floatAppendTo(Appendable)}, adds null check; do not use directly. + * + * @param value DFP argument + * @param appendable an object, implementing Appendable interface + * @return .. + * @throws IOException from {@link Appendable#append(char)} + */ + @Deprecated + public static Appendable floatAppendToChecked(@Decimal final long value, final Appendable appendable) throws IOException { + checkNull(value); + return floatAppendTo(value, appendable); + } + + /** + * Implements {@link Decimal64#floatAppendTo(Appendable)}, adds null check; do not use directly. + * + * @param value DFP argument + * @param decimalMark A decimal separator used to separate the integer part from the fractional part. + * @param appendable an object, implementing Appendable interface + * @return .. + * @throws IOException from {@link Appendable#append(char)} + */ + @Deprecated + public static Appendable floatAppendToChecked(@Decimal final long value, final char decimalMark, final Appendable appendable) throws IOException { + checkNull(value); + return floatAppendTo(value, decimalMark, appendable); + } /** * Append string representation of {@code DFP} value to {@link StringBuilder} {@code sb} @@ -1336,7 +1401,7 @@ public static Appendable scientificAppendToChecked(@Decimal final long value, fi * @return the value of 2nd argument ({@link StringBuilder} {@code sb}) */ public static StringBuilder appendTo(@Decimal final long value, final StringBuilder sb) { - return JavaImpl.fastAppendToStringBuilder(value, DECIMAL_MARK_DEFAULT, sb); + return JavaImpl.fastAppendToStringBuilder(value, DECIMAL_MARK_DEFAULT, false, sb); } /** @@ -1350,7 +1415,7 @@ public static StringBuilder appendTo(@Decimal final long value, final StringBuil * @return the value of 2nd argument ({@link StringBuilder} {@code sb}) */ public static StringBuilder appendTo(@Decimal final long value, final char decimalMark, final StringBuilder sb) { - return JavaImpl.fastAppendToStringBuilder(value, decimalMark, sb); + return JavaImpl.fastAppendToStringBuilder(value, decimalMark, false, sb); } /** @@ -1407,6 +1472,60 @@ public static StringBuilder scientificAppendToChecked(@Decimal final long value, return scientificAppendTo(value, decimalMark, sb); } + /** + * Append string representation of {@code DFP} value to {@link StringBuilder} {@code sb} + *
+ * Same as {@code sb.append(value.toFloatString());}, but more efficient. + * + * @param value {@code DFP64} argument + * @param sb {@link StringBuilder} instance to which the string representation of the {@code value} will be appended + * @return the value of 2nd argument ({@link StringBuilder} {@code sb}) + */ + public static StringBuilder floatAppendTo(@Decimal final long value, final StringBuilder sb) { + return JavaImpl.fastAppendToStringBuilder(value, DECIMAL_MARK_DEFAULT, true, sb); + } + + /** + * Append string representation of {@code DFP} value to {@link StringBuilder} {@code sb} + *
+ * Same as {@code sb.append(value.toFloatString());}, but more efficient. + * + * @param value {@code DFP64} argument + * @param decimalMark A decimal separator used to separate the integer part from the fractional part. + * @param sb {@link StringBuilder} instance to which the string representation of the {@code value} will be appended + * @return the value of 2nd argument ({@link StringBuilder} {@code sb}) + */ + public static StringBuilder floatAppendTo(@Decimal final long value, final char decimalMark, final StringBuilder sb) { + return JavaImpl.fastAppendToStringBuilder(value, decimalMark, true, sb); + } + + /** + * Implements {@link Decimal64#floatAppendTo(StringBuilder)}, adds null check; do not use directly. + * + * @param value DFP argument + * @param sb {@link StringBuilder} instance to which the string representation of the {@code value} will be appended + * @return .. + */ + @Deprecated + public static StringBuilder floatAppendToChecked(@Decimal final long value, final StringBuilder sb) { + checkNull(value); + return floatAppendTo(value, sb); + } + + /** + * Implements {@link Decimal64#floatAppendTo(StringBuilder)}, adds null check; do not use directly. + * + * @param value DFP argument + * @param decimalMark A decimal separator used to separate the integer part from the fractional part. + * @param sb {@link StringBuilder} instance to which the string representation of the {@code value} will be appended + * @return .. + */ + @Deprecated + public static StringBuilder floatAppendToChecked(@Decimal final long value, final char decimalMark, final StringBuilder sb) { + checkNull(value); + return floatAppendTo(value, decimalMark, sb); + } + /** * Try parse a dfp floating-point value from the given textual representation. *
@@ -2676,6 +2795,31 @@ public static String toScientificStringChecked(@Decimal final long value, final return toScientificString(value, decimalMark); } + /** + * Implements {@link Decimal64#toFloatString()}, adds null checks; do not use directly. + * + * @param value DFP argument + * @return .. + */ + @Deprecated + public static String toFloatStringChecked(@Decimal final long value) { + checkNull(value); + return toFloatString(value); + } + + /** + * Implements {@link Decimal64#toFloatString()}, adds null checks; do not use directly. + * + * @param value DFP argument + * @param decimalMark A decimal separator used to separate the integer part from the fractional part. + * @return .. + */ + @Deprecated + public static String toFloatStringChecked(@Decimal final long value, final char decimalMark) { + checkNull(value); + return toFloatString(value, decimalMark); + } + /** * Implements {@link Decimal64#equals(Decimal64)}, adds null checks; do not use directly. * diff --git a/java/dfp/src/main/java/com/epam/deltix/dfp/JavaImpl.java b/java/dfp/src/main/java/com/epam/deltix/dfp/JavaImpl.java index 5cfddd0..4a1dee4 100644 --- a/java/dfp/src/main/java/com/epam/deltix/dfp/JavaImpl.java +++ b/java/dfp/src/main/java/com/epam/deltix/dfp/JavaImpl.java @@ -367,7 +367,7 @@ public static long fromDecimalDouble(final double x) { // } // }; - public static Appendable appendToRefImpl(final long value, final char decimalMark, final Appendable appendable) throws IOException { + public static Appendable appendToRefImpl(final long value, final char decimalMark, final boolean floatStyle, final Appendable appendable) throws IOException { if (isNull(value)) { return appendable.append("null"); } @@ -419,8 +419,12 @@ public static Appendable appendToRefImpl(final long value, final char decimalMar } } - if (0 == partsCoefficient) - return appendable.append("0" + decimalMark + "0"); + if (0 == partsCoefficient) { + appendable.append('0'); + if (floatStyle) + appendable.append(decimalMark).append('0'); + return appendable; + } if (value < 0) appendable.append('-'); @@ -432,7 +436,8 @@ public static Appendable appendToRefImpl(final long value, final char decimalMar appendLongTo(partsCoefficient, appendable, digits); for (int i = 0; i < exponent; i += 1) appendable.append('0'); - appendable.append(decimalMark).append('0'); + if (floatStyle) + appendable.append(decimalMark).append('0'); } else if (digits + exponent > 0) { final long integralPart = partsCoefficient / POWERS_OF_TEN[-exponent]; final long fractionalPart = partsCoefficient % POWERS_OF_TEN[-exponent]; @@ -443,7 +448,8 @@ public static Appendable appendToRefImpl(final long value, final char decimalMar appendable.append('0'); appendLongTo(dropTrailingZeros(fractionalPart), appendable); } else { - appendable.append(decimalMark).append('0'); + if (floatStyle) + appendable.append(decimalMark).append('0'); } } else { appendable.append('0').append(decimalMark); @@ -496,7 +502,7 @@ private static char[] makeBcdTable(final int tenPowerMaxIndex) { return table; } - public static String fastToString(final long value, final char decimalMark) { + public static String fastToString(final long value, final char decimalMark, final boolean floatStyle) { if (isNull(value)) return "null"; @@ -548,8 +554,9 @@ public static String fastToString(final long value, final char decimalMark) { } } - if (partsCoefficient == 0) - return "0" + decimalMark + "0"; + if (partsCoefficient == 0) { + return !floatStyle ? "0" : ("0" + decimalMark + "0"); + } int exponent = partsExponent - EXPONENT_BIAS; @@ -557,8 +564,10 @@ public static String fastToString(final long value, final char decimalMark) { if (exponent >= 0) { int bi = buffer.length; - buffer[--bi] = '0'; - buffer[--bi] = decimalMark; + if (floatStyle) { + buffer[--bi] = '0'; + buffer[--bi] = decimalMark; + } for (int i = 0; i < exponent; ++i) buffer[--bi] = '0'; @@ -613,8 +622,13 @@ public static String fastToString(final long value, final char decimalMark) { while (buffer[be - 1] == '0') --be; - if (buffer[be - 1] == decimalMark && be < buffer.length) - buffer[be++] = '0'; + if (buffer[be - 1] == decimalMark) { + if (!floatStyle) { + --be; + } else if (be < buffer.length) { + buffer[be++] = '0'; + } + } return new String(buffer, bi, be - bi); @@ -738,7 +752,7 @@ public static String fastToScientificString(final long value, final char decimal } // Copy-paste of the fastToString - public static StringBuilder fastAppendToStringBuilder(final long value, final char decimalMark, final StringBuilder stringBuilder) { + public static StringBuilder fastAppendToStringBuilder(final long value, final char decimalMark, final boolean floatStyle, final StringBuilder stringBuilder) { if (isNull(value)) return stringBuilder.append("null"); @@ -790,8 +804,12 @@ public static StringBuilder fastAppendToStringBuilder(final long value, final ch } } - if (partsCoefficient == 0) - return stringBuilder.append('0').append(decimalMark).append('0'); + if (partsCoefficient == 0) { + stringBuilder.append('0'); + if (floatStyle) + stringBuilder.append(decimalMark).append('0'); + return stringBuilder; + } int exponent = partsExponent - EXPONENT_BIAS; @@ -799,8 +817,10 @@ public static StringBuilder fastAppendToStringBuilder(final long value, final ch if (exponent >= 0) { int bi = buffer.length; - buffer[--bi] = '0'; - buffer[--bi] = decimalMark; + if (floatStyle) { + buffer[--bi] = '0'; + buffer[--bi] = decimalMark; + } for (int i = 0; i < exponent; ++i) buffer[--bi] = '0'; @@ -855,8 +875,13 @@ public static StringBuilder fastAppendToStringBuilder(final long value, final ch while (buffer[be - 1] == '0') --be; - if (buffer[be - 1] == decimalMark && be < buffer.length) - buffer[be++] = '0'; + if (buffer[be - 1] == decimalMark) { + if (!floatStyle) { + --be; + } else if (be < buffer.length) { + buffer[be++] = '0'; + } + } return stringBuilder.append(buffer, bi, be - bi); @@ -1037,7 +1062,7 @@ protected MutableCharBuffer initialValue() { }; // Copy-paste of the fastToString - public static Appendable fastAppendToAppendable(final long value, final char decimalMark, final Appendable appendable) throws IOException { + public static Appendable fastAppendToAppendable(final long value, final char decimalMark, final boolean floatStyle, final Appendable appendable) throws IOException { if (isNull(value)) return appendable.append("null"); @@ -1089,8 +1114,12 @@ public static Appendable fastAppendToAppendable(final long value, final char dec } } - if (partsCoefficient == 0) - return appendable.append('0').append(decimalMark).append('0'); + if (partsCoefficient == 0) { + appendable.append('0'); + if (floatStyle) + appendable.append(decimalMark).append('0'); + return appendable; + } int exponent = partsExponent - EXPONENT_BIAS; @@ -1099,8 +1128,10 @@ public static Appendable fastAppendToAppendable(final long value, final char dec if (exponent >= 0) { int bi = buffer.length; - buffer[--bi] = '0'; - buffer[--bi] = decimalMark; + if (floatStyle) { + buffer[--bi] = '0'; + buffer[--bi] = decimalMark; + } for (int i = 0; i < exponent; ++i) buffer[--bi] = '0'; @@ -1155,8 +1186,13 @@ public static Appendable fastAppendToAppendable(final long value, final char dec while (buffer[be - 1] == '0') --be; - if (buffer[be - 1] == decimalMark && be < buffer.length) - buffer[be++] = '0'; + if (buffer[be - 1] == decimalMark) { + if (!floatStyle) { + --be; + } else if (be < buffer.length) { + buffer[be++] = '0'; + } + } return appendable.append(charBuffer.setRange(bi, be - bi)); diff --git a/java/dfp/src/test/java/com/epam/deltix/dfp/BigDecimalTest.java b/java/dfp/src/test/java/com/epam/deltix/dfp/BigDecimalTest.java index 9b55860..212bb9f 100644 --- a/java/dfp/src/test/java/com/epam/deltix/dfp/BigDecimalTest.java +++ b/java/dfp/src/test/java/com/epam/deltix/dfp/BigDecimalTest.java @@ -40,9 +40,7 @@ public void conversionToBigDecimalAndBack() { } private static void toBigDecimalAndBack(@Decimal final long aD64) { - String aStr = Decimal64Utils.toString(aD64); - if (aStr.endsWith(".0")) - aStr = aStr.substring(0, aStr.length() - 2); + final String aStr = Decimal64Utils.toString(aD64); final BigDecimal big = Decimal64Utils.toBigDecimal(aD64); @@ -96,10 +94,10 @@ private static void toDecimal64(final BigDecimal a) { final String aStr = a.toPlainString(); String bStr = Decimal64Utils.toString(b); - if (!aStr.contains(".") && (aStr.length() + 2) != bStr.length()) + if (!aStr.contains(".") && aStr.length() != bStr.length()) throw new RuntimeException("BigDecimal(=" + a + ") conversion to Decimal64 order mismatch."); - bStr = trimBackZerosAndDot(bStr.endsWith(".0") ? bStr.substring(0, bStr.length() - 2) : bStr); + bStr = trimBackZerosAndDot(Decimal64Utils.toString(b)); if (!bStr.isEmpty()) bStr = bStr.substring(0, bStr.length() - 1); // Remove rounded symbol diff --git a/java/dfp/src/test/java/com/epam/deltix/dfp/Decimal64Test.java b/java/dfp/src/test/java/com/epam/deltix/dfp/Decimal64Test.java index ae2fb54..1849e80 100644 --- a/java/dfp/src/test/java/com/epam/deltix/dfp/Decimal64Test.java +++ b/java/dfp/src/test/java/com/epam/deltix/dfp/Decimal64Test.java @@ -261,14 +261,14 @@ public void numberConversionTest() { @Test public void toStringTest() { - Assert.assertEquals("0.0", Decimal64.fromLong(0).toString()); - Assert.assertEquals("42.0", Decimal64.fromLong(42).toString()); - Assert.assertEquals(Integer.MAX_VALUE + ".0", Decimal64.fromInt(Integer.MAX_VALUE).toString()); - Assert.assertEquals(Integer.MIN_VALUE + ".0", Decimal64.fromInt(Integer.MIN_VALUE).toString()); - Assert.assertEquals(Integer.MAX_VALUE + ".0", Decimal64.fromLong(Integer.MAX_VALUE).toString()); - Assert.assertEquals(Integer.MIN_VALUE + ".0", Decimal64.fromLong(Integer.MIN_VALUE).toString()); - Assert.assertEquals(Integer.MAX_VALUE + ".0", Decimal64.fromFixedPoint(Integer.MAX_VALUE, 0).toString()); - Assert.assertEquals(Integer.MIN_VALUE + ".0", Decimal64.fromFixedPoint(Integer.MIN_VALUE, 0).toString()); + Assert.assertEquals("0", Decimal64.fromLong(0).toString()); + Assert.assertEquals("42", Decimal64.fromLong(42).toString()); + Assert.assertEquals(String.valueOf(Integer.MAX_VALUE), Decimal64.fromInt(Integer.MAX_VALUE).toString()); + Assert.assertEquals(String.valueOf(Integer.MIN_VALUE), Decimal64.fromInt(Integer.MIN_VALUE).toString()); + Assert.assertEquals(String.valueOf(Integer.MAX_VALUE), Decimal64.fromLong(Integer.MAX_VALUE).toString()); + Assert.assertEquals(String.valueOf(Integer.MIN_VALUE), Decimal64.fromLong(Integer.MIN_VALUE).toString()); + Assert.assertEquals(String.valueOf(Integer.MAX_VALUE), Decimal64.fromFixedPoint(Integer.MAX_VALUE, 0).toString()); + Assert.assertEquals(String.valueOf(Integer.MIN_VALUE), Decimal64.fromFixedPoint(Integer.MIN_VALUE, 0).toString()); Assert.assertEquals("123.456", Decimal64.fromDouble(123.456).toString()); Assert.assertEquals("123.4567", Decimal64.fromFixedPoint(1234567, 4).toString()); @@ -282,8 +282,8 @@ public void toStringTest() { Assert.assertEquals("Infinity", Decimal64.toString(Decimal64.POSITIVE_INFINITY)); Assert.assertEquals("-Infinity", Decimal64.toString(Decimal64.NEGATIVE_INFINITY)); - Assert.assertEquals("0.0", Decimal64.toString(Decimal64.ZERO)); - Assert.assertEquals("1000000.0", Decimal64.toString(Decimal64.MILLION)); + Assert.assertEquals("0", Decimal64.toString(Decimal64.ZERO)); + Assert.assertEquals("1000000", Decimal64.toString(Decimal64.MILLION)); Assert.assertEquals("0.01", Decimal64.toString(Decimal64.ONE_HUNDREDTH)); } } diff --git a/java/dfp/src/test/java/com/epam/deltix/dfp/Decimal64UtilsTest.java b/java/dfp/src/test/java/com/epam/deltix/dfp/Decimal64UtilsTest.java index e141f9f..1cd8e8a 100644 --- a/java/dfp/src/test/java/com/epam/deltix/dfp/Decimal64UtilsTest.java +++ b/java/dfp/src/test/java/com/epam/deltix/dfp/Decimal64UtilsTest.java @@ -414,15 +414,15 @@ public Long apply(Long aLong) { }, a, b, message); } }, - "-1.0", "-1.0", - "0.0", "-0.7", - "0.0", "-0.5", - "0.0", "-0.2", - "0.0", "0.0", - "1.0", "0.2", - "1.0", "0.5", - "1.0", "0.7", - "1.0", "1.0" + "-1", "-1.0", + "0", "-0.7", + "0", "-0.5", + "0", "-0.2", + "0", "0.0", + "1", "0.2", + "1", "0.5", + "1", "0.7", + "1", "1.0" ); @Decimal final long multiple = Decimal64Utils.parse("0.1"); @@ -441,17 +441,17 @@ public Long apply(Long x) { } }, "-0.1", "-0.10", - "0.0", "-0.07", - "0.0", "-0.05", - "0.0", "-0.02", - "0.0", "-0.02", - "0.0", "0.0", + "0", "-0.07", + "0", "-0.05", + "0", "-0.02", + "0", "-0.02", + "0", "0.0", "0.1", "0.02", "0.1", "0.05", "0.1", "0.07", "0.1", "0.10", - "1000.0", "999.9001", - "-1000.0", "-1000.0999" + "1000", "999.9001", + "-1000", "-1000.0999" ); } @@ -472,16 +472,16 @@ public Long apply(Long aLong) { }, a, b, message); } }, - "-1.0", "-1.0", - "-1.0", "-1.0", - "-1.0", "-0.7", - "-1.0", "-0.5", - "-1.0", "-0.2", - "0.0", "0.0", - "0.0", "0.2", - "0.0", "0.5", - "0.0", "0.7", - "1.0", "1.0" + "-1", "-1.0", + "-1", "-1.0", + "-1", "-0.7", + "-1", "-0.5", + "-1", "-0.2", + "0", "0.0", + "0", "0.2", + "0", "0.5", + "0", "0.7", + "1", "1.0" ); @Decimal final long multiple = Decimal64Utils.parse("0.1"); @@ -504,13 +504,13 @@ public Long apply(Long x) { "-0.1", "-0.05", "-0.1", "-0.02", "-0.1", "-0.02", - "0.0", "0.0", - "0.0", "0.02", - "0.0", "0.05", - "0.0", "0.07", + "0", "0.0", + "0", "0.02", + "0", "0.05", + "0", "0.07", "0.1", "0.10", - "1000.0", "1000.09", - "-1000.0", "-999.95" + "1000", "1000.09", + "-1000", "-999.95" ); } @@ -523,15 +523,15 @@ public void accept(String s1, String s2) { checkRoundTowardsZero(s1, s2); } }, - "-1.0", "-1.0", - "0.0", "-0.7", - "0.0", "-0.5", - "0.0", "-0.2", - "0.0", "0.0", - "0.0", "0.2", - "0.0", "0.5", - "0.0", "0.7", - "1.0", "1.0" + "-1", "-1.0", + "0", "-0.7", + "0", "-0.5", + "0", "-0.2", + "0", "0.0", + "0", "0.2", + "0", "0.5", + "0", "0.7", + "1", "1.0" ); } @@ -557,17 +557,17 @@ public Long apply(Long aLong) { }, a, b, message); } }, - "-1.0", "-1.0", - "-1.0", "-0.7", - "-1.0", "-0.5", - "0.0", "-0.2", - "0.0", "0.0", - "0.0", "0.2", - "1.0", "0.5", - "1.0", "0.7", - "1.0", "1.0", - "10000.0", "9999.5", - "-10000.0", "-9999.5" + "-1", "-1.0", + "-1", "-0.7", + "-1", "-0.5", + "0", "-0.2", + "0", "0.0", + "0", "0.2", + "1", "0.5", + "1", "0.7", + "1", "1.0", + "10000", "9999.5", + "-10000", "-9999.5" ); @Decimal final long multiple = Decimal64Utils.parse("0.1"); @@ -588,14 +588,14 @@ public Long apply(Long x) { "-0.1", "-0.10", "-0.1", "-0.07", "-0.1", "-0.05", - "0.0", "-0.02", - "0.0", "0.0", - "0.0", "0.02", + "0", "-0.02", + "0", "0.0", + "0", "0.02", "0.1", "0.05", "0.1", "0.07", "0.1", "0.10", - "1000.0", "999.95", - "-1000.0", "-999.95" + "1000", "999.95", + "-1000", "-999.95" ); } @@ -616,17 +616,17 @@ public Long apply(Long aLong) { }, a, b, message); } }, - "-1.0", "-1.0", - "-1.0", "-0.7", - "0.0", "-0.5", - "0.0", "-0.2", - "0.0", "0.0", - "0.0", "0.2", - "0.0", "0.5", - "1.0", "0.7", - "1.0", "1.0", - "10000.0", "9999.5", - "-10000.0", "-9999.5" + "-1", "-1.0", + "-1", "-0.7", + "0", "-0.5", + "0", "-0.2", + "0", "0.0", + "0", "0.2", + "0", "0.5", + "1", "0.7", + "1", "1.0", + "10000", "9999.5", + "-10000", "-9999.5" ); @Decimal final long multiple = Decimal64Utils.parse("0.1"); @@ -646,15 +646,15 @@ public Long apply(Long x) { }, "-0.1", "-0.10", "-0.1", "-0.07", - "0.0", "-0.05", - "0.0", "-0.02", - "0.0", "0.0", - "0.0", "0.02", - "0.0", "0.05", + "0", "-0.05", + "0", "-0.02", + "0", "0.0", + "0", "0.02", + "0", "0.05", "0.1", "0.07", "0.1", "0.10", - "1000.0", "999.95", - "-1000.0", "-999.95" + "1000", "999.95", + "-1000", "-999.95" ); } diff --git a/java/dfp/src/test/java/com/epam/deltix/dfp/FromDoubleTest.java b/java/dfp/src/test/java/com/epam/deltix/dfp/FromDoubleTest.java index 74f852e..d346591 100644 --- a/java/dfp/src/test/java/com/epam/deltix/dfp/FromDoubleTest.java +++ b/java/dfp/src/test/java/com/epam/deltix/dfp/FromDoubleTest.java @@ -99,10 +99,10 @@ public void accept(String s) { FromDoubleTest.checkDecimalDoubleConversion(s); } }, - "0.0", "NaN", "Infinity", "-Infinity", + "0", "NaN", "Infinity", "-Infinity", "1E0", "1000000000000000E0", "0.000000009412631", "0.95285752", - "9.2", "25107188000000000000000000000000000000000000000000000000.0", + "9.2", "25107188000000000000000000000000000000000000000000000000", "9.888888888888888", // Exponent limits "-1E-308", "-1000000000000000E-322" diff --git a/java/dfp/src/test/java/com/epam/deltix/dfp/JavaImplTest.java b/java/dfp/src/test/java/com/epam/deltix/dfp/JavaImplTest.java index 86bd64a..3807fa7 100644 --- a/java/dfp/src/test/java/com/epam/deltix/dfp/JavaImplTest.java +++ b/java/dfp/src/test/java/com/epam/deltix/dfp/JavaImplTest.java @@ -123,25 +123,25 @@ public void appendTo() throws IOException { final StringBuilder string = new StringBuilder(); string.setLength(0); - assertEquals("NaN", appendToRefImpl(Decimal64Utils.NaN, DECIMAL_MARK_DOT, string).toString()); + assertEquals("NaN", appendToRefImpl(Decimal64Utils.NaN, DECIMAL_MARK_DOT, false, string).toString()); string.setLength(0); - assertEquals("Infinity", appendToRefImpl(Decimal64Utils.POSITIVE_INFINITY, DECIMAL_MARK_DOT, string).toString()); + assertEquals("Infinity", appendToRefImpl(Decimal64Utils.POSITIVE_INFINITY, DECIMAL_MARK_DOT, false, string).toString()); string.setLength(0); - assertEquals("-Infinity", appendToRefImpl(Decimal64Utils.NEGATIVE_INFINITY, DECIMAL_MARK_DOT, string).toString()); + assertEquals("-Infinity", appendToRefImpl(Decimal64Utils.NEGATIVE_INFINITY, DECIMAL_MARK_DOT, false, string).toString()); string.setLength(0); - assertEquals("100000010000.0", JavaImpl.appendToRefImpl(Decimal64Utils.fromDouble(10000001E+04), DECIMAL_MARK_DOT, string).toString()); + assertEquals("100000010000", JavaImpl.appendToRefImpl(Decimal64Utils.fromDouble(10000001E+04), DECIMAL_MARK_DOT, false, string).toString()); string.setLength(0); - assertEquals("10000001.0", JavaImpl.appendToRefImpl(Decimal64Utils.fromDouble(10000001), DECIMAL_MARK_DOT, string).toString()); + assertEquals("10000001", JavaImpl.appendToRefImpl(Decimal64Utils.fromDouble(10000001), DECIMAL_MARK_DOT, false, string).toString()); string.setLength(0); - assertEquals("1000.0001", JavaImpl.appendToRefImpl(Decimal64Utils.fromDouble(10000001E-04), DECIMAL_MARK_DOT, string).toString()); + assertEquals("1000.0001", JavaImpl.appendToRefImpl(Decimal64Utils.fromDouble(10000001E-04), DECIMAL_MARK_DOT, false, string).toString()); string.setLength(0); - assertEquals("9.2", appendToRefImpl(Decimal64Utils.fromDecimalDouble(92E-01), DECIMAL_MARK_DOT, string).toString()); + assertEquals("9.2", appendToRefImpl(Decimal64Utils.fromDecimalDouble(92E-01), DECIMAL_MARK_DOT, false, string).toString()); } @Test @@ -217,12 +217,12 @@ public void toStringSpecialCase1() { @Test public void toStringSpecialCase2() { - checkToString(null, "31800000013474D8", "202150.0", "+20215000E-2"); + checkToString(null, "31800000013474D8", "202150", "+20215000E-2"); } @Test public void toStringSpecialCase3() { - checkToString(null, "8020000000000000", "0.0", "-0E-397"); + checkToString(null, "8020000000000000", "0", "-0E-397"); } @Test @@ -528,7 +528,7 @@ public void testToStringRandomly() throws Exception { private static void checkStrEq(final long value) { try { - final String inStr = JavaImpl.appendToRefImpl(value, DECIMAL_MARK_DEFAULT, new StringBuilder()).toString(); + final String inStr = JavaImpl.appendToRefImpl(value, DECIMAL_MARK_DEFAULT, false, new StringBuilder()).toString(); final String testStr = Decimal64Utils.toString(value); if (!inStr.equals(testStr)) throw new RuntimeException("Case toString(" + value + "L) error: ref toString(=" + inStr + ") != test toString(=" + testStr + ")"); @@ -1009,17 +1009,17 @@ public void issue84ToStringAsDouble() throws IOException { for (int shift = 0, f = 1; shift < 6; ++shift, f *= 10) { final long d64up = shift == 0 ? d64u : JavaImplMul.get_BID64(dParts.signMask, dParts.exponent - shift, dParts.coefficient * f); - assertEquals(str, Decimal64Utils.toString(d64up)); + assertEquals(str, Decimal64Utils.toFloatString(d64up)); { final CharArrayWriter writer = new CharArrayWriter(1024); - Decimal64Utils.appendTo(d64up, writer); + Decimal64Utils.floatAppendTo(d64up, writer); assertEquals(str, writer.toString()); } { final StringBuilder sb = new StringBuilder(); - Decimal64Utils.appendTo(d64up, sb); + Decimal64Utils.floatAppendTo(d64up, sb); assertEquals(str, sb.toString()); } @@ -1037,18 +1037,66 @@ public void issue84ToStringAsDoublePart2() throws IOException { final long d64Up = JavaImplMul.get_BID64(dParts.signMask, dParts.exponent - 6 - 6, dParts.coefficient * 1000_000); final String strUp = "0.001234"; - assertEquals(strUp, Decimal64Utils.toString(d64Up)); + assertEquals(strUp, Decimal64Utils.toFloatString(d64Up)); { final CharArrayWriter writer = new CharArrayWriter(1024); - Decimal64Utils.appendTo(d64Up, writer); + Decimal64Utils.floatAppendTo(d64Up, writer); assertEquals(strUp, writer.toString()); } { final StringBuilder sb = new StringBuilder(); - Decimal64Utils.appendTo(d64Up, sb); + Decimal64Utils.floatAppendTo(d64Up, sb); assertEquals(strUp, sb.toString()); } } + + private static class ToStringData { + public final Decimal64 testValue; + public final String normalOut; + public final String floatOut; + + public ToStringData(final Decimal64 testValue, final String normalOut, final String floatOut) { + this.testValue = testValue; + this.normalOut = normalOut; + this.floatOut = floatOut; + } + } + + @Test + public void issue91ToFloatString() throws IOException { + final ToStringData[] testCases = new ToStringData[] + { + new ToStringData(Decimal64.fromFixedPoint(14L, 0), "14", "14.0"), + new ToStringData(Decimal64.fromFixedPoint(140000000000L, 10), "14", "14.0"), + new ToStringData(Decimal64.ZERO, "0", "0.0") + }; + + for (ToStringData testCase : testCases) { + final Decimal64 testValue = testCase.testValue; + assertEquals(testCase.normalOut, testValue.toString()); + assertEquals(testCase.floatOut, testValue.toFloatString()); + + { + final StringBuilder sb = new StringBuilder(); + assertEquals(testCase.normalOut, testValue.appendTo(sb).toString()); + } + + { + final StringBuilder sb = new StringBuilder(); + assertEquals(testCase.floatOut, testValue.floatAppendTo(sb).toString()); + } + + { + final StringBuilder sb = new StringBuilder(); + assertEquals(testCase.normalOut, testValue.appendTo((Appendable) sb).toString()); + } + + { + final StringBuilder sb = new StringBuilder(); + assertEquals(testCase.floatOut, testValue.floatAppendTo((Appendable) sb).toString()); + } + } + } } diff --git a/java/dfpNativeTests/src/jmh/java/com/epam/deltix/dfp/FormattingBenchmark.java b/java/dfpNativeTests/src/jmh/java/com/epam/deltix/dfp/FormattingBenchmark.java index 025a855..711b694 100644 --- a/java/dfpNativeTests/src/jmh/java/com/epam/deltix/dfp/FormattingBenchmark.java +++ b/java/dfpNativeTests/src/jmh/java/com/epam/deltix/dfp/FormattingBenchmark.java @@ -39,26 +39,26 @@ public void setUp() { @Benchmark public Appendable appendToRefImplRet() throws IOException { stringBuilder.setLength(0); - JavaImpl.appendToRefImpl(decimalValue, DECIMAL_MARK_DEFAULT, stringBuilder); + JavaImpl.appendToRefImpl(decimalValue, DECIMAL_MARK_DEFAULT, false, stringBuilder); return stringBuilder; } @Benchmark public void appendToRefImplBlackHole(Blackhole bh) throws IOException { stringBuilder.setLength(0); - bh.consume(JavaImpl.appendToRefImpl(decimalValue, DECIMAL_MARK_DEFAULT, stringBuilder)); + bh.consume(JavaImpl.appendToRefImpl(decimalValue, DECIMAL_MARK_DEFAULT, false, stringBuilder)); } @Benchmark public void fastAppendToAppendable(Blackhole bh) throws IOException { stringBuilder.setLength(0); - bh.consume(JavaImpl.fastAppendToAppendable(decimalValue, DECIMAL_MARK_DEFAULT, stringBuilder)); + bh.consume(JavaImpl.fastAppendToAppendable(decimalValue, DECIMAL_MARK_DEFAULT, false, stringBuilder)); } @Benchmark public void fastAppendToStringBuilder(Blackhole bh) { stringBuilder.setLength(0); - bh.consume(JavaImpl.fastAppendToStringBuilder(decimalValue, DECIMAL_MARK_DEFAULT, stringBuilder)); + bh.consume(JavaImpl.fastAppendToStringBuilder(decimalValue, DECIMAL_MARK_DEFAULT, false, stringBuilder)); } @Benchmark @@ -83,7 +83,7 @@ public void appendJustDouble(Blackhole bh) { public void toStringJavaImpl(Blackhole bh) { for (int i = 0; i < decimalValues.length; ++i) { try { - bh.consume(JavaImpl.appendToRefImpl(decimalValues[i], DECIMAL_MARK_DEFAULT, new StringBuilder()).toString()); + bh.consume(JavaImpl.appendToRefImpl(decimalValues[i], DECIMAL_MARK_DEFAULT, false, new StringBuilder()).toString()); } catch (final Exception e) { throw new RuntimeException(e); } @@ -93,7 +93,7 @@ public void toStringJavaImpl(Blackhole bh) { @Benchmark public void toStringJavaFastImpl(Blackhole bh) { for (int i = 0; i < decimalValues.length; ++i) - bh.consume(JavaImpl.fastToString(decimalValues[i], DECIMAL_MARK_DEFAULT)); + bh.consume(JavaImpl.fastToString(decimalValues[i], DECIMAL_MARK_DEFAULT, false)); } @Benchmark diff --git a/native/src/NativeImpl.c b/native/src/NativeImpl.c index 3181e73..4394e55 100644 --- a/native/src/NativeImpl.c +++ b/native/src/NativeImpl.c @@ -43,6 +43,7 @@ OPN(tryParse, dfp64_try_parse(str, exception), const char* str, uint32* exceptio OPNR(to_string, const char*, dfp64_to_string(x), BID_UINT64 x) OPNR(to_string_2, const char*, dfp64_to_string_2(x, decimalMark), BID_UINT64 x, char decimalMark) OPNR(to_string_3, const char*, dfp64_to_string_3(x, decimalMark, buffer512), BID_UINT64 x, char decimalMark, char* buffer512) +OPNR(to_string_4, const char*, dfp64_to_string_4(x, decimalMark, buffer512, floatStyle), BID_UINT64 x, char decimalMark, char* buffer512, int32 floatStyle) OPNR(to_scientific_string, const char*, dfp64_to_scientific_string(x), BID_UINT64 x) OPNR(to_scientific_string_2, const char*, dfp64_to_scientific_string_2(x, decimalMark), BID_UINT64 x, char decimalMark) diff --git a/native/src/NativeImplToString.c b/native/src/NativeImplToString.c index bc26cce..4ac217b 100644 --- a/native/src/NativeImplToString.c +++ b/native/src/NativeImplToString.c @@ -121,6 +121,10 @@ const char* dfp64_to_string_2(BID_UINT64 value, char decimalMark) { } const char* dfp64_to_string_3(BID_UINT64 value, char decimalMark, char* buffer512) { + return dfp64_to_string_4(value, decimalMark, buffer512, false); +} + +const char* dfp64_to_string_4(BID_UINT64 value, char decimalMark, char* buffer512, int floatStyle) { if (isNull(value)) return "null"; @@ -136,9 +140,13 @@ const char* dfp64_to_string_3(BID_UINT64 value, char decimalMark, char* buffer51 if (partsCoefficient == 0) { // return "0" + decimalMark + "0"; buffer512[0] = '0'; - buffer512[1] = decimalMark; - buffer512[2] = '0'; - buffer512[3] = '\0'; + if (!floatStyle) { + buffer512[1] = '\0'; + } else { + buffer512[1] = decimalMark; + buffer512[2] = '0'; + buffer512[3] = '\0'; + } return buffer512; } @@ -148,8 +156,10 @@ const char* dfp64_to_string_3(BID_UINT64 value, char decimalMark, char* buffer51 if (exponent >= 0) { int bi = bufferMinLength; - buffer512[--bi] = '0'; - buffer512[--bi] = decimalMark; + if (floatStyle) { + buffer512[--bi] = '0'; + buffer512[--bi] = decimalMark; + } for (int i = 0; i < exponent; ++i) buffer512[--bi] = '0'; @@ -161,7 +171,7 @@ const char* dfp64_to_string_3(BID_UINT64 value, char decimalMark, char* buffer51 while (buffer512[bi] == '0') ++bi; - if (value < 0) + if (partsSignMask) buffer512[--bi] = '-'; return buffer512 + bi; @@ -198,15 +208,21 @@ const char* dfp64_to_string_3(BID_UINT64 value, char decimalMark, char* buffer51 while (buffer512[bi] == '0') ++bi; - if (value < 0) + if (partsSignMask) buffer512[--bi] = '-'; int be = bufferMinLength; while (buffer512[be - 1] == '0') --be; - if (buffer512[be - 1] == decimalMark && be < bufferMinLength) - buffer512[be++] = '0'; + if (buffer512[be - 1] == decimalMark) { + if (!floatStyle) { + --be; + } + else if (be < bufferMinLength) { + buffer512[be++] = '0'; + } + } buffer512[be] = '\0'; return buffer512 + bi; @@ -228,7 +244,7 @@ const char* dfp64_to_string_3(BID_UINT64 value, char decimalMark, char* buffer51 buffer512[--bi] = decimalMark; buffer512[--bi] = '0'; - if (value < 0) + if (partsSignMask) buffer512[--bi] = '-'; int be = bufferMinLength; @@ -291,7 +307,7 @@ const char* dfp64_to_scientific_string_3(BID_UINT64 value, char decimalMark, cha buffer512[bi] = buffer512[bi + 1]; buffer512[bi + 1] = decimalMark; - if (value < 0) + if (partsSignMask) buffer512[--bi] = '-'; buffer512[be++] = 'e'; diff --git a/native/src/NativeImplToString.h b/native/src/NativeImplToString.h index 74822a2..459e13c 100644 --- a/native/src/NativeImplToString.h +++ b/native/src/NativeImplToString.h @@ -6,6 +6,7 @@ BID_EXTERN_C const char * dfp64_to_string(BID_UINT64 value); BID_EXTERN_C const char * dfp64_to_string_2(BID_UINT64 value, char decimalMark); BID_EXTERN_C const char * dfp64_to_string_3(BID_UINT64 value, char decimalMark, char* buffer512); +BID_EXTERN_C const char * dfp64_to_string_4(BID_UINT64 value, char decimalMark, char* buffer512, int floatStyle); BID_EXTERN_C const char * dfp64_to_scientific_string(BID_UINT64 value); BID_EXTERN_C const char * dfp64_to_scientific_string_2(BID_UINT64 value, char decimalMark);