From 30bce5a0c6e73ce8b3b6fc0adc37a63697ec3472 Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Sun, 11 Apr 2021 13:58:53 -0500 Subject: [PATCH 1/2] Decimal storage and string conversion works. --- lib/pure/decimal.nim | 1040 +++++++++++++++++++++++++ tests/stdlib/decimal/helpers.nim | 32 + tests/stdlib/decimal/tbid_storage.nim | 345 ++++++++ tests/stdlib/decimal/tconversion.nim | 175 +++++ 4 files changed, 1592 insertions(+) create mode 100644 lib/pure/decimal.nim create mode 100644 tests/stdlib/decimal/helpers.nim create mode 100644 tests/stdlib/decimal/tbid_storage.nim create mode 100644 tests/stdlib/decimal/tconversion.nim diff --git a/lib/pure/decimal.nim b/lib/pure/decimal.nim new file mode 100644 index 0000000000000..441473cc00fa9 --- /dev/null +++ b/lib/pure/decimal.nim @@ -0,0 +1,1040 @@ +##[ + The ``decimal`` module supports the storage of numbers in decimal format. + + The primary benefit of this is it avoids conversion errors when converting to/from + decimal (base 10) and binary (base 2). The is critical for applications where + the numbers are start out as decimal, must be output as decimal, and even minor + error are problematic. Financial and scientific lab programs, for example, often + meet this requirement. + + As a secondary benefit, this library also honors the "significance" of a number + and properly handles significance during mathematic operations. + + Examples + ======== + .. code-block:: nim + import decimal + + Parsing + ======= + blah blah blah + +]## + +import strutils except strip +import strformat +import unicode + +# +# Type definitions +# + +# public types +type + Decimal* = object of RootObj + a: uint32 + b: uint32 + c: uint32 + d: uint32 + +# private types +type + DecimalKind = enum + # internal use: the state of the Decimal128 variable + dkValued, + dkInfinite, + dkNaN + SignificandArray = array[34, byte] + TempSignificandArray = array[70, byte] # (34+1)*2 + Quotient = tuple[a: uint32, b: uint32, c: uint32, d: uint32] + +# +# constants +# + +# private constants +const + significandSize: int = 34 + bias: int16 = 6176 + expLowerBound: int16 = 0 - bias + tempSignificandSize: int = 70 + billion: uint32 = 1000 * 1000 * 1000 # a billion has 10 digits and fits into 32 bits + tenThousand: uint16 = 10 * 1000 # 10 thousand has 5 digits and fits into 16 bits + allZeroes: SignificandArray = [0.byte, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + transientOffset = tempSignificandSize - significandSize + noValue: int16 = -32768 + + +# masks (private) +const + # all flags/exponents are in the first 32 bits ('a'), so they + # are represented by the uint32 + # + # the exponent fits into 14 bits, but the mask determines the prefix, so you + # really only have a range of 3 x 2^12 = 12288. So the bias of 6176 splits + # that in half, thus fitting the official range of -6143 to +6144. + signMask = 0b1000_0000_0000_0000_0000_0000_0000_0000'u32 + # + comboMask = 0b0111_1111_1111_1111_1000_0000_0000_0000'u32 + # + comboShortMask = 0b0110_0000_0000_0000_0000_0000_0000_0000'u32 + comboShortSignalsMedium = 0b0110_0000_0000_0000_0000_0000_0000_0000'u32 + comboShortExponentMask = 0b0111_1111_1111_1110_0000_0000_0000_0000'u32 # 14 bits + comboShortExponentShiftR = 16 + 1 + # + # medium is used when the leading bit in the significand happens to be 1 + comboMediumMask = 0b0111_1000_0000_0000_0000_0000_0000_0000'u32 + comboMediumSignalsLong = 0b0111_1000_0000_0000_0000_0000_0000_0000'u32 + comboMediumExponentMask = 0b0001_1111_1111_1111_1000_0000_0000_0000'u32 + comboMediumExponentShiftR = 15 + # + comboLongMask = 0b0111_1100_0000_0000_0000_0000_0000_0000'u32 + comboLongInfinityFlag = 0b0111_1000_0000_0000_0000_0000_0000_0000'u32 + comboLongNanFlag = 0b0111_1100_0000_0000_0000_0000_0000_0000'u32 + comboLongSignalingNanFlag = 0b0000_0010_0000_0000_0000_0000_0000_0000'u32 + # + # NOTE: the spec allows for sig combo bits and we are following BID variant. + # + # the largest significand is: + # 9999999999999999999999999999999999 (decimal) or + # 0001ed09_bead87c0_378d8e63_ffffffff (hex) + # which fits neatly into 113 bits. So, the masks for the first 32 bits is: + significandMaskUpper = 0b0000_0000_0000_0001_1111_1111_1111_1111'u32 + significandMediumBit = 0b0000_0000_0000_0001_0000_0000_0000_1111'u32 + # + allOnes = 0b1111_1111_1111_1111_1111_1111_1111_1111'u32 + +when not defined(js): + const + upperOnes = 0xFFFFFFFF00000000'u64 + lowerOnes = 0x00000000FFFFFFFF'u64 + + +# public constants +let + nan* = Decimal(a: comboLongNanFlag, b:0, c:0, d:0) + infinity* = Decimal(a: comboLongInfinityFlag, b: 0, c:0, d:0) + negativeInfinity* = Decimal(a: (comboLongInfinityFlag or signMask), b: 0, c:0, d:0) + +# +# private helpers +# + +proc shiftDecimalsLeftWithZero(values: SignificandArray, shiftNeeded: int16): SignificandArray = + for index in 0 ..< significandSize: + result[index] = values[index] + for _ in 0 ..< shiftNeeded: + for index in 0 ..< (significandSize - 1): + result[index] = result[index + 1] + result[33] = 0.byte + + +proc shiftDecimalsLeftTransientWithZero(values: TempSignificandArray, shiftNeeded: int16): TempSignificandArray = + for index in 0 ..< tempSignificandSize: + result[index] = values[index] + for _ in 0 ..< shiftNeeded: + for index in 0 ..< (tempSignificandSize - 1): + result[index] = result[index + 1] + result[tempSignificandSize - 1] = 0.byte + + +proc shiftDecimalsRight(values: SignificandArray, shiftNeeded: int16): SignificandArray = + for index in 0 ..< significandSize: + result[index] = values[index] + for _ in 0 ..< shiftNeeded: + for index in 1 ..< significandSize: + let place = significandSize - index + result[place] = result[place - 1] + result[0] = 0.byte + + +proc shiftDecimalsRightTransient(values: TempSignificandArray, shiftNeeded: int16): TempSignificandArray = + for index in 0 ..< tempSignificandSize: + result[index] = values[index] + for _ in 0 ..< shiftNeeded: + for index in 1 ..< tempSignificandSize: + let place = tempSignificandSize - index + result[place] = result[place - 1] + result[0] = 0.byte + +proc digitCount(significand: SignificandArray): int = + # get the number of digits, ignoring the leading zeroes; + # special case: all zeroes results returns a result of zero + result = 0 + var nonZeroFound = false + for d in significand: + if d != 0.byte: + nonZeroFound = true + if nonZeroFound: + result += 1 + +proc digitCount(significand: TempSignificandArray): int = + # get the number of digits, ignoring the leading zeroes; + # special case: all zeroes results returns a result of zero + result = 0 + var nonZeroFound = false + for d in significand: + if d != 0.byte: + nonZeroFound = true + if nonZeroFound: + result += 1 + +proc toTempSignificandArray(original: SignificandArray): TempSignificandArray = + for index in 0 ..< tempSignificandSize: + let sIndex = index - transientOffset + if sIndex >= 0: + result[index] = original[sIndex] + else: + result[index] = 0.byte + +when not defined(js): + proc greaterOrEqualToOneBillion(val: Quotient): bool = + # is the number in the four 32-bit uints greater than 1_000_000_000 + if val.a != 0'u32: + result = true + elif val.b != 0'u32: + result = true + elif ((val.c.uint64 shl 32) or val.d.uint64) >= billion.uint64: + result = true + else: + result = false + + proc leftHalf(value: uint64): uint32 = + # get the left (most significant) half of a 64-bit uint + result = (value shr 32).uint32 + + proc setLeftHalf(value: var uint64, newValue: uint64) = + # when defined(js): + # value = (value and lowerOnes) shr 0 + # let temp = (newValue and lowerOnes) shl 32 # shift new value to left + # value = (value or temp) shr 0 # OR into place + # else: + value = value and lowerOnes + let temp = (newValue and lowerOnes) shl 32 # shift new value to left + value = value or temp # OR into place + + proc rightHalf(value: uint64): uint32 = + # get the right (least significant) half of a 64-bit uint + result = ((value and lowerOnes) shr 0).uint32 + + proc setRightHalf(value: var uint64, newValue: uint64) = + value = (value and upperOnes) shr 0 # wipe out the right + value = (value or (newValue and lowerOnes)) shr 0 + +proc divide(quotient: Quotient, divisor: uint32): (Quotient, uint32) = + when defined(js): + # TODO: write a version that only uses 32 bit uints + # For JS, this is needed for bitops + raise newException(Exception, "js support not written yet") + else: + var remainder = 0'u64 + var pending: tuple[left: uint64, right: uint64] + pending.left = (quotient.a.uint64 shl 32) or quotient.b.uint64 + pending.right = (quotient.c.uint64 shl 32) or quotient.d.uint64 + + remainder += pending.left.leftHalf() + pending.left.setLeftHalf(remainder div divisor) + remainder = remainder mod divisor + + remainder = remainder shl 32 + remainder += pending.left.rightHalf() + pending.left.setRightHalf(remainder div divisor) + remainder = remainder mod divisor + + remainder = remainder shl 32 + remainder += pending.right.leftHalf() + pending.right.setLeftHalf(remainder div divisor) + remainder = remainder mod divisor + + remainder = remainder shl 32 + remainder += pending.right.rightHalf() + pending.right.setRightHalf(remainder div divisor) + remainder = remainder mod divisor + + var resultQuotient: Quotient + resultQuotient.a = pending.left.leftHalf() + resultQuotient.b = pending.left.rightHalf() + resultQuotient.c = pending.right.leftHalf() + resultQuotient.d = pending.right.rightHalf() + result = (resultQuotient, remainder.uint32) + +proc shouldRoundUpWhenBankersRoundingToEven(values: TempSignificandArray, keyDigitIndex: int): bool = + let keyDigit = values[keyDigitIndex] + var lastDigit = 0.byte + if keyDigitIndex > 0: + lastDigit = values[keyDigitIndex - 1] + # + var AllZeroesFollowingKeyDigit: bool = true + if keyDigitIndex < (tempSignificandSize - 1): + for index in (keyDigitIndex + 1) ..< tempSignificandSize: + if values[index] > 0.byte: + AllZeroesFollowingKeyDigit = false + # + if keyDigit < 5: # ...123[4]12 becomes ...123 + result = false + elif keyDigit > 5: # ...123[6]12 becomes ...124 + result = true + elif (keyDigit == 5) and (AllZeroesFollowingKeyDigit == false): # ...123[5]12 becomes ...124 + result = true + else: # keydigit == 5 and all zeroes followed the 5 + # let evenFlag = ((values[keyDigitIndex - 1] mod 2.byte) == 0.byte) # is the last digit (before the key digit) even? + let evenFlag = ((lastDigit mod 2.byte) == 0.byte) # is the last digit (before the key digit) even? + if evenFlag: + result = false # ...123[5]00 becomes ...124 + else: + result = true # ...122[5]00 becomes ...122 + + +# TODO: add rounding test suite +proc bankersRoundingToEven(values: TempSignificandArray, reduction: int): (SignificandArray, int) = + # Uses "bankers rounding" algorithm AKA "dutch rounding" and "round half to + # even", a standard used in both finance and statistics to avoid bias. + # + # https://en.wikipedia.org/wiki/Rounding#Round_half_to_even + # + # returns with the results fit into 34-digit array and actual reduction + var sig: SignificandArray + let origSignificance = digitCount(values) + if origSignificance == 0: + result = (allZeroes, 0) + else: + # + # gather info needed for the rounding decision + # + var newSignificance = origSignificance - reduction + var trimCount = reduction + if newSignificance > significandSize: + trimCount += (newSignificance - significandSize) + newSignificance = origSignificance - trimCount + if trimCount > 0: + # + # trimmed cut + # + let keyDigitIndex = tempSignificandSize - trimCount + let roundUpFlag = shouldRoundUpWhenBankersRoundingToEven(values, keyDigitIndex) + # + # build trimmed significand (and detect problematic all-nines scenario) + # + var allNines = true + for index in 0 ..< significandSize: + sig[index] = values[index + transientOffset - trimCount] + if sig[index] != 9.byte: + allNines = false + # + # adjust if all-nines + # + if allNines: + sig[0] = 0.byte + trimCount += 1 + # + # and do rounding + # + if roundUpFlag: + var index = significandSize - 1 # start with last digit + for counter in 0 ..< significandSize: + sig[index] += 1.byte + if sig[index] < 10: + break # done + else: + sig[index] = 0 # if it rounds up to "11", then set to zero and + index -= 1 # increment the previous digit + else: + # + # plain cut + # + for index in 0 ..< significandSize: + sig[index] = values[index + transientOffset - trimCount] + result = (sig, trimCount) + +proc trimToSignificand(values: TempSignificandArray): (SignificandArray, int) = + # Round from the temp array to a final array with rounding. + # + # A tuple is returned containing both the array and any exponential offset + # needed. + # + # Uses "bankers rounding" algorithm AKA "dutch rounding" and "round half to + # even", a standard used in both finance and statistics to avoid bias. + # + # https://en.wikipedia.org/wiki/Rounding#Round_half_to_even + result = bankersRoundingToEven(values, 0) + +proc generateDigits(src: Decimal, upper: uint32): SignificandArray = + # We are going to play a "trick" with the number "one billion" aka 1,000,000,000. That number has the following traits: + # + # * it fits into a 32-bit unsigned integer; we have a way of dividing a 113-bit (or 128-bit) number by a 32-bit number + # * when a big number is divided by a billion: + # - the remainder is the "bottom nine" digits of that number (which also fits in 32-bit number) + # - the quotient is a number representing the top part of that number + # + # The maximum digits allowed by decimal is 34. We will keep dividing the number by a billion until we have a + # quotient below one billion. Then, that quotient followed by the remainders represents the digits. + # + # here is an example showing the same idea but used with 100 (and two digits) to make it easier to visualize: + # + # starting number: 90230427 + # + # 90230427 / 100 = 902304 with remainder 27 # digits so far = "27" + # 902304 / 100 = 9023 with remainder 4 # digits so far = "0427" + # 9023 / 100 = 90 with remainder 23 # digits so far = "230427" + # + # 90 is less than 100, so the answer is "90" & "230427" which is "90230427" + # + when defined(js): + # TODO: rewrite a version of that only uses 32bit numbers for bitops for javascript + raise newException(Exception, "js support not written yet") + else: + var resultStr = "" + var quotient: Quotient = (upper, src.b, src.c, src.d) + if (quotient.a == 0) and (quotient.b == 0): + # these are the easy cases: + if quotient.c == 0: + resultStr = $quotient.d + else: + resultStr = $(((quotient.c.uint64 shl 32) + quotient.d.uint64) shr 0) + else: + var remainder = 0'u32 + while quotient.greaterOrEqualToOneBillion: + (quotient, remainder) = divide(quotient, billion) + resultStr = fmt"{remainder:09}" & resultStr + let quotientSmaller = quotient.d + resultStr = fmt"{quotientSmaller:09}" & resultStr + result = allZeroes + var index = 33 + for ch in resultStr.reversed: + result[index] = (ch.byte - '0'.byte) + index -= 1 + if index < 0: + break + +proc determineKindExponentAndUpper(d: Decimal): (DecimalKind, int16, uint32) = + var exponent = 0'i16 + var decType = dkValued + var upper = 0'u32 + when defined(js): + let combo = (d.a and comboMask) shr 0 + let comboShort = (combo and comboShortMask) shr 0 + if comboShort == comboShortSignalsMedium: + let comboMedium = (combo and comboMediumMask) shr 0 + if comboMedium == comboMediumSignalsLong: + # + # interpret long combo + # + let comboLong = (combo and comboLongMask) shr 0 + if comboLong == comboLongInfinityFlag: + decType = dkInfinite + else: + decType = dkNaN + else: + # + # interpret medium combo + # + exponent = (((comboMedium and comboMediumExponentMask) shr comboMediumExponentShiftR).int32 - bias).int16 + upper = (((d.a and significandMaskUpper) shr 0) or significandMediumBit) shr 0 + else: + # + # interpret short combo + # + exponent = (((combo and comboShortExponentMask) shr comboShortExponentShiftR).int32 - bias).int16 + upper = (d.a and significandMaskUpper) shr 0 + else: + let combo = d.a and comboMask + let comboShort = combo and comboShortMask + if comboShort == comboShortSignalsMedium: + let comboMedium = combo and comboMediumMask + if comboMedium == comboMediumSignalsLong: + # + # interpret long combo + # + let comboLong = combo and comboLongMask + if comboLong == comboLongInfinityFlag: + decType = dkInfinite + else: + decType = dkNaN + else: + # + # interpret medium combo + # + exponent = (((comboMedium and comboMediumExponentMask) shr comboMediumExponentShiftR).int32 - bias).int16 + upper = (d.a and significandMaskUpper) or significandMediumBit + else: + # + # interpret short combo + # + exponent = (((combo and comboShortExponentMask) shr comboShortExponentShiftR).int32 - bias).int16 + upper = d.a and significandMaskUpper + result = (decType, exponent, upper) + +when not defined(js): + proc multU64(left, right: uint64): (uint64, uint64) = + # + # multiply two unsigned 64bit numbers into an unsigned 128bit result + # + let leftHigh = (left shr 32) and lowerOnes + let leftLow = left and lowerOnes + let rightHigh = (right shr 32) and lowerOnes + let rightLow = right and lowerOnes + + var productHigh = leftHigh * rightHigh + var productMidA = leftHigh * rightLow + var productMidB = leftLow * rightHigh + var productLow = leftLow * rightLow + + productHigh += productMidA shr 32 + productMidA = (productMidA and lowerOnes) + productMidB + (productLow shr 32) + + productHigh += productMidA shr 32 + productLow = (productMidA shl 32) + (productLow and lowerOnes) + + result = (productHigh, productLow) + +proc createDecimal(negativeFlag: bool, significand: TempSignificandArray, startingExponent: int): Decimal = + when defined(js): + # TODO: write a version that only uses 32 bit uints + # For JS, this is needed for bitops + raise newException(Exception, "js support not written yet") + else: + const hundredQuadrillion: uint64 = 100_000_000_000_000_000'u64 # 1 followed by 17 zeroes + var exponent = startingExponent + + var (digits, expAdjustment) = trimToSignificand(significand) + exponent += expAdjustment + # + # calculate the binary value of the upper/left-hand 17 digits and the lower/right-hand 17 digits + # + var upperPart: uint64 = 0 + var lowerPart: uint64 = 0 + var power: uint64 = hundredQuadrillion + for index in 0 ..< 17: + power = power div 10 + if digits[index] != 0: # to speed things up, we avoid multiplication by zero + upperPart += digits[index].uint64 * power + if digits[index + 17] != 0: + lowerPart += (digits[index + 17].uint64 * power) shr 0 + if upperPart == 0.uint64: + # + # if the answer fits into the lower 17 digits, we are done already + # + result.a = 0'u32 + result.b = 0'u32 + result.c = (lowerPart shr 32).uint32 + result.d = (lowerPart and allOnes).uint32 + else: + # + # move into 128 bits (which, at 34 decimals, will fit into 113 bits) + # + # four digit equivalent of algorithm using bytes and 100 as splitter: + # digits = [9, 4, 8, 7] + # (upperPart, lowerPart) = (94, 87) + # (upperBig, lowerBig) = 94 * 100 = 9400 = 0x03AE = (03, AE) = (3, 174) + # lowerFinal = (87 + 174) mod 256 = 5 # unsigned ints do modulo automatically + # upperFinal = upperBig + # if 5 < 174 or 5 < 87: + # upperFinal += 1 # aka "carry the one" + let (upperBig, lowerBig) = multU64(upperPart, hundredQuadrillion) + var lowerFinal = lowerPart + lowerBig + + var upperFinal = upperBig + if (lowerFinal < lowerBig) or (lowerFinal < lowerPart): + upperFinal += 1.uint64 # "carry the one" (in binary) + + result.a = (upperFinal shr 32).uint32 + result.b = (upperFinal and allOnes).uint32 + result.c = (lowerFinal shr 32).uint32 + result.d = (lowerFinal and allOnes).uint32 + # + # set combo bits + # + if negativeFlag: + result.a = result.a or signMask + # unless invoking the medium/long combo (11), the exponent will overlay 00, 01, or 10 bits 1, 2 + # in other works, an in-range exponent will never have 11 in those two bits; which is why 11 is a flag + let exponentMask = (exponent + bias.int).uint32 shl comboShortExponentShiftR + result.a = result.a or exponentMask + +proc parseFromString(str: string): (DecimalKind, bool, TempSignificandArray, int) = + # used internally to parse a decimal string into a temporarary tuple value. + type + ParseState = enum + psPre, # we haven't found the number yet + psLeadingZeroes, # we are ignoring any leading zero(s) + psLeadingMinus, # we found a minus sign + psIntCoeff, # we are reading the the integer part of a decimal number (NN.nnn) + psDecimalPoint, # we found a single decimal point + psFracCoeff, # we are reading the decimals of a decimal number (nn.NNN) + psSignForExp, # we are reading the +/- of an exponent + psExp, # we are reading the decimals of an exponent + psDone # ignore everything else + + const + IGNORED_CHARS: seq[char] = @[ + '_', + ',' + ] + + var significand: TempSignificandArray + var negative: bool = false + var exponent: int = 0 + + let s = str.toLower().strip() + + if s.startsWith("nan"): + result = (dkNaN, false, significand, exponent) + return + + if s.startsWith("+inf"): + result = (dkInfinite, false, significand, exponent) + return + if s.startsWith("inf"): + result = (dkInfinite, false, significand, exponent) + return + if s.startsWith("-inf"): + result = (dkInfinite, true, significand, exponent) + return + + var state: ParseState = psPre + var legit = false + var digitList: seq[byte] = @[] + var expDigitList = "" + var expNegative = false + + for ch in s: + # + # detect change first + # + case state: + of psPre: + if ch == '-': + state = psLeadingMinus + elif ch == '0': + state = psLeadingZeroes + elif DIGITS.contains(ch): + state = psIntCoeff + elif ch == '.': # yes, we are allowing numbers like ".123" even though that is bad form + state = psDecimalPoint + of psLeadingMinus: + if ch == '0': + state = psLeadingZeroes + elif DIGITS.contains(ch): + state = psIntCoeff + elif ch == '.': # yes, we are allowing numbers like "-.123" even though that is bad form + state = psDecimalPoint + else: + state = psDone # anything else is not legit + of psLeadingZeroes: + if ch == '0': + discard + elif DIGITS.contains(ch): + state = psIntCoeff + elif ch == '.': + state = psDecimalPoint + elif ch == 'e': + state = psSignForExp + else: + state = psDone + of psIntCoeff: + if DIGITS.contains(ch): + discard + elif IGNORED_CHARS.contains(ch): + discard + elif ch == '.': + state = psDecimalPoint + elif ch == 'e': + state = psSignForExp + else: + state = psDone + of psDecimalPoint: + if DIGITS.contains(ch): + state = psFracCoeff + else: + state = psDone + of psFracCoeff: + if DIGITS.contains(ch): + discard + elif IGNORED_CHARS.contains(ch): + discard + elif ch == 'e': + state = psSignForExp + else: + state = psDone + of psSignForExp: + if DIGITS.contains(ch): + state = psExp + elif (ch == '-') or (ch == '+'): + discard + else: + state = psDone + of psExp: + if DIGITS.contains(ch): + discard + else: + state = psDone + of psDone: + discard + # + # act on state + # + case state: + of psPre: + discard + of psLeadingMinus: + negative = true + of psLeadingZeroes: + legit = true + of psIntCoeff: + # given the state table, the 'find' function should never return -1 + if not IGNORED_CHARS.contains(ch): + digitList.add(DIGITS.find(ch).byte) + legit = true + of psDecimalPoint: + discard + of psFracCoeff: + # given the state table, the 'find' function should never return -1 + if not IGNORED_CHARS.contains(ch): + digitList.add(DIGITS.find(ch).byte) + exponent -= 1 + legit = true + of psSignForExp: + if ch == '-': + expNegative = true + of psExp: + expDigitList &= ch + of psDone: + discard + # + # remove leading zeroes + # + var nonZeroFound = false + var temp: seq[byte] = @[] + for val in digitList: + if val != 0.byte: + nonZeroFound = true + if nonZeroFound: + temp.add val + digitList = temp + # + # if too many digits, removing trailing digits. + # Note: because this is on 70-digit Transient, simple "truncation" is good enough + # + if digitList.len > tempSignificandSize: + let digitsToRemove = digitList.len - tempSignificandSize + digitList = digitList[0 ..< tempSignificandSize] + exponent += digitsToRemove + # + # move to result to final significand + # + let offset = tempSignificandSize - digitList.len + for index, val in digitList: + significand[index + offset] = val + # + # parse the exponent value + # + if expDigitList.len > 0: + try: + let exp = parseInt(expDigitList) + if expNegative: + exponent -= exp + else: + exponent += exp + if exponent < expLowerBound: + let shiftNeeded = (expLowerBound - exponent).int16 + significand = shiftDecimalsRightTransient(significand, shiftNeeded) + exponent += shiftNeeded + except: + discard + # + result = (dkValued, negative, significand, exponent) + +# +# attributes +# + +proc negative*(number: Decimal): bool = + when defined(js): + if (number.a and signMask) shr 0 == signMask: + result = true + else: + result = false + else: + if (number.a and signMask) == signMask: + result = true + else: + result = false + +proc significance*(number: Decimal): int = + ## Get the precise number of significant digits in the decimal number. + ## + ## If a real number, then it will be a number between 1 and 34. Even a value of "0" has + ## one digit of Precision. + ## + ## A zero is returned if the number is not-a-number (NaN) or Infinity. + let (decKind, exponent, upper) = determineKindExponentAndUpper(number) + case decKind: + of dkValued: + let digits = generateDigits(number, upper) + result = digitCount(digits) + if result == 0: # only a true zero value can generate this + if exponent < 0: + result = -exponent + else: + result = 1 + of dkInfinite: + result = 0 + of dkNaN: + result = 0 + +proc places*(number: Decimal): int = + ## Get the precise number of known digits after/before the decimal point. + ## Also referred to as "the number of decimal places" + ## + ## An integer has zero places. + ## + ## Some numbers can have negative places if the significance does not include + ## lesser whole digits. For example, an estimate of 45 million is 45E+6 + ## (or 4.5E+7) and has -6 places. + ## + ## Think of missing digits as Nulls (?). So, + ## + ## 1123.30 (aka 1123.30???????...) has 2 places + ## 1123.3 (aka 1123.3????????...) has 1 places + ## 1123 (aka 1123.?????????...) has 0 places + ## 1.1E+3 (aka 11??.?????????...) has -2 places + ## 1E+3 (aka 1???.?????????...) has -3 places + ## + ## Zero can also be given decimal places. + ## + ## 0 (aka 0.??????????...) has 0 places + ## 0.00000 (aka 0.00000?????...) has 5 places + ## + ## "0.00000" means the number is precisely zero to 5 decimals places. + ## + ## Infinite and NaN have no decimal places and will return a zero (0). + let (decKind, exponent, upper) = determineKindExponentAndUpper(number) + case decKind: + of dkValued: + result = -exponent + of dkInfinite: + result = 0 + of dkNaN: + result = 0 + +proc `places=`*(number: var Decimal, newValue: int) {.inline.} = + ## Change a Decimal to the supplied number decimal places. + ## + ## The scale must be a value from −6144 to +6143. + ## + ## A negative value means the significance is adjusted so that the + ## value is only accurate to the number of digits *before* the decimal place. + ## + ## For example: + ## var x = newDecimal("123.4") + ## x.places = -1 + ## assert $x == "12E1" # essentially, 120 rounded to the nearest ten. + ## + ## When NaN or Infinity is passed, the decimal is unchanged. + let (decKind, exponent, upper) = determineKindExponentAndUpper(number) + case decKind: + of dkValued: + if newValue == noValue: + return + let currentPlaces = -exponent + if currentPlaces != newValue: + let digits = generateDigits(number, upper) + var diff = (currentPlaces - newValue).int16 + if diff > 0: # take away precision + let temp = digits.toTempSignificandArray() + let (newDigits, roundingEffect) = bankersRoundingToEven(temp, diff) + number = createDecimal(number.negative, newDigits.toTempSignificandArray, exponent + roundingEffect) + else: + let significance = digitCount(digits) + let newSig = significance - diff + if newSig > significandSize: + raise newException( + ValueError, + "Too many decimal places ($1 places for $2). A 128-bit decimal can only hold 34 digits ($3 rose to $4)." + .format(newValue, $number, significance, newSig) + ) + else: + let newDigits = shiftDecimalsLeftWithZero(digits, -diff) + number = createDecimal(number.negative, newDigits.toTempSignificandArray, exponent + diff) + of dkInfinite: + discard + of dkNaN: + discard + + +# +# ops +# + +proc `~==`(left: Decimal, right: Decimal): bool = + ## Determines whether two numbers match in the context of their + ## least-common significance. + ## + ## For example: + ## + ## assert (1.1'm ~== 1.0'm ) == false + ## assert (1.0'm ~== 1.0'm ) == true + ## assert (1.0'm ~== 1.01'm) == true + ## assert (1.01'm ~== 1.0'm ) == true + ## assert (1.01'm ~== 1.00'm) == false + ## + ## Use this with care with currency or financial work: + ## + ## assert (5'm ~== 5.23'm) == true + ## assert (5.00'm ~== 5.23'm) == false + # let (leftKind, leftExponent, leftUpper) = determineKindExponentAndUpper(left) + # let (rightKind, rightExponent, rightUpper) = determineKindExponentAndUpper(right) + # case leftKind: + # of dkValued: + # if rightKind != dkValues: + # result = false + # return + # if (leftExponent > rightExponent): # these values are -(places) + # let stripped = right.strip(-leftExponent) + # result = left == stripped + # else: + # let stripped = left.strip(-rightExponent) + # result = stripped == right + # of dkInfinite: + # result = left == right + # of dkNaN: + # result = left == right + result = false # TODO + +# proc '<' + +# proc round # bankers rounding + +# proc roundUp + +# proc strip + +# +# assignment / input +# + +proc newDecimal*(numberString: string, places: int = noValue): Decimal = + var (decimalKind, negativeFlag, significand, exponent) = parseFromString(numberString) + case decimalKind: + of dkNaN: + result = nan + of dkInfinite: + if negativeFlag: + result = negativeInfinity + else: + result = infinity + of dkValued: + result = createDecimal(negativeFlag, significand, exponent) + result.places = places + +proc `'m`*(numberString: string, places: int = noValue): Decimal = + newDecimal(numberString, places) + +proc decodeHex*(hex: string): Decimal = + if len(hex) != 32: + raise newException(ValueError, "The Decimal data type takes 32 hex characters. This string has $1.".format(hex.len)) + var part = hex[0 .. 7] + result.a = fromHex[uint32](part) + part = hex[8 .. 15] + result.b = fromHex[uint32](part) + part = hex[16 .. 23] + result.c = fromHex[uint32](part) + part = hex[24 .. 31] + result.d = fromHex[uint32](part) + +# +# output +# + +proc simpleDigitStr(dList: SignificandArray): string = + var firstDigitSeen = false + for digit in dList: + if digit != 0.byte: + firstDigitSeen = true + if firstDigitSeen: + result &= $(digit.int) + if not firstDigitSeen: + result = "0" + +proc generateNumberString(number: Decimal, exponent: int16, upper: uint32): string = + # from the lower three uint32 in decimal and the masked upper uint32 (upper), + # derive a sequence of decimal bytes. + if number.negative: + result = "-" + else: + result = "" + + let digits = generateDigits(number, upper) + let justDigits = simpleDigitStr(digits) + + let scientificExponent = justDigits.len - 1 + exponent + if (scientificExponent < -6) or (exponent > 0): + # express with scientific notation + for index, ch in justDigits: + if index == 1: + result &= "." + result &= ch + result &= "E" + if scientificExponent >= 0: + result &= "+" + result &= $scientificExponent + elif exponent == 0: + # if zero decimal places, then it is a simple integer + result &= justDigits + else: + let significance = -exponent + let leadingZeroes = significance - justDigits.len + if leadingZeroes >= 0: + result &= "0." + result &= "0".repeat(leadingZeroes) + result &= justDigits + else: + let depth = -leadingZeroes + for index, ch in justDigits: + if index == depth: + result &= "." + result &= ch + +proc sci*(number: Decimal): string = + ## Express the Decimal value in Scientific Notation + var (decKind, exponent, upper) = determineKindExponentAndUpper(number) + case deckind: + of dkValued: + let digits = generateDigits(number, upper) + let justDigits = simpleDigitStr(digits) + let scientificExponent = justDigits.len - 1 + exponent + for index, ch in justDigits: + if index == 1: + result &= "." + result &= ch + result &= "E" + if scientificExponent >= 0: + result &= "+" + result &= $scientificExponent + of dkInfinite: + if number.negative: + result = "-Infinity" + else: + result = "Infinity" + of dkNaN: + result = "NaN" + +proc `$`*(number: Decimal): string = + ## Express the Decimal value as a canonical string + var (decKind, exponent, upper) = determineKindExponentAndUpper(number) + case deckind: + of dkValued: + result &= generateNumberString(number, exponent, upper) + of dkInfinite: + if number.negative: + result = "-Infinity" + else: + result = "Infinity" + of dkNaN: + result = "NaN" + +proc internalRepr*(number: Decimal): string = + result = "Decimal($1 $2 $3 $4)".format(number.a, number.b, number.c, number.d) + +proc toHex*(number: Decimal): string = + result = number.a.toHex & number.b.toHex & number.c.toHex & number.d.toHex diff --git a/tests/stdlib/decimal/helpers.nim b/tests/stdlib/decimal/helpers.nim new file mode 100644 index 0000000000000..32205e8e2b95d --- /dev/null +++ b/tests/stdlib/decimal/helpers.nim @@ -0,0 +1,32 @@ +import std/[decimal, strutils] + +template assertEquals*(actual, expected: untyped): untyped = + doAssert actual == expected, + "\n" & + " Got: " & $(actual) & "\n" & + "Expected: " & $(expected) & "\n" + +template assertEquals*(testNo, actual, expected: untyped): untyped = + doAssert actual == expected, + "\n" & + " Test: " & testNo & "\n" & + " Got: " & $(actual) & "\n" & + "Expected: " & $(expected) & "\n" + +proc conversionTest*(testNo: int, hexStr: string, canonicalStr: string, lossy: bool, nonCanonicalStrs: seq[string]) = + let upperHexStr = hexStr.toUpperAscii + let decoded = hexStr.decodeHex() + let s = $decoded + let test = $testNo + assertEquals(test & " decode", s, canonicalStr) + + let parsed = newDecimal(canonicalStr) + assertEquals(test & " parse canonical (" & canonicalStr & ")", parsed.toHex, upperHexStr) + # + var ctr = 0 + for nonCanonStr in nonCanonicalStrs: + let ncParsed = newDecimal(nonCanonStr) + assertEquals(test & " parse noncanonical " & $ctr, ncParsed.toHex, upperHexStr) + ctr += 1 + echo " test $1: $2 and $3 ... all good!".format(testNo, canonicalStr, hexStr) + diff --git a/tests/stdlib/decimal/tbid_storage.nim b/tests/stdlib/decimal/tbid_storage.nim new file mode 100644 index 0000000000000..3bab7c6e1a432 --- /dev/null +++ b/tests/stdlib/decimal/tbid_storage.nim @@ -0,0 +1,345 @@ +discard """ + targets: "c cpp" +""" + +import helpers + +# Internally, decimal is stored per the IEEE spec with the Binary Integer +# Decimal variant; https://en.wikipedia.org/wiki/Binary_integer_decimal +# +# these test ensure that the internal storage is being correctly built +# and interpreted. + +# Many original test cases adapted from: +# * https://github.com/mongodb/mongo-java-driver/blob/master/bson/src/test/unit/org/bson/types/Decimal128Test.java +# * ... + +var canonicalHexBin = "" +var canonicalStr = "" +var nonCanonicalStrs: seq[string] = @[] +var resultLossy = false + +canonicalHexBin = "7c000000000000000000000000000000" +canonicalStr = "NaN" +nonCanonicalStrs = @[ + "nan", + "nAn" +] +resultLossy = false +conversionTest(1, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +canonicalHexBin = "78000000000000000000000000000000" +canonicalStr = "Infinity" +nonCanonicalStrs = @[ + "infinity", + "+infinity", + "inf", + "+inf", + "infiniTY", + "inF" +] +resultLossy = false +conversionTest(2, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +canonicalHexBin = "f8000000000000000000000000000000" +canonicalStr = "-Infinity" +nonCanonicalStrs = @[ + "-infinity", + "-inf" +] +resultLossy = false +conversionTest(3, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +canonicalHexBin = "30400000000000000000000000000000" +canonicalStr = "0" +resultLossy = false +conversionTest(4, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "b0400000000000000000000000000000" +canonicalStr = "-0" +resultLossy = false +conversionTest(5, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "30400000000000000000000000000001" +canonicalStr = "1" +resultLossy = false +conversionTest(6, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "b0400000000000000000000000000001" +canonicalStr = "-1" +resultLossy = false +conversionTest(7, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "3040000000000000000000000000007B" +canonicalStr = "123" +resultLossy = false +conversionTest(8, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "304000000000000000000000000001C8" +canonicalStr = "456" +resultLossy = false +conversionTest(9, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "30400000000000000000000000000315" +canonicalStr = "789" +resultLossy = false +conversionTest(10, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "304000000000000000000000000003E7" +canonicalStr = "999" +resultLossy = false +conversionTest(11, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "304000000000000000000000000003E8" +canonicalStr = "1000" +resultLossy = false +conversionTest(12, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "304000000000000000000000000003FF" +canonicalStr = "1023" +resultLossy = false +conversionTest(13, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "30400000000000000000000000000400" +canonicalStr = "1024" +resultLossy = false +conversionTest(14, canonicalHexBin, canonicalStr, resultLossy, @[]) + +canonicalHexBin = "3040000000000000000000000098967F" +canonicalStr = "9999999" +resultLossy = false +conversionTest(15, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# nine nines (one less than 1 billion) +canonicalHexBin = "3040000000000000000000003B9AC9FF" +canonicalStr = "999999999" +resultLossy = false +conversionTest(16, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# one billion exactly +canonicalHexBin = "3040000000000000000000003B9ACA00" +canonicalStr = "1000000000" +resultLossy = false +conversionTest(17, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# one billion plus 1 +canonicalHexBin = "3040000000000000000000003B9ACA01" +canonicalStr = "1000000001" +resultLossy = false +conversionTest(18, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# 10 ^ 25 (more than 64 bits and lots of zeroes) +canonicalHexBin = "3040000000084595161401484A000000" +canonicalStr = "10000000000000000000000000" +resultLossy = false +conversionTest(19, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# all 34 digits filled +canonicalHexBin = "30403CDE6FFF9732DE825CD07E96AFF2" +canonicalStr = "1234567890123456789012345678901234" +nonCanonicalStrs = @[ + "+1234567890123456789012345678901234" +] +resultLossy = false +conversionTest(20, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +# 34 nines +canonicalHexBin = "3041ED09BEAD87C0378D8E63FFFFFFFF" +canonicalStr = "9999999999999999999999999999999999" +resultLossy = false +conversionTest(21, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# 34 nines and a zero +canonicalHexBin = "3044314DC6448D9338C15B09FFFFFFFF" +canonicalStr = "9.99999999999999999999999999999999E+34" +nonCanonicalStrs = @[ + "99999999999999999999999999999999990", + "9999999999999999999999999999999999E1" +] +resultLossy = false +conversionTest(22, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# 34 nines and a six +canonicalHexBin = "3044314DC6448D9338C15B0A00000000" +canonicalStr = "1.000000000000000000000000000000000E+35" +nonCanonicalStrs = @[ + "99999999999999999999999999999999996" +] +resultLossy = false +conversionTest(23, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Regular - 0.1 +canonicalHexBin = "303E0000000000000000000000000001" +canonicalStr = "0.1" +resultLossy = false +conversionTest(24, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Regular - 0.1234567890123456789012345678901234 +canonicalHexBin = "2FFC3CDE6FFF9732DE825CD07E96AFF2" +canonicalStr = "0.1234567890123456789012345678901234" +resultLossy = false +conversionTest(25, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Regular - Small +canonicalHexBin = "303400000000000000000000000004D2" +canonicalStr = "0.001234" +resultLossy = false +conversionTest(26, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Regular - Small with Trailing Zeros +canonicalHexBin = "302C0000000000000000000000BC4B20" +canonicalStr = "0.0012340000" +resultLossy = false +conversionTest(27, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Small with Max Significance (34 digits) +canonicalHexBin = "2FF83CD7450BE3F39FA2D32880000000" +canonicalStr = "0.001234000000000000000000000000000000" +nonCanonicalStrs = @[ + "0.001234000000000000000000000000000000E0", + "0.1234000000000000000000000000000000E-2", + "1.234000000000000000000000000000000E-3", + "0.0012340000000000000000000000000000000", # should "trim" back to 34 + "0.00123400000000000000000000000000000000000" # should "trim" back to 34 +] +resultLossy = false +conversionTest(28, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +# Regular - -0.0", +canonicalHexBin = "B03E0000000000000000000000000000" +canonicalStr = "-0.0" +resultLossy = false +conversionTest(29, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Regular - 2.000 +canonicalHexBin = "303A00000000000000000000000007D0" +canonicalStr = "2.000" +resultLossy = false +conversionTest(30, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Scientific - Tiniest +canonicalHexBin = "0001ED09BEAD87C0378D8E63FFFFFFFF" +canonicalStr = "9.999999999999999999999999999999999E-6143" +nonCanonicalStrs = @[ + "9.9999999999999999999999999999999999999999E-6143" # should "clamp" back to 34 nines +] +resultLossy = false +conversionTest(31, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +# Scientific - Tiny +canonicalHexBin = "00000000000000000000000000000001" +canonicalStr = "1E-6176" +resultLossy = false +conversionTest(32, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Scientific - Negative Tiny +canonicalHexBin = "80000000000000000000000000000001" +canonicalStr = "-1E-6176" +nonCanonicalStrs = @[ + "-10E-6177" +] +resultLossy = false +conversionTest(33, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +# Scientific - Adjusted Exponent Limit +canonicalHexBin = "2FF03CDE6FFF9732DE825CD07E96AFF2" +canonicalStr = "1.234567890123456789012345678901234E-7" +resultLossy = false +conversionTest(34, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Scientific - Fractional +canonicalHexBin = "B02C0000000000000000000000000064" +canonicalStr = "-1.00E-8" +nonCanonicalStrs = @[ + "-100E-10" +] +resultLossy = false +conversionTest(35, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +# Scientific - 0 with Exponent +canonicalHexBin = "5F200000000000000000000000000000" +canonicalStr = "0E+6000" +resultLossy = false +conversionTest(36, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Scientific - 0 with Negative Exponent +canonicalHexBin = "2B7A0000000000000000000000000000" +canonicalStr = "0E-611" +resultLossy = false +conversionTest(37, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Scientific - No Decimal with Signed Exponent +canonicalHexBin = "30460000000000000000000000000001" +canonicalStr = "1E+3" +nonCanonicalStrs = @[ + "1E3", + "1e3" +] +resultLossy = false +conversionTest(38, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +# Scientific - Trailing Zero +canonicalHexBin = "3042000000000000000000000000041A" +canonicalStr = "1.050E+4" +resultLossy = false +conversionTest(39, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Scientific - With Decimal +canonicalHexBin = "30420000000000000000000000000069" +canonicalStr = "1.05E+3" +resultLossy = false +conversionTest(40, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Scientific - Full +canonicalHexBin = "3040FFFFFFFFFFFFFFFFFFFFFFFFFFFF" +canonicalStr = "5192296858534827628530496329220095" +resultLossy = false +conversionTest(41, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Scientific - Large +canonicalHexBin = "5FFE314DC6448D9338C15B0A00000000" +canonicalStr = "1.000000000000000000000000000000000E+6144" +resultLossy = false +conversionTest(42, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Scientific - Largest +canonicalHexBin = "5FFFED09BEAD87C0378D8E63FFFFFFFF" +canonicalStr = "9.999999999999999999999999999999999E+6144" +resultLossy = false +conversionTest(43, canonicalHexBin, canonicalStr, resultLossy, @[]) + +# Non-Canonical Parsing - Long Decimal String +canonicalStr = "1E-999" +canonicalHexBin = "28720000000000000000000000000001" +nonCanonicalStrs = @[ + ".000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001" +] +resultLossy = false +conversionTest(44, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +# Non-Canonical Parsing - Long Significand with Exponent +canonicalHexBin = "305800000000029D42DA3A76F9E0D979" +canonicalStr = "1.2345689012345789012345E+34" +nonCanonicalStrs = @[ + "12345689012345789012345E+12" +] +resultLossy = false +conversionTest(45, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +# Exact rounding +canonicalHexBin = "37CC314DC6448D9338C15B0A00000000" +canonicalStr = "1.000000000000000000000000000000000E+999" +nonCanonicalStrs = @[ + "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" +] +resultLossy = false +conversionTest(46, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) + +# from "Clamped" check in other test suite, but ... corrected? +canonicalHexBin = "5FFE000000000000000000000000000A" +canonicalStr = "1.0E+6112" +nonCanonicalStrs = @[ + "10E6111" +] +resultLossy = false +conversionTest(47, canonicalHexBin, canonicalStr, resultLossy, nonCanonicalStrs) diff --git a/tests/stdlib/decimal/tconversion.nim b/tests/stdlib/decimal/tconversion.nim new file mode 100644 index 0000000000000..411abf8d85b12 --- /dev/null +++ b/tests/stdlib/decimal/tconversion.nim @@ -0,0 +1,175 @@ +discard """ + targets: "c cpp" +""" + +import std/decimal + +import helpers + +let refAThree = newDecimal("199") # 3 sig digits +let refANine = newDecimal("199.000000") # 9 sig digits + +let refBThree = newDecimal("0.123") +let refBNine = newDecimal("0.123000000") + +let refCThree = newDecimal("567E+6") +let refCNine = newDecimal("567_000_000") + +block: # string tests + let testAThree = newDecimal("199.") + + assertEquals(testAThree, refAThree) + assertEquals($testAThree, "199") + assertEquals(testAThree.sci, "1.99E+2") + assertEquals(testAThree.significance, 3) + assertEquals(testAThree.places, 0) + assertEquals(testAThree, 199'm) # this is actually a string test + assertEquals(testAThree, newDecimal("199.00", places=0)) + assertEquals(testAThree, 199.00'm(0)) + + let testANine = newDecimal("000199.000000") + + assertEquals(testANine, refANine) + assertEquals($testANine, "199.000000") + assertEquals(testANine.sci, "1.99000000E+2") + assertEquals(testANine.significance, 9) + assertEquals(testANine.places, 6) + assertEquals(testANine, newDecimal("199.00", places=6)) + assertEquals(testANine, 199.0'm(places=6)) + + let testBThree = newDecimal(".123") + + assertEquals(testBThree, refBThree) + assertEquals($testBThree, "0.123") + assertEquals(testBThree.sci, "1.23E-1") + assertEquals(testBThree.significance, 3) + assertEquals(testBThree.places, 3) + assertEquals(testBThree, newDecimal("0.12340", places=3)) + assertEquals(testBThree, 0.123444'm(places = 3)) + + let testBNine = newDecimal("0000.123000000") + + assertEquals(testBNine, refBNine) + assertEquals($testBNine, "0.123000000") + assertEquals(testBNine.sci, "1.23000000E-1") + assertEquals(testBNine.significance, 9) + assertEquals(testBNine.places, 9) + assertEquals(testBNine, newDecimal("0.123", places=9)) + assertEquals(testBNine, 0.12300'm(places=9)) + + let testCThree = newDecimal("0.567E9") + + assertEquals(testCThree, refCThree) + assertEquals($testCThree, "5.67E+8") + assertEquals(testCThree.sci, "5.67E+8") + assertEquals(testCThree.significance, 3) + assertEquals(testCThree.places, -6) + assertEquals(testCThree, newDecimal("5.670000E+8", places= -6)) + assertEquals(testCThree, 567000000'm(places= -6)) + + let testCNine = newDecimal("000000000567000000") + + assertEquals(testCNine, refCNine) + assertEquals($testCNine, "567000000") + assertEquals(testCNine, 567000000'm) + assertEquals(testCNine.sci, "5.67000000E+8") + assertEquals(testCNine.significance, 9) + assertEquals(testCNine.places, 0) + assertEquals(testCNine, newDecimal("5.67E8", places=0)) + + # TODO: add octal, binary, hex string conversions (sans 'E' exponent support) + +# block: # integer tests +# let testAThree = newDecimal(199) + +# assertEquals(testAThree, refAThree) +# assertEquals($testAThree, "199") +# assertEquals(testAThree.significance, 3) +# # assertEquals(testAThree.places, 0) +# # assertEquals(testAThree.getExponent, 0) +# # assertEquals(testAThree.toInt, 199) +# # assertEquals(testAThree, newDecimal(199,) places=0) + +# let testANine = newDecimal(199, significance=9) + +# assertEquals(testANine, refANine) +# assertEquals($testANine, "199.000000") +# assertEquals(testANine.significance, 9) +# # assertEquals(testANine.getExponent, 0) +# # assertEquals(testANine.places, 6) +# # assertEquals(testANine.toInt, 199) +# # assertEquals(testANine, newDecimal(199,) places=6) + +# let testCThree = newDecimal(567000000, significance=3) + +# assertEquals(testCThree, refCThree) +# assertEquals($testCThree, "5.67E+8") +# assertEquals(testCThree.significance, 3) +# # assertEquals(testCThree.places, 0) +# # assertEquals(testCThree.getExponent, 6) +# # assertEquals(testCThree.toInt, 567000000) +# assertEquals(testCThree, newDecimal(567000000,) places= -6) + +# let testCNine = newDecimal(567000000, scale=9) + +# assertEquals(testCNine, refCNine) +# assertEquals($testCNine, "567000000") +# assertEquals(testCNine.significance, 9) +# # assertEquals(testCNine.places, 0) +# # assertEquals(testCNine.getExponent, 0) +# # assertEquals(testCNine.toInt, 567000000) +# assertEquals(testCNine, newDecimal(567000000,) scale=0) + +# block: # test float conversion +# let testA = newDecimal(199.0) # floats don't store "zeroes after decimal point" + +# assertEquals(testA, refAThree) +# assertEquals($testA, "199") +# assertEquals(testA.significance, 3) +# # assertEquals(testA.places, 0) +# # assertEquals(testA.getExponent, 0) +# # assertEquals(testA.toFloat, 199.0) +# # assertEquals(testA.toFloat, 199'f) +# # assertEquals(testA.toFloat, 199.0000000) + +# let testB = newDecimal(0.123) + +# assertEquals($testB, "0.123") +# assertEquals(testB.significance, 3) +# # assertEquals(testB.places, 3) +# # assertEquals(testB.getExponent, 0) +# # assertEquals(testB.toFloat, 0.123) + +# let testC = newDecimal(567000000.0) + +# assertEquals($testC, "567000000") +# assertEquals(testC.significance, 9) +# assertEquals(testC.places, 0) +# # assertEquals(testC.getExponent, 0) +# # assertEquals(testC.toFloat, 567000000.0) + +# let testD = newDecimal(1.0 / 7.0) # this is a repeating number in binary float + +# assertEquals($testD, "0.1428571428571428") +# assertEquals(testD.significance, 16) +# assertEquals(testD.places, 16) +# # assertEquals(testD.getExponent, -16) +# # assertEquals(testD.toFloat, 0.1428571428571428) +# # doAssert testD.toFloat != (1.0 / 7.0) # due to base10/base2 conversions + +# let testE = newDecimal( -5000.0 * (1.0 / 5.0) ) + +# assertEquals($testE, "-1000") +# assertEquals(testE.significance, 4) +# # assertEquals(testE.places, 0) +# # assertEquals(testE.getExponent, 0) +# # assertEquals(testE.toFloat, -1000.0) +# # assertEquals(testE.toFloat, () -5000.0 * (1.0 / 5.0) ) + +# let testF = newDecimal(1.123) + +# assertEquals($testF, "1.123") +# assertEquals(testF.significance, 4) +# # assertEquals(testF.places, 3) +# # assertEquals(testF.getExponent, 0) +# # assertEquals(testF.toFloat, 1.123) From fc16b472f609ac168dea71fcc8d628ab2e4c99af Mon Sep 17 00:00:00 2001 From: John Dupuy Date: Sat, 29 May 2021 16:48:43 -0500 Subject: [PATCH 2/2] Minor changes based on review. --- lib/{pure => std}/decimal.nim | 84 ++++++++++++++-------------- tests/stdlib/decimal/tconversion.nim | 36 ++++++------ 2 files changed, 61 insertions(+), 59 deletions(-) rename lib/{pure => std}/decimal.nim (96%) diff --git a/lib/pure/decimal.nim b/lib/std/decimal.nim similarity index 96% rename from lib/pure/decimal.nim rename to lib/std/decimal.nim index 441473cc00fa9..cb0a435c4e4ef 100644 --- a/lib/pure/decimal.nim +++ b/lib/std/decimal.nim @@ -1,5 +1,5 @@ ##[ - The ``decimal`` module supports the storage of numbers in decimal format. + The `decimal` module supports the storage of numbers in decimal format. The primary benefit of this is it avoids conversion errors when converting to/from decimal (base 10) and binary (base 2). The is critical for applications where @@ -12,6 +12,13 @@ Examples ======== +]## +runnableExamples: + var a = newDecimal("1234.50") + doAssert a.places == 2 + doAssert a.significantDigitCount == 6 + doAssert a.sci == "1.23450E+3" +##[ .. code-block:: nim import decimal @@ -21,9 +28,8 @@ ]## -import strutils except strip -import strformat -import unicode +import std/strutils except strip +import std/[strformat, unicode] # # Type definitions @@ -31,7 +37,7 @@ import unicode # public types type - Decimal* = object of RootObj + Decimal* = object a: uint32 b: uint32 c: uint32 @@ -111,7 +117,7 @@ when not defined(js): # public constants -let +const nan* = Decimal(a: comboLongNanFlag, b:0, c:0, d:0) infinity* = Decimal(a: comboLongInfinityFlag, b: 0, c:0, d:0) negativeInfinity* = Decimal(a: (comboLongInfinityFlag or signMask), b: 0, c:0, d:0) @@ -564,19 +570,14 @@ proc parseFromString(str: string): (DecimalKind, bool, TempSignificandArray, int psSignForExp, # we are reading the +/- of an exponent psExp, # we are reading the decimals of an exponent psDone # ignore everything else - const - IGNORED_CHARS: seq[char] = @[ - '_', - ',' - ] + ignoredChars = ['_', ','] var significand: TempSignificandArray var negative: bool = false var exponent: int = 0 let s = str.toLower().strip() - if s.startsWith("nan"): result = (dkNaN, false, significand, exponent) return @@ -634,7 +635,7 @@ proc parseFromString(str: string): (DecimalKind, bool, TempSignificandArray, int of psIntCoeff: if DIGITS.contains(ch): discard - elif IGNORED_CHARS.contains(ch): + elif ignoredChars.contains(ch): discard elif ch == '.': state = psDecimalPoint @@ -650,7 +651,7 @@ proc parseFromString(str: string): (DecimalKind, bool, TempSignificandArray, int of psFracCoeff: if DIGITS.contains(ch): discard - elif IGNORED_CHARS.contains(ch): + elif ignoredChars.contains(ch): discard elif ch == 'e': state = psSignForExp @@ -682,14 +683,14 @@ proc parseFromString(str: string): (DecimalKind, bool, TempSignificandArray, int legit = true of psIntCoeff: # given the state table, the 'find' function should never return -1 - if not IGNORED_CHARS.contains(ch): + if not ignoredChars.contains(ch): digitList.add(DIGITS.find(ch).byte) legit = true of psDecimalPoint: discard of psFracCoeff: # given the state table, the 'find' function should never return -1 - if not IGNORED_CHARS.contains(ch): + if not ignoredChars.contains(ch): digitList.add(DIGITS.find(ch).byte) exponent -= 1 legit = true @@ -760,7 +761,7 @@ proc negative*(number: Decimal): bool = else: result = false -proc significance*(number: Decimal): int = +proc significantDigitCount*(number: Decimal): int = ## Get the precise number of significant digits in the decimal number. ## ## If a real number, then it will be a number between 1 and 34. Even a value of "0" has @@ -785,28 +786,28 @@ proc significance*(number: Decimal): int = proc places*(number: Decimal): int = ## Get the precise number of known digits after/before the decimal point. ## Also referred to as "the number of decimal places" - ## + ## ## An integer has zero places. - ## + ## ## Some numbers can have negative places if the significance does not include ## lesser whole digits. For example, an estimate of 45 million is 45E+6 ## (or 4.5E+7) and has -6 places. - ## + ## ## Think of missing digits as Nulls (?). So, - ## + ## ## 1123.30 (aka 1123.30???????...) has 2 places ## 1123.3 (aka 1123.3????????...) has 1 places ## 1123 (aka 1123.?????????...) has 0 places ## 1.1E+3 (aka 11??.?????????...) has -2 places ## 1E+3 (aka 1???.?????????...) has -3 places - ## + ## ## Zero can also be given decimal places. - ## + ## ## 0 (aka 0.??????????...) has 0 places ## 0.00000 (aka 0.00000?????...) has 5 places - ## + ## ## "0.00000" means the number is precisely zero to 5 decimals places. - ## + ## ## Infinite and NaN have no decimal places and will return a zero (0). let (decKind, exponent, upper) = determineKindExponentAndUpper(number) case decKind: @@ -821,10 +822,10 @@ proc `places=`*(number: var Decimal, newValue: int) {.inline.} = ## Change a Decimal to the supplied number decimal places. ## ## The scale must be a value from −6144 to +6143. - ## + ## ## A negative value means the significance is adjusted so that the ## value is only accurate to the number of digits *before* the decimal place. - ## + ## ## For example: ## var x = newDecimal("123.4") ## x.places = -1 @@ -869,19 +870,20 @@ proc `places=`*(number: var Decimal, newValue: int) {.inline.} = proc `~==`(left: Decimal, right: Decimal): bool = ## Determines whether two numbers match in the context of their ## least-common significance. - ## + ## ## For example: - ## - ## assert (1.1'm ~== 1.0'm ) == false - ## assert (1.0'm ~== 1.0'm ) == true - ## assert (1.0'm ~== 1.01'm) == true - ## assert (1.01'm ~== 1.0'm ) == true - ## assert (1.01'm ~== 1.00'm) == false - ## + runnableExamples: + assert (1.1'm ~== 1.0'm ) == false + assert (1.0'm ~== 1.0'm ) == true + assert (1.0'm ~== 1.01'm) == true + assert (1.01'm ~== 1.0'm ) == true + assert (1.01'm ~== 1.00'm) == false + ## ## Use this with care with currency or financial work: - ## - ## assert (5'm ~== 5.23'm) == true - ## assert (5.00'm ~== 5.23'm) == false + runnableExamples: + assert (5'm ~== 5.23'm) == true + assert (5.00'm ~== 5.23'm) == false + # # let (leftKind, leftExponent, leftUpper) = determineKindExponentAndUpper(left) # let (rightKind, rightExponent, rightUpper) = determineKindExponentAndUpper(right) # case leftKind: @@ -997,7 +999,7 @@ proc generateNumberString(number: Decimal, exponent: int16, upper: uint32): stri proc sci*(number: Decimal): string = ## Express the Decimal value in Scientific Notation - var (decKind, exponent, upper) = determineKindExponentAndUpper(number) + let (decKind, exponent, upper) = determineKindExponentAndUpper(number) case deckind: of dkValued: let digits = generateDigits(number, upper) @@ -1034,7 +1036,7 @@ proc `$`*(number: Decimal): string = result = "NaN" proc internalRepr*(number: Decimal): string = - result = "Decimal($1 $2 $3 $4)".format(number.a, number.b, number.c, number.d) + "Decimal($1 $2 $3 $4)".format(number.a, number.b, number.c, number.d) proc toHex*(number: Decimal): string = - result = number.a.toHex & number.b.toHex & number.c.toHex & number.d.toHex + number.a.toHex & number.b.toHex & number.c.toHex & number.d.toHex diff --git a/tests/stdlib/decimal/tconversion.nim b/tests/stdlib/decimal/tconversion.nim index 411abf8d85b12..6fb3ff73e2368 100644 --- a/tests/stdlib/decimal/tconversion.nim +++ b/tests/stdlib/decimal/tconversion.nim @@ -21,7 +21,7 @@ block: # string tests assertEquals(testAThree, refAThree) assertEquals($testAThree, "199") assertEquals(testAThree.sci, "1.99E+2") - assertEquals(testAThree.significance, 3) + assertEquals(testAThree.significantDigitCount, 3) assertEquals(testAThree.places, 0) assertEquals(testAThree, 199'm) # this is actually a string test assertEquals(testAThree, newDecimal("199.00", places=0)) @@ -32,7 +32,7 @@ block: # string tests assertEquals(testANine, refANine) assertEquals($testANine, "199.000000") assertEquals(testANine.sci, "1.99000000E+2") - assertEquals(testANine.significance, 9) + assertEquals(testANine.significantDigitCount, 9) assertEquals(testANine.places, 6) assertEquals(testANine, newDecimal("199.00", places=6)) assertEquals(testANine, 199.0'm(places=6)) @@ -42,7 +42,7 @@ block: # string tests assertEquals(testBThree, refBThree) assertEquals($testBThree, "0.123") assertEquals(testBThree.sci, "1.23E-1") - assertEquals(testBThree.significance, 3) + assertEquals(testBThree.significantDigitCount, 3) assertEquals(testBThree.places, 3) assertEquals(testBThree, newDecimal("0.12340", places=3)) assertEquals(testBThree, 0.123444'm(places = 3)) @@ -52,7 +52,7 @@ block: # string tests assertEquals(testBNine, refBNine) assertEquals($testBNine, "0.123000000") assertEquals(testBNine.sci, "1.23000000E-1") - assertEquals(testBNine.significance, 9) + assertEquals(testBNine.significantDigitCount, 9) assertEquals(testBNine.places, 9) assertEquals(testBNine, newDecimal("0.123", places=9)) assertEquals(testBNine, 0.12300'm(places=9)) @@ -62,7 +62,7 @@ block: # string tests assertEquals(testCThree, refCThree) assertEquals($testCThree, "5.67E+8") assertEquals(testCThree.sci, "5.67E+8") - assertEquals(testCThree.significance, 3) + assertEquals(testCThree.significantDigitCount, 3) assertEquals(testCThree.places, -6) assertEquals(testCThree, newDecimal("5.670000E+8", places= -6)) assertEquals(testCThree, 567000000'm(places= -6)) @@ -73,7 +73,7 @@ block: # string tests assertEquals($testCNine, "567000000") assertEquals(testCNine, 567000000'm) assertEquals(testCNine.sci, "5.67000000E+8") - assertEquals(testCNine.significance, 9) + assertEquals(testCNine.significantDigitCount, 9) assertEquals(testCNine.places, 0) assertEquals(testCNine, newDecimal("5.67E8", places=0)) @@ -84,27 +84,27 @@ block: # string tests # assertEquals(testAThree, refAThree) # assertEquals($testAThree, "199") -# assertEquals(testAThree.significance, 3) +# assertEquals(testAThree.significantDigitCount, 3) # # assertEquals(testAThree.places, 0) # # assertEquals(testAThree.getExponent, 0) # # assertEquals(testAThree.toInt, 199) # # assertEquals(testAThree, newDecimal(199,) places=0) -# let testANine = newDecimal(199, significance=9) +# let testANine = newDecimal(199, significantDigitCount=9) # assertEquals(testANine, refANine) # assertEquals($testANine, "199.000000") -# assertEquals(testANine.significance, 9) +# assertEquals(testANine.significantDigitCount, 9) # # assertEquals(testANine.getExponent, 0) # # assertEquals(testANine.places, 6) # # assertEquals(testANine.toInt, 199) # # assertEquals(testANine, newDecimal(199,) places=6) -# let testCThree = newDecimal(567000000, significance=3) +# let testCThree = newDecimal(567000000, significantDigitCount=3) # assertEquals(testCThree, refCThree) # assertEquals($testCThree, "5.67E+8") -# assertEquals(testCThree.significance, 3) +# assertEquals(testCThree.significantDigitCount, 3) # # assertEquals(testCThree.places, 0) # # assertEquals(testCThree.getExponent, 6) # # assertEquals(testCThree.toInt, 567000000) @@ -114,7 +114,7 @@ block: # string tests # assertEquals(testCNine, refCNine) # assertEquals($testCNine, "567000000") -# assertEquals(testCNine.significance, 9) +# assertEquals(testCNine.significantDigitCount, 9) # # assertEquals(testCNine.places, 0) # # assertEquals(testCNine.getExponent, 0) # # assertEquals(testCNine.toInt, 567000000) @@ -125,7 +125,7 @@ block: # string tests # assertEquals(testA, refAThree) # assertEquals($testA, "199") -# assertEquals(testA.significance, 3) +# assertEquals(testA.significantDigitCount, 3) # # assertEquals(testA.places, 0) # # assertEquals(testA.getExponent, 0) # # assertEquals(testA.toFloat, 199.0) @@ -135,7 +135,7 @@ block: # string tests # let testB = newDecimal(0.123) # assertEquals($testB, "0.123") -# assertEquals(testB.significance, 3) +# assertEquals(testB.significantDigitCount, 3) # # assertEquals(testB.places, 3) # # assertEquals(testB.getExponent, 0) # # assertEquals(testB.toFloat, 0.123) @@ -143,7 +143,7 @@ block: # string tests # let testC = newDecimal(567000000.0) # assertEquals($testC, "567000000") -# assertEquals(testC.significance, 9) +# assertEquals(testC.significantDigitCount, 9) # assertEquals(testC.places, 0) # # assertEquals(testC.getExponent, 0) # # assertEquals(testC.toFloat, 567000000.0) @@ -151,7 +151,7 @@ block: # string tests # let testD = newDecimal(1.0 / 7.0) # this is a repeating number in binary float # assertEquals($testD, "0.1428571428571428") -# assertEquals(testD.significance, 16) +# assertEquals(testD.significantDigitCount, 16) # assertEquals(testD.places, 16) # # assertEquals(testD.getExponent, -16) # # assertEquals(testD.toFloat, 0.1428571428571428) @@ -160,7 +160,7 @@ block: # string tests # let testE = newDecimal( -5000.0 * (1.0 / 5.0) ) # assertEquals($testE, "-1000") -# assertEquals(testE.significance, 4) +# assertEquals(testE.significantDigitCount, 4) # # assertEquals(testE.places, 0) # # assertEquals(testE.getExponent, 0) # # assertEquals(testE.toFloat, -1000.0) @@ -169,7 +169,7 @@ block: # string tests # let testF = newDecimal(1.123) # assertEquals($testF, "1.123") -# assertEquals(testF.significance, 4) +# assertEquals(testF.significantDigitCount, 4) # # assertEquals(testF.places, 3) # # assertEquals(testF.getExponent, 0) # # assertEquals(testF.toFloat, 1.123)