Home Reference Source

src/StringMask/StringMask.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.
 */

const isNumberRegex = /^\d+$/
const isWhitespaceRegex = /\s/
const punctuationList = '!"#$%&\'()*+,-./:;<=>?@[]^_`{|}~'

/**
 * Check if the given string is in lower case
 *
 * @param {String} str
 */
const isLowerCase = str => {
  return str == str.toLowerCase() && str != str.toUpperCase()
}

/**
 * Check if the given string is in upper case
 *
 * @param {String} str
 */
const isUpperCase = str => {
  return str == str.toUpperCase() && str != str.toLowerCase()
}

const passOrThrowError = (loose, ret, i, str) => {
  if (!loose) {
    const char = str.charAt(i)
    const pos = i + 1
    throw {
      name: 'StringMaskError',
      message: `StringMaskError: error applying mask at position "${pos}" , char "${char}"`,
      pos,
      char
    }
  } else ret[i] = ' '
}

/**
 * NumberMask
 *
 * A javascript implementation for BBj numbers masking
 *
 * @author Hyyan Abo Fakher <habofakher@basis.com>
 */
class StringMask {
  /**
   * Mask the given string with the given mask according to BBj rules
   *
   * @param {String} str the string to mask
   * @param {String} mask the mask to use for formatting
   * @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.
   * 
   * @throws {MaskIsTooShortError}
   * @throws {StringMaskError}
   * @throws {MaskError}
   * 
   * @returns {String} the masked string
   */
  static mask(str, mask, loose = true) {
    str = String(str)
    mask = String(mask)
    const maskLen = mask.length
    const strLen = str.length

    if (strLen > maskLen) {
      if (loose) return str
      // friendly silent fail
      else
        throw {
          name: 'MaskIsTooShortError',
          message: `MaskIsTooShortError: Mask is shorter than the passed string`
        }
    }

    const ret = new Array(maskLen)
    let pos = 0 // to keep track of the current position in the str
    let maskByte = ''

    for (let i = 0; i < maskLen; i++) {
      maskByte = mask.charAt(i)
      switch (maskByte) {
        case 'X': // match any character
          ret[i] = pos < strLen ? str.charAt(pos) : ' '
          ++pos
          break

        case 'A': // match letter; force upper case
          if (pos < strLen) {
            const byte = str.charAt(pos)
            if (isUpperCase(byte)) ret[i] = byte
            else if (isLowerCase(byte)) ret[i] = byte.toUpperCase()
            else passOrThrowError(loose, ret, i, str)
          } else ret[i] = ' '
          ++pos
          break

        case 'a': // match letter
          if (pos < strLen) {
            const byte = str.charAt(pos)
            if (isUpperCase(byte) || isLowerCase(byte)) ret[i] = byte
            else passOrThrowError(loose, ret, i, str)
          } else ret[i] = ' '
          ++pos
          break
        case '0': // match digit
          if (pos < strLen) {
            const byte = str.charAt(pos)
            if (isNumberRegex.test(byte)) ret[i] = byte
            else passOrThrowError(loose, ret, i, str)
          } else ret[i] = ' '
          ++pos
          break
        case 'Z': // match letter or digit; force upper case
          if (pos < strLen) {
            const byte = str.charAt(pos)
            if (isUpperCase(byte) || isNumberRegex.test(byte)) ret[i] = byte
            else if (isLowerCase(byte)) ret[i] = byte.toUpperCase()
            else passOrThrowError(loose, ret, i, str)
          } else ret[i] = ' '
          ++pos
          break
        case 'z': // match letter or digit
          if (pos < strLen) {
            const byte = str.charAt(pos)
            if (
              isUpperCase(byte) ||
              isLowerCase(byte) ||
              isNumberRegex.test(byte)
            )
              ret[i] = byte
            else passOrThrowError(loose, ret, i, str)
          } else ret[i] = ' '
          ++pos
          break

          break
        case 'U': // match letter (force upper case), digit, whitespace or punctuation.
          if (pos < strLen) {
            const byte = str.charAt(pos)
            if (isLowerCase(byte)) ret[i] = byte.toUpperCase()
            else if (
              isUpperCase(byte) ||
              isNumberRegex.test(byte) ||
              isWhitespaceRegex.test(byte) ||
              punctuationList.indexOf(byte) > -1
            )
              ret[i] = byte
            else passOrThrowError(loose, ret, i, str)
          } else ret[i] = ' '
          ++pos
          break
        default:
          ret[i] = maskByte
          break
      }
    }

    if (pos < strLen) {
      if (!loose) {
        throw { name: 'MaskError', message: 'Mask cannot be applied' }
      }
    }

    return ret.join('')
  }
}

export default StringMask