fix! Nat accepts safe JavaScript numbers as well as bigints.
MODIFY
.eslintrc.js
.eslintrc.js
@@ -20,5 +20,6 @@ module.exports = {
'no-return-assign': 'off',
'no-param-reassign': 'off',
'no-restricted-syntax': ['off', 'ForOfStatement'],
+ 'import/prefer-default-export': 'off',
},
};

MODIFY
NEWS.md
NEWS.md
+## Next Release
+
+* BREAKING CHANGE: In addition to non-negative bigints, `Nat` will also accept a
+ non-negative number with the range of
+ [integers safely representable in floating point](https://tc39.es/ecma262/#sec-number.issafeinteger).
+ Its output will always be a non-negative bigint or a thrown error.
+* Also exports an `isNum` function which can be used to test anything without
+ error. It returns a boolean indicating whether the argument is accepted by
+ `Nat` as input.
+
## Release v3.0.1 (12/4/2019)
* Re-release v3.0.0 after running `npm run build` (previous release

MODIFY
README.md
README.md
[![dev dependency status][dev-deps-svg]][dev-deps-url]
[![License][license-image]][license-url]
-`Nat(value)` returns its argument if it is a non-negative BigInt
- instance. If the argument is negative, an ordinary `Number`, or some
- other non-`BigInt` type, an exception is thrown. This makes it easy
- to use `Nat()` on incoming arguments, or as an assertion on generated
- values. You can think of `Nat()` as a type enforcement.
-
-[`BigInts`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt)
- can accurately represent arbitrarily large integers without concern
- of rounding or loss of precision. Two Nats added together will always
- produce another Nat. To create a BigInt, you can append `n` to the
- end of an integer literal (`3n`) or by call the function `BigInt()`.
- Note that as of 12/5/2019, `BigInt` (and therefore `Nat`) is at Stage
- 4 in the standardization process and is not yet supported in Edge,
- Internet Explorer, or Safari.
-
-## How to use
-
-`Nat()` can be used to enforce desired properties on account balances, where precision is important.
-
-For instance, in a deposit scenario, you would want to defend against someone "depositing" a negative value. Use `Nat` to validate the amount to be deposited before proceeding:
-
-```
-deposit: function(amount) {
- amount = Nat(amount);
- ...
-}
+Numbers in a programming language are meaningful because we take them to
+represent abstract mathematical numbers. JavaScript has two data types
+representing numbers, *JS numbers* (IEEE 64 bit floating point) and *bigints*
+(arbitrary precision integers). Not all abstact mathematical
+numbers are representable by these data types, and not all values of one of
+these data types represent mathematical numbers (The JS number type's `NaN`,
+`Infinity`, and `-Infinity`). Because JavaScript uses "number" to refer to
+its floating point data type, we'll always say "mathematical number" when
+that's what we mean.
+
+This package is concerned the mathematical *natural numbers*, the non-negative
+integers. All of these can be safely represented as bigints, given enough
+memory. Some of these can be represented as JS numbers, and a smaller set can
+*safely* be represented as JS numbers, given a specific notion of safety.
+
+A skippable detail about floating point:
+ * The JavaScript expression `2**70` evaluates to a JS number that exactly
+ represents the mathematical number you expect. However, the JavaScript
+ expression `2**70+1 === 2**70` evaluates to `true` because this JS number is
+ outside the contiguous range of integers that the JS number type can
+ represent *unambiguously*.
+ The contiguous range of exactly representable integers is
+ `-(2**53)` to `2**53`. However, `2**53+1 === 2**53` is `true`, demonstrating
+ that other integers will round to `2**53`. The JavaScript standard defines
+ the [*safe* JS numbers](https://tc39.es/ecma262/#sec-number.issafeinteger)
+ to be the JS numbers that represent mathematical integers and lie in the
+ range `-(2**53-1)` to `2**53` . The JS safe natural numbers are the
+ non-negative subset of that, between `0` and `2**53-1`. No other integers
+ coerce to any of these. If in JavaScript `a + b === c` and all three values
+ are JS safe integers, then this accurately represents the mathematical sum
+ of the mathematical numbers they represent.
+
+The bigint datatype, by contrast, is inherently safe. Every bigints `>= 0n`
+safely represents a natural number.
+
+This package exports two functions, `isNat(allegedNum)` and `Nat(allegedNum)`.
+
+## isNat(allegedNum: any) => boolean
+
+```js
+isNat(3); // true
+isNat(3n); // true
+isNat('3'); // false
+isNat(2**70); // false
+isNat(2n**70n); // true
+isNat(-3n); // false
+isNat(3.1); // false
```
-Any addition or subtraction expressions dealing with monetary amounts should protected with `Nat()` to guard against overflow/underflow errors. Without this check, the two balances might both be safe, but their sum might be too large to represent accurately, causing precision errors in subsequent computation:
-
-```
-const myOldBal = 12n; // BigInt numeric literal
-const amount = 3n;
-Nat(myOldBal + amount);
-
-const withdrawalAmount = 2n;
-// balances cannot be negative
-const newBal = Nat(myOldBal - withdrawalAmount);
+The `isNat` function is a predicate that accepts any input and returns `true`
+iff that input safely represents a natural number, i.e., if it is a non-negative
+bigint or it is a non-negative JS number safely representing an integer. To the
+extent that we consider this abstract notion of mathematical natural number a
+type, `isNat` is a *type tester* of possible representations of this type.
+
+## Nat(allegedNum: bigint | number) => bigint
+
+```js
+Nat(3); // 3n
+Nat(3n); // 3n
+Nat('3'); // throws TypeError
+Nat(2 ** 70); // throws RangeError
+Nat(2n ** 70n); // 1180591620717411303424n
+Nat(-3); // throws RangeError
+Nat(3.1); // throws RangeError
```
-## Non-monetary usage
+The `Nat` function accepts exactly those values that pass the `isNat`
+predicate. For those it returns bigint that represents the same natural number.
+Otherwise it throws.
-Array indexes can be wrapped with `Nat()`, to guard against the surprising string coercion of non-integral index values:
-
-```
-const a = ['hello', 'my', 'name', 'is'];
-function add(index, value) {
- a[Nat(index)] = value;
-}
-add(4n, 'alice'); // works
-add(2.5, 'bob'); // throws rather than add a key named "2.5"
-a // [ 'hello', 'my', 'name', 'is', 'alice' ]
-```
+## Validators and Coercers
-Nat can be used even in cases where it is not strictly necessary, for extra protection against human error.
+Functions like `Nat` and the standard JavaScript `BigInt` can be
+classified _validators_ or _coercers_.
-## Bounds
+When a validator accepts---returns normally rather than throwing---the caller
+knows that their input argument is as expected, and the output is the same as
+the input. When a coercer accepts, the caller knows that the output is as
+expected, but only knows that the input was one the coercer was willing to
+convert from. The `BigInt` is a coercer. It will even accept
+strings as input but its output is always a bigint. `Nat` is an interesting
+mixture. It is a coercer at one level of abstraction, and a validator at
+another level of abstraction.
-Because `Nat` uses JavaScript's upcoming [`BigInt` standard](https://tc39.github.io/proposal-bigint/), the range of accurately-representable integers is effectively unbounded.
+At the level of concrete JavaScript data representations, `Nat` is clearly a
+coercer---`Nat` will convert a qualifying JS number into a bigint. At the level
+of abstraction of the mathematical number any accepted input represents, `Nat`
+is a validator. If `Nat` succeeds the caller knows that their input safely
+represents some abstract mathematical natural number, and that the output
+safely represents the same abstract mathematical natural number. At this level
+of abstraction, on success, the output is the same as the input.
## History
-Nat comes from the Google Caja project, which tested whether a number was a primitive integer within the range of continguously representable non-negative integers.
+`Nat` comes from the Google Caja project, which tested whether a JS number was a
+primitive integer within the range of continguously and unambiguously
+representable non-negative integers.
For more, see the [discussion in TC39 notes](https://github.com/rwaldron/tc39-notes/blob/master/es6/2013-07/july-25.md#59-semantics-and-bounds-of-numberisinteger-and-numbermax_integer)

MODIFY
package.json
package.json
{
"name": "@agoric/nat",
- "version": "3.0.2-dev.0",
+ "version": "4.0.0-dev.0",
"description": "Ensures that a number is within the natural numbers (0, 1, 2...) or throws a RangeError",
"main": "dist/nat.cjs.js",
"module": "dist/nat.esm.js",
"eslint-plugin-import": "^2.22.1",
"eslint-plugin-prettier": "^3.1.4",
"esm": "^3.2.25",
- "prettier": "^2.2.1",
+ "prettier": "^1.19.1",
"rollup": "^2.34.0"
},
"directories": {

MODIFY
src/index.js
src/index.js
// See the License for the specific language governing permissions and
// limitations under the License.
+// @ts-check
+
/**
- * Is allegedNum a number in the contiguous range of exactly and
- * unambiguously representable natural numbers (non-negative integers)?
+ * Is `allegedNum` a number in the [contiguous range of exactly and
+ * unambiguously
+ * representable](https://esdiscuss.org/topic/more-numeric-constants-please-especially-epsilon#content-14)
+ * natural numbers (non-negative integers)?
+ *
+ * To qualify `allegedNum` must either be a
+ * non-negative `bigint`, or a non-negative `number` representing an integer
+ * within range of [integers safely representable in
+ * floating point](https://tc39.es/ecma262/#sec-number.issafeinteger).
*
- * <p>See <a href=
- * "https://code.google.com/p/google-caja/issues/detail?id=1801"
- * >Issue 1801: Nat must include at most (2**53)-1</a>
- * and <a href=
- * "https://mail.mozilla.org/pipermail/es-discuss/2013-July/031716.html"
- * >Allen Wirfs-Brock's suggested phrasing</a> on es-discuss.
+ * @param {any} allegedNum
+ * @returns {boolean}
*/
+function isNat(allegedNum) {
+ if (typeof allegedNum === 'bigint') {
+ return allegedNum >= 0;
+ }
+
+ return Number.isSafeInteger(allegedNum) && allegedNum >= 0;
+}
+/**
+ * If `allegedNumber` passes the `isNat` test, then return it as a bigint.
+ * Otherwise throw an appropriate error.
+ *
+ * If `allegedNum` is neither a bigint nor a number, `Nat` throws a `TypeError`.
+ * Otherwise, if it is not a [safely
+ * representable](https://esdiscuss.org/topic/more-numeric-constants-please-especially-epsilon#content-14)
+ * non-negative integer, `Nat` throws a `RangeError`.
+ * Otherwise, it is converted to a bigint if necessary and returned.
+ *
+ * @param {bigint | number} allegedNum
+ * @returns {bigint}
+ */
function Nat(allegedNum) {
- if (typeof allegedNum !== 'bigint') {
- throw new TypeError(`${allegedNum} is not a BigInt`);
+ if (typeof allegedNum === 'bigint') {
+ if (allegedNum < 0) {
+ throw new RangeError(`${allegedNum} is negative`);
+ }
+ return allegedNum;
}
+ if (typeof allegedNum === 'number') {
+ if (!Number.isSafeInteger(allegedNum)) {
+ throw new RangeError(`${allegedNum} not a safe integer`);
+ }
if (allegedNum < 0) {
throw new RangeError(`${allegedNum} is negative`);
}
+ return BigInt(allegedNum);
+ }
- return allegedNum;
+ throw new TypeError(
+ `${allegedNum} is a ${typeof allegedNum} but must be a bigint or a number`,
+ );
}
-export default Nat;
+export { isNat, Nat };

MODIFY
test/test-nat.js
test/test-nat.js
import test from 'ava';
-import Nat from '../src/index';
+import { isNat, Nat } from '../src/index';
-test('Nat() works for natural BigInts', (t) => {
+test('Nat() works for natural BigInts', t => {
t.is(Nat(1n), 1n);
t.is(Nat(BigInt('1')), BigInt('1'));
t.is(Nat(BigInt('1')), 1n);
@@ -9,64 +9,48 @@ test('Nat() works for natural BigInts', (t) => {
t.is(Nat(9007199254741000n), 9007199254741000n);
});
-test('Nat() throws for non-natural BigInts', (t) => {
+test('Nat() throws for non-natural BigInts', t => {
t.throws(() => Nat(-1n), {
message: `-1 is negative`,
instanceOf: RangeError,
});
});
-test('Nat() throws for non-BigInts', (t) => {
- t.throws(() => Nat(0), {
- message: `0 is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat(1), {
- message: `1 is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat(999), {
- message: `999 is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat(3.0), {
- message: `3 is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat('not a number'), {
- message: `not a number is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat(-1), {
- message: `-1 is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat(0.5), {
- message: `0.5 is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat(2 ** 60), {
- message: `1152921504606847000 is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat(NaN), {
- message: `NaN is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat(Infinity), {
- message: `Infinity is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat('3'), {
- message: `3 is not a BigInt`,
- instanceOf: TypeError,
- });
- t.throws(() => Nat(3.1), {
- message: `3.1 is not a BigInt`,
- instanceOf: TypeError,
+test('Nat() throws when not a natural number', t => {
+ t.is(Nat(0), 0n);
+ t.is(Nat(1), 1n);
+ t.is(Nat(999), 999n);
+ t.is(Nat(3.0), 3n);
+ t.throws(() => Nat('not a number'), { instanceOf: TypeError });
+ t.throws(() => Nat(-1), { instanceOf: RangeError });
+ t.throws(() => Nat(0.5), { instanceOf: RangeError });
+ t.throws(() => Nat(2 ** 60), { instanceOf: RangeError });
+ t.throws(() => Nat(NaN), { instanceOf: RangeError });
+ t.throws(() => Nat(Infinity), { instanceOf: RangeError });
+ t.throws(() => Nat('3'), { instanceOf: TypeError });
+ t.throws(() => Nat(3.1), { instanceOf: RangeError });
+
+ // works for safe integers only
+ t.is(Nat(2 ** 53 - 1), 2n ** 53n - 1n);
+ t.throws(() => Nat(2 ** 53), { instanceOf: RangeError });
});
- t.throws(() => Nat(2 ** 53 - 1), {
- message: `9007199254740991 is not a BigInt`,
- instanceOf: TypeError,
+
+test('isNat examples from the README', t => {
+ t.assert(isNat(3));
+ t.assert(isNat(3n));
+ t.false(isNat('3'));
+ t.false(isNat(2 ** 70));
+ t.assert(isNat(2n ** 70n));
+ t.false(isNat(-3n));
+ t.false(isNat(3.1));
});
+
+test('Nat examples from the README', t => {
+ t.is(Nat(3), 3n);
+ t.is(Nat(3n), 3n);
+ t.throws(() => Nat('3'), { instanceOf: TypeError });
+ t.throws(() => Nat(2 ** 70), { instanceOf: RangeError });
+ t.is(Nat(2n ** 70n), 1180591620717411303424n);
+ t.throws(() => Nat(-3n), { instanceOf: RangeError });
+ t.throws(() => Nat(3.1), { instanceOf: RangeError });
});

MODIFY
yarn.lock
yarn.lock
@@ -2070,10 +2070,10 @@ prettier-linter-helpers@^1.0.0:
dependencies:
fast-diff "^1.1.2"
-prettier@^2.2.1:
- version "2.2.1"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
- integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
+prettier@^1.19.1:
+ version "1.19.1"
+ resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb"
+ integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==
pretty-ms@^7.0.1:
version "7.0.1"

fix! Nat accepts safe JavaScript numbers as well as bigints.