src/NumberMask/NumberMask.js
/*
* This file is part of bbj-masks lib.
* (c) Basis Europe <eu@basis.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* NumberMask
*
* A javascript implementation for BBj numbers masking
*
* @author Hyyan Abo Fakher <habofakher@basis.com>
*/
class NumberMask {
/**
* Mask the given number with the given mask according to BBj rules
*
* @param {Number} number the number to format
* @param {String} mask the mask to use for formatting
* @param {String} [groupingSeparator=,] - a char which will be used as a grouping separator
* @param {String} [decimalSeparator=.] - a char which will be used as a decimal separator
* @param {Boolean} [forceTrailingZeros=false] - Affects the output by switching the way a mask with "#" characters in the trailing positions is filled.
* for example, the function `NumberMask.mask(.10:"#.##")` returns ` .10` instead of ` .1 `
* @param {Boolean} [loose=true] when true , errors will be ignored and the method will try at apply the mask
* anyway , otherwise it will stop at first error and throw it.
* @param {Boolean} [ignoreFillChar=false] - when true , then the fill char will always be an empty space
* event if the mask start with a `*`
* @param {Boolean} [trimSpaces=false] - When true , the final masked value will not contain any spaces
* @param {Boolean} [floatSpecialChars=true] - When true , then if any of "-", "+", "$", and "(". characters
* is present in the mask, the first one encountered will be moved
* to the last position where a "#" or "," was replaced by the fill
* character. If no such position exists, the float character is
* left where it
*
* @throws {MaskError} only if loose is disabled
*
* @returns {String} the masked number
*/
static mask(
number,
mask,
groupingSeparator = ',',
decimalSeparator = '.',
forceTrailingZeros = false,
loose = true,
ignoreFillChar = false,
trimSpaces = false,
floatSpecialChars = true
) {
const groupingSeparatorPlaceholder = '__GROUPING__SEPARATOR__PLACEHOLDER__';
const decimalSeparatorPlaceholder = '__DECIMAL__SEPARATOR__PLACEHOLDER__'
const maskLen = mask.length
if (0 === maskLen) {
if (loose) return str
// friendly silent fail
else
throw {
name: 'MaskError',
message: `MaskError: Mask is empty`
}
}
// Get magnitude and precision of MASK
let maskBeforeDecimal = 0
let maskAfterDecimal = 0
let foundDecimal = false
for (let i = 0; i < maskLen; ++i) {
const m = mask.charAt(i)
if (m == '0' || m == '#') {
if (foundDecimal) ++maskAfterDecimal
else ++maskBeforeDecimal
} else if (m == '.') foundDecimal = true
}
let num = NumberMask._round(number, maskAfterDecimal)
let bytes = NumberMask._toCharArray(num)
// Get magnitude and precision of NUMBER
let inLen = bytes.length
let numBeforeDecimal = 0
let numAfterDecimal = 0
foundDecimal = false
for (let i = 0; i < inLen; ++i) {
if (bytes[i] == '.') foundDecimal = true
else {
if (foundDecimal) ++numAfterDecimal
else ++numBeforeDecimal
}
}
// always ignore mask overflow
if (numBeforeDecimal > maskBeforeDecimal) {
if (loose) return number.toString()
// friendly silent fail
else
throw {
name: 'MaskError',
message: `MaskError: Number is too large for mask`
}
}
// round if mask is for a lower precision number
if (numAfterDecimal > maskAfterDecimal) {
num = NumberMask._round(num, maskAfterDecimal)
bytes = NumberMask._toCharArray(num)
inLen = bytes.length
// Get new magnitude and precision of NUMBER
numBeforeDecimal = 0
numAfterDecimal = 0
foundDecimal = false
for (let i = 0; i < inLen; ++i) {
if (bytes[i] == '.') foundDecimal = true
else {
if (foundDecimal) ++numAfterDecimal
else ++numBeforeDecimal
}
}
// always ignore mask overflow
if (numBeforeDecimal > maskBeforeDecimal) {
if (loose) return number.toString()
// friendly silent fail
else
throw {
name: 'MaskError',
message: `MaskError: Number is too large for mask`
}
}
}
let fillByte = ' ',
floatByte = ' '
let inPos = 0,
outPos = 0,
floatPos = 0
if (mask.charAt(0) == '*' && ignoreFillChar === false) fillByte = '*'
const fillInit = fillByte
const isNegative = NumberMask._getSign(num) < 0
let emitDecimal = inLen > 0 || mask.indexOf('0') >= 0
let foundZero = false
let foundDigit = false
foundDecimal = false
let ret = new Array(maskLen)
for (let maskPos = 0; maskPos < maskLen; ++maskPos) {
let m = mask.charAt(maskPos)
switch (m) {
case '0':
--maskBeforeDecimal
if (maskBeforeDecimal < numBeforeDecimal && inPos < inLen) {
ret[outPos] = bytes[inPos]
++inPos
foundDigit = true
} else {
ret[outPos] = '0'
foundZero = true
}
++outPos
break
case '#':
--maskBeforeDecimal
if (maskBeforeDecimal < numBeforeDecimal && inPos < inLen) {
ret[outPos] = bytes[inPos]
++inPos
foundDigit = true
} else {
ret[outPos] =
foundDecimal &&
forceTrailingZeros &&
NumberMask._getSign(num) != 0
? '0'
: fillByte
if (!foundDecimal) floatPos = maskPos
}
++outPos
break
case ',':
if (foundZero || inPos > 0) ret[outPos] = groupingSeparatorPlaceholder
else {
ret[outPos] = fillByte
if (!foundDecimal) floatPos = maskPos
}
++outPos
break
case '-':
if (!foundDigit && (floatByte == ' ' && floatSpecialChars)) {
if (isNegative) floatByte = '-'
ret[outPos] = fillByte
floatPos = foundDecimal ? -1 : maskPos
} else ret[outPos] = isNegative ? '-' : fillByte
++outPos
break
case '+':
if (!foundDigit && (floatByte == ' ' && floatSpecialChars)) {
floatByte = isNegative ? '-' : '+'
ret[outPos] = fillByte
floatPos = foundDecimal ? -1 : maskPos
} else ret[outPos] = isNegative ? '-' : '+'
++outPos
break
case '$':
if (!foundDigit && (floatByte == ' ' && floatSpecialChars)) {
floatByte = '$'
ret[outPos] = fillByte
floatPos = foundDecimal ? -1 : maskPos
} else {
ret[outPos] = '$'
}
// ret[outPos] = '$'
++outPos
break
case '(':
if (!foundDigit && (floatByte == ' ') && floatSpecialChars) {
if (isNegative) floatByte = '('
ret[outPos] = fillByte
floatPos = foundDecimal ? -1 : maskPos
} else {
if (isNegative) {
ret[outPos] = '('
} else {
ret[outPos] = foundDecimal ? ' ' : fillByte
}
}
// if(floatSpecialChars) {
// if (!foundDigit && (floatByte == ' ')) {
// if (isNegative) floatByte = '('
// ret[outPos] = fillByte
// floatPos = foundDecimal ? -1 : maskPos
// } else {
// if (isNegative) {
// ret[outPos] = '('
// } else {
// ret[outPos] = foundDecimal ? ' ' : fillByte
// }
// }
// } else {
// ret[outPos] = '('
// }
++outPos
break
case ')':
if (isNegative) {
ret[outPos] = ')'
} else {
ret[outPos] = foundDecimal ? ' ' : fillByte
}
// if(floatSpecialChars) {
// if (isNegative) {
// ret[outPos] = ')'
// } else {
// ret[outPos] = foundDecimal ? ' ' : fillByte
// }
// } else {
// ret[outPos] = ')'
// }
++outPos
break
case 'C':
if (maskPos < maskLen - 1 && mask.charAt(maskPos + 1) == 'R') {
if (isNegative) {
ret[outPos] = 'C'
ret[outPos + 1] = 'R'
} else {
ret[outPos] = ' '
ret[outPos + 1] = ' '
}
outPos += 2
++maskPos
} else {
ret[outPos] = 'C'
++outPos
}
break
case 'D':
if (maskPos < maskLen - 1 && mask.charAt(maskPos + 1) == 'R') {
if (isNegative) {
ret[outPos] = 'C'
ret[outPos + 1] = 'R'
} else {
ret[outPos] = 'D'
ret[outPos + 1] = 'R'
}
outPos += 2
++maskPos
} else {
ret[outPos] = 'D'
++outPos
}
break
case '*':
ret[outPos] = '*'
++outPos
break
case '.':
ret[outPos] = emitDecimal ? decimalSeparatorPlaceholder : fillByte
fillByte = ' '
foundDecimal = true
++inPos
++outPos
break
case 'B':
ret[outPos] = ' '
++outPos
break
default:
ret[outPos] = m
++outPos
break
}
}
if (floatByte != ' ') {
if (floatPos < 0) floatPos = outPos
while (floatPos >= maskLen) --floatPos
if (ret[floatPos] == fillInit) ret[floatPos] = floatByte
}
ret = ret.join('')
if (trimSpaces) ret = ret.replace(/\s/g, '')
ret = ret.replaceAll(groupingSeparatorPlaceholder, groupingSeparator);
ret = ret.replaceAll(decimalSeparatorPlaceholder, decimalSeparator);
return ret
}
static _shift(number, precision, reverseShift) {
if (reverseShift) precision = -precision
var numArray = ('' + number).split('e')
return +(
numArray[0] +
'e' +
(numArray[1] ? +numArray[1] + precision : precision)
)
}
static _round(number, precision) {
return NumberMask._shift(
Math.round(NumberMask._shift(number, precision, false)),
precision,
true
)
}
static _toCharArray(number) {
const signum = NumberMask._getSign(number)
let chars = []
if (signum !== 0) {
let string = signum < 0 ? `${-1 * number.toString()}` : number.toString()
if (string.length > 1 && string.charAt(0) == '0')
string = string.substring(1)
// The string contains only [0-9] and '.'
chars = string.split('')
}
return chars
}
/**
* Returns the sign of a number
*
* @param {Number} x number
* @returns {Number} A number representing the sign of the given argument.
* If the argument is a positive number, negative number, positive zero
* or negative zero, the function will return 1, -1, 0 or -0 respectively.
* Otherwise, NaN is returned.
*/
static _getSign(x) {
return (x > 0) - (x < 0) || +x
}
}
export default NumberMask