<?php
/*
  CifrasEnLetras.php — ProInf.net — nov-2009, ago-2011

  CifrasEnLetras sirve para expresar una serie de cifras en letras.
  A modo de ejemplo convierte "22"> en "veintidós".
  Puede convertir un número entre una y ciento veintiséis cifras como máximo.

  Ejemplos de uso:
    CifrasEnLetras::convertirEurosEnLetras(22.34) --> "veintidós euros con treinta y cuatro céntimos"
    CifrasEnLetras::convertirNumeroEnLetras("35,67") --> "treinta y cinco con sesenta y siete"

  Enlaces de referencia:
    http://es.wikipedia.org/wiki/Anexo:Lista_de_n%C3%BAmeros
    http://es.encarta.msn.com/encyclopedia_761577100/Signos_matem%C3%A1ticos.html
    http://es.wikipedia.org/wiki/Nombres_de_los_n%C3%BAmeros_en_espa%C3%B1ol

  Licencia:
    http://creativecommons.org/licenses/GPL/2.0/deed.es
    Este software está sujeto a la CC-GNU GPL</a>
*/

//===============================================

class CifrasEnLetras {

  //---------------------------------------------
  // CONSTANTES
  const PREFIJO_ERROR = 'Error: ';
  const COMA = ',';
  const MENOS = '-';

  //---------------------------------------------
  // ENUMERACIONES
  const NEUTRO = 'neutro';
  const MASCULINO = 'masculino';
  const FEMENINO = 'femenino';

  //---------------------------------------------
  // GLOBAL
  public static $SEPARADOR_SEIS_CIFRAS = " ";

  //---------------------------------------------
  // LISTAS

  public static $listaUnidades = array( // Letras de los números entre el 0 y el 29
    "cero", "un", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve",
    "diez", "once", "doce", "trece", "catorce", "quince", "dieciséis", "diecisiete", "dieciocho", "diecinueve",
    "veinte", "veintiún", "veintidós", "veintitrés", "veinticuatro", "veinticinco", "veintiséis", "veintisiete", "veintiocho", "veintinueve"
  );
  public static $listaDecenas = array( // Letras de las decenas
    "", "diez", "veinte", "treinta", "cuarenta", "cincuenta", "sesenta", "setenta", "ochenta", "noventa"
  );
  public static $listaCentenas = array ( // Letras de las centenas
    "", "cien", "doscientos", "trescientos", "cuatrocientos", "quinientos", "seiscientos", "setecientos", "ochocientos", "novecientos"
  );
  public static $listaOrdenesMillonSingular = array ( // Letras en singular de los órdenes de millón
    "", "millón", "billón", "trillón", "cuatrillón", "quintillón",
    "sextillón", "septillón", "octillón", "nonillón", "decillón",
    "undecillón", "duodecillón", "tridecillón", "cuatridecillón", "quidecillón",
    "sexdecillón", "septidecillón", "octodecillón", "nonidecillón", "vigillón"
  );
  public static $listaOrdenesMillonPlural = array ( // Letras en plural de los órdenes de millón
    "", "millones", "billones", "trillones", "cuatrillones", "quintillones",
    "sextillones", "septillones", "octillones", "nonillones", "decillones",
    "undecillones", "duodecillones", "tridecillones", "cuatridecillones", "quidecillones",
    "sexdecillones", "septidecillones", "octodecillones", "nonidecillones", "vigillones"
  );

  // ORDINALES (Wikipedia: "Nombres de los números en español", "Número ordinal")

  public static $listaUnidadesOrdinales = array ( // Letras de los ordinales entre 0º y 19º
    "ningún", "primer", "segundo", "tercer", "cuarto", "quinto", "sexto", "séptimo", "octavo", "noveno",
    "décimo", "undécimo", "duodécimo", "decimotercer", "decimocuarto", "decimoquinto", "decimosexto", "decimoséptimo", "decimoctavo", "decimonoveno"
  );
  public static $listaDecenasOrdinales = array ( // Letras de las decenas ordinales
    "", "décimo", "vigésimo", "trigésimo", "cuadragésimo", "quincuagésimo", "sexagésimo", "septuagésimo", "octogésimo", "nonagésimo"
  );
  public static $listaCentenasOrdinales = array ( // Letras de las centenas ordinales
    "", "centésimo", "ducentésimo", "tricentésimo", "cuadringentésimo", "quingentésimo", "sexcentésimo", "septingentésimo", "octingentésimo", "noningentésimo"
  );
  public static $listaPotenciasDiezOrdinales = array ( // Letras de las potencias de diez ordinales
    "", "décimo", "centésimo", "milésimo", "diezmilésimo", "cienmilésimo", "millonésimo"
  );

  //---------------------------------------------
  // MÉTODOS PRINCIPALES

  /*
    Convierte a letras los números entre 0 y 29
  */
  protected static function convertirUnidades($unidades, $genero='neutro') {
    if ($unidades == 1) {
      if ($genero == 'masculino') return 'uno';
      elseif ($genero == 'femenino') return 'una';
    }
    else if ($unidades == 21) {
      if ($genero == 'masculino') return 'veintiuno';
      elseif ($genero == 'femenino') return 'veintiuna';
    }
    return self::$listaUnidades[$unidades];
  }

  /*
    Convierte a letras las centenas
  */
  protected static function convertirCentenas($centenas, $genero='neutro') {
    $resultado = self::$listaCentenas[$centenas];
    if ($genero == 'femenino') $resultado = str_replace('iento','ienta', $resultado);
    return $resultado;
  }

  /*
    Primer centenar: del cero al noventa y nueve
  */
  public static function convertirDosCifras($cifras, $genero='neutro') {
    $unidad = $cifras % 10;
    $decena = intval($cifras / 10);
    if ($cifras < 30) return self::convertirUnidades($cifras, $genero);
    elseif ($unidad == 0) return self::$listaDecenas[$decena];
    else return self::$listaDecenas[$decena].' y '.self::convertirUnidades($unidad, $genero);
  }

  /*
    Primer millar: del cero al novecientos noventa y nueve
  */
  public static function convertirTresCifras($cifras, $genero='neutro') {
    $decenas_y_unidades = $cifras % 100;
    $centenas = intval($cifras / 100);
    if ($cifras < 100) return self::convertirDosCifras($cifras, $genero);
    elseif ($decenas_y_unidades == 0) return self::convertirCentenas($centenas, $genero);
    elseif ($centenas == 1) return 'ciento '.self::convertirDosCifras($decenas_y_unidades, $genero);
    else return self::convertirCentenas($centenas, $genero).' '.self::convertirDosCifras($decenas_y_unidades, $genero);
  }

  /*
    Primer millón: del cero al novecientos noventa y nueve mil noventa y nueve
  */
  public static function convertirSeisCifras($cifras, $genero='neutro') {
    $primer_millar = $cifras % 1000;
    $grupo_miles = intval($cifras / 1000);
    $genero_miles = $genero=='masculino'? 'neutro': $genero;
    if ($grupo_miles == 0) return self::convertirTresCifras($primer_millar, $genero);
    elseif ($grupo_miles == 1) {
      if ($primer_millar == 0) return 'mil';
      else return 'mil '.self::convertirTresCifras($primer_millar, $genero);
    }
    elseif ($primer_millar == 0) return self::convertirTresCifras($grupo_miles, $genero_miles).' mil';
    else return self::convertirTresCifras($grupo_miles, $genero_miles).' mil '.self::convertirTresCifras($primer_millar, $genero);
  }

  /*
    Números enteros entre el cero y novecientos noventa y nueve mil novecientos noventa y nueve vigillones... etc, etc.
    Es decir entre el 0 y el (10^126)-1 o bien números entre 1 y 126 cifras.
    Las cifras por debajo del millón pueden ir en masculino o en femenino.
  */
  public static function convertirCifrasEnLetras($cifras, $genero='neutro', $separadorGruposSeisCifras=null) {

    // Inicialización
    $cifras = trim($cifras);
    $numeroCifras = strlen($cifras);
    if ($separadorGruposSeisCifras == null) $separadorGruposSeisCifras = self::$SEPARADOR_SEIS_CIFRAS;

    // Comprobación
    if ($numeroCifras == 0) return self::PREFIJO_ERROR.'No hay ningún número';
    for ($indiceCifra=0; $indiceCifra<$numeroCifras; $indiceCifra++) {
      $cifra = substr($cifras, $indiceCifra, 1);
      $esDecimal = strpos('0123456789', $cifra) !== false;
      if (!$esDecimal) return self::PREFIJO_ERROR.'Uno de los caracteres no es una cifra decimal';
    }
    if ($numeroCifras > 126) return self::PREFIJO_ERROR.'El número es demasiado grande ya que tiene más de 126 cifras';

    // Preparación
    $numeroGruposSeisCifras = intval($numeroCifras / 6) + (($numeroCifras % 6)? 1: 0);
    $cerosIzquierda = str_repeat('0', $numeroGruposSeisCifras*6 - $numeroCifras);
    $cifras = $cerosIzquierda.$cifras;
    $ordenMillon = $numeroGruposSeisCifras - 1;

    // Procesamiento
    $resultado = array();
    for ($indiceGrupo=0; $indiceGrupo<$numeroGruposSeisCifras*6; $indiceGrupo+=6) {
      $seisCifras = substr($cifras, $indiceGrupo, 6);

      if ($seisCifras != 0) {
        if (count($resultado) > 0) $resultado[] = $separadorGruposSeisCifras;

        if ($ordenMillon == 0) {
          $resultado[] = self::convertirSeisCifras($seisCifras, $genero);
        }
        elseif ($seisCifras == 1) {
          $resultado[] = 'un '.self::$listaOrdenesMillonSingular[$ordenMillon];
        }
        else {
          $resultado[] = self::convertirSeisCifras($seisCifras, 'neutro').' '.
                         self::$listaOrdenesMillonPlural[$ordenMillon];
        }
      }
      $ordenMillon--;
    }

    // Finalización
    if (count($resultado) == 0) $resultado[] = self::$listaUnidades[0];
    return implode('', $resultado);
  }

  public static function convertirCifrasEnLetrasMasculinas($cifras) {
    return self::convertirCifrasEnLetras($cifras, "masculino");
  }

  public static function convertirCifrasEnLetrasFemeninas($cifras) {
    return self::convertirCifrasEnLetras($cifras, "femenino");
  }

  /*
    Expresa un número con decimales y signo en letras acompañado del tipo de medida
    para la parte entera y la parte decimal.
    - Los caracters no numéricos son ignorados.
    - Los múltiplos de millón tienen la preposición 'de' antes de la palabra.
    - El género masculino o femenino sólo puede influir en las cifras inferiores al millón.
  */
  public static function convertirNumeroEnLetras(
    $cifras, $numeroDecimales=-1,
    $palabraEntera='', $palabraEnteraPlural='', $esFemeninaPalabraEntera=false,
    $palabraDecimal='', $palabraDecimalPlural='', $esFemeninaPalabraDecimal=false)
  {
    // Argumentos
    $cifras = is_float($cifras)? str_replace(".", self::COMA, $cifras): "$cifras";
    $palabraEnteraPlural = ($palabraEnteraPlural=='')? $palabraEntera.'s': $palabraEnteraPlural;
    $palabraDecimalPlural = ($palabraDecimalPlural=='')? $palabraDecimal.'s': $palabraDecimalPlural;

    // Limpieza
    $cifras = self::dejarSoloCaracteresDeseados($cifras, '0123456789' . self::COMA . self::MENOS);

    // Comprobaciones
    $repeticionesMenos = substr_count($cifras, self::MENOS);
    $repeticionesComa = substr_count($cifras, self::COMA);
    if ($repeticionesMenos > 1 || ($repeticionesMenos == 1 && !self::empiezaPor($cifras, self::MENOS)) ) {
      return self::PREFIJO_ERROR . 'Símbolo negativo incorrecto o demasiados símbolos negativos';
    }
    else if ($repeticionesComa > 1) {
      return self::PREFIJO_ERROR . 'Demasiadas comas decimales';
    }

    // Negatividad
    $esNegativo = self::empiezaPor($cifras, self::MENOS);
    if ($esNegativo) $cifras = substr($cifras, 1);

    // Preparación
    $posicionComa = strpos($cifras, self::COMA);
    if ($posicionComa === false) $posicionComa = strlen($cifras);

    $cifrasEntera = substr($cifras, 0, $posicionComa);
    if ($cifrasEntera == "" || $cifrasEntera == self::MENOS) $cifrasEntera = "0";
    $cifrasDecimal = substr($cifras, min($posicionComa + 1, strlen($cifras)));

    $esAutomaticoNumeroDecimales = $numeroDecimales < 0;
    if ($esAutomaticoNumeroDecimales) {
      $numeroDecimales = strlen($cifrasDecimal);
    }
    else {
      $cifrasDecimal = substr($cifrasDecimal, 0, min($numeroDecimales, strlen($cifrasDecimal)));
      $cerosDerecha = str_repeat('0', $numeroDecimales - strlen($cifrasDecimal));
      $cifrasDecimal = $cifrasDecimal . $cerosDerecha;
    }

    // Cero
    $esCero = self::dejarSoloCaracteresDeseados($cifrasEntera,"123456789") == "" &&
      self::dejarSoloCaracteresDeseados($cifrasDecimal,"123456789") == "";

    // Procesar
    $resultado = array();

    if ($esNegativo && !$esCero) $resultado[]= "menos ";

    $parteEntera = self::procesarEnLetras($cifrasEntera,
      $palabraEntera, $palabraEnteraPlural, $esFemeninaPalabraEntera);
    if (self::empiezaPor($parteEntera, self::PREFIJO_ERROR)) return $parteEntera;
    $resultado[]= $parteEntera;

    if ($cifrasDecimal != "") {
      $parteDecimal = self::procesarEnLetras($cifrasDecimal,
        $palabraDecimal, $palabraDecimalPlural, $esFemeninaPalabraDecimal);
      if (self::empiezaPor($parteDecimal, self::PREFIJO_ERROR)) return $parteDecimal;
      $resultado[]= " con ";
      $resultado[]= $parteDecimal;
    }

    return implode('', $resultado);
  }

  /*
    Convertir euros en letras

    Ejemplos:
      CifrasEnLetras::convertirEurosEnLetras("44276598801,2",2) --> "cuatrocientos noventa y ocho mil un euros con veinte céntimos"
      CifrasEnLetras::convertirEurosEnLetras(85009) --> "ochenta y cinco mil nueve euros"
      CifrasEnLetras::convertirEurosEnLetras(10200.35) --> "diez mil doscientos euros con treinta y cinco céntimos"
  */
  public static function convertirEurosEnLetras($euros, $numeroDecimales=2) {
    return self::convertirNumeroEnLetras($euros, $numeroDecimales,
      "peso", "pesos", false, "centavo", "centavos", false);
  }

  /*
    Separa las cifras en grupos de 6 con subrayados y los grupos de 6 en grupos de 2 con punto
    Ejemplos: CifrasEnLetras::formatearCifras("-4739249,2") --> "-4_739.249,2"
   */
  public static function formatearCifras($cifras, $formato="") {
    $cifras = self::dejarSoloCaracteresDeseados("$cifras", "0123456789" . self::COMA . self::MENOS);

    if (strlen($cifras) == 0) return $cifras;

    $esNegativo = self::empiezaPor($cifras, self::MENOS);
    if ($esNegativo) $cifras = substr($cifras, 1);

    $posicionComa = strpos($cifras, self::COMA);
    $esDecimal = $posicionComa !== false;

    if (!$esDecimal) $posicionComa = strlen($cifras);
    $cifrasEntera = substr($cifras, 0, $posicionComa);
    $cifrasDecimal = "";

    if ($esDecimal) $cifrasDecimal = substr($cifras, min($posicionComa + 1, strlen($cifras)));
    if ($cifrasEntera == "") $cifrasEntera = "0";

    $resultado = array();
    $numeroCifras = strlen($cifrasEntera);
    $par = true;
    $contador = 1;

    for ($indice=0; $indice<$numeroCifras; $indice+=3) {
      $indiceGrupo = $numeroCifras - $indice;
      $tresCifras = substr($cifras, max($indiceGrupo-3,0), min(3, $numeroCifras-$indice));

      if ($indice > 0) {
        switch ($formato) {
          case 'html':
            $resultado[]= $par? ".": "<sub>$contador</sub>";
            if (!$par) $contador++;
            break;
          default:
            $resultado[]= $par? '.': '_';
        }
        $par = !$par;
      }
      $resultado[]= $tresCifras;
    }

    if ($esNegativo) $resultado[]= self::MENOS;

    $resultado = array_reverse($resultado);

    if ($esDecimal) {
      $resultado[]= self::COMA;
      $resultado[]= $cifrasDecimal;
    }

    return implode('', $resultado);
  }


  //---------------------------------------------
  // MÉTODOS AUXILIARES

  /*
    Borra todos los caracteres del texto que no sea alguno de los caracteres deseados.
    Ejemplos:
      self::dejarSoloCaracteresDeseados("89.500.400","0123456789") --> "89500400"
      self::dejarSoloCaracteresDeseados("ABC-000-123-X-456","0123456789") --> "000123456"
  */
  private static function dejarSoloCaracteresDeseados($texto, $caracteresDeseados) {
    $resultado = array();
    for ($indice = 0; $indice < strlen($texto); $indice++) {
      $caracter = $texto[$indice];
      if (strpos($caracteresDeseados, $caracter) !== false) $resultado[]= $caracter;
    }
    return implode('', $resultado);
  }

  /*
    Función auxiliar de convertirNumeroEnLetras
    para procesar por separado la parte entera y la decimal
  */
  private static function procesarEnLetras($cifras, $palabraSingular, $palabraPlural, $esFemenina) {
    // Género
    $genero = "neutro";
    if ($esFemenina) $genero = "femenino";
    else if ($palabraSingular == "") $genero = "masculino";

    // Letras
    $letras = self::convertirCifrasEnLetras($cifras, $genero);
    if (self::empiezaPor($letras, self::PREFIJO_ERROR)) return $letras;

    // Propiedades
    $esCero = $letras == self::convertirUnidades(0, $genero) || $letras == "";
    $esUno = $letras == self::convertirUnidades(1, $genero);
    $esMultiploMillon = !$esCero && self::acabaPor($cifras, "000000");

    // Palabra
    $palabra = "";
    if ($palabraSingular != "") {
      if ($esUno || $palabraPlural == "")
        $palabra = $palabraSingular;
      else
        $palabra = $palabraPlural;
    }

    // Resultado
    $resultado = array();
    $resultado[]= $letras;
    if ($palabra != "") {
      $resultado[]= $esMultiploMillon? " de ": " ";
      $resultado[]= $palabra;
    }
    return implode('', $resultado);
  }


  private static function empiezaPor($texto, $prefijo) {
    //return strstr($texto, $prefijo) !== false;
    return substr($texto, 0, strlen($prefijo)) === $prefijo;
  }

  private static function acabaPor($texto, $sufijo) {
    /*$largoTexto = strlen($texto);
    $largoSufijo = strlen($sufijo);
    if ($largoSufijo > $largoTexto) return false;
    return substr_compare($texto, $sufijo, -$largoSufijo) === 0;*/
    return substr($texto, -strlen($sufijo)) == $sufijo;
  }


} // class CifrasEnLetras

?>
