@adaskothebeast/http-params-processor-value-to-decimal
v12.0.0
Published
Plain, fixed, numeric and exponential decimal output strategies for HttpParamsProcessor, keeping decimal.js values lossless.
Maintainers
Readme
🧮 @adaskothebeast/http-params-processor-value-to-decimal
Decimal output strategies for HttpParamsProcessor: plain literals, fixed decimal places, number literals and exponential notation.
Peer dependencies: core + decimal.js (types ship with decimal.js, no companion @types package). ESM + CJS. sideEffects: false.
📦 Install
npm i @adaskothebeast/http-params-processor-value-to-decimal @adaskothebeast/http-params-processor-core decimal.js🎯 What it does
These are value-to strategies: the second half of the conversion pipeline. Each one consumes the neutral DecimalComponents shape from core ({ decimal: string }) - normally produced by -value-from-decimal - and renders it as a query string value.
| Class | Renders as | Example output |
| ----------------------------------- | --------------------------------- | ------------------------------------------ |
| DecimalStringValueToStrategy | the lossless literal, verbatim | 12345678901234567890.1234567890123456789 |
| DecimalFixedValueToStrategy | fixed decimal places (money) | 12.50 |
| DecimalNumberValueToStrategy | JavaScript number literal | 12.5 |
| DecimalExponentialValueToStrategy | exponential / scientific notation | 1.25e+8 |
DecimalStringValueToStrategy is the safe default: it is the format ASP.NET Core, Rails and Spring decimal binders expect, and it is the only one of the four that cannot lose a digit.
⚡ Usage
import { ParamsProcessor, createValueConverter } from '@adaskothebeast/http-params-processor-core';
import { DecimalValueFromStrategy } from '@adaskothebeast/http-params-processor-value-from-decimal';
import { DecimalFixedValueToStrategy, DecimalStringValueToStrategy } from '@adaskothebeast/http-params-processor-value-to-decimal';
import Decimal from 'decimal.js';
const processor = new ParamsProcessor({
valueConverters: [createValueConverter(new DecimalValueFromStrategy(), new DecimalStringValueToStrategy())],
});
processor.process('p', { total: new Decimal('12.5') });
// [['p.total', '12.5']]
// money oriented processor: always two decimal places
const money = new ParamsProcessor({
valueConverters: [createValueConverter(new DecimalValueFromStrategy(), new DecimalFixedValueToStrategy(2))],
});
money.process('p', { total: new Decimal('12.5') });
// [['p.total', '12.50']]🎛️ Options and configuration
| Class | Constructor | Defaults |
| ----------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- |
| DecimalStringValueToStrategy | new DecimalStringValueToStrategy() | no options |
| DecimalFixedValueToStrategy | new DecimalFixedValueToStrategy(decimalPlaces?, rounding?) | decimalPlaces = 2, rounding = Decimal.ROUND_HALF_UP |
| DecimalNumberValueToStrategy | new DecimalNumberValueToStrategy() | no options |
| DecimalExponentialValueToStrategy | new DecimalExponentialValueToStrategy(significantDigits?) | significantDigits = undefined (as many digits as needed) |
roundingis anydecimal.jsrounding constant (Decimal.ROUND_HALF_UP,Decimal.ROUND_DOWN, …) and is forwarded toDecimal.prototype.toFixed(decimalPlaces, rounding).significantDigitsis forwarded toDecimal.prototype.toExponential, which interprets it as the number of digits after the decimal point, so2renders125400000as1.25e+8.
All four strategies share the same canHandle, a structural guard over DecimalComponents: an object whose decimal property is a string matching /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.
| canHandle input | Result |
| ------------------------ | -------------------------------------------------- |
| { decimal: '-0.5' } | true |
| { decimal: '1.25e+8' } | false (exponential input is not a plain literal) |
| { decimal: 'abc' } | false |
| '1.5' | false (a bare string is never claimed) |
| null | false |
📤 Output examples
new DecimalStringValueToStrategy().serializeValue({
decimal: '12345678901234567890.1234567890123456789',
});
// '12345678901234567890.1234567890123456789'| Strategy | DecimalComponents | Output |
| -------------------------------------------------------- | -------------------------- | --------- |
| new DecimalFixedValueToStrategy() | { decimal: '12.5' } | 12.50 |
| new DecimalFixedValueToStrategy(2) | { decimal: '1.005' } | 1.01 |
| new DecimalFixedValueToStrategy(2) | { decimal: '1.004' } | 1.00 |
| new DecimalFixedValueToStrategy(0, Decimal.ROUND_DOWN) | { decimal: '1.9' } | 1 |
| new DecimalNumberValueToStrategy() | { decimal: '12.50' } | 12.5 |
| new DecimalNumberValueToStrategy() | { decimal: '-0.001' } | -0.001 |
| new DecimalExponentialValueToStrategy() | { decimal: '125000000' } | 1.25e+8 |
| new DecimalExponentialValueToStrategy(2) | { decimal: '125400000' } | 1.25e+8 |
⚠️ Edge cases
DecimalNumberValueToStrategythrows when the literal cannot become a finite double:Error: Decimal '<literal>' cannot be represented as a finite numberThis is deliberate - silently emitting
Infinitywould be worse. UseDecimalStringValueToStrategyfor values outside the double range.DecimalNumberValueToStrategyalso drops trailing zeros ('12.50'→12.5) and can lose digits beyond 17 significant figures, because the value passes throughNumber. Never use it for money you intend to compare byte for byte.DecimalFixedValueToStrategyrounds, it does not validate.1.005becomes1.01with the default half-up mode, whileDecimal.ROUND_DOWNtruncates instead (1.9→1at zero decimal places).DecimalExponentialValueToStrategyoutput always carries an explicit sign in the exponent (1.25e+8,1e-7). Round-tripping it needs a backend that accepts scientific notation.Exponential inputs are rejected by
canHandle, so{ decimal: '1.25e+8' }is not claimed by any of these strategies.DecimalValueFromStrategynever produces such a literal (it usestoFixed()), so this only matters if you buildDecimalComponentsby hand.Only one converter can win per value: converters are tried in registration order and the first matching
canHandleclaims the value. Since all four strategies accept exactly the same input shape, register at most one decimal converter per processor - or branch by key with separate processors.These strategies do not depend on the from side at runtime; anything that produces
{ decimal: '<plain literal>' }works, including your ownbigintor string based normalizer.
🔗 Related packages
- Inputs:
-value-from-decimal - Other outputs:
-value-to-uuid,-value-to-iso,-value-to-nodatime,-value-to-unix-timestamp,-value-to-ms-timestamp,-value-to-date-fns - Engine:
-core
Full matrix and adapter recipes: main README.
📄 License
MIT © Adam Pluciński
