Mbn (Multi-byte number) Library
Library for PHP and JS to do calculations with any precision and correct (half-up) rounding.
About
The main job of the library is to regain control of numbers.
Most of computer maths is based on float/double numbers which are fast and precise, but cause some problems in
fixed-precision (e.g. financial) calculations.
It's also easy to get unexpected NaN and Infinity values. Results often need to be formatted in a particular
way, which might or might not be available across languages.
With Mbn library:
- parsing invalid strings, division by zero, and many more problems are thrown as exceptions
- all calculations have predictable results, e.g. 1.4-0.4 gives always 1, not 0.9999999999999999
- syntax is almost identical between JS and PHP, all operations supported by a single class
- fixed precision with any size of fractional part: from zero to thousands or more
- built in expression parser, by default =2+2*2 gives 6, =2PI gives
6.28 (depending on precision), see calc example
- built in split and reduce
functions for some useful array operations
- custom formatting: dot/comma separator, thousands separator, truncating trailing zeros
- exception messages can be easily translated
- compatibility: PHP 5.4+ (5.4-8.3 tested), JS ES3+ (IE6+)
Tests and benchmark ↻
..
..
Downloads
Minified PHP is created with custom text replacements, intended to be used in online PHP sandboxes like
3v4l.org
Generally code is optimized for speed and size; not for readability
mbn.js [ show | download ] (48.83 kB)
Library in JS
mbn.php [ show | download ] (46.93 kB)
Library in PHP
mbn.min.js [ show | download ] (14.49 kB)
Minified library in JS
mbn.min.php [ show | download ] (21.98 kB)
Minified library in PHP
mbn.d.ts [ show | download ] (2.18 kB)
TypeScript declaration file
Mbn.php [ show | download ] (41.91 kB)
Mbn class in PHP (with namespace, without MbnErr class)
MbnErr.php [ show | download ] (5.16 kB)
MbnErr class in PHP (with namespace)
Reference
JS and Mbn code equivalents.
In most cases Mbn code in PHP and JS is identical - a.f() in JS is $a->f() in PHP
Class declarations
Each Mbn class has parameters defining its precision, default format and behavior
The library provides a single class named Mbn with default parameters (both in JS and PHP).
This class can be extended.
Available parameters:
- MbnP - precision - number of digits in fractional part, defines how many digits will be
stored
by default also defines the string representation: MbnP=0 → "0", MbnP=2 → "0.00"
- Default: 2
- Note that this affects all the computations, not only the formatting of the result,
e.g.: new Mbn("=1/3*3").toString() === "0.99"
-
MbnS - separator - dot or comma, decimal separator in the string representation
-
MbnT - truncation - true or false, truncation of trailing zeros in the string
representation
for MbnP=2 and MbnT=true: 1.12 → "1.12", 1.10 → "1.1", 1.00 → "1"
- Default: false (no truncation)
-
MbnE - evaluating - true, false or null, triggers usage of
the expression parser
- true: all expressions are evaluated
- null: expressions starting with "=", like "=2+3", are evaluated
- false: no expressions are evaluated, "=2+3" causes invalid format exception
- new Mbn("=2+3", true) is parsed always regardless of MbnE
- new Mbn("=2+3", false) is never parsed regardless of MbnE
- When an object [js] or an array [php] is passed as the second argument, expression is also parsed:
new Mbn("=2a", {a: 1}) [js] / new Mbn('=2a', ['a' => 1])
[php]
- MbnE doesn't affect Mbn.calc("2+3") [js] / Mbn::calc("2+3")
[php]
- Default: null
-
MbnF - formatting - true or false, use space as the thousands separator in the string
representation
for MbnP=5 and MbnF=true 12345.12345 → "12 345.12345"
- Default: false (no thousands separator)
-
MbnL - limit - number of digits that will cause limit_exceeded exception
some short expressions like "=9!!" or "=9^9^9" can have really big results and take much time to
evaluate
MbnL can avoid interface freeze or server overload
hint: some operations, like power, may exceed the limit even when the final result doesn't, because of
storing exact
partial results
Class declarations in JS
Default Mbn class can be extended with Mbn.extend() method
Single precision as number, or object with Mbn* parameters can be passed.
hint: Mbn in JS is not exactly class, it's a class, a function and an object
Derived classes cannot be extended further
Class declarations in PHP
Default Mbn class can be extended with standard inheritance by overriding protected static fields
Mbn* fields which are not overridden have default values
Derived classes shouldn't be extended further
class Mbn0 extends Mbn {
protected static $MbnP = 0;
}
class Mbn4c extends Mbn {
protected static $MbnP = 4;
protected static $MbnS = ',';
}
class Mbn5t extends Mbn {
protected static $MbnP = 5;
protected static $MbnT = true;
}
Object declarations
There are several types of values that can be passed as the first constructor argument - the value
- none - new Mbn() → 0
- boolean - new Mbn(true / false) → 1 / 0
- string - value from string, examples of valid arguments for default Mbn
- dot/comma decimal separator: "12.123", "12,123"
- missing fractional or integer part: ".123" → "0.12", "12." → "12.00"
- number with thousands separator in the integer part: "12 345,123" → "12345.12"
- expression, like mentioned in class declarations and expression parser sections
- object - the object is converted to string and parsed
e.g. instance of another Mbn class
- Mbn object - if an instance of the same Mbn class, operation is faster
- cannot be array - e.g. array [1, 2] has valid string representation "1,2",
but
shouldn't be parsed
Second argument to constructor may be true / false or object [js] / array [php], which affects expression
evaluation
as mentioned in
class declarations and
expression parser sections
In JS Mbn called as a function also returns new instance of Mbn (Mbn() instead
of new Mbn())
Dealing with Mbn objects
- Mbn objects have "magic" .toString() [js] / ->__toString()
[php] methods
(new Mbn(2)) + "x" [js] / (new Mbn(2)) . 'x' [php] gives "2.00x"
- value passed as the first argument to a two-argument method is first converted to Mbn class of the object
- two-argument functions: add, sub, mul, div, mod, min, max, pow
- invalid value passed as this argument causes exception
(new Mbn(2)).add("x")
- value with bigger precision is first truncated to precision
(new Mbn(2)).add("1.999")
→ (new Mbn(2)).add(new Mbn("1.999")) → "4.00"
- true passed as the last argument to any of the standard methods triggers modification of the original object
(===
true)
- standard functions: two-argument functions and round, floor ceil, intp, abs, inva, invm, sqrt, sgn
- for a = new Mbn(2); b = a.add(1); "a" stays unchanged and "b" is set
to the result
- for a = new Mbn(2); b = a.add(1, true); "a" is changed, but "b"
becomes
simply a reference to "a"
- as the results are Mbn objects, it's possible to use method chaining
- sum of 3 numbers: a = b.add(c).add(d)
ad 2 numbers to "a": a.add(b, true).add(c, true)
- sum of 2 numbers, but not less than zero: b = a.add(x).max(0)
limit "a" to be between two and three: a.max(2, true).min(3, true)
- losing / keeping precision
- results of operations are predictable, but in some cases may lead to loss of precision, which is not
an implementation problem, e.g. result of 0.01*0.01 may be 0.00001, but this number doesn't exist in
Mbn class with 2 digits of fractional part
- some Mbn functions never lead to a loss of precision: add, sub, mod, min, max, fact, inva
- for mul (and precision = 2) it depends on the size of arguments' fractional parts, 1.23*5 or
1.2*3.4 never loses precision, 1.23*4.5 always does
- for div (and precision = 2) it depends, e.g. division of integer by 100 or by one of its divisors
never
loses precision
- pow and sqrt should be used with care
- when loss of precision is necessary, operations should be done in the right order, only the last
operation should lose precision, sometimes multiplication by 100 may be needed
- exact result of 4.11*0.23/2 is 0.47265, and this will be the result such calculations in Mbn
with precision=5, while with precision = 2 the result should be 0.47, but 4.11*0.23/2 =
0.48,
because precision is lost in multiplication and division; to fix this: 411*0.23/200 gives
the
right result, as precision is lost only in division
- 0.01/2*100 = 1, because precision is lost in division, while 0.01*100/2 is correct,
no precision is lost
Exceptions
All exceptions are instances of MbnErr class
JS: MbnErr has field "message", and method "toString" returns that message: ex.message, String(ex)
PHP: MbnErr extends Exception, message available with $ex->getMessage()
Moreover MbnErr has fields "errorKey" and "errorValues" which represent the particular erroneous situation.
Field errorValues contains string representations of values to the message, or is empty when there is no value
to
pass
Possible values of errorKey:
- mbn.invalid_argument - value passed to Mbn constructor is of the wrong type,
e.g.
function, array, ..
- errorValues.v is string representation of value
- new Mbn(function(){}), new Mbn([1,2]),
new Mbn(NaN)
- mbn.invalid_format - string value passed to Mbn constructor is invalid
- errorValues.v is passed string value or string value of passed object
- new Mbn("x"), new Mbn("1..2")
- Mbn({toString:function(){return "x"}})
- mbn.limit_exceeded - value reaches limit of digits from MbnL
- errorValues.v is MbnL, exact value which caused exception is unknown
- new Mbn("=9^9^9"), (new Mbn(1000)).fact()
- mbn.div.zero_divisor - division by zero
- errorValues is empty
- a.div(0), a.mod(0), (new Mbn(0)).invm()
- mbn.pow.unsupported_exponent - only integer exponents are supported
- errorValues.v is the given exponent
- a.pow(0.5), a.pow(1.5), Mbn.calc("2^.5")
- mbn.fact.invalid_value - factorial can be calculated only for non-negative
integers
- errorValues.v is current value
- (new Mbn(-2)).fact(), Mbn.calc("0.5!")
- mbn.sqrt.negative_value - square root can be calculated only for
non-negative
numbers
- errorValues.v is current value
- (new Mbn(-2)).sqrt(), Mbn.calc("sqrt(-2)"),
Mbn.reduce("sqrt", [2, -2])
- mbn.cmp.negative_diff - maximal difference cannot be negative
- errorValues.v is current value
- (new Mbn(2)).cmp(3, -1), (new Mbn(2)).eq(3, -1)
- mbn.extend.invalid_precision - invalid value for precision (MbnP)
- errorValues.v is the given precision
- Mbn.extend(-2), Mbn.extend({MbnP: 0.5})
- PHP: derived classes are not checked in runtime, but method Mbn::prop()
checks it
- class Mbn_5 extends Mbn {protected static $MbnP = 0.5;} Mbn_5::prop();
- mbn.format.invalid_precision - invalid value for precision (MbnP)
- errorValues.v is the given precision
- a.format({MbnP: 0.5}) [js], $a->format(['MbnP' => 0.5])
[php]
- mbn.extend.invalid_separator - invalid value for decimal separator (MbnS)
- errorValues.v is the given separator
- Mbn.extend({MbnS: 1}), Mbn.extend({MbnS: ':'})
- class MbnCol extends Mbn {protected static $MbnS = ':';} MbnCol::prop();
- mbn.format.invalid_separator - invalid value for decimal separator (MbnS)
- errorValues.v is the given separator
- a.format({MbnS: 1}) [js], $a->format(['MbnS' => 1]) [php]
- mbn.extend.invalid_truncation - invalid value for truncation of trailing
zeros
(MbnT)
- errorValues.v is the given truncation
- Mbn.extend({MbnT: 1})
- class MbnT1 extends Mbn {protected static $MbnT = 1;} MbnT1::prop();
- mbn.format.invalid_truncation - invalid value for truncation of trailing
zeros
(MbnT)
- errorValues.v is the given truncation
- a.format({MbnT: 1}) [js], $a->format(['MbnT' => 1]) [php]
- mbn.extend.invalid_evaluating - invalid value for evaluating trigger (MbnE)
- errorValues.v is the given evaluating trigger
- Mbn.extend({MbnE: 1})
- class MbnE1 extends Mbn {protected static $MbnE = 1;} MbnE1::prop();
- mbn.format.invalid_evaluating - invalid value for evaluating trigger (MbnE)
- hint: MbnE doesn't affect format(), but is validated; this behavior may be changed
- errorValues.v is the given evaluating trigger
- a.format({MbnE: 1}) [js], $a->format(['MbnE' => 1]) [php]
- mbn.extend.invalid_formatting - invalid value for formatting (MbnF)
- errorValues.v is the given formatting
- Mbn.extend({MbnF: 1})
- class MbnF1 extends Mbn {protected static $MbnF = 1;} MbnF1::prop();
- mbn.format.invalid_formatting - invalid value for formatting (MbnF)
- errorValues.v is the given formatting
- a.format({MbnF: 1}) [js], $a->format(['MbnF' => 1]) [php]
- mbn.extend.invalid_limit - invalid value digit limit (MbnL)
- errorValues.v is the given limit
- Mbn.extend({MbnE: Infinity}), Mbn.extend({MbnE: -1})
- class MbnLm1 extends Mbn {protected static $MbnL = -1;} MbnLm1::prop();
- mbn.format.invalid_limit - invalid value digit limit (MbnL)
- hint: MbnL doesn't affect format(), but is validated; this behavior may be changed
- errorValues.v is the given limit
- a.format({MbnL: -1}) [js], $a->format(['MbnL' => -1])
[php]
- mbn.calc.undefined - undefined variable in expression
- errorValues.v is name of undefined variable
- Mbn.calc("a*b", {a: 5}) [js], Mbn::calc("a*b", ['a' => 5])
[php]
- mbn.calc.unexpected - unexpected token in expression
- errorValues.v is unexpected token or rest of expression starting with that token
- Mbn.calc("/ 2"), Mbn.calc("(2 * 3")
- mbn.def.undefined - constant is not defined
- errorValues.v is name of undefined constant
- Mbn.def("A")
- mbn.def.already_set - constant already has a value
- errorValues.v is name of constant
- errorValues.w is current value of constant
- Mbn.def("PI", 2), Mbn.def("A", 2); Mbn.def("A", 2)
- mbn.def.invalid_name - invalid name for constant
- errorValues.v is name of constant
- Mbn.def("2", 2), Mbn.def("2"), Mbn.def(null, "2")
- mbn.split.invalid_part_count - invalid number of parts, should be positive
integer
- errorValues.v is number of parts
- a.split(0), a.split(-0.5), a.split([])
- mbn.split.zero_part_sum - sum of parts is zero, value cannot be split
- errorValues is empty
- a.split([-1, 1]), a.split([1, -2, 1])
- mbn.reduce.invalid_function - invalid function name passed to "reduce"
- errorValues.v is the given function name
- a.reduce("x", [1])
- mbn.reduce.no_array - no array given
- errorValues is empty
- a.reduce("sqrt", 1), a.reduce("add", 1, 2)
- mbn.reduce.invalid_argument_count - two arguments passed to single-argument
function
- errorValues is empty
- a.reduce("sqrt", [1, 2], [3, 4]), a.reduce("inva", [1, 2], 3),
a.reduce("abs", 1, [2, 3])
- mbn.reduce.different_lengths - given arrays have different lengths
- errorValues.v is length of first array
- errorValues.w is length of second array
- a.reduce("add", [1, 2], [3])
- mbn.reduce.different_keys - given arrays have different keys
- hint: only may be thrown in PHP
- errorValues.v is keys of first array, e.g. "0,a"
- errorValues.w is keys of second array"
- a::reduce("add", [1, 'a' => 2], [3, 4])
Changelog
- 09.02.2023 - added r0X references, JS: allow creating from bigint (1.52.1)
- 05.07.2022 - added multipart expressions (1.52.0)
- 05.07.2022 - JS: fixed ES3 compatibility, quoted access to properties with reserved names
- 05.07.2022 - fixed factorial, 0! was 0 (since 08.01.2019)
- 22.02.2022 - fixed .d.ts and npm types link (1.51.1)
- 21.02.2022 - fixed problem with mbn from big floats, some implementation changes, npm ready
(1.51)
- 21.02.2022 - string values in error message and errorValues in quotes
- 21.02.2022 - separated parsing integer in PHP, uniformed float parsing with JS
- 31.03.2020 - added {comments} to expression parser (1.50)
- 31.03.2020 - fixed multiline expressions, "2\n+3" is 5, not 2
- 31.03.2020 - invalid grouping like "1 .0" or "1(multiple spaces)0" no longer parsed
- 03.01.2020 - added factorial to reduce
- 13.12.2019 - minor PHP code changes
- 11.12.2019 - changed MbnErr.errorValue (string|null) to errorValues (array[php], object[js])
(1.49)
- 20.11.2019 - fixed PHP 5.4 compatibility (1.48)
- 20.11.2019 - fixed PHP bug when creating basic Mbn object from object of derived class (since 28.09.2017)
- 21.10.2019 - fixed PHP wrong errorValue for reduce.different_keys
- 21.10.2019 - validating constant name also for checking of existence e.g. Mbn::def(null, "2")
- 20.10.2019 - NaN as argument throws mbn.invalid_argument exception instead of mbn.limit_exceeded
- 20.10.2019 - fixed MbnL validation
- 18.10.2019 - format(5) worked as format(false), now throws mbn.format.invalid_formatting exception
- 18.10.2019 - PHP: Mbn::prop() throws mbn.extend, not Mbn.prop exceptions, also mbn.prop exceptions were
broken
- 17.10.2019 - better representation of passed invalid values (1.47)
- 15.10.2019 - all code reformatted to 4-space indents
- 14.10.2019 - fixed wrong message for limit_exceeded (since 10.10.2019)
- 14.10.2019 - fixed JS formatting bug - undeclared variable (since 08.01.2018)
- 11.10.2019 - added omitConsts param to Mbn.check()
- 11.10.2019 - added MbnErr.translate() - error translation function
- 10.10.2019 - added errorKey / errorValue fields to MbnErr (1.46)
- 10.10.2019 - JS: MbnErr accessible as Mbn.MbnErr
- 10.10.2019 - fixed JS bug for constant named "hasOwnProperty"
- 23.09.2019 - PHP: Mbn and MbnErr published separately with namespace
- 19.09.2019 - minor changes and optimisations in Mbn.calc()
- 18.09.2019 - added Mbn.check() - check and get list of used variables (1.45)
- 18.09.2019 - fixed JS bug for variable named "hasOwnProperty" passed to Mbn.calc()
- 24.05.2019 - fixed PHP split bug for mixed positive/negative parts (since 26.02.2019)
(1.44)
[php]
- 23.05.2019 - fixed JS split bug for mixed positive/negative parts (since 26.02.2019) (1.44)
[js]
- 26.02.2019 - allow split with positive and negative parts (1.43)
- 05.02.2019 - added MbnL - digit limit (1.42)
- 05.02.2019 - fixed Mbn.calc() (%!) vs unary operator operator order (since 09.01.2019)
- 22.01.2019 - PHP: added formatting with Mbn* params (1.41) [php]
- 22.01.2019 - fixed PHP factorial - Mbn instead of static (since 09.01.2019)
- 21.01.2019 - fixed (%!) operation order (since 09.01.2019)
- 18.01.2019 - PHP: Mbn.prop() checks Mbn* params
- 09.01.2019 - PHP: factorial
- 08.01.2019 - JS: added formatting with Mbn* params (1.41) [js]
- 08.01.2019 - JS: factorial
- 08.08.2018 - minor changes (1.40)
- 03.04.2018 - added @return and @throws annotations(1.39)
- 03.04.2018 - allow constants starting with lower case
- 22.03.2018 - fixed PHP that toString() wasn't public (__toString() was public)
- 11.03.2018 - PHP: tri-state MbnE, Mbn.calc("==5") is simply parsed as string (1.38) [php]
- 09.03.2018 - JS: tri-state MbnE, Mbn.calc("==5") is simply parsed as string (1.38) [js]
- 07.03.2018 - allow Mbn.calc("=4") (1.37)
- 07.03.2018 - fixed errors for MbnE=false (1.36)
Other methods
Array methods - split - split value into an array
A value can be split into an array of Mbn values that sum up to the original value
- splitting into given number of parts
-
(new Mbn(3)).split() gives array [1.50, 1.50]
(new Mbn(4)).split(3) gives array [1.33, 1.34, 1.33]
(or similar)
- splitting into given proportions
-
(new Mbn(4.5)).split([1, 2]) gives array [1.50, 3.00]
-
(new Mbn(4.5)).split([-1, -2]) gives array [1.50, 3.00]
-
proportions can have mixed signs, so (new Mbn(4.5)).split([-1, 2])
gives array [-4.50, 9.00]
hint: sum of proportions cannot be zero
- PHP: array of proportions can be associative, so (new Mbn(4.5)).split(['a' => 1, 'b' => 2])
gives array ['a' => 1.50, 'b' => 3.00]
Array methods - reduce - map or reduce array
Array can be mapped or reduced with one of the Mbn methods
PHP: array can be associative
- one-argument functions: abs, inva, invm, ceil, floor, sqrt, round, sgn, intp, fact
- result is mapped array, so Mbn.reduce("abs", [1, -2]) is equivalent
to
[1, -2].map(v => (new Mbn(v)).abs()) and returns
[1.00, 2.00]
- PHP: Mbn::reduce("abs", ['a' => 1, 'b' => -2]) gives array
['a' => 1.00, 'b' => 2.00]
- "set" may be used as a function, to create array of Mbn objects without any other actions
- two-argument functions: add, sub, mul, div, mod, min, max, pow
- two-argument function and a single value gives mapped array
- Mbn.reduce("pow", [3, 4, 5], 2) gives array [3^2, 4^2, 5^2] → [9.00, 16.00, 25.00]
- Mbn.reduce("pow", 2, [3, 4, 5]) gives array [2^3, 2^4, 2^5] → [8.00, 16.00, 32.00]
- two-argument function and two arrays give mapped array
- Mbn.reduce("mul", [3, 4, 5], [1, 2, 3]) gives array [3*1, 4*2, 5*3] → [3.00, 8.00, 15.00]
hint: arrays have to have the same length
PHP: hint: associative arrays have to have identical keys
- two-argument function and one array reduces the array
- Mbn.reduce("mul", [3, 4, 5]) gives 3*4*5 = 60.00
- asymmetric functions work identical, not really useful: Mbn.reduce("sub", [3, 4, 5])
gives 3-4-5 = -6.00
- for empty array returns 0.00
Other methods - calc