denary 1.2.1
denary: ^1.2.1 copied to clipboard
Decimal numbers with a fixed point and no loss of precision. Two independent families - one on BigInt with no bounds on magnitude, one on int for speed.
1.2.1 #
ShortDecimal divides faster where the exact quotient does not fit a machine
word. Four places in the short family reached for BigInt where a pair of
int covers 128 bits; they no longer do. No API changed and no answer changed:
the suite and the General Decimal Arithmetic vectors both say every result is
the value it was.
- Rounding a quotient that has no finite decimal form is 1.8–2.35× quicker on
values whose exact dividend outgrows an
int, and unchanged where it does not. - Multiplication decides an overflow on the exact product rather than on a
doublethat stands in for it — a few percent, and one guess fewer. - Comparing fractions and adding a sum that overflowed no longer build a
BigIntat all. - Documented rather than changed:
parsewants a point followed by digits, so.5is a number and5.is not.
1.2.0 #
The package is now called denary. It was published as
yet_another_decimal up to 1.1.2; that name is discontinued and points here.
Nothing inside changed with it: the types are still Decimal and
ShortDecimal, and only the import lines move.
import 'package:denary/denary.dart'; // was yet_another_decimal.dart
import 'package:denary/decimal.dart';
import 'package:denary/short_decimal.dart';
Beside the name: fifteen defects closed, the gaps in the public API filled in, and the whole package measured. Almost all of it is a fix or an addition; the handful of places where working code can notice the difference are gathered under Changed.
Compatibility #
- The SDK floor comes down from
^3.9.0to^3.6.0, andmetafrom^1.17.0to^1.15.0. Flutter takesmetaout of its own SDK at an exact version — 1.15.0 on Flutter 3.27, 1.16.0 on 3.29 — so the old constraint put the real Flutter floor at 3.35 whatever the SDK range said. The package now resolves on Flutter 3.27 and later. Nothing in it needed a language feature newer than 3.6; the whole test suite runs on Dart 3.6.0.
Added #
- Total division.
divideOrNullreturns null where the result has no finite decimal form,divide(other, scaleOnInfinitePrecision: n)rounds tondigits, andisDivisibleByasks the question in advance.operator /still throws and stays the fast form for the case where the division is known to be exact. - Exponential notation in
parseandtryParse:Decimal.parse('1.5e21'). - Two typed refusals, shared by both families:
ScaleOutOfRangeErrorwhere a scale would leave int64, andDecimalDigitsOutOfRangeErrorwhere a number of digits — or a power of ten an operation needs — goes past a million. Both extendArgumentErrorand carry the samename,invalidValue,messageand text as before, so code catchingArgumentErrorsees no change; the type is there for code that wants to tell one refusal from another. toStringAsEngineeringin both families: an exponential form whose exponent is a multiple of three, so the mantissa carries one, two or three digits before the point — the form an SI prefix is read in.Decimal.parse('12345').toStringAsEngineering(2)is12.35e+3.toStringAsExponentialandtoStringAsPrecisionin both families. They used to throwUnimplementedErrorinDecimaland were absent fromShortDecimal.- A bridge between the families:
ShortDecimal.toDecimal()andDecimal.toShortDecimalOrNull(). - The members only one of the two families had:
toJsonandfromJson,Decimal.toInt,ShortDecimal.toBigInt,precision,isPositive,inverse,powwith a negative exponent,ShortFraction.toShortDecimal, andcompareTo,abs,inverseandtoDoubleon both fraction types. normalized(), which answers with the value in its canonical form. It replacesoptimize(), which returned nothing and changed the receiver instead and is gone from this package (see Removed).roundAwayFromZero, in the same six places asroundToEven: any remainder at all moves the last digit one step further from zero, so 2.01 to one digit is 2.1 and -2.01 is -2.1. The mirror oftruncate, andROUND_UPin General Decimal Arithmetic — a name that would read asceilhere.roundToEven, on both number types, both fraction types and both divide exceptions: rounds a half to the even neighbour — 2.5 to 2, 3.5 to 4 — instead of away from zero. The rule accounting asks for, and the default one in General Decimal Arithmetic.unscaledValueandexponent: the number taken apart. Both read the canonical form, so equal decimals answer equally whatever produced them.movePointLeftandmovePointRight— the same thing as>>and<<, spelled out.>>moves the point left, which is easy to read the wrong way round.- Two narrow entry points beside the umbrella one:
package:denary/decimal.dartandpackage:denary/short_decimal.dart. Whoever needs one family no longer carries the other. ShortDecimalis annotatedvm:deeply-immutable: the VM may share its instances between isolates.Decimalcannot be — aBigIntfield is rejected by that annotation.
Removed #
Both members below existed in yet_another_decimal and are not carried over.
Nothing in denary ever exposed them, and moving to denary is a hand edit of
pubspec.yaml and of every import line anyway — there is no better moment to
leave a deprecation behind than the one where the code is already being
touched.
- The
toDecimal()extensions onintandBigInt. Packagedecimalcarries the same two, and while both exist a plain5.toDecimal()is ambiguous — including the call meant for the other package. UseDecimal(value)andDecimal.fromBigInt(value). The two packages now coexist: what is left to sort out is the type nameDecimal, which both of them use, and ahideor ashowsettles that the way it does for any two packages. optimize(), which packed the receiver in place and returned nothing. Usenormalized(), which answers with the value instead.
Changed #
- Dividing by zero throws
UnsupportedErroreverywhere.~/,%andremainderused to throwIntegerDivisionByZeroExceptionwhile/threwUnsupportedError: one mistake, two types. The SDK marks the first deprecated, and it implements the second, so code catchingUnsupportedErrorsees no change. parseandtryParseno longer read hexadecimal, a lone-or+0— neither for the two number types nor for the two fraction types, whereFraction.parse('0x10')used to be sixteen. Whitespace around a number is still read, as it always was.- A negative shift in a constructor throws
ArgumentErrorinstead of tripping an assertion.Decimal(1, shiftRight: -3)used to crash a debug build and quietly answer1000in a release one, andShortDecimalhad three such assertions, one of them forshiftLeftandshiftRightgiven together. A shift is an argument, and an argument is checked in every build.
Fixed #
ShortFractiondivision refused an answer that fits: two divided by-1/2^62is the minimum integer exactly, but the sign of the divisor was moved only after the products were built, leaving a pair with nowhere to put it. The sign moves up first now, and a brute force over every representable ratio of the edge values refuses none of them.powwith a negative exponent divided by a power that had wrapped:ShortDecimal(int.min).pow(-3)reported a division by zero for a base that is not zero, andShortDecimal(5).pow(-30)was refused although its answer fits int64 with room to spare. The power is built with the overflow checked now; where it leaves int64 the answer comes from the reciprocal of the base, and where the answer itself has no int64 form the refusal isUnsupportedError— the oneinversemakes, for the same reason. The last power that fits is answered too:(-2) ^ 63is the minimum integer exactly, andShortDecimal.parse('-0.5').pow(-63)gives it.toStringAsExponentialrefused to print a number of a large scale, naming an argument the caller never passed:1e-1000000to two digits asked for a million and two of them. The leading digits are rounded as digits now, so no power of ten is built at all — which also takes the cost of the notation off the size of the number.toStringAsPrecisionwrites a number out in full, so what it holds to a million is the position it rounds at — a million digits after the point, and a million before it — and the refusal says which of the two it is. The digits the number already carries are not counted:toStringwrites them all.- Rounding at a position the scale is further than int64 from raised a
RangeErrorfrom a table of powers, in every rounding mode and in both families:Decimal(1, shiftRight: 9223372036854775807).round(-1). The BigInt family now refuses withDecimalDigitsOutOfRangeError— the power of ten it would need is past the million — and the int64 family answers, the whole value standing to the right of the position asked for. - A divide exception could not be asked again when its own pair has no fraction
in int64:
ShortDecimal.one / ShortDecimal(-9223372036854775808)produced an exception whoseround,floorand every other recovery threw, although the rounded answers are ordinary numbers. They are rounded from the pair now, the waydividewith a number of digits rounds it. ShortDecimal.toIntwrapped past int64, turning-1e19into a positive number. It clamps now, asBigInt.toInt,double.toIntand~/all do.parseread a fractional part longer than the exponent it allows, and the value it built could then be neither compared nor rounded nor printed. One bound for both spellings now.- Canonicalising could change the value: stripping a zero at the floor of int64
wrapped the scale, and a huge number came back as a vanishing one. Both
families refuse it now, and
exponentrefuses the floor it cannot negate. - Taking the factors out of a divisor wrapped the scale the same way, in both families. Where it would, the division answers null.
- A quotient with no representation in int64 came back with the opposite sign.
divideOrNullanswers null there — the silent overflow of arithmetic is not a licence to invent a number. - Rounding to a scale that int64 cannot hold rounded twice: the digits were
dropped one at a time, so
.85365became.85and then.8, where.9is the answer. Every mode now rounds once, against the whole of the remainder. dividewith a number of digits could answer where its own neighbours refused: the fast path added up three scales before checking any of them, and a sum that wrapped landed inside the allowed window.- The scale check in division read the stored scales rather than the values, so a number and its own canonical twin divided differently.
- Addition and subtraction lost a result whose canonical form fits: the sum of
9223372036854775807and3wrapped instead of moving its trailing zero into the scale. The same argument that fixed multiplication. powwith a negative exponent answered differently for equal values: ten held as10 × 10^0refused where the same ten held as1 × 10^1did not. It works from the canonical form now.ShortDecimal.inversethrewArgumentErrorfrom the fraction factory where its own doc promisesUnsupportedError.- Every int64 operation that aligns two scales raised a
RangeErrorwhere the gap between them leaves int64 —operator ==among them, so a decimal at a scale near the ceiling could not be put in a set or compared to a plain ten. The gap saturates now, and the operations answer:1e-9223372036854775807compares below ten, is not equal to it, and divides into it nothing. - The same alignment built a power of ten of the width of the gap, however wide
that was: a gap of ten million asked for a number of ten million digits, and
~/,%,remainderanddividenever came back from it. Past a gap of eighteen the answer follows from the gap alone — the quotient is nothing and the remainder is the whole of the smaller side — and it is given without building anything. Past a million, where no such answer exists, the refusal is now theDecimalDigitsOutOfRangeErrorthe BigInt family has always made on the same operation at the same gap. - Rounding to a large number of digits stepped the scale down one digit at a
time, dividing the whole pair again at every step:
ShortFraction(1, 3)rounded to a million digits — a count the package accepts — never returned. The digits to drop are counted now: ten thousand of them took nine tenths of a second and take none. powrefused the minimum exponent on the three bases whose power does not depend on it:Decimal.one.pow(-9223372036854775808)is one, andDecimal.zeroat that exponent is a division by zero rather than an argument out of range.powwith a negative exponent refused where the scale of the positive power leaves int64 but the answer does not:Decimal(5, shiftRight: 2^62)to the minus second is four at a scale int64 holds. The reciprocal of the base carries the scale the other way, and both families take that road now.- A zero kept at a large scale refused to be rounded, in all six modes, while
toStringprinted it as0: the BigInt family went looking for a power of ten past the million to answer nothing. Zero rounds to zero wherever the position is, and the two families agree again. - Dividing zero by a negative number gave
0.0in the BigInt family and-0.0in the int64 one. Both give-0.0now, as plain Dart does. - A divide exception could not be printed:
toStringbuilt parts that throw on the very values that produced the exception. divideToFractiontruncated a ratio that has no fraction in int64 instead of refusing it, anddividerounded the truncated pair — a wrong answer with nothing to give it away.ShortFraction.floorandceillost their direction when the result needed more digits than int64 holds: both answered with the closest value, so a floor could come back above its own fraction. They now drop digits the way they were asked to.ShortFraction.toDoublerounded twice past 2^53 and missed the nearest double. It now takes the exact path, asFractiondoes.- Division let the scale overflow where the shifts refuse it. A quotient whose scale wrapped printed as an integer and compared as one; now it throws with the name of the argument, in both families.
- Division by a negative number gave the wrong sign.
- Division by zero hung instead of throwing.
- Comparison, sorting, rounding and
toIntbroke on values whose scales were more than eighteen apart, and answered differently on the VM and on the web. toBigIntlost the magnitude of a scaled value.toStringAsFixeddid not pad the result out to the number of digits asked for.divideToDoublereturnedNaNwhere the ratio did not fit aBigIntdivision.ShortDecimal.tenwas stored in a non-canonical form, so itshashCodedisagreed with an equal value built another way.Fractionrejected a negative number of fraction digits, whichDecimalaccepted.ShortFractionoverflowed on values that fit into int64, and lost the sign ofint.min.ShortDecimalmultiplication overflowed where the exact result fits.- Assertions crashed a debug build on values a release build handled.
normalized()did not recognise its own answer as canonical, so calling it on the result packed and allocated all over again.ShortDecimal.inversewrapped around past ten to the eighteenth and answered a positive number with a negative one. Where the fraction has no int64 to live in it throws now.- The scale wrapped around silently:
Decimal.parse('0.01').pow(int.max)printed100.pow,<<,>>and bothmovePointmethods refuse such a shift now, in both families. - A power of ten past ten to the millionth is refused rather than attempted.
Decimal.one.round(-1000000000)went looking for ten to the billionth and took the memory of the process with it; the same held forfloor,ceil,truncate,divide, the threetoStringAs…methods and both fraction types. The bound is the oneparsehas always had, so no number this package can read in needs more, and it is checked both where the request comes in and where the power would be built — so a scale shifted that far is caught as well. DecimalDivideException.forTestand its twin accepted a zero divisor, and everything about the exception it built —toStringincluded — threw.ShortDecimal.divide(other, scaleOnInfinitePrecision: n)threwArgumentErrorwhen the divisor wasint.min. The ratio has no fraction in int64 — a denominator of 2^63 cannot be positive there — but the rounded quotient it was asked for is an ordinary number, and it is answered now.
Performance #
parse reads the shape a loop reads — a sign, digits, a point, digits —
without building anything on the way: the number is accumulated as it is
scanned. The full grammar, with its exponent and its whitespace, is still there
and still read; it is just no longer what every ordinary string pays for.
ShortDecimal reads such a string twice as fast as a version ago, and
Decimal nine times — that road used to hand the digits to BigInt.parse
as a freshly built string, and now makes one BigInt from a number it already
has.
Multiplication on ShortDecimal clears the common case with four integer
comparisons before it asks anything else: two values under 2^31 cannot make a
product that leaves int64, and the check that follows — an approximate product
in double — is a chain three operations long that every multiplication would
otherwise wait on. Worth about a fifth of the operation. The check itself
stays: it is what keeps 2^62 * 5 from wrapping.
Rounding on ShortDecimal is a quarter faster than it was a version ago —
round and its five siblings hand their rules to the shared road as static
functions rather than as closures over the value, and a closure is an object
built on every call. toStringAsFixed, which rounds on the way, is a tenth
faster with it.
Addition and subtraction are a tenth faster on Decimal, and a fifth where the
two scales already match: the alignment is written out in the operators instead
of being taken from a helper that returned a record, and the record was an
allocation on the shortest operation in the package. Division is several times
faster, printing the same value twice costs almost nothing, toDouble no longer goes through a string, and rounding a quotient
that has no finite decimal form — divide(other, scaleOnInfinitePrecision: n)
— is about 1.6 times faster than it was, having stopped building an exact
fraction only to divide it again, and then twice as fast again: where the
divisor is coprime with ten, which is every division by three, the remainder of
the rounding answers by itself whether there was a finite form to return
instead, and the division that used to ask separately was half the cost of the
operation. divideOrNull and isDivisibleBy are about 1.8 times faster on
such a divisor for the same reason — a gcd that could not change the answer is
no longer spent. ShortDecimal.divide with a digit count is twenty times
faster: it folds both scales and the digits asked for into one power of ten
instead of aligning the pair into a fraction and scaling that, which used to
carry the arithmetic out of int64 and into BigInt on values that fit int64
comfortably. The table in the
README is a fresh run of the bench in example/, which was rebuilt for this
release: it checks every answer before timing it, measures a series and reports
the median, and no longer reports an absent method as a failure.
1.1.0-1.1.2 #
- Update dependencies.
- Fix links in README.
1.0.0-1.0.2 #
- Initial version.