Non-canonical Decimal128 serializes incorrectly

XMLWordPrintableJSON

    • Type: Bug
    • Resolution: Unresolved
    • Priority: Minor - P4
    • None
    • Affects Version/s: bson-6.10.4
    • Component/s: BSON
    • 2
    • None
    • None
    • None
    • None
    • None
    • None
    • None

      Here is a minimal example of the problem

      What happens: Decimal128.toString() produces a string with 35 digits of precision
      Expected behavior: Serialize to a string with a maximum 34 digits of precision

      Effects of the problem:

      • Unable to reconstruct a Decimal128 from the string
      • The string representation is actually incorrect because Decimal128 is specifically designed to represent 34 digits, not 35. There is no 35th digit represented, so the final digit is necessarily wrong/nonexistent
      import { Decimal128 } from "bson";
      
      const assertions = []
      
      function removeDecimalPoint(str: string): string {
        return str.replace(".", "");
      }
      
      function truncateByOne(str: string): string {
        return str.slice(0, -1);
      }
      
      const bytes = Buffer.from("b2d734fc610c88c439e6222a4fed0330", "hex");
      const d128 = new Decimal128(bytes);
      const str = d128.toString();
      
      
      
      function constructs(str: string): Decimal128 | Error {
        try {
          return Decimal128.fromString(str);
        } catch (e) {
          return e;
        }
      }
      
      assertions.push(str === "1000.5500000000000682121026329696178")
      const constructionResult = constructs(str) as Error;
      assertions.push(constructionResult instanceof Error)
      const errorMessage = (constructionResult as Error).message;
      assertions.push(errorMessage === "\"1000.5500000000000682121026329696178\" is not a valid Decimal128 string - inexact rounding")
      
      //The decimal128 format supports 34 decimal digits of significand - https://en.wikipedia.org/wiki/Decimal128_floating-point_format#Format
      assertions.push(removeDecimalPoint(str).length === 35)
      
      // It works with one digit removed
      const truncatedStr = truncateByOne(str);
      assertions.push(truncatedStr === "1000.550000000000068212102632969617") // missing final 8
      const truncatedConstructionResult = constructs(truncatedStr) as Decimal128;
      assertions.push(truncatedConstructionResult instanceof Decimal128);
      
      let i = 1;
      for (const assertion of assertions) {
        if (!assertion) {
          // This won't happen. Assertions will pass
          console.error(`❌ Assertion ${i} failed`);
          process.exit(1);
        }
        i++;
      }
      
      console.log("✅ All assertions passed");
      process.exit(0);
      

            Assignee:
            Unassigned
            Reporter:
            Ben Thayer
            None
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated: