Source: phony.js

/**
 * Library for translating to/from the phonetic alphabet.
 *
 * @module phony
 * @version 1.3.0
 * @copyright Alasdair Mercer 2015
 * @license MIT
 */
(function(factory) {
  'use strict';

  /**
   * The root object that has been determined for the current environment (browser, server, <code>WebWorker</code>).
   *
   * @access private
   * @type {*}
   */
  var root = (typeof self === 'object' && self.self === self && self) ||
      (typeof global === 'object' && global.global === global && global);

  if (typeof define === 'function' && define.amd) {
    // Defines for AMD but also exports to root for those expecting global phony
    define(function() {
      root.phony = factory(root);

      return root.phony;
    });
  } else if (typeof module !== 'undefined' && module.exports) {
    // Supports Node.js and the CommonJS patterns
    exports = module.exports = factory(root);
  } else {
    // Falls back on browser support
    root.phony = factory(root);
  }
}(function(root) {
  'use strict';

  /**
   * A phonetic alphabet configuration.
   *
   * @typedef {Object} PhoneticAlphabet
   * @property {Object.<String, String>} characters - The map of characters and their phonetic counterparts.
   * @property {String} [fallback] - The name of the fallback alphabet.
   */

  /**
   * Options to be passed to the primary {@linkcode phony} methods.
   *
   * @typedef {Object} PhonyOptions
   * @property {String} [alphabet="itu"] - The default name of the alphabet to be used for the translation.
   * @property {Boolean} [cache=true] - Whether a cache should be used for built alphabets to to improve performance.
   * This cache can be disabled and/or cleared to avoid when modifying alphabets.
   * @property {String} [letterSplitter=" "] - The default string to be used to split alphabetic letters (may be passed
   * to <code>RegExp</code> constructor).
   * @property {String} [wordSplitter="space"] - The default string to be used to split words (may be passed to
   * <code>RegExp</code> constructor).
   */

  /**
   * The main <code>phony</code> object to be exported.
   *
   * @access public
   * @type {Object}
   * @alias module:phony
   */
  var phony = {};

  /**
   * The previous value of the <code>phony</code> variable.
   *
   * @access private
   * @type {*}
   */
  var previousPhony = root.phony;

  /**
   * A cache of built alphabets that can be used to improve performance and avoid unnecessary rebuilds of the same
   * alphabets.
   * <p/>
   * This cache can be cleared at any time by using {@linkcode phony.clearCache} or bypassed on a case-by-case basis
   * via the <code>cache</code> option.
   *
   * @access private
   * @type {Object.<String, Object<String, String>>}
   */
  var alphabetCache = {};

  /**
   * Populates the specified <code>target</code> mapping with key/value pairs extracted from the character mapping from
   * the named alphabet those from the fallback chain, where applicable.
   * <p/>
   * The key/value pairs are determined by the return value of the <code>iterator</code> provided, which should provide
   * a simple two-element array where the first element will be used as the key and the second as the value. However,
   * that key/value pair will only be set on <code>target</code> if a previous alphabet hasn't already set the same
   * key.
   *
   * @param {Object.<String, String>} target - the mapping to be populated
   * @param {String} name - the name of the alphabet to be built
   * @param {String} method - the name of the method responsible for building this alphabet (used for caching purposes)
   * @param {Boolean} cache - <code>true</code> to check and use a previously built and cached alphabet or caching this
   * one after building it if not previously cached; otherwise <code>false</code>
   * @param {Function} iterator - iterator which will be passed a character and phonetic mapping and needs to return a
   * two-element array for the key/value mapping to be set on <code>target</code>
   * @returns {Object.<String, String>} The populated <code>target</code>.
   * @access private
   */
  function buildAlphabet(target, name, method, cache, iterator) {
    var alphabet = phony.alphabets[name];

    if (!alphabet) {
      return target;
    }

    var cacheHash = name + ':' + method;

    if (cache && alphabetCache[cacheHash]) {
      return alphabetCache[cacheHash];
    }

    each(alphabet.characters, function(character, phonetic) {
      var keyValue = iterator(character, phonetic);
      var key = keyValue[0];
      var value = keyValue[1];

      if (!target.hasOwnProperty(key)) {
        target[key] = value;
      }
    });

    buildAlphabet(target, alphabet.fallback, method, false, iterator);

    if (cache) {
      alphabetCache[cacheHash] = target;
    }

    return target;
  }

  /**
   * Iterates over a given <code>object</code>.
   * <p/>
   * If <code>object</code> is an array, each element is yielded in turn to the specified <code>iterator</code>
   * function; otherwise this is done for the key/value mapping of the hash.
   *
   * @param {Array|Object} [object] - the object or array to be iterated over
   * @param {Function} iterator - the iterator function to be passed index/element if <code>object</code> is an array
   * or key/value if it is a hash
   * @returns {Array|Object} The specified <code>object</code>.
   * @access private
   */
  function each(object, iterator) {
    if (!object) {
      return object;
    }

    var key;
    var index;
    var length = object.length;

    if (length === +length) {
      for (index = 0; index < length; index++) {
        iterator(object[index], index, object);
      }
    } else {
      for (key in object) {
        if (object.hasOwnProperty(key)) {
          iterator(key, object[key], object);
        }
      }
    }

    return object;
  }

  /**
   * Returns the given <code>options</code> with all of the <code>defaults</code> applied.
   * <p/>
   * This function <i>will change</i> the specified <code>options</code>.
   *
   * @param {PhonyOptions} [options={}] - the options to be extended
   * @param {PhonyOptions} defaults - the default options
   * @returns {PhonyOptions} The specified <code>options</code> with modifications.
   * @access private
   */
  function getOptions(options, defaults) {
    options = options || {};

    for (var key in defaults) {
      if (typeof options[key] === 'undefined') {
        options[key] = defaults[key];
      }
    }

    options.alphabet = options.alphabet.toLocaleLowerCase();
    options.wordSplitter = options.wordSplitter.toLocaleLowerCase();

    return options;
  }

  /**
   * Prepares a given string to simplify translation.
   *
   * @param {String} str - the string to prepare
   * @param {String} [transformer] - the name of a <code>String.prototype</code> method to be used to transform
   * <code>str</code> prior to preparation
   * @param {String} wordSplitter - the string used to split words (will be passed to <code>RegExp</code> constructor)
   * @param {String} letterSplitter - the string used to split alphabetic letters
   * @returns {Array.<Array.<String>>} A multi-dimensional array of words and their alphabetic letters contained
   * within.
   * @access private
   */
  function prepare(str, transformer, wordSplitter, letterSplitter) {
    if (typeof str !== 'string') {
      throw new TypeError('Invalid value type: ' + typeof str);
    }

    if (transformer && typeof str[transformer] === 'function') {
      str = str[transformer]();
    }

    var rWordSplitter = new RegExp(wordSplitter + '|[\\n\\r]+', 'gi');
    var result = str.trim().split(rWordSplitter);

    each(result, function(word, i) {
      result[i] = word.split(letterSplitter);
    });

    return result;
  }

  /**
   * Transforms a given string in to title case.
   *
   * @param {String} str - the string to be transformed
   * @returns {String} <code>str</code> in title case form.
   * @access private
   */
  function toTitleCase(str) {
    return str[0].toLocaleUpperCase() + str.substring(1).toLocaleLowerCase();
  }

  /**
   * The current version of {@linkcode phony}.
   *
   * @access public
   * @static
   * @constant
   * @type {String}
   */
  phony.VERSION = '1.3.0';

  /**
   * The configurations for the initially supported phonetic alphabets.
   *
   * @access public
   * @static
   * @type {Object.<String, PhoneticAlphabet>}
   * @property {PhoneticAlphabet} ansi - The American National Standards Institute (ANSI) phonetic alphabet
   * configuration.
   * @property {PhoneticAlphabet} faa - The Federal Aviation Administration (FAA) phonetic alphabet configuration.
   * @property {PhoneticAlphabet} icao - The International Civil Aviation Organization (ICAO) phonetic alphabet
   * configuration.
   * @property {PhoneticAlphabet} itu - The International Telecommunication Union (ITU) phonetic alphabet
   * configuration.
   */
  phony.alphabets = {
    ansi: {
      fallback: 'itu',
      characters: {
        'A': 'alpha',
        'J': 'juliet'
      }
    },
    faa: {
      fallback: 'itu',
      characters: {
        '0': 'zero',
        '1': 'one',
        '2': 'two',
        '3': 'three',
        '4': 'four',
        '5': 'five',
        '6': 'six',
        '7': 'seven',
        '8': 'eight',
        '9': 'nine'
      }
    },
    icao: {
      fallback: 'faa',
      characters: {
        '9': 'niner'
      }
    },
    itu: {
      characters: {
        'A': 'alfa',
        'B': 'bravo',
        'C': 'charlie',
        'D': 'delta',
        'E': 'echo',
        'F': 'foxtrot',
        'G': 'golf',
        'H': 'hotel',
        'I': 'india',
        'J': 'juliett',
        'K': 'kilo',
        'L': 'lima',
        'M': 'mike',
        'N': 'november',
        'O': 'oscar',
        'P': 'papa',
        'Q': 'quebec',
        'R': 'romeo',
        'S': 'sierra',
        'T': 'tango',
        'U': 'uniform',
        'V': 'victor',
        'W': 'whiskey',
        'X': 'x-ray',
        'Y': 'yankee',
        'Z': 'zulu',
        '0': 'nadazero',
        '1': 'unaone',
        '2': 'bissotwo',
        '3': 'terrathree',
        '4': 'kartefour',
        '5': 'pantafive',
        '6': 'soxisix',
        '7': 'setteseven',
        '8': 'oktoeight',
        '9': 'novenine',
        '.': 'stop',
        '-': 'dash'
      }
    }
  };

  /**
   * The default values to be used when options are not specified or incomplete.
   *
   * @access public
   * @static
   * @type {PhonyOptions}
   */
  phony.defaults = {
    alphabet: 'itu',
    cache: true,
    letterSplitter: ' ',
    wordSplitter: 'space'
  };

  /**
   * Clears any previously cached built alphabets.
   * <p/>
   * This can be useful for when modifications have been made to the alphabet mapping.
   *
   * @returns {Object} A reference to this {@linkcode phony} for chaining.
   * @access public
   * @static
   */
  phony.clearCache = function() {
    alphabetCache = {};

    return this;
  };

  /**
   * Translates the <code>message</code> provided <i>from</i> the phonetic alphabet.
   * <p/>
   * The message may not be translated correctly if the some of the options used to translate the message originally
   * are not the same as those in <code>options</code>.
   *
   * @param {String} [message=""] - the string to be translated from the phonetic alphabet
   * @param {PhonyOptions} [options={}] - the options to be used
   * @returns {String} The translation from the specified phonetic alphabet <code>message</code>.
   * @access public
   * @static
   */
  phony.from = function(message, options) {
    message = message || '';
    options = getOptions(options, phony.defaults);

    var result = '';
    var value = prepare(message, 'toLocaleLowerCase', options.wordSplitter, options.letterSplitter);

    // Ensures message was prepared successfully and that a valid alphabet was specified
    if (!value || !phony.alphabets[options.alphabet]) {
      return result;
    }

    var alphabet = buildAlphabet({}, options.alphabet, 'from', options.cache, function(character, phonetic) {
      return [phonetic, character];
    });

    // Iterates over each word
    each(value, function(word, i) {
      // Inserts space between each word
      if (i > 0) {
        result += ' ';
      }

      // Iterates over each phonetic representation in the word
      each(word, function(phonetic) {
        // Reverse engineers character from phonetic representation
        var character = alphabet[phonetic];

        // Checks if character is supported
        if (typeof character === 'string') {
          result += character;
        }
      });
    });

    return result;
  };

  /**
   * Translates the <code>message</code> provided <i>to</i> the phonetic alphabet.
   *
   * @param {String} [message=""] - the string to be translated to the phonetic alphabet
   * @param {PhonyOptions} [options={}] - the options to be used
   * @returns {String} The phonetic alphabet translation of the specified <code>message</code>.
   * @access public
   * @static
   */
  phony.to = function(message, options) {
    message = message || '';
    options = getOptions(options, phony.defaults);

    var letterSplitter = options.letterSplitter;
    var result = '';
    var value = prepare(message, 'toLocaleUpperCase', '\\s+', '');
    var wordSplitter = letterSplitter + toTitleCase(options.wordSplitter) + letterSplitter;

    // Ensures message was prepared successfully and that a valid alphabet was specified
    if (!value || !phony.alphabets[options.alphabet]) {
      return result;
    }

    var alphabet = buildAlphabet({}, options.alphabet, 'to', options.cache, function(character, phonetic) {
      return [character, phonetic];
    });

    // Iterates over each word
    each(value, function(word, i) {
      // Inserts wordSplitter option between each word
      if (i > 0) {
        result += wordSplitter;
      }

      // Iterates over each character in the word
      each(word, function(character, j) {
        // Reverse engineers character from phonetic representation
        var phonetic = alphabet[character];

        // Checks if phonetic representation is supported
        if (typeof phonetic === 'string') {
          // Inserts letterSplitter option between each character
          if (j > 0) {
            result += letterSplitter;
          }

          result += toTitleCase(phonetic);
        }
      });
    });

    return result;
  };

  /**
   * Runs phony in <i>no conflict</i> mode, returning the <code>phony</code> global variable to it's previous owner, if
   * any.
   *
   * @returns {Object} A reference to this {@linkcode phony}.
   * @access public
   * @static
   */
  phony.noConflict = function() {
    root.phony = previousPhony;

    return this;
  };

  return phony;
}));