text: //     wink-ner
  //     Language agnostic named entity recognizer
  //
  //     Copyright (C) 2017-19  GRAYPE Systems Private Limited
  //
  //     This file is part of “wink-ner”.
  //
  //     Permission is hereby granted, free of charge, to any person obtaining a
  //     copy of this software and associated documentation files (the "Software"),
  //     to deal in the Software without restriction, including without limitation
  //     the rights to use, copy, modify, merge, publish, distribute, sublicense,
  //     and/or sell copies of the Software, and to permit persons to whom the
  //     Software is furnished to do so, subject to the following conditions:
  //
  //     The above copyright notice and this permission notice shall be included
  //     in all copies or substantial portions of the Software.
  //
  //     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
  //     OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  //     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
  //     THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  //     LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  //     FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
  //     DEALINGS IN THE SOFTWARE.
  
  //
  var singularize = require( 'wink-lemmatizer' ).lemmatizeNoun;
  var helpers = require( 'wink-helpers' );
  
  // ### ner
  /**
   *
   * Creates an instance of {@link NER}.
   *
   * @return {NER} object conatining set of API methods for named entity
   * training, recognition, etc.
   * @example
   * // Load wink ner.
   * var ner = require( 'wink-ner' );
   * // Create your instance of wink ner.
   * var myNER = ner();
  */
  var ner = function () {
  
    /**
     * @classdesc NER class
     * @class NER
     * @hideconstructor
     */
    var methods = Object.create( null );
    // Configuration - what tags or values to ignore during recognition phase.
    var cfg = Object.create( null );
    // Learning from single word entities and the first word of multi-word entities
    // goes here. For multi-words entity, the first word goes here; and the word count
    // is pushed into a words-count array. This array may have multiple entries if
    // the first word is common between multiple wluti-word entities. Similarly
    // if the first word is shared between a single word and multi-word entities,
    // then this will contain the properties of the single word entity also.
    // The array of word-counts is sorted in descending order — thus giving preference
    // to longest word-length entity during detection.
    var uniWordEntities = Object.create( null );
    // Learning from multiple word entities goes here.
    var multiWordEntities = Object.create( null );
  
    // ### defineConfig
    /**
     *
     * Defines the criteria to ignore one or more
     * [**tokens**](http://winkjs.org/wink-tokenizer/) during entity detection.
     * The criteria is specified in terms of array of specific **tags** and/or **values**
     * to ignore; this means if any of the listed tag or value is found in a token,
     * it is ignored and it’s value is not considered during entity recognition.
     *
     * For example by including `punctuation`  in the array of tags to ignore,
     * tokens containing punctuations like `-` or `.` will be skipped. This will
     * result in recognition of **kg** and **k.g.** as **kg** (kilogram symbol)
     * or **Guinea-Bissau** and **Guinea Bissau** as **Guinea-Bissau**
     * (a country in West Africa).
     *
     * @method NER#defineConfig
     * @param {object} config — defines the `values` and/or `tags` to be ignore
     * during entity detection. Note if the match occurs in any one of the array,
     * the token is ignored.
     *
     * *An empty config object is equivalent to setting default configuration.*
     *
     * The table below details the properties of `config` object:
     * @param {string[]} [config.valuesToIgnore=undefined] contains **values**
     * to be ignored.
     * @param {string[]} [config.tagsToIgnore=[ 'punctuation' ]] contains **tags**
     * to be ignored. Duplicate and invaid tags, if any, are ignored.
     * Note: `number` and `word` tags can never be ignored.
     * @param {string[]} [config.ignoreDiacritics=true] a `true` ensures that diacritic
     * marks are ignored, whereas `false` will ensure that they are not ignored.
     * @return {object} a copy of configuration defined.
     * @throws {error} if `valuesToIgnore` is not an array of strings.
     * @throws {error} if `tagsToIgnore` is not an array of strings.
     * @example
     * // Do not ignore anything!
     * myNER.defineConfig( { tagsToIgnore: [], ignoreDiacritics: false } );
     * // -> { tagsToIgnore: [], valuesToIgnore: [], ignoreDiacritics: false }
     *
     * // Ignore only '-' and '.'
     * myNER.defineConfig( {
     *   tagsToIgnore: [],
     *   valuesToIgnore: [ '-', '.' ],
     *   ignoreDiacritics: false
     * } );
     * // -> {
     * //      tagsToIgnore: [],
     * //      valuesToIgnore: [ '-', '.' ],
     * //      ignoreDiacritics: false
     * //    }
    */
    var defineConfig = function ( config ) {
      // Check if `config` is a valid object.
      if ( !helpers.validate.isObject( config ) ) {
        throw Error( 'wink-ner/defineConfig: config must be an object, instead found: ' + JSON.stringify( config ) );
      }
      // Check if `valuesToIgnore` is an array provided it is a truthy.
      if ( config.valuesToIgnore && !helpers.validate.isArray( config.valuesToIgnore ) ) {
        throw Error( 'wink-ner/defineConfig: valuesToIgnore must be an array, instead found: ' + JSON.stringify( config.valuesToIgnore ) );
      }
      // Check if `tagsToIgnore` is an array provided it is a truthy.
      if ( config.tagsToIgnore && !helpers.validate.isArray( config.tagsToIgnore ) ) {
        throw Error( 'wink-ner/defineConfig: tagsToIgnore must be an array, instead found: ' + JSON.stringify( config.tagsToIgnore ) );
      }
  
      var validTags = Object.create( null );
      // Valid tags to exclude `number` and `word` tags.
      validTags.emoticon = true;
      validTags.email = true;
      validTags.emoji = true;
      validTags.hashtag = true;
      validTags.mention = true;
      validTags.quoted_phrase = true; // eslint-disable-line camelcase
      validTags.currency = true;
      validTags.time = true;
      validTags.url = true;
      validTags.unknown = true;
  
      cfg.tagsToIgnore = Object.create( null );
      cfg.valuesToIgnore = Object.create( null );
      if ( Object.keys( config ).length === 0 ) {
        // Empty `config` means, restore default configuration!
        cfg.tagsToIgnore.punctuation = true;
        cfg.ignoreDiacritics = true;
      } else {
        // Configure diacritics.
        if ( config.ignoreDiacritics !== undefined ) cfg.ignoreDiacritics =  !!config.ignoreDiacritics;
  
        // Configure tags to ignore.
        let ignore = config.tagsToIgnore;
        if ( ignore ) {
          for ( let i = 0; i < ignore.length; i += 1 ) {
            const key = ignore[ i ];
            if ( key && ( typeof key !== 'string' ) ) {
              throw Error( 'wink-ner/defineConfig: tagsToIgnore must contain strings, instead found: ' + JSON.stringify( ignore ) );
            }
            // Include only valid tags; others are ignored.
            if ( validTags[ key ] ) cfg.tagsToIgnore[ key ] = true;
          }
        } // if ( !ignore )...
        // Configure values to ignore.
        ignore = config.valuesToIgnore;
        if ( ignore ) {
          for ( let i = 0; i < ignore.length; i += 1 ) {
            const key = ignore[ i ];
            if ( key && ( typeof key !== 'string' ) ) {
              throw Error( 'wink-ner/defineConfig: valuesToIgnore must contain strings, instead found: ' + JSON.stringify( ignore ) );
            }
            cfg.valuesToIgnore[ key ] = true;
          }
        } // if ( !ignore )...
      }
  
      return {
        tagsToIgnore: Object.keys( cfg.tagsToIgnore),
        valuesToIgnore: Object.keys( cfg.valuesToIgnore ),
        ignoreDiacritics: cfg.ignoreDiacritics
      };
    }; // defineConfig()
  
    // ### cloneEntity
    /**
     *
     * Clones the input entity — `e` after excluding it's `text` property.
     *
     * @method NER#cloneEntity
     * @param {object} e — to be cloned.
     * @return {object} the clone.
     * @private
    */
    var cloneEntity = function ( e ) {
      var clone = Object.create( null );
  
      for ( const key in e ) {
        if ( key !== 'text' ) clone[ key ] = e[ key ];
      }
      return clone;
    }; // cloneEntity()
  
    // ### copyKVPs
    /**
     *
     * Assign all the key/value pairs of `source` to `target`, excluding **wordCounts**.
     *
     * @method NER#copyKVPs
     * @param {object} target — to which key/value pairs from source are copied.
     * @param {object} source — of key/value pairs.
     * @return {object} the updated `target`.
     * @private
    */
    var copyKVPs = function ( target, source ) {
      //
      for ( var key in source ) {
        if ( key !== 'wordCounts' ) target[ key ] = source[ key ];
      }
      return target;
    }; // copyKVPs()
  
    // ### normalize
    /**
     *
     * Normalizes the input according to the diacritic configuration.
     *
     * @method NER#normalize
     * @param {string} value — to be normalized.
     * @return {string} normalized `value.`
     * @private
    */
    var normalize = function ( value ) {
      return (
        ( cfg.ignoreDiacritics ) ?
          helpers.string.normalize( value ) :
          value.toLowerCase()
        );
    }; // normalize()
  
    // ### addUniWordEntity
    /**
     *
     * Process uni-word entity and adds it to `uniWordEntities`.
     *
     * @method NER#addUniWordEntity
     * @param {string[]} words — from entity's text.
     * @param {object} entity — to be added.
     * @return {undefined} nothing!
     * @private
    */
    var addUniWordEntity = function ( words, entity ) {
      const firstWord = words[ 0 ];
      let wordCounts;
      // Latest value overrides previous value, if any.
      if ( uniWordEntities[ firstWord ] ) wordCounts = uniWordEntities[ firstWord ].wordCounts;
      uniWordEntities[ firstWord ] = cloneEntity( entity );
      if ( wordCounts ) uniWordEntities[ firstWord ].wordCounts = wordCounts;
    }; // addUniWordEntity()
  
    // ### addUniWordEntity
    /**
     *
     * Process multi-word entity and adds it to `multiWordEntities`.
     *
     * @method NER#addMultiWordEntity
     * @param {string} text — property of entity.
     * @param {string[]} words — from entity's text.
     * @param {object} entity — to be added.
     * @return {undefined} nothing!
     * @private
    */
    var addMultiWordEntity = function ( text, words, entity ) {
      const firstWord = words[ 0 ];
      uniWordEntities[ firstWord ] = uniWordEntities[ firstWord ] || Object.create( null );
      uniWordEntities[ firstWord ].wordCounts = uniWordEntities[ firstWord ].wordCounts || [];
      if ( uniWordEntities[ firstWord ].wordCounts.indexOf( words.length ) === -1 ) uniWordEntities[ firstWord ].wordCounts.push( words.length );
      multiWordEntities[ words.join( ' ' ) ] = cloneEntity( entity );
      // The expression is a simple arithmatic formulation to detect acronyms.
      if ( words.length === ( ( text.length + 1 ) / 2 ) ) addUniWordEntity( [ words.join( '' ) ], entity );
    }; // addMultiWordEntity()
  
    // ### learn
    /**
     *
     * Learns the entities that must be detected via `recognize()/predict()` API
     * calls in a sentence that has been already tokenized either using
     * [wink-tokenizer](https://www.npmjs.com/package/wink-tokenizer) or follows
     * it's token format.
     *
     * It can be used to learn or update learnings incrementally; but it can not be
     * used to unlearn or delete one or more entities.
     *
     * If duplicate entity definitions are enountered then all the entries except
     * the **last one** are ignored.
     *
     * Acronyms must be added with space between each character; for example USA
     * should be added as `'u s a'` — this ensure correct detection of
     * `U S A` or `U. S. A.` or `U.S.A.` as `USA` \[Refer to the example below\].
     *
     * @method NER#learn
     * @param {object[]} entities — where each element defines an entity via
     * two mandatory properties viz. `text` and `entityType` as described later.
     * Note if an element is *not an object* or *does not contain the mandatory
     * properties,* it is ignored.
     *
     * In addition to these two properties, you may optionally define two more
     * properties viz. `uid` and `value`, as described in the table below.
     *
     * <b>Note:</b> Apart from the above mentioned properties, you may also define additional properties .
     * Such properties, along with their values, will be copied to the output token as-is for consumption
     * by any down stream code in the NLP pipe. An example use-case is pos tagging.
     * You can define **pos** property in an entity defition as
     * `{ text: 'manchester united', entityType: 'club', pos: 'NNP' }`.
     * The [wink-pos-tagger](https://www.npmjs.com/package/wink-pos-tagger) will
     * automatically use the `pos` property (if available) to ensure correct
     * tagging in your context by overriding its algorithm.
     *
     * @param {string} entities[].text that must be detected as entity and may
     * consist of more than one word; for example, **`India`** or **`United Kindom.`**
     * @param {string} entities[].entityType type of the entity; for example
     * **`country`**
     * @param {string} [entities[].uid=undefined] unique id for the entity; example
     * usecase of `uid` is using it to access more properties of the entity from a
     * database. If it is `undefined` then it is automatically generated by joining
     * the key words of the detected entity by underscore (_). For example, `'india'`
     * or `'united_kingdom'.`
     * @param {string} [entities[].value=undefined] that is assigned to the value
     * property of the token; if `undefined` then it is equal to the **value**
     * of the token in case of uni-word entities; for multi-word entities, it is
     * generated automatically by joining the key words of the entries by space
     * character. For example, `'india'` or `'united kingdom'.`
     * @return {number} of actual entities learned.
     * @example
     * var trainingData = [
     *   { text: 'manchester united', entityType: 'club', uid: 'manu' },
     *   { text: 'manchester', entityType: 'city' },
     *   { text: 'U K', entityType: 'country', uid: 'uk' }
     * ];
     * myNER.learn( trainingData );
     * // -> 3
    */
    var learn = function ( entities ) {
      // Refer to comments for variables `uniWordEntities` & `multiWordEntities`
      // declarations in the beginning.
      let length = 0;
      for ( let i = 0, imax = entities.length; i < imax; i += 1 ) {
        const entity = entities[ i ];
        // Normalize after removing extra white spaces; required for acronyms processing.
        const text = normalize( ( entity.text || '' ).trim().replace( /\s+/, ' ' ) );
        // Add if `text` and `entityType` are defined.
        if ( text && entity.entityType ) {
          const words = text.split( /\s+/ );
          length += 1;
          if ( words.length === 1 ) {
            addUniWordEntity( words, entity );
          } else {
            addMultiWordEntity( text, words, entity );
          }
        }
      }
  
      // Ensure longest sequence is highlighted first.
      for ( var key in uniWordEntities ) {
        if ( uniWordEntities[ key ].wordCounts ) uniWordEntities[ key ].wordCounts.sort( helpers.array.descending );
      }
  
      return length;
    }; // learn()
  
    // ### isIgnorable
    /**
     *
     * Tests if the input `value` and `tag` can be ignored.
     *
     * @method NER#isIgnorable
     * @param {string} value — to be tested.
     * @param {string} tag — to be tested.
     * @return {boolean} returns `true` if it can be ignored otherwise returns `false.`
     * @private
    */
    var isIgnorable = function ( value, tag ) {
      return (
        ( cfg.tagsToIgnore && cfg.tagsToIgnore[ tag ] ) ||
        ( cfg.valuesToIgnore && cfg.valuesToIgnore[ value ] )
      );
    }; // isIgnorable()
  
    // ### getNextNTokens
    // Tries next `n` `tokens` for a possible entity.
    var getNextNTokens = function ( i, n, tokens ) {
      // Words that are joined to form candidate entity.
      var words = [];
      // Original sequence is captured here.
      var originalSeq = [];
      // Candidate entity
      var entity;
      // The merged token for multi-word entities.
      var mergedToken;
      // Helper variables.
      var tag, value;
      var k, kmax;
  
      for ( k = i, kmax = tokens.length; k < kmax && words.length < n; k += 1 ) {
        value = tokens[ k ].value;
        tag = tokens[ k ].tag;
        // If ignorable then simply follow the next iteration, after saving the
        // current value into the original sequence of tokens; otherwise if
        // token is a word or number save it in words else just reset & break!
        if ( isIgnorable( value, tag ) ) {
          originalSeq.push( value );
        } else if ( tag === 'word' || tag === 'number' ) {
                 // `word` and `number` tags form entities, include them in `words`,
                 // while ensuring `words` receive normalized value.
                 words.push( normalize( value ) );
                 // Here `value` must go **as is**.
                 originalSeq.push( value );
               } else {
                 // Can not be ignored: reset & break!
                 words = [];
                 originalSeq = [];
                 break;
               }
      } // for ( k = i... )
  
      if ( words.length > 0 ) {
        entity = multiWordEntities[ words.join( ' ' ) ];
        if ( !entity ) {
          words.push( singularize( words.pop() ) );
  
          entity = multiWordEntities[ words.join( ' ' ) ];
        }
  
        if ( entity ) {
          // Copy the entity into the merged token.
          mergedToken = copyKVPs( Object.create( null ), entity );
          // Save original sequence of tokens that were detected as an entity.
          mergedToken.originalSeq = originalSeq;
          // If id is missing, create id by joining `words`.
          mergedToken.uid = entity.uid || words.join( '_' );
          // If value was not defined, default to words.
          mergedToken.value = mergedToken.value || words.join( ' ' );
          // move the next token's index — `nti` to `k`.
          return { token: mergedToken, nti: k };
        }
      }
  
      // No success, do not move the next token's index — `nti`.
      return { nti: i };
    }; // getNextNTokens()
  
    // ### lookup
    /**
     * Exports the JSON of the learnings generated by `learn()`, which may be
     * saved in a file that may be used later for NER purpose.
     *
     * @method NER#lookup
     * @private
     * @param {number} i — index of tokens, from where the lookup is performed.
     * @param {object[]} tokens — input tokens to NER.
     * @param {object} candidate — result of lookup in `uniWordEntities`.
     * @param {array} nerdts — NERed tokens.
     * @return {number} of the learnings.
     * @example
     * var learnings = myNER.exportJSON();
    */
    var lookup = function ( i, tokens, candidate, nerdts ) {
      var result;
      for ( var k = 0, kmax = candidate.wordCounts.length; k < kmax; k += 1 ) {
        result = getNextNTokens( i, candidate.wordCounts[ k ], tokens );
        if ( result.token ) {
          result.token.tag = 'word';
          nerdts.push( result.token );
          break;
        }
      }
      return result.nti;
    };
  
    // ### recognize
    /**
     *
     * Recognizes entities in the input `tokens.` Any token(s), which is recognized
     * as an entity, will automatically receive the properties that have been defined
     * for the *detected entity* using [`learn()`](#learn). If a set of tokens together
     * are recognized as a single entity, then they are merged in to a single
     * token; the merged tokens `value` property becomes the concatenation of all
     * the `values` from merged tokens, separated by space.
     *
     * @method NER#recognize
     * @param {object[]} tokens — tokenized either using
     * [wink-tokenizer](https://www.npmjs.com/package/wink-tokenizer) or follow
     * it's standards.
     *
     * @return {object[]} of updated `tokens` with entities tagged.
     * @example
     * // Use wink tokenizer.
     * var winkTokenizer = require( 'wink-tokenizer' );
     * // Instantiate it and use tokenize() api.
     * var tokenize = winkTokenizer().tokenize;
     * var tokens = tokenize( 'Manchester United is a professional football club based in Manchester, U. K.' )
     * // Detect entities.
     * myNER.recognize( tokens );
     * // -> [
     * //      { entityType: 'club', uid: 'manu', originalSeq: [ 'Manchester', 'United' ], value: 'manchester united', tag: 'word' },
     * //      { value: 'is', tag: 'word' },
     * //      { value: 'a', tag: 'word' },
     * //      { value: 'professional', tag: 'word' },
     * //      { value: 'football', tag: 'word' },
     * //      { value: 'club', tag: 'word' },
     * //      { value: 'based', tag: 'word' },
     * //      { value: 'in', tag: 'word' },
     * //      { value: 'Manchester', tag: 'word', originalSeq: [ 'Manchester' ], uid: 'manchester', entityType: 'city' },
     * //      { value: ',', tag: 'punctuation' },
     * //      { entityType: 'country', uid: 'uk', originalSeq: [ 'U', '.', 'K' ], value: 'u k', tag: 'word' },
     * //      { value: '.', tag: 'punctuation' }
     * //    ]
    */
    var recognize = function ( tokens ) {
      // var tokens = sentence; // tokenize( sentence.toLowerCase() );
      // NERed Tokens.
      var nerdts = [];
      var t;
      var inew;
      var action, candidate;
      for ( var i = 0, imax = tokens.length; i < imax; i += 1 ) {
        t = tokens[ i ];
        if ( t.tag === 'word' ) {
          // Look up for the word; if not found try its base form.
          let value = t.value;
          value = normalize( value );
          candidate = uniWordEntities[ value ];
          if ( !candidate ) {
            // This ensures that right `value` is used for `id`.
            value = singularize( value );
            candidate = uniWordEntities[ value ];
          }
          // NE lookup may return `undefined` meaning that the current token is
          // neither an entity or the first word of a multi-word entity (1).
          // Otherwise if `wordCounts` is `undefined` means that it is an uni-word
          // entity (2). Finally with given `wordCounts`, if the `entityType` is
          // `undefined` means that it will be a multi-word entity (3); otherwise
          // it is an uni/multi word entity (4) — try to match the longest one first!
          action = ( candidate === undefined ) ?
                      1 : ( candidate.wordCounts === undefined ) ?
                        2 : ( candidate.entityType === undefined ) ?
                          3 : 4;
  
          switch ( action ) { // eslint-disable-line default-case
            case 1:
              // Non-entity, just push the token to `nerdts`.
              nerdts.push( t );
              break;
            case 2:
              // The value pointed to an uni-word named entity, push it to `nerdts`
              t.originalSeq = [ t.value ];
              t.uid = value;
              // If value was not defined, default to `value`.
              candidate.value = candidate.value || value;
              nerdts.push( copyKVPs( t, candidate ) );
              break;
            case 3:
              // Lookup multi-word entity(s) as per the `candiadte.wordCounts`.
              inew = lookup( i, tokens, candidate, nerdts );
              if ( i === inew ) nerdts.push( t );
              else i = inew - 1;
              break;
            case 4:
              // Lookup for uni/multi-word entity, starting with longest one first.
              inew = lookup( i, tokens, candidate, nerdts );
              if ( i === inew ) {
                t.originalSeq = [ t.value ];
                t.uid = value;
                nerdts.push( copyKVPs( t, candidate ) );
              } else i = inew - 1;
              break;
          }
        } else {
          nerdts.push( t );
        }
      }
      return ( nerdts );
    }; // recognize()
  
    // ### initialize
    /**
     * Initalizes the config and learning related variables.
     *
     * @method NER#initialize
     * @private
     * @return {undefined} of the learnings.
    */
    var initialize = function ( ) {
      // Define default configuration.
      // Ignore `punctuation`;
      cfg.tagsToIgnore = Object.create( null );
      cfg.tagsToIgnore.punctuation = true;
      // Leave values to ignore `undefined`;
      cfg.valuesToIgnore = Object.create( null );
      // And ignore diacritics by default.
      cfg.ignoreDiacritics = true;
      // Initialize learnings
      uniWordEntities = Object.create( null );
      multiWordEntities = Object.create( null );
    }; // initialize()
  
    // ### reset
    /**
     *
     * Resets the named entity recognizer by re-initializing all the learnings and
     * by setting the configuration to default.
     *
     * @method NER#reset
     * @return {boolean} always true.
     * @example
     * myNER.reset( );
     * // -> true
    */
    var reset = function ( ) {
      initialize();
      return true;
    }; // reset()
  
    // ### exportJSON
    /**
     *
     * Exports the JSON of the learnings generated by `learn()`, which may be
     * saved in a file that may be used later for NER purpose.
     *
     * @method NER#exportJSON
     * @return {json} of the learnings.
     * @example
     * var learnings = myNER.exportJSON();
    */
    var exportJSON = function ( ) {
      return ( JSON.stringify( [
        cfg,
        uniWordEntities,
        multiWordEntities,
        // For future expansion but the import will have to have intelligence to
        // set the default values and still ensure nothing breaks! Hopefully!!
        {},
        [],
        []
      ] ) );
    }; // exportJSON()
  
    // ### importJSON
    /**
     *
     * Imports the ner learnings from an already exported ner learnings via the
     * [`exportJSON()`](#exportjson).
     *
     * @method NER#importJSON
     * @param {json} json — containg an earlier exported learnings in JSON format.
     * @return {boolean} always `true`.
     * @throws {error} if invalid JSON is encountered.
     * @example
     * var myNER = ner();
     * // Assuming that `json` has valid learnings.
     * myNER.importJSON( json );
    */
    var importJSON = function ( json ) {
      if ( !json ) {
        throw Error( 'wink-ner: undefined or null JSON encountered, import failed!' );
      }
      // Validate json format
      var isOK = [
        helpers.object.isObject,
        helpers.object.isObject,
        helpers.object.isObject,
        helpers.object.isObject,
        helpers.array.isArray,
        helpers.array.isArray
      ];
      var parsedJSON;
      try {
        parsedJSON = JSON.parse( json );
      } catch ( ex ) {
        throw Error( 'wink-ner: invalid JSON structure encountered, can not import.' );
      }
  
      if ( !helpers.array.isArray( parsedJSON ) || parsedJSON.length !== isOK.length ) {
        throw Error( 'wink-ner: invalid JSON format encountered, can not import.' );
      }
  
      for ( var i = 0; i < isOK.length; i += 1 ) {
        if ( !isOK[ i ]( parsedJSON[ i ] ) ) {
          throw Error( 'wink-ner: invalid JSON element encountered, can not import.' );
        }
      }
      // All good, setup variable values.
      // First reset everything.
      reset();
      // Load variable values.
      cfg = parsedJSON[ 0 ];
      uniWordEntities = parsedJSON[ 1 ];
      multiWordEntities = parsedJSON[ 2 ];
      // Return success.
      return true;
    }; // importJSON()
  
    // Main Code starts here!
    initialize();
  
    methods.defineConfig = defineConfig;
    methods.learn = learn;
    methods.predict = recognize;
    methods.recognize = recognize;
    methods.exportJSON = exportJSON;
    methods.importJSON = importJSON;
    methods.reset = reset;
  
    return methods;
  };
  
  module.exports = ner;
  