Skip to content

Instantly share code, notes, and snippets.

@nicolo-ribaudo
Last active September 17, 2024 09:44
Show Gist options
  • Save nicolo-ribaudo/27c6156cefe27cf488f028e0236dc667 to your computer and use it in GitHub Desktop.
Save nicolo-ribaudo/27c6156cefe27cf488f028e0236dc667 to your computer and use it in GitHub Desktop.

"numeric value with precision" V3

NOTE: The other file in the gist shows the same but using "significant digits" rather than "fractional digits". After trying to use it, I noticed that almost always we want to start counting fro mthe units and not from the most significant digit.

A "numeric with precision" is a pair consisting of a numeric value and an indication of how many of its digits are significant. The number of digits is counted starting from the units: 1 means the first digit after the dot, 0 means that we have an integer, -1 means that we have a multiple of 10.

It can be obtained from a numeric value (wether it's decimal, float, or bigint) by "decorating it":

new Decimal("123.45").withFractionalDigits(1);
123.45.withFractionalDigits(1);

For any numeric type, x.withFractionalDigits(y).toString() is equivalent to x.toFixed(y). There is also a x.withFractionalDigits(y).toLocaleString().

Given a "numeric with precision", it's possible to separately extract the numeric value that it contains and the associated precision:

123.41.withFractionalDigits(1).valueOf(); // 123.4
123.41.withFractionalDigits(1).numericObject; // Number { 123.4 }
123.41.withFractionalDigits(1).fractionalDigits; // 1

new Decimal("123.41").withFractionalDigits(-1).valueOf(); // TypeError (because decimal primitives are hidden)
new Decimal("123.41").withFractionalDigits(-1).numericObject; // Decimal { 12e1 }
new Decimal("123.41").withFractionalDigits(-1).fractionalDigits; // -1

"numeric with precision"s don't have any built-in arithmetic operations. Instead, developers must perform arithmetics on the underlying value, and then attatch the correct precision based on the error propagation theory that they are following.

When Intl.NumberFormat and Intl.PluralRules receive a "numeric with precision", they must default to using the encoded precision for minimumFractionalDigits and maximumFractionalDigits.

Open questions

Rounding

When doing .withFractionalDigits(1) of 0.123, it should probably be rounded to loose the 2 and the 3. How does this rounding happen? Like Math.round()? Or should it be configurable?

How should Intl detect that an object is a "numeric with precision"?

Can it check an internal slot? Should this use a protocol (obj[Symbol.numericAndPrecision]() -> { numericObject, fractionalDigits })? Using a protocol would allow developers to create their own "numeric with precision" implementations that automatically do error propagation the way they want them to.

Possible extensions

  • "Numeric with precision" can itself have a .withFractionalDigits() method, to reduce (or also expand?) the precision of the underlying value.
  • There can be a Decimal.parseWithFractionalDigits(), Number.parseWithFractionalDigits() that parse a number from a string and preserve the info about the precision, returning a "number with precision" object.

Example implementation

class NumericWithPrecision {
  #numericObject;
  #fractionalDigits;
  constructor(numericObject, fractionalDigits) {
    this.#numericObject = numericObject;
    this.#fractionalDigits = fractionalDigits;
  }

  get numericObject() { return this.#numericObject }
  get fractionalDigits() { return this.#fractionalDigits }

  valueOf() { return this.#numericObject.valueOf() }

  toString() {
    return this.#numericObject.toString() // do some adjustment to represent the precision properly, probably
  }
  
  toLocaleString(lang, options) {
    return this.#numericObject.toLocaleString(lang, {
      minimumFractionalDigits: this.#fractionalDigits,
      maximumFractionalDigits: this.#fractionalDigits,
      ...options
    });
  }
}

Decimal.prototype.withFractionalDigits = function (fractionalDigits) {
  return new NumericWithPrecision(this, fractionalDigits)
}

Number.prototype.withFractionalDigits = function (fractionalDigits) {
  return new NumericWithPrecision(Object(this), fractionalDigits)
}

"numeric value with precision" V2

V1: https://gist.github.com/nicolo-ribaudo/1ae2f261f2513c45f4bd3d7ede06c42f

A "numeric with precision" is a pair consisting of a numeric value and an indication of how many of its digits are significant.

It can be obtained from a numeric value (wether it's decimal, float, or bigint) by "decorating it":

new Decimal("123.45").withPrecision(3);
123.45.withPrecision(3);
12345n.withPrecision(3);

For any numeric type, x.withPrecision(y).toString() is equivalent to x.toPrecision(y). There is also a x.withPrecision(y).toLocaleString().

Given a "numeric with precision", it's possible to separately extract the numeric value that it contains and the associated precision:

123.41.withPrecision(4).valueOf(); // 123.4
123.41.withPrecision(4).numericObject; // Number { 123.4 }
123.41.withPrecision(4).significantDigits; // 4

12341n.withPrecision(4).valueOf(); // 12340n
12341n.withPrecision(4).numericObject; // BigInt { 12340n }
12341n.withPrecision(4).significantDigits; // 4

new Decimal("123.41").withPrecision(4).valueOf(); // TypeError (because decimal primitives are hidden)
new Decimal("123.41").withPrecision(4).numericObject; // Decimal { 123.4 }
new Decimal("123.41").withPrecision(4).significantDigits; // 4

"numeric with precision"s don't have any built-in arithmetic operations. Instead, developers must perform arithmetics on the underlying value, and then attatch the correct precision based on the error propagation theory that they are following.

When Intl.NumberFormat and Intl.PluralRules receive a "numeric with precision", they must default to using the encoded precision for minimumSignificantDigits and maximumSignificantDigits.

Open questions

Rounding

When doing .withPrecision(1) of 0.123, it should probably be rounded to loose the 2 and the 3. How does this rounding happen? Like Math.round()? Or should it be configurable?

toPrecision() or toFixed()?

Do we want .withPrecision() (equivalent of .toPrecision()) that stores the number of significant digits counting from the most significant one, or do we want something equivalent to .toFixed() that counts starting from the units?

How should Intl detect that an object is a "numeric with precision"?

Can it check an internal slot? Should this use a protocol (obj[Symbol.numericAndPrecision]() -> { numericObject, significantDigits })? Using a protocol would allow developers to create their own "numeric with precision" implementations that automatically do error propagation the way they want them to.

Possible extensions

  • "Numeric with precision" can itself have a .withPrecision() method, to reduce (or also expand?) the precision of the undelrying value.
  • There can be a Decimal.parseWithPrecision(), Number.parseWithPrecision(), BigInt.parseWithPrecision() that parse a number from a string and preserve the info about the precision, returning a "number with precision" object.
@kriskowal
Copy link

@nicolo-ribaudo this seems like a good place to start. I am wondering whether the Decimal champions would want to take this idea as a dependency and also introduce AccountingDecimal or DecimalWithPrecision that does provide math prototype methods for automatic propagation of precision backed by the IEEE Decimal internal representation and its accounting-oriented rules.

@nicolo-ribaudo
Copy link
Author

I would leave that as a follow-up, because to be honest I am not convinced by how much need there is out there for built-in utilities that match accounting rules.

@jessealama
Copy link

@nicolo-ribaudo this seems like a good place to start. I am wondering whether the Decimal champions would want to take this idea as a dependency and also introduce AccountingDecimal or DecimalWithPrecision that does provide math prototype methods for automatic propagation of precision backed by the IEEE Decimal internal representation and its accounting-oriented rules.

Just want to agree with @nicolo-ribaudo here. That's worth pursuing in the future, I think, if we get good feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment