Source: utils/filters.js

/**
 * This file is part of SesaJsTools.
 *   Copyright 2014-2015, Association Sésamath
 *
 * SesaJsTools is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation.
 *
 * SesaJsTools is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with SesaJsTools (LICENCE.txt).
 * @see http://www.gnu.org/licenses/agpl.txt
 *
 *
 * Ce fichier fait partie de l'application Sésathèque, créée par l'association Sésamath.
 *
 * Sésathèque est un logiciel libre ; vous pouvez le redistribuer ou le modifier suivant
 * les termes de la GNU Affero General Public License version 3 telle que publiée par la
 * Free Software Foundation.
 * Sésathèque est distribué dans l'espoir qu'il sera utile, mais SANS AUCUNE GARANTIE,
 * sans même la garantie tacite de QUALITÉ MARCHANDE ou d'ADÉQUATION à UN BUT PARTICULIER.
 * Consultez la GNU Affero General Public License pour plus de détails.
 * Vous devez avoir reçu une copie de la GNU General Public License en même temps que Sésathèque
 * (cf LICENCE.txt et http://vvlibri.org/fr/Analyse/gnu-affero-general-public-license-v3-analyse
 * pour une explication en français)
 */

'use strict'

var tools = require('../index')
var hasProp = tools.hasProp

/**
 * Retourne le tableau passé en argument ou un tableau vide si l'argument n'était pas un Array
 * @param {Array} arg L'array à controler
 * @returns {Array}
 */
function filterArray (arg) {
  return (Array.isArray(arg)) ? arg : []
}

/**
 * Retourne le tableau passé en argument ou un tableau vide si l'argument n'était pas un Array
 * Tous les éléments qui ne sont pas des entiers positifs (0 accepté) seront éliminés
 * @param {Array} arg L'array à controler
 * @param {boolean} [strict=true]
 * @returns {Array}
 */
function filterArrayInt (arg, strict) {
  if (typeof strict !== 'boolean') strict = true
  arg = filterArray(arg)
  if (strict) {
    return arg.filter(function (elt) {
      return typeof elt === 'number' && Math.round(elt) === elt && elt > -1
    })
  } else {
    var retour = []
    arg.forEach(function (elt) {
      switch (typeof elt) {
        case 'string':
          elt = Number(elt)
          // pas de break exprès pour passer aussi dans le suivant
        case 'number': // eslint-disable-line no-fallthrough
          var i = Math.round(elt)
          if (i > -1) retour.push(i)
        // pas besoin de default, pour le reste on fait rien
      }
    })
    return retour
  }
}

/**
 * Retourne le tableau passé en argument ou un tableau vide si l'argument n'était pas un Array
 * Tous les éléments qui ne sont pas des strings seront éliminés
 * @param {Array} arg L'array à controler
 * @param {boolean} strict passer false pour ajouter un toString sur tous les éléments qui ont la méthode
 * @returns {Array}
 */
function filterArrayString (arg, strict) {
  if (typeof strict === 'undefined') strict = true
  arg = filterArray(arg)
  if (strict) {
    return arg.filter(function (elt) {
      return (typeof elt === 'string')
    })
  } else {
    var retour = []
    arg.forEach(function (elt) {
      if (typeof elt === 'string') {
        retour.push(elt)
      } else {
        // on ajoute si ça renvoie autre chose que du falsy après cast
        var val = filterString(elt)
        if (val !== '') retour.push(val)
      }
    })
    return retour
  }
}

/**
 * Retourne false pour 'false' et '0' et un cast en Boolean sinon
 * @param {*} arg
 * @returns {boolean}
 */
function filterBoolean (arg) {
  if (arg === 'false' || arg === '0') return false
  return Boolean(arg)
}

/**
 * Retourne l'entier positif fourni ou 0
 * @param {number|string} arg
 * @returns {number}
 */
function filterInt (arg) {
  var int = 0
  var argNb = Number(arg)
  if (typeof arg === 'string') int = Math.floor(argNb)
  else if (typeof arg === 'number') int = Math.floor(arg)
  if (int < 0 || int !== argNb) int = 0
  return int
}

/**
 * Retourne un objet Date (on tente un cast si on nous fourni une string ou un entier) ou undefined
 * @param arg
 * @returns {Date|undefined}
 */
function filterDate (arg) {
  if (!arg) return
  if (arg instanceof Date) return arg
  if (typeof arg === 'number') return new Date(arg)
  if (typeof arg === 'string') {
    // presque un nombre… On considère ça un timestamp
    if (tools.isInt(Number(arg))) return new Date(arg)
    // date au format YYYY-MM-DD, ça match le format json YYYY-MM-DDThh:mm:ss.sssZ
    if (/^\d{4}-\d{2}-\d{2}/.test(arg)) return new Date(arg)
    // sinon ça se complique, car avec des slashes c'est interprété comme MM/JJ/YYYY :
    // new Date("02/05/2017") => 2017-02-05
    // on veut imposer JJ/MM, donc on réécrit avec des tirets pour éviter d'embarquer moment
    var chunks = /^(\d{2})\/(\d{2})\/(\d{4})(.*)?/.exec(arg)
    if (chunks && chunks.length > 3) return new Date(chunks[3] + '-' + chunks[2] + '-' + chunks[1] + (chunks[4] ? chunks[4] : ''))
  }
}

/**
 * Retourne la chaine passée en argument, un cast en string si ça existe pour le type ou une chaine vide sinon
 * @param arg
 * @returns {string}
 */
function filterString (arg) {
  if (arg === null || arg === undefined) return ''
  if (typeof arg === 'string') return arg
  // Object a une méthode toString, on peut donc pas tester son existence,
  // on cast en string, si ça donne du [object Truc] on renvoie une chaine vide,
  // sinon le résultat du cast
  var retour = String(arg)
  if (/^\[object /.test(retour)) return ''
  return retour
}

/**
 * Retourne un object éventuellement filtré, vide si autre chose qu'un plain object est fourni
 * (Date, Array, etc, sont transformés en objets vides, mais ces types sont préservés dans le contenu de l'objet passé)
 * L'objet retourné est cloné, ses objets enfants aussi sauf ceux qui ne sont pas des objets littéraux
 * pour lesquels ça reste une référence (pour des valeurs de type Date, Array & co)
 * @param {*}               obj
 * @param {RegExp|function} [excludeFilter] si regexp elle sera testée sur les propriétés pour les filtrer,
 *                                          si function elle sera appelée avec (propriété, valeur) et devra renvoyer true pour exclure
 * @returns {object} Un nouvel objet issu de obj filtré (ou obj si on a pas fourni de excludeFilter)
 */
function filterObject (obj, excludeFilter) {
  // notre fct récursive pour filtrer (uniquement des objets "plain")
  function filterObject (obj) {
    var objCleaned = {}
    var value
    for (var prop in obj) {
      if (hasProp(obj, prop)) {
        value = obj[prop]
        if (hasProp(obj, prop) && !excludeFilter(prop, value)) {
          if (tools.isObjectPlain(value)) {
            objCleaned[prop] = filterObject(value)
          } else if (Array.isArray(value) && value.length) {
            objCleaned[prop] = filterArray(value)
          } else {
            objCleaned[prop] = value
          }
        }
      }
    }

    return objCleaned
  }
  // idem sur les tableaux
  function filterArray (arr) {
    return arr.map(function (elt) {
      if (tools.isObjectPlain(elt)) return filterObject(elt)
      if (Array.isArray(elt)) return filterArray(elt)
      return elt
    })
  }

  var retour = {}
  if (tools.isObjectPlain(obj)) {
    // on transforme une regex en fct filterCallback
    if (excludeFilter instanceof RegExp) {
      var re = excludeFilter
      excludeFilter = function (prop) {
        return re.test(prop)
      }
    }
    // et on applique le filtre si y'en a un
    if (typeof excludeFilter === 'function') {
      retour = filterObject(obj)
    } else {
      retour = obj
    }
  }

  return retour
}

/**
 * Collection de fonctions permettant de filtrer une variable d'après le type attendu
 * @service sesajstools/utils/filters
 */
module.exports = {
  array: filterArray,
  arrayInt: filterArrayInt,
  arrayString: filterArrayString,
  boolean: filterBoolean,
  date: filterDate,
  int: filterInt,
  object: filterObject,
  string: filterString
}