Skip to content

Commit

Permalink
fix: #3142 support BigNumber values for the options of function `form…
Browse files Browse the repository at this point in the history
…at`: `precision`, `wordSize`, `lowerExp`, `upperExp`
  • Loading branch information
josdejong committed Jan 31, 2024
1 parent 5fb909a commit c60f637
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 76 deletions.
3 changes: 3 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
Thanks @dvd101x.
- Fix: #3114 build warnings related to a number of wrong `/* #__PURE__ */`
annotations.
- Fix: #3142 support BigNumber values for the options of function `format`:
`precision`, `wordSize`, `lowerExp`, `upperExp`. Support BigNumber values
for the option `wordSize` in the functions `hex`, `bin`, and `oct`.
- Docs: #3145 fix documentation about REPL, it does require a build step
nowadays.

Expand Down
6 changes: 3 additions & 3 deletions src/function/string/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,16 @@ const dependencies = ['typed', 'format']
* oct
* hex
*
* @param {number} value Value to be stringified
* @param {number} wordSize Optional word size (see `format`)
* @param {number | BigNumber} value Value to be stringified
* @param {number | BigNumber} wordSize Optional word size (see `format`)
* @return {string} The formatted value
*/
export const createBin = factory(name, dependencies, ({ typed, format }) => {
return typed(name, {
'number | BigNumber': function (n) {
return format(n, { notation: 'bin' })
},
'number | BigNumber, number': function (n, wordSize) {
'number | BigNumber, number | BigNumber': function (n, wordSize) {
return format(n, { notation: 'bin', wordSize })
}
})
Expand Down
6 changes: 3 additions & 3 deletions src/function/string/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ export const createFormat = /* #__PURE__ */ factory(name, dependencies, ({ typed
* - `'bin'`, `'oct'`, or `'hex'`
* Format the number using binary, octal, or hexadecimal notation.
* For example `'0b1101'` and `'0x10fe'`.
* - `wordSize: number`
* - `wordSize: number | BigNumber`
* The word size in bits to use for formatting in binary, octal, or
* hexadecimal notation. To be used only with `'bin'`, `'oct'`, or `'hex'`
* values for `notation` option. When this option is defined the value
* is formatted as a signed twos complement integer of the given word
* size and the size suffix is appended to the output.
* For example `format(-1, {notation: 'hex', wordSize: 8}) === '0xffi8'`.
* Default value is undefined.
* - `precision: number`
* - `precision: number | BigNumber`
* Limit the number of digits of the formatted value.
* For regular numbers, must be a number between `0` and `16`.
* For bignumbers, the maximum depends on the configured precision,
Expand Down Expand Up @@ -125,6 +125,6 @@ export const createFormat = /* #__PURE__ */ factory(name, dependencies, ({ typed
*/
return typed(name, {
any: formatString,
'any, Object | function | number': formatString
'any, Object | function | number | BigNumber': formatString
})
})
6 changes: 3 additions & 3 deletions src/function/string/hex.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@ const dependencies = ['typed', 'format']
* oct
* bin
*
* @param {number} value Value to be stringified
* @param {number} wordSize Optional word size (see `format`)
* @param {number | BigNumber} value Value to be stringified
* @param {number | BigNumber} wordSize Optional word size (see `format`)
* @return {string} The formatted value
*/
export const createHex = factory(name, dependencies, ({ typed, format }) => {
return typed(name, {
'number | BigNumber': function (n) {
return format(n, { notation: 'hex' })
},
'number | BigNumber, number': function (n, wordSize) {
'number | BigNumber, number | BigNumber': function (n, wordSize) {
return format(n, { notation: 'hex', wordSize })
}
})
Expand Down
6 changes: 3 additions & 3 deletions src/function/string/oct.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const dependencies = ['typed', 'format']
* bin
* hex
*
* @param {number} value Value to be stringified
* @param {number} wordSize Optional word size (see `format`)
* @param {number | BigNumber} value Value to be stringified
* @param {number | BigNumber} wordSize Optional word size (see `format`)
* @return {string} The formatted value
*/

Expand All @@ -30,7 +30,7 @@ export const createOct = factory(name, dependencies, ({ typed, format }) => {
'number | BigNumber': function (n) {
return format(n, { notation: 'oct' })
},
'number | BigNumber, number': function (n, wordSize) {
'number | BigNumber, number | BigNumber': function (n, wordSize) {
return format(n, { notation: 'oct', wordSize })
}
})
Expand Down
45 changes: 16 additions & 29 deletions src/utils/bignumber/formatter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isInteger } from '../number.js'
import { isBigNumber, isNumber } from '../is.js'
import { isInteger, normalizeFormatOptions } from '../number.js'

/**
* Formats a BigNumber in a given base
Expand Down Expand Up @@ -115,7 +116,7 @@ function formatBigNumberToBase (n, base, size) {
* format(12400, {notation: 'engineering'}) // returns '12.400e+3'
*
* @param {BigNumber} value
* @param {Object | Function | number} [options]
* @param {Object | Function | number | BigNumber} [options]
* @return {string} str The formatted value
*/
export function format (value, options) {
Expand All @@ -129,31 +130,7 @@ export function format (value, options) {
return value.isNaN() ? 'NaN' : (value.gt(0) ? 'Infinity' : '-Infinity')
}

// default values for options
let notation = 'auto'
let precision
let wordSize

if (options !== undefined) {
// determine notation from options
if (options.notation) {
notation = options.notation
}

// determine precision from options
if (typeof options === 'number') {
precision = options
} else if (options.precision !== undefined) {
precision = options.precision
}

if (options.wordSize) {
wordSize = options.wordSize
if (typeof (wordSize) !== 'number') {
throw new Error('Option "wordSize" must be a number')
}
}
}
const { notation, precision, wordSize } = normalizeFormatOptions(options)

// handle the various notations
switch (notation) {
Expand All @@ -179,8 +156,8 @@ export function format (value, options) {
{
// determine lower and upper bound for exponential notation.
// TODO: implement support for upper and lower to be BigNumbers themselves
const lowerExp = (options && options.lowerExp !== undefined) ? options.lowerExp : -3
const upperExp = (options && options.upperExp !== undefined) ? options.upperExp : 5
const lowerExp = _toNumberOrDefault(options?.lowerExp, -3)
const upperExp = _toNumberOrDefault(options?.upperExp, 5)

// handle special case zero
if (value.isZero()) return '0'
Expand Down Expand Up @@ -257,3 +234,13 @@ export function toExponential (value, precision) {
export function toFixed (value, precision) {
return value.toFixed(precision)
}

function _toNumberOrDefault (value, defaultValue) {
if (isNumber(value)) {
return value
} else if (isBigNumber(value)) {
return value.toNumber()
} else {
return defaultValue
}
}
97 changes: 68 additions & 29 deletions src/utils/number.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { isNumber } from './is.js'
import { isBigNumber, isNumber, isObject } from './is.js'

/**
* @typedef {{sign: '+' | '-' | '', coefficients: number[], exponent: number}} SplitValue
Expand Down Expand Up @@ -239,31 +239,7 @@ export function format (value, options) {
return 'NaN'
}

// default values for options
let notation = 'auto'
let precision
let wordSize

if (options) {
// determine notation from options
if (options.notation) {
notation = options.notation
}

// determine precision from options
if (isNumber(options)) {
precision = options
} else if (isNumber(options.precision)) {
precision = options.precision
}

if (options.wordSize) {
wordSize = options.wordSize
if (typeof (wordSize) !== 'number') {
throw new Error('Option "wordSize" must be a number')
}
}
}
const { notation, precision, wordSize } = normalizeFormatOptions(options)

// handle the various notations
switch (notation) {
Expand All @@ -287,7 +263,7 @@ export function format (value, options) {

case 'auto':
// remove trailing zeros after the decimal point
return toPrecision(value, precision, options && options)
return toPrecision(value, precision, options)
.replace(/((\.\d*?)(0+))($|e)/, function () {
const digits = arguments[2]
const e = arguments[4]
Expand All @@ -300,6 +276,49 @@ export function format (value, options) {
}
}

/**
* Normalize format options into an object:
* {
* notation: string,
* precision: number | undefined,
* wordSize: number | undefined
* }
*/
export function normalizeFormatOptions (options) {
// default values for options
let notation = 'auto'
let precision
let wordSize

if (options !== undefined) {
if (isNumber(options)) {
precision = options
} else if (isBigNumber(options)) {
precision = options.toNumber()
} else if (isObject(options)) {
if (options.precision !== undefined) {
precision = _toNumberOrThrow(options.precision, () => {
throw new Error('Option "precision" must be a number or BigNumber')
})
}

if (options.wordSize !== undefined) {
wordSize = _toNumberOrThrow(options.wordSize, () => {
throw new Error('Option "wordSize" must be a number or BigNumber')
})
}

if (options.notation) {
notation = options.notation
}
} else {
throw new Error('Unsupported type of options, number, BigNumber, or object expected')
}
}

return { notation, precision, wordSize }
}

/**
* Split a number into sign, coefficients, and exponent
* @param {number | string} value
Expand Down Expand Up @@ -478,8 +497,8 @@ export function toPrecision (value, precision, options) {
}

// determine lower and upper bound for exponential notation.
const lowerExp = (options && options.lowerExp !== undefined) ? options.lowerExp : -3
const upperExp = (options && options.upperExp !== undefined) ? options.upperExp : 5
const lowerExp = _toNumberOrDefault(options?.lowerExp, -3)
const upperExp = _toNumberOrDefault(options?.upperExp, 5)

const split = splitNumber(value)
const rounded = precision ? roundDigits(split, precision) : split
Expand Down Expand Up @@ -696,3 +715,23 @@ export function copysign (x, y) {
const signy = y > 0 ? true : y < 0 ? false : 1 / y === Infinity
return signx ^ signy ? -x : x
}

function _toNumberOrThrow (value, onError) {
if (isNumber(value)) {
return value
} else if (isBigNumber(value)) {
return value.toNumber()
} else {
onError()
}
}

function _toNumberOrDefault (value, defaultValue) {
if (isNumber(value)) {
return value
} else if (isBigNumber(value)) {
return value.toNumber()
} else {
return defaultValue
}
}
32 changes: 32 additions & 0 deletions test/unit-tests/function/string/format.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import assert from 'assert'

import math from '../../../../src/defaultInstance.js'
import { format } from '../../../../src/utils/number.js'

describe('format', function () {
it('should format numbers', function () {
Expand Down Expand Up @@ -186,11 +187,15 @@ describe('format', function () {
assert.strictEqual(math.format(1.25, { notation: 'hex' }), '0x1.4')
assert.strictEqual(math.bin(-128), '-0b10000000')
assert.strictEqual(math.bin(-128, 8), '0b10000000i8')
assert.strictEqual(math.bin(math.bignumber(-128), math.bignumber(8)), '0b10000000i8')
assert.strictEqual(math.oct(-128), '-0o200')
assert.strictEqual(math.oct(-128, 8), '0o200i8')
assert.strictEqual(math.oct(math.bignumber(-128), math.bignumber(8)), '0o200i8')
assert.strictEqual(math.hex(-128), '-0x80')
assert.strictEqual(math.hex(-128, 8), '0x80i8')
assert.strictEqual(math.hex(math.bignumber(-128), math.bignumber(8)), '0x80i8')
})

it('should throw an error for invalid values', function () {
assert.throws(function () { math.format(1.25, { notation: 'hex', wordSize: 8 }) }, 'Error: Value must be an integer')
assert.throws(function () { math.format(1, { notation: 'hex', wordSize: -8 }) }, 'Error: size must be greater than 0')
Expand Down Expand Up @@ -230,6 +235,19 @@ describe('format', function () {
assert.strictEqual(math.format(math.bignumber('123456789.123456789'), { notation: 'fixed', precision: 5 }), '123456789.12346')
})

it('should support a Bignumber as precision', function () {
assert.strictEqual(math.format(1.2345, math.bignumber(3)), '1.23')
assert.strictEqual(math.format(1.2345, { precision: math.bignumber(3) }), '1.23')
assert.strictEqual(math.format(math.bignumber('1.2345'), math.bignumber(3)), '1.23')
assert.strictEqual(math.format(math.bignumber('1.2345'), { precision: math.bignumber(3) }), '1.23')
})

it('should support a Bignumber as wordSize', function () {
const options = { notation: 'hex', wordSize: math.bignumber(32) }
assert.strictEqual(math.format(-830, options), '0xfffffcc2i32')
assert.strictEqual(math.format(math.bignumber('-830'), options), '0xfffffcc2i32')
})

describe('engineering notation', function () {
const bignumber = math.bignumber

Expand Down Expand Up @@ -358,6 +376,20 @@ describe('format', function () {
assert.strictEqual(math.format(bignumber(777777.77777777), { notation: 'engineering', precision: 2 }), '780e+3')
assert.strictEqual(math.format(bignumber(-0.000000000777777), { notation: 'engineering', precision: 2 }), '-780e-12')
})

it('should support BigNumber lowerExp and upperExp', function () {
const options = {
lowerExp: math.bignumber('-2'),
upperExp: math.bignumber('2')
}
assert.strictEqual(format(1, options), '1')
assert.strictEqual(format(1e-1, options), '0.1')
assert.strictEqual(format(1e-2, options), '0.01')
assert.strictEqual(format(1e-3, options), '1e-3')
assert.strictEqual(format(1e1, options), '10')
assert.strictEqual(format(1e2, options), '1e+2')
assert.strictEqual(format(1e3, options), '1e+3')
})
})

describe('non decimal base formatting', function () {
Expand Down
Loading

0 comments on commit c60f637

Please sign in to comment.